From d5b8d58a82a040a2facb5d226b64648aa66b2629 Mon Sep 17 00:00:00 2001
From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>
Date: Fri, 9 Oct 2026 09:47:24 +0900
Subject: bluebey-studio: /bluebey-studio/ をテスト公開用にBasic認証つきで復帰
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
準拠版アプリを public/bluebey-studio/ に戻し、.htaccess でBasic認証(ユーザー bluebey)を設定。/bluebey-studio/ のみ保護し他は素通し。本公開は市の確認後に認証を外す想定。
---
bluebey-studio/assets/backgrounds/CREDITS.json | 187 -
.../backgrounds/abandoned_factory_canteen_01.webp | Bin 50074 -> 0 bytes
bluebey-studio/assets/backgrounds/autoshop_01.webp | Bin 36042 -> 0 bytes
bluebey-studio/assets/backgrounds/autumn_park.webp | Bin 160340 -> 0 bytes
bluebey-studio/assets/backgrounds/ballroom.webp | Bin 70838 -> 0 bytes
.../assets/backgrounds/basement_boxing_ring.webp | Bin 38548 -> 0 bytes
bluebey-studio/assets/backgrounds/dokan.webp | Bin 33248 -> 0 bytes
.../assets/backgrounds/empty_warehouse_01.webp | Bin 49154 -> 0 bytes
.../assets/backgrounds/kloppenheim_06_puresky.webp | Bin 14722 -> 0 bytes
.../assets/backgrounds/minedump_flats.webp | Bin 88496 -> 0 bytes
bluebey-studio/assets/backgrounds/mutoujima.webp | Bin 13282 -> 0 bytes
.../assets/backgrounds/newman_cafeteria.webp | Bin 62478 -> 0 bytes
.../assets/backgrounds/spiaggia_di_mondello.webp | Bin 103002 -> 0 bytes
bluebey-studio/assets/backgrounds/uchu.webp | Bin 33900 -> 0 bytes
.../assets/backgrounds/urban_alley_01.webp | Bin 79528 -> 0 bytes
.../assets/backgrounds/wooden_lounge.webp | Bin 40662 -> 0 bytes
bluebey-studio/assets/beards/kaiser-right.png | Bin 1603 -> 0 bytes
bluebey-studio/assets/beards/scotch.png | Bin 1332 -> 0 bytes
bluebey-studio/assets/bluebey.glb | Bin 3109288 -> 0 bytes
bluebey-studio/assets/brows/gol-right.png | Bin 2537 -> 0 bytes
bluebey-studio/assets/fonts/METADATA.pb | 84 -
bluebey-studio/assets/fonts/OFL.txt | 93 -
bluebey-studio/assets/fonts/bluebey-caption.ttf | Bin 1224372 -> 0 bytes
bluebey-studio/assets/fonts/bluebey-caption.woff2 | Bin 430196 -> 0 bytes
bluebey-studio/index.html | 241 -
bluebey-studio/src/animation.js | 204 -
bluebey-studio/src/background.js | 666 -
bluebey-studio/src/clip.js | 202 -
bluebey-studio/src/exporter.js | 306 -
bluebey-studio/src/face.js | 375 -
bluebey-studio/src/faceArt.js | 1283 -
bluebey-studio/src/gacha.js | 481 -
bluebey-studio/src/gion.js | 226 -
bluebey-studio/src/glbExport.js | 148 -
bluebey-studio/src/handDrawn.js | 391 -
bluebey-studio/src/hats.js | 135 -
bluebey-studio/src/history.js | 105 -
bluebey-studio/src/look.js | 184 -
bluebey-studio/src/main.js | 2798 -
bluebey-studio/src/model.js | 283 -
bluebey-studio/src/outline.js | 368 -
bluebey-studio/src/panel.js | 2506 -
bluebey-studio/src/presets.js | 668 -
bluebey-studio/src/props.js | 646 -
bluebey-studio/src/rig.js | 212 -
bluebey-studio/src/style.css | 880 -
bluebey-studio/src/styles.js | 457 -
bluebey-studio/src/textOutlines.js | 432 -
bluebey-studio/src/trace.js | 410 -
bluebey-studio/src/ui.js | 367 -
bluebey-studio/src/urlState.js | 237 -
bluebey-studio/src/zip.js | 218 -
bluebey-studio/vendor/lib/opentype.module.js | 14460 -----
.../vendor/lib/opentype.module.js.LICENSE | 20 -
.../vendor/lib/opentype.module.js.VERSION | 1 -
bluebey-studio/vendor/three/LICENSE | 21 -
bluebey-studio/vendor/three/VERSION | 1 -
bluebey-studio/vendor/three/build/three.core.js | 60586 -------------------
bluebey-studio/vendor/three/build/three.module.js | 19719 ------
.../three/examples/jsm/controls/OrbitControls.js | 1972 -
.../examples/jsm/controls/TransformControls.js | 2003 -
.../examples/jsm/environments/RoomEnvironment.js | 185 -
.../three/examples/jsm/exporters/GLTFExporter.js | 3856 --
.../three/examples/jsm/loaders/GLTFLoader.js | 4925 --
.../examples/jsm/utils/BufferGeometryUtils.js | 1501 -
.../three/examples/jsm/utils/SkeletonUtils.js | 496 -
public/bluebey-studio/.htaccess | 10 +
public/bluebey-studio/.htpasswd | 1 +
.../bluebey-studio/assets/backgrounds/CREDITS.json | 187 +
.../backgrounds/abandoned_factory_canteen_01.webp | Bin 0 -> 50074 bytes
.../assets/backgrounds/autoshop_01.webp | Bin 0 -> 36042 bytes
.../assets/backgrounds/autumn_park.webp | Bin 0 -> 160340 bytes
.../assets/backgrounds/ballroom.webp | Bin 0 -> 70838 bytes
.../assets/backgrounds/basement_boxing_ring.webp | Bin 0 -> 38548 bytes
.../bluebey-studio/assets/backgrounds/dokan.webp | Bin 0 -> 33248 bytes
.../assets/backgrounds/empty_warehouse_01.webp | Bin 0 -> 49154 bytes
.../assets/backgrounds/kloppenheim_06_puresky.webp | Bin 0 -> 14722 bytes
.../assets/backgrounds/minedump_flats.webp | Bin 0 -> 88496 bytes
.../assets/backgrounds/mutoujima.webp | Bin 0 -> 13282 bytes
.../assets/backgrounds/newman_cafeteria.webp | Bin 0 -> 62478 bytes
.../assets/backgrounds/spiaggia_di_mondello.webp | Bin 0 -> 103002 bytes
public/bluebey-studio/assets/backgrounds/uchu.webp | Bin 0 -> 33900 bytes
.../assets/backgrounds/urban_alley_01.webp | Bin 0 -> 79528 bytes
.../assets/backgrounds/wooden_lounge.webp | Bin 0 -> 40662 bytes
.../bluebey-studio/assets/beards/kaiser-right.png | Bin 0 -> 1603 bytes
public/bluebey-studio/assets/beards/scotch.png | Bin 0 -> 1332 bytes
public/bluebey-studio/assets/bluebey.glb | Bin 0 -> 3109288 bytes
public/bluebey-studio/assets/brows/gol-right.png | Bin 0 -> 2537 bytes
public/bluebey-studio/assets/fonts/METADATA.pb | 84 +
public/bluebey-studio/assets/fonts/OFL.txt | 93 +
.../assets/fonts/bluebey-caption.ttf | Bin 0 -> 1224372 bytes
.../assets/fonts/bluebey-caption.woff2 | Bin 0 -> 430196 bytes
public/bluebey-studio/index.html | 241 +
public/bluebey-studio/src/animation.js | 204 +
public/bluebey-studio/src/background.js | 666 +
public/bluebey-studio/src/clip.js | 202 +
public/bluebey-studio/src/exporter.js | 306 +
public/bluebey-studio/src/face.js | 375 +
public/bluebey-studio/src/faceArt.js | 1283 +
public/bluebey-studio/src/gacha.js | 481 +
public/bluebey-studio/src/gion.js | 226 +
public/bluebey-studio/src/glbExport.js | 148 +
public/bluebey-studio/src/handDrawn.js | 391 +
public/bluebey-studio/src/hats.js | 135 +
public/bluebey-studio/src/history.js | 105 +
public/bluebey-studio/src/look.js | 184 +
public/bluebey-studio/src/main.js | 2798 +
public/bluebey-studio/src/model.js | 283 +
public/bluebey-studio/src/outline.js | 368 +
public/bluebey-studio/src/panel.js | 2506 +
public/bluebey-studio/src/presets.js | 668 +
public/bluebey-studio/src/props.js | 646 +
public/bluebey-studio/src/rig.js | 212 +
public/bluebey-studio/src/style.css | 880 +
public/bluebey-studio/src/styles.js | 457 +
public/bluebey-studio/src/textOutlines.js | 432 +
public/bluebey-studio/src/trace.js | 410 +
public/bluebey-studio/src/ui.js | 367 +
public/bluebey-studio/src/urlState.js | 237 +
public/bluebey-studio/src/zip.js | 218 +
.../bluebey-studio/vendor/lib/opentype.module.js | 14460 +++++
.../vendor/lib/opentype.module.js.LICENSE | 20 +
.../vendor/lib/opentype.module.js.VERSION | 1 +
public/bluebey-studio/vendor/three/LICENSE | 21 +
public/bluebey-studio/vendor/three/VERSION | 1 +
.../vendor/three/build/three.core.js | 60586 +++++++++++++++++++
.../vendor/three/build/three.module.js | 19719 ++++++
.../three/examples/jsm/controls/OrbitControls.js | 1972 +
.../examples/jsm/controls/TransformControls.js | 2003 +
.../examples/jsm/environments/RoomEnvironment.js | 185 +
.../three/examples/jsm/exporters/GLTFExporter.js | 3856 ++
.../three/examples/jsm/loaders/GLTFLoader.js | 4925 ++
.../examples/jsm/utils/BufferGeometryUtils.js | 1501 +
.../three/examples/jsm/utils/SkeletonUtils.js | 496 +
134 files changed, 125550 insertions(+), 125539 deletions(-)
delete mode 100644 bluebey-studio/assets/backgrounds/CREDITS.json
delete mode 100644 bluebey-studio/assets/backgrounds/abandoned_factory_canteen_01.webp
delete mode 100644 bluebey-studio/assets/backgrounds/autoshop_01.webp
delete mode 100644 bluebey-studio/assets/backgrounds/autumn_park.webp
delete mode 100644 bluebey-studio/assets/backgrounds/ballroom.webp
delete mode 100644 bluebey-studio/assets/backgrounds/basement_boxing_ring.webp
delete mode 100644 bluebey-studio/assets/backgrounds/dokan.webp
delete mode 100644 bluebey-studio/assets/backgrounds/empty_warehouse_01.webp
delete mode 100644 bluebey-studio/assets/backgrounds/kloppenheim_06_puresky.webp
delete mode 100644 bluebey-studio/assets/backgrounds/minedump_flats.webp
delete mode 100644 bluebey-studio/assets/backgrounds/mutoujima.webp
delete mode 100644 bluebey-studio/assets/backgrounds/newman_cafeteria.webp
delete mode 100644 bluebey-studio/assets/backgrounds/spiaggia_di_mondello.webp
delete mode 100644 bluebey-studio/assets/backgrounds/uchu.webp
delete mode 100644 bluebey-studio/assets/backgrounds/urban_alley_01.webp
delete mode 100644 bluebey-studio/assets/backgrounds/wooden_lounge.webp
delete mode 100644 bluebey-studio/assets/beards/kaiser-right.png
delete mode 100644 bluebey-studio/assets/beards/scotch.png
delete mode 100644 bluebey-studio/assets/bluebey.glb
delete mode 100644 bluebey-studio/assets/brows/gol-right.png
delete mode 100644 bluebey-studio/assets/fonts/METADATA.pb
delete mode 100644 bluebey-studio/assets/fonts/OFL.txt
delete mode 100644 bluebey-studio/assets/fonts/bluebey-caption.ttf
delete mode 100644 bluebey-studio/assets/fonts/bluebey-caption.woff2
delete mode 100644 bluebey-studio/index.html
delete mode 100644 bluebey-studio/src/animation.js
delete mode 100644 bluebey-studio/src/background.js
delete mode 100644 bluebey-studio/src/clip.js
delete mode 100644 bluebey-studio/src/exporter.js
delete mode 100644 bluebey-studio/src/face.js
delete mode 100644 bluebey-studio/src/faceArt.js
delete mode 100644 bluebey-studio/src/gacha.js
delete mode 100644 bluebey-studio/src/gion.js
delete mode 100644 bluebey-studio/src/glbExport.js
delete mode 100644 bluebey-studio/src/handDrawn.js
delete mode 100644 bluebey-studio/src/hats.js
delete mode 100644 bluebey-studio/src/history.js
delete mode 100644 bluebey-studio/src/look.js
delete mode 100644 bluebey-studio/src/main.js
delete mode 100644 bluebey-studio/src/model.js
delete mode 100644 bluebey-studio/src/outline.js
delete mode 100644 bluebey-studio/src/panel.js
delete mode 100644 bluebey-studio/src/presets.js
delete mode 100644 bluebey-studio/src/props.js
delete mode 100644 bluebey-studio/src/rig.js
delete mode 100644 bluebey-studio/src/style.css
delete mode 100644 bluebey-studio/src/styles.js
delete mode 100644 bluebey-studio/src/textOutlines.js
delete mode 100644 bluebey-studio/src/trace.js
delete mode 100644 bluebey-studio/src/ui.js
delete mode 100644 bluebey-studio/src/urlState.js
delete mode 100644 bluebey-studio/src/zip.js
delete mode 100644 bluebey-studio/vendor/lib/opentype.module.js
delete mode 100644 bluebey-studio/vendor/lib/opentype.module.js.LICENSE
delete mode 100644 bluebey-studio/vendor/lib/opentype.module.js.VERSION
delete mode 100644 bluebey-studio/vendor/three/LICENSE
delete mode 100644 bluebey-studio/vendor/three/VERSION
delete mode 100644 bluebey-studio/vendor/three/build/three.core.js
delete mode 100644 bluebey-studio/vendor/three/build/three.module.js
delete mode 100644 bluebey-studio/vendor/three/examples/jsm/controls/OrbitControls.js
delete mode 100644 bluebey-studio/vendor/three/examples/jsm/controls/TransformControls.js
delete mode 100644 bluebey-studio/vendor/three/examples/jsm/environments/RoomEnvironment.js
delete mode 100644 bluebey-studio/vendor/three/examples/jsm/exporters/GLTFExporter.js
delete mode 100644 bluebey-studio/vendor/three/examples/jsm/loaders/GLTFLoader.js
delete mode 100644 bluebey-studio/vendor/three/examples/jsm/utils/BufferGeometryUtils.js
delete mode 100644 bluebey-studio/vendor/three/examples/jsm/utils/SkeletonUtils.js
create mode 100644 public/bluebey-studio/.htaccess
create mode 100644 public/bluebey-studio/.htpasswd
create mode 100644 public/bluebey-studio/assets/backgrounds/CREDITS.json
create mode 100644 public/bluebey-studio/assets/backgrounds/abandoned_factory_canteen_01.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/autoshop_01.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/autumn_park.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/ballroom.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/basement_boxing_ring.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/dokan.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/empty_warehouse_01.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/kloppenheim_06_puresky.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/minedump_flats.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/mutoujima.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/newman_cafeteria.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/spiaggia_di_mondello.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/uchu.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/urban_alley_01.webp
create mode 100644 public/bluebey-studio/assets/backgrounds/wooden_lounge.webp
create mode 100644 public/bluebey-studio/assets/beards/kaiser-right.png
create mode 100644 public/bluebey-studio/assets/beards/scotch.png
create mode 100644 public/bluebey-studio/assets/bluebey.glb
create mode 100644 public/bluebey-studio/assets/brows/gol-right.png
create mode 100644 public/bluebey-studio/assets/fonts/METADATA.pb
create mode 100644 public/bluebey-studio/assets/fonts/OFL.txt
create mode 100644 public/bluebey-studio/assets/fonts/bluebey-caption.ttf
create mode 100644 public/bluebey-studio/assets/fonts/bluebey-caption.woff2
create mode 100644 public/bluebey-studio/index.html
create mode 100644 public/bluebey-studio/src/animation.js
create mode 100644 public/bluebey-studio/src/background.js
create mode 100644 public/bluebey-studio/src/clip.js
create mode 100644 public/bluebey-studio/src/exporter.js
create mode 100644 public/bluebey-studio/src/face.js
create mode 100644 public/bluebey-studio/src/faceArt.js
create mode 100644 public/bluebey-studio/src/gacha.js
create mode 100644 public/bluebey-studio/src/gion.js
create mode 100644 public/bluebey-studio/src/glbExport.js
create mode 100644 public/bluebey-studio/src/handDrawn.js
create mode 100644 public/bluebey-studio/src/hats.js
create mode 100644 public/bluebey-studio/src/history.js
create mode 100644 public/bluebey-studio/src/look.js
create mode 100644 public/bluebey-studio/src/main.js
create mode 100644 public/bluebey-studio/src/model.js
create mode 100644 public/bluebey-studio/src/outline.js
create mode 100644 public/bluebey-studio/src/panel.js
create mode 100644 public/bluebey-studio/src/presets.js
create mode 100644 public/bluebey-studio/src/props.js
create mode 100644 public/bluebey-studio/src/rig.js
create mode 100644 public/bluebey-studio/src/style.css
create mode 100644 public/bluebey-studio/src/styles.js
create mode 100644 public/bluebey-studio/src/textOutlines.js
create mode 100644 public/bluebey-studio/src/trace.js
create mode 100644 public/bluebey-studio/src/ui.js
create mode 100644 public/bluebey-studio/src/urlState.js
create mode 100644 public/bluebey-studio/src/zip.js
create mode 100644 public/bluebey-studio/vendor/lib/opentype.module.js
create mode 100644 public/bluebey-studio/vendor/lib/opentype.module.js.LICENSE
create mode 100644 public/bluebey-studio/vendor/lib/opentype.module.js.VERSION
create mode 100644 public/bluebey-studio/vendor/three/LICENSE
create mode 100644 public/bluebey-studio/vendor/three/VERSION
create mode 100644 public/bluebey-studio/vendor/three/build/three.core.js
create mode 100644 public/bluebey-studio/vendor/three/build/three.module.js
create mode 100644 public/bluebey-studio/vendor/three/examples/jsm/controls/OrbitControls.js
create mode 100644 public/bluebey-studio/vendor/three/examples/jsm/controls/TransformControls.js
create mode 100644 public/bluebey-studio/vendor/three/examples/jsm/environments/RoomEnvironment.js
create mode 100644 public/bluebey-studio/vendor/three/examples/jsm/exporters/GLTFExporter.js
create mode 100644 public/bluebey-studio/vendor/three/examples/jsm/loaders/GLTFLoader.js
create mode 100644 public/bluebey-studio/vendor/three/examples/jsm/utils/BufferGeometryUtils.js
create mode 100644 public/bluebey-studio/vendor/three/examples/jsm/utils/SkeletonUtils.js
diff --git a/bluebey-studio/assets/backgrounds/CREDITS.json b/bluebey-studio/assets/backgrounds/CREDITS.json
deleted file mode 100644
index d4114c7..0000000
--- a/bluebey-studio/assets/backgrounds/CREDITS.json
+++ /dev/null
@@ -1,187 +0,0 @@
-{
- "note": "背景素材は Poly Haven (CC0-1.0)。tools/fetch-backgrounds.py で取得。ただし mutoujima.webp は作者(安竹洋平)によるオリジナルの描き起こし。dokan.webp と uchu.webp も作者のオリジナル(blender-scenes でレンダリング)。",
- "items": [
- {
- "file": "empty_warehouse_01.webp",
- "label": "倉庫",
- "yaw": 0,
- "name": "Empty Warehouse 01",
- "authors": {
- "Sergej Majboroda": "All"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/empty_warehouse_01"
- },
- {
- "file": "ballroom.webp",
- "label": "広間",
- "yaw": 0,
- "name": "Ballroom",
- "authors": {
- "Sergej Majboroda": "All"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/ballroom"
- },
- {
- "file": "kloppenheim_06_puresky.webp",
- "label": "空と雲",
- "yaw": 0,
- "name": "Kloppenheim 06 (Pure Sky)",
- "authors": {
- "Greg Zaal": "Original",
- "Jarod Guest": "Sky edits"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/kloppenheim_06_puresky"
- },
- {
- "file": "autumn_park.webp",
- "label": "秋の公園",
- "yaw": 0,
- "name": "Autumn Park",
- "authors": {
- "Sergej Majboroda": "All"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/autumn_park"
- },
- {
- "file": "newman_cafeteria.webp",
- "label": "学校",
- "yaw": 0,
- "name": "Newman Cafeteria",
- "authors": {
- "Savva Zakharov": "All"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/newman_cafeteria"
- },
- {
- "file": "minedump_flats.webp",
- "label": "砂漠",
- "yaw": 0,
- "name": "Minedump Flats",
- "authors": {
- "Dimitrios Savva": "Photography",
- "Jarod Guest": "Processing"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/minedump_flats"
- },
- {
- "file": "spiaggia_di_mondello.webp",
- "label": "海岸",
- "yaw": 0,
- "name": "Spiaggia di Mondello",
- "authors": {
- "Andreas Mischok": "All"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/spiaggia_di_mondello"
- },
- {
- "file": "wooden_lounge.webp",
- "label": "社長室(木の部屋)",
- "yaw": 0,
- "name": "Wooden Lounge",
- "authors": {
- "Greg Zaal": "All"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/wooden_lounge"
- },
- {
- "file": "abandoned_factory_canteen_01.webp",
- "label": "廃工場の食堂",
- "yaw": 0,
- "name": "Abandoned Factory Canteen 01",
- "authors": {
- "Sergej Majboroda": "All"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/abandoned_factory_canteen_01"
- },
- {
- "file": "basement_boxing_ring.webp",
- "label": "地下室のリング",
- "yaw": 180,
- "name": "Basement Boxing Ring",
- "authors": {
- "Sergej Majboroda": "All"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/basement_boxing_ring"
- },
- {
- "file": "autoshop_01.webp",
- "label": "自動車工場",
- "yaw": 0,
- "name": "Autoshop 01",
- "authors": {
- "Oliksiy Yakovlyev": "All"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/autoshop_01"
- },
- {
- "file": "urban_alley_01.webp",
- "label": "路地",
- "yaw": 0,
- "name": "Urban Alley 01",
- "authors": {
- "Andreas Mischok": "All"
- },
- "license": "CC0-1.0",
- "kind": "hdri",
- "source": "https://polyhaven.com/a/urban_alley_01"
- },
- {
- "file": "mutoujima.webp",
- "label": "無人島",
- "yaw": 0,
- "name": "Mutoujima",
- "authors": {
- "安竹洋平": "All"
- },
- "license": "original (not CC0)",
- "kind": "illustration",
- "source": "作者(安竹洋平)が bluebey-studio のために描き起こしたオリジナル作品"
- },
- {
- "file": "dokan.webp",
- "label": "土管のある空き地",
- "yaw": 0,
- "name": "Dokan",
- "authors": {
- "安竹洋平": "All"
- },
- "license": "original (not CC0)",
- "kind": "illustration",
- "source": "作者(安竹洋平)が blender-scenes でレンダリングした bluebey-studio 用のオリジナル作品"
- },
- {
- "file": "uchu.webp",
- "label": "宇宙船の中",
- "yaw": 0,
- "name": "Uchu",
- "authors": {
- "安竹洋平": "All"
- },
- "license": "original (not CC0)",
- "kind": "illustration",
- "source": "作者(安竹洋平)が blender-scenes でレンダリングした bluebey-studio 用のオリジナル作品"
- }
- ]
-}
\ No newline at end of file
diff --git a/bluebey-studio/assets/backgrounds/abandoned_factory_canteen_01.webp b/bluebey-studio/assets/backgrounds/abandoned_factory_canteen_01.webp
deleted file mode 100644
index 20650a1..0000000
Binary files a/bluebey-studio/assets/backgrounds/abandoned_factory_canteen_01.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/autoshop_01.webp b/bluebey-studio/assets/backgrounds/autoshop_01.webp
deleted file mode 100644
index e8275f9..0000000
Binary files a/bluebey-studio/assets/backgrounds/autoshop_01.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/autumn_park.webp b/bluebey-studio/assets/backgrounds/autumn_park.webp
deleted file mode 100644
index d96f889..0000000
Binary files a/bluebey-studio/assets/backgrounds/autumn_park.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/ballroom.webp b/bluebey-studio/assets/backgrounds/ballroom.webp
deleted file mode 100644
index 43214cc..0000000
Binary files a/bluebey-studio/assets/backgrounds/ballroom.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/basement_boxing_ring.webp b/bluebey-studio/assets/backgrounds/basement_boxing_ring.webp
deleted file mode 100644
index 3e04ce2..0000000
Binary files a/bluebey-studio/assets/backgrounds/basement_boxing_ring.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/dokan.webp b/bluebey-studio/assets/backgrounds/dokan.webp
deleted file mode 100644
index 7d408d3..0000000
Binary files a/bluebey-studio/assets/backgrounds/dokan.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/empty_warehouse_01.webp b/bluebey-studio/assets/backgrounds/empty_warehouse_01.webp
deleted file mode 100644
index 68890fe..0000000
Binary files a/bluebey-studio/assets/backgrounds/empty_warehouse_01.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/kloppenheim_06_puresky.webp b/bluebey-studio/assets/backgrounds/kloppenheim_06_puresky.webp
deleted file mode 100644
index 45764b1..0000000
Binary files a/bluebey-studio/assets/backgrounds/kloppenheim_06_puresky.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/minedump_flats.webp b/bluebey-studio/assets/backgrounds/minedump_flats.webp
deleted file mode 100644
index 7225a03..0000000
Binary files a/bluebey-studio/assets/backgrounds/minedump_flats.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/mutoujima.webp b/bluebey-studio/assets/backgrounds/mutoujima.webp
deleted file mode 100644
index 3d79b6b..0000000
Binary files a/bluebey-studio/assets/backgrounds/mutoujima.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/newman_cafeteria.webp b/bluebey-studio/assets/backgrounds/newman_cafeteria.webp
deleted file mode 100644
index fc5cdfe..0000000
Binary files a/bluebey-studio/assets/backgrounds/newman_cafeteria.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/spiaggia_di_mondello.webp b/bluebey-studio/assets/backgrounds/spiaggia_di_mondello.webp
deleted file mode 100644
index 356793d..0000000
Binary files a/bluebey-studio/assets/backgrounds/spiaggia_di_mondello.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/uchu.webp b/bluebey-studio/assets/backgrounds/uchu.webp
deleted file mode 100644
index 3ab794d..0000000
Binary files a/bluebey-studio/assets/backgrounds/uchu.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/urban_alley_01.webp b/bluebey-studio/assets/backgrounds/urban_alley_01.webp
deleted file mode 100644
index 91df0a1..0000000
Binary files a/bluebey-studio/assets/backgrounds/urban_alley_01.webp and /dev/null differ
diff --git a/bluebey-studio/assets/backgrounds/wooden_lounge.webp b/bluebey-studio/assets/backgrounds/wooden_lounge.webp
deleted file mode 100644
index ff3fbda..0000000
Binary files a/bluebey-studio/assets/backgrounds/wooden_lounge.webp and /dev/null differ
diff --git a/bluebey-studio/assets/beards/kaiser-right.png b/bluebey-studio/assets/beards/kaiser-right.png
deleted file mode 100644
index 31edd28..0000000
Binary files a/bluebey-studio/assets/beards/kaiser-right.png and /dev/null differ
diff --git a/bluebey-studio/assets/beards/scotch.png b/bluebey-studio/assets/beards/scotch.png
deleted file mode 100644
index 3978993..0000000
Binary files a/bluebey-studio/assets/beards/scotch.png and /dev/null differ
diff --git a/bluebey-studio/assets/bluebey.glb b/bluebey-studio/assets/bluebey.glb
deleted file mode 100644
index 0072864..0000000
Binary files a/bluebey-studio/assets/bluebey.glb and /dev/null differ
diff --git a/bluebey-studio/assets/brows/gol-right.png b/bluebey-studio/assets/brows/gol-right.png
deleted file mode 100644
index 73086a8..0000000
Binary files a/bluebey-studio/assets/brows/gol-right.png and /dev/null differ
diff --git a/bluebey-studio/assets/fonts/METADATA.pb b/bluebey-studio/assets/fonts/METADATA.pb
deleted file mode 100644
index 55ae891..0000000
--- a/bluebey-studio/assets/fonts/METADATA.pb
+++ /dev/null
@@ -1,84 +0,0 @@
-name: "M PLUS Rounded 1c"
-designer: "Coji Morishita, M+ Fonts Project"
-license: "OFL"
-category: "SANS_SERIF"
-date_added: "2018-05-17"
-fonts {
- name: "M PLUS Rounded 1c"
- style: "normal"
- weight: 100
- filename: "MPLUSRounded1c-Thin.ttf"
- post_script_name: "MPLUSRounded1c-Thin"
- full_name: "M PLUS Rounded 1c Thin"
- copyright: "Copyright 2016 The Rounded M+ Project Authors."
-}
-fonts {
- name: "M PLUS Rounded 1c"
- style: "normal"
- weight: 300
- filename: "MPLUSRounded1c-Light.ttf"
- post_script_name: "MPLUSRounded1c-Light"
- full_name: "M PLUS Rounded 1c Light"
- copyright: "Copyright 2016 The Rounded M+ Project Authors."
-}
-fonts {
- name: "M PLUS Rounded 1c"
- style: "normal"
- weight: 400
- filename: "MPLUSRounded1c-Regular.ttf"
- post_script_name: "MPLUSRounded1c-Regular"
- full_name: "M PLUS Rounded 1c"
- copyright: "Copyright 2016 The Rounded M+ Project Authors."
-}
-fonts {
- name: "M PLUS Rounded 1c"
- style: "normal"
- weight: 500
- filename: "MPLUSRounded1c-Medium.ttf"
- post_script_name: "MPLUSRounded1c-Medium"
- full_name: "M PLUS Rounded 1c Medium"
- copyright: "Copyright 2016 The Rounded M+ Project Authors."
-}
-fonts {
- name: "M PLUS Rounded 1c"
- style: "normal"
- weight: 700
- filename: "MPLUSRounded1c-Bold.ttf"
- post_script_name: "MPLUSRounded1c-Bold"
- full_name: "M PLUS Rounded 1c Bold"
- copyright: "Copyright 2016 The Rounded M+ Project Authors."
-}
-fonts {
- name: "M PLUS Rounded 1c"
- style: "normal"
- weight: 800
- filename: "MPLUSRounded1c-ExtraBold.ttf"
- post_script_name: "MPLUSRounded1c-ExtraBold"
- full_name: "M PLUS Rounded 1c ExtraBold"
- copyright: "Copyright 2016 The Rounded M+ Project Authors."
-}
-fonts {
- name: "M PLUS Rounded 1c"
- style: "normal"
- weight: 900
- filename: "MPLUSRounded1c-Black.ttf"
- post_script_name: "MPLUSRounded1c-Black"
- full_name: "M PLUS Rounded 1c Black"
- copyright: "Copyright 2016 The Rounded M+ Project Authors."
-}
-subsets: "cyrillic"
-subsets: "cyrillic-ext"
-subsets: "greek"
-subsets: "greek-ext"
-subsets: "hebrew"
-subsets: "japanese"
-subsets: "latin"
-subsets: "latin-ext"
-subsets: "menu"
-subsets: "vietnamese"
-
-source {
- repository_url: "https://github.com/coz-m/MPLUS_FONTS"
- commit: "eb604901d6f04b6f7f2a84b0378c58df84a9dba6"
-}
-primary_script: "Jpan"
diff --git a/bluebey-studio/assets/fonts/OFL.txt b/bluebey-studio/assets/fonts/OFL.txt
deleted file mode 100644
index b038fd1..0000000
--- a/bluebey-studio/assets/fonts/OFL.txt
+++ /dev/null
@@ -1,93 +0,0 @@
-Copyright 2021 The M+ FONTS Project Authors (https://github.com/coz-m/MPLUS_FONTS)
-
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-https://scripts.sil.org/OFL
-
-
------------------------------------------------------------
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
------------------------------------------------------------
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide
-development of collaborative font projects, to support the font creation
-efforts of academic and linguistic communities, and to provide a free and
-open framework in which fonts may be shared and improved in partnership
-with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and
-redistributed freely as long as they are not sold by themselves. The
-fonts, including any derivative works, can be bundled, embedded,
-redistributed and/or sold with any software provided that any reserved
-names are not used by derivative works. The fonts and derivatives,
-however, cannot be released under any other type of license. The
-requirement for fonts to remain under this license does not apply
-to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-"Font Software" refers to the set of files released by the Copyright
-Holder(s) under this license and clearly marked as such. This may
-include source files, build scripts and documentation.
-
-"Reserved Font Name" refers to any names specified as such after the
-copyright statement(s).
-
-"Original Version" refers to the collection of Font Software components as
-distributed by the Copyright Holder(s).
-
-"Modified Version" refers to any derivative made by adding to, deleting,
-or substituting -- in part or in whole -- any of the components of the
-Original Version, by changing formats or by porting the Font Software to a
-new environment.
-
-"Author" refers to any designer, engineer, programmer, technical
-writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of the Font Software, to use, study, copy, merge, embed, modify,
-redistribute, and sell modified and unmodified copies of the Font
-Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components,
-in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled,
-redistributed and/or sold with any software, provided that each copy
-contains the above copyright notice and this license. These can be
-included either as stand-alone text files, human-readable headers or
-in the appropriate machine-readable metadata fields within text or
-binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font
-Name(s) unless explicit written permission is granted by the corresponding
-Copyright Holder. This restriction only applies to the primary font name as
-presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
-Software shall not be used to promote, endorse or advertise any
-Modified Version, except to acknowledge the contribution(s) of the
-Copyright Holder(s) and the Author(s) or with their explicit written
-permission.
-
-5) The Font Software, modified or unmodified, in part or in whole,
-must be distributed entirely under this license, and must not be
-distributed under any other license. The requirement for fonts to
-remain under this license does not apply to any document created
-using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are
-not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
-OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/bluebey-studio/assets/fonts/bluebey-caption.ttf b/bluebey-studio/assets/fonts/bluebey-caption.ttf
deleted file mode 100644
index 9ae8eae..0000000
Binary files a/bluebey-studio/assets/fonts/bluebey-caption.ttf and /dev/null differ
diff --git a/bluebey-studio/assets/fonts/bluebey-caption.woff2 b/bluebey-studio/assets/fonts/bluebey-caption.woff2
deleted file mode 100644
index 69410e1..0000000
Binary files a/bluebey-studio/assets/fonts/bluebey-caption.woff2 and /dev/null differ
diff --git a/bluebey-studio/index.html b/bluebey-studio/index.html
deleted file mode 100644
index 7111b9b..0000000
--- a/bluebey-studio/index.html
+++ /dev/null
@@ -1,241 +0,0 @@
-
-
-
-
-
-ぶるべー スタジオ
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ぶるべー スタジオ
-
- PNG保存
- コピー
- ?
- ≡
-
-
-
-
-
-
- ドラッグ:回転 / ホイール:ズーム / 右ドラッグ:移動 / ぶるべーをドラッグ:移動
-
-
- 2本指でドラッグ:カメラ移動(ぶるべーは動きません)/ 2本指をつまむ:ズーム
- ぶるべーの上で1本指:ぶるべーを移動(上下で高さも)/ 何もない所で1本指:回転
-
-
-
-
-
-
-
-
-
×
-
ぶるべー スタジオの使い方
-
-
これは何?
-
- bluebey.glb(ぶるべーの3Dモデル)をブラウザで読み込み、自由にポーズをとらせ、
- 顔の表情を自由に作って、2Dの画像(PNG)として書き出せるスタジオです。
- 元の手描きパーツ(GIMPで作った目と口の画像)もそのまま使えます。
-
-
-
ポーズ
-
- 右パネルの「ポーズ」でボーンを選び、X/Y/Zのスライダーを動かします。
- モデルをクリックすると、その場所にいちばん近いボーンが選ばれます。
- ギズモ(回転の輪)を直接ドラッグしても回せます。
- スライダーの値は「休めの姿勢(レストポーズ)」からの差分です。0,0,0 で元の姿勢。
- 気に入ったポーズは「ポーズを保存」でファイルに残せます。
-
-
-
スマホでの操作
-
- 2本指でぐりぐり :カメラ(見ている基準点)を動かします。ぶるべーは基準点から動きません。
- 2本指をつまむ(ピンチ) :ズーム(寄る/引く)。
- ぶるべーの上で1本指でぐりぐり :ぶるべーを基準点から動かします。上下に動かすと高さも変わります。
- 何もない所で1本指でぐりぐり :カメラが回ります。
- パネル(下のシート)は、上の横線(ハンドル)をドラッグすると上下に動かせます。
-
-
-
表情
-
- 目:開き具合(まばたき・ウインク)、目線(上下左右)、瞳の大きさ、ハイライト、閉じ方の種類。
- 口:笑いの深さ、口の開き、舌、口角の点、太さ、色。
- 「目線パッド」をドラッグすると、選んだ側の目線が動きます(「左右を連動させる」中は両目)。
- プリセット(にっこり・ウインク・びっくり…)から始めると早いです。
- 「手描きパーツを使う」を選ぶと、元のGIMP製パーツに切り替わります。
-
-
-
3Dのまま/2Dにする
-
- 「表示」で リアル/フラット(トゥーン)/線画/線画(透明) を切り替えます。
- 線画は輪郭線+白ぬり。線の太さと色を変えられます。
- 「正投影」にすると遠近のゆがみがなくなり、図面のような絵になります。
- 背景は透明・白・好きな色から選べます。PNG保存は透明のまま保存できます。
-
-
-
キーボード
-
- S PNG保存
- C 画像をコピー
- Tab パネルの表示/非表示
- ? このヘルプ
- 1 –5 カメラの向き(正面・斜め・横・後ろ・俯瞰)
- R ポーズをリセット
- G ギズモの切り替え
- Space 生きている演出(ゆれ・まばたき)の開始/停止
-
-
-
書き出しのヒント
-
- PNGは「書き出しサイズ」を2倍・3倍にすると高解像度になります(印刷やスライド用)。
- ポーズと表情の全設定は「設定を保存」でJSONに残せます。READMEも参照してください。
-
-
-
-
-
-
-
ご利用の前に
-
このスタジオは、市政・政治活動とは関係のない個人的な制作物です。
-
このスタジオで作成した画像などは、自由に使っていただけます。ただし、商用利用やそのほか特殊な使い方をされる場合は、小平市の確認が必要ですので、このサイトのお問い合わせ からご連絡ください。
-
次のような使い方はご遠慮ください。
-
- 公序良俗に反する目的での利用
- ぶるべーのイメージを損なうような利用(たとえば、攻撃的・差別的・性的・過激な利用、誹謗中傷など)
- 反社会的勢力や違法行為に関わる利用
- 政治的意図があるように見える利用(例:特定の政治家や政党をぶるべーが応援しているような表現)
- 特定の宗教・宗派への勧誘や布教と誤解される利用
- 小平市が公認・推薦・後援しているように見える利用(市の公式発表や公文書と誤認させる利用を含む)
- 特定の企業・商品・思想・団体を、ぶるべーや小平市が推薦・保証しているように見える利用
-
-
ぶるべーの著作権は小平市にあります。このスタジオと3Dモデルは作者(安竹洋平)が自作したものです。
-
- 次回から表示しない
- 同意して使う
-
-
-
-
-
-
-
-
-
diff --git a/bluebey-studio/src/animation.js b/bluebey-studio/src/animation.js
deleted file mode 100644
index e569d12..0000000
--- a/bluebey-studio/src/animation.js
+++ /dev/null
@@ -1,204 +0,0 @@
-/**
- * The "living" layer: idle body motion, blinking and idle eye drift.
- *
- * These never touch the saved state. `pose()` returns the base pose with the
- * idle offsets added, and `eyeOpen`/`look` are multipliers the caller merges
- * into the face parameters, so turning the animation off restores exactly the
- * pose and expression the user had before.
- */
-
-export class Animator {
- constructor({ state }) {
- this.state = state;
- this.time = 0;
- this.blinkPhase = -1;
- this.blinkDuration = 0.17;
- this.blinkTimer = 2 + Math.random() * 1.5;
- this.eyeOpen = 1;
- this.look = { x: 0, y: 0 };
- this.idleWeight = 0;
- this.snotScale = 1;
- }
-
- reset() {
- this.eyeOpen = 1;
- this.look = { x: 0, y: 0 };
- this.blinkPhase = -1;
- this.blinkTimer = 2;
- this.time = 0;
- this.snotScale = 1;
- }
-
- update(dt) {
- const speed = clamp(this.state.anim.speed ?? 1, 0.1, 3);
- const step = Math.min(dt, 0.1) * speed;
- this.time += step;
-
- const moving = (this.state.anim.mode ?? 'off') !== 'off';
- this.idleWeight += ((moving ? 1 : 0) - this.idleWeight) * Math.min(1, step * 4);
-
- // 鼻ちょうちん: while sleeping, the bubble swells on the out-breath and shrinks
- // again. It is a real object (see src/main.js), so the motion is published here
- // as a plain multiplier the render loop applies to the bubble's scale.
- this.snotScale = (this.state.anim.mode ?? 'off') === 'sleep'
- ? 0.45 + 0.55 * (0.5 - 0.5 * Math.cos(this.time * 1.3))
- : 1;
-
- this.updateBlink(step);
- this.updateLook(step);
- }
-
- updateBlink(dt) {
- if ((this.state.anim.mode ?? 'off') === 'sleep') {
- // Asleep: the eyes stay shut, whatever the blink setting says.
- this.eyeOpen = 0;
- this.blinkPhase = -1;
- return;
- }
- if (!this.state.anim.blink) {
- this.eyeOpen = 1;
- this.blinkPhase = -1;
- return;
- }
- if (this.blinkPhase >= 0) {
- this.blinkPhase += dt / this.blinkDuration;
- if (this.blinkPhase >= 1) {
- this.blinkPhase = -1;
- this.eyeOpen = 1;
- const base = Math.max(0.5, this.state.anim.blinkInterval ?? 3.4);
- // Blink again sooner sometimes, so it does not look metronomic.
- this.blinkTimer = base * (0.55 + Math.random());
- if (Math.random() < 0.22) this.blinkTimer *= 0.35; // occasional double blink
- } else {
- this.eyeOpen = 1 - Math.pow(Math.sin(Math.PI * this.blinkPhase), 0.8);
- }
- return;
- }
- this.blinkTimer -= dt;
- if (this.blinkTimer <= 0) this.blinkPhase = 0;
- }
-
- updateLook(dt) {
- if (!this.state.anim.lookAround) {
- this.look.x = 0;
- this.look.y = 0;
- return;
- }
- const t = this.time;
- this.look.x = Math.sin(t * 0.37) * 0.3 + Math.sin(t * 0.13 + 1.7) * 0.14;
- this.look.y = Math.sin(t * 0.29 + 0.6) * 0.2;
- }
-
- /** Base pose plus the current movement offsets; `pose` is `{ bones, root }`. */
- pose(basePose = {}) {
- const bones = { ...(basePose.bones ?? {}) };
- const root = [...(basePose.root ?? [0, 0, 0])];
- const mode = this.state.anim.mode ?? 'off';
- if (mode === 'off' || this.idleWeight <= 0.001) return { bones, root };
-
- const w = this.idleWeight;
- const t = this.time;
- const add = (name, dx, dy, dz) => {
- const current = bones[name] ?? [0, 0, 0];
- bones[name] = [current[0] + dx * w, current[1] + dy * w, current[2] + dz * w];
- };
-
- if (mode === 'walk') {
- // A waddle for a character with no legs: a step bob, a side-to-side rock
- // and alternating arms and feet.
- //
- // Z is the forward/back swing, and it is mirrored between the sides - so the
- // SAME value on both arms swings them opposite ways, which is what a walk
- // wants. X (which the first version used) is the *lift*, so opposite signs
- // there just raised one flipper and dropped the other one.
- // `legsupport` pivots at the middle of the body, so the legs take a much
- // smaller angle than the arms. The legs swing on the OPPOSITE phase to the
- // arms, so when a hand comes forward it is the opposite foot that steps out
- // (the same value as the arms put them in step, i.e. a "same-side" waddle).
- const phase = Math.sin(t * 2.4);
- const bob = Math.abs(Math.sin(t * 2.4));
- add('master', -2, 0, phase * 4);
- add('arm.l', 6, 0, phase * 26);
- add('arm.r', 6, 0, phase * 26);
- add('hand.l', 0, 0, phase * 10);
- add('hand.r', 0, 0, phase * 10);
- add('legsupport.l', 0, 0, -phase * 9);
- add('legsupport.r', 0, 0, -phase * 9);
- root[1] += bob * 0.05;
- return { bones, root };
- }
-
- if (mode === 'wave') {
- // One flipper is held up and sweeps back and forth, with the hand lagging a
- // little behind it; the body and the other flipper rock along gently. The
- // lift is kept modest: raising the arm much further folds the flipper over
- // the top of the head, where its inner edge cuts into the face, so the X
- // here (and its swing) stop well short of that. The Z swing is what reads
- // as the wave, and it is biased slightly *back* for the same reason.
- const wave = Math.sin(t * 3.2);
- add('arm.l', 66 + wave * 8, 0, 6 + wave * 10);
- add('hand.l', 0, 0, Math.sin(t * 3.2 + 0.6) * 14);
- add('arm.r', 0, 0, Math.sin(t * 1.6 + 1) * 4);
- add('master', Math.sin(t * 1.6) * 1.2, 0, Math.sin(t * 1.6 + 0.4) * 3);
- return { bones, root };
- }
-
- if (mode === 'sleep') {
- // Slow breathing: the body rises and settles, the flippers drift, and the
- // bubble swells and shrinks (that swell is `snotScale`, read by the loop).
- const breath = 0.5 - 0.5 * Math.cos(t * 1.3);
- add('master', Math.sin(t * 0.65) * 1.1, 0, 0);
- add('arm.l', 0, 0, Math.sin(t * 0.9) * 2.5);
- add('arm.r', 0, 0, -Math.sin(t * 0.9) * 2.5);
- root[1] += breath * 0.03;
- return { bones, root };
- }
-
- // Default: gentle breathing, as if standing there alive.
- add('master', Math.sin(t * 1.5) * 1.3, 0, Math.sin(t * 0.81) * 1.6);
- add('arm.l', 0, 0, Math.sin(t * 1.28) * 4.5);
- add('arm.r', 0, 0, -Math.sin(t * 1.28 + 0.5) * 4.5);
- add('hand.l', 0, 0, Math.sin(t * 1.05 + 1) * 3);
- add('hand.r', 0, 0, -Math.sin(t * 1.05 + 1) * 3);
- root[1] += Math.sin(t * 1.5) * 0.022;
- return { bones, root };
- }
-}
-
-/**
- * Records the viewport canvas to a WebM blob while the animation plays.
- * Chrome and Edge support `MediaRecorder` on `canvas.captureStream()`.
- */
-export function createRecorder(canvas) {
- if (typeof MediaRecorder === 'undefined' || !canvas.captureStream) return null;
- const mimeType = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm']
- .find((type) => MediaRecorder.isTypeSupported?.(type));
- return { canvas, mimeType: mimeType ?? '', recorder: null, chunks: [] };
-}
-
-export function startRecording(session) {
- if (!session) return false;
- const stream = session.canvas.captureStream(30);
- const recorder = session.mimeType
- ? new MediaRecorder(stream, { mimeType: session.mimeType, videoBitsPerSecond: 8000000 })
- : new MediaRecorder(stream);
- session.chunks = [];
- session.recorder = recorder;
- recorder.ondataavailable = (event) => { if (event.data?.size) session.chunks.push(event.data); };
- recorder.start(100);
- return true;
-}
-
-export function stopRecording(session) {
- return new Promise((resolve) => {
- if (!session?.recorder || session.recorder.state === 'inactive') {
- resolve(null);
- return;
- }
- const { recorder, chunks } = session;
- recorder.onstop = () => resolve(new Blob(chunks, { type: recorder.mimeType || 'video/webm' }));
- recorder.stop();
- });
-}
-
-const clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
diff --git a/bluebey-studio/src/background.js b/bluebey-studio/src/background.js
deleted file mode 100644
index adc7f94..0000000
--- a/bluebey-studio/src/background.js
+++ /dev/null
@@ -1,666 +0,0 @@
-/**
- * The studio backdrop, as a DOM layer that sits behind the WebGL canvas.
- *
- * The renderer is created with `alpha: true`, so whenever it is cleared to
- * alpha 0 the page shows through. Painting the backdrop into a sibling layer
- * placed *under* `#view` therefore gives the character a scene without touching
- * the scene graph: the CC0 photo, the user's own picture and the phone's camera
- * feed are all just CSS on one element, and no texture has to be uploaded per
- * frame.
- *
- * `backdropStyle()` is deliberately pure. It is the only place that turns the
- * `view.background*` slice into a CSS descriptor, so the behaviour `apply()`
- * ships with is exactly the behaviour the unit tests cover.
- */
-
-/** The library already downloaded into `assets/backgrounds/` (see CREDITS.json).
- * Most are Poly Haven *HDRIs* - CC0 photographs of real places - so each one comes
- * with a floor, a horizon and perspective, which is what gives a picture depth
- * that a flat wall texture cannot. `mutoujima`, `dokan` and `uchu` are the
- * exceptions: original illustrations drawn for this studio by the author, not
- * CC0 photos. */
-export const BACKGROUND_PRESETS = [
- { name: 'empty_warehouse_01', label: '倉庫' },
- { name: 'ballroom', label: '広間' },
- { name: 'kloppenheim_06_puresky', label: '空と雲' },
- { name: 'autumn_park', label: '秋の公園' },
- // The places a councillor explains things in, or stands in to make a point.
- // Poly Haven has no true classroom, desert or 社長室, so these are the closest
- // real places it does have.
- { name: 'newman_cafeteria', label: '学校' },
- { name: 'wooden_lounge', label: '社長室(木の部屋)' },
- { name: 'minedump_flats', label: '砂漠' },
- { name: 'spiaggia_di_mondello', label: '海岸' },
- // ポリヘイブンの写真ではなく、作者(安竹洋平)が描き起こしたオリジナルの一枚絵。
- { name: 'mutoujima', label: '無人島' },
- { name: 'dokan', label: '土管のある空き地' },
- { name: 'uchu', label: '宇宙船の中' },
- // 「ぶるべーを探せ!」用。物がたくさん詰まっていて、同じ形が何度も出てくる場所。
- { name: 'abandoned_factory_canteen_01', label: '廃工場の食堂' },
- { name: 'basement_boxing_ring', label: '地下室のリング' },
- { name: 'autoshop_01', label: '自動車工場' },
- { name: 'urban_alley_01', label: '路地' },
-];
-
-const DEFAULT_PRESET = 'autumn_park';
-
-/**
- * Manga effect-line backdrops, drawn *procedurally* (no file, no network).
- *
- * They are the classic stress marks: a burst of lines from behind the character
- * (`focus`), lines raining down (`fall`), lines streaming sideways (`speed`) and
- * short lines ringing a wide ellipse so the empty middle reads as isolation
- * (`ellipse`).
- * Each is a canvas painted black on white; the same generator runs for the
- * preview, the PNG and the standalone build.
- */
-export const EFFECT_PRESETS = [
- { name: 'focus', label: '集中線' },
- { name: 'fall', label: '落ち込み線' },
- { name: 'speed', label: '疾走線' },
- { name: 'ellipse', label: '楕円集中線' },
-];
-
-const DEFAULT_EFFECT = 'focus';
-
-/** The canvas every effect is painted on, so preview and export agree. */
-export const EFFECT_SIZE = { width: 1200, height: 800 };
-
-/**
- * A small deterministic PRNG (FNV-1a seeding + mulberry32), so an effect looks
- * the same on every run, in every build, and in the tests.
- */
-function makeRandom(seedText) {
- let seed = 2166136261;
- for (let i = 0; i < seedText.length; i++) {
- seed ^= seedText.charCodeAt(i);
- seed = Math.imul(seed, 16777619);
- }
- return () => {
- seed = (seed + 0x6d2b79f5) | 0;
- let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
- };
-}
-
-/**
- * Paint one effect-line backdrop onto `ctx`. Pure and deterministic: given the
- * same name and size it always issues the same black-on-white strokes, so the
- * unit tests can pin the look down and the cache below stays sound.
- *
- * @param {CanvasRenderingContext2D} ctx
- * @param {'focus'|'fall'|'speed'|'ellipse'} name
- * @param {{width:number,height:number}} [size]
- */
-export function drawEffectLines(ctx, name, size = EFFECT_SIZE) {
- const width = positiveOrOne(size?.width);
- const height = positiveOrOne(size?.height);
- const kind = EFFECT_PRESETS.some((preset) => preset.name === name) ? name : DEFAULT_EFFECT;
- const random = makeRandom(kind);
-
- ctx.save();
- ctx.fillStyle = '#ffffff';
- ctx.fillRect(0, 0, width, height);
- ctx.strokeStyle = '#111111';
- ctx.lineCap = 'butt';
-
- if (kind === 'focus') {
- // A burst: lines radiate from a point behind the head, and stop short of it
- // so the character's face is not crossed by ink.
- const cx = width * 0.5;
- const cy = height * 0.42;
- const reach = Math.hypot(width, height) * 1.1;
- const near = Math.min(width, height);
- for (let i = 0; i < 150; i++) {
- const angle = (i / 150) * Math.PI * 2 + (random() - 0.5) * 0.03;
- const inner = near * (0.12 + random() * 0.5);
- ctx.lineWidth = 1 + random() * 4;
- ctx.beginPath();
- ctx.moveTo(cx + Math.cos(angle) * inner, cy + Math.sin(angle) * inner);
- ctx.lineTo(cx + Math.cos(angle) * reach, cy + Math.sin(angle) * reach);
- ctx.stroke();
- }
- } else if (kind === 'fall') {
- // Lines raining down from the top edge, of uneven length.
- for (let i = 0; i < 90; i++) {
- const x = Math.min(width, Math.max(0, ((i + 0.5) / 90) * width + (random() - 0.5) * 8));
- const length = height * (0.25 + random() * 0.7);
- ctx.lineWidth = 1 + random() * 3;
- ctx.beginPath();
- ctx.moveTo(x, 0);
- ctx.lineTo(x, length);
- ctx.stroke();
- }
- } else if (kind === 'speed') {
- // Lines streaming in from one side or the other.
- for (let i = 0; i < 70; i++) {
- const y = Math.min(height, Math.max(0, ((i + 0.5) / 70) * height + (random() - 0.5) * 6));
- const length = width * (0.35 + random() * 0.65);
- const x = random() < 0.5 ? 0 : width - length;
- ctx.lineWidth = 1 + random() * 3.5;
- ctx.beginPath();
- ctx.moveTo(x, y);
- ctx.lineTo(x + length, y);
- ctx.stroke();
- }
- } else if (kind === 'ellipse') {
- // A dense ring of short lines *outside* a wide ellipse: the blank inside is
- // the point (the 「ひとり」 panel), so nothing is drawn there. The start
- // points are spaced by arc length, not by angle - an equal angular step
- // bunches the lines at the two ends of a wide ellipse and leaves gaps along
- // the flat top and bottom, which is what made the old ring look uneven. Each
- // line then leaves along the ellipse's outward normal, so the inner edge
- // stays a clean ellipse all the way round. Lengths vary (0.5-1.7x reach) and
- // every line is anchored to the ring, so none reads as a stray scratch.
- const cx = width * 0.5;
- const cy = height * 0.5;
- const rx = Math.min(width * 0.42, height * 0.9);
- const ry = rx * 0.55;
- const lines = 200;
- const reach = Math.min(width, height) * 0.24;
-
- // Cumulative arc length around the ring, so the lines can be spread evenly
- // around it rather than evenly by angle.
- const samples = 512;
- const arc = [0];
- for (let s = 1; s <= samples; s++) {
- const prev = ((s - 1) / samples) * Math.PI * 2;
- const next = (s / samples) * Math.PI * 2;
- const dx = (Math.cos(next) - Math.cos(prev)) * rx;
- const dy = (Math.sin(next) - Math.sin(prev)) * ry;
- arc.push(arc[s - 1] + Math.hypot(dx, dy));
- }
- const total = arc[samples];
-
- let s = 1;
- for (let i = 0; i < lines; i++) {
- const target = ((i + 0.5) / lines) * total;
- while (s < samples && arc[s] < target) s++;
- const span = arc[s] - arc[s - 1] || 1;
- const t = (((s - 1) + (target - arc[s - 1]) / span) / samples) * Math.PI * 2;
- const cos = Math.cos(t);
- const sin = Math.sin(t);
- // The outward normal of an ellipse points along (cos/rx, sin/ry), not along
- // the radius from the centre, so the lines meet the ring at a right angle.
- const nx = cos / rx;
- const ny = sin / ry;
- const length = (reach * (0.5 + random() * 1.1)) / (Math.hypot(nx, ny) || 1);
- ctx.lineWidth = 1 + random() * 3.5;
- const startX = cx + cos * rx;
- const startY = cy + sin * ry;
- ctx.beginPath();
- ctx.moveTo(startX, startY);
- ctx.lineTo(startX + nx * length, startY + ny * length);
- ctx.stroke();
- }
- }
- ctx.restore();
-}
-
-/** How each `backgroundFit` paints: a CSS size, plus whether the image tiles. */
-const FIT_STYLES = {
- cover: { backgroundSize: 'cover', backgroundRepeat: 'no-repeat' },
- contain: { backgroundSize: 'contain', backgroundRepeat: 'no-repeat' },
- stretch: { backgroundSize: '100% 100%', backgroundRepeat: 'no-repeat' },
- tile: { backgroundSize: 'auto', backgroundRepeat: 'repeat' },
-};
-const DEFAULT_FIT = 'cover';
-
-/** A live frame is a replaced element, so the same fit maps to `object-fit`. */
-const OBJECT_FIT = { cover: 'cover', contain: 'contain', '100% 100%': 'fill', auto: 'cover' };
-
-const MODES = new Set(['solid', 'transparent', 'preset', 'image', 'effect', 'camera']);
-
-/** Device orientation fires around 60 Hz; 30 Hz is plenty for swinging a camera. */
-const GYRO_INTERVAL_MS = 33;
-
-/** Coerce to a finite number, or `null` when the value is not one. */
-function finiteOrNull(value) {
- const n = Number(value);
- return Number.isFinite(n) ? n : null;
-}
-
-function normalizeMode(mode) {
- return MODES.has(mode) ? mode : 'solid';
-}
-
-function clamp01(value) {
- const n = finiteOrNull(value);
- if (n == null) return 0;
- return Math.min(1, Math.max(0, n));
-}
-
-function nonNegative(value) {
- const n = finiteOrNull(value);
- return n == null ? 0 : Math.max(0, n);
-}
-
-function positiveOrOne(value) {
- const n = finiteOrNull(value);
- return n == null || n <= 0 ? 1 : n;
-}
-
-/** The shortest signed distance between two angles, in degrees. */
-function wrapDeg(degrees) {
- return ((degrees + 540) % 360) - 180;
-}
-
-/**
- * Turn the `view` slice into the plain descriptor `apply()` renders from.
- * Every field is optional, so a half-written state still yields a usable look;
- * an unknown `background` falls back to `solid` and an unknown `backgroundFit`
- * to `cover`.
- *
- * `offset` is in fractions of the layer size, and `visible` is false only for
- * `transparent` (where the page background is meant to show through).
- */
-export function backdropStyle(viewState) {
- const state = viewState && typeof viewState === 'object' ? viewState : {};
- const fit = FIT_STYLES[state.backgroundFit] ?? FIT_STYLES[DEFAULT_FIT];
- const offset = state.backgroundOffset && typeof state.backgroundOffset === 'object'
- ? state.backgroundOffset
- : {};
-
- return {
- backgroundSize: fit.backgroundSize,
- backgroundRepeat: fit.backgroundRepeat,
- mirror: state.cameraMirror === true,
- darken: clamp01(state.backgroundDarken),
- blur: nonNegative(state.backgroundBlur),
- offset: { x: finiteOrNull(offset.x) ?? 0, y: finiteOrNull(offset.y) ?? 0 },
- scale: positiveOrOne(state.backgroundScale),
- visible: normalizeMode(state.background) !== 'transparent',
- };
-}
-
-/** `assets/backgrounds/.webp` - what the multi-file build serves. */
-function defaultResolveUrl(name) {
- return `assets/backgrounds/${name}.webp`;
-}
-
-/**
- * Build the backdrop layer for a `#stage` element.
- *
- * `resolveUrl(name)` maps a preset name to an image URL; the single-file build
- * passes one that reads an inlined map, while the default points at
- * `assets/backgrounds/`. `onNeedsRender` is called whenever the layer changes so
- * the host can repaint.
- *
- * The returned object is the only way in: the layer never reads or writes the
- * studio state itself, `apply(viewState)` is the single entry point.
- */
-export function createBackdrop({ stage, resolveUrl, onNeedsRender } = {}) {
- if (!stage || typeof stage.prepend !== 'function') {
- throw new Error('createBackdrop: ステージ要素(#stage)が必要です');
- }
-
- const doc = stage.ownerDocument ?? globalThis.document;
- const resolve = typeof resolveUrl === 'function' ? resolveUrl : defaultResolveUrl;
- const needsRender = typeof onNeedsRender === 'function' ? onNeedsRender : () => {};
-
- const el = doc.createElement('div');
- el.className = 'backdrop';
- el.setAttribute('aria-hidden', 'true');
- Object.assign(el.style, {
- position: 'absolute',
- inset: '0',
- zIndex: '0',
- overflow: 'hidden',
- pointerEvents: 'none',
- transformOrigin: 'center',
- });
-
- // The image surface is a plain div so blur/scale/tint stay pure CSS.
- const media = doc.createElement('div');
- Object.assign(media.style, { position: 'absolute', inset: '0', backgroundPosition: 'center' });
-
- // The camera feed is a muted, inline, autoplaying video for the AR mode.
- const video = doc.createElement('video');
- video.muted = true;
- video.playsInline = true;
- video.autoplay = true;
- video.setAttribute('muted', '');
- video.setAttribute('playsinline', '');
- video.setAttribute('aria-hidden', 'true');
- Object.assign(video.style, { position: 'absolute', inset: '0', display: 'none' });
-
- // The dark veil sits on top of whichever surface is showing, so the character
- // keeps its contrast against a busy photo.
- const veil = doc.createElement('div');
- Object.assign(veil.style, { position: 'absolute', inset: '0' });
-
- el.append(media, video, veil);
- // Prepended, i.e. before the canvas; the canvas is lifted back above it.
- stage.prepend(el);
-
- const canvas = stage.querySelector('canvas');
- if (canvas) {
- canvas.style.position = 'relative';
- canvas.style.zIndex = '1';
- }
-
- /** The most recent state slice, so the imperative helpers keep its styling. */
- let last = {};
- let stream = null;
-
- let gyroEnabled = false;
- let gyroAttached = false;
- let gyroBase = null;
- let gyroCallback = null;
- let lastGyroAt = 0;
-
- const effectUrls = new Map();
-
- /** The data URL for an effect-line backdrop, generated once and cached. */
- function effectUrl(name) {
- const key = EFFECT_PRESETS.some((preset) => preset.name === name) ? name : DEFAULT_EFFECT;
- if (effectUrls.has(key)) return effectUrls.get(key);
- const surface = doc.createElement('canvas');
- surface.width = EFFECT_SIZE.width;
- surface.height = EFFECT_SIZE.height;
- const surfaceCtx = surface.getContext('2d');
- if (!surfaceCtx) return null;
- drawEffectLines(surfaceCtx, key, EFFECT_SIZE);
- const url = surface.toDataURL('image/png');
- effectUrls.set(key, url);
- return url;
- }
-
- function imageUrlOf(mode, viewState) {
- if (mode === 'image') {
- return typeof viewState.backgroundImage === 'string' && viewState.backgroundImage
- ? viewState.backgroundImage
- : null;
- }
- if (mode === 'effect') {
- const name = typeof viewState.backgroundEffect === 'string' && viewState.backgroundEffect
- ? viewState.backgroundEffect
- : DEFAULT_EFFECT;
- return effectUrl(name);
- }
- if (mode !== 'preset') return null;
-
- const name = typeof viewState.backgroundPreset === 'string' && viewState.backgroundPreset
- ? viewState.backgroundPreset
- : DEFAULT_PRESET;
- try {
- const url = resolve(name);
- if (typeof url === 'string' && url) return url;
- } catch {
- // Fall through to the file path: an unknown preset is not worth throwing.
- }
- return defaultResolveUrl(name);
- }
-
- function colorOr(value) {
- return typeof value === 'string' && value ? value : '#ffffff';
- }
-
- /**
- * Fit, mirror, blur and the blur bleeding past the edges (the negative inset
- * keeps `filter: blur()` from fading the border to transparent).
- */
- function styleSurface(node, style) {
- node.style.inset = style.blur > 0 ? `-${style.blur * 2}px` : '0';
- node.style.filter = style.blur > 0 ? `blur(${style.blur}px)` : 'none';
- node.style.transform = style.mirror ? 'scaleX(-1)' : 'none';
-
- if (node === media) {
- node.style.backgroundSize = style.backgroundSize;
- node.style.backgroundRepeat = style.backgroundRepeat;
- } else {
- node.style.objectFit = OBJECT_FIT[style.backgroundSize] ?? 'cover';
- }
- }
-
- function apply(viewState) {
- last = viewState && typeof viewState === 'object' ? viewState : {};
- const style = backdropStyle(last);
-
- const wanted = normalizeMode(last.background);
- const url = imageUrlOf(wanted, last);
- // A preset that cannot be resolved is shown as a plain colour, never blank.
- const drawable = wanted === 'preset' || wanted === 'image' || wanted === 'effect';
- const mode = drawable && !url ? 'solid' : wanted;
-
- el.style.display = style.visible ? 'block' : 'none';
- el.style.backgroundColor = mode === 'solid' ? colorOr(last.backgroundColor) : 'transparent';
- el.style.transform = style.scale === 1 && style.offset.x === 0 && style.offset.y === 0
- ? 'none'
- : `scale(${style.scale}) translate(${style.offset.x * 100}%, ${style.offset.y * 100}%)`;
-
- veil.style.background = `rgba(0, 0, 0, ${style.darken})`;
- veil.style.display = style.darken > 0 ? 'block' : 'none';
-
- const showCamera = style.visible && mode === 'camera';
- const showImage = style.visible && (mode === 'preset' || mode === 'image' || mode === 'effect');
- // Leaving the AR mode must release the camera, not just hide the video.
- if (!showCamera && stream) stopCamera();
-
- styleSurface(media, style);
- styleSurface(video, style);
- media.style.display = showImage ? 'block' : 'none';
- video.style.display = showCamera ? 'block' : 'none';
- media.style.backgroundImage = showImage ? `url("${url}")` : 'none';
-
- needsRender();
- }
-
- /** Show a built-in backdrop, keeping the fit/darken/blur already in play. */
- function loadPreset(name) {
- const preset = typeof name === 'string' && name ? name : DEFAULT_PRESET;
- apply({ ...last, background: 'preset', backgroundPreset: preset });
- }
-
- /** Apply a stored data URL (as kept in `view.backgroundImage`). */
- function applyImage(dataUrl) {
- if (typeof dataUrl !== 'string' || !dataUrl) {
- apply({ ...last, background: 'solid', backgroundImage: null });
- return;
- }
- apply({ ...last, background: 'image', backgroundImage: dataUrl });
- }
-
- /** Read a user File into a data URL, show it, and hand the URL back to save. */
- async function loadImageFile(file) {
- const dataUrl = await new Promise((resolveUrl, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolveUrl(String(reader.result));
- reader.onerror = () => reject(reader.error ?? new Error('画像を読み込めませんでした'));
- reader.readAsDataURL(file);
- });
- applyImage(dataUrl);
- return dataUrl;
- }
-
- /* Shown whenever the page is not a secure context. This is a fact about the
- * web platform, not about any one browser, so the wording must not point a
- * finger at the browser the user happens to be holding. */
- const INSECURE_CAMERA_REASON =
- 'この開き方(http のアドレス)では、どのブラウザでもカメラを使えません。https のサイトか localhost で開いてください。';
-
- /**
- * Japanese, actionable text for the ways `getUserMedia` usually fails. On a
- * phone this toast is all the user has to go on, so each case names the exact
- * thing to tap instead of just stating that something went wrong.
- */
- function cameraFailureReason(error) {
- switch (error?.name) {
- case 'NotAllowedError':
- case 'PermissionDeniedError':
- return 'カメラが許可されていません。URLバーのアイコン→カメラ→「許可」に変え(一度「ブロック」するとブラウザは覚えています)、端末の設定でもアプリに許可してください(Android: 設定→アプリ→Brave→権限→カメラ/iOS: 設定→Brave→カメラ)。';
- case 'NotFoundError':
- case 'DevicesNotFoundError':
- return '使えるカメラが見つかりませんでした';
- case 'NotReadableError':
- case 'TrackStartError':
- return 'カメラを起動できませんでした(他のアプリが使用中の可能性があります)';
- case 'OverconstrainedError':
- case 'ConstraintNotSatisfiedError':
- return '指定したカメラを使用できません';
- case 'SecurityError':
- // No `mediaDevices` at all is the usual symptom, but this covers the
- // same cause when a sandboxed frame raises the error instead.
- return INSECURE_CAMERA_REASON;
- default:
- return 'カメラを起動できませんでした';
- }
- }
-
- async function startCamera(facing = 'environment') {
- const mediaDevices = globalThis.navigator?.mediaDevices;
- if (!mediaDevices || typeof mediaDevices.getUserMedia !== 'function') {
- // An insecure page has no `mediaDevices` at all. Say so plainly rather
- // than leaving the user to think their camera or browser is broken.
- return {
- ok: false,
- reason: globalThis.isSecureContext === false
- ? INSECURE_CAMERA_REASON
- : 'カメラを使うには https か localhost が必要です',
- };
- }
-
- stopCamera();
- try {
- const next = await mediaDevices.getUserMedia({ video: { facingMode: facing } });
- stream = next;
- video.srcObject = next;
- try {
- await video.play();
- } catch {
- // A blocked autoplay still resolves to a painting stream in practice.
- }
- apply({ ...last, background: 'camera', cameraFacing: facing });
- return { ok: true };
- } catch (error) {
- stopCamera();
- return { ok: false, reason: cameraFailureReason(error) };
- }
- }
-
- function stopCamera() {
- const active = stream;
- stream = null;
- if (active) {
- try {
- for (const track of active.getTracks()) track.stop();
- } catch {
- // A track that cannot be stopped is already gone as far as we care.
- }
- }
- if (video) {
- try {
- video.pause();
- } catch {
- // Pausing a video without a source is expected to throw.
- }
- video.srcObject = null;
- video.style.display = 'none';
- }
- }
-
- function emitGyro(yaw, pitch, roll) {
- gyroCallback?.({ yaw, pitch, roll });
- }
-
- /**
- * Attach the orientation listener and take the current pose as zero, so the
- * first reading a callback sees is `{ yaw: 0, pitch: 0, roll: 0 }`.
- */
- function enableGyro() {
- gyroEnabled = true;
- gyroBase = null;
- if (!gyroAttached && typeof globalThis.addEventListener === 'function') {
- globalThis.addEventListener('deviceorientation', handleOrientation);
- gyroAttached = true;
- }
- emitGyro(0, 0, 0);
- }
-
- /**
- * Degrees of turn from the pose gyro started at. `yaw` grows clockwise
- * (turning the device to the right); `pitch` and `roll` follow the raw
- * `beta` and `gamma` axes. Readings are throttled to roughly 30 Hz.
- */
- function handleOrientation(event) {
- if (!gyroEnabled) return;
-
- const alpha = typeof event?.alpha === 'number' ? event.alpha : null;
- const beta = typeof event?.beta === 'number' ? event.beta : null;
- const gamma = typeof event?.gamma === 'number' ? event.gamma : null;
- if (alpha == null && beta == null && gamma == null) return;
-
- if (!gyroBase) gyroBase = { alpha: alpha ?? 0, beta: beta ?? 0, gamma: gamma ?? 0 };
-
- const now = Date.now();
- if (now - lastGyroAt < GYRO_INTERVAL_MS) return;
- lastGyroAt = now;
-
- emitGyro(
- alpha == null ? 0 : wrapDeg(gyroBase.alpha - alpha),
- beta == null ? 0 : wrapDeg(beta - gyroBase.beta),
- gamma == null ? 0 : wrapDeg(gamma - gyroBase.gamma),
- );
- }
-
- /** Ask for orientation permission (iOS) and start listening. */
- async function requestGyro() {
- const OrientationEvent = globalThis.DeviceOrientationEvent;
- if (!OrientationEvent || typeof globalThis.addEventListener !== 'function') return false;
-
- if (typeof OrientationEvent.requestPermission === 'function') {
- let granted = false;
- try {
- granted = (await OrientationEvent.requestPermission()) === 'granted';
- } catch {
- granted = false;
- }
- if (!granted) return false;
- }
-
- enableGyro();
- return true;
- }
-
- /** Subscribe to `{ yaw, pitch, roll }`; returns an unsubscribe function. */
- function onGyro(callback) {
- gyroCallback = typeof callback === 'function' ? callback : null;
- if (!gyroEnabled) enableGyro();
- return () => {
- if (gyroCallback === callback) gyroCallback = null;
- };
- }
-
- function dispose() {
- stopCamera();
- if (gyroAttached) {
- globalThis.removeEventListener?.('deviceorientation', handleOrientation);
- gyroAttached = false;
- }
- gyroEnabled = false;
- gyroCallback = null;
- gyroBase = null;
- el.remove();
- }
-
- return {
- el,
- apply,
- loadPreset,
- loadImageFile,
- applyImage,
- startCamera,
- stopCamera,
- /** The data URL of a drawn effect line, so exports can composite the same
- * bitmap the preview shows. */
- effectUrl,
- get cameraActive() {
- return stream !== null;
- },
- requestGyro,
- onGyro,
- snapshot() {},
- dispose,
- };
-}
diff --git a/bluebey-studio/src/clip.js b/bluebey-studio/src/clip.js
deleted file mode 100644
index 4fdcfd9..0000000
--- a/bluebey-studio/src/clip.js
+++ /dev/null
@@ -1,202 +0,0 @@
-import * as THREE from 'three';
-
-/**
- * 見えない壁 (the invisible wall): a clipping plane that hides whatever falls
- * behind it, so the character can be buried in the wall and only the rest of the
- * body shows.
- *
- * The wall is a *finite* rectangle: an invisible quad that writes depth and no
- * colour, drawn before everything else in the opaque pass, so the character
- * behind it is culled by the depth test. That is what a real wall does, and it
- * is the only way to bound the effect - a `renderer.clippingPlanes` entry is an
- * infinite half-space, so it can never be limited to a rectangle.
- *
- * Nothing needs to agree per-material: the depth buffer does the work, so the
- * body, the outline hulls, the face plates, the ink pass and the offscreen
- * renders an export uses are all hidden by the same wall.
- *
- * `wallPlane` is pure (no three.js maths), so the geometry can be unit-tested
- * without a renderer.
- */
-
-const DEG = Math.PI / 180;
-
-/**
- * A hair toward the kept side. The guide sits exactly on the cut, and a plane
- * on its own boundary is half inside the discarded half - nudging it along the
- * normal keeps all of it on the visible side.
- */
-const GUIDE_EPSILON = 0.002;
-
-/**
- * The plane a wall setting lies in.
- *
- * The wall is a *finite* quad, so this no longer decides which half of space is
- * hidden - the quad's own depth does that. What `apply` needs from here is the
- * normal, which is the direction the quad faces (and so the direction the wall
- * lies along), plus the constant of the plane through the point it sits on.
- *
- * By default the wall lies in the XY plane through `(x, y, z)`; `yaw` turns it
- * about Y and `tilt` leans it about X afterwards.
- *
- * @param {{x?:number,y?:number,z?:number,yaw?:number,tilt?:number}} [wall]
- * @returns {{normal:[number,number,number], constant:number}}
- */
-export function wallPlane(wall = {}) {
- const yaw = (wall.yaw ?? 0) * DEG;
- const tilt = (wall.tilt ?? 0) * DEG;
- const cosYaw = Math.cos(yaw);
- const sinYaw = Math.sin(yaw);
- const cosTilt = Math.cos(tilt);
- const sinTilt = Math.sin(tilt);
-
- // Start from +Z, turn about Y, then lean about the (already turned) X axis.
- let nx = sinYaw;
- let ny = -cosYaw * sinTilt;
- let nz = cosYaw * cosTilt;
-
- const length = Math.hypot(nx, ny, nz) || 1;
- nx /= length;
- ny /= length;
- nz /= length;
-
- // Constant so the plane passes through (x, y, z): dot(n, p) + c = 0.
- const constant = -(nx * (wall.x ?? 0) + ny * (wall.y ?? 0) + nz * (wall.z ?? 0));
- return { normal: [nx, ny, nz], constant };
-}
-
-/** The faint plane shown while placing the wall. Hidden from every export. */
-/**
- * The wall itself: an invisible, *finite* quad that writes depth but no colour.
- *
- * It is drawn before everything else in the opaque pass (`renderOrder`), so the
- * character behind it is culled by the depth test. That is what a real wall does,
- * and - unlike a clip plane, which is an infinite half-space - it only hides what
- * the rectangle actually covers. So the size sliders are the wall's real size.
- */
-function makeWall() {
- const material = new THREE.MeshBasicMaterial({
- colorWrite: false,
- side: THREE.DoubleSide,
- toneMapped: false,
- });
- const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
- mesh.name = 'wall';
- // Before everything, including the silhouette-only hulls (renderOrder -1), so
- // its depth hides a nose buried in the wall (see styles.js hullMaterialFor).
- mesh.renderOrder = -2;
- mesh.visible = false;
- mesh.castShadow = false;
- mesh.receiveShadow = false;
- return mesh;
-}
-
-function makeGuide() {
- const geometry = new THREE.PlaneGeometry(1, 1);
- const material = new THREE.MeshBasicMaterial({
- color: 0x8a4fe0,
- transparent: true,
- opacity: 0.16,
- side: THREE.DoubleSide,
- depthWrite: false,
- toneMapped: false,
- });
- const mesh = new THREE.Mesh(geometry, material);
- mesh.name = 'wall-guide';
- mesh.userData.isHelper = true;
- mesh.visible = false;
-
- // An edge, so the plane's extent is legible even where the fill is faint.
- const edge = new THREE.LineSegments(
- new THREE.EdgesGeometry(geometry),
- new THREE.LineBasicMaterial({ color: 0x8a4fe0, transparent: true, opacity: 0.55, toneMapped: false }),
- );
- edge.userData.isHelper = true;
- mesh.add(edge);
- return mesh;
-}
-
-export function createClipper({ scene, renderer, model }) {
- // A little larger than the character: big enough to read as a wall, small
- // enough not to cover the whole viewport.
- const guideSpan = Math.max(model.size.x, model.size.y, model.size.z) * 1.5;
- const FROM = new THREE.Vector3(0, 0, 1); // the plane the quad starts in
-
- function makeSlot() {
- const occluder = makeWall();
- const guide = makeGuide();
- scene.add(guide);
- scene.add(occluder);
- return { occluder, guide, normal: new THREE.Vector3(0, 0, 1) };
- }
- // Two walls. Each slot keeps its own rectangle, so the two are independent.
- const slots = [makeSlot(), makeSlot()];
-
- /**
- * @param {Array} walls up to two `state.render.wall` settings, in slot
- * order.
- * @param {{x?:number,y?:number,z?:number}} [offset] the character's own
- * translation. A wall's position is stored *relative to the character*, so
- * adding this makes the walls travel with the body when it is moved.
- * @param {number} [yaw] the character's own turn about Y, in radians. The stored
- * position is rotated by it (and added to the wall's own `yaw`), so a wall
- * stays glued to the body when the character is turned.
- *
- * The guide is drawn only when `wall.guide` is on, which is also when the wall
- * can be grabbed in the viewport. Off, the wall is invisible: the cut still
- * applies, so you can see the character half-hidden with nothing in the way.
- */
- function apply(walls, offset, yaw = 0) {
- const list = Array.isArray(walls) ? walls : [walls];
- const ox = offset?.x ?? 0;
- const oy = offset?.y ?? 0;
- const oz = offset?.z ?? 0;
- const cos = Math.cos(yaw);
- const sin = Math.sin(yaw);
- const yawDeg = (yaw * 180) / Math.PI;
- slots.forEach((slot, index) => {
- const wall = list[index];
- if (!wall || wall.on !== true) {
- slot.occluder.visible = false;
- slot.guide.visible = false;
- return;
- }
- // The stored position is relative to the character, so it turns with the
- // body: rotate (x, z) about Y by the character's yaw, then add the offset.
- const lx = wall.x ?? 0;
- const lz = wall.z ?? 0;
- const x = cos * lx + sin * lz + ox;
- const y = (wall.y ?? 0) + oy;
- const z = -sin * lx + cos * lz + oz;
- const { normal } = wallPlane({ yaw: (wall.yaw ?? 0) + yawDeg, tilt: wall.tilt, x, y, z });
- slot.normal.set(normal[0], normal[1], normal[2]);
- const span = guideSpan * Math.max(0.05, wall.size ?? 1);
- // The wall and its guide are the same rectangle, turned to face along the
- // plane's normal: the wall is the occluder, the guide is the tinted copy
- // that is shown only while placing.
- for (const mesh of [slot.occluder, slot.guide]) {
- mesh.quaternion.setFromUnitVectors(FROM, slot.normal);
- mesh.scale.setScalar(span);
- }
- slot.occluder.position.set(x, y, z);
- slot.occluder.visible = true;
- slot.guide.position.set(x, y, z).addScaledVector(slot.normal, GUIDE_EPSILON);
- slot.guide.visible = wall.guide === true;
- });
- }
-
- return {
- guides: slots.map((slot) => slot.guide),
- occluders: slots.map((slot) => slot.occluder),
- apply,
- dispose() {
- for (const slot of slots) {
- for (const mesh of [slot.guide, slot.occluder]) {
- mesh.geometry.dispose();
- mesh.material.dispose();
- mesh.removeFromParent();
- }
- }
- },
- };
-}
diff --git a/bluebey-studio/src/exporter.js b/bluebey-studio/src/exporter.js
deleted file mode 100644
index ea9356e..0000000
--- a/bluebey-studio/src/exporter.js
+++ /dev/null
@@ -1,306 +0,0 @@
-import { traceAlphaContours, contoursToPathData } from './trace.js';
-import { roughenContours } from './handDrawn.js';
-import { EYE_LAYOUT, MOUTH_LAYOUT } from './faceArt.js';
-
-/**
- * Export helpers.
- *
- * Everything renders through the live renderer at a temporary resolution, so an
- * export always matches what is on screen (same camera, same framing) and the
- * only difference is the pixel size.
- */
-
-/** Render the current view into an offscreen 2D canvas at an arbitrary size. */
-export function renderStill(view, { width, height, background }) {
- const { renderer, scene, camera } = view;
- const previousAspect = camera.aspect;
-
- renderer.setPixelRatio(1);
- renderer.setSize(width, height, false);
- camera.aspect = width / height;
- camera.updateProjectionMatrix();
-
- applyBackground(renderer, background);
- // The screen-space outline renders the scene itself, so it replaces the plain
- // render rather than following it. (buildSVG passes outlineOptions of its own:
- // it wants the ink and nothing else, because the ink is what it traces.)
- if (view.outline && view.outlineOptions?.enabled) {
- view.outline.setSize(width, height);
- view.outline.render(() => renderer.render(scene, camera), { camera, ...view.outlineOptions });
- } else {
- renderer.render(scene, camera);
- }
-
- // Copy before restoring: resizing the renderer throws the frame away.
- const canvas = document.createElement('canvas');
- canvas.width = width;
- canvas.height = height;
- canvas.getContext('2d').drawImage(renderer.domElement, 0, 0);
-
- camera.aspect = previousAspect;
- camera.updateProjectionMatrix();
- view.restore?.();
-
- return canvas;
-}
-
-/** `background` is `{ mode: 'transparent' }` or `{ mode: 'solid', color }`. */
-export function applyBackground(renderer, background) {
- if (!background || background.mode === 'transparent') {
- renderer.setClearColor(0x000000, 0);
- } else {
- renderer.setClearColor(background.color ?? '#ffffff', 1);
- }
-}
-
-/** A PNG data URL of the current view, scaled up by `scale`. */
-export async function capturePNG(view, { scale = 2, background, width, height }) {
- const baseWidth = view.width || view.renderer.domElement.clientWidth || 1280;
- const baseHeight = view.height || view.renderer.domElement.clientHeight || 800;
- const outWidth = Math.min(8192, Math.max(64, Math.round((width ?? baseWidth) * scale)));
- const outHeight = Math.min(8192, Math.max(64, Math.round((height ?? baseHeight) * scale)));
- const canvas = renderStill(view, { width: outWidth, height: outHeight, background });
- const blob = await canvasToBlob(canvas);
- return { blob, canvas, width: outWidth, height: outHeight };
-}
-
-/**
- * Encode a canvas as a PNG blob. Exported because the caller composites the
- * backdrop and the 擬音 stamps onto the rendered canvas afterwards, and then has
- * to re-encode it.
- */
-export function canvasToBlob(canvas) {
- return new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
-}
-
-export async function copyCanvasToClipboard(canvas) {
- if (!navigator.clipboard || typeof ClipboardItem === 'undefined') {
- throw new Error('この環境ではクリップボードにコピーできません');
- }
- const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
- await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
-}
-
-/**
- * Vector line art: render the outline-only style, trace the coverage mask of
- * the result and emit one even-odd path. Rendering at a high resolution keeps
- * the traced curves smooth.
- */
-export function buildSVG(view, {
- styles,
- face,
- faceParams,
- width = 2048,
- threshold = 0.4,
- lineColor = '#111111',
- lineWidth = null,
- background = null,
- handDrawn = null,
- outlinePixels = 2,
-} = {}) {
- const previousStyle = styles.style;
- const previousMode = face.styleMode;
-
- // The `outline` style keeps every body mesh invisible (colorWrite off) and lets
- // the outline passes draw the lines, exactly as on screen: the hulls ink the
- // body, the screen-space pass inks the leaves. What reaches the trace is
- // therefore the ink alone - and a traced stroke comes out as its own outline,
- // i.e. two nested contours that `fill-rule="evenodd"` fills as a line of the
- // same width. That is what finally gives the leaves, which no inverted hull can
- // outline, clean even lines in the vector file too.
- //
- // `withOutlineFor` in main.js sets that split up for this call, and guards it so
- // a renderer without the outline pass still falls back to a hull-only trace.
- const screenSpace = Boolean(view.outline);
-
- styles.setStyle('outline');
- face.setParams(faceParams, 'line');
- face.flush(performance.now(), 0);
-
- const aspect = (view.height || 800) / (view.width || 1280);
- const height = Math.max(64, Math.round(width * aspect));
- // `radius` is in screen pixels, so it has to grow with the render: the SVG is
- // drawn far larger than the viewport, and a 2px line would become a hairline.
- const scale = width / Math.max(1, view.width || 1280);
- const canvas = renderStill(
- {
- ...view,
- outlineOptions: {
- enabled: screenSpace,
- color: lineColor,
- radius: outlinePixels * scale,
- },
- },
- { width, height, background: { mode: 'transparent' } },
- );
-
- styles.setStyle(previousStyle);
- face.setParams(faceParams, previousMode);
- face.flush(performance.now(), 0);
-
- const pixels = canvas.getContext('2d').getImageData(0, 0, width, height).data;
- const alpha = new Uint8Array(width * height);
- for (let i = 0; i < alpha.length; i++) alpha[i] = pixels[i * 4 + 3];
-
- let contours = traceAlphaContours(alpha, width, height, {
- threshold,
- simplifyTolerance: 0.6,
- minArea: 5,
- });
- // 手描き風: nudge the traced outlines so they read as pen strokes instead of
- // the mathematically smooth curves a mask trace produces.
- if (handDrawn && handDrawn.amount > 0) {
- contours = roughenContours(contours, {
- amount: handDrawn.amount,
- seed: handDrawn.seed ?? 1,
- scale: handDrawn.scale ?? 40,
- passes: handDrawn.passes ?? 1,
- });
- }
- const d = contoursToPathData(contours, (x, y) => [x, y], 2);
-
- const rect = background
- ? ` \n`
- : '';
- const widthAttr = lineWidth ? ` stroke="${lineColor}" stroke-width="${lineWidth}"` : '';
-
- return {
- contours: contours.length,
- svg: `
-
-ぶるべー 線画
-${rect}
-
-`,
- };
-}
-
-/** Alignment guides: blue on the white paper, light enough to paint over. */
-const FACE_MAP_GUIDE = 'rgba(90, 140, 220, 0.55)';
-/** Half-length of the centre ticks, in artwork-window pixels. */
-const FACE_MAP_TICK = 16;
-
-/**
- * Build the「下地」image an author paints a custom face texture on.
- *
- * The studio loads a hand-drawn image by drawing it at the artwork window's own
- * offset and size (see `Face.loadImage` / `placeInWindow`), so an image the size
- * of the *plate canvas*, with its artwork aligned to `offsetX/offsetY`, imports
- * 1:1. That is exactly what this hands out: the live drawing as a reference, an
- * opaque white window to paint on, and faint guides for the eye centres, the lid
- * line or the mouth chord.
- *
- * The white and the guides go on a **copy**: the plate canvas *is* the live
- * texture, and painting it would show up in the view.
- *
- * @param {import('./face.js').Face} face
- * @param {'eyes'|'mouth'} kind
- * @returns {HTMLCanvasElement}
- */
-export function buildFaceMap(face, kind) {
- const eyes = kind === 'eyes';
- const layout = eyes ? face.eyeLayout : face.mouthLayout;
- const plate = eyes ? face.eyeCanvas : face.mouthCanvas;
-
- const canvas = document.createElement('canvas');
- canvas.width = layout.width;
- canvas.height = layout.height;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(plate, 0, 0);
-
- const { offsetX, offsetY, window: win } = layout;
- // `destination-over` so the white paper lands *under* the copy: filling it
- // normally would erase the very drawing the author lines the new art up against.
- ctx.globalCompositeOperation = 'destination-over';
- ctx.fillStyle = '#ffffff';
- ctx.fillRect(offsetX, offsetY, win.width, win.height);
- ctx.globalCompositeOperation = 'source-over';
-
- // The plate canvas keeps the artwork at its original pixel size (only the canvas
- // grows around it), so a fixed-width stroke reads the same on both parts.
- ctx.strokeStyle = FACE_MAP_GUIDE;
- ctx.fillStyle = FACE_MAP_GUIDE;
- ctx.lineWidth = 2;
-
- // Where the drawing has to fit, and where its middle is. The cross uses short
- // ticks, not full-width lines, so it cannot be mistaken for artwork.
- ctx.strokeRect(offsetX, offsetY, win.width, win.height);
- const cx = offsetX + win.width / 2;
- const cy = offsetY + win.height / 2;
- guideLine(ctx, cx - FACE_MAP_TICK, cy, cx + FACE_MAP_TICK, cy);
- guideLine(ctx, cx, cy - FACE_MAP_TICK, cx, cy + FACE_MAP_TICK);
-
- if (eyes) {
- // Both eyeballs (`radius`) and the lid line through their centres, so a
- // hand-drawn brow or eye can be placed against the parametric ones.
- const [a, b] = EYE_LAYOUT.eyes;
- const y = offsetY + a.cy;
- guideLine(ctx, offsetX + a.cx, y, offsetX + b.cx, y);
- for (const eye of EYE_LAYOUT.eyes) {
- const ex = offsetX + eye.cx;
- const ey = offsetY + eye.cy;
- ctx.beginPath();
- ctx.arc(ex, ey, EYE_LAYOUT.radius, 0, Math.PI * 2);
- ctx.stroke();
- // A small dot marks the exact centre, where the eyeball pivots.
- ctx.beginPath();
- ctx.arc(ex, ey, ctx.lineWidth * 1.5, 0, Math.PI * 2);
- ctx.fill();
- }
- } else {
- // The lip line the mouth is drawn around: end to end, plus a tick at the centre.
- const y = offsetY + MOUTH_LAYOUT.chordY;
- const { centreX, halfChord } = MOUTH_LAYOUT;
- guideLine(ctx, offsetX + centreX - halfChord, y, offsetX + centreX + halfChord, y);
- guideLine(ctx, offsetX + centreX, y - FACE_MAP_TICK, offsetX + centreX, y + FACE_MAP_TICK);
- }
-
- return canvas;
-}
-
-/** One guide segment, kept out of the map builders so the placements stay readable. */
-function guideLine(ctx, x0, y0, x1, y1) {
- ctx.beginPath();
- ctx.moveTo(x0, y0);
- ctx.lineTo(x1, y1);
- ctx.stroke();
-}
-
-export function downloadBlob(blob, filename) {
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = filename;
- document.body.append(link);
- link.click();
- link.remove();
- setTimeout(() => URL.revokeObjectURL(url), 4000);
-}
-
-export function downloadText(text, filename, type = 'application/json') {
- downloadBlob(new Blob([text], { type }), filename);
-}
-
-export function readFileAsText(file) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolve(String(reader.result));
- reader.onerror = () => reject(reader.error);
- reader.readAsText(file);
- });
-}
-
-export function readFileAsArrayBuffer(file) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolve(reader.result);
- reader.onerror = () => reject(reader.error);
- reader.readAsArrayBuffer(file);
- });
-}
-
-export function timestamp() {
- const now = new Date();
- const pad = (n) => String(n).padStart(2, '0');
- return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
-}
diff --git a/bluebey-studio/src/face.js b/bluebey-studio/src/face.js
deleted file mode 100644
index f42b823..0000000
--- a/bluebey-studio/src/face.js
+++ /dev/null
@@ -1,375 +0,0 @@
-import * as THREE from 'three';
-import { drawEyes, drawMouth, canvasLayout } from './faceArt.js';
-
-/**
- * Owns the two procedural textures (eyes and mouth) and keeps them in sync with
- * the face parameters.
- *
- * The artwork is carried by the model's face plates, but those plates are shells
- * of the *whole* front half of the body, so their UVs cover far more than the
- * 0..1 window the drawing lives in. `remapPlate` rewrites each plate's UVs onto
- * 0..1 and the canvas is enlarged to match, which is what removes the
- * clamp-to-edge smear at the border and what gives a moved tear or mouth room to
- * move (see `canvasLayout` in src/faceArt.js).
- *
- * Both parts can independently fall back to the original hand-drawn texture
- * that ships inside the GLB.
- */
-export class Face {
- constructor({ eyeMesh, mouthMesh, hairMesh = null, originals }) {
- this.eyeMesh = eyeMesh;
- this.mouthMesh = mouthMesh;
- // The `hair-plate`, if the model has one, is never drawn: ぶるべー has no
- // hair. Hiding the mesh outright (rather than showing an empty texture) keeps
- // it from tinting the head on platforms where transparent textures do.
- this.hairMesh = hairMesh;
- if (hairMesh) hairMesh.visible = false;
-
- // Rewrite each plate's UVs so its own range fills 0..1, and work out the
- // canvas that keeps the artwork exactly where the 2022 textures put it.
- this.eyeLayout = remapPlate(eyeMesh);
- this.mouthLayout = remapPlate(mouthMesh);
-
- // The hand-drawn eye textures bake the face colour into their background,
- // which is invisible in the lit "real" style but shows up as a flat purple
- // patch in the flat and line-art styles. Key it out once, here. Both kinds
- // of original are window-sized artwork, so they belong *in* the window, not
- // stretched across the enlarged canvas.
- this.originals = {
- eyes: Object.fromEntries(
- Object.entries(originals.eyes).map(([key, texture]) => [key, keyOutBackground(texture, this.eyeLayout)]),
- ),
- mouth: placeInWindowTexture(originals.mouth, this.mouthLayout),
- };
-
- this.customNames = new Map();
- this.eyeCanvas = makeCanvas(this.eyeLayout);
- this.mouthCanvas = makeCanvas(this.mouthLayout);
- this.eyeCtx = this.eyeCanvas.getContext('2d');
- this.mouthCtx = this.mouthCanvas.getContext('2d');
-
- this.eyeTexture = makeTexture(this.eyeCanvas);
- this.mouthTexture = makeTexture(this.mouthCanvas);
-
- this.params = null;
- this.styleMode = 'paint';
- this.eyeSource = 'parametric';
- this.mouthSource = 'parametric';
- this.dirty = true;
- this.lastDraw = -Infinity;
-
- // The face plates are shells of the body surface, pushed a hair outwards by
- // the loader, so they win the depth test against the body by themselves.
- // Nudging them with a polygon offset as well keeps the two from z-fighting
- // along grazing angles. Depth *testing* stays on: that is what lets the nose
- // (which pokes further out) and an arm waved in front of the face hide the
- // artwork the way they should - it only ever hid the old planes because
- // those did not reach far enough down the head.
- const orders = new Map([[eyeMesh, 2], [mouthMesh, 1]]);
- if (hairMesh) orders.set(hairMesh, 3);
- for (const mesh of [eyeMesh, mouthMesh, ...(hairMesh ? [hairMesh] : [])]) {
- const material = mesh.material;
- material.polygonOffset = true;
- material.polygonOffsetFactor = -1;
- material.polygonOffsetUnits = -2;
- material.depthTest = true;
- material.depthWrite = false;
- // The GLB may describe the plate material as opaque; the artwork is a
- // texture with alpha, so blend instead of replacing what is behind it.
- material.transparent = true;
- material.side = THREE.DoubleSide;
- material.needsUpdate = true;
- // Tears are drawn on the eye plate and the mouth on the other one, and
- // both sit on the same shell: draw the mouth first so a teardrop can fall
- // across it instead of being painted over.
- mesh.renderOrder = orders.get(mesh) ?? 0;
- }
-
- this.applyTextures();
- }
-
- /** Show or hide the mouth without touching the artwork. */
- setMouthVisible(visible) {
- if (this.mouthMesh.visible !== visible) this.mouthMesh.visible = visible;
- }
-
- applyTextures() {
- const eyeMap = this.eyeSource === 'parametric'
- ? this.eyeTexture
- : (this.originals.eyes[this.eyeSource] ?? this.eyeTexture);
- const mouthMap = this.mouthSource === 'parametric'
- ? this.mouthTexture
- : (this.originals.mouth ?? this.mouthTexture);
-
- if (this.eyeMesh.material.map !== eyeMap) {
- this.eyeMesh.material.map = eyeMap;
- this.eyeMesh.material.needsUpdate = true;
- }
- if (this.mouthMesh.material.map !== mouthMap) {
- this.mouthMesh.material.map = mouthMap;
- this.mouthMesh.material.needsUpdate = true;
- }
- }
-
- /**
- * Load a hand-drawn image and use it for the eyes or the mouth. Any size is
- * accepted; it is scaled to the layout the model expects (1024 x 380).
- * Returns the key the new artwork is registered under.
- */
- async loadImage(kind, file) {
- const layout = kind === 'eyes' ? this.eyeLayout : this.mouthLayout;
- const bitmap = await createImageBitmap(file);
- // Any size is accepted; it is scaled into the artwork window, which is where
- // the plate's rewritten UVs expect it.
- const canvas = placeInWindow(bitmap, layout);
- if (typeof bitmap.close === 'function') bitmap.close();
- const texture = makeTexture(canvas);
-
- if (kind === 'eyes') {
- const key = `custom-${this.customNames.size + 1}`;
- this.customNames.set(key, file.name);
- this.originals.eyes[key] = texture;
- this.setSource('eyes', key);
- return key;
- }
- this.originals.mouth = texture;
- this.setSource('mouth', 'original');
- return 'original';
- }
-
- /** `kind` is `'eyes'` or `'mouth'`; `source` is `'parametric'` or a variant key. */
- setSource(kind, source) {
- if (kind === 'eyes') {
- if (this.eyeSource === source) return;
- this.eyeSource = source;
- } else {
- if (this.mouthSource === source) return;
- this.mouthSource = source;
- }
- this.applyTextures();
- this.dirty = true;
- }
-
- setParams(params, styleMode = this.styleMode) {
- this.params = params;
- this.styleMode = styleMode;
- this.dirty = true;
- }
-
- /**
- * Redraw the procedural textures when they are stale. `minInterval` throttles
- * redraws while animating; pass 0 before an export so nothing is left pending.
- */
- flush(now = performance.now(), minInterval = 0) {
- if (!this.dirty) return false;
- if (now - this.lastDraw < minInterval) return false;
- this.redraw();
- this.lastDraw = now;
- return true;
- }
-
- redraw() {
- const p = this.params;
- if (!p) {
- this.dirty = false;
- return;
- }
- // Hiding the mouth has to hide the plane itself, not just stop drawing on
- // it - otherwise the last drawing stays on screen.
- this.setMouthVisible(p.mouth.visible !== false);
- // A tongue poking over a *closed* lip can reach up behind the 3D nose, which
- // would hide it. In that one case let the mouth artwork win the depth test,
- // so the tongue reads; otherwise the nose (and an arm) still hide the mouth
- // the way they should. The lip line and the rest of the mouth sit clear of
- // the nose, so only the tongue can overlap it.
- const mouth = p.mouth ?? {};
- const tongueOverLip = (mouth.round ?? 0) <= 0.004
- && (mouth.open ?? 0) <= 0.004
- && (mouth.tongue ?? 1) > 0.01;
- this.mouthMesh.material.depthTest = !tongueOverLip;
- const mode = this.styleMode === 'line' ? 'line' : 'paint';
-
- if (this.eyeSource === 'parametric') {
- const layout = this.eyeLayout;
- const ctx = this.eyeCtx;
- ctx.clearRect(0, 0, layout.width, layout.height);
- ctx.save();
- // The artwork window sits at an offset inside the bigger canvas.
- ctx.translate(layout.offsetX, layout.offsetY);
- drawEyes(ctx, {
- mode,
- eyes: { left: p.eyes.left, right: p.eyes.right },
- style: {
- white: p.eyes.white,
- iris: p.eyes.iris,
- line: p.eyes.line,
- irisScale: p.eyes.irisScale,
- lookMax: p.eyes.lookMax,
- highlight: p.eyes.highlight,
- lidWidth: p.eyes.lidWidth,
- lowerLid: p.eyes.lowerLid,
- lidShape: p.eyes.lidShape,
- lidTilt: p.eyes.lidTilt,
- lashes: p.eyes.lashes,
- // 眼鏡 / サングラス are shared by both eyes and drawn into this same
- // texture, so they travel with the other shared eye fields.
- glasses: p.eyes.glasses,
- heartScale: p.eyes.heartScale,
- heartColor: p.eyes.heartColor,
- tearColor: p.eyes.tearColor,
- // Where the artwork's real edges are, so a brow lifted too far or a
- // tear dropped too low can stop inside the artwork (see `canvasLayout`).
- limits: layout.limits,
- },
- });
- ctx.restore();
- this.eyeTexture.needsUpdate = true;
- }
-
- if (this.mouthSource === 'parametric' && p.mouth.visible) {
- const layout = this.mouthLayout;
- const ctx = this.mouthCtx;
- ctx.clearRect(0, 0, layout.width, layout.height);
- ctx.save();
- ctx.translate(layout.offsetX, layout.offsetY);
- drawMouth(ctx, { mode, ...p.mouth, limits: layout.limits });
- ctx.restore();
- this.mouthTexture.needsUpdate = true;
- }
-
- this.dirty = false;
- }
-}
-
-function makeCanvas({ width, height }, scale = 1) {
- const canvas = document.createElement('canvas');
- canvas.width = Math.round(width * scale);
- canvas.height = Math.round(height * scale);
- return canvas;
-}
-
-/**
- * Rewrite a plate's UVs so its own range maps onto 0..1, and return the canvas
- * that keeps the artwork at its original pixel size (`canvasLayout`).
- *
- * The plate covers the whole front half of the body, so its UVs used to run well
- * outside the artwork (v -1.02..1.93 on the eye plate). Everything past the edge
- * clamped to the canvas border, which is what stretched a tear or a brow that
- * reached it. After this the whole plate samples real canvas, so nothing clamps.
- */
-function remapPlate(mesh) {
- const uv = mesh?.geometry?.attributes?.uv;
- if (!uv) return canvasLayout(null);
- let u0 = Infinity;
- let u1 = -Infinity;
- let v0 = Infinity;
- let v1 = -Infinity;
- for (let i = 0; i < uv.count; i += 1) {
- const u = uv.getX(i);
- const v = uv.getY(i);
- if (u < u0) u0 = u;
- if (u > u1) u1 = u;
- if (v < v0) v0 = v;
- if (v > v1) v1 = v;
- }
- const du = Math.max(1e-6, u1 - u0);
- const dv = Math.max(1e-6, v1 - v0);
- for (let i = 0; i < uv.count; i += 1) {
- uv.setXY(i, (uv.getX(i) - u0) / du, (uv.getY(i) - v0) / dv);
- }
- uv.needsUpdate = true;
- return canvasLayout({ u0, u1, v0, v1 });
-}
-
-/**
- * Draw a hand-drawn 1024 x 380 image where the artwork window now sits, on a
- * canvas as big as the plate's own UV range. The plates cover far more than the
- * window (see `canvasLayout`) and the UVs were rewritten to match, so the window
- * is exactly where that image belongs.
- */
-function placeInWindow(source, layout) {
- const canvas = document.createElement('canvas');
- canvas.width = layout.width;
- canvas.height = layout.height;
- if (source) {
- canvas.getContext('2d').drawImage(
- source, layout.offsetX, layout.offsetY, layout.window.width, layout.window.height,
- );
- }
- return canvas;
-}
-
-/** `placeInWindow` for an existing texture (keeps `null` as `null`). */
-function placeInWindowTexture(texture, layout) {
- const image = texture?.image;
- if (!image || !image.width || !image.height) return texture ?? null;
- return makeTexture(placeInWindow(image, layout));
-}
-
-function makeTexture(canvas) {
- const texture = new THREE.CanvasTexture(canvas);
- // glTF puts v = 0 at the top of the image and the loader uploads the model's
- // own textures with flipY = false; matching that keeps the artwork aligned
- // with the mesh UVs.
- texture.flipY = false;
- texture.colorSpace = THREE.SRGBColorSpace;
- texture.premultiplyAlpha = false;
- texture.needsUpdate = true;
- return clampToEdge(texture);
-}
-
-/**
- * The face plates are shells of the whole body, so their UVs run well past the
- * 0..1 of the artwork (a plate corner can sit at v = -1.26). glTF's default
- * sampler repeats, which would stamp the drawing back onto the model several
- * times - a frown flicked up onto the forehead, a smile's end onto its side.
- * Clamping sends everything outside the artwork to the blank edge of the canvas
- * instead, where nothing is drawn.
- */
-function clampToEdge(texture) {
- if (!texture) return texture;
- texture.wrapS = THREE.ClampToEdgeWrapping;
- texture.wrapT = THREE.ClampToEdgeWrapping;
- texture.needsUpdate = true;
- return texture;
-}
-
-/**
- * Make the flat colour that fills the background of a hand-drawn texture
- * transparent. The edge is feathered rather than hard, so the anti-aliased
- * pixels along the artwork do not leave a pale fringe.
- */
-function keyOutBackground(texture, layout) {
- const image = texture?.image;
- if (!image || !image.width || !image.height) return texture;
-
- const canvas = document.createElement('canvas');
- canvas.width = image.width;
- canvas.height = image.height;
- const ctx = canvas.getContext('2d', { willReadFrequently: true });
- ctx.drawImage(image, 0, 0);
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const pixels = imageData.data;
-
- // The background is uniform, so the top-left pixel is a reliable sample.
- const baseR = pixels[0];
- const baseG = pixels[1];
- const baseB = pixels[2];
- const fullyClear = 30; // at or below this distance: transparent
- const fullySolid = 96; // at or above this distance: untouched
-
- for (let i = 0; i < pixels.length; i += 4) {
- const distance = Math.abs(pixels[i] - baseR)
- + Math.abs(pixels[i + 1] - baseG)
- + Math.abs(pixels[i + 2] - baseB);
- if (distance <= fullyClear) {
- pixels[i + 3] = 0;
- } else if (distance < fullySolid) {
- const ratio = (distance - fullyClear) / (fullySolid - fullyClear);
- pixels[i + 3] = Math.round(pixels[i + 3] * ratio);
- }
- }
- ctx.putImageData(imageData, 0, 0);
-
- return makeTexture(placeInWindow(canvas, layout));
-}
diff --git a/bluebey-studio/src/faceArt.js b/bluebey-studio/src/faceArt.js
deleted file mode 100644
index 4aaa2d6..0000000
--- a/bluebey-studio/src/faceArt.js
+++ /dev/null
@@ -1,1283 +0,0 @@
-/**
- * The face artwork: everything that used to be a hand-drawn PNG in GIMP is now
- * drawn from parameters onto a canvas, which is then used as the texture of the
- * two overlay planes that already exist on the model.
- *
- * The plates have a linear UV mapping fitted to the original artwork, so drawing
- * in "texture pixels" lands exactly where the original artwork did. Every constant
- * below was measured from the original textures (see README for the numbers), so
- * the defaults reproduce the 2022 artwork while every part of it stays editable.
- *
- * Texture space: the artwork WINDOW, 1024 x 380, origin at the top-left, y grows
- * downwards. `canvasLayout` then says where that window sits on the bigger canvas
- * a face plate actually needs (see below), and `Face` hands the bounds back as
- * `limits`, so the drawing can clamp against the real edge instead of assuming
- * the window is all there is.
- *
- * glTF stores v downwards too, and the textures are uploaded with flipY = false,
- * so no flipping is needed anywhere.
- */
-
-const TAU = Math.PI * 2;
-const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
-const HUGE = 4000;
-
-/**
- * The artwork window: the rectangle the 2022 hand-drawn textures occupied. Every
- * constant below is written in this space.
- */
-export const ART_WINDOW = { width: 1024, height: 380 };
-
-/** Keep this much clear at the canvas border, so filtering has room to breathe. */
-const EDGE_MARGIN = 8;
-
-/** What a drawing falls back to when no plate bounds are supplied. */
-const WINDOW_LIMITS = {
- top: 0, bottom: ART_WINDOW.height, left: 0, right: ART_WINDOW.width,
-};
-
-/**
- * The canvas a face plate's drawing needs, and where the artwork window sits on it.
- *
- * WHY: the plates are the *whole* front half of the body, so their UVs run far
- * outside the 0..1 the artwork was drawn in (measured on the shipped model: the
- * eye plate covers v -1.02..1.93, and 9 of 10 of its vertices sit within a
- * twentieth of the border). Everything outside 0..1 used to clamp to the canvas
- * edge row, so the moment a drawing reached the border - a tear dropped low, a
- * brow lifted, a thick mouth pushed down - that edge row was copied across the
- * rest of the plate and the mark stretched into a long smear.
- *
- * `Face` rewrites each plate's UVs so this range maps onto 0..1 instead, which
- * removes the clamp altogether. The map is affine, so the artwork keeps its exact
- * pixel size and position and merely shifts by `offset`; the canvas grows by the
- * same factor, and those extra rows are the room a moved tear, brow or mouth
- * needs (`limits` says where the real edge now is, in window coordinates).
- *
- * @param {{u0:number,u1:number,v0:number,v1:number}} range the plate's UV range
- * @param {{width:number,height:number}} [window] the artwork window
- */
-export function canvasLayout(range, window = ART_WINDOW) {
- const u0 = Number.isFinite(range?.u0) ? range.u0 : 0;
- const u1 = Number.isFinite(range?.u1) ? range.u1 : 1;
- const v0 = Number.isFinite(range?.v0) ? range.v0 : 0;
- const v1 = Number.isFinite(range?.v1) ? range.v1 : 1;
- const du = Math.max(1e-6, u1 - u0);
- const dv = Math.max(1e-6, v1 - v0);
- const width = Math.max(1, Math.round(window.width * du));
- const height = Math.max(1, Math.round(window.height * dv));
- // `+ 0` turns a `-0` into a plain `0`, so the offsets compare cleanly.
- const offsetX = Math.round(-u0 * window.width) + 0;
- const offsetY = Math.round(-v0 * window.height) + 0;
- return {
- width,
- height,
- offsetX,
- offsetY,
- window,
- // The canvas edges in the artwork's own coordinates. `top` is negative when
- // the plate reaches above the window: that is the extra room.
- limits: {
- top: -offsetY + 0,
- bottom: height - offsetY,
- left: -offsetX + 0,
- right: width - offsetX,
- },
- };
-}
-
-/** Fixed layout of the eye plane, measured from the original `eyes-open.png`. */
-export const EYE_LAYOUT = {
- radius: 84,
- irisRadius: 62.5,
- highlightRadius: 28,
- highlightOffset: { x: 29.5, y: -18 },
- // The model's left eye (its own left, +x) sits in the u > 0.5 half.
- eyes: [
- { key: 'right', cx: 267.5, cy: 240, towardNose: 1 },
- { key: 'left', cx: 755.5, cy: 240, towardNose: -1 },
- ],
- lidRadiusFactor: 1.35,
- lidStroke: 14,
- // A teardrop hangs well below the eye. The plate keeps the whole canvas, so
- // the drop can sit low without being clipped (measured with a ruler grid).
- tear: { size: 34, offsetX: 66, offsetY: 88 },
- closedLine: {
- // The single shut line is the *long* form of the eyelid, the same reach as
- // the arms of the "ぎゅっ" below, so switching between 1 and 3 lines does not
- // change how wide the eye reads.
- length: 205,
- offsetX: 6,
- offsetY: -10,
- slantDeg: 3.9,
- bow: 3,
- // 2- and 3-line shut eyes, copied from the original `eyes-close-tight`
- // artwork: three strokes sharing one vertex that points at the nose, opening
- // to a wide bird's foot / arrow shape. `spread` is half the height the arms
- // open to at the far end.
- armSpan: 205,
- spread: 62,
- vertex: 87.5,
- armX: 104,
- },
- arch: { edgesUp: 8, apexUp: 44 },
- // まつげ: a few strokes flicking out from the upper-outer lid (see drawEyes).
- // `raise` lifts them a touch above the lid edge (2% of the lid slider).
- lash: { length: 30, width: 8, angles: [46, 66, 86], raise: 0.04 },
- // "ふつう"の目の瞳を、両目とも少し鼻側に寄せてかわいく見せる(px)。
- irisInward: 7,
-};
-
-
-/**
- * 眼鏡 / サングラス, drawn into the same texture as the eyes and brows.
- *
- * Not measured off any artwork - the character has no glasses - but the numbers
- * are tied to the eyeball so a lens is always a little wider than the eye it
- * covers.
- *
- * The two kinds get separate shapes on purpose. They used to share one ellipse
- * and differ only in how dark the lens was filled, which made them hard to tell
- * apart; the round 眼鏡 below and the wide, angular サングラス in
- * `SUNGLASSES_LAYOUT` now read as different objects at a glance.
- */
-const GLASSES_LAYOUT = {
- widthFactor: 1.08, // lens half-width, as a multiple of the eyeball radius
- heightFactor: 1.0, // a circle, so the round 眼鏡 stays round (a flatter
- // ellipse used to drift towards the shades below)
- drop: 20, // the lens centre hangs this far below the eye centre (artwork px)
- bridgeRise: 8, // how much the bridge arcs up over the nose
- bridgeWidth: 0.75, // bridge thickness, as a fraction of the frame stroke
- templeLength: 1.5, // temple stub length, as a multiple of the eyeball radius
- templeRise: 30, // how far the temple climbs towards the side of the head
-};
-
-/**
- * サングラス: a long, pointed cat-eye rather than the round 眼鏡 lens. Each side is a
- * slim wedge whose *outer end starts low*, rises to a *high, pointed outer corner*
- * set further out (`tipX`), and whose top edge then sweeps back down towards the
- * nose - the "外側が長くとがって上に上がる" shape the reference shows. The inner end is
- * short, so the lens tapers inwards. The eye is allowed to poke out above and
- * below it, so the lens no longer has to cover the whole eyeball. A level bar
- * joins the two inner ends, so the pair still reads as one dark visor. The ratios
- * are of the eyeball radius, exactly like `GLASSES_LAYOUT`, so `scale` means the
- * same thing for both kinds.
- */
-const SUNGLASSES_LAYOUT = {
- widthFactor: 1.6, // a long lens: the outer end reaches well past the eyeball
- heightFactor: 0.58, // slim, so the eye is free to show above and below it
- drop: 8, // sits a little lower than the round pair
- corner: 0.05, // corner rounding, as a fraction of the half-height; kept
- // tiny so the outer corner stays pointed
- topSkew: 1.0, // the pointed corner rises this far above the lens top (x half-height)
- innerTop: 0.42, // the inner top is low, which is what makes the top edge climb
- innerBottom: 0.34, // the inner bottom is pinched up towards the nose (x half-height)
- outerEndX: 0.75, // the low outer end sits this far out (x half-width)
- outerBottom: 0.82, // ...and this deep (x half-height)
- tipX: 1.22, // the pointed corner juts this far out (x half-width)
- bridgeWidth: 1.15, // a short, thick bar - thicker than the frame stroke
- bridgeLift: 0.25, // the bar sits this far above the lens centre (x half-height)
- templeLength: 1.15, // a short stub, like the round pair's but a little shorter
- templeRise: 24,
- // A heavy rim. It used to be slimmer than the round pair's stroke, which read
- // as a thin pair of shades; the reference look is a chunky frame.
- frameFactor: 1.6,
-};
-
-/** The mouth artwork is drawn on the `mouth-plate` shell, which covers the whole
- * 1024 x 380 canvas (see tools/build-face-plates.py), so the drawing no longer
- * has to be squeezed into a band. The only limit left is the canvas itself. */
-export const MOUTH_LAYOUT = {
- centreX: 511.5,
- chordY: 88,
- halfChord: 420.5,
- sag: 177,
- // With the ends and the depth both fixed - which is what the slider promises -
- // the only thing left to choose about the curve is *where* it bends. 1/3 is a
- // quadratic Bézier (a parabola): it is flattest at the apex and falls away
- // fastest near the ends. Pulling the cubic's control points in towards the ends
- // makes the middle of the smile straighter still and lets the fall happen near
- // the corners, which is what reads as a *gentler* curve at the same depth.
- sagBend: 0.2,
- thickness: 21,
- // How big the round "O" oval gets at `round: 1`, as a multiple of the smile's
- // own sag. The oval's size comes from this and `round` alone: `thickness` is
- // only the stroke weight, so 口の太さ and 丸く開く no longer move together.
- roundScale: 1.3,
- // Kept for reference: the 2022 plane only showed these rows.
- bandTop: 46,
- bandBottom: 332,
- // Only used to cap the *size* of the round "O" mouth, so the surprise face
- // stays a mouth and not a hole. The mouth's travel no longer stops here: the
- // drawing is given the plate's real edges through `limits`.
- safeBottom: 372,
- // The nose is a separate mesh that pokes out in front of the plate. The plate
- // is depth tested, so the nose hides whatever is drawn behind it - but a frown
- // arcs *up* into that hiding place, so slide the mouth down until the middle
- // of the arc clears the nose. Only the middle is checked: a smile curves away
- // from the nose, and checking its ends instead is what used to pin the whole
- // mouth in place and make the height slider do nothing.
- noseClear: 132,
- // The original hand-drawn mouth is a *shallow* arc with tall corner strokes
- // flicking up at the ends (measured from the artwork: the arc's own sag is
- // ~0.13 of its chord, while the corners reach ~90px above it). Keeping that
- // split is what makes `smile: 1` read as the original; making the arc itself
- // deep instead looked too steep.
- //
- // The tongue rises from the lip line to a rounded top just above the corner
- // strokes. Measured off the original: its crown is a *super-ellipse* -
- // `rise = height * (1 - |dx/half|^2.5)` - so it is steep-sided with a smooth,
- // almost flat top. It is convex upwards, but it is **not** a point.
- tongue: { pos: 0.84, width: 126, height: 115, crown: 2.5 },
- corner: { fromX: 26, fromY: 3, toX: 32, toY: 42, width: 11, curve: 26 },
- openRise: 64,
- // Was 0.25: opening the mouth used to flatten the smile to stay inside the
- // texture band. There is no band to stay inside now.
- openFlatten: 0,
-};
-
-/* ------------------------------------------------------------------ helpers */
-
-function circlePath(ctx, cx, cy, r) {
- ctx.beginPath();
- ctx.arc(cx, cy, r, 0, TAU);
- ctx.closePath();
-}
-
-function fillCircle(ctx, cx, cy, r, color) {
- circlePath(ctx, cx, cy, r);
- ctx.fillStyle = color;
- ctx.fill();
-}
-
-/**
- * A heart, used for the "love" eyes. The path is wider than it is tall, which
- * is what makes it read as a heart rather than a blob at small sizes.
- */
-function heartPath(ctx, cx, cy, r) {
- ctx.beginPath();
- ctx.moveTo(cx, cy + r * 0.80);
- ctx.bezierCurveTo(cx - r * 1.24, cy - r * 0.34, cx - r * 0.50, cy - r * 1.18, cx, cy - r * 0.40);
- ctx.bezierCurveTo(cx + r * 0.50, cy - r * 1.18, cx + r * 1.24, cy - r * 0.34, cx, cy + r * 0.80);
- ctx.closePath();
-}
-
-function fillHeart(ctx, cx, cy, r, color) {
- heartPath(ctx, cx, cy, r);
- ctx.fillStyle = color;
- ctx.fill();
-}
-
-/** A teardrop, used by the crying expression. */
-function dropPath(ctx, cx, cy, size) {
- ctx.beginPath();
- ctx.moveTo(cx, cy - size * 1.32);
- ctx.bezierCurveTo(cx + size * 0.95, cy - size * 0.34, cx + size * 0.95, cy + size * 0.78, cx, cy + size * 0.78);
- ctx.bezierCurveTo(cx - size * 0.95, cy + size * 0.78, cx - size * 0.95, cy - size * 0.34, cx, cy - size * 1.32);
- ctx.closePath();
-}
-
-function strokeCircle(ctx, cx, cy, r, color, width) {
- circlePath(ctx, cx, cy, r);
- ctx.strokeStyle = color;
- ctx.lineWidth = width;
- ctx.stroke();
-}
-
-function tracePolyline(ctx, points, closed) {
- if (!points.length) return;
- ctx.beginPath();
- ctx.moveTo(points[0].x, points[0].y);
- for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
- if (closed) ctx.closePath();
-}
-
-function strokePolyline(ctx, points, { color, width, closed = false }) {
- if (points.length < 2) return;
- ctx.save();
- ctx.strokeStyle = color;
- ctx.lineWidth = width;
- ctx.lineJoin = 'round';
- ctx.lineCap = 'round';
- tracePolyline(ctx, points, closed);
- ctx.stroke();
- ctx.restore();
-}
-
-function fillPolygon(ctx, points, color) {
- if (points.length < 3) return;
- ctx.save();
- ctx.fillStyle = color;
- tracePolyline(ctx, points, true);
- ctx.fill();
- ctx.restore();
-}
-
-/**
- * Trace a closed polygon with rounded corners. Used for the squarish
- * サングラス lens: an ellipse cannot be angular, and a plain polygon has cusps.
- * Each corner is cut back by `radius` (clamped so short edges cannot overlap)
- * and joined with a quadratic through the original vertex.
- */
-function traceRoundedPolygon(ctx, points, radius) {
- const n = points.length;
- ctx.beginPath();
- for (let i = 0; i < n; i++) {
- const prev = points[(i + n - 1) % n];
- const cur = points[i];
- const next = points[(i + 1) % n];
- const inLen = Math.hypot(cur.x - prev.x, cur.y - prev.y) || 1;
- const outLen = Math.hypot(next.x - cur.x, next.y - cur.y) || 1;
- const cut = Math.min(radius, inLen / 2, outLen / 2);
- const from = { x: cur.x + ((prev.x - cur.x) / inLen) * cut, y: cur.y + ((prev.y - cur.y) / inLen) * cut };
- const to = { x: cur.x + ((next.x - cur.x) / outLen) * cut, y: cur.y + ((next.y - cur.y) / outLen) * cut };
- if (i === 0) ctx.moveTo(from.x, from.y);
- else ctx.lineTo(from.x, from.y);
- ctx.quadraticCurveTo(cur.x, cur.y, to.x, to.y);
- }
- ctx.closePath();
-}
-
-/** Points along a quadratic Bézier, `steps` segments (steps + 1 points). */
-function quadraticPoints(p0, p1, p2, steps) {
- const out = [];
- for (let i = 0; i <= steps; i++) {
- const t = i / steps;
- const u = 1 - t;
- out.push({
- x: u * u * p0.x + 2 * u * t * p1.x + t * t * p2.x,
- y: u * u * p0.y + 2 * u * t * p1.y + t * t * p2.y,
- });
- }
- return out;
-}
-
-/** A point on a cubic Bézier. */
-function cubicPoint(p0, p1, p2, p3, t) {
- const u = 1 - t;
- const a = u * u * u;
- const b = 3 * u * u * t;
- const c = 3 * u * t * t;
- const d = t * t * t;
- return {
- x: a * p0.x + b * p1.x + c * p2.x + d * p3.x,
- y: a * p0.y + b * p1.y + c * p2.y + d * p3.y,
- };
-}
-
-/** Points along a cubic Bézier, `steps` segments (steps + 1 points). */
-function cubicPoints(p0, p1, p2, p3, steps) {
- const out = [];
- for (let i = 0; i <= steps; i++) out.push(cubicPoint(p0, p1, p2, p3, i / steps));
- return out;
-}
-
-/**
- * The four control points of the mouth's centreline: the same two ends, the same
- * depth, but `bend` decides where the curve actually bends (see `sagBend`).
- */
-function mouthControls(left, right, sag, bend) {
- const width = right.x - left.x;
- const y = left.y + (4 / 3) * sag;
- return {
- p0: left,
- p1: { x: left.x + width * bend, y },
- p2: { x: right.x - width * bend, y },
- p3: right,
- };
-}
-
-function rotate(points, pivot, degrees) {
- if (!degrees) return points;
- const a = (degrees * Math.PI) / 180;
- const cos = Math.cos(a);
- const sin = Math.sin(a);
- return points.map((p) => {
- const dx = p.x - pivot.x;
- const dy = p.y - pivot.y;
- return { x: pivot.x + dx * cos - dy * sin, y: pivot.y + dx * sin + dy * cos };
- });
-}
-
-/* --------------------------------------------------------------------- eyes */
-
-/**
- * Intersect the canvas clip with the eyeball disc and the (possibly closed)
- * lids. Because the overlay plane is transparent and sits in front of the face,
- * clipping is all that is needed: whatever is clipped away shows the real
- * shaded face behind it. That is what makes the eyelid blend perfectly without
- * baking a face-coloured background into the texture.
- */
-/** Clip to the upper and lower lids only, without the eyeball circle. */
-function clipLids(ctx, eye, open, lowerLid, flat = false, tiltDeg = 0) {
- const tilt = (clamp(tiltDeg, -45, 45) * Math.PI) / 180;
- const rotated = Math.abs(tilt) > 1e-4;
- const spin = () => {
- ctx.translate(eye.cx, eye.cy);
- ctx.rotate(tilt);
- ctx.translate(-eye.cx, -eye.cy);
- };
- if (rotated) spin();
-
- const lower = clamp(lowerLid, 0, 1);
- // The upper and lower lids are independent: 上まぶたの高さ sets where the upper
- // lid sits on its own, so raising 下まぶたの高さ does not drag the upper lid
- // down with it. A blink still shuts the eye because `open` reaching 0 (or the
- // upper lid dropping to the lower one) hands over to the shut-eye drawing
- // before this clip is used.
- const upper = 1 - clamp(open, 0, 1);
- if (upper > 0.0005) {
- const rl = eye.r * EYE_LAYOUT.lidRadiusFactor;
- const lowest = eye.cy - eye.r + 2 * eye.r * upper;
- ctx.beginPath();
- if (flat) {
- // A flat lid: a straight edge straight across the eye.
- ctx.rect(eye.cx - HUGE, lowest, HUGE * 2, HUGE);
- } else {
- const cy = lowest - rl;
- ctx.moveTo(eye.cx - HUGE, cy);
- ctx.lineTo(eye.cx - rl, cy);
- ctx.arc(eye.cx, cy, rl, Math.PI, 0, true); // lower semicircle: bulges down
- ctx.lineTo(eye.cx + HUGE, cy);
- ctx.lineTo(eye.cx + HUGE, cy + HUGE);
- ctx.lineTo(eye.cx - HUGE, cy + HUGE);
- }
- ctx.closePath();
- ctx.clip();
- }
-
- const bottom = lower;
- if (bottom > 0.0005) {
- const rl = eye.r * EYE_LAYOUT.lidRadiusFactor;
- const highest = eye.cy + eye.r - 2 * eye.r * bottom;
- ctx.beginPath();
- if (flat) {
- ctx.rect(eye.cx - HUGE, highest - HUGE, HUGE * 2, HUGE);
- } else {
- const cy = highest + rl;
- ctx.moveTo(eye.cx - HUGE, cy);
- ctx.lineTo(eye.cx - rl, cy);
- ctx.arc(eye.cx, cy, rl, Math.PI, 0, false); // upper semicircle: bulges up
- ctx.lineTo(eye.cx + HUGE, cy);
- ctx.lineTo(eye.cx + HUGE, cy - HUGE);
- ctx.lineTo(eye.cx - HUGE, cy - HUGE);
- }
- ctx.closePath();
- ctx.clip();
- }
-
- if (rotated) {
- // Undo the rotation for the caller, but keep the clip we just set.
- ctx.translate(eye.cx, eye.cy);
- ctx.rotate(-tilt);
- ctx.translate(-eye.cx, -eye.cy);
- }
-}
-
-/** Clip to the eyeball circle as well (the iris and heart are eyeball content). */
-function clipEye(ctx, eye, open, lowerLid, flat = false, tiltDeg = 0) {
- circlePath(ctx, eye.cx, eye.cy, eye.r);
- ctx.clip();
- clipLids(ctx, eye, open, lowerLid, flat, tiltDeg);
-}
-
-function lidPath(ctx, eye, amount, lower, flat = false) {
- ctx.beginPath();
- if (flat) {
- // A straight lid edge; the caller clips it to the eyeball.
- const y = lower
- ? eye.cy + eye.r - 2 * eye.r * amount
- : eye.cy - eye.r + 2 * eye.r * (1 - amount);
- ctx.moveTo(eye.cx - eye.r * 2, y);
- ctx.lineTo(eye.cx + eye.r * 2, y);
- return;
- }
- const rl = eye.r * EYE_LAYOUT.lidRadiusFactor;
- if (!lower) {
- const lowest = eye.cy - eye.r + 2 * eye.r * (1 - amount);
- ctx.arc(eye.cx, lowest - rl, rl, Math.PI, 0, true);
- } else {
- const highest = eye.cy + eye.r - 2 * eye.r * amount;
- ctx.arc(eye.cx, highest + rl, rl, Math.PI, 0, false);
- }
-}
-
-/** The "eyes shut" artwork: a line, a chevron or a happy arch. */
-function drawShutEye(ctx, eye, spec, style, shapeScale = 1, maxHalf = Infinity) {
- const layout = EYE_LAYOUT.closedLine;
- const { line, width } = style;
- // 形の大きさ is a multiplier on the shape's *fitting* size. With the white shown
- // that fitting size is what just fits inside the eyeball, so the default (1)
- // sits in the white; raising it may push the shape out past the white, which is
- // allowed. `maxHalf` is Infinity when the white is hidden, so nothing is scaled
- // back there.
- const k = (unitHalf) => shapeScale * (maxHalf === Infinity ? 1 : Math.min(1, maxHalf / unitHalf));
-
- if (spec.closed === 'chevron') {
- // The chevron points *at* the nose, matching the original artwork.
- const scale = k(layout.armX);
- const vertex = { x: eye.cx + eye.towardNose * layout.vertex * scale, y: eye.cy + layout.offsetY * scale - 8 * scale };
- const armX = eye.cx - eye.towardNose * layout.armX * scale;
- const spread = layout.spread * (spec.spread ?? 1) * scale;
- strokePolyline(ctx, [vertex, { x: armX, y: vertex.y - spread }], { color: line, width });
- strokePolyline(ctx, [vertex, { x: armX, y: vertex.y + spread }], { color: line, width });
- return;
- }
-
- if (spec.closed === 'three') {
- // A real "3": an upper bowl and a lower bowl that meet at a single pinch on
- // the nose side. Stacking two C's instead made the two lobes sit on top of
- // each other; joining them at a point is what makes it read as the digit.
- const r = 34 * k(1.7 * 34);
- // Which way the digit faces. It used to follow the nose side, which made the
- // pair a mirror image; each eye can now be set either way, because a pair of
- // mirrored 3s does not always read the way you want.
- const dir = (spec.threeFlip ? -1 : 1) * eye.towardNose;
- const sx = eye.cx;
- const cy = eye.cy + layout.offsetY;
- const at = (x, y) => ({ x: sx + dir * r * x, y: cy + r * y });
- const pinch = at(0.72, 0);
- const upper = quadraticPoints(at(0.02, -1.62), at(2.05, -1.40), pinch, 26);
- const lower = quadraticPoints(pinch, at(2.05, 1.46), at(0.02, 1.78), 26);
- strokePolyline(ctx, upper, { color: line, width });
- strokePolyline(ctx, lower, { color: line, width });
- return;
- }
-
- if (spec.closed === 'arch') {
- const scale = k(eye.r);
- const edges = { x: eye.r * scale, y: EYE_LAYOUT.arch.edgesUp * scale };
- const points = quadraticPoints(
- { x: eye.cx - edges.x, y: eye.cy + edges.y },
- { x: eye.cx, y: eye.cy + edges.y - EYE_LAYOUT.arch.apexUp * 2 * scale },
- { x: eye.cx + edges.x, y: eye.cy + edges.y },
- 24,
- );
- strokePolyline(ctx, points, { color: line, width });
- return;
- }
-
- // Default: the gently slanted line of the original artwork, mirrored so it
- // always slopes down towards the nose. With `closedLines` set to 2 or 3 the
- // strokes share one endpoint instead - a ">" or bird's-foot shape, which is
- // how manga draws a happily squeezed-shut eye.
- const count = clamp(Math.round(spec.closedLines ?? 1), 1, 3);
- const scale = k(layout.length / 2);
- if (count >= 2) {
- const vertex = { x: eye.cx + eye.towardNose * layout.vertex * scale, y: eye.cy + layout.offsetY * scale - 8 * scale };
- // Reach as far as the single line does, not just a short stub: the arms are
- // the *long* form of the same eyelid the one-line version covers.
- const outX = -eye.towardNose * layout.armSpan * scale;
- const spread = layout.spread * scale;
- const arms = count === 2
- ? [{ x: outX, y: -spread }, { x: outX, y: spread }]
- : [
- { x: outX, y: -spread },
- { x: -eye.towardNose * Math.hypot(layout.armSpan, layout.spread) * scale, y: 0 },
- { x: outX, y: spread },
- ];
- for (const arm of arms) {
- strokePolyline(ctx, [vertex, { x: vertex.x + arm.x, y: vertex.y + arm.y }], { color: line, width });
- }
- return;
- }
-
- const half = (layout.length / 2) * (spec.length ?? 1) * scale;
- const midX = eye.cx - eye.towardNose * layout.offsetX * scale;
- const midY = eye.cy + layout.offsetY * scale;
- const angle = ((layout.slantDeg * (spec.slant ?? 1) * eye.towardNose) * Math.PI) / 180;
- const bow = (layout.bow ?? 0) * (spec.slant ?? 1) * scale;
- const from = { x: midX - Math.cos(angle) * half, y: midY - Math.sin(angle) * half };
- const to = { x: midX + Math.cos(angle) * half, y: midY + Math.sin(angle) * half };
- const points = quadraticPoints(from, { x: midX, y: midY + bow * 2 }, to, 20);
- strokePolyline(ctx, points, { color: line, width });
-}
-
-
-
-
-
-
-/**
- * One lens of a pair of 眼鏡 / サングラス, plus this eye's half of the bridge and its
- * temple.
- *
- * Called once per eye from `drawEyes`, after the eyeball (or the shut-eye artwork)
- * so the lens sits in front of the eye, and before that eye's tear so a teardrop
- * falls past the lens. Nothing about a pair of glasses is per-eye, so the two
- * halves are mirrored from `eye.towardNose` and meet at the face's midline. The
- * kind picks the shape: a round 眼鏡 lens, or a wide angular サングラス one.
- *
- * `style.glasses` is the shared spec. In a line drawing the lens is left empty, so
- * the paper behind shows through exactly as the eyeball's white does (see the note
- * in `drawEyes`); the frames are then stroked in the line colour.
- *
- * @param {CanvasRenderingContext2D} ctx
- * @param {{cx:number,cy:number,r:number,towardNose:number}} eye
- * @param {boolean} line true in 線画 mode
- * @param {object} style the bag `drawEyes` received
- * @param {{top:number,bottom:number,left:number,right:number}} limits
- */
-function drawGlasses(ctx, eye, line, style, limits) {
- const spec = style.glasses;
- if (!spec?.enabled) return;
-
- const r = eye.r;
- const scale = clamp(spec.scale ?? 1, 0.2, 3);
- // The two kinds share everything about *where* they sit but not their shape:
- // 眼鏡 is a round lens, サングラス a wide angular one (see the layouts above).
- const shades = spec.kind === 'sunglasses';
- const L = shades ? SUNGLASSES_LAYOUT : GLASSES_LAYOUT;
- const rx = r * L.widthFactor * scale;
- const ry = rx * L.heightFactor;
- const frame = Math.max(
- 1,
- (spec.frameWidth ?? 1) * EYE_LAYOUT.lidStroke * (shades ? L.frameFactor : 1),
- );
- const colour = line ? (style.line ?? '#55386e') : (spec.frameColor ?? '#2a1e33');
- // The outward direction (towards the temple) for this eye. The angular lens is
- // built in a local frame whose u runs outwards, so the two sides mirror exactly.
- const out = -eye.towardNose;
- // The pointed outer corner makes the shades taller on that side; clamp against
- // the full height so even that point cannot reach the canvas border.
- const halfV = shades ? ry * Math.max(L.innerTop + L.topSkew, L.outerBottom) : ry;
-
- // The face's midline: the two eyes are symmetric about it, so it is where the
- // two halves of the bridge meet and what `tilt` turns the pair about.
- const midX = (EYE_LAYOUT.eyes[0].cx + EYE_LAYOUT.eyes[1].cx) / 2;
- // `lensGap` slides the two lenses apart (positive) or together (negative) along
- // the face, symmetrically about the midline. It is clamped so the inner edges may
- // meet at the midline but the two lenses can never cross each other.
- const inwardRoom = Math.max(0, Math.abs(midX - eye.cx) - rx);
- const gap = clamp(spec.lensGap ?? 0, -inwardRoom, HUGE);
- // Keep the lens (and so everything that hangs off it) inside the artwork. The
- // rest of the file clamps against `limits` for the same reason: a mark that
- // reaches the canvas border gets the edge row copied across the whole plate.
- const lensCx = clamp(
- eye.cx + out * gap,
- limits.left + EDGE_MARGIN + rx,
- limits.right - EDGE_MARGIN - rx,
- );
- const lensCy = clamp(
- eye.cy + L.drop + (spec.offsetY ?? 0),
- limits.top + EDGE_MARGIN + halfV,
- limits.bottom - EDGE_MARGIN - halfV,
- );
- // Local (u, v) -> canvas, with u outwards and v downwards.
- const at = (u, v) => ({ x: lensCx + out * u, y: lensCy + v });
-
- ctx.save();
- // `tilt` leans the whole pair at once. Rotating about the midline keeps the
- // bridge centred between the lenses instead of swinging it off to one side.
- if (spec.tilt) {
- ctx.translate(midX, lensCy);
- ctx.rotate((clamp(spec.tilt, -90, 90) * Math.PI) / 180);
- ctx.translate(-midX, -lensCy);
- }
-
- // --- lens ------------------------------------------------------------
- // The one path is filled and then stroked, so the fill and the frame can never
- // drift apart.
- if (shades) {
- // A long cat-eye, not an ellipse: the outer end starts low, rises to a
- // pointed, lifted outer corner set further out, and the top edge sweeps back
- // towards the nose. The inner end is short, so the lens tapers inwards.
- const lens = [
- at(-rx, -ry * L.innerTop), // inner top (near the nose)
- at(rx * L.tipX, -ry * (L.innerTop + L.topSkew)), // pointed, lifted outer corner
- at(rx * L.outerEndX, ry * L.outerBottom), // low outer end
- at(-rx, ry * L.innerBottom), // inner bottom (pinched towards the nose)
- ];
- traceRoundedPolygon(ctx, lens, ry * L.corner);
- if (!line) {
- ctx.globalAlpha = clamp(spec.lensOpacity ?? 0, 0, 1);
- ctx.fillStyle = spec.lensColor ?? '#2b2433';
- ctx.fill();
- ctx.globalAlpha = 1;
- }
- ctx.strokeStyle = colour;
- ctx.lineWidth = frame;
- ctx.stroke();
- } else {
- ctx.beginPath();
- ctx.ellipse(lensCx, lensCy, rx, ry, 0, 0, TAU);
- ctx.closePath();
- if (!line) {
- ctx.globalAlpha = clamp(spec.lensOpacity ?? 0, 0, 1);
- ctx.fillStyle = spec.lensColor ?? '#2b2433';
- ctx.fill();
- ctx.globalAlpha = 1;
- }
- ctx.strokeStyle = colour;
- ctx.lineWidth = frame;
- ctx.stroke();
- }
-
- // --- bridge ----------------------------------------------------------
- // Each eye draws the half of the bridge from its own lens to the midline.
- const innerX = eye.towardNose > 0
- ? Math.min(lensCx + rx, midX)
- : Math.max(lensCx - rx, midX);
- if (shades) {
- // A short, thick, level bar high on the lenses: with the two lenses it reads
- // as one continuous visor across the eyes.
- const barY = lensCy - ry * L.bridgeLift;
- strokePolyline(ctx, [{ x: innerX, y: barY }, { x: midX, y: barY }], {
- color: colour,
- width: frame * L.bridgeWidth,
- });
- } else {
- // The two halves meet at the midline with a horizontal tangent, so the join is smooth.
- const noseX = lensCy - L.bridgeRise;
- strokePolyline(ctx, quadraticPoints(
- { x: innerX, y: lensCy },
- { x: (innerX + midX) / 2, y: noseX },
- { x: midX, y: noseX },
- 12,
- ), { color: colour, width: frame * L.bridgeWidth });
- }
-
- // --- temple ----------------------------------------------------------
- // A stub outwards from the outer edge of the lens. It stays short on purpose:
- // the plate only reaches so far, and a temple that ran off it would smear. The
- // round pair's outer edge is a vertical line at the lens centre height. The
- // shades' outer end drops low, so their arm leaves from the *top* edge instead
- // - from the pointed outer corner - which is where a cat-eye's arm attaches.
- const outerX = shades
- ? lensCx - eye.towardNose * rx * L.tipX
- : lensCx - eye.towardNose * rx;
- const outerY = shades ? lensCy - ry * (L.innerTop + L.topSkew) : lensCy;
- const reach = clamp(
- outerX - eye.towardNose * r * L.templeLength,
- limits.left + EDGE_MARGIN,
- limits.right - EDGE_MARGIN,
- );
- strokePolyline(ctx, [
- { x: outerX, y: outerY },
- {
- x: eye.towardNose > 0 ? Math.min(reach, outerX) : Math.max(reach, outerX),
- y: clamp(outerY - L.templeRise, limits.top + EDGE_MARGIN, limits.bottom - EDGE_MARGIN),
- },
- ], { color: colour, width: frame });
-
- ctx.restore();
-}
-
-/**
- * Draw the pair of eyes into a 1024 x 380 canvas.
- *
- * @param {CanvasRenderingContext2D} ctx
- * @param {object} p
- * @param {'paint'|'line'} [p.mode] paint = filled cartoon eyes, line = outline only
- * @param {object} p.eyes `{ left, right }`, each `{ open, lookX, lookY, closed }`
- * @param {object} [p.style] colours and shared shaping parameters
- */
-/**
- * Which artwork an eye shows: 'open', 'heart', 'line1'..'line3', 'three' or
- * 'arch'. `shape` is the explicit choice; older saved states and the おまかせ
- * rolls only set `open`/`closed`/`irisShape`, so this falls back to those.
- */
-function eyeShapeKey(spec) {
- const shape = spec.shape;
- const explicitShut = shape === 'three' || shape === 'arch'
- || (typeof shape === 'string' && shape.startsWith('line'));
- // An explicit shut artwork is used as-is; the lid height only trims it.
- if (explicitShut) return shape;
- // A lowered lid on an open or heart eye reads as a shut line - a blink. It must
- // NOT borrow a 3 or an arch from a stale `closed`, which used to flash on screen
- // for a frame when blinking after picking 3/わらう and then switching to heart.
- if ((spec.open ?? 1) <= 0.02) {
- if (!shape) {
- // Older saved states have no `shape`; derive the shut artwork from `closed`.
- if (spec.closed === 'three') return 'three';
- if (spec.closed === 'arch') return 'arch';
- return `line${clamp(Math.round(spec.closedLines ?? 1), 1, 3)}`;
- }
- return 'line1';
- }
- if (shape) return shape;
- return (spec.irisShape ?? 'circle') === 'heart' ? 'heart' : 'open';
-}
-
-export function drawEyes(ctx, p) {
- const mode = p.mode ?? 'paint';
- const line = mode === 'line';
- const style = p.style ?? {};
- const limits = style.limits ?? WINDOW_LIMITS;
- const white = style.white ?? '#ffffff';
- const irisColor = style.iris ?? '#150e1b';
- const lineColor = style.line ?? '#55386e';
- const lookMax = style.lookMax ?? 1;
- const heartScale = 2;
- const heartColor = style.heartColor ?? '#e0344f';
- // A lens that is effectively opaque hides the eye completely, so a blink would
- // show only as a flicker of the shut artwork through the lens. While such a
- // lens is on, the eyes are drawn open and no blink is visible at all. This is a
- // rendering decision, not an animation one - the blink still runs underneath.
- const lensOpaque = style.glasses?.enabled === true
- && clamp(style.glasses.lensOpacity ?? 0, 0, 1) >= 0.99;
-
- for (const layout of EYE_LAYOUT.eyes) {
- const spec = (p.eyes ?? {})[layout.key] ?? {};
- const eye = {
- // `eyeX` slides this eye - and its lid and tear - sideways on its own.
- cx: layout.cx + clamp(spec.eyeX ?? 0, -400, 400),
- cy: layout.cy,
- r: EYE_LAYOUT.radius,
- towardNose: layout.towardNose,
- };
- // Each lid can be set per eye, falling back to the shared value.
- const lidWidth = spec.lidWidth ?? style.lidWidth ?? EYE_LAYOUT.lidStroke;
- const lidFlat = (spec.lidShape ?? style.lidShape ?? 'curve') === 'flat';
- // How much the whole narrowed eye is tilted (e.g. an angry or gentle squint).
- const lidTilt = clamp(spec.lidTilt ?? style.lidTilt ?? 0, -45, 45);
- const lineStyle = { line: lineColor, width: lidWidth };
- // Which artwork this eye shows. `shape` is the explicit choice; older saved
- // states and the おまかせ rolls only set `open`/`closed`, so fall back.
- const shapeKey = eyeShapeKey(lensOpaque ? { ...spec, open: 1 } : spec);
- const explicitShut = typeof spec.shape === 'string'
- && (spec.shape === 'three' || spec.shape === 'arch' || spec.shape.startsWith('line'));
- const isHeart = shapeKey === 'heart';
- const isShutShape = shapeKey === 'three' || shapeKey === 'arch' || shapeKey.startsWith('line');
- // The shut artwork reads `closed`/`closedLines`; build them from `shape` so a
- // line, the 3 and the arch share one path with the older fields.
- const shapeSpec = isShutShape
- ? {
- ...spec,
- closed: shapeKey === 'three' ? 'three' : (shapeKey === 'arch' ? 'arch' : 'line'),
- closedLines: shapeKey.startsWith('line') ? Number(shapeKey.slice(4)) : (spec.closedLines ?? 1),
- }
- : spec;
- // The upper lid (`open`) and the lower lid are independent, and both shape
- // every eye now: the shut artwork is drawn as the eye's content and is
- // trimmed by them. A shut eye saved the old way stored open = 0 meaning
- // "shut"; lift it or the artwork would be clipped away. An explicit shut
- // `shape` keeps its own lid height, so the lid can be lowered onto it.
- const rawOpen = spec.open ?? 1;
- const open = lensOpaque
- ? 1
- : clamp(explicitShut ? rawOpen : (rawOpen <= 0.02 ? 1 : rawOpen), 0, 1);
- const lowerLid = clamp(spec.lowerLid ?? style.lowerLid ?? 0, 0, 1);
- // 大きさ: one knob for the iris, the heart and the shut artwork.
- const shapeScale = clamp(style.irisScale ?? 1, 0.4, 2);
- const irisRadius = EYE_LAYOUT.irisRadius * shapeScale;
- // `white` is tri-state: only the plain open eye shows it by default.
- const showWhite = spec.white != null ? spec.white === true : shapeKey === 'open';
- // While the white is shown, a shape that would poke out of the eyeball is
- // scaled back inside it; `shapeHalfLimit` is the half-size it may reach.
- const shapeHalfLimit = showWhite ? eye.r * 0.92 : Infinity;
-
- // --- tear ----------------------------------------------------------
- // A teardrop hangs below the eye, so it is drawn on top of everything else
- // (the opaque eyeball would otherwise cover it) and for a shut eye too. The
- // amount, the height and the tilt are all *per eye*, because one eye crying
- // while the other does not is a real expression.
- const drawTear = () => {
- if (spec.tearOn !== true) return;
- const amount = clamp(spec.tear ?? 0.3, 0, 1.6);
- if (amount <= 0.01) return;
- const size = EYE_LAYOUT.tear.size * amount;
- const tearX = eye.cx + clamp(spec.tearX ?? 0, -400, 400) - layout.towardNose * EYE_LAYOUT.tear.offsetX;
- let tearY = eye.cy + EYE_LAYOUT.tear.offsetY + (spec.tearY ?? 0);
- // The drop hangs from `cy - 1.32r` to `cy + 0.78r`. Keep its lower tip
- // inside the artwork: a drop that reached the canvas border used to be
- // smeared down the chin by the clamp.
- const tearBottom = limits.bottom - EDGE_MARGIN;
- if (tearY + size * 0.78 > tearBottom) tearY = tearBottom - size * 0.78;
- ctx.save();
- if (spec.tearTilt) {
- ctx.translate(tearX, tearY);
- ctx.rotate((spec.tearTilt * Math.PI) / 180);
- ctx.translate(-tearX, -tearY);
- }
- dropPath(ctx, tearX, tearY, size);
- if (!line) {
- ctx.fillStyle = style.tearColor ?? '#8fd8ff';
- ctx.fill();
- }
- ctx.strokeStyle = lineColor;
- ctx.lineWidth = lidWidth * 0.55;
- ctx.stroke();
- ctx.restore();
- };
-
- // How far the gaze can push the content. A heart (or a shut line shown on the
- // white) is much bigger than the iris, so it gets a smaller travel - and with
- // the white shown the content is also clipped to the eyeball - so it never
- // pokes outside the white.
- const heartFit = isHeart && showWhite
- ? Math.min(1, shapeHalfLimit / (EYE_LAYOUT.irisRadius * heartScale))
- : 1;
- const heartRadius = Math.min(
- EYE_LAYOUT.irisRadius * heartScale * shapeScale * heartFit,
- EYE_LAYOUT.radius * 2.1,
- );
- let travelBase = irisRadius;
- if (isHeart) travelBase = heartRadius;
- const eyeTravel = Math.max(0, EYE_LAYOUT.radius - travelBase) * lookMax;
- let dx = clamp(spec.lookX ?? 0, -1, 1) * eyeTravel;
- let dy = -clamp(spec.lookY ?? 0, -1, 1) * eyeTravel;
- const dist = Math.hypot(dx, dy);
- if (dist > eyeTravel && dist > 0) {
- dx = (dx / dist) * eyeTravel;
- dy = (dy / dist) * eyeTravel;
- }
-
- // --- the eye's content, clipped by the lids -------------------------
- // Every shape is drawn the same way now: a white backing, then the artwork
- // (iris, heart or the shut line/3/arch), trimmed by the upper and lower lids.
- // The shut artwork and a heart both reach past the eyeball, so they are only
- // lid-clipped - but once the white is shown they are clipped to the eyeball
- // too, so they stay inside it. Clipping the heart by its *lids* rather than by
- // the eyeball is also what lets a raised lower lid cover it from below: the
- // heart replaces the eyeball, but it must still sit behind both lids.
- ctx.save();
- if ((isShutShape || isHeart) && !showWhite) {
- clipLids(ctx, eye, open, lowerLid, lidFlat, lidTilt * eye.towardNose);
- } else if (!isHeart || open < 0.999 || showWhite) {
- clipEye(ctx, eye, open, lowerLid, lidFlat, lidTilt * eye.towardNose);
- }
- if (!line && showWhite) fillCircle(ctx, eye.cx, eye.cy, eye.r, white);
- if (isShutShape) {
- ctx.translate(dx, dy);
- drawShutEye(ctx, eye, shapeSpec, lineStyle, shapeScale, shapeHalfLimit);
- } else if (isHeart) {
- fillHeart(ctx, eye.cx + dx, eye.cy + dy, heartRadius, line ? lineColor : heartColor);
- } else {
- const irisX = eye.cx + dx + eye.towardNose * EYE_LAYOUT.irisInward;
- const irisY = eye.cy + dy;
- fillCircle(ctx, irisX, irisY, irisRadius, line ? lineColor : irisColor);
- // 光彩 (the white glint) is per eye, falling back to the shared switch.
- const highlightOn = spec.highlight != null ? spec.highlight === true : style.highlight !== false;
- if (highlightOn) {
- const hx = irisX + layout.towardNose * EYE_LAYOUT.highlightOffset.x * shapeScale;
- const hy = irisY + EYE_LAYOUT.highlightOffset.y * shapeScale;
- const hr = EYE_LAYOUT.highlightRadius * shapeScale;
- if (line) {
- // Punch the sparkle out of the iris rather than painting it white. On
- // screen the paper behind shows through, and in the SVG export (which
- // only sees alpha) it stays a hole in the pupil.
- ctx.save();
- ctx.globalCompositeOperation = 'destination-out';
- fillCircle(ctx, hx, hy, hr, '#000000');
- ctx.restore();
- } else {
- fillCircle(ctx, hx, hy, hr, white);
- }
- }
- }
- ctx.restore();
-
- // Between the eyeball and the tear: the lens covers the eye, and a tear still
- // falls in front of it.
- drawGlasses(ctx, eye, line, style, limits);
-
- drawTear();
-
- // --- lid strokes ---------------------------------------------------
- // Faded in as the lid starts to cover the eye, so opening the eye all the
- // way leaves the clean original artwork with no extra line.
- const lidFade = clamp((0.95 - open) / 0.1, 0, 1);
- // The tilt is mirrored between the eyes, so a positive value reads the same
- // way on both (an inward, angry-looking squint).
- const eyeTilt = lidTilt * eye.towardNose;
- // The upper lid line has to sit where the clip put it - and that is set by
- // `open` alone, so it stays put when the lower lid moves.
- const upperAmt = clamp(open, 0, 1);
- // A heart is a replacement for the eyeball, not an eye behind a lid: it keeps
- // no lower-lid line, which used to cut across the heart.
- const showLowerLid = lowerLid > 0.01;
- if (lidFade > 0.01 || showLowerLid) {
- ctx.save();
- if (eyeTilt) {
- ctx.translate(eye.cx, eye.cy);
- ctx.rotate((eyeTilt * Math.PI) / 180);
- ctx.translate(-eye.cx, -eye.cy);
- }
- circlePath(ctx, eye.cx, eye.cy, eye.r);
- ctx.clip();
- ctx.globalAlpha = line ? 1 : lidFade;
- ctx.strokeStyle = lineColor;
- ctx.lineWidth = lidWidth;
- if (lidFade > 0.01) {
- lidPath(ctx, eye, upperAmt, false, lidFlat);
- ctx.stroke();
- }
- if (showLowerLid) {
- ctx.globalAlpha = line ? 1 : lowerLid;
- lidPath(ctx, eye, lowerLid, true, lidFlat);
- ctx.stroke();
- }
- ctx.restore();
- }
-
- // --- まつげ --------------------------------------------------------
- // Short strokes flicking outwards from the upper-outer lid. They sit outside
- // the eyeball, so they are drawn after the lid block and are not clipped.
- if (spec.lashes ?? style.lashes) {
- // The lashes ride the upper lid: they drop as the lid lowers (2r per unit of
- // open) and tilt with まぶたの傾き, so they stay on the lid edge. まつげの位置
- // slides them along the lid, まつげの角度 tilts the strokes themselves.
- const outDir = -eye.towardNose;
- const lidDrop = 2 * eye.r * (1 - open);
- const lashPos = clamp(spec.lashPos ?? style.lashPos ?? 0, -80, 80);
- const lashAngle = clamp(spec.lashAngle ?? style.lashAngle ?? 0, -80, 80) * outDir;
- const a = (lashAngle * Math.PI) / 180;
- const ca = Math.cos(a);
- const sa = Math.sin(a);
- ctx.save();
- if (eyeTilt) {
- ctx.translate(eye.cx, eye.cy);
- ctx.rotate((eyeTilt * Math.PI) / 180);
- ctx.translate(-eye.cx, -eye.cy);
- }
- for (const deg of EYE_LAYOUT.lash.angles) {
- const t = ((deg + lashPos) * Math.PI) / 180;
- const ux = outDir * Math.cos(t);
- const uy = -Math.sin(t);
- // The stroke points along the radial direction, tilted by まつげの角度.
- const dx = ux * ca - uy * sa;
- const dy = ux * sa + uy * ca;
- const r0 = eye.r * 0.96;
- const bx = eye.cx + ux * r0;
- const by = eye.cy + uy * r0 + lidDrop - eye.r * EYE_LAYOUT.lash.raise;
- strokePolyline(ctx, [
- { x: bx, y: by },
- { x: bx + dx * EYE_LAYOUT.lash.length, y: by + dy * EYE_LAYOUT.lash.length },
- ], { color: lineColor, width: EYE_LAYOUT.lash.width });
- }
- ctx.restore();
- }
-
- // --- outline (line-art mode only) ---------------------------------
- if (line) strokeCircle(ctx, eye.cx, eye.cy, eye.r, lineColor, lidWidth * 0.75);
- }
-}
-
-/* -------------------------------------------------------------------- mouth */
-
-/**
- * Draw the mouth into a 1024 x 380 canvas.
- *
- * @param {CanvasRenderingContext2D} ctx
- * @param {object} p
- * @param {'paint'|'line'} [p.mode]
- */
-export function drawMouth(ctx, p) {
- const mode = p.mode ?? 'paint';
- const line = mode === 'line';
- const L = MOUTH_LAYOUT;
- const limits = p.limits ?? WINDOW_LIMITS;
- // The artwork's real edges, in the artwork's own coordinates, with a margin
- // kept clear. `top` is negative when the plate reaches above the window, which
- // is the room a moved mouth has to work with.
- const edgeLeft = limits.left + EDGE_MARGIN;
- const edgeRight = limits.right - EDGE_MARGIN;
- const edgeTop = limits.top + EDGE_MARGIN;
- const edgeBottom = limits.bottom - EDGE_MARGIN;
-
- const openAmount = clamp(p.open ?? 0, 0, 1);
- const thickness = L.thickness * clamp(p.thickness ?? 1, 0.2, 3);
- const cornerAmount = clamp(p.corners ?? 1, 0, 1.6);
- // The corner strokes flick out past the ends of the chord, so the ends cannot
- // use the whole canvas: a very wide mouth used to smear sideways off the plate.
- const cornerReach = L.corner.toX * cornerAmount;
- const halfChord = Math.min(
- L.halfChord * clamp(p.width ?? 1, 0.2, 1.6),
- Math.min(L.centreX - (edgeLeft + cornerReach), (edgeRight - cornerReach) - L.centreX),
- );
-
- // A negative smile bulges the line upwards, which reads as a frown.
- const sagRaw = L.sag * clamp(p.smile ?? 1, -0.55, 1.4);
- // A frown's middle is the highest point of the mouth, and that is exactly
- // where the nose is in the way, so slide the whole mouth down until the curve
- // is in the open. The push is worked out from the *base* position and the
- // height slider is added afterwards: adding it first made the push cancel the
- // slider exactly, which is why dragging ‟口の高さ” did nothing at all on a
- // frowning mouth like むっと.
- const base = L.chordY + Math.max(0, L.noseClear - (L.chordY + sagRaw));
- let y0 = base + (p.offsetY ?? 0);
- // The smile flattens slightly as the mouth opens, so the whole mouth keeps
- // fitting inside the band the mouth plane actually shows.
- let sag = sagRaw * (1 - L.openFlatten * openAmount);
- // Keep the whole mark on the canvas. The corner strokes sit *above* the ends of
- // the chord, so the allowance is not symmetric - padding the bottom by the
- // corner's depth used to cost most of the height slider's travel.
- const cornerRise = L.corner.toY * cornerAmount + thickness;
- const bottomEdge = y0 + Math.max(0, sag) + thickness;
- if (bottomEdge > edgeBottom) y0 -= bottomEdge - edgeBottom;
- const topEdge = y0 + Math.min(0, sag) - cornerRise;
- if (topEdge < edgeTop) y0 += edgeTop - topEdge;
- const pivot = { x: L.centreX, y: y0 + sag * 0.5 };
- const tilt = p.tilt ?? 0;
-
- const strokeColor = line ? (p.line ?? '#3a2a4a') : (p.color ?? '#ff1a44');
- const innerColor = p.innerColor ?? '#4a0f1e';
- const tongueColor = line ? (p.line ?? '#3a2a4a') : (p.tongueColor ?? '#ff2d2d');
-
- const left = { x: L.centreX - halfChord, y: y0 };
- const right = { x: L.centreX + halfChord, y: y0 };
- const lip = mouthControls(left, right, sag, L.sagBend);
- const arc = cubicPoints(lip.p0, lip.p1, lip.p2, lip.p3, 96);
-
- const tongueAmount = clamp(p.tongue ?? 1, 0, 1.6);
-
- // --- a round "O" mouth (surprised, singing) ---------------------------
- const roundAmount = clamp(p.round ?? 0, 0, 1);
- if (roundAmount > 0.004) {
- let ry = L.sag * 1.05 * L.roundScale * roundAmount;
- // Keep the oval below the nose and inside the plate, but allow it to grow to
- // roughly half the head. The stroke's share is a fixed margin, not the live
- // `thickness`: otherwise 口の太さ would quietly resize the oval too.
- ry = Math.min(ry, (edgeBottom - L.thickness / 2 - L.noseClear) / 2.1);
- const rx = Math.min(ry * 1.15 * clamp(p.width ?? 1, 0.2, 1.6), L.halfChord * 1.4);
- const centreY = clamp(y0 + sag * 0.5, L.noseClear + ry * 1.02, edgeBottom - L.thickness / 2 - ry * 1.02);
- if (ry > 5) {
- ctx.save();
- ctx.translate(pivot.x, pivot.y);
- ctx.rotate((tilt * Math.PI) / 180);
- ctx.translate(-pivot.x, -pivot.y);
- ctx.beginPath();
- ctx.ellipse(L.centreX, centreY, rx, ry, 0, 0, TAU);
- if (!line) {
- ctx.fillStyle = innerColor;
- ctx.fill();
- if (tongueAmount > 0.01) {
- ctx.beginPath();
- ctx.ellipse(L.centreX, centreY + ry * 0.46, rx * 0.52, ry * 0.4, 0, 0, TAU);
- ctx.fillStyle = tongueColor;
- ctx.fill();
- }
- }
- ctx.strokeStyle = strokeColor;
- ctx.lineWidth = line ? thickness * 0.8 : thickness;
- ctx.stroke();
- ctx.restore();
- }
- return;
- }
-
- // The smile line is the upper lip. Opening the mouth drops the lower jaw
- // below it, and the room for that is limited by the texture band the mouth
- // plane shows - otherwise the jaw is silently clipped away.
- // Everything has to fit in the part of the plane that faces the camera.
- let rise = L.openRise * openAmount;
- const budget = edgeBottom - thickness / 2 - y0;
- if (sag + rise > budget) {
- rise = Math.max(0, budget - Math.min(sag, budget));
- if (sag + rise > budget) sag = Math.max(-80, budget - rise);
- }
- const jawCubic = mouthControls(left, right, sag + rise, L.sagBend);
- const jaw = cubicPoints(jawCubic.p0, jawCubic.p1, jawCubic.p2, jawCubic.p3, 96);
- const jawAt = (t) => cubicPoint(jawCubic.p0, jawCubic.p1, jawCubic.p2, jawCubic.p3, t);
- const lipAt = (t) => cubicPoint(lip.p0, lip.p1, lip.p2, lip.p3, t);
- const openShape = rotate([...arc, ...jaw.slice(1, -1).reverse()], pivot, tilt);
-
- const tonguePos = clamp(p.tonguePos ?? L.tongue.pos, 0.05, 0.95);
- const tongueWidth = L.tongue.width * tongueAmount;
- const tongueHeight = L.tongue.height * tongueAmount;
-
- /**
- * The tongue, rising from `curve` to `height` above its middle.
- *
- * `pointAt(t)` is that same curve as a function of t, so the tongue's foot can
- * sit along it without this needing to know what kind of curve it is.
- */
- function drawTongue(pointAt, curve, height) {
- const arcLength = Math.max(1, chordLength(curve));
- const dt = clamp(tongueWidth / 2 / arcLength, 0.01, 0.45);
- const t0 = clamp(tonguePos - dt, 0, 1);
- const t1 = clamp(tonguePos + dt, 0, 1);
- const base = [];
- for (let i = 0; i <= 16; i++) base.push(pointAt(t0 + ((t1 - t0) * i) / 16));
- const mid = pointAt(clamp(tonguePos, 0, 1));
- const half = tongueWidth / 2;
- // The foot runs left to right; the crown comes back right to left, so the two
- // together are already a closed ring.
- const footRight = base[base.length - 1];
- const footLeft = base[0];
- const crown = [];
- const steps = 26;
- for (let i = 0; i <= steps; i++) {
- const f = i / steps;
- const dx = 1 - 2 * f;
- const footY = footRight.y + (footLeft.y - footRight.y) * f;
- crown.push({
- x: mid.x + dx * half,
- y: footY - height * (1 - Math.pow(Math.abs(dx), L.tongue.crown)),
- });
- }
- const shaped = rotate([...base, ...crown], pivot, tilt);
- if (line) {
- strokePolyline(ctx, shaped, { color: strokeColor, width: thickness * 0.8, closed: true });
- return;
- }
- tracePolyline(ctx, shaped, true);
- ctx.fillStyle = tongueColor;
- ctx.fill();
- }
-
- if (openAmount > 0.004) {
- if (!line) fillPolygon(ctx, openShape, innerColor);
- // The tongue rises from the lower lip *into* the mouth, so it has to arrive
- // with the opening: at a hair's width the jaw curve is a sliver, and the
- // tongue used to burst straight out of it and sit on the chin.
- const tongueOpen = clamp((openAmount - 0.06) / 0.24, 0, 1);
- if (tongueAmount > 0.01 && tongueOpen > 0.01) {
- ctx.save();
- tracePolyline(ctx, openShape, true);
- ctx.clip();
- // Inside an open mouth the tongue sits on the lower jaw.
- drawTongue(jawAt, jaw, tongueHeight * tongueOpen * (1 + openAmount * 0.3));
- ctx.restore();
- }
- // One outline around the whole mouth reads as lips; the smile stroke alone
- // would leave the lower edge as a bare colour change.
- strokePolyline(ctx, openShape, { color: strokeColor, width: thickness, closed: true });
- } else {
- // Closed lips - but the tongue still pokes over the lip. That is what the
- // original artwork does (the mouth reads as a smile with the tongue showing,
- // like ペコちゃん), and it is why a plain line looked wrong there. The lip
- // line goes on afterwards, so the tongue comes out from under it.
- if (tongueAmount > 0.01) {
- drawTongue(lipAt, arc, tongueHeight);
- }
- strokePolyline(ctx, rotate(arc, pivot, tilt), { color: strokeColor, width: thickness });
- }
-
- // --- corner marks ---------------------------------------------------
- if (cornerAmount > 0.01) {
- const C = L.corner;
- const cornerAngle = p.cornerAngle ?? 0;
- for (const [end, side] of [[left, -1], [right, 1]]) {
- const from = { x: end.x + C.fromX * side * cornerAmount, y: end.y - C.fromY * cornerAmount };
- const to = { x: end.x - C.toX * side * cornerAmount, y: end.y - C.toY * cornerAmount };
- // A smooth curve, not a three-point kink. `mid` is the control point, so it
- // is pulled twice as far as the bow should reach; both corners bow the same
- // way (downwards on screen), which is what the original artwork does.
- const control = {
- x: (from.x + to.x) / 2,
- y: (from.y + to.y) / 2 + (C.curve ?? 26) * cornerAmount,
- };
- const curve = quadraticPoints(from, control, to, 16);
- // `cornerAngle` tilts the whole mark around the mouth corner, mirrored so
- // both sides move together.
- const shaped = cornerAngle ? rotate(curve, end, cornerAngle * side) : curve;
- strokePolyline(ctx, rotate(shaped, pivot, tilt), {
- color: line ? strokeColor : (p.cornerColor ?? '#725497'),
- // The *length* of the corner mark follows `corners`, but its thickness
- // does not: `corners: 0.71` is the length that matches the original, and
- // the original's stroke is the full thickness.
- width: C.width,
- });
- }
- }
-}
-
-/** Approximate length of a polyline. */
-function chordLength(points) {
- let total = 0;
- for (let i = 1; i < points.length; i++) {
- total += Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
- }
- return total;
-}
diff --git a/bluebey-studio/src/gacha.js b/bluebey-studio/src/gacha.js
deleted file mode 100644
index c44f80f..0000000
--- a/bluebey-studio/src/gacha.js
+++ /dev/null
@@ -1,481 +0,0 @@
-/**
- * Seeded "おまかせ" rolls for the face and the pose.
- *
- * The studio is a hundred sliders, and that is exactly the problem: when you sit
- * down to make an expression you reach for the same three settings every time.
- * This module is the antidote - one press and a stranger picks the numbers for
- * you, from ranges a person would actually dial in, so the result is plausible
- * rather than a mash of the full slider span.
- *
- * Two ideas hold it together:
- *
- * - A roll picks an *emotion* first (ふつう, うれしい, びっくり, ...), then samples
- * every part inside that emotion's window. Correlated parts read as one face;
- * an independent draw per slider reads as noise.
- *
- * - The randomness is seeded, and the seed is a short base36 string that fits in
- * a URL. `rollAll(seed)` is a pure function of that string, so a look the user
- * liked can be reproduced and shared (`?seed=...`) instead of lost. A "seed"
- * is either a number or text; text that is already a base36 number is read as
- * one, so `decodeSeed(seed)` is always accepted back by `makeRandom`.
- *
- * The face and pose vocabulary, and every bound quoted below, follows
- * `src/presets.js` and the slider ranges in `src/panel.js`. Nothing here touches
- * the DOM or the app state: the functions return patches for `applyPatch`.
- */
-
-/**
- * FNV-1a over UTF-16 code units, for text that is not already a seed.
- */
-function fnv1a(text) {
- let hash = 0x811c9dc5;
- for (let i = 0; i < text.length; i += 1) {
- hash ^= text.charCodeAt(i);
- hash = Math.imul(hash, 0x01000193);
- }
- return hash >>> 0;
-}
-
-/**
- * Normalise any seed to a 32-bit integer. Text that is already a base36 number
- * is read as one - that is what `decodeSeed` emits, so it has to come back in
- * unchanged - and anything else is hashed. `hashSeed`, `makeRandom` and
- * `decodeSeed` all go through this, so the four entry points never disagree.
- */
-function toSeedInt(seed) {
- if (typeof seed === 'number' && Number.isFinite(seed)) return Math.trunc(seed) >>> 0;
- const text = String(seed ?? '').trim();
- if (/^[0-9a-z]+$/.test(text)) {
- const parsed = parseInt(text, 36);
- if (Number.isFinite(parsed)) return parsed >>> 0;
- }
- return fnv1a(text);
-}
-
-/**
- * Turn text into a stable 32-bit seed (for `?seed=` in a URL).
- *
- * A word becomes an FNV-1a hash; text that already looks like a base36 seed is
- * passed through, so `decodeSeed`'s output round-trips.
- */
-export function hashSeed(text) {
- return toSeedInt(text);
-}
-
-/**
- * A seeded PRNG (mulberry32). Returns `() => number` in `[0, 1)`.
- * Accepts a number or a string; strings go through the same rule as `decodeSeed`.
- */
-export function makeRandom(seed) {
- let state = toSeedInt(seed);
- return function random() {
- state = (state + 0x6d2b79f5) >>> 0;
- let t = state;
- t = Math.imul(t ^ (t >>> 15), t | 1);
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
- };
-}
-
-/**
- * The short, readable form of a seed (base36), for the "現在のシード" field.
- * Idempotent: `decodeSeed(decodeSeed(x)) === decodeSeed(x)`, and the result is
- * accepted straight back by `makeRandom`.
- */
-export function decodeSeed(seed) {
- return (toSeedInt(seed) >>> 0).toString(36);
-}
-
-function rand(random, min, max) {
- return min + random() * (max - min);
-}
-
-/** An angle in whole degrees, so the pose matches the step-1 sliders. */
-function deg(random, min, max) {
- return Math.round(rand(random, min, max));
-}
-
-function round2(value) {
- return Math.round(value * 100) / 100;
-}
-
-function pick(random, list) {
- return list[Math.floor(random() * list.length)];
-}
-
-function chance(random, probability) {
- return random() < probability;
-}
-
-function clamp(value, min, max) {
- return Math.min(max, Math.max(min, value));
-}
-
-/**
- * The mood table. Each entry samples only inside a range that reads as that
- * emotion, which is what keeps the parts agreeing with each other.
- *
- * Fields: `eyes` (open/irisScale/lookMax/lookX/lookY, all in slider units),
- * `mouth` (smile/open/round/corners/cornerAngle/tilt/offsetY/tongue/width/
- * thickness), plus optional `tear`, `lowerLid`, `heart`, `wink` and `shutBoth`.
- */
-const MOODS = [
- {
- id: 'normal',
- eyes: {
- open: [0.92, 1], irisScale: [0.95, 1.06], lookMax: [0.55, 1],
- lookX: [-0.28, 0.28], lookY: [-0.16, 0.16],
- },
- mouth: {
- smile: [0.62, 0.95], open: [0, 0.05], width: [0.9, 1.1], thickness: [0.9, 1.15],
- corners: [0.8, 1.2], cornerAngle: [-6, 6], tilt: [-4, 4], offsetY: [-5, 6],
- tongue: [0, 1.1],
- },
- },
- {
- id: 'happy',
- eyes: {
- open: [0.88, 1], irisScale: [0.96, 1.08], lookMax: [0.5, 0.95],
- lookX: [-0.3, 0.3], lookY: [-0.2, 0.05],
- },
- mouth: {
- smile: [1.05, 1.3], open: [0.02, 0.22], width: [0.95, 1.2], thickness: [0.9, 1.15],
- corners: [1.05, 1.4], cornerAngle: [0, 12], tilt: [-4, 6], offsetY: [-8, 2],
- tongue: [0.9, 1.35],
- },
- },
- {
- id: 'surprised',
- eyes: {
- open: [0.95, 1], irisScale: [0.8, 0.9], lookMax: [0.3, 0.5],
- lookX: [-0.15, 0.15], lookY: [-0.1, 0.1],
- },
- mouth: {
- smile: [0, 0.2], open: [0.1, 0.32], round: [0.55, 0.8], width: [0.82, 0.98],
- thickness: [1, 1.2], corners: [0, 0.15], cornerAngle: [-3, 3], tilt: [-3, 3],
- offsetY: [-6, 6], tongue: [0, 0.4],
- },
- },
- {
- id: 'sad',
- eyes: {
- open: [0.55, 0.75], irisScale: [0.95, 1.05], lookMax: [0.45, 0.85],
- lookX: [-0.2, 0.2], lookY: [-0.35, -0.12],
- },
- mouth: {
- smile: [-0.7, -0.4], open: [0, 0.05], width: [0.68, 0.88], thickness: [1.05, 1.3],
- corners: [0, 0.1], cornerAngle: [-4, 4], tilt: [-5, 5], offsetY: [6, 16],
- tongue: [0, 0.1],
- },
- lowerLid: [0.16, 0.28],
- tear: [0.6, 1.2],
- tearChance: 1,
- },
- {
- id: 'angry',
- eyes: {
- open: [0.55, 0.7], irisScale: [0.82, 0.92], lookMax: [0.5, 0.9],
- lookX: [-0.15, 0.15], lookY: [-0.16, 0.02],
- },
- mouth: {
- smile: [-0.85, -0.5], open: [0, 0.04], width: [0.78, 0.94], thickness: [1.05, 1.3],
- corners: [0, 0.05], cornerAngle: [-6, 6], tilt: [-6, 6], offsetY: [8, 18],
- tongue: [0, 0.05],
- },
- lowerLid: [0.08, 0.2],
- },
- {
- id: 'love',
- eyes: {
- open: [1, 1], irisScale: [0.9, 1.02], lookMax: [0.4, 0.8],
- lookX: [-0.16, 0.16], lookY: [-0.36, -0.2],
- },
- mouth: {
- smile: [1.15, 1.3], open: [0.08, 0.3], width: [0.98, 1.18], thickness: [0.98, 1.2],
- corners: [1.2, 1.4], cornerAngle: [4, 14], tilt: [2, 8], offsetY: [-2, 6],
- tongue: [1.1, 1.4],
- },
- heart: true,
- },
- {
- id: 'sleepy',
- eyes: {
- open: [0.32, 0.5], irisScale: [0.95, 1.05], lookMax: [0.4, 0.7],
- lookX: [-0.18, 0.18], lookY: [-0.26, -0.08],
- },
- mouth: {
- smile: [0.5, 0.82], open: [0, 0.08], width: [0.82, 1], thickness: [0.92, 1.1],
- corners: [0.5, 0.9], cornerAngle: [-4, 4], tilt: [-7, -1], offsetY: [4, 13],
- tongue: [0.3, 0.8],
- },
- lowerLid: [0.1, 0.24],
- tear: [0.2, 0.5],
- tearChance: 0.5,
- shutBoth: { chance: 0.4, closed: 'line', closedLines: 1 },
- },
- {
- id: 'wink',
- eyes: {
- open: [0.95, 1], irisScale: [0.95, 1.05], lookMax: [0.5, 0.9],
- lookX: [-0.2, 0.2], lookY: [-0.15, 0.1],
- },
- mouth: {
- smile: [1, 1.3], open: [0.02, 0.18], width: [0.95, 1.15], thickness: [0.92, 1.12],
- corners: [0.9, 1.2], cornerAngle: [4, 12], tilt: [-5, 5], offsetY: [-6, 4],
- tongue: [0.8, 1.25],
- },
- wink: true,
- },
-];
-
-/**
- * Sample the eyes. Both eyes normally agree (a tiny jitter keeps the face from
- * looking printed); a wink shuts exactly one, and ねむい sometimes shuts both.
- * A shut-eye style below 1 line is only ever picked when that eye is closed.
- */
-function rollEyes(random, spec) {
- const [openMin, openMax] = spec.open;
- let leftOpen = round2(rand(random, openMin, openMax));
- let rightOpen = round2(clamp(leftOpen + rand(random, -0.06, 0.06), 0, 1));
- let leftShape = 'open';
- let rightShape = 'open';
- let leftClosed = 'line';
- let rightClosed = 'line';
- let leftLines = 1;
- let rightLines = 1;
-
- if (spec.wink) {
- const side = chance(random, 0.5) ? 'left' : 'right';
- const lines = pick(random, [2, 3]);
- // A shut eye is the artwork with the lids open now; the lid height is its own
- // slider, so `open` is 1 and the shape says which line it is.
- if (side === 'left') {
- leftOpen = 1;
- leftLines = lines;
- leftShape = `line${lines}`;
- } else {
- rightOpen = 1;
- rightLines = lines;
- rightShape = `line${lines}`;
- }
- } else if (spec.shutBoth && chance(random, spec.shutBoth.chance)) {
- leftOpen = 1;
- rightOpen = 1;
- leftClosed = spec.shutBoth.closed;
- rightClosed = spec.shutBoth.closed;
- leftLines = spec.shutBoth.closedLines;
- rightLines = spec.shutBoth.closedLines;
- const shape = spec.shutBoth.closed === 'line'
- ? `line${spec.shutBoth.closedLines}`
- : spec.shutBoth.closed;
- leftShape = shape;
- rightShape = shape;
- }
-
- const lookX = round2(rand(random, spec.lookX[0], spec.lookX[1]));
- const lookY = round2(rand(random, spec.lookY[0], spec.lookY[1]));
-
- return {
- left: { shape: leftShape, open: leftOpen, lookX, lookY, closed: leftClosed, closedLines: leftLines, irisShape: 'circle' },
- right: { shape: rightShape, open: rightOpen, lookX, lookY, closed: rightClosed, closedLines: rightLines, irisShape: 'circle' },
- irisScale: round2(rand(random, spec.irisScale[0], spec.irisScale[1])),
- lookMax: round2(rand(random, spec.lookMax[0], spec.lookMax[1])),
- highlight: true,
- };
-}
-
-/** Sample the mouth. A round "O" and a smile are mutually exclusive by design. */
-function rollMouth(random, spec) {
- return {
- visible: true,
- smile: round2(rand(random, spec.smile[0], spec.smile[1])),
- open: round2(rand(random, spec.open[0], spec.open[1])),
- round: spec.round ? round2(rand(random, spec.round[0], spec.round[1])) : 0,
- width: round2(rand(random, spec.width[0], spec.width[1])),
- thickness: round2(rand(random, spec.thickness[0], spec.thickness[1])),
- tilt: Math.round(rand(random, spec.tilt[0], spec.tilt[1])),
- offsetY: Math.round(rand(random, spec.offsetY[0], spec.offsetY[1])),
- corners: round2(rand(random, spec.corners[0], spec.corners[1])),
- cornerAngle: Math.round(rand(random, spec.cornerAngle[0], spec.cornerAngle[1])),
- tongue: round2(rand(random, spec.tongue[0], spec.tongue[1])),
- };
-}
-
-/**
- * A patch for the `face` section: `{ eyes, mouth }`, ready for
- * `applyPatch(state.face, patch)`.
- *
- * Bounds, in case you are reading this from the tests:
- * eyes.open 0..1, irisScale 0.8..1.1, lookMax 0.3..1,
- * lookX -0.35..0.35, lookY -0.45..0.2,
- * mouth.smile -1..1.4 (negative = frown, only when the eyes agree),
- * mouth.open 0..0.35, mouth.round 0 or 0.4..0.8,
- * corners 0..1.4, cornerAngle -30..30, tilt -8..8, offsetY -20..30,
- * width 0.6..1.3, thickness 0.7..1.4, tongue 0..1.6,
- * eyes.left/right.tear 0..1.6.
- */
-export function randomFace(random) {
- const mood = pick(random, MOODS);
- const eyes = rollEyes(random, mood.eyes);
- const mouth = rollMouth(random, mood.mouth);
-
- // Tears only read on a sad or sleepy face: a frown, or eyes that are half shut.
- // A fully shut eye never cries.
- const halfShut = eyes.left.open > 0 && eyes.left.open < 0.78;
- if (mood.tear && eyes.left.open > 0 && (mouth.smile < 0 || halfShut)
- && chance(random, mood.tearChance ?? 1)) {
- // The tears are per eye, so both have to be set. (A crying face wants both;
- // the shape still allows one eye to cry on its own.)
- const amount = round2(rand(random, mood.tear[0], mood.tear[1]));
- eyes.left.tear = amount;
- eyes.right.tear = amount;
- eyes.left.tearOn = true;
- eyes.right.tearOn = true;
- }
-
- if (mood.lowerLid) {
- eyes.lowerLid = round2(rand(random, mood.lowerLid[0], mood.lowerLid[1]));
- }
-
- // A heart pupil needs both eyes open, or it reads as a broken iris.
- if (mood.heart && eyes.left.open >= 0.9 && eyes.right.open >= 0.9) {
- eyes.left.irisShape = 'heart';
- eyes.right.irisShape = 'heart';
- eyes.left.shape = 'heart';
- eyes.right.shape = 'heart';
- eyes.heartScale = round2(rand(random, 0.9, 1.15));
- eyes.heartColor = '#e0344f';
- eyes.highlight = false;
- }
-
- return { eyes, mouth };
-}
-
-/**
- * The pose moves. Each composes one to three bones (never the whole rig) inside
- * safe limits: arms up to ~74°, master lean up to 24°, root offsets small.
- *
- * Bounds: master [-20..24, -28..28, -14..14], armsupport [-18..76, -12..12,
- * -24..24], arm [-14..20, -8..8, -12..12], legsupport [-8..24, -8..8, -12..12],
- * root x/z -0.15..0.15 and y 0..0.45.
- */
-const POSE_MOVES = [
- // A lean or a nod - one bone, the safest thing in the table.
- (random) => ({
- bones: { master: [deg(random, 6, 24), deg(random, -10, 10), deg(random, -6, 6)] },
- }),
- // A curious tilt to one side.
- (random) => {
- const sign = chance(random, 0.5) ? 1 : -1;
- return { bones: { master: [deg(random, -6, 8), 0, sign * deg(random, 8, 14)] } };
- },
- // A wave from one arm.
- (random) => {
- const side = pick(random, ['l', 'r']);
- return {
- bones: {
- [`armsupport.${side}`]: [deg(random, 42, 74), deg(random, -8, 8), deg(random, -14, 14)],
- [`arm.${side}`]: [deg(random, 6, 18), 0, 0],
- },
- };
- },
- // A two-armed cheer, with a small hop.
- (random) => ({
- bones: {
- master: [deg(random, -14, -2), 0, 0],
- 'armsupport.l': [deg(random, 52, 74), 0, deg(random, 4, 18)],
- 'armsupport.r': [deg(random, 52, 74), 0, -deg(random, 4, 18)],
- },
- root: [0, round2(rand(random, 0.05, 0.3)), 0],
- }),
- // Introducing something off to one side.
- (random) => {
- const side = pick(random, ['l', 'r']);
- const sign = side === 'l' ? 1 : -1;
- const other = side === 'l' ? 'r' : 'l';
- return {
- bones: {
- master: [deg(random, 2, 8), sign * deg(random, 16, 26), 0],
- [`armsupport.${side}`]: [deg(random, 24, 40), 0, sign * deg(random, 10, 20)],
- [`armsupport.${other}`]: [deg(random, 2, 14), 0, 0],
- },
- };
- },
- // A little hop on the spot.
- (random) => ({
- bones: {
- master: [deg(random, -12, -2), 0, 0],
- 'legsupport.l': [deg(random, 8, 22), 0, deg(random, -8, 8)],
- 'legsupport.r': [deg(random, 8, 22), 0, deg(random, -8, 8)],
- },
- root: [0, round2(rand(random, 0.18, 0.42)), 0],
- }),
- // Both arms up in a simple stretch.
- (random) => ({
- bones: {
- 'armsupport.l': [deg(random, 46, 74), 0, deg(random, 4, 16)],
- 'armsupport.r': [deg(random, 46, 74), 0, -deg(random, 4, 16)],
- },
- }),
- // A dance sway: one arm leads, the other follows.
- (random) => {
- const sign = chance(random, 0.5) ? 1 : -1;
- return {
- bones: {
- master: [deg(random, -4, 4), 0, sign * deg(random, 4, 12)],
- 'armsupport.l': [deg(random, 30, 46), 0, sign * deg(random, 8, 18)],
- 'armsupport.r': [deg(random, 12, 26), 0, -sign * deg(random, 8, 18)],
- },
- root: [round2(rand(random, -0.08, 0.08)), 0, 0],
- };
- },
-];
-
-/** A patch for the `pose` section: `{ bones, root }`. */
-export function randomPose(random) {
- const move = pick(random, POSE_MOVES)(random);
- return { bones: move.bones, root: move.root ?? [0, 0, 0] };
-}
-
-/**
- * A patch for the `lookAt` section. About half the time it enables a target off
- * to one side, so the character glances away instead of staring at the camera.
- * Bounds: |x| 1.2..3.2 when enabled, y 1.6..3.4, z 2..4.2, amount 0.5..1.
- */
-export function randomLook(random) {
- if (chance(random, 0.45)) {
- const sign = chance(random, 0.5) ? 1 : -1;
- return {
- lookAt: {
- enabled: true,
- x: round2(sign * rand(random, 1.2, 3.2)),
- y: round2(rand(random, 1.6, 3.4)),
- z: round2(rand(random, 2, 4.2)),
- turnBody: chance(random, 0.35),
- amount: round2(rand(random, 0.5, 1)),
- },
- };
- }
- return {
- lookAt: { enabled: false, x: 0, y: 2.6, z: 3, turnBody: false, amount: round2(rand(random, 0.6, 1)) },
- };
-}
-
-/**
- * One press of おまかせ: face, pose and look, all from the same seed.
- *
- * @param {number | string} seed a number, or text that `decodeSeed` also accepts
- * @returns {{ seed: string, patch: { face: object, pose: object, lookAt: object } }}
- * `seed` is the readable base36 form, for the URL and the seed field.
- */
-export function rollAll(seed) {
- const random = makeRandom(seed);
- return {
- seed: decodeSeed(seed),
- patch: {
- face: randomFace(random),
- pose: randomPose(random),
- lookAt: randomLook(random).lookAt,
- },
- };
-}
diff --git a/bluebey-studio/src/gion.js b/bluebey-studio/src/gion.js
deleted file mode 100644
index 832313b..0000000
--- a/bluebey-studio/src/gion.js
+++ /dev/null
@@ -1,226 +0,0 @@
-/**
- * 擬音 (マンガのオノマトペ) のスタンプ.
- *
- * The source images are dense sheets: several sounds, each in a white-outline and
- * a solid version, packed so tightly that an automatic slice (outline tracing /
- * connected components) tears a single character into fragments. So nothing here
- * guesses at a sheet's layout. Instead the user draws a rectangle over the sheet
- * in the picker and the app crops exactly that rectangle, which means the feature
- * works for any sheet that arrives later.
- *
- * That is why the crops are stored as *source pixels* (`sx, sy, sw, sh`) taken
- * from the bitmap's own `naturalWidth` / `naturalHeight`, never as fractions of
- * some assumed grid: the sheet's pixel size is read at runtime.
- *
- * The module is pure and DOM-free in the same sense as `trace.js` and
- * `caption.js`: no globals are touched except a read of the injected
- * `__BLUEBEY_GIONS__` lookup, and `drawGion` takes the loaded bitmaps as an
- * argument, so the caller owns loading and the tests can drive the geometry in
- * Node with a stub context.
- */
-
-/**
- * The sheets the studio offers. It ships with none: the original otarunet sheet
- * may not be redistributed with the app, and the images the author draws later are
- * added here. So this list starts empty and the 擬音 section stays out of the panel
- * until an entry appears.
- *
- * To add one: put the image in `assets/manga-gion/`, add `{ name, label }` here
- * (`name` is the file name *with* its extension, e.g. `dokaan.png`) and add the
- * same `name` to `GION_NAMES` in `tools/build-standalone.mjs` for the one-file
- * build. Use an image with a transparent background - the crop is drawn as-is, so
- * a white background would sit on the picture as a white rectangle.
- */
-export const GION_SHEETS = [];
-
-/**
- * The default display width of a stamp, in pixels at 1x. A crop that is tall and
- * thin will be narrower than this after the aspect is applied; the user resizes
- * it from the panel either way.
- */
-export const DEFAULT_STAMP_WIDTH = 240;
-
-/**
- * Where a sheet's image lives. The single-file build inlines every sheet as a
- * data URL in `window.__BLUEBEY_GIONS__`; the normal build reads the file.
- *
- * @param {string} name the sheet file name, with its extension, e.g. `dokaan.png`
- * @returns {string}
- */
-export function gionSheetUrl(name) {
- const inlined = globalThis.__BLUEBEY_GIONS__ ?? {};
- return inlined[name] ?? `assets/manga-gion/${name}`;
-}
-
-/** Clamp `value` into `lo..hi`; a non-finite value becomes `lo`. */
-export function clamp(value, lo, hi) {
- if (!Number.isFinite(value)) return lo;
- return Math.min(hi, Math.max(lo, value));
-}
-
-/** True when `image` has pixels to draw (a not-yet-loaded Image has none). */
-export function imageReady(image) {
- if (!image) return false;
- const width = Number(image.naturalWidth ?? image.width ?? 0);
- const height = Number(image.naturalHeight ?? image.height ?? 0);
- // `complete` is the browser's own answer; a stub in a test omits it, so only an
- // explicit `false` blocks the draw.
- return width > 0 && height > 0 && image.complete !== false;
-}
-
-/** The bitmap's width / height. 1 when it is not loaded yet. */
-export function imageAspect(image) {
- const width = toPositive(image?.naturalWidth ?? image?.width, 1);
- const height = toPositive(image?.naturalHeight ?? image?.height, 1);
- return width / height;
-}
-
-/**
- * The rectangle a stamp occupies on screen, in pixels, with rotation left out
- * (the caller rotates about the returned centre).
- *
- * `item.x` / `item.y` are the centre as a fraction of the viewport, `item.w` is
- * the width in pixels at 1x, and the height follows the bitmap's aspect ratio,
- * so a wide crop and a tall crop are both placed by their width alone.
- *
- * @param {object} item a stamp: `{ x, y, w, ... }`
- * @param {object} [viewport] `{ width, height, scale }` of the target surface
- * @param {number} [aspect] bitmap width / height
- * @returns {{x: number, y: number, w: number, h: number, cx: number, cy: number}}
- */
-export function stampRect(item, { width = 0, height = 0, scale = 1 } = {}, aspect = 1) {
- const w = toPositive(item?.w, DEFAULT_STAMP_WIDTH) * positiveScale(scale);
- const h = w / toPositive(aspect, 1);
- const cx = toFinite(item?.x, 0.5) * toNonNegative(width, 0);
- const cy = toFinite(item?.y, 0.5) * toNonNegative(height, 0);
- return { x: cx - w / 2, y: cy - h / 2, w, h, cx, cy };
-}
-
-/** Turn two corner points into a rectangle with a positive width and height. */
-export function normalizeRect(a, b) {
- const ax = toFinite(a?.x, 0);
- const ay = toFinite(a?.y, 0);
- const bx = toFinite(b?.x, 0);
- const by = toFinite(b?.y, 0);
- return { x: Math.min(ax, bx), y: Math.min(ay, by), w: Math.abs(bx - ax), h: Math.abs(by - ay) };
-}
-
-/**
- * Map a marquee (drawn in screen pixels over the fitted sheet) to a pixel crop of
- * the source bitmap.
- *
- * `sheetRect` is where the sheet is currently displayed, so the marquee becomes a
- * fraction of the sheet and then a fraction of the bitmap - the sheet's own pixel
- * size comes from `natural`, never from a hard-coded number. The crop is clamped
- * to the bitmap and is at least 1x1, so it can always be drawn.
- *
- * @param {{x: number, y: number, w: number, h: number}} marquee screen pixels
- * @param {{x: number, y: number, w: number, h: number}} sheetRect screen pixels
- * @param {object} natural the sheet image (or any `{ naturalWidth, naturalHeight }`)
- * @returns {{sx: number, sy: number, sw: number, sh: number}}
- */
-export function cropFromMarquee(marquee, sheetRect, natural) {
- const nw = Math.max(1, Math.round(toPositive(natural?.naturalWidth, 1)));
- const nh = Math.max(1, Math.round(toPositive(natural?.naturalHeight, 1)));
- const rect = {
- x: toFinite(marquee?.x, 0),
- y: toFinite(marquee?.y, 0),
- w: toNonNegative(marquee?.w, 0),
- h: toNonNegative(marquee?.h, 0),
- };
- const displayW = toNonNegative(sheetRect?.w, 0);
- const displayH = toNonNegative(sheetRect?.h, 0);
-
- const left = displayW > 0 ? clamp((rect.x - sheetRect.x) / displayW, 0, 1) : 0;
- const right = displayW > 0 ? clamp((rect.x + rect.w - sheetRect.x) / displayW, 0, 1) : 0;
- const top = displayH > 0 ? clamp((rect.y - sheetRect.y) / displayH, 0, 1) : 0;
- const bottom = displayH > 0 ? clamp((rect.y + rect.h - sheetRect.y) / displayH, 0, 1) : 0;
-
- const sx = clamp(Math.round(left * nw), 0, nw - 1);
- const sy = clamp(Math.round(top * nh), 0, nh - 1);
- return {
- sx,
- sy,
- sw: clamp(Math.round((right - left) * nw), 1, nw - sx),
- sh: clamp(Math.round((bottom - top) * nh), 1, nh - sy),
- };
-}
-
-/**
- * Fit a bitmap inside a box, keeping its aspect ratio and centring it. The picker
- * uses this to show a whole sheet - however large - at a size the user can drag
- * over.
- *
- * @param {object} natural the sheet image
- * @param {{width: number, height: number}} box the available area, in pixels
- * @param {object} [options]
- * @param {number} [options.padding=0] a margin to keep inside the box
- * @returns {{x: number, y: number, w: number, h: number}}
- */
-export function fitSheet(natural, box, { padding = 0 } = {}) {
- const nw = toPositive(natural?.naturalWidth ?? natural?.width, 1);
- const nh = toPositive(natural?.naturalHeight ?? natural?.height, 1);
- const pad = toNonNegative(padding, 0);
- const availW = Math.max(0, toNonNegative(box?.width, 0) - pad * 2);
- const availH = Math.max(0, toNonNegative(box?.height, 0) - pad * 2);
- const scale = Math.min(availW / nw, availH / nh);
- const w = nw * scale;
- const h = nh * scale;
- return { x: pad + (availW - w) / 2, y: pad + (availH - h) / 2, w, h };
-}
-
-/**
- * Paint every stamp onto a 2D context.
- *
- * The same function runs over the live viewport overlay (scale 1) and into the
- * exported PNG (scale = output pixels / CSS pixels), which is what keeps the
- * preview and the file in step. A stamp whose sheet has not loaded yet is simply
- * skipped, so the rest of the picture is never held up by one image.
- *
- * @param {CanvasRenderingContext2D} ctx
- * @param {Array} items the stamps
- * @param {Map} images loaded sheets, by name
- * @param {object} [options]
- * @param {number} [options.width=0]
- * @param {number} [options.height=0]
- * @param {number} [options.scale=1]
- */
-export function drawGion(ctx, items, images, { width = 0, height = 0, scale = 1 } = {}) {
- if (!ctx || !Array.isArray(items)) return;
- const viewport = { width, height, scale: positiveScale(scale) };
- for (const item of items) {
- const image = images?.get?.(item?.sheet);
- if (!imageReady(image)) continue;
- const rect = stampRect(item, viewport, imageAspect(image));
- if (!(rect.w > 0) || !(rect.h > 0)) continue;
- const rot = toFinite(item?.rot, 0);
- ctx.save();
- ctx.translate(rect.cx, rect.cy);
- if (rot !== 0) ctx.rotate((rot * Math.PI) / 180);
- // The flip is applied before the draw so the same source rect feeds both.
- if (item?.flip) ctx.scale(-1, 1);
- ctx.drawImage(image, item.sx, item.sy, item.sw, item.sh, -rect.w / 2, -rect.h / 2, rect.w, rect.h);
- ctx.restore();
- }
-}
-
-/* ----------------------------------------------------------------- numbers */
-
-function toFinite(value, fallback) {
- const n = Number(value);
- return Number.isFinite(n) ? n : fallback;
-}
-
-function toPositive(value, fallback) {
- const n = Number(value);
- return Number.isFinite(n) && n > 0 ? n : fallback;
-}
-
-function toNonNegative(value, fallback) {
- const n = Number(value);
- return Number.isFinite(n) && n >= 0 ? n : fallback;
-}
-
-function positiveScale(value) {
- return toPositive(value, 1);
-}
diff --git a/bluebey-studio/src/glbExport.js b/bluebey-studio/src/glbExport.js
deleted file mode 100644
index 6e3e997..0000000
--- a/bluebey-studio/src/glbExport.js
+++ /dev/null
@@ -1,148 +0,0 @@
-import * as THREE from 'three';
-import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
-
-/**
- * Hand the posed character back as a 3D file.
- *
- * WHY: a PNG or a WebM only travels as pixels. Being able to take the pose, the
- * expression and the props into Blender (or embed them elsewhere as a model)
- * turns the studio into a front end for the character instead of a picture
- * maker, and it costs nothing: what is on screen already *is* a glTF scene, so
- * the only job here is to hand the live objects to three's exporter with the
- * right options and a Blob around the result.
- *
- * The caller passes exactly what it wants in the file - the character root and
- * any props - so no lights, helpers or ground ever enter it. The inverted-hull
- * outlines are the one wrinkle: they are visible (they are part of the look)
- * but they are duplicates of the body geometry, so exporting them would double
- * the file and leave a black shell around the character. `needsTemporaryHide`
- * decides what to hide for the duration, and `exportGLB` restores it after.
- */
-
-const GLTF_BINARY_TYPE = 'model/gltf-binary';
-const GLTF_JSON_TYPE = 'model/gltf+json';
-/** The largest texture a GPU can be assumed to handle everywhere. */
-const MAX_TEXTURE_SIZE = 4096;
-
-/**
- * True for objects the exporter must skip: the inverted-hull outlines (named
- * `...:outline`), anything the app tagged as a helper or as excluded from the
- * export, and anything already hidden. The name check is the important one: an
- * outline is visible on screen, so `onlyVisible` alone would not drop it.
- *
- * @param {THREE.Object3D} object
- * @returns {boolean}
- */
-export function needsTemporaryHide(object) {
- if (!object) return false;
- if (typeof object.name === 'string' && object.name.endsWith(':outline')) return true;
- if (object.userData?.isHelper === true) return true;
- if (object.userData?.excludeFromExport === true) return true;
- return object.visible === false;
-}
-
-/** Triangles in a geometry, ignoring draw ranges (the export does too). */
-function triangleCount(geometry) {
- if (!geometry) return 0;
- const index = geometry.index;
- const count = index ? index.count : geometry.attributes?.position?.count ?? 0;
- return Math.floor(count / 3);
-}
-
-/**
- * Count what `exportGLB` will write, for the summary the app shows first.
- *
- * Walks the same pruned tree the exporter sees, so hiding an outline or a
- * helper is reflected in the numbers. A `SkinnedMesh` is also a `Mesh`, so it
- * counts in both `meshes` and `skinnedMeshes`; a material shared by several
- * meshes counts once, and every texture a counted material refers to counts
- * once.
- *
- * @param {THREE.Object3D[]} objects
- * @returns {{ meshes: number, skinnedMeshes: number, materials: number, textures: number, triangles: number }}
- */
-export function describeScene(objects) {
- const materials = new Set();
- const textures = new Set();
- let meshes = 0;
- let skinnedMeshes = 0;
- let triangles = 0;
-
- const visit = (object) => {
- if (!object || needsTemporaryHide(object)) return;
- if (object.isMesh) {
- meshes += 1;
- if (object.isSkinnedMesh) skinnedMeshes += 1;
- const list = Array.isArray(object.material) ? object.material : [object.material];
- for (const material of list) {
- if (!material) continue;
- materials.add(material);
- for (const value of Object.values(material)) {
- if (value && value.isTexture) textures.add(value);
- }
- }
- triangles += triangleCount(object.geometry);
- }
- for (const child of object.children ?? []) visit(child);
- };
-
- for (const root of Array.isArray(objects) ? objects : []) visit(root);
- return { meshes, skinnedMeshes, materials: materials.size, textures: textures.size, triangles };
-}
-
-/**
- * Hide every object the file must not contain, remembering what to restore.
- * Stops at the first excluded ancestor: the exporter skips a hidden subtree, so
- * there is no need to walk inside one.
- */
-function hideExcluded(object, hidden) {
- if (needsTemporaryHide(object)) {
- hidden.push({ object, visible: object.visible });
- object.visible = false;
- return;
- }
- for (const child of object.children ?? []) hideExcluded(child, hidden);
-}
-
-/**
- * Export `objects` as a GLB (or a `.gltf` JSON) Blob.
- *
- * @param {THREE.Object3D[]} objects the character root and any props
- * @param {{ binary?: boolean, name?: string }} [options]
- * @returns {Promise} `model/gltf-binary`, or `model/gltf+json` when not binary
- */
-export async function exportGLB(objects, { binary = true, name = 'bluebey' } = {}) {
- const roots = (Array.isArray(objects) ? objects : []).filter(Boolean);
- if (roots.length === 0) {
- throw new Error('exportGLB: objects is empty, there is nothing to export');
- }
- if (describeScene(roots).meshes === 0) {
- throw new Error('exportGLB: objects contain no visible meshes to export');
- }
-
- const hidden = [];
- try {
- for (const root of roots) hideExcluded(root, hidden);
-
- // The exporter names a bare array of objects "AuxScene". Wrapping them in a
- // named Scene keeps the caller's objects where they are (we push straight
- // into `children`, exactly as the exporter does) and gives the file a real
- // scene name.
- const scene = new THREE.Scene();
- scene.name = name;
- for (const root of roots) scene.children.push(root);
-
- const exporter = new GLTFExporter();
- const result = await exporter.parseAsync(scene, {
- binary,
- onlyVisible: true,
- truncateDrawRange: false,
- maxTextureSize: MAX_TEXTURE_SIZE,
- });
-
- if (binary) return new Blob([result], { type: GLTF_BINARY_TYPE });
- return new Blob([JSON.stringify(result)], { type: GLTF_JSON_TYPE });
- } finally {
- for (const { object, visible } of hidden) object.visible = visible;
- }
-}
diff --git a/bluebey-studio/src/handDrawn.js b/bluebey-studio/src/handDrawn.js
deleted file mode 100644
index e7fbce5..0000000
--- a/bluebey-studio/src/handDrawn.js
+++ /dev/null
@@ -1,391 +0,0 @@
-import { contoursToPathData } from './trace.js';
-
-/**
- * Hand-drawn distortion for traced contours.
- *
- * WHY: `trace.js` recovers the silhouette of the mascot from a rendered alpha
- * mask. The result is geometrically faithful but *mechanically* smooth: it reads
- * as the outline of a printed sticker, not as a pen stroke. This module nudges
- * the traced points along a smooth, seeded wobble so the very same silhouette
- * looks inked by hand, without changing its point count or where it sits.
- *
- * The displacement is value noise interpolated along the contour, so neighbouring
- * points move by nearly the same amount and the outline stays a wobbly *line*
- * rather than pixel jitter. Everything is seeded (never `Math.random`), so a
- * build is reproducible, and the module is pure: it never touches the DOM and
- * its only import is the path-data helper in `trace.js`, which keeps the output
- * format identical to the un-roughened export.
- */
-
-/** Lattice cells in the non-wrapping noise table (the pattern repeats after this). */
-const NOISE_PERIOD = 4096;
-
-const EPSILON = 1e-9;
-
-/**
- * mulberry32: a tiny, fast 32-bit generator. Good enough for visual noise and,
- * crucially, fully reproducible; the seed is the only source of variation.
- *
- * @param {number} seed
- * @returns {() => number} values in [0, 1)
- */
-function mulberry32(seed) {
- let a = seed >>> 0;
- return function next() {
- a = (a + 0x6d2b79f5) >>> 0;
- let t = a;
- t = Math.imul(t ^ (t >>> 15), t | 1);
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
- };
-}
-
-/** Hermite ramp: 0 at t=0, 1 at t=1, flat at both ends (C1 continuity). */
-function smoothstep(t) {
- return t * t * (3 - 2 * t);
-}
-
-/**
- * A circular table of random values in -1..1. `count` is the number of cells in
- * one lap; sampling wraps at `count`, which is what lets a closed stroke's
- * wobble meet itself exactly at the seam.
- *
- * @param {number} seed
- * @param {number} count
- * @returns {Float64Array}
- */
-function buildNoiseTable(seed, count) {
- const rng = mulberry32(seed);
- const table = new Float64Array(count);
- for (let i = 0; i < count; i++) table[i] = rng() * 2 - 1;
- return table;
-}
-
-/**
- * Smoothly interpolate the table at `t`, wrapping around its ends. `smoothstep`
- * makes the value continuous and its slope continuous at every cell boundary, so
- * the wobble has no visible kinks.
- */
-function sampleTable(table, t) {
- const len = table.length;
- const base = Math.floor(t);
- const f = t - base;
- const i0 = ((base % len) + len) % len;
- const i1 = (i0 + 1) % len;
- const a = table[i0];
- const b = table[i1];
- return a + (b - a) * smoothstep(f);
-}
-
-/**
- * One-dimensional value noise, exposed mainly so tests can pin its behaviour.
- * The returned function is continuous, roughly in -1..1, deterministic for a
- * given seed, and returns 0 for non-finite input.
- *
- * @param {number} [seed=1]
- * @returns {(t: number) => number}
- */
-export function makeNoise(seed = 1) {
- const table = buildNoiseTable(seed, NOISE_PERIOD);
- return function noise(t) {
- if (!Number.isFinite(t)) return 0;
- return sampleTable(table, t);
- };
-}
-
-/** Euclidean distance between two points. */
-function distance(a, b) {
- return Math.hypot(b.x - a.x, b.y - a.y);
-}
-
-/** Cumulative arc length of every point, measured from the first. */
-function arcPositions(points) {
- const pos = new Float64Array(points.length);
- for (let i = 1; i < points.length; i++) {
- pos[i] = pos[i - 1] + distance(points[i - 1], points[i]);
- }
- return pos;
-}
-
-/** True when a closed contour repeats its first point at the end. */
-function hasClosingDuplicate(points) {
- const first = points[0];
- const last = points[points.length - 1];
- return Math.abs(first.x - last.x) <= EPSILON && Math.abs(first.y - last.y) <= EPSILON;
-}
-
-/**
- * Neighbour index for each point, honouring the wrap of a closed contour. Open
- * ends get -1, which makes the tangent fall back to a one-sided difference.
- */
-function neighborIndices(count, closed, duplicate) {
- const prev = new Int32Array(count);
- const next = new Int32Array(count);
- if (!closed) {
- for (let i = 0; i < count; i++) {
- prev[i] = i > 0 ? i - 1 : -1;
- next[i] = i < count - 1 ? i + 1 : -1;
- }
- return { prev, next };
- }
- // A repeated closing point is a copy of point 0, so the ring has one fewer
- // distinct vertex and the last index borrows point 0's two neighbours, which
- // is what makes its wobble identical to the first point's.
- const ring = duplicate ? count - 1 : count;
- const firstPrev = (ring - 1) % ring;
- const firstNext = ring > 1 ? 1 : 0;
- for (let i = 0; i < count; i++) {
- if (duplicate && i === count - 1) {
- prev[i] = firstPrev;
- next[i] = firstNext;
- } else {
- prev[i] = (i - 1 + ring) % ring;
- next[i] = (i + 1) % ring;
- }
- }
- return { prev, next };
-}
-
-/**
- * Unit tangent at `i`, measured from the point before to the point after so the
- * wobble follows the stroke instead of the sampling. Returns null for a
- * degenerate point, where there is no direction to displace along.
- */
-function tangentAt(points, i, prev, next) {
- const before = prev[i] >= 0 ? points[prev[i]] : points[i];
- const after = next[i] >= 0 ? points[next[i]] : points[i];
- const dx = after.x - before.x;
- const dy = after.y - before.y;
- const len = Math.hypot(dx, dy);
- if (!(len > EPSILON)) return null;
- return { x: dx / len, y: dy / len };
-}
-
-/**
- * Fade the wobble to zero at both ends of an open stroke, over `ramp` units.
- * Without this the ends would fly off the traced geometry; a smooth ramp keeps
- * the stroke anchored while still looking freehand.
- */
-function edgeWindow(s, length, ramp) {
- if (!(ramp > 0)) return 1;
- const head = Math.min(1, s / ramp);
- const tail = Math.min(1, (length - s) / ramp);
- return smoothstep(head) * smoothstep(tail);
-}
-
-/** Derive a distinct, deterministic seed for each extra pass. */
-function mixSeed(seed, pass) {
- return (seed + pass * 0x9e3779b1) >>> 0;
-}
-
-/** Apply one wobble pass. `roughenPolyline` owns the input copy and the passes. */
-function roughenOnce(points, { amount, seed, closed, scale }) {
- const count = points.length;
- const copy = () => points.map((p) => ({ x: p.x, y: p.y }));
- if (count < 2 || !(amount > 0) || !(scale > 0)) return copy();
-
- const duplicate = closed ? hasClosingDuplicate(points) : false;
- const pos = arcPositions(points);
- const length = pos[count - 1];
- let perimeter = length;
- if (closed) perimeter += distance(points[count - 1], points[0]);
-
- const { prev, next } = neighborIndices(count, closed, duplicate);
-
- // A closed stroke samples a circular table with a whole number of cells per
- // lap, so the last point lands on the first cell and the seam closes. An open
- // stroke samples plain (non-wrapping) noise and fades it out near the ends.
- let table;
- let cells = 0;
- if (closed) {
- if (!(perimeter > 0)) return copy();
- cells = Math.max(2, Math.round(perimeter / scale));
- table = buildNoiseTable(seed, cells);
- } else {
- table = buildNoiseTable(seed, NOISE_PERIOD);
- }
- const ramp = Math.min(scale, length * 0.25);
-
- const out = new Array(count);
- for (let i = 0; i < count; i++) {
- const tangent = tangentAt(points, i, prev, next);
- const p = points[i];
- if (!tangent) {
- out[i] = { x: p.x, y: p.y };
- continue;
- }
- const t = closed ? (pos[i] / perimeter) * cells : pos[i] / scale;
- let weight = sampleTable(table, t);
- if (!closed) weight *= edgeWindow(pos[i], length, ramp);
- const shift = amount * weight;
- if (shift === 0) {
- // Keep the original exactly (also avoids turning -0 into 0).
- out[i] = { x: p.x, y: p.y };
- continue;
- }
- // Displace perpendicular to the tangent, i.e. along the local pen normal.
- out[i] = { x: p.x - tangent.y * shift, y: p.y + tangent.x * shift };
- }
- if (closed && duplicate) {
- // Belt and braces: pin the explicit seam shut after any rounding.
- out[count - 1] = { x: out[0].x, y: out[0].y };
- }
- return out;
-}
-
-/**
- * Displace every point of a polyline perpendicular to its local direction by
- * smooth noise. The input is never mutated and the point count never changes.
- *
- * Open polylines keep their first and last point exactly; closed ones wrap, so
- * the wobble is continuous across the seam. `amount` is the peak displacement in
- * the same units as the points, and `scale` is how much arc length one wobble
- * spans (larger = lazier, longer wobble). With `passes > 1` the displacement is
- * re-noised a few times at a share of `amount`, so the peak stays within
- * `amount` however many passes are used.
- *
- * @param {Array<{x: number, y: number}>} points
- * @param {object} [options]
- * @param {number} [options.amount=2] peak displacement, in point units
- * @param {number} [options.seed=1] deterministic seed
- * @param {boolean} [options.closed=false] treat the polyline as a ring
- * @param {number} [options.scale=40] arc length covered by one wobble
- * @param {number} [options.passes=1] number of noise layers
- * @returns {Array<{x: number, y: number}>} a new array of new points
- */
-export function roughenPolyline(points, options = {}) {
- const list = Array.isArray(points) ? points : [];
- const amount = options.amount ?? 2;
- const seed = options.seed ?? 1;
- const closed = options.closed ?? false;
- const scale = options.scale ?? 40;
- const passes = Math.max(1, Math.floor(options.passes ?? 1));
-
- let current = list.map((p) => ({ x: p.x, y: p.y }));
- if (current.length < 2 || amount === 0) return current;
- for (let pass = 0; pass < passes; pass++) {
- current = roughenOnce(current, {
- amount: amount / passes,
- seed: mixSeed(seed, pass),
- closed,
- scale,
- });
- }
- return current;
-}
-
-/**
- * Roughen a list of contours, with the closed/open choice per contour. Following
- * `trace.js`'s convention, a contour is assumed to be a closed ring unless the
- * caller says otherwise: pass `options.closed` as a boolean for all of them or
- * as an array of flags indexed like `contours`.
- *
- * @param {Array>} contours
- * @param {object} [options] see `roughenPolyline`, plus `closed` as an array
- * @returns {Array>}
- */
-export function roughenContours(contours, options = {}) {
- const list = Array.isArray(contours) ? contours : [];
- const closedOption = options.closed;
- const out = [];
- for (let i = 0; i < list.length; i++) {
- const closed = Array.isArray(closedOption) ? closedOption[i] ?? true : closedOption ?? true;
- out.push(roughenPolyline(list[i], { ...options, closed }));
- }
- return out;
-}
-
-/**
- * Roughen a list of contours and return their SVG `d` attribute, using exactly
- * the format of `contoursToPathData`: one `M … L … Z` subpath per contour,
- * coordinates rounded to `options.round` decimal places (default 2, the same
- * meaning as that function's `decimals` argument).
- *
- * @param {Array>} contours
- * @param {object} [options] roughening options, plus `round` and `mapPoint`
- * @param {number} [options.round=2] decimal places in the output
- * @param {(x: number, y: number) => [number, number]} [options.mapPoint]
- * @returns {string}
- */
-export function handDrawnPathData(contours, options = {}) {
- const roughened = roughenContours(contours, options);
- const mapPoint = options.mapPoint ?? ((x, y) => [x, y]);
- return contoursToPathData(roughened, mapPoint, options.round ?? 2);
-}
-
-/**
- * Offset a stroke to both sides by a width that breathes slightly along its
- * length, giving the `[left, right]` polylines a pen stroke can be filled
- * between. Both sides keep the point count and order of `points`.
- *
- * The width only varies (it never reaches zero), so the two sides stay well
- * defined; `variation` is the fraction of `width` the wobble may add or remove.
- *
- * @param {Array<{x: number, y: number}>} points
- * @param {object} [options]
- * @param {number} [options.width=3] full stroke width
- * @param {number} [options.seed=1]
- * @param {boolean} [options.closed=false]
- * @param {number} [options.variation=0.35] relative width wobble
- * @param {number} [options.scale=40] arc length covered by one width wobble
- * @returns {[Array<{x: number, y: number}>, Array<{x: number, y: number}>]}
- */
-export function taperStroke(points, options = {}) {
- const list = Array.isArray(points) ? points : [];
- const width = options.width ?? 3;
- const seed = options.seed ?? 1;
- const closed = options.closed ?? false;
- const variation = options.variation ?? 0.35;
- const scale = options.scale ?? 40;
-
- const count = list.length;
- const left = new Array(count);
- const right = new Array(count);
- if (count === 0) return [left, right];
- if (count === 1) {
- left[0] = { x: list[0].x, y: list[0].y };
- right[0] = { x: list[0].x, y: list[0].y };
- return [left, right];
- }
-
- const duplicate = closed ? hasClosingDuplicate(list) : false;
- const pos = arcPositions(list);
- const length = pos[count - 1];
- let perimeter = length;
- if (closed) perimeter += distance(list[count - 1], list[0]);
-
- const { prev, next } = neighborIndices(count, closed, duplicate);
-
- let table;
- let cells = 0;
- if (closed && perimeter > 0 && scale > 0) {
- cells = Math.max(2, Math.round(perimeter / scale));
- table = buildNoiseTable(seed, cells);
- } else {
- table = buildNoiseTable(seed, NOISE_PERIOD);
- }
- const span = scale > 0 ? scale : 1;
- const halfWidth = Math.abs(width) / 2;
-
- for (let i = 0; i < count; i++) {
- const tangent = tangentAt(list, i, prev, next);
- const p = list[i];
- if (!tangent) {
- left[i] = { x: p.x, y: p.y };
- right[i] = { x: p.x, y: p.y };
- continue;
- }
- const t = closed && cells > 0 ? (pos[i] / perimeter) * cells : pos[i] / span;
- // Clamped well above zero so both sides keep a usable offset even when the
- // caller asks for a large `variation`.
- const factor = Math.max(0.05, 1 + variation * sampleTable(table, t));
- const w = halfWidth * factor;
- left[i] = { x: p.x - tangent.y * w, y: p.y + tangent.x * w };
- right[i] = { x: p.x + tangent.y * w, y: p.y - tangent.x * w };
- }
- if (closed && duplicate) {
- left[count - 1] = { x: left[0].x, y: left[0].y };
- right[count - 1] = { x: right[0].x, y: right[0].y };
- }
- return [left, right];
-}
diff --git a/bluebey-studio/src/hats.js b/bluebey-studio/src/hats.js
deleted file mode 100644
index 7a54c46..0000000
--- a/bluebey-studio/src/hats.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import * as THREE from 'three';
-
-/**
- * Hats for ぶるべー. Each hat is a small group of primitives whose base sits at
- * y = 0, so the app can drop it straight onto the top of the head (see
- * `hatMount` in src/main.js). The character's front is +z, so a brim or a badge
- * faces +z.
- *
- * These are deliberately not textured: like the props, they are built from a few
- * rounded primitives so they take a clean outline and read in every render style.
- */
-
-const material = (color, opts = {}) => new THREE.MeshStandardMaterial({
- color,
- roughness: 0.72,
- metalness: 0,
- ...opts,
-});
-
-function add(parent, geometry, mat, x = 0, y = 0, z = 0) {
- const mesh = new THREE.Mesh(geometry, mat);
- mesh.position.set(x, y, z);
- mesh.castShadow = true;
- mesh.receiveShadow = false;
- parent.add(mesh);
- return mesh;
-}
-
-const cylinder = (rt, rb, h, seg = 24) => new THREE.CylinderGeometry(rt, rb, h, seg);
-const sphere = (r, w = 24, h = 16, phiStart = 0, phiLength = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI) => (
- new THREE.SphereGeometry(r, w, h, phiStart, phiLength, thetaStart, thetaLength)
-);
-const box = (w, h, d) => new THREE.BoxGeometry(w, h, d);
-
-/**
- * The 線画 fill a part takes, as how far it steps from the paper towards the ink
- * (see `addMesh` in src/styles.js). Every hat part already gets the light default
- * tone, but two parts of the *same* hat then come out the same shade - so a part
- * that sits flush on another (a band on a crown) loses its seam. Tagging it with
- * a darker tone puts that seam back, without touching リアル / フラット, where the
- * part's own colour already draws it.
- */
-function tone(mesh, strength) {
- mesh.userData.tone = strength;
- return mesh;
-}
-
-/** シルクハット: a tall black crown, a narrow band and a flat brim. */
-function buildSilk() {
- const group = new THREE.Group();
- const black = material('#1b1722');
- const band = material('#3a2f4a');
- add(group, cylinder(1.52, 1.52, 0.12, 32), black, 0, 0.06, 0);
- add(group, cylinder(1.00, 1.04, 1.62, 32), black, 0, 0.93, 0);
- tone(add(group, cylinder(1.05, 1.05, 0.24, 32), band, 0, 0.30, 0), 0.34);
- return group;
-}
-
-/** キャップ: a baseball cap - a low dome, a rim and a bill at the front. */
-function buildCap() {
- const group = new THREE.Group();
- const blue = material('#3f6fd0');
- const dark = material('#2f54a4');
- // The crown: a hemisphere whose flat face sits on y = 0.
- const crown = add(group, sphere(1.08, 28, 18, 0, Math.PI * 2, 0, Math.PI / 2), blue, 0, 0, 0);
- crown.scale.set(1, 0.92, 1);
- tone(add(group, cylinder(1.10, 1.10, 0.18, 28), dark, 0, 0.09, 0), 0.30);
- // The bill, tipped down a little at the front.
- const bill = tone(add(group, cylinder(0.86, 0.86, 0.09, 24), dark, 0, 0.06, 0.92), 0.30);
- bill.scale.set(1, 1, 1.25);
- bill.rotation.x = -0.12;
- tone(add(group, sphere(0.13, 12, 10), dark, 0, 1.02, 0), 0.30);
- return group;
-}
-
-/** コック帽: a tall pleated toque - a cylindrical band under a big puffy crown. */
-function buildChef() {
- const group = new THREE.Group();
- const white = material('#f7f6f2');
- const shade = material('#e6e3da');
- // The band: a tall cylinder that sits on the head.
- add(group, cylinder(0.86, 0.92, 0.95, 28), white, 0, 0.475, 0);
- tone(add(group, cylinder(0.90, 0.90, 0.10, 28), shade, 0, 0.90, 0), 0.26);
- // The crown: a big squashed sphere ringed with lobes, so the top reads as the
- // classic pleated toque rather than a plain dome.
- const puff = add(group, sphere(1.24, 26, 20), white, 0, 1.30, 0);
- puff.scale.set(1, 0.82, 1);
- for (let i = 0; i < 8; i += 1) {
- const a = (i / 8) * Math.PI * 2;
- const lobe = add(group, sphere(0.5, 14, 12), white, Math.cos(a) * 0.92, 1.52, Math.sin(a) * 0.92);
- lobe.scale.set(1, 0.9, 1);
- }
- const top = add(group, sphere(0.62, 16, 12), white, 0, 1.92, 0);
- top.scale.set(1, 0.78, 1);
- return group;
-}
-
-/** 消防士の帽子: a red helmet with a brim, a top ridge and a small front badge. */
-function buildFire() {
- const group = new THREE.Group();
- const red = material('#c5342f');
- const dark = material('#8f241f');
- const gold = material('#f0c24a', { metalness: 0.4, roughness: 0.4 });
- const dome = add(group, sphere(1.06, 28, 18, 0, Math.PI * 2, 0, Math.PI / 2), red, 0, 0, 0);
- dome.scale.set(1, 0.95, 1);
- const brim = tone(add(group, cylinder(1.34, 1.34, 0.12, 28), dark, 0, 0.06, 0.18), 0.28);
- brim.scale.set(1, 1, 1.1);
- // The ridge along the top, front to back.
- const crest = tone(add(group, box(0.16, 0.42, 1.5), dark, 0, 1.02, 0), 0.28);
- crest.rotation.x = -0.05;
- tone(add(group, sphere(0.2, 14, 12), gold, 0, 0.62, 1.02), 0.22);
- return group;
-}
-
-const BUILDERS = {
- silk: buildSilk,
- cap: buildCap,
- chef: buildChef,
- fire: buildFire,
-};
-
-/** The hats offered in the panel: `none` clears it. */
-export const HAT_LIBRARY = [
- { id: 'none', label: 'なし' },
- { id: 'silk', label: 'シルクハット' },
- { id: 'cap', label: 'キャップ' },
- { id: 'chef', label: 'コック帽' },
- { id: 'fire', label: '消防士' },
-];
-
-/** Builds the named hat, or `null` for `none` / an unknown name. */
-export function buildHat(id) {
- const builder = BUILDERS[id];
- return builder ? builder() : null;
-}
diff --git a/bluebey-studio/src/history.js b/bluebey-studio/src/history.js
deleted file mode 100644
index d988b50..0000000
--- a/bluebey-studio/src/history.js
+++ /dev/null
@@ -1,105 +0,0 @@
-/**
- * Undo / redo for the whole studio state.
- *
- * The studio is a big pile of sliders, and before this every experiment was
- * one-way: nudging the wrong slider meant dialling the old value back by hand.
- * A history of whole-state snapshots is the simplest thing that can possibly
- * work here, because the state is already the single source of truth and every
- * control funnels through `applyState`.
- *
- * Two details matter for it to feel right rather than merely correct:
- *
- * - Dragging a slider fires an event per pixel, which would bury the history in
- * hundreds of near-identical entries. Entries therefore carry a label, and a
- * new entry with the *same* label within `coalesceMs` REPLACES the previous
- * one instead of stacking on top of it. So a whole drag becomes one step.
- *
- * - `view.backgroundImage` can be a multi-megabyte data URL, and `state` also
- * holds the caption text. The snapshots copy objects by hand rather than via
- * `JSON.parse(JSON.stringify(...))`, because assigning a string in JavaScript
- * shares it instead of duplicating it - so a hundred snapshots of a heavy
- * state stay cheap.
- */
-
-const SHALLOW_TYPES = new Set(['string', 'number', 'boolean', 'undefined']);
-
-/** Deep copy that shares string data (and handles the odd null/array). */
-function copy(value) {
- if (value === null || SHALLOW_TYPES.has(typeof value)) return value;
- if (Array.isArray(value)) return value.map(copy);
- if (typeof value === 'object') {
- const out = {};
- for (const [key, inner] of Object.entries(value)) out[key] = copy(inner);
- return out;
- }
- return value; // functions, symbols: not part of the saved state
-}
-
-export class History {
- constructor({ limit = 120, coalesceMs = 700, onChange = null } = {}) {
- this.limit = Math.max(2, limit);
- this.coalesceMs = coalesceMs;
- this.onChange = onChange;
- /** @type {{ state: object, label: string, at: number }[]} */
- this.entries = [];
- this.index = -1;
- }
-
- /** Forget everything and start from `state` (call after load/reset). */
- reset(state, label = 'start') {
- this.entries = [{ state: copy(state), label, at: Date.now() }];
- this.index = 0;
- this.onChange?.(this);
- }
-
- get canUndo() { return this.index > 0; }
- get canRedo() { return this.index >= 0 && this.index < this.entries.length - 1; }
-
- /** Label of the step undo would jump to, for the button tooltip. */
- get undoLabel() { return this.canUndo ? this.entries[this.index].label : null; }
- get redoLabel() { return this.canRedo ? this.entries[this.index + 1].label : null; }
-
- /**
- * Record the state *after* a change. Repeating the same label in quick
- * succession (a slider drag) keeps a single entry that follows the value.
- */
- push(state, label = '変更') {
- const now = Date.now();
- const top = this.entries[this.index];
- const sameDrag = top
- && top.label === label
- && now - top.at <= this.coalesceMs
- && this.index === this.entries.length - 1;
-
- if (sameDrag) {
- this.entries[this.index] = { state: copy(state), label, at: now };
- } else {
- this.entries.length = this.index + 1;
- this.entries.push({ state: copy(state), label, at: now });
- if (this.entries.length > this.limit) this.entries.shift();
- this.index = this.entries.length - 1;
- }
- this.onChange?.(this);
- return this;
- }
-
- /** The previous snapshot, or `null` when there is nothing to go back to. */
- undo() {
- if (!this.canUndo) return null;
- this.index -= 1;
- this.onChange?.(this);
- return copy(this.entries[this.index].state);
- }
-
- redo() {
- if (!this.canRedo) return null;
- this.index += 1;
- this.onChange?.(this);
- return copy(this.entries[this.index].state);
- }
-
- /** A plain description of where we are, for tests and debug output. */
- describe() {
- return this.entries.map((entry, i) => `${i === this.index ? '*' : ' '}${entry.label}`).join(' | ');
- }
-}
diff --git a/bluebey-studio/src/look.js b/bluebey-studio/src/look.js
deleted file mode 100644
index d755c3b..0000000
--- a/bluebey-studio/src/look.js
+++ /dev/null
@@ -1,184 +0,0 @@
-import * as THREE from 'three';
-import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
-
-/**
- * The "look" of the scene, as opposed to the character's shape: body colours,
- * mirroring, shadows and the lighting environment.
- *
- * These all live here rather than in `main.js` because they are a *policy* about
- * how the model should be presented, and they need to be re-applied as a group
- * whenever any of them changes. `main.js` only has to call `apply()`.
- *
- * Two things are worth knowing:
- *
- * - Colours are written onto the model's ORIGINAL materials. The flat/toon
- * style caches its own materials derived from them, so `styles.refreshColors()`
- * has to be called afterwards or the toon shading keeps the old colour.
- *
- * - Shadow softness is `LightShadow.radius` with a PCF soft shadow map, plus a
- * hand-drawn contact blob for the "ground shadow only" mode. (VSM was tried
- * for its wider blur and cut the shadow off in a straight line where it met
- * the feet - see the note in `apply`.)
- */
-
-export const ENVIRONMENTS = [
- { value: 'gradient', label: 'スタジオ(明るい)' },
- { value: 'room', label: '室内(自然な反射)' },
- { value: 'none', label: 'なし(のっぺり)' },
-];
-
-/** Material name -> which entry of `render.colors` it takes. */
-const PART_COLORS = {
- blb: 'body',
- Hand: 'accent',
- Foots: 'feet',
- Nose: 'nose',
- Leaf: 'leaf',
- Vein: 'vein',
-};
-
-const clamp01 = (value) => Math.min(1, Math.max(0, Number(value) || 0));
-
-/**
- * A soft blob that sits under the feet: the shadow a toy would cast on a table.
- * It is a plain plane with a radial gradient, so it is cheap and it works in the
- * line-art styles too, where the real shadow map is switched off.
- */
-function makeContactShadow(size) {
- const canvas = document.createElement('canvas');
- canvas.width = 128;
- canvas.height = 128;
- const ctx = canvas.getContext('2d');
- const gradient = ctx.createRadialGradient(64, 64, 4, 64, 64, 62);
- gradient.addColorStop(0, 'rgba(0,0,0,0.55)');
- gradient.addColorStop(0.55, 'rgba(0,0,0,0.28)');
- gradient.addColorStop(1, 'rgba(0,0,0,0)');
- ctx.fillStyle = gradient;
- ctx.fillRect(0, 0, 128, 128);
-
- const texture = new THREE.CanvasTexture(canvas);
- texture.colorSpace = THREE.SRGBColorSpace;
- const material = new THREE.MeshBasicMaterial({
- map: texture,
- transparent: true,
- depthWrite: false,
- toneMapped: false,
- });
- const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
- mesh.rotation.x = -Math.PI / 2;
- mesh.renderOrder = -0.5;
- mesh.userData.isHelper = true;
- mesh.name = 'contact-shadow';
- mesh.scale.setScalar(Math.max(1, size.x) * 1.15);
- return mesh;
-}
-
-export function createLook({ renderer, scene, styles, model, ground, key, character, gradientEnvironment }) {
- const pmrem = new THREE.PMREMGenerator(renderer);
- const roomEnvironment = pmrem.fromScene(new RoomEnvironment(), 0.06).texture;
-
- const contact = makeContactShadow(model.size);
- scene.add(contact);
-
- const environmentFor = (name) => {
- if (name === 'room') return roomEnvironment;
- if (name === 'none') return null;
- return gradientEnvironment ?? null;
- };
-
- function applyColors(colors) {
- if (!colors) return;
- let touched = false;
- for (const mesh of model.parts.body) {
- const material = styles.originals.get(mesh) ?? mesh.material;
- const part = PART_COLORS[material?.name];
- if (!part || !colors[part]) continue;
- if (!material.color) material.color = new THREE.Color();
- material.color.set(colors[part]);
- touched = true;
- }
- // The toon materials are cached copies, so they need the same edit.
- if (touched) styles.refreshColors?.();
- return touched;
- }
-
- return {
- contact,
-
- /**
- * @param {object} render `state.render`
- * @param {{ line?: boolean }} [context] `line` is true in the line-art
- * styles, where shadows and reflections are deliberately switched off.
- */
- apply(render, { line = false } = {}) {
- if (!render) return;
- applyColors(render.colors);
-
- // Mirroring by negative scale is safe: the renderer flips the winding for
- // a negative determinant, and the normals go through the inverse-transpose.
- character.scale.x = render.mirror ? -1 : 1;
-
- const wantsShadow = render.shadow !== false && !line;
- const blobOnly = render.contactShadow === true;
- ground.visible = wantsShadow && !blobOnly;
- ground.material.opacity = clamp01(render.shadowOpacity ?? 0.22);
- key.castShadow = wantsShadow && !blobOnly;
- key.shadow.needsUpdate = true;
-
- // The blob is drawn in BOTH modes. On its own it *is* the shadow (the
- // "ground shadow only" setting); with the shadow map on it fills the gap
- // where the body hides its own shadow right at the feet, which otherwise
- // reads as "the shadow is cut off". It is the only thing keeping the
- // character looking like it is standing on the floor rather than above it.
- // The blob sits at the feet. It used to be drawn whenever the shadow was on,
- // which left a faint ring behind at the origin once the character was moved;
- // it is now opt-in (`render.contactBlob`, default off). It is always shown in
- // the 「接地影だけ」 mode, where it is the only shadow there is.
- contact.visible = wantsShadow && (blobOnly || render.contactBlob === true);
- contact.material.opacity = clamp01((render.shadowOpacity ?? 0.22) * (blobOnly ? 2.6 : 1.6));
- const spread = Math.max(0.2, (model.size.x * 1.15) / Math.max(0.05, render.shadowSoftness ?? 1.6));
- contact.scale.setScalar(Math.max(0.5, model.size.x * 1.35 - spread * 0.25));
- contact.position.y = 0.012;
-
- const softness = Number(render.shadowSoftness) || 1.6;
- // PCF, always. three 0.186 removed PCFSoftShadowMap (asking for it warns and
- // falls back to this anyway), and VSM needs a depth-variance bias that cut
- // the shadow away in a straight line right at the feet. `radius` is what
- // softens PCF.
- if (renderer.shadowMap.type !== THREE.PCFShadowMap) {
- renderer.shadowMap.type = THREE.PCFShadowMap;
- // Every shadow-receiving material has to be recompiled for the switch.
- ground.material.needsUpdate = true;
- for (const mesh of model.parts.body) {
- const material = mesh.material;
- if (Array.isArray(material)) material.forEach((m) => { m.needsUpdate = true; });
- else if (material) material.needsUpdate = true;
- }
- }
- // PCF's radius is in *shadow map texels*, and the map covers about 15 world
- // units, so a radius of 1-2 is invisible. This scales it into something the
- // eye can see without the sampling turning into noise.
- if ('radius' in key.shadow) key.shadow.radius = Math.max(0.5, softness * 5);
-
- const environment = line ? null : environmentFor(render.environment);
- if (scene.environment !== environment) scene.environment = environment;
- if ('environmentIntensity' in scene) {
- scene.environmentIntensity = Number(render.envIntensity ?? 1);
- }
- },
-
- /** The colours a theme would set, as a plain `{ part: '#rrggbb' }`. */
- palette(theme) {
- return { ...(theme?.colors ?? {}) };
- },
-
- dispose() {
- contact.geometry.dispose();
- contact.material.map?.dispose();
- contact.material.dispose();
- contact.removeFromParent();
- roomEnvironment.dispose();
- pmrem.dispose();
- },
- };
-}
diff --git a/bluebey-studio/src/main.js b/bluebey-studio/src/main.js
deleted file mode 100644
index f861ade..0000000
--- a/bluebey-studio/src/main.js
+++ /dev/null
@@ -1,2798 +0,0 @@
-import * as THREE from 'three';
-import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
-import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
-import { loadModel } from './model.js';
-import { Face } from './face.js';
-import { Styles, isLineStyle } from './styles.js';
-import { Rig } from './rig.js';
-import { Animator, createRecorder, startRecording, stopRecording } from './animation.js';
-import { defaultState, applyPatch } from './presets.js';
-import { buildPanel } from './panel.js';
-import * as exporter from './exporter.js';
-import { toast } from './ui.js';
-import { History } from './history.js';
-import { createLook } from './look.js';
-import { createClipper } from './clip.js';
-import { buildHat } from './hats.js';
-import { createBackdrop, backdropStyle } from './background.js';
-import { captionFontStack } from './textOutlines.js';
-import { buildProp, PROP_DEFAULTS, disposeProp, applyPropText } from './props.js';
-import { rollAll, decodeSeed } from './gacha.js';
-import { encodeState, decodeState } from './urlState.js';
-import { createScreenOutline, BEHIND_LABEL, SOLID_LABEL, LEAF_LABEL, NOSE_LABEL } from './outline.js';
-import { gionSheetUrl, stampRect, imageAspect, imageReady, drawGion, DEFAULT_STAMP_WIDTH } from './gion.js';
-
-const DEG = Math.PI / 180;
-/** The other direction: what three's Spherical reports is radians. */
-const RAD_TO_DEG = 180 / Math.PI;
-const MODEL_URL = 'assets/bluebey.glb';
-
-/** How far the eyes can swing for a look-at target, in radians. */
-const MAX_LOOK_YAW = 0.5;
-const MAX_LOOK_PITCH = 0.34;
-
-/** Undo steps are labelled by the part of the state that changed. */
-const SCOPE_LABELS = {
- all: '変更', render: '見た目', face: '表情', view: 'カメラ・背景', pose: 'ポーズ', gion: '擬音',
-};
-
-/**
- * The single-file build inlines the fonts and the backdrop library, so the same
- * code reads a data URL there and a file URL in the normal build.
- */
-function assetUrl(file, kind) {
- const inlined = globalThis.__BLUEBEY_FONTS__ ?? {};
- if (kind === 'background') {
- const backdrops = globalThis.__BLUEBEY_BACKGROUNDS__ ?? {};
- return backdrops[file] ?? `assets/backgrounds/${file}.webp`;
- }
- if (kind === 'beard') {
- const beards = globalThis.__BLUEBEY_BEARDS__ ?? {};
- return beards[file] ?? `assets/beards/${file}`;
- }
- return inlined[file] ?? `assets/fonts/${file}`;
-}
-
-/**
- * Where the model comes from. The normal build fetches `assets/bluebey.glb`;
- * the single-file build (tools/build-standalone.mjs) inlines the same bytes as
- * base64 in `window.__BLUEBEY_MODEL__`, which also lets the page be opened
- * straight from disk without any web server.
- */
-function resolveModelSource() {
- const injected = globalThis.__BLUEBEY_MODEL__;
- if (!injected) return MODEL_URL;
- if (typeof injected !== 'string') return injected;
- try {
- const binary = atob(injected);
- const bytes = new Uint8Array(binary.length);
- for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
- return bytes.buffer;
- } catch (error) {
- console.error('inline model could not be decoded, falling back to the file', error);
- return MODEL_URL;
- }
-}
-
-const state = defaultState();
-
-const app = {
- state,
- model: null,
- container: null,
- ready: false,
- needsRender: true,
- /** The 擬音 stamp the panel is editing, so the viewport can outline it. */
- gionSelected: null,
- /** Set by init(), used by the panel. */
- actions: {},
-};
-
-/* ------------------------------------------------------------------ startup */
-
-// The view-rig classes are declared below this point; deferring by a microtask
-// guarantees the whole module has finished evaluating before anything is built.
-queueMicrotask(() => {
- init().catch((error) => {
- console.error(error);
- showLoadError(error);
- });
-});
-
-async function init() {
- const canvas = document.getElementById('view');
- // #stage owns the layout; the canvas only fills it (see style.css).
- const stage = document.getElementById('stage') ?? canvas;
-
- const renderer = new THREE.WebGLRenderer({
- canvas,
- antialias: true,
- alpha: true,
- preserveDrawingBuffer: true,
- });
- renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
- renderer.outputColorSpace = THREE.SRGBColorSpace;
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
- renderer.shadowMap.enabled = true;
- renderer.shadowMap.type = THREE.PCFShadowMap;
-
- const scene = new THREE.Scene();
- const environment = makeEnvironment(renderer);
-
- const key = new THREE.DirectionalLight(0xffffff, 2.1);
- key.castShadow = true;
- key.shadow.mapSize.set(1536, 1536);
- key.shadow.bias = -0.0008;
- key.shadow.normalBias = 0.02;
- const fill = new THREE.DirectionalLight(0xffffff, 0.5);
- const ambient = new THREE.HemisphereLight(0xffffff, 0xd9d0f2, 0.9);
- scene.add(key, key.target, fill, ambient);
-
- // A shadow catcher, not a solid floor: `depthWrite: false` keeps the plane from
- // ever hiding the character, so moving ぶるべー below it no longer makes the body
- // (or its textures) vanish - the shadow still falls on it, but it never occludes.
- const ground = new THREE.Mesh(
- new THREE.PlaneGeometry(200, 200),
- new THREE.ShadowMaterial({ opacity: 0.22, transparent: true, depthWrite: false }),
- );
- ground.rotation.x = -Math.PI / 2;
- ground.receiveShadow = true;
- scene.add(ground);
-
- // The camera lives in two places: `state.view` (what the panel, the saved file
- // and a shared link use) and the orbit controls (what the mouse moves). Capture
- // the controls' own position back into the state on every change, so the two
- // never disagree - otherwise any refresh of the view yanked the camera back to
- // wherever the panel had last put it.
- const view = new ViewRig(canvas, () => {
- view.captureInto(state.view);
- app.needsRender = true;
- });
-
- /**
- * The screen-space outline (see src/outline.js). The normal pass must not see
- * the ground, the contact shadow or the gizmo, so they are excluded once those
- * objects exist (see below).
- */
- const outline = createScreenOutline({
- renderer,
- scene,
- camera: view.camera,
- width: canvas.clientWidth || 1280,
- height: canvas.clientHeight || 800,
- });
- outline.exclude([ground]);
-
- /**
- * The drawing buffer must follow the canvas' CSS size. Without this the
- * browser stretches the default 300x150 buffer across the window and the
- * whole picture looks coarse (and every exported trace is built from those
- * few pixels).
- *
- * But it must not be *unlimited* either: on a large or high-density display
- * the buffer can reach many millions of pixels, and with antialiasing, a
- * shadow map and preserveDrawingBuffer on top, a weak GPU can take seconds
- * per frame - which looks exactly like "nothing is showing". So the pixel
- * count is capped and the ratio is lowered automatically on slow hardware.
- */
- const MAX_DRAWING_PIXELS = 2_600_000;
- const QUALITY_FLOOR = 0.5;
- let quality = Math.min(window.devicePixelRatio || 1, 2);
- let autoReductions = 0;
-
- function fitQuality(width, height, wanted) {
- let ratio = wanted;
- while (ratio > QUALITY_FLOOR && width * height * ratio * ratio > MAX_DRAWING_PIXELS) {
- ratio = Math.round((ratio - 0.1) * 100) / 100;
- }
- return Math.max(QUALITY_FLOOR, ratio);
- }
-
- let lastResize = 0;
- let resizeBurst = 0;
-
- /** Declared early: the viewport can resize before the model has loaded. */
- let overlayCanvas = null;
- /** Set while undo/redo replays a snapshot, so it is not recorded again. */
- let suspendHistory = false;
-
- // ------------------------------------------------------------------- 擬音
- // `redrawOverlay` runs from the very first `resizeViewport`, so the sheet cache
- // and the drag handles have to exist before it does (a `const` cannot be read
- // before it is evaluated). The drawing itself lives further down.
- const gionImages = new Map(); // sheet name -> loaded Image
- const gionLoading = new Map(); // sheet name -> in-flight Promise
- const gionHandles = new Map(); // stamp id -> transparent drag div
- let gionDrag = null;
- /** Hands out stamp ids; a counter so removing one never reuses a live id. */
- let gionSeq = 0;
-
- function resizeViewport() {
- const width = stage.clientWidth || window.innerWidth;
- const height = stage.clientHeight || window.innerHeight;
-
- // Safety net: if something ever feeds the viewport size back into itself,
- // stop flipping the drawing buffer on and off and say so.
- const now = performance.now();
- if (now - lastResize < 60) {
- resizeBurst += 1;
- if (resizeBurst > 30 && resizeBurst % 30 === 0) {
- console.warn('[bluebey] too many resizes in a row; ignoring them', resizeBurst);
- }
- if (resizeBurst > 30) return;
- } else {
- resizeBurst = 0;
- }
- lastResize = now;
-
- const wanted = Math.min(window.devicePixelRatio || 1, 2);
- quality = fitQuality(width, height, wanted);
- renderer.setPixelRatio(quality);
- renderer.setSize(width, height, false);
- outline.setSize(width * quality, height * quality);
- view.resize();
- redrawOverlay();
- app.needsRender = true;
- }
- resizeViewport();
- new ResizeObserver(() => resizeViewport()).observe(stage);
-
- // Losing the WebGL context leaves a permanently blank canvas, so say so
- // instead of leaving the user staring at nothing.
- canvas.addEventListener('webglcontextlost', (event) => {
- event.preventDefault();
- showNotice('描画が止まりました(WebGLコンテキストを失いました)。\n'
- + 'ブラウザのウィンドウを小さくするか、再読み込みしてください。');
- });
- canvas.addEventListener('webglcontextrestored', () => {
- app.needsRender = true;
- });
-
- // -------------------------------------------------------------- the model
- const model = await loadModel(resolveModelSource(), {
- onProgress: (ratio) => setLoadingProgress(ratio),
- });
- app.model = model;
-
- // The character hangs off a mirror group of its own, so "左右反転" flips the
- // model without touching the pose offset (`container.position`) or the props.
- const mirrorGroup = new THREE.Group();
- mirrorGroup.add(model.root);
- const container = new THREE.Group();
- container.add(mirrorGroup);
- scene.add(container);
- app.container = container;
-
- // 小物 live outside the mirror group: they are scenery, not part of the body.
- const propRoot = new THREE.Group();
- scene.add(propRoot);
-
- // --- 鼻ちょうちん: a modelled bubble hung under the nose --------------------
- // The nose is a *skinned* part of the body, so a child of the nose mesh would
- // sit at the origin instead of on the face. The whole body (nose included)
- // rides the `master` bone, so the bubble is parented there, placed at the nose's
- // rest position in that bone's local frame, and read from the face state each
- // frame in the render loop.
- function applySnotBubble(group, snot) {
- if (!group) return;
- group.visible = snot?.enabled === true;
- if (!group.visible) return;
- // The offsets are applied in the *bone's* frame, along the world axes measured
- // once at rest, so the bubble travels with the body. Recomputing them against
- // the live world matrix made it snap back whenever the body had moved.
- const k = 0.01; // artwork px -> world units, enough to nudge the bubble
- const { localBase, axes } = group.userData;
- group.position.copy(localBase)
- .addScaledVector(axes.x, (snot.offsetX ?? 0) * k)
- .addScaledVector(axes.y, (snot.offsetY ?? 0) * k)
- .addScaledVector(axes.z, (snot.offsetZ ?? 0) * k);
- group.userData.baseScale = Number.isFinite(snot.size) ? snot.size : 1;
- group.scale.setScalar(group.userData.baseScale);
- group.userData.material.color.set(snot.color ?? '#dfe8ff');
- }
-
- const snotBubble = (() => {
- const nose = model.parts.noseMesh;
- const master = model.bones.find((entry) => entry.name.replace(/[.\s]/g, '').toLowerCase() === 'master');
- if (!nose || !master || !nose.isSkinnedMesh) return null;
- model.root.updateMatrixWorld(true);
- // The nose is a *skinned* mesh, so its raw geometry sits in bind space (at the
- // back of the model, in fact) - sample the skinned positions instead, which is
- // where the nose actually ends up.
- const attr = nose.geometry.attributes.position;
- const step = Math.max(1, Math.floor(attr.count / 400));
- const point = new THREE.Vector3();
- const centre = new THREE.Vector3();
- let samples = 0;
- for (let i = 0; i < attr.count; i += step) {
- point.fromBufferAttribute(attr, i);
- nose.applyBoneTransform(i, point);
- centre.add(point);
- samples += 1;
- }
- if (!samples) return null;
- centre.multiplyScalar(1 / samples);
- nose.localToWorld(centre);
- let radius = 0;
- for (let i = 0; i < attr.count; i += step) {
- point.fromBufferAttribute(attr, i);
- nose.applyBoneTransform(i, point);
- nose.localToWorld(point);
- radius = Math.max(radius, point.distanceTo(centre));
- }
- if (!Number.isFinite(radius) || radius <= 0) radius = 0.25;
- const material = new THREE.MeshStandardMaterial({
- color: 0xdfe8ff, roughness: 0.45, metalness: 0, transparent: true, opacity: 0.92,
- });
- const group = new THREE.Group();
- group.name = 'snot-bubble';
- const bubble = new THREE.Mesh(new THREE.SphereGeometry(radius * 1.15, 28, 20), material);
- group.add(bubble);
- const bone = master.bone;
- const baseWorld = centre.clone().add(new THREE.Vector3(radius * 1.9, -radius * 0.85, radius * 0.7));
- const localBase = bone.worldToLocal(baseWorld.clone());
- // World-axis directions expressed in the bone's frame, captured at rest: the
- // sliders keep meaning "right / up / towards the viewer" while the bubble rides
- // with the body.
- const boneQuat = new THREE.Quaternion();
- bone.getWorldQuaternion(boneQuat);
- const invQuat = boneQuat.clone().invert();
- const axes = {
- x: new THREE.Vector3(1, 0, 0).applyQuaternion(invQuat),
- y: new THREE.Vector3(0, 1, 0).applyQuaternion(invQuat),
- z: new THREE.Vector3(0, 0, 1).applyQuaternion(invQuat),
- };
- group.position.copy(localBase);
- group.userData = { bubble, material, bone, localBase, axes, baseScale: 1 };
- group.visible = false;
- bone.add(group);
- return group;
- })();
- const snotMeshes = snotBubble ? [...snotBubble.children] : [];
- applySnotBubble(snotBubble, state.face.eyes.snot);
-
- // The hat rides the `master` bone like the snot bubble does: it is parented
- // there, placed at the top of the head in that bone's local frame, and turned
- // so it is world-aligned at rest (the bone's own rest rotation cancelled once).
- // The character's front is +z, so the hats are modelled facing +z.
- const hatMount = (() => {
- const master = model.bones.find((entry) => entry.name.replace(/[.\s]/g, '').toLowerCase() === 'master');
- if (!master) return null;
- model.root.updateMatrixWorld(true);
- const bone = master.bone;
- const group = new THREE.Group();
- group.name = 'hat';
- group.position.copy(bone.worldToLocal(new THREE.Vector3(0, model.bounds.max.y - 0.06, 0)));
- group.quaternion.copy(bone.getWorldQuaternion(new THREE.Quaternion()).invert());
- bone.add(group);
- return group;
- })();
-
- let hatKindCurrent = null;
- let hatObject = null;
- /** A GLB hat the user loaded; kept so it can be re-chosen without re-importing. */
- let customHatObject = null;
- const hatMeshes = [];
- /** Last transform applied, so the render loop only wakes when it changes. */
- const hatXform = { x: null, z: null, height: null, tiltX: null, tiltZ: null };
- /** Set once the outline pass exists, so a hat change can refresh the exclude set. */
- let refreshOutlineExclusion = null;
- /** Set once the style system exists, so a hat can register for styles + outline. */
- let hatStyleRegister = null;
- let hatStyleUnregister = null;
-
- /**
- * Swap the mounted hat for `built` (or clear it), keeping the style system and
- * the outline exclusion in step.
- *
- * The old meshes are collected BEFORE they are unregistered: `removeMesh`
- * deletes the outline hull (a child of the hat group) while we traverse, which
- * used to shorten `children` mid-loop and call `.traverse` on `undefined`.
- */
- function mountHat(built) {
- if (hatObject) {
- const old = [];
- hatObject.traverse((o) => { if (o.isMesh) old.push(o); });
- hatMount.remove(hatObject);
- for (const mesh of old) hatStyleUnregister?.(mesh);
- // The custom model is kept so it can be re-chosen; a built hat is dropped.
- if (hatObject !== customHatObject) {
- for (const mesh of old) {
- mesh.geometry?.dispose();
- if (Array.isArray(mesh.material)) mesh.material.forEach((m) => m.dispose());
- else mesh.material?.dispose();
- }
- }
- hatObject = null;
- }
- hatMeshes.length = 0;
- if (built) {
- hatMount.add(built);
- hatObject = built;
- const meshes = [];
- built.traverse((o) => { if (o.isMesh) meshes.push(o); });
- for (const mesh of meshes) {
- hatMeshes.push(mesh);
- hatStyleRegister?.(mesh);
- }
- }
- refreshOutlineExclusion?.();
- }
-
- function applyHat(kind) {
- if (!hatMount) return;
- const custom = state.face.hat?.custom === true && customHatObject;
- const key = custom ? 'custom' : kind;
- if (key !== hatKindCurrent) {
- hatKindCurrent = key;
- mountHat(custom ? customHatObject : buildHat(kind));
- }
-
- // 高さ and 傾き. The mount is world-aligned (see above), so +y is up, +z is the
- // character's front and +x its right: a plain translation and rotation read
- // the way the sliders say.
- const hat = state.face.hat ?? {};
- const x = hat.x ?? 0;
- const z = hat.z ?? 0;
- const height = hat.height ?? 0;
- const tiltX = hat.tiltX ?? 0;
- const tiltZ = hat.tiltZ ?? 0;
- if (hatObject) {
- hatObject.position.set(x, height, z);
- hatObject.rotation.set((tiltX * Math.PI) / 180, 0, (tiltZ * Math.PI) / 180);
- }
- if (x !== hatXform.x || z !== hatXform.z || height !== hatXform.height || tiltX !== hatXform.tiltX || tiltZ !== hatXform.tiltZ) {
- hatXform.x = x;
- hatXform.z = z;
- hatXform.height = height;
- hatXform.tiltX = tiltX;
- hatXform.tiltZ = tiltZ;
- app.needsRender = true;
- }
- }
- /**
- * Import a GLB hat. It is centred sideways, dropped so its base sits at y = 0
- * (the mount sits on the head), and scaled to about the head's width, then
- * mounted like any built hat.
- */
- async function loadHatModel(file) {
- if (!hatMount) return;
- const buffer = await exporter.readFileAsArrayBuffer(file);
- const loader = new GLTFLoader();
- const gltf = await new Promise((resolve, reject) => loader.parse(buffer, '', resolve, reject));
- const root = gltf.scene;
- const box = new THREE.Box3().setFromObject(root);
- const size = box.getSize(new THREE.Vector3());
- const target = Math.max(0.2, model.size.x * 0.95);
- const scale = target / Math.max(size.x, size.z, 0.001);
- root.scale.setScalar(scale);
- box.setFromObject(root);
- const center = box.getCenter(new THREE.Vector3());
- root.position.x -= center.x;
- root.position.z -= center.z;
- root.position.y -= box.min.y;
- root.traverse((o) => { if (o.isMesh) { o.castShadow = true; o.receiveShadow = false; } });
- customHatObject = root;
- state.face.hat.custom = true;
- state.face.hat.kind = 'none';
- hatKindCurrent = null;
- refresh('all');
- app.panel?.sync();
- }
-
- applyHat(state.face.hat?.kind ?? 'none');
-
- const halfHeight = model.size.y * 0.5;
- view.frame(model.size, halfHeight);
-
- // shadow camera big enough for the whole character
- const radius = Math.max(model.size.x, model.size.y, model.size.z) * 1.6;
- const shadowCamera = key.shadow.camera;
- shadowCamera.left = -radius;
- shadowCamera.right = radius;
- shadowCamera.top = radius;
- shadowCamera.bottom = -radius;
- shadowCamera.near = 0.5;
- shadowCamera.far = radius * 9;
- shadowCamera.updateProjectionMatrix();
- key.target.position.set(0, halfHeight, 0);
-
- // The light's offset from the character, so the key light (and therefore its
- // shadow camera) can follow the character around: moving far from the origin
- // used to leave the shadow behind and cut it off at the shadow camera's edge.
- const keyOffset = new THREE.Vector3(0, model.size.y * 3, model.size.y * 3);
- function updateKeyLight() {
- key.target.position.set(container.position.x, halfHeight + container.position.y, container.position.z);
- key.position.copy(key.target.position).add(keyOffset);
- }
-
- // ------------------------------------------------------------- controllers
- const styles = new Styles({ meshes: model.parts.body });
- // Hats are built at runtime, so they are not in the GLB's mesh list. Register
- // them with the style system (and the current hat) so they follow flat/line art
- // and get an outline hull like every other part.
- hatStyleRegister = (mesh) => styles.addMesh(mesh, { tone: true, lineOnly: true });
- hatStyleUnregister = (mesh) => styles.removeMesh(mesh);
- for (const mesh of hatMeshes) styles.addMesh(mesh, { tone: true, lineOnly: true });
- const face = new Face({
- eyeMesh: model.parts.eyeMesh,
- mouthMesh: model.parts.mouthMesh,
- hairMesh: model.parts.hairMesh,
- originals: model.originals,
- });
- const animator = new Animator({ state });
-
- // ------------------------------------------------------------------- look
- const look = createLook({
- renderer,
- scene,
- styles,
- model,
- ground,
- key,
- character: mirrorGroup,
- gradientEnvironment: environment,
- });
-
- // --------------------------------------------------------------- 見えない壁
- // A clip plane that hides part of the character, so it can look half buried
- // in a wall (see src/clip.js). Applied in `refresh`, with the render settings.
- const clip = createClipper({ scene, renderer, model });
- // A wall's position is stored relative to the character, so every time the
- // walls are applied they are shifted by the body's own offset - that is what
- // makes them travel with ぶるべー when it is moved.
- const characterOffset = () => {
- const root = state.pose?.root;
- return { x: root?.[0] ?? 0, y: root?.[1] ?? 0, z: root?.[2] ?? 0 };
- };
- const applyWalls = () => clip.apply(
- [state.render.wall, state.render.wall2],
- characterOffset(),
- wallYaw(),
- );
- // The character's own turn about Y: the `master` bone's Y (Shift+drag) plus any
- // body yaw the 見る先 mode added. The walls take this too, so turning the
- // character with Shift+drag no longer slides the hidden region around.
- const wallYaw = () => {
- const masterY = state.pose?.bones?.master?.[1] ?? 0;
- return (masterY + (container.rotation.y * 180) / Math.PI) * DEG;
- };
-
- // --------------------------------------------------------------- backdrop
- const backdrop = createBackdrop({
- stage,
- resolveUrl: (name) => assetUrl(name, 'background'),
- onNeedsRender: () => { app.needsRender = true; },
- });
- /** The bitmap the exports paint behind the model (an Image or the camera video). */
- let backdropImage = null;
-
- /**
- * Story-panel previews (keyed by panel id) and the counter that hands out
- * those ids.
- *
- * WHY the previews live outside the state: a thumbnail is an image, and the
- * state is exactly what a shared link carries - a data URL per panel would
- * push the link past what a URL can hold. They are keyed by id and rebuilt as
- * panels are added, so a restored link simply shows panels without previews.
- * (Declared up here because `buildPanel` runs - and syncs - before the panel
- * functions below are reached.)
- */
- const storyThumbs = new Map();
- let storySeq = 0;
-
- // ------------------------------------------------------- 擬音 overlay
- // The 擬音 stamps are drawn into their own 2D canvas laid over the WebGL one.
- overlayCanvas = document.createElement('canvas');
- overlayCanvas.id = 'overlay-layer';
- overlayCanvas.style.cssText =
- 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:2';
- stage.append(overlayCanvas);
-
- function redrawOverlay() {
- if (!overlayCanvas) return;
- const width = stage.clientWidth || 1280;
- const height = stage.clientHeight || 800;
- const ratio = Math.min(2, window.devicePixelRatio || 1);
- const wantW = Math.round(width * ratio);
- const wantH = Math.round(height * ratio);
- if (overlayCanvas.width !== wantW || overlayCanvas.height !== wantH) {
- overlayCanvas.width = wantW;
- overlayCanvas.height = wantH;
- }
- const ctx = overlayCanvas.getContext('2d');
- ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
- ctx.clearRect(0, 0, width, height);
- const gionItems = state.gion?.items ?? [];
- for (const item of gionItems) ensureGionSheet(item.sheet);
- drawGion(ctx, gionItems, gionImages, { width, height, scale: 1 });
- ensureGionHandles(gionItems);
- positionGionHandles(gionItems);
- }
-
- // ----------------------------------------------------------------- 擬音
- // A stamp is a crop of a sheet, drawn on the overlay canvas.
- // The canvas is `pointer-events: none`, so each stamp gets its own transparent
- // drag box, parked over it by `positionGionHandles`.
-
- /** Load a sheet once, on first use, and redraw when it arrives. */
- function ensureGionSheet(name) {
- if (gionImages.has(name)) return Promise.resolve(gionImages.get(name));
- if (gionLoading.has(name)) return gionLoading.get(name);
- const image = new Image();
- const promise = new Promise((resolve) => {
- image.onload = () => resolve(image);
- image.onerror = () => resolve(null);
- }).then((loaded) => {
- gionLoading.delete(name);
- if (loaded) {
- gionImages.set(name, loaded);
- redrawOverlay();
- }
- return loaded;
- });
- gionLoading.set(name, promise);
- image.src = gionSheetUrl(name);
- return promise;
- }
-
- /** Create one drag target per stamp, and drop the ones whose stamp is gone. */
- function ensureGionHandles(items) {
- const live = new Set();
- for (const item of items) {
- live.add(item.id);
- if (gionHandles.has(item.id)) continue;
- const handle = document.createElement('div');
- handle.className = 'gion-handle';
- handle.addEventListener('pointerdown', (event) => startGionDrag(event, item.id));
- handle.addEventListener('pointermove', moveGionDrag);
- handle.addEventListener('pointerup', endGionDrag);
- handle.addEventListener('pointercancel', endGionDrag);
- handle.dataset.gionHandle = '1';
- stage.append(handle);
- gionHandles.set(item.id, handle);
- }
- for (const [id, handle] of gionHandles) {
- if (live.has(id)) continue;
- handle.remove();
- gionHandles.delete(id);
- }
- }
-
- /** Park each drag target over its stamp (or hide it while the sheet loads). */
- function positionGionHandles(items) {
- const width = stage.clientWidth || 1280;
- const height = stage.clientHeight || 800;
- for (const item of items) {
- const handle = gionHandles.get(item.id);
- if (!handle) continue;
- const image = gionImages.get(item.sheet);
- if (!imageReady(image)) {
- handle.style.display = 'none';
- continue;
- }
- const rect = stampRect(item, { width, height, scale: 1 }, imageAspect(image));
- handle.style.display = 'block';
- handle.style.left = `${rect.x}px`;
- handle.style.top = `${rect.y}px`;
- handle.style.width = `${rect.w}px`;
- handle.style.height = `${rect.h}px`;
- // Match the drawn stamp, which is rotated about its centre.
- handle.style.transformOrigin = 'center';
- handle.style.transform = item.rot ? `rotate(${item.rot}deg)` : 'none';
- handle.classList.toggle('selected', item.id === app.gionSelected);
- }
- }
-
- function startGionDrag(event, id) {
- const item = (state.gion?.items ?? []).find((entry) => entry.id === id);
- if (!item) return;
- // Touching a stamp also selects it in the panel, so the sliders edit the one
- // you just grabbed.
- app.panel?.selectGion?.(id);
- event.preventDefault();
- event.stopPropagation();
- try {
- gionHandles.get(id)?.setPointerCapture(event.pointerId);
- } catch {
- /* capture is a nicety, not a requirement */
- }
- gionDrag = {
- id,
- pointerId: event.pointerId,
- fromX: event.clientX,
- fromY: event.clientY,
- x: item.x ?? 0.5,
- y: item.y ?? 0.5,
- };
- }
-
- function moveGionDrag(event) {
- if (!gionDrag || event.pointerId !== gionDrag.pointerId) return;
- const item = (state.gion?.items ?? []).find((entry) => entry.id === gionDrag.id);
- if (!item) return;
- event.preventDefault();
- const width = stage.clientWidth || 1280;
- const height = stage.clientHeight || 800;
- item.x = clamp01(gionDrag.x + (event.clientX - gionDrag.fromX) / width);
- item.y = clamp01(gionDrag.y + (event.clientY - gionDrag.fromY) / height);
- refresh('gion');
- }
-
- function endGionDrag(event) {
- if (!gionDrag || (event && event.pointerId !== gionDrag.pointerId)) return;
- gionDrag = null;
- app.actions.syncPanel?.();
- }
-
- /** Add a stamp from a picker crop, centred, and hand it to the panel. */
- function addGionStamp({ sheet, sx, sy, sw, sh }) {
- gionSeq += 1;
- const item = {
- id: `g${gionSeq}`,
- sheet,
- sx, sy, sw, sh,
- x: 0.5,
- y: 0.5,
- w: DEFAULT_STAMP_WIDTH,
- rot: 0,
- flip: false,
- };
- state.gion = state.gion ?? { items: [] };
- state.gion.items = [...(state.gion.items ?? []), item];
- app.gionSelected = item.id;
- void ensureGionSheet(sheet);
- applyState({}, { scope: 'gion', sync: true });
- toast('擬音を追加しました');
- }
-
- // ------------------------------------------------------------------ props
- let propSignature = '';
-
- function applyProps() {
- const items = state.props?.items ?? [];
- const signature = JSON.stringify([items, state.render.colors]);
- if (signature === propSignature) return;
- propSignature = signature;
- for (const child of [...propRoot.children]) {
- // Hand every mesh back to the style system before the prop is disposed.
- // `removeMesh` restores each mesh's own material (a prop under 線画 is
- // wearing the shared paper material, which must not be disposed). Collect
- // the meshes first: `removeMesh` deletes the outline hull, a child of the
- // prop group.
- const meshes = [];
- child.traverse((object) => {
- if (styles.originals.has(object)) meshes.push(object);
- });
- for (const mesh of meshes) styles.removeMesh(mesh);
- propRoot.remove(child);
- disposeProp(child);
- }
- for (const item of items) {
- const def = PROP_DEFAULTS[item.kind] ?? { x: 0, y: 0, z: 0, rotX: 0, rotY: 0, rotZ: 0, scale: 1 };
- let group;
- try {
- group = buildProp(item.kind, { colors: state.render.colors, scale: item.scale ?? def.scale ?? 1 });
- } catch (error) {
- console.warn('[bluebey] unknown prop', item.kind, error);
- continue;
- }
- group.position.set(item.x ?? def.x ?? 0, item.y ?? def.y ?? 0, item.z ?? def.z ?? 0);
- group.rotation.set(
- (item.rotX ?? def.rotX ?? 0) * DEG,
- (item.rotY ?? def.rotY ?? 0) * DEG,
- (item.rotZ ?? def.rotZ ?? 0) * DEG,
- );
- propRoot.add(group);
- // Register with the style system, so フラット and 線画 reach the props too
- // and each part gets an outline hull like the body and the hats. `lineOnly`
- // keeps that outline out of リアル/フラット, where the props read by shading.
- group.traverse((object) => {
- if (object.isMesh) styles.addMesh(object, { lineOnly: true });
- });
- // 看板 only: paint the saved text onto the freshly built face (no-op for
- // every other prop, which has no writing surface).
- applyPropText(group, item.text);
- }
- refreshOutlineExclusion?.();
- app.needsRender = true;
- }
-
- // ------------------------------------------------------------ prop dragging
- // A prop can be dragged with the mouse. The pointer picks one, then moves it in
- // the plane that faces the camera, so it follows the cursor at whatever depth
- // it already sits at. (The ground plane is the other obvious choice, but the
- // camera sits almost level with the floor, so the floor is nearly edge-on and
- // a prop dragged across it would fly off.)
- //
- // The listener is on `stage` in the CAPTURE phase: that runs before the canvas
- // and `view`'s container, so stopping the event there keeps the orbit controls
- // and the bone picking out of a prop drag.
- const propRaycaster = new THREE.Raycaster();
- const propPointer = new THREE.Vector2();
- const propPlane = new THREE.Plane();
- const propPoint = new THREE.Vector3();
- const propNormal = new THREE.Vector3();
- let propDrag = null;
- // The 見えない壁 uses the same machinery: `wallDrag` keeps the grabbed point's
- // offset from the wall's anchor so grabbing a corner does not make it jump.
- const wallDrag = { pointerId: null, key: 'wall', offset: new THREE.Vector3() };
-
- /** The pointer in normalised device coordinates, i.e. what a raycaster wants. */
- function pointerNdc(event) {
- const rect = canvas.getBoundingClientRect();
- return propPointer.set(
- ((event.clientX - rect.left) / Math.max(1, rect.width)) * 2 - 1,
- -((event.clientY - rect.top) / Math.max(1, rect.height)) * 2 + 1,
- );
- }
-
- /** The prop under the pointer as `{ index, group, point }`, or null. */
- function propAt(event) {
- if (!propRoot.children.length) return null;
- propRaycaster.setFromCamera(pointerNdc(event), view.camera);
- const hits = propRaycaster.intersectObjects(propRoot.children, true);
- if (!hits.length) return null;
- // The meshes hang off the group `propRoot` holds, so walk up to that group.
- let node = hits[0].object;
- while (node.parent && node.parent !== propRoot) node = node.parent;
- const items = state.props?.items ?? [];
- const index = propRoot.children.indexOf(node);
- if (index < 0 || index >= items.length) return null;
- return { index, group: node, point: hits[0].point.clone() };
- }
-
- /**
- * The point on the wall's guide under the pointer, or null.
- *
- * Only while the wall is on AND its guide is switched on: the guide is both
- * what there is to grab and the switch that says "I am placing the wall now",
- * so with it off a huge invisible plane cannot swallow every orbit drag.
- */
- function wallAt(event) {
- propRaycaster.setFromCamera(pointerNdc(event), view.camera);
- let best = null;
- for (let index = 0; index < clip.guides.length; index += 1) {
- const key = index === 0 ? 'wall' : 'wall2';
- const wall = state.render[key];
- if (wall?.on !== true || wall?.guide !== true) continue;
- const hits = propRaycaster.intersectObject(clip.guides[index], false);
- if (!hits.length) continue;
- if (!best || hits[0].distance < best.distance) {
- best = { index, key, point: hits[0].point.clone() };
- }
- }
- return best;
- }
-
- function onPropPointerDown(event) {
- if (event.button !== 0) return;
- if (rig?.controls?.dragging) return; // the rotation gizmo is in charge
- const hit = propAt(event);
- if (hit) {
- // Claim the pointer, so nothing underneath sees it.
- event.stopPropagation();
- event.preventDefault();
- view.camera.getWorldDirection(propNormal);
- propPlane.setFromNormalAndCoplanarPoint(propNormal.negate(), hit.point);
- propDrag = {
- pointerId: event.pointerId,
- index: hit.index,
- group: hit.group,
- // Grabbing a corner rather than the middle must not make it jump.
- offset: hit.group.position.clone().sub(hit.point),
- };
- stage.style.cursor = 'grabbing';
- app.needsRender = true;
- return;
- }
-
- const onWall = wallAt(event);
- if (!onWall) {
- // Nothing to grab. Remember the press, and settle on pointer-up whether it
- // was a tap (set the 見る先 target) or the start of an orbit.
- if (state.lookAt?.enabled === true && !rig?.controls?.dragging) {
- lookTap = { pointerId: event.pointerId, x: event.clientX, y: event.clientY };
- }
- return;
- }
- // A prop wins if it is under the pointer; otherwise the wall takes it.
- event.stopPropagation();
- event.preventDefault();
- view.camera.getWorldDirection(propNormal);
- propPlane.setFromNormalAndCoplanarPoint(propNormal.negate(), onWall.point);
- const wall = state.render[onWall.key] ?? (state.render[onWall.key] = defaultState().render[onWall.key]);
- const offset = characterOffset();
- const yaw = wallYaw();
- const cos = Math.cos(yaw);
- const sin = Math.sin(yaw);
- const px = onWall.point.x - offset.x;
- const pz = onWall.point.z - offset.z;
- wallDrag.pointerId = event.pointerId;
- wallDrag.key = onWall.key;
- // Keep the grab point's offset *in the wall's own (character-relative) frame*,
- // so grabbing a corner does not make the wall jump.
- wallDrag.offset.set(
- (wall.x ?? 0) - (cos * px - sin * pz),
- (wall.y ?? 0) - (onWall.point.y - offset.y),
- (wall.z ?? 0) - (sin * px + cos * pz),
- );
- stage.style.cursor = 'grabbing';
- app.needsRender = true;
- }
-
- function onPropPointerMove(event) {
- if (lookTap && event.pointerId === lookTap.pointerId
- && Math.hypot(event.clientX - lookTap.x, event.clientY - lookTap.y) > LOOK_TAP_SLOP) {
- lookTap = null; // that was an orbit drag, not a tap
- }
- if (wallDrag.pointerId != null && event.pointerId === wallDrag.pointerId) {
- event.stopPropagation();
- propRaycaster.setFromCamera(pointerNdc(event), view.camera);
- if (!propRaycaster.ray.intersectPlane(propPlane, propPoint)) return;
- const offset = characterOffset();
- const yaw = wallYaw();
- const cos = Math.cos(yaw);
- const sin = Math.sin(yaw);
- const px = propPoint.x - offset.x;
- const pz = propPoint.z - offset.z;
- const wall = state.render[wallDrag.key] ?? (state.render[wallDrag.key] = defaultState().render[wallDrag.key]);
- // Back into the wall's character-relative frame, then the same ranges the
- // panel's sliders offer, so a drag and a number always describe the same
- // place.
- wall.x = round2(clampNumber(cos * px - sin * pz + wallDrag.offset.x, -4, 4));
- wall.y = round2(clampNumber(propPoint.y - offset.y + wallDrag.offset.y, -2, 4));
- wall.z = round2(clampNumber(sin * px + cos * pz + wallDrag.offset.z, -4, 4));
- applyWalls();
- app.needsRender = true;
- return;
- }
- if (!propDrag || event.pointerId !== propDrag.pointerId) return;
- event.stopPropagation();
- propRaycaster.setFromCamera(pointerNdc(event), view.camera);
- if (!propRaycaster.ray.intersectPlane(propPlane, propPoint)) return;
- const next = propPoint.add(propDrag.offset);
- const item = state.props?.items?.[propDrag.index];
- if (!item) return;
- // Clamp to the ranges the panel's sliders offer, so a drag can never put a
- // prop somewhere the numbers cannot describe (and never through the floor).
- item.x = round2(clampNumber(next.x, -4, 4));
- item.y = round2(clampNumber(next.y, 0, 2));
- item.z = round2(clampNumber(next.z, -4, 4));
- propDrag.group.position.set(item.x, item.y, item.z);
- // Keep `applyProps`' signature in step with what is on screen, so a refresh
- // during the drag does not tear the group down and rebuild it from the state
- // (which would re-create every geometry on every pointer move).
- propSignature = JSON.stringify([state.props?.items ?? [], state.render.colors]);
- app.needsRender = true;
- }
-
- function onPropPointerUp(event) {
- if (lookTap && (!event || event.pointerId === lookTap.pointerId)) {
- lookTap = null;
- if (event) applyLookTap(event);
- return;
- }
- if (wallDrag.pointerId != null && (!event || event.pointerId === wallDrag.pointerId)) {
- wallDrag.pointerId = null;
- stage.style.cursor = '';
- history.push(state, '見えない壁');
- app.panel?.sync();
- return;
- }
- if (!propDrag || (event && event.pointerId !== propDrag.pointerId)) return;
- propDrag = null;
- stage.style.cursor = '';
- // One undo step for the whole drag, and the panel catches up with the value.
- history.push(state, '小物');
- app.panel?.sync();
- }
-
- // A tap (a press that does not turn into a drag) in the viewport sets the
- // 見る先 target, when that mode is on. Cancelled as soon as the pointer moves,
- // so an orbit drag never moves the target by accident.
- let lookTap = null;
- const LOOK_TAP_SLOP = 5;
-
- stage.addEventListener('pointerdown', onPropPointerDown, { capture: true });
- window.addEventListener('pointermove', onPropPointerMove);
- window.addEventListener('pointerup', onPropPointerUp);
- window.addEventListener('pointercancel', onPropPointerUp);
-
- // ---------------------------------------------------------------- look-at
- const lookScratch = new THREE.Vector3();
- const headScratch = new THREE.Vector3();
- const lookPlane = new THREE.Plane();
- const floorPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
- const lookPlaneNormal = new THREE.Vector3();
- const lookPivot = new THREE.Vector3();
- const lookHit = new THREE.Vector3();
- const lookFloor = new THREE.Vector3();
- /** Where the eyes should aim this frame, from `state.lookAt` (a world point). */
- let aimOffset = { x: 0, y: 0 };
-
- /**
- * The world point under a viewport tap, for the 見る先 target.
- *
- * The tap is projected onto the first thing it can sensibly mean: a vertical
- * plane through the character that faces the camera (so a tap on the body or
- * the backdrop keeps the character's own depth), or the floor, for a tap that
- * lands on the ground in front. The nearer of the two wins.
- */
- function lookAtPointFrom(event) {
- propRaycaster.setFromCamera(pointerNdc(event), view.camera);
- const ray = propRaycaster.ray;
- const headY = container.position.y + model.size.y * 0.72;
- lookPlaneNormal.set(ray.direction.x, 0, ray.direction.z);
- if (lookPlaneNormal.lengthSq() < 1e-8) lookPlaneNormal.set(0, 0, 1);
- lookPlane.setFromNormalAndCoplanarPoint(
- lookPlaneNormal.normalize(),
- lookPivot.set(container.position.x, headY, container.position.z),
- );
- const onPlane = ray.intersectPlane(lookPlane, lookHit);
- const onFloor = ray.direction.y < -1e-4 ? ray.intersectPlane(floorPlane, lookFloor) : null;
- if (!onPlane) return onFloor;
- if (!onFloor) return onPlane;
- return onPlane.distanceToSquared(ray.origin) <= onFloor.distanceToSquared(ray.origin) ? onPlane : onFloor;
- }
-
- /** A tap in the viewport: aim the eyes at whatever was under the pointer. */
- function applyLookTap(event) {
- if (state.lookAt?.enabled !== true) return;
- const point = lookAtPointFrom(event);
- if (!point) return;
- const cfg = state.lookAt;
- // The same ranges the panel's sliders offer, so a tap and a number describe
- // the same place.
- cfg.x = round2(clampNumber(point.x, -6, 6));
- cfg.y = round2(clampNumber(point.y, 0, 5));
- cfg.z = round2(clampNumber(point.z, -6, 6));
- history.push(state, '見る先');
- refresh('lookAt');
- app.panel?.sync();
- }
-
- function applyLookAt() {
- const cfg = state.lookAt;
- if (!cfg?.enabled) {
- // Switching the mode off leaves the character where it was: the body turn
- // and the gaze are remembered, not re-derived, so nothing snaps back.
- container.rotation.y = (cfg?.bodyYawDeg ?? 0) * DEG;
- const frozen = cfg?.freeze;
- return frozen
- ? { x: clamp(frozen.x ?? 0, -1, 1), y: clamp(frozen.y ?? 0, -1, 1) }
- : { x: 0, y: 0 };
- }
-
- container.updateMatrixWorld(true);
- lookScratch.set(cfg.x ?? 0, cfg.y ?? 0, cfg.z ?? 0);
- mirrorGroup.worldToLocal(lookScratch);
- headScratch.set(0, model.size.y * 0.72, 0);
- lookScratch.sub(headScratch);
-
- const flat = Math.hypot(lookScratch.x, lookScratch.z) || 1e-6;
- const yaw = Math.atan2(lookScratch.x, lookScratch.z);
- const pitch = Math.atan2(lookScratch.y, flat);
- const amount = clamp(cfg.amount ?? 1, 0, 1);
- const aim = {
- x: clamp((yaw / MAX_LOOK_YAW) * amount, -1, 1),
- y: clamp((pitch / MAX_LOOK_PITCH) * amount, -1, 1),
- };
-
- // Turning the whole body is what a person does to look behind themselves; a
- // half-strength turn keeps the feet planted while the body leans round. The
- // turn is *stored*, so switching 体も向ける off stops updating it and the body
- // stays where it is instead of snapping back to the front.
- if (cfg.turnBody) {
- cfg.bodyYawDeg = round2(clamp(yaw, -MAX_LOOK_YAW * 1.6, MAX_LOOK_YAW * 1.6) * amount * 0.7 / DEG);
- }
- container.rotation.y = (cfg.bodyYawDeg ?? 0) * DEG;
-
- // Keep the gaze too, for the moment the mode is switched off.
- cfg.freeze = aim;
- return aim;
- }
-
- // ---------------------------------------------------- history, gacha
- const history = new History({ limit: 120 });
-
- let rig = null;
- // The gizmo always belongs to whichever camera is currently active.
- view.onCameraChange = (camera, controls) => {
- if (!rig) return;
- rig.controls.camera = camera;
- rig.orbit = controls;
- };
-
- rig = new Rig({
- bones: model.bones,
- scene,
- camera: view.camera,
- domElement: canvas,
- orbit: view.controls,
- pickTargets: () => [model.parts.eyeMesh, model.parts.mouthMesh, ...model.parts.body],
- onChange: () => {
- capturePose();
- app.panel?.onRigChanged?.();
- app.needsRender = true;
- },
- });
-
- // The ground, the contact shadow and the gizmo are scenery, not character, and
- // the two face plates are the *front half* of the body: their open edge would be
- // picked up as a silhouette and draw a line across the face, while contributing
- // nothing, since they sit exactly on the body.
- //
- // The outline hulls must stay out too, and that one is easy to miss: they are
- // extra copies of every mesh sitting in the scene graph, and this pass swaps the
- // material of every visible mesh. A hull is an expanded *back-face* shell, so
- // handing it a plain front-side label material puts an expanded copy of the
- // whole character in front of itself - it then paints its own label over
- // everything, and every leaf edge against the body is inked as if the body were
- // empty paper. That is exactly the faint dotted line along the leaves' bases.
- function outlineExclusion() {
- return [
- ground,
- look.contact,
- ...clip.guides,
- rig.helper,
- model.parts.eyeMesh,
- model.parts.mouthMesh,
- ...(model.parts.hairMesh ? [model.parts.hairMesh] : []),
- ...snotMeshes,
- ...hatMeshes,
- ...styles.outlineMeshes,
- ];
- }
- outline.exclude(outlineExclusion());
- refreshOutlineExclusion = () => outline.exclude(outlineExclusion());
- // The wall is *not* excluded: it is handed to the pass as an occluder, so the
- // labels behind it are culled as well. Excluded, the leaves and the nose
- // buried in the wall kept their outlines, because the labels are read before
- // its depth is applied.
- outline.occlude(clip.occluders);
-
- /**
- * The parts the screen-space pass draws, with the label each one writes.
- *
- * The leaves are the hard case: a hull cannot outline a shell that thin. They
- * are drawn with the LEAF label, which means the pass inks them where they meet
- * the paper and where they fold against each other, but *not* where they run
- * into the body - a line there reads as the leaf sinking into the body, and the
- * original artwork has none (the leaf simply passes behind the body).
- *
- * The nose joins them in the line-art styles, with the NOSE label, so its ring
- * against the body *is* drawn: it is the only thing that shows the nose in a
- * line drawing. In the shaded styles it is left out entirely, and its hull then
- * keeps only the part that pokes out of the silhouette - which is what the
- * original artwork does (the nose reads by its own colour there).
- *
- * @param {string} style
- * @returns {Array<[import('three').Object3D, number]>}
- */
- function screenParts(style) {
- const parts = (model.parts.leaves ?? []).map((mesh) => [mesh, LEAF_LABEL]);
- if (isLineStyle(style) && model.parts.noseMesh) parts.push([model.parts.noseMesh, NOSE_LABEL]);
- return parts;
- }
-
- /**
- * The label every part writes (see src/outline.js).
- *
- * The leaves and the nose are the outlined ones. The body's label is the one
- * real choice here, and it is exposed as `render.leafBodyLine`:
- *
- * - `BEHIND` (the default) makes the body count as paper, so each leaf is
- * outlined where it emerges from the body. In a line drawing the two are
- * paper on paper, so without this the leaves and the body merge into one
- * white shape with no way to tell them apart - which is the greater evil.
- * - `SOLID` makes the body block that line, so a leaf simply passes behind it.
- * Cleaner where it works, but the leaves lose their outline there.
- *
- * Everything else - the feet, the hands, the props - is *merely behind*, so it
- * stays BEHIND either way: it still hides what is behind it, but a leaf lying
- * across it keeps its outline.
- */
- function outlineLabels(parts) {
- const labels = new Map(parts);
- const bodyLabel = state.render.leafBodyLine === false ? SOLID_LABEL : BEHIND_LABEL;
- for (const mesh of model.parts.body ?? []) {
- if (labels.has(mesh)) continue;
- labels.set(mesh, model.parts.kinds?.get(mesh) === 'blb' ? bodyLabel : BEHIND_LABEL);
- }
- return [...labels];
- }
-
- /** Point both outline systems at the right parts for `method`. */
- function applyOutlineMethod(style, method = state.render.outlineMethod) {
- const hybrid = method === 'screen';
- const parts = hybrid ? screenParts(style) : [];
- styles.setHullHidden(parts.map(([mesh]) => mesh));
- outline.only(parts.length ? outlineLabels(parts) : null);
- }
-
- /**
- * Run `fn` with the outline pass set up for another style, then put the
- * on-screen configuration back. Both the hull/screen split and which parts the
- * screen pass may see are shared by the live view and every offscreen render,
- * so a pass that draws another style has to say so - the SVG and the "lines
- * only" PNG are ink through and through, so they always want the line-art
- * arrangement (a hull cannot outline the leaves at all).
- */
- async function withOutlineFor({ style, method = 'screen' }, fn) {
- applyOutlineMethod(style, method);
- try {
- return await fn();
- } finally {
- applyOutlineMethod(state.render.style);
- }
- }
-
- Object.assign(app, {
- renderer, scene, camera: view.camera, view, styles, face, rig, animator,
- key, ambient, ground, environment, halfHeight, outline, clip,
- look, backdrop, history, propRoot, mirrorGroup, container,
- get backdropImage() { return backdropImage; },
- });
-
- const recorder = createRecorder(canvas);
-
- // -------------------------------------------------------------- the panel
- const panel = buildPanel(app, document.getElementById('panel'));
- app.panel = panel;
-
- // On a phone the panel is a bottom sheet. Its grab handle drags the sheet's top
- // edge up and down, so the menu can be pulled out of the way of the character.
- const panelEl = document.getElementById('panel');
- const panelGrip = document.createElement('div');
- panelGrip.className = 'panel-grip';
- panelGrip.setAttribute('aria-hidden', 'true');
- panelEl.prepend(panelGrip);
- installSheetDrag(panelEl, panelGrip);
-
- /** Drag on the phone sheet's handle; a no-op anywhere else. */
- function installSheetDrag(sheet, grip) {
- const phone = window.matchMedia('(max-width: 768px), (max-height: 500px) and (pointer: coarse)');
- let drag = null;
- const minHeight = () => Math.round(window.innerHeight * 0.16);
- const maxHeight = () => Math.round(window.innerHeight * 0.94);
-
- grip.addEventListener('pointerdown', (event) => {
- if (!phone.matches) return;
- const rect = sheet.getBoundingClientRect();
- // Start from the height the sheet has now, so the first move does not jump.
- drag = { id: event.pointerId, fromY: event.clientY, startH: rect.height };
- sheet.style.maxHeight = 'none';
- sheet.style.height = `${rect.height}px`;
- try { grip.setPointerCapture(event.pointerId); } catch { /* capture is optional */ }
- event.preventDefault();
- });
- grip.addEventListener('pointermove', (event) => {
- if (!drag || event.pointerId !== drag.id) return;
- // Dragging up grows the sheet (its top edge climbs); dragging down shrinks it.
- const next = drag.startH + (drag.fromY - event.clientY);
- sheet.style.height = `${Math.min(maxHeight(), Math.max(minHeight(), next))}px`;
- event.preventDefault();
- });
- const end = (event) => {
- if (!drag || (event && event.pointerId !== drag.id)) return;
- drag = null;
- };
- grip.addEventListener('pointerup', end);
- grip.addEventListener('pointercancel', end);
-
- // A window grown to the desktop layout keeps the side column: drop the height
- // the drag had fixed, so the panel goes back to filling the screen.
- const clearOnDesktop = () => {
- if (phone.matches) return;
- sheet.style.height = '';
- sheet.style.maxHeight = '';
- };
- window.addEventListener('resize', clearOnDesktop);
- }
-
- Object.assign(app.actions, {
- capturePose,
- applyState,
- refresh,
- loadHatModel,
- patch: (scope, value) => applyState({ [scope]: value }, { scope }),
- syncPanel: () => app.panel?.sync(),
- imageCanvas: (options) => captureImage(options),
- savePNG: (options = {}) => savePNG(options),
- copyPNG: () => copyPNG(),
- faceMapCanvas: (kind) => faceMapCanvas(kind),
- saveFaceMap: (kind) => saveFaceMap(kind),
- addGionStamp: (item) => addGionStamp(item),
- saveSettings: () => exporter.downloadText(JSON.stringify(state, null, 2), `bluebey-settings-${exporter.timestamp()}.json`),
- loadSettings: (text) => {
- const data = JSON.parse(text);
- applyState(data, { full: true, sync: true });
- toast('設定を読み込みました');
- },
- toggleRecording: () => toggleRecording(recorder),
- resetPose: () => {
- rig.reset();
- state.pose = { bones: {}, root: [0, 0, 0] };
- applyState({}, { scope: 'pose', sync: true });
- toast('ポーズをリセットしました');
- },
- setCameraPreset: (id) => setCameraPreset(id),
- undo: () => restoreFromHistory('undo'),
- redo: () => restoreFromHistory('redo'),
- rollGacha: (seed) => rollGacha(seed),
- copyShareLink: () => copyShareLink(),
- shareImage: (network) => shareImage(network),
- storyThumbs: () => storyThumbs,
- toggleCamera: (on) => toggleCamera(on),
- addProp: (kind) => addProp(kind),
- addStoryPanel: () => addStoryPanel(),
- saveStory: (options) => saveStory(options),
- loadFromHash: () => loadFromHash(),
- toggleAnimation: () => {
- state.anim.mode = (state.anim.mode ?? 'off') === 'off' ? 'idle' : 'off';
- app.animator.reset();
- panel.sync();
- app.needsRender = true;
- },
- });
-
- installKeys();
- installTopbar();
- window.addEventListener('focus', () => { app.needsRender = true; });
- document.addEventListener('visibilitychange', () => {
- if (!document.hidden) app.needsRender = true;
- });
-
- {
- const buffer = renderer.getDrawingBufferSize(new THREE.Vector2());
- console.log(
- '[bluebey] ready'
- + ` canvas=${canvas.clientWidth}x${canvas.clientHeight}`
- + ` buffer=${Math.round(buffer.x)}x${Math.round(buffer.y)}`
- + ` quality=${quality}`
- + ` dpr=${window.devicePixelRatio}`
- + ` model=${model.size.toArray().map((v) => v.toFixed(2)).join(',')}`
- + ` camera=${view.camera.position.toArray().map((v) => v.toFixed(2)).join(',')}`,
- );
- }
- window.addEventListener('resize', () => resizeViewport());
-
- applyState({}, {});
- await loadFromHash();
- history.reset(state);
- rig.select('master', { silent: true });
- panel.sync();
-
- let last = performance.now();
- let faceSignature = '';
- let slowFrames = 0;
- let framesSinceFps = 0;
- let fpsAt = performance.now();
- const warmupUntil = performance.now() + 2500;
-
- let snotSignature = null;
- const loop = (now) => {
- const rawDelta = now - last;
- const dt = Math.min(0.05, rawDelta / 1000);
- last = now;
-
- animator.update(dt);
-
- const posed = animator.pose(state.pose);
- if (!rig.controls.dragging) rig.applyPose(posed);
- container.position.set(posed.root[0] ?? 0, posed.root[1] ?? 0, posed.root[2] ?? 0);
- updateKeyLight();
- // The contact blob rides under the character, so it never lingers at the origin.
- look.contact.position.x = container.position.x;
- look.contact.position.z = container.position.z;
-
- applyHat(state.face.hat?.kind ?? 'none');
-
- if (snotBubble) {
- const snot = state.face.eyes.snot;
- const sig = `${snot.enabled}|${snot.size}|${snot.offsetX}|${snot.offsetY}|${snot.offsetZ}|${snot.color}`;
- if (sig !== snotSignature) {
- snotSignature = sig;
- applySnotBubble(snotBubble, snot);
- app.needsRender = true;
- }
- // The sleeping breath swells the bubble; `snotScale` is 1 otherwise.
- if (snotBubble.visible) {
- const scale = (snotBubble.userData.baseScale ?? 1) * (animator.snotScale ?? 1);
- if (snotBubble.scale.x !== scale) {
- snotBubble.scale.setScalar(scale);
- app.needsRender = true;
- }
- }
- }
-
- const params = buildFaceParams();
- const signature = JSON.stringify(params);
- if (signature !== faceSignature) {
- faceSignature = signature;
- face.setParams(params, faceMode());
- app.needsRender = true;
- }
- if (face.flush(now, 26)) app.needsRender = true;
-
- view.controls.update(dt);
-
- const animating = (state.anim.mode ?? 'off') !== 'off'
- || state.anim.lookAround
- || animator.blinkPhase >= 0
- || recorder?.recorder?.state === 'recording';
- // Always paint for the first couple of seconds, so a missed "needs render"
- // can never leave an empty window staring back at the user.
- if (app.needsRender || animating || view.controlsChanged || now < warmupUntil) {
- app.needsRender = false;
-
- // The screen-space outline needs one extra pass *before* the scene, so it
- // wraps the normal render instead of following it. `outline.render` paints
- // the scene itself (ink and all), so there must be no second render after
- // it - that would clear the canvas and wipe the ink straight back off.
- const wantsScreen = state.render.outlineMethod === 'screen' && state.render.outline !== false;
- outline.render(() => renderer.render(scene, view.camera), {
- enabled: wantsScreen,
- camera: view.camera,
- color: state.render.outlineColor,
- // The label buffer is `quality`x the CSS size, so the radius has to carry
- // that factor to keep the leaf/nose line a constant width in CSS pixels -
- // otherwise it halves on a HiDPI display and jumps when the app lowers
- // the quality on slow frames.
- radius: outlineRadiusFor(view, state, quality),
- });
- view.controlsChanged = false;
- framesSinceFps += 1;
-
- // Hardware that cannot keep up gets a smaller drawing buffer instead of a
- // slideshow (a very large buffer can look like "nothing ever appears").
- if (rawDelta > 90) slowFrames += 1;
- else slowFrames = 0;
- if (slowFrames >= 8 && quality > QUALITY_FLOOR + 0.01 && autoReductions < 3) {
- autoReductions += 1;
- quality = Math.max(QUALITY_FLOOR, Math.round((quality - 0.25) * 100) / 100);
- renderer.setPixelRatio(quality);
- slowFrames = 0;
- console.warn('[bluebey] frames are slow, lowering the drawing quality to', quality);
- }
- }
-
- if (now - fpsAt > 1000) {
- const fps = Math.round((framesSinceFps * 1000) / (now - fpsAt));
- framesSinceFps = 0;
- fpsAt = now;
- const buffer = renderer.getDrawingBufferSize(new THREE.Vector2());
- panel.setStats?.(
- `描画 ${Math.round(buffer.x)}×${Math.round(buffer.y)}(画質 ${quality.toFixed(2)}×)/約 ${fps}fps`
- + (autoReductions > 0 ? ' ※重いので自動で軽くしました' : ''),
- );
- }
-
- requestAnimationFrame(loop);
- };
- requestAnimationFrame(loop);
-
- hideLoading();
- app.ready = true;
- window.__bluebeyReady = true;
- window.__bluebey = {
- app,
- state,
- set: (patch) => applyState(patch),
- face: (patch) => applyState({ face: patch }),
- pose: (patch) => applyState({ pose: patch }),
- view: (patch) => applyState({ view: patch }),
- render: (patch) => applyState({ render: patch }),
- selectBone: (name) => rig.select(name),
- preset: (id) => app.actions.applyFacePreset?.(id),
- posePreset: (id) => app.actions.applyPosePreset?.(id),
- png: (options) => captureImage(options),
- };
-
- /* ------------------------------------------------------------- internals */
-
- /** The camera the outline pass should use (kept in step with the view rig). */
- function outlineCamera() {
- return view.camera;
- }
-
- /** The gizmo/frame loop re-reads the bones, so store them back into state. */
- function capturePose() {
- const pose = rig.getPose();
- state.pose.bones = pose.bones;
- }
-
- function buildFaceParams() {
- const eyes = state.face.eyes;
- const openScale = animator.eyeOpen;
- const drift = animator.look;
-
- const eyeFor = (key) => {
- const own = eyes[key];
- // A shut artwork (1〜3線 / 3の目 / わらう) does not blink: its lid stays where
- // the user put it, so the blink animation must not scale its `open`.
- const shutArt = own.shape === 'three' || own.shape === 'arch'
- || (typeof own.shape === 'string' && own.shape.startsWith('line'));
- return {
- // Which artwork this eye shows (ふつう / 1〜3線 / 3の目 / わらう / ハート).
- // The renderer chooses the shut artwork from this, so it must travel.
- shape: own.shape,
- open: clamp01(own.open * (shutArt ? 1 : openScale)),
- lookX: clamp(own.lookX + drift.x + aimOffset.x, -1, 1),
- lookY: clamp(own.lookY + drift.y + aimOffset.y, -1, 1),
- closed: own.closed,
- closedLines: own.closedLines,
- irisShape: own.irisShape,
- threeFlip: own.threeFlip === true,
- // 白目 / 光彩 are per eye and tri-state: `null` means "the usual" (open
- // eyes show their white; the shared 光彩 switch decides the glint).
- white: own.white ?? null,
- highlight: own.highlight ?? null,
- // 形の大きさ scales the drawn eye shape (the shut lines, the 3, the arch)
- // and the heart.
- shapeScale: own.shapeScale ?? 1,
- // The tears are per eye, and only drawn when their own switch is on.
- tearOn: own.tearOn === true,
- tear: own.tear ?? 0.3,
- tearY: own.tearY ?? 0,
- // Where this eye sits, and where its teardrop hangs, can be nudged one
- // side at a time (an asymmetric face).
- eyeX: own.eyeX ?? 0,
- tearX: own.tearX ?? 0,
- tearTilt: own.tearTilt ?? 0,
- // The lower lid can differ per eye (falling back to the shared value).
- lowerLid: own.lowerLid ?? eyes.lowerLid ?? 0,
- // The rest of the lids are per eye too now (null = use the shared value).
- lidShape: own.lidShape ?? null,
- lidWidth: own.lidWidth ?? null,
- lidTilt: own.lidTilt ?? null,
- lashes: own.lashes ?? null,
- lashAngle: own.lashAngle ?? null,
- lashPos: own.lashPos ?? null,
- };
- };
-
- // A look-at target drives the eyes without being part of the saved
- // expression; the mouth is used as-is.
- const mouth = { ...state.face.mouth };
-
- return {
- eyes: {
- // Everything shared by both eyes (colours, iris scale, glasses, ...) has
- // to travel with the per-eye values, because the face renderer
- // reads them from here. Spreading `...eyes` is also what makes the face
- // redraw when one of them (e.g. `glasses`) changes: the loop compares the
- // JSON of this object against the last one it drew.
- ...eyes,
- left: eyeFor('left'),
- right: eyeFor('right'),
- },
- mouth,
- };
- }
-
- function faceMode() {
- return isLineStyle(state.render.style) ? 'line' : 'paint';
- }
-
- function applyState(patch, { silent = false, full = false, scope = 'all', sync = false } = {}) {
- if (full) {
- const fresh = defaultState();
- applyPatch(fresh, patch);
- for (const key of Object.keys(fresh)) state[key] = fresh[key];
- } else {
- applyPatch(state, patch);
- }
- refresh(scope);
- app.needsRender = true;
- if (sync && !silent) app.panel?.sync();
- }
-
- /** Re-apply the parts of the state named by `scope` ('all' by default). */
- function refresh(scope = 'all') {
- app.needsRender = true;
- // Where the eyes aim depends on where the character is, so it is recomputed
- // whenever anything that moves could change.
- if (scope === 'all' || scope === 'view' || scope === 'pose' || scope === 'face' || scope === 'lookAt') {
- aimOffset = applyLookAt();
- }
- if (scope === 'all' || scope === 'render') {
- styles.setStyle(state.render.style);
- styles.setPaper(state.render.paper);
- styles.setOutlineWidth(state.render.outlineWidth);
- styles.setOutlineColor(state.render.outlineColor);
- styles.setOutlineEnabled(state.render.outline);
- // Which parts each outline method draws, and which hulls stand down for it
- // (the leaves, plus the nose in a line drawing). See `applyOutlineMethod`.
- applyOutlineMethod(state.render.style);
-
- const line = isLineStyle(state.render.style);
- // Shadows, reflections, body colours and mirroring are the look module's
- // job, so that they are applied together and stay consistent.
- look.apply(state.render, { line });
- applyProps();
- // 見えない壁: the one setting that changes what is drawn rather than how.
- applyWalls();
-
- const azimuth = state.render.lightAzimuth * DEG;
- const elevation = state.render.lightElevation * DEG;
- const distance = model.size.y * 3;
- keyOffset.set(
- Math.cos(elevation) * Math.sin(azimuth) * distance,
- Math.sin(elevation) * distance,
- Math.cos(elevation) * Math.cos(azimuth) * distance,
- );
- updateKeyLight();
- key.intensity = state.render.lightIntensity;
- ambient.intensity = state.render.ambient;
- key.color.set(state.render.lightColor ?? '#ffffff');
- ambient.color.set(state.render.ambientColor ?? '#ffffff');
- renderer.toneMappingExposure = state.render.exposure;
- applyBackground();
- renderer.shadowMap.needsUpdate = true;
- }
-
- if (scope === 'all' || scope === 'view') {
- applyBackdrop();
- // The renderer's *clear* depends on the background mode as well: a preset
- // photo, a loaded picture and the camera all show through a transparent
- // clear (see `backgroundOf`). Without this, switching the background left
- // the canvas clearing to the old opaque colour and hid the layer that had
- // just been set up behind it - which is why the background only appeared
- // after some other change (adding a prop) re-ran this.
- applyBackground();
- }
- if (scope === 'all' || scope === 'view' || scope === 'gion') redrawOverlay();
-
- // A style change flips the face between paint and ink (`faceMode`), so the
- // 'render' scope has to rebuild the face too - otherwise the old drawing
- // stays on the plates until something else (a blink) happens to dirty them.
- if (scope === 'all' || scope === 'face' || scope === 'render') {
- face.setSource('eyes', state.face.eyes.source);
- face.setSource('mouth', state.face.mouth.source);
- face.setParams(buildFaceParams(), faceMode());
- }
-
- if (scope === 'all' || scope === 'view') {
- view.setProjection(state.view.projection);
- view.apply(state.view, model.size);
- view.setAutoRotate(state.view.autoRotate, state.view.autoRotateSpeed);
- }
-
- if (scope === 'all' || scope === 'pose') {
- rig.applyPose(state.pose);
- const root = state.pose.root ?? [0, 0, 0];
- container.position.set(root[0] ?? 0, root[1] ?? 0, root[2] ?? 0);
- }
-
- // Every user-driven change funnels through here, so this is the one place
- // that has to remember a step for undo. Undo itself sets `suspendHistory`.
- if (!suspendHistory) history.push(state, SCOPE_LABELS[scope] ?? scope);
- }
-
- /** Paint the backdrop bitmap behind the model, for an export. */
- function backdropSource() {
- if (state.view.background === 'camera') {
- const video = backdrop.el?.querySelector?.('video');
- return video && video.readyState >= 2 ? video : null;
- }
- return backdropImage;
- }
-
- /**
- * Freeze the current camera frame into a canvas.
- *
- * The AR preview is a live video, so by the time the PNG finishes encoding the
- * feed has moved on and the saved picture no longer matches what was on screen
- * when the shutter was pressed. Snapping the frame first keeps the two in step.
- */
- function snapshotBackdrop() {
- if (state.view.background !== 'camera') return undefined;
- const video = backdrop.el?.querySelector?.('video');
- if (!video || video.readyState < 2 || !video.videoWidth || !video.videoHeight) return undefined;
- const frame = document.createElement('canvas');
- frame.width = video.videoWidth;
- frame.height = video.videoHeight;
- try {
- frame.getContext('2d').drawImage(video, 0, 0);
- } catch {
- return undefined;
- }
- return frame;
- }
-
- /**
- * Paint the backdrop *behind* the rendered model.
- *
- * WHY the scratch canvas and `destination-over`: `ctx` already holds the
- * character on a transparent field (see `exporter.renderStill`), so drawing the
- * photo straight onto it with the default `source-over` painted OVER the
- * character and the export came out as a bare photograph. Building the scene in
- * a scratch canvas first also lets the dark veil sit on top of the photo, which
- * it could not do once the photo had been slipped underneath.
- */
- function drawBackdropInto(ctx, width, height, scale, sourceOverride) {
- const source = sourceOverride ?? backdropSource();
- if (!source) return false;
- const style = backdropStyle(state.view);
- if (!style.visible) return false;
- const iw = source.naturalWidth || source.videoWidth || source.width || 0;
- const ih = source.naturalHeight || source.videoHeight || source.height || 0;
- if (!iw || !ih) return false;
-
- const back = document.createElement('canvas');
- back.width = width;
- back.height = height;
- const paint = back.getContext('2d');
-
- // The same CSS the live layer uses: blur (bleeding past the edges), the fit,
- // the mirror and the scale/offset transform. The offset is in the element's
- // own (pre-scale) space, so it is multiplied by `scale` here, exactly as
- // `transform: scale(s) translate(...)` does in CSS.
- const bleed = style.blur > 0 ? style.blur * 2 : 0;
- const w = width + bleed * 2;
- const h = height + bleed * 2;
- paint.save();
- if (style.blur > 0) paint.filter = `blur(${style.blur}px)`;
- paint.translate(
- width / 2 + style.scale * style.offset.x * width,
- height / 2 + style.scale * style.offset.y * height,
- );
- paint.scale(style.scale * (style.mirror ? -1 : 1), style.scale);
- paint.translate(-width / 2, -height / 2);
-
- if (style.backgroundSize === 'cover' || style.backgroundSize === 'contain') {
- const fit = style.backgroundSize === 'cover'
- ? Math.max(w / iw, h / ih)
- : Math.min(w / iw, h / ih);
- const dw = iw * fit;
- const dh = ih * fit;
- paint.drawImage(source, (w - dw) / 2 - bleed, (h - dh) / 2 - bleed, dw, dh);
- } else if (style.backgroundSize === 'auto') {
- // CSS repeats the picture at its own size and anchors the grid on the box
- // centre (`background-position: center`). A canvas pattern instead repeats
- // at the image's *intrinsic* size, which is `scale`x too small for a
- // canvas that is `scale`x the CSS box - so the export tiled `scale`x more
- // densely than the preview. Paint the tiles by hand at `scale`, on a grid
- // centred on the box centre; the CTM above already carries the offset /
- // backgroundScale / mirror, so the tiles must not re-apply them.
- const tw = iw * scale;
- const th = ih * scale;
- // Cover only the user-space area the visible canvas needs (widened by the
- // blur bleed), mapped back through the CTM. The CTM holds no rotation, so
- // its `a`/`d` scale factors are enough to invert it for the bounds.
- const matrix = paint.getTransform();
- const [left, right] = [
- (-bleed - matrix.e) / matrix.a,
- (width + bleed - matrix.e) / matrix.a,
- ].sort((a, b) => a - b);
- const [top, bottom] = [
- (-bleed - matrix.f) / matrix.d,
- (height + bleed - matrix.f) / matrix.d,
- ].sort((a, b) => a - b);
- const cx = width / 2;
- const cy = height / 2;
- const firstCol = Math.floor((left - cx) / tw);
- const lastCol = Math.ceil((right - cx) / tw);
- const firstRow = Math.floor((top - cy) / th);
- const lastRow = Math.ceil((bottom - cy) / th);
- for (let row = firstRow; row <= lastRow; row++) {
- for (let col = firstCol; col <= lastCol; col++) {
- paint.drawImage(source, cx + col * tw - tw / 2, cy + row * th - th / 2, tw, th);
- }
- }
- } else {
- paint.drawImage(source, -bleed, -bleed, w, h);
- }
- paint.restore();
-
- if (style.darken > 0) {
- paint.fillStyle = `rgba(0, 0, 0, ${style.darken})`;
- paint.fillRect(0, 0, width, height);
- }
-
- ctx.save();
- ctx.globalCompositeOperation = 'destination-over';
- ctx.drawImage(back, 0, 0);
- ctx.restore();
- return true;
- }
-
- /** Load (or clear) the backdrop bitmap that the exports composite. */
- function applyBackdrop() {
- const viewState = state.view;
- backdrop.apply(viewState);
- // The AR shutter rides the viewport, not the panel, so it shows/hides here.
- if (app.arShutter) app.arShutter.hidden = viewState.background !== 'camera';
- // The touch hint would sit under the shutter, so it steps aside in AR mode.
- const touchHint = document.getElementById('viewport-hint-touch');
- if (touchHint) touchHint.style.visibility = viewState.background === 'camera' ? 'hidden' : '';
- const url = viewState.background === 'preset'
- ? assetUrl(viewState.backgroundPreset, 'background')
- : viewState.background === 'image'
- ? viewState.backgroundImage
- : viewState.background === 'effect'
- ? backdrop.effectUrl(viewState.backgroundEffect)
- : null;
- if (!url) {
- if (viewState.background !== 'camera') backdropImage = null;
- return;
- }
- if (backdropImage?.dataset?.url === url) return;
- const image = new Image();
- image.dataset.url = url;
- image.onload = () => { backdropImage = image; app.needsRender = true; };
- image.onerror = () => { backdropImage = null; };
- image.src = url;
- }
-
- function applyBackground() {
- exporter.applyBackground(renderer, backgroundOf(state.view));
- }
-
- async function captureImage({ scale = 2, transparent = null, lines = null } = {}) {
- // Freeze the live camera frame up front, so the saved picture holds the frame
- // that was on screen when the button was pressed (see `snapshotBackdrop`).
- const frozenBackdrop = snapshotBackdrop();
- return withHiddenHelpers(async () => {
- // The face texture is redrawn on a throttle (every 26 ms) while the render
- // loop runs, so a capture taken straight after a change could still hold the
- // *previous* expression. That is what made a comic panel occasionally show
- // its neighbour's face: force the pending redraw through first.
- face.flush(performance.now(), 0);
- const wantsTransparent = transparent === true
- || (transparent == null && state.view.background === 'transparent');
- // A transparent export of a line drawing drops the paper as well. The whole
- // point of transparent line art is to lay the lines over another picture,
- // and a white silhouette would just hide it - so this renders the `outline`
- // style, which draws the ink and leaves the body out. A shaded style keeps
- // its body, because there it is an ordinary cut-out of the character.
- const wantsLines = lines ?? (wantsTransparent && isLineStyle(state.render.style));
- const drawn = wantsLines ? 'outline' : state.render.style;
- const background = wantsTransparent ? { mode: 'transparent' } : backgroundOf(state.view);
-
- const onScreen = styles.style;
- if (drawn !== onScreen) styles.setStyle(drawn);
- let result;
- try {
- result = await withOutlineFor({ style: drawn, method: wantsLines ? 'screen' : state.render.outlineMethod }, () => exporter.capturePNG({
- renderer,
- scene,
- camera: view.camera,
- outline,
- outlineOptions: {
- enabled: state.render.outlineMethod === 'screen' && state.render.outline !== false,
- color: state.render.outlineColor,
- // The export's label buffer is `scale`x the one on screen (renderStill
- // sets it to the output size), so its radius grows with it to keep the
- // line's weight relative to the model the same as in the preview.
- radius: outlineRadiusFor(view, state, scale),
- },
- width: canvas.clientWidth || window.innerWidth,
- height: canvas.clientHeight || window.innerHeight,
- restore: () => { resizeViewport(); applyStateBackgroundAgain(); },
- }, { scale, background }));
- } finally {
- if (drawn !== onScreen) styles.setStyle(onScreen);
- }
-
- // The backdrop and the 擬音 stamps live in DOM layers behind the WebGL
- // canvas, so an export has to paint them in itself - in the same order as
- // the screen: backdrop, then stamps.
- const ctx = result.canvas.getContext('2d');
- // The canvas' pixels-per-CSS-pixel: the same factor the 擬音 stamps are
- // drawn at below. The backdrop's tiled fit needs it too, so its tiles come
- // out the size the preview's CSS paints them.
- const base = canvas.clientWidth || 1280;
- const outputScale = result.width / Math.max(1, base);
- let changed = false;
- if (!wantsTransparent && drawBackdropInto(ctx, result.width, result.height, outputScale, frozenBackdrop)) changed = true;
- const gionItems = state.gion?.items ?? [];
- if (gionItems.length) {
- for (const item of gionItems) await ensureGionSheet(item.sheet);
- drawGion(ctx, gionItems, gionImages, {
- width: result.width,
- height: result.height,
- scale: outputScale,
- });
- changed = true;
- }
- if (changed) result.blob = await exporter.canvasToBlob(result.canvas);
- return result;
- });
- }
-
- function applyStateBackgroundAgain() {
- exporter.applyBackground(renderer, backgroundOf(state.view));
- }
-
- async function savePNG({
- scale = state.render.pngScale ?? 2,
- transparent = state.render.pngTransparent ? true : undefined,
- } = {}) {
- const result = await captureImage({ scale, transparent });
- exporter.downloadBlob(result.blob, `bluebey-${exporter.timestamp()}.png`);
- toast(`PNGを書き出しました(${result.width}×${result.height})`);
- }
-
- /**
- * The canvas the 下地 export paints on, *without* downloading it. Kept separate
- * from `saveFaceMap` so the in-page round-trip check can look at exactly the
- * pixels the button writes.
- */
- function faceMapCanvas(kind) {
- // A redraw is throttled while the render loop runs (see the loop's
- // `face.flush(now, 26)`), so force any pending one through first or the map
- // would show the expression before last.
- face.flush(performance.now(), 0);
- return exporter.buildFaceMap(face, kind);
- }
-
- async function saveFaceMap(kind) {
- const canvas = faceMapCanvas(kind);
- if (!canvas) {
- toast('この下地は書き出せませんでした');
- return;
- }
- const blob = await exporter.canvasToBlob(canvas);
- const label = kind === 'eyes' ? '目' : '口';
- exporter.downloadBlob(blob, `bluebey-face-${kind}-${exporter.timestamp()}.png`);
- toast(`${label}の下地を書き出しました(${canvas.width}×${canvas.height})。`
- + `この画像に描いて「${label}の画像を読み込む(PNG)」で読み込めます`);
- }
-
- /** Hide the gizmo and helpers so they never end up in an export. */
- async function withHiddenHelpers(fn) {
- const wasVisible = rig.helper.visible;
- const guidesWereVisible = clip.guides.map((guide) => guide.visible);
- rig.helper.visible = false;
- clip.guides.forEach((guide) => { guide.visible = false; });
- try {
- return await fn();
- } finally {
- rig.setGizmoVisible(wasVisible);
- clip.guides.forEach((guide, index) => { guide.visible = guidesWereVisible[index]; });
- app.needsRender = true;
- }
- }
-
- async function copyPNG() {
- const result = await captureImage({ scale: 2 });
- await exporter.copyCanvasToClipboard(result.canvas);
- toast('画像をクリップボードにコピーしました');
- }
-
- /**
- * Swap in another GLB.
- *
- * Kept as the implementation, but no longer wired to anything: the panel's
- * 「GLBを差し替える」 button and the window-wide drop handler were removed on
- * 2026-09-27. Putting the feature back is a button plus an action that calls
- * this (the model returns on reload, so nothing is saved).
- */
- async function replaceModel(file) {
- if (!file) return;
- try {
- const buffer = await exporter.readFileAsArrayBuffer(file);
- const next = await loadModel(buffer);
- styles.dispose();
- rig.dispose();
- container.remove(model.root);
- scene.remove(container);
- container.add(next.root);
- scene.add(container);
- app.model = next;
- toast(`${file.name} を読み込みました(このモデルは再読込で元に戻ります)`);
- app.needsRender = true;
- } catch (error) {
- console.error(error);
- toast('このファイルは読み込めませんでした');
- }
- }
-
- let recording = false;
- async function toggleRecording(session) {
- if (!session) {
- toast('この環境では録画できません');
- return;
- }
- if (!recording) {
- if (!state.anim.idle) {
- state.anim.idle = true;
- panel.sync();
- }
- recording = startRecording(session);
- toast('録画を開始しました(もう一度押すと停止)');
- } else {
- const blob = await stopRecording(session);
- recording = false;
- if (blob) {
- exporter.downloadBlob(blob, `bluebey-animation-${exporter.timestamp()}.webm`);
- toast('録画を保存しました(WebM)');
- }
- }
- app.panel?.setRecording(recording);
- }
-
- function setCameraPreset(id) {
- const presets = {
- front: { azimuth: 0, polar: 82 },
- threeQuarter: { azimuth: 34, polar: 78 },
- side: { azimuth: 90, polar: 84 },
- back: { azimuth: 180, polar: 82 },
- top: { azimuth: 24, polar: 26 },
- };
- applyState({ view: presets[id] ?? presets.front }, { scope: 'view', sync: true });
- }
-
- function restoreFromHistory(kind) {
- const snapshot = kind === 'undo' ? history.undo() : history.redo();
- if (!snapshot) {
- toast(kind === 'undo' ? 'これ以上戻れません' : 'やり直す操作がありません');
- return;
- }
- suspendHistory = true;
- try {
- applyState(snapshot, { full: true, sync: true });
- } finally {
- suspendHistory = false;
- }
- toast(kind === 'undo' ? '1つ戻しました' : 'やり直しました');
- }
-
- /** おまかせ: a seeded random expression, so it can be reproduced and shared. */
- function rollGacha(seedText) {
- const seed = seedText || String(Math.floor(Math.random() * 1e9));
- const roll = rollAll(seed);
- suspendHistory = true;
- try {
- applyState(roll.patch, { scope: 'all' });
- } finally {
- suspendHistory = false;
- }
- history.push(state, 'おまかせ');
- app.panel?.sync();
- const shown = decodeSeed(seed) ?? String(seed);
- toast(`おまかせ表情(シード ${shown})`);
- return shown;
- }
-
- async function copyShareLink() {
- try {
- const encoded = await encodeState(state);
- const link = `${location.origin}${location.pathname}#s=${encoded}`;
- await navigator.clipboard.writeText(link);
- toast(`この見た目のリンクをコピーしました(${link.length}文字)`);
- } catch (error) {
- console.error(error);
- toast('リンクを作れませんでした');
- }
- }
-
- /** The tag every shared picture carries. */
- const SHARE_TAG = '#ぶるべースタジオ';
-
- /** The studio's own address - the landing page itself, not a state-reproducing
- * `#s=` link. Shared alongside the tag so a viewer can find the studio. */
- function studioUrl() {
- return `${location.origin}${location.pathname}`;
- }
-
- /**
- * Share the current look as a *picture*, not as a link.
- *
- * Neither X nor Facebook's web dialog can attach an image, so the picture goes
- * through the OS share sheet (`navigator.share` with files) wherever the browser
- * has one - a single tap on a phone, and it can go to any app. Where there is no
- * share sheet (most desktops), the PNG is put on the clipboard and the network's
- * compose window is opened with the tag already typed, so the picture only has to
- * be pasted. The tag *and* the studio link always travel together: the user asks
- * for both to be kept when the picture is shared.
- */
- async function shareImage(network) {
- let shot;
- try {
- shot = await captureImage({ scale: 1.5, transparent: false });
- } catch (error) {
- console.error(error);
- toast('画像を作れませんでした');
- return;
- }
- const file = new File([shot.blob], `bluebey-${exporter.timestamp()}.png`, { type: 'image/png' });
-
- if (!network && navigator.canShare?.({ files: [file] })) {
- try {
- await navigator.share({ files: [file], text: SHARE_TAG, url: studioUrl() });
- return;
- } catch (error) {
- if (error?.name === 'AbortError') return;
- console.error(error);
- }
- }
-
- // No share sheet (or a named network): put the picture on the clipboard and
- // open that network's compose window with the tag and the studio link filled in.
- let copied = false;
- try {
- await exporter.copyCanvasToClipboard(shot.canvas);
- copied = true;
- } catch (error) {
- console.error(error);
- }
- const studio = studioUrl();
- const target = network === 'facebook'
- ? `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(studio)}`
- : `https://twitter.com/intent/tweet?text=${encodeURIComponent(SHARE_TAG)}&url=${encodeURIComponent(studio)}`;
- window.open(target, '_blank');
- toast(copied
- ? '画像をコピーしました。開いた画面に貼り付けて投稿してください(Ctrl+V)'
- : '投稿画面を開きました(この環境では画像を自動で貼り付けできません)');
- }
-
- async function toggleCamera(on) {
- if (!on) {
- backdrop.stopCamera();
- applyState({ view: { background: 'solid' } }, { scope: 'view', sync: true });
- return;
- }
- const result = await backdrop.startCamera(state.view.cameraFacing ?? 'environment');
- if (!result?.ok) {
- toast(result?.reason ?? 'カメラを使えません');
- return;
- }
- applyState({ view: { background: 'camera' } }, { scope: 'view', sync: true });
- toast('カメラの映像を背景にしました(そのまま写真に撮れます)');
- }
-
- function addProp(kind) {
- const def = PROP_DEFAULTS[kind] ?? { x: 1.6, y: 0, z: 0, rotX: 0, rotY: 0, rotZ: 0, scale: 1 };
- state.props.items = [...(state.props.items ?? []), {
- kind,
- x: def.x ?? 0,
- y: def.y ?? 0,
- z: def.z ?? 0,
- rotX: def.rotX ?? 0,
- rotY: def.rotY ?? 0,
- rotZ: def.rotZ ?? 0,
- scale: def.scale ?? 1,
- }];
- applyState({}, { scope: 'render', sync: true });
- }
-
- async function addStoryPanel() {
- const index = (state.story.panels?.length ?? 0) + 1;
- // A counter, not the array length: removing a panel and then adding another
- // used to hand out an id that was already taken, which confused the list.
- storySeq += 1;
- const id = `p${storySeq}`;
- state.story.panels = [...(state.story.panels ?? []), {
- id,
- label: `コマ${index}`,
- pose: JSON.parse(JSON.stringify(state.pose)),
- face: JSON.parse(JSON.stringify(state.face)),
- // The 擬音 stamps as well, so a panel comes back with everything it showed.
- gion: JSON.parse(JSON.stringify(state.gion)),
- // The camera as well as the pose: recalling a panel should put you back
- // where you were looking when you recorded it.
- view: JSON.parse(JSON.stringify(state.view)),
- }];
- applyState({}, { scope: 'gion', sync: true });
-
- // Then photograph what was just recorded, so the list shows the shot rather
- // than only its name.
- try {
- const shot = await captureImage({ scale: 0.35, transparent: false });
- storyThumbs.set(id, {
- version: (storyThumbs.get(id)?.version ?? 0) + 1,
- url: thumbDataUrl(shot.canvas),
- });
- toast(`コマ${index}を追加しました`);
- } catch (error) {
- console.warn('[bluebey] story thumbnail failed', error);
- toast(`コマ${index}を追加しました(プレビューは作れませんでした)`);
- }
- app.panel?.sync();
- }
-
- /** Shrink a capture down to a thumbnail data URL for the panel list. */
- function thumbDataUrl(source, width = 168) {
- const scale = width / Math.max(1, source.width);
- const canvas = document.createElement('canvas');
- canvas.width = Math.max(1, Math.round(source.width * scale));
- canvas.height = Math.max(1, Math.round(source.height * scale));
- const ctx = canvas.getContext('2d');
- ctx.imageSmoothingQuality = 'high';
- ctx.drawImage(source, 0, 0, canvas.width, canvas.height);
- return canvas.toDataURL('image/png');
- }
-
- /**
- * A number in the corner of a comic panel, for sheets whose reading order is
- * not obvious.
- */
- function drawPanelNumber(ctx, number, x, y, cellW, cellH) {
- const radius = Math.max(18, Math.min(64, Math.min(cellW, cellH) * 0.075));
- const margin = radius * 0.7;
- const cx = x + margin + radius;
- const cy = y + margin + radius;
- ctx.save();
- ctx.beginPath();
- ctx.arc(cx, cy, radius, 0, Math.PI * 2);
- ctx.fillStyle = 'rgba(255, 255, 255, 0.92)';
- ctx.fill();
- ctx.lineWidth = Math.max(2, radius * 0.14);
- ctx.strokeStyle = '#3f2b52';
- ctx.stroke();
- ctx.fillStyle = '#3f2b52';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.font = `bold ${Math.round(radius * 1.25)}px ${captionFontStack()}`;
- ctx.fillText(String(number), cx, cy + radius * 0.04);
- ctx.restore();
- }
-
- /**
- * まんが: replay the panels, capture each one, and hand back a framed sheet.
- */
- async function saveStory({ scale = 2 } = {}) {
- const panels = state.story?.panels ?? [];
- if (!panels.length) {
- toast('コマがありません。「今の状態をコマに追加」で作ってください');
- return;
- }
- const saved = JSON.parse(JSON.stringify(state));
- const captures = [];
- suspendHistory = true;
- try {
- for (const panel of panels) {
- const pose = panel.pose ?? {};
- state.pose = { bones: pose.bones ?? {}, root: pose.root ?? [0, 0, 0] };
- state.face = JSON.parse(JSON.stringify(saved.face));
- applyPatch(state.face, panel.face ?? {});
- // The camera the panel was framed with, so a comic keeps its angles.
- if (panel.view) state.view = JSON.parse(JSON.stringify(panel.view));
- refresh('all');
- // Two frames: one to apply the pose, one to draw it.
- await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
- captures.push(await captureImage({ scale, transparent: false }));
- }
- } finally {
- suspendHistory = false;
- applyState(saved, { full: true, sync: true });
- }
-
- const columns = Math.max(1, Math.min(4, state.story?.columns ?? 2));
- const gap = state.story?.gap ?? 12;
- const padding = state.story?.padding ?? 20;
- const rows = Math.ceil(captures.length / columns);
- const cellW = Math.max(...captures.map((item) => item.width));
- const cellH = Math.max(...captures.map((item) => item.height));
-
- // Once the sheet is more than one column wide, the reading order stops being
- // obvious from the layout alone, so the panels are numbered. A single column
- // (or two panels) reads top to bottom without help.
- const numbered = columns >= 2 && captures.length >= 3;
-
- // A manga page reads by its panel frames. They are drawn inside each cell so
- // the artwork keeps its full size, and are thick enough to survive the 1×
- // export.
- const frame = Math.max(3, Math.round(Math.min(cellW, cellH) * 0.012));
-
- const sheet = document.createElement('canvas');
- sheet.width = padding * 2 + cellW * columns + gap * (columns - 1);
- sheet.height = padding * 2 + cellH * rows + gap * (rows - 1);
- const ctx = sheet.getContext('2d');
- ctx.fillStyle = state.story?.sheetBackground ?? '#ffffff';
- ctx.fillRect(0, 0, sheet.width, sheet.height);
- captures.forEach((capture, index) => {
- const x = padding + (index % columns) * (cellW + gap);
- const y = padding + Math.floor(index / columns) * (cellH + gap);
- ctx.drawImage(capture.canvas, x, y, cellW, cellH);
- ctx.lineWidth = frame;
- ctx.strokeStyle = '#2a1e33';
- ctx.strokeRect(x + frame / 2, y + frame / 2, cellW - frame, cellH - frame);
- if (numbered) drawPanelNumber(ctx, index + 1, x, y, cellW, cellH);
- });
-
- const stamp = exporter.timestamp();
- exporter.downloadBlob(await exporter.canvasToBlob(sheet), `bluebey-comic-${stamp}.png`);
- toast(`まんがを書き出しました(${captures.length}コマ)`);
- }
-
- /** A shared link restores the whole look; otherwise this is the start point. */
- async function loadFromHash() {
- // 共有ボタンは `?s=`、ボタン「この見た目のリンクをコピー」は `#s=` を使う。
- // Facebook は `#` 以降を落とすので、共有はクエリにする必要がある - どちらでも読む。
- const query = new URLSearchParams(location.search).get('s');
- const hash = location.hash.replace(/^#/, '');
- const encoded = query ?? new URLSearchParams(hash).get('s');
- if (!encoded) return false;
- const decoded = await decodeState(encoded);
- if (!decoded?.state) return false;
- suspendHistory = true;
- try {
- applyState(decoded.state, { full: true, sync: true });
- } finally {
- suspendHistory = false;
- }
- return true;
- }
-
- function installTopbar() {
- // The panel itself is visible from the start, but each of its sections starts
- // collapsed (see `section()` in src/ui.js): the panel reads as a short list of
- // headings, and you open the one you want instead of scrolling past twelve.
- resizeViewport();
-
- // AR (カメラ=AR) get a camera-app style shutter over the viewport, so a photo
- // is one tap away. It is a DOM layer over the canvas, so it never lands in the
- // export; its visibility follows the background mode (see `applyBackdrop`).
- const shutter = document.createElement('button');
- shutter.type = 'button';
- shutter.id = 'ar-shutter';
- shutter.className = 'ar-shutter';
- shutter.title = '写真を撮る';
- shutter.setAttribute('aria-label', '写真を撮る');
- shutter.hidden = true;
- shutter.addEventListener('click', () => { savePNG(); });
- stage.append(shutter);
- app.arShutter = shutter;
-
- document.getElementById('btn-shot')?.addEventListener('click', () => app.actions.savePNG());
- document.getElementById('btn-copy')?.addEventListener('click', () => app.actions.copyPNG());
- document.getElementById('btn-help')?.addEventListener('click', toggleHelp);
- document.getElementById('btn-panel')?.addEventListener('click', togglePanel);
- document.getElementById('help-close')?.addEventListener('click', () => { toggleHelp(false); });
- const help = document.getElementById('help');
- help?.addEventListener('click', (event) => { if (event.target === help) toggleHelp(false); });
-
- // --- the mouse on the character ----------------------------------------
- // Dragging the model slides it across the view; holding Shift and dragging
- // turns it around. Bone posing lives in the panel, so the mouse never grabs a
- // bone (see `Rig` in src/rig.js). On a small screen the panel is a sheet over
- // the view, so a tap on the view first just dismisses the sheet.
- const dragRay = new THREE.Raycaster();
- const dragNdc = new THREE.Vector2();
- const dragHitAt = (event) => {
- const rect = canvas.getBoundingClientRect();
- dragNdc.set(
- ((event.clientX - rect.left) / Math.max(1, rect.width)) * 2 - 1,
- -((event.clientY - rect.top) / Math.max(1, rect.height)) * 2 + 1,
- );
- dragRay.setFromCamera(dragNdc, view.camera);
- const targets = [model.parts.eyeMesh, model.parts.mouthMesh, ...model.parts.body]
- .filter((mesh) => mesh?.visible);
- return dragRay.intersectObjects(targets, false)[0] ?? null;
- };
- // World units per screen pixel, so the model keeps up with the cursor.
- const dragScale = () => {
- const camera = view.camera;
- const height = Math.max(1, canvas.clientHeight);
- if (camera.isOrthographicCamera) return (camera.top - camera.bottom) / camera.zoom / height;
- const distance = camera.position.distanceTo(view.controls.target);
- return (2 * distance * Math.tan((camera.fov * Math.PI) / 360)) / height;
- };
- let modelDrag = null;
-
- canvas.addEventListener('pointerdown', (event) => {
- if (event.button !== 0 || event.target !== canvas) return;
- if (narrowScreenLayout.matches && !document.body.classList.contains('panel-hidden')) {
- togglePanel();
- return;
- }
- if (!dragHitAt(event)) return;
- const root = state.pose.root ?? [0, 0, 0];
- const master = state.pose.bones?.master ?? [0, 0, 0];
- modelDrag = {
- pointerId: event.pointerId,
- spin: event.shiftKey,
- x: event.clientX,
- y: event.clientY,
- root: [...root],
- masterY: master[1] ?? 0,
- };
- view.controls.enabled = false;
- stage.style.cursor = 'grabbing';
- try { canvas.setPointerCapture(event.pointerId); } catch { /* ignore */ }
- });
-
- canvas.addEventListener('pointermove', (event) => {
- if (!modelDrag || event.pointerId !== modelDrag.pointerId) return;
- const dx = event.clientX - modelDrag.x;
- const dy = event.clientY - modelDrag.y;
- if (modelDrag.spin) {
- const bones = { ...(state.pose.bones ?? {}) };
- const master = bones.master ?? [0, 0, 0];
- bones.master = [master[0], round2(modelDrag.masterY + dx * 0.7), master[2]];
- state.pose.bones = bones;
- } else {
- const k = dragScale();
- state.pose.root = [
- round2(modelDrag.root[0] + dx * k),
- round2(modelDrag.root[1] - dy * k),
- modelDrag.root[2],
- ];
- }
- app.panel?.onRigChanged?.();
- app.needsRender = true;
- });
-
- const endModelDrag = (event) => {
- if (!modelDrag || (event && event.pointerId !== modelDrag.pointerId)) return;
- modelDrag = null;
- view.controls.enabled = true;
- stage.style.cursor = '';
- if (event) { try { canvas.releasePointerCapture(event.pointerId); } catch { /* ignore */ } }
- capturePose();
- app.panel?.onRigChanged?.();
- app.needsRender = true;
- };
- canvas.addEventListener('pointerup', endModelDrag);
- canvas.addEventListener('pointercancel', endModelDrag);
- }
-
- function togglePanel() {
- document.body.classList.toggle('panel-hidden');
- resizeViewport();
- }
-
- // The panel is a side column on a wide screen and a bottom sheet on a small
- // one, where it starts closed so the 3D view fills the screen (see the
- // small-screen rules in src/style.css). When the window is resized across that
- // breakpoint, match the layout the other side expects instead of leaving the
- // panel stuck in the previous state.
- const narrowScreenLayout = window.matchMedia('(max-width: 768px), (max-height: 500px) and (pointer: coarse)');
- narrowScreenLayout.addEventListener('change', (event) => {
- document.body.classList.toggle('panel-hidden', event.matches);
- resizeViewport();
- });
-
- function installKeys() {
- window.addEventListener('keydown', (event) => {
- // Undo/redo are the only shortcuts that need a modifier, so they are dealt
- // with before the "no modifiers" rule below.
- if ((event.ctrlKey || event.metaKey) && !event.altKey) {
- const combo = event.key.toLowerCase();
- if (combo === 'z' || combo === 'y') {
- event.preventDefault();
- restoreFromHistory(combo === 'y' || event.shiftKey ? 'redo' : 'undo');
- return;
- }
- }
- if (event.metaKey || event.ctrlKey || event.altKey) return;
- const target = event.target;
- if (target instanceof HTMLInputElement || target instanceof HTMLSelectElement) return;
- switch (event.key) {
- case 's': case 'S': savePNG(); break;
- case 'c': case 'C': copyPNG(); break;
- case 'r': case 'R': app.actions.resetPose(); break;
- case 'g': case 'G': {
- const visible = !rig.helper.visible;
- rig.setGizmoVisible(visible);
- toast(visible ? 'ギズモを表示' : 'ギズモを隠しました');
- app.needsRender = true;
- break;
- }
- case ' ': event.preventDefault(); app.actions.toggleAnimation(); break;
- case '?': toggleHelp(); break;
- case 'Tab': event.preventDefault(); togglePanel(); break;
- case '1': setCameraPreset('front'); break;
- case '2': setCameraPreset('threeQuarter'); break;
- case '3': setCameraPreset('side'); break;
- case '4': setCameraPreset('back'); break;
- case '5': setCameraPreset('top'); break;
- default: break;
- }
- });
- }
-
-}
-
-function toggleHelp(force) {
- const help = document.getElementById('help');
- if (help) help.hidden = force === undefined ? !help.hidden : !force;
-}
-
-/** A persistent message for problems that need the user to do something. */
-function showNotice(text) {
- let node = document.getElementById('notice');
- if (!node) {
- node = document.createElement('div');
- node.id = 'notice';
- document.body.append(node);
- }
- node.textContent = text;
- node.hidden = false;
-}
-
-/* ------------------------------------------------------------------- camera */
-
-/**
- * Orbiting camera with both a perspective and an orthographic projection.
- * Only one of them is enabled at a time; switching copies the framing across so
- * the model does not jump.
- */
-class ViewRig {
- constructor(domElement, onChange) {
- this.domElement = domElement;
- this.onChange = onChange;
- this.projection = 'persp';
- this.controlsChanged = false;
-
- this.cameras = {
- // A tight near/far keeps depth precision high, which matters because the
- // outline hull is only a few hundredths of a unit away from the surface.
- // The far plane has to sit well past the furthest the camera may go, or
- // zooming all the way out slices the character on it - which is exactly
- // what the old `far = 120` did, because the zoom limit was also 120.
- persp: new THREE.PerspectiveCamera(30, 1, 1, 2000),
- ortho: new THREE.OrthographicCamera(-1, 1, 1, -1, -2000, 2000),
- };
- this.controlsMap = {};
- for (const [key, camera] of Object.entries(this.cameras)) {
- const controls = new OrbitControls(camera, domElement);
- controls.enableDamping = false;
- controls.enablePan = true;
- controls.minDistance = 1;
- controls.maxDistance = 400;
- controls.addEventListener('change', () => {
- this.controlsChanged = true;
- this.onChange?.();
- });
- this.controlsMap[key] = controls;
- }
- this.controlsMap.ortho.enabled = false;
- this.camera = this.cameras.persp;
- this.controls = this.controlsMap.persp;
- this.target = new THREE.Vector3(0, 1, 0);
- this.autoDistance = 12;
- // `orthoHeight` is where the orthographic camera is *now* - `apply()` writes
- // the camera's actual height back into it - so the framing `frame()` chose
- // needs its own field to measure the zoom against (see `zoomFactor`).
- this.orthoHeight = 6;
- this.autoOrthoHeight = 6;
- this.resize();
- }
-
- /** Fit the model into view. */
- frame(size, targetY) {
- this.target.set(0, targetY, 0);
- for (const controls of Object.values(this.controlsMap)) controls.target.copy(this.target);
- const fov = this.cameras.persp.fov * Math.PI / 180;
- const heightDistance = size.y / (2 * Math.tan(fov / 2));
- const widthDistance = size.x / (2 * Math.tan(fov / 2) * Math.max(0.4, this.aspect));
- this.autoDistance = Math.max(heightDistance, widthDistance) * 1.45;
- this.orthoHeight = size.y * 1.5;
- this.autoOrthoHeight = this.orthoHeight;
- for (const camera of Object.values(this.cameras)) {
- camera.position.set(0, targetY + this.autoDistance * 0.12, this.autoDistance);
- }
- this.resize();
- }
-
- get aspect() {
- const width = this.domElement.clientWidth || 1;
- const height = this.domElement.clientHeight || 1;
- return width / height;
- }
-
- resize() {
- const width = this.domElement.clientWidth || window.innerWidth;
- const height = this.domElement.clientHeight || window.innerHeight;
- const aspect = width / height;
- this.cameras.persp.aspect = aspect;
- this.cameras.persp.updateProjectionMatrix();
- this.setOrthoHeight(this.orthoHeight);
- }
-
- setOrthoHeight(height) {
- const ortho = this.cameras.ortho;
- const halfHeight = height / 2;
- const halfWidth = halfHeight * this.aspect;
- ortho.left = -halfWidth;
- ortho.right = halfWidth;
- ortho.top = halfHeight;
- ortho.bottom = -halfHeight;
- ortho.zoom = 1;
- ortho.updateProjectionMatrix();
- }
-
- /**
- * How large the character looks now, relative to the auto-fit framing.
- *
- * 1 means "the default framing", which is where the panel's 大きさ sliders sit
- * and so where the screen-space outline's pixel width is written against. The
- * two projections scale differently - a perspective camera shows 1/distance as
- * much per world unit, an orthographic one 1/height, and OrbitControls zooms
- * the latter with `camera.zoom` rather than by moving it - so each is
- * normalised by its own auto-fit value.
- */
- zoomFactor() {
- if (this.projection === 'ortho') {
- const ortho = this.cameras.ortho;
- const height = (ortho.top - ortho.bottom) / Math.max(ortho.zoom, 0.0001);
- return this.autoOrthoHeight > 0 && height > 0 ? this.autoOrthoHeight / height : 1;
- }
- const distance = this.camera.position.distanceTo(this.controls?.target ?? this.target);
- return this.autoDistance > 0 && distance > 0 ? this.autoDistance / distance : 1;
- }
-
- setProjection(kind) {
- if (kind === this.projection) return;
- const previous = this.camera;
- const next = kind === 'ortho' ? this.cameras.ortho : this.cameras.persp;
- next.position.copy(previous.position);
- next.quaternion.copy(previous.quaternion);
- this.controlsMap[this.projection].enabled = false;
- this.projection = kind;
- this.camera = next;
- this.controls = this.controlsMap[kind];
- this.controls.enabled = true;
- this.controls.target.copy(this.target);
- this.resize();
- this.onCameraChange?.(next, this.controls);
- this.controlsChanged = true;
- this.onChange?.();
- }
-
- /** Apply the serialisable view state (azimuth/polar/zoom). */
- apply(viewState, size) {
- const distance = viewState.distance > 0 ? viewState.distance : this.autoDistance;
- const targetY = viewState.targetY || this.target.y;
- this.target.set(viewState.targetX || 0, targetY, viewState.targetZ || 0);
- const spherical = new THREE.Spherical(
- distance,
- clamp(viewState.polar, 1, 179) * Math.PI / 180,
- viewState.azimuth * Math.PI / 180,
- );
- const offset = new THREE.Vector3().setFromSpherical(spherical);
- for (const camera of Object.values(this.cameras)) {
- camera.position.copy(this.target).add(offset);
- camera.lookAt(this.target);
- }
- if (viewState.orthoHeight > 0) this.setOrthoHeight(viewState.orthoHeight);
- else if (size) this.setOrthoHeight(this.orthoHeight);
- for (const controls of Object.values(this.controlsMap)) {
- controls.target.copy(this.target);
- controls.update();
- }
- this.orthoHeight = this.cameras.ortho.top - this.cameras.ortho.bottom;
- this.cameras.persp.updateProjectionMatrix();
- this.cameras.ortho.updateProjectionMatrix();
- }
-
- /**
- * Write where the camera actually is back into the serialisable view state.
- *
- * WHY: the mouse orbit and pan move the `controls`, not the state. Without this
- * the state kept the last camera the *panel* set, so any refresh of the view
- * snapped the camera back to it - which is what made picking a background jump
- * the camera. Keeping the two in step also means a shared link carries the
- * camera you framed.
- */
- captureInto(viewState) {
- if (!viewState || typeof viewState !== 'object') return;
- const target = this.controls?.target ?? this.target;
- const spherical = new THREE.Spherical().setFromVector3(
- new THREE.Vector3().subVectors(this.camera.position, target),
- );
- viewState.azimuth = Math.round(spherical.theta * RAD_TO_DEG * 100) / 100;
- viewState.polar = Math.round(spherical.phi * RAD_TO_DEG * 100) / 100;
- viewState.distance = Math.round(spherical.radius * 1000) / 1000;
- viewState.targetX = Math.round(target.x * 1000) / 1000;
- viewState.targetY = Math.round(target.y * 1000) / 1000;
- viewState.targetZ = Math.round(target.z * 1000) / 1000;
- if (this.projection === 'ortho') {
- const ortho = this.cameras.ortho;
- viewState.orthoHeight = Math.round((ortho.top - ortho.bottom) * 1000) / 1000;
- }
- }
-
- setAutoRotate(enabled, speed) {
- for (const controls of Object.values(this.controlsMap)) {
- controls.autoRotate = enabled;
- controls.autoRotateSpeed = speed;
- }
- }
-}
-
-/**
- * How far the screen-space outline may follow the zoom (see `outlineRadiusFor`).
- *
- * A clamp keeps a very deep zoom from turning the line into either a smear or
- * nothing at all, which is what the linear factor would do at the extremes of
- * the 大きさ slider.
- */
-const OUTLINE_ZOOM_MIN = 0.25;
-const OUTLINE_ZOOM_MAX = 3;
-
-/**
- * The screen-space outline's radius, in pixels of the label buffer.
- *
- * The body's lines are inverted hulls, i.e. an offset in *world* units, so they
- * thicken as the character grows on screen and thin as it shrinks. This pass
- * works in pixels instead, so left alone its lines - the waist leaves and the
- * nose - keep one width at every zoom, and the leaf skirt reads as far too heavy
- * beside the body the moment you pull back. Scaling the radius by the rig's
- * `zoomFactor` makes the two behave the same way, with `outlinePixels` staying
- * the width at the auto-fit framing (so the slider keeps its meaning).
- *
- * `extra` carries whatever else changes the buffer's pixels-per-CSS-pixel: the
- * preview's buffer is `quality`x the CSS size (and `quality` itself follows the
- * display density and the automatic reductions), and an export renders the label
- * buffer at `scale`x. Passing that factor keeps the line a constant *CSS* width,
- * so `outlinePixels` reads as CSS pixels whichever buffer it lands in.
- */
-function outlineRadiusFor(view, state, extra = 1) {
- const zoom = clamp(view.zoomFactor(), OUTLINE_ZOOM_MIN, OUTLINE_ZOOM_MAX);
- return (state.render.outlinePixels ?? 2) * zoom * extra;
-}
-
-/* ----------------------------------------------------------------- scenery */
-
-function makeEnvironment(renderer) {
- const canvas = document.createElement('canvas');
- canvas.width = 64;
- canvas.height = 32;
- const ctx = canvas.getContext('2d');
- const gradient = ctx.createLinearGradient(0, 0, 0, 32);
- gradient.addColorStop(0, '#ffffff');
- gradient.addColorStop(0.5, '#ece7fa');
- gradient.addColorStop(1, '#b9aade');
- ctx.fillStyle = gradient;
- ctx.fillRect(0, 0, 64, 32);
- const texture = new THREE.CanvasTexture(canvas);
- texture.mapping = THREE.EquirectangularReflectionMapping;
- texture.colorSpace = THREE.SRGBColorSpace;
- const generator = new THREE.PMREMGenerator(renderer);
- const environment = generator.fromEquirectangular(texture).texture;
- texture.dispose();
- generator.dispose();
- return environment;
-}
-
-/** Round to 2 decimals: the precision the panel's prop sliders show. */
-const round2 = (value) => Math.round(value * 100) / 100;
-
-const clampNumber = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
-
-export function backgroundOf(viewState) {
- if (viewState.background === 'transparent') return { mode: 'transparent' };
- // A preset, a loaded photo, a drawn effect line or the phone's camera all
- // live in a DOM layer behind the WebGL canvas, so the renderer is left clear
- // and the exports paint the bitmap into the picture themselves (see
- // drawBackdropInto in main.js).
- if (viewState.background === 'preset' || viewState.background === 'image'
- || viewState.background === 'effect' || viewState.background === 'camera') {
- return { mode: 'transparent' };
- }
- return { mode: 'solid', color: viewState.backgroundColor };
-}
-
-/* ------------------------------------------------------------------- errors */
-
-function setLoadingProgress(ratio) {
- const node = document.getElementById('loading-text');
- if (node) node.textContent = `ぶるべーを読み込んでいます… ${Math.round(ratio * 100)}%`;
-}
-
-function hideLoading() {
- const node = document.getElementById('loading');
- if (node) node.classList.add('done');
-}
-
-function showLoadError(error) {
- const text = document.getElementById('loading-text');
- const detail = document.getElementById('loading-error');
- if (text) text.textContent = '読み込みに失敗しました。';
- if (detail) {
- detail.textContent = `${error?.message ?? error}\n\n`
- + 'このページはローカルサーバー経由で開く必要があります。\n'
- + 'bluebey-studio フォルダで python serve.py を実行し、\n'
- + '表示された http://127.0.0.1:8000/ をブラウザで開いてください。';
- }
-}
-
-const clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
-const clamp01 = (value) => clamp(value, 0, 1);
diff --git a/bluebey-studio/src/model.js b/bluebey-studio/src/model.js
deleted file mode 100644
index 3925e85..0000000
--- a/bluebey-studio/src/model.js
+++ /dev/null
@@ -1,283 +0,0 @@
-import * as THREE from 'three';
-import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
-
-/**
- * Loads bluebey.glb and works out everything the studio needs to know about it:
- * the rig, which meshes are the face overlays, and the original hand-drawn
- * textures that ship inside the file.
- *
- * The GLB layout (materials carry the reliable names; meshes are named after
- * Blender objects):
- * body parts Body, Foots, Leaf, Vein, LeftHand, RightHand, Nose
- * overlays eyes-* (eight hand-drawn variants, one texture each) and Mouth
- * face plates eyes-plate / mouth-plate, the big shells the studio draws on
- * hair plate hair-plate, a shell that wraps right round the head (optional)
- *
- * The two face plates are what the artwork is painted onto. They are the front
- * half of the body, so wherever the drawing lands there is a surface to carry
- * it - the old hand-drawn planes only covered a patch of the head, which clipped
- * the bottom of a deep smile and most of a teardrop. When a model has no plates
- * (the 2022 file), the studio falls back to drawing on the hand-drawn planes.
- */
-
-/** Hand-drawn texture variants inside the GLB, keyed by their material suffix. */
-export const EYE_VARIANTS = [
- { key: 'opened', label: '開いた目' },
- { key: 'closed', label: '閉じた目' },
- { key: 'close-tight', label: 'ぎゅっと閉じ' },
- { key: 'left-wink', label: '左ウインク' },
- { key: 'look-up', label: '上を見る' },
- { key: 'look-down', label: '下を見る' },
- { key: 'look-left', label: '左を見る' },
- { key: 'look-right', label: '右を見る' },
-];
-
-/**
- * Friendly names for the eleven bones of the rig. Keys are lower-cased with
- * dots removed, because three's GLTFLoader sanitises node names (`armsupport.l`
- * arrives as `armsupportl`).
- */
-const BONE_LABELS = {
- master: '全身',
- armsupportl: '左うでの付け根',
- arml: '左うで',
- handl: '左手先',
- armr: '右うで',
- handr: '右手先',
- legsupportl: '左あしの付け根',
- footl: '左足',
- toel: '左つまさき',
- legsupportr: '右あしの付け根',
- footr: '右足',
- toer: '右つまさき',
-};
-
-const normalizeBoneName = (name) => (name ?? '').replace(/[.\s]/g, '').toLowerCase();
-
-/**
- * The overlay planes hug the head, so nudge them outwards before anything else
- * happens. They are pushed along their own normals (not away from the head
- * centre): the mouth in particular needs to clear the head's lower surface and
- * the ring of leaves around the base, otherwise a low or frowning mouth gets
- * swallowed by them.
- */
-const OVERLAY_INFLATE = { eyes: 0.004, mouth: 0.004, hair: 0.008 };
-
-export async function loadModel(source, { onProgress } = {}) {
- const loader = new GLTFLoader();
- let gltf;
- if (typeof source === 'string') {
- gltf = await loader.loadAsync(source, (event) => {
- if (onProgress && event.lengthComputable) onProgress(event.loaded / event.total);
- });
- } else {
- // A File/ArrayBuffer, e.g. from drag and drop or the file picker.
- gltf = await loader.parseAsync(source, '');
- }
-
- const root = gltf.scene;
-
- // The file carries its own camera node; we drive our own.
- const cameras = [];
- root.traverse((object) => { if (object.isCamera) cameras.push(object); });
- for (const camera of cameras) camera.removeFromParent();
-
- const eyes = new Map();
- const body = [];
- /** The original material name of every body mesh (`blb`, `leaf`, `vein`, ...). */
- const kinds = new Map();
- /** The plane holding the hand-drawn mouth texture (a texture source only). */
- let mouthOriginal = null;
- /** The big shells the artwork is drawn onto, if the file has them. */
- let eyePlate = null;
- let mouthPlate = null;
- /** The shell that wraps round the head, so hair shows from behind (optional). */
- let hairPlate = null;
-
- root.traverse((object) => {
- if (!object.isMesh) return;
- // Skinned bounds stop matching the pose, so culling would pop meshes away.
- object.frustumCulled = false;
- const material = Array.isArray(object.material) ? object.material[0] : object.material;
- const materialName = (material?.name ?? '').toLowerCase();
- if (materialName === 'eyes-plate') {
- eyePlate = object;
- return;
- }
- if (materialName === 'mouth-plate') {
- mouthPlate = object;
- return;
- }
- if (materialName === 'hair-plate') {
- hairPlate = object;
- return;
- }
- if (materialName.startsWith('eyes-')) {
- eyes.set(materialName.slice('eyes-'.length), object);
- return;
- }
- if (materialName === 'mouth') {
- mouthOriginal = object;
- return;
- }
- body.push(object);
- kinds.set(object, materialName);
- });
-
- // The waist leaves - the `Leaf` blade and the `Vein` rim that shares its mesh.
- // These are the thin, overlapping shells the inverted-hull outline cannot draw
- // (see MODEL-GUIDE.md §5), so the screen-space pass takes them over on its own.
- const leaves = body.filter((mesh) => kinds.get(mesh) === 'leaf' || kinds.get(mesh) === 'vein');
-
- // The drawing surface: the big plate when present, otherwise the 2022 plane.
- const eyeMesh = eyePlate ?? eyes.get('opened') ?? eyes.values().next().value;
- const mouth = mouthPlate ?? mouthOriginal;
- // The wrapping hair shell, if the model carries one (`hair-plate`).
- const hairMesh = hairPlate;
- // Its cylindrical UV has a seam at the back of the head. Triangles that cross
- // it get one UV near 0 and the next near 1, so the whole texture is stretched
- // across them - a visible streak down the back. Close the seam per triangle.
- if (hairMesh) healCylinderSeam(hairMesh);
- if (!eyeMesh) throw new Error('eye mesh not found in the GLB');
- if (!mouth) throw new Error('mouth mesh not found in the GLB');
-
- // The nose ball. It is a bump sitting on the body rather than a part with a
- // silhouette of its own, so the outline pass treats it per style - see
- // `outlineExclusion` in main.js.
- const noseMesh = body.find((mesh) => (mesh.material?.name ?? '').toLowerCase() === 'nose') ?? null;
-
- // Only the drawing surface stays visible; every other overlay is a texture
- // source, so hide it. (`mouthOriginal` may be the drawing surface itself.)
- for (const mesh of eyes.values()) mesh.visible = mesh === eyeMesh;
- if (mouthOriginal && mouthOriginal !== mouth) mouthOriginal.visible = false;
- if (mouthPlate && eyePlate) mouthPlate.visible = true;
-
- /** @type {{ eyes: Record, mouth: THREE.Texture|null }} */
- const originals = { eyes: {}, mouth: mouthOriginal?.material?.map ?? mouth.material.map ?? null };
- for (const [key, mesh] of eyes) {
- if (mesh.material.map) originals.eyes[key] = mesh.material.map;
- }
-
- // Push the overlay planes a hair off the body surface, so the two do not
- // z-fight. The plates are shells of the body, so the offset is tiny; a large
- // one is what made the mouth look like it floated in profile.
- const bodyMesh = body.find((mesh) => mesh.geometry?.attributes?.position?.count > 2000) ?? body[0];
- const headCentre = new THREE.Box3()
- .setFromBufferAttribute(bodyMesh.geometry.attributes.position)
- .getCenter(new THREE.Vector3());
- inflateOverlay(eyeMesh, headCentre, OVERLAY_INFLATE.eyes);
- inflateOverlay(mouth, headCentre, OVERLAY_INFLATE.mouth);
- if (hairMesh) inflateOverlay(hairMesh, headCentre, OVERLAY_INFLATE.hair);
-
- // Recentre: put the character on the origin with its feet on the ground so the
- // camera maths stays trivial.
- const box = new THREE.Box3().setFromObject(root);
- const centre = box.getCenter(new THREE.Vector3());
- root.position.x -= centre.x;
- root.position.z -= centre.z;
- root.position.y -= box.min.y;
- root.updateMatrixWorld(true);
-
- const bounds = new THREE.Box3().setFromObject(root);
- const size = bounds.getSize(new THREE.Vector3());
- const middle = bounds.getCenter(new THREE.Vector3());
-
- const skinned = [];
- root.traverse((object) => { if (object.isSkinnedMesh && object.skeleton) skinned.push(object); });
- const skeleton = skinned[0]?.skeleton ?? null;
-
- const bones = [];
- if (skeleton) {
- for (const bone of skeleton.bones) {
- bones.push({
- name: bone.name,
- label: BONE_LABELS[normalizeBoneName(bone.name)] ?? bone.name,
- bone,
- rest: bone.quaternion.clone(),
- });
- }
- }
-
- return {
- gltf,
- root,
- size,
- middle,
- bounds,
- skeleton,
- bones,
- parts: {
- body,
- kinds,
- leaves,
- overlays: [eyeMesh, mouth, ...(hairMesh ? [hairMesh] : [])],
- eyeMesh,
- mouthMesh: mouth,
- hairMesh,
- noseMesh,
- hasFacePlates: Boolean(eyePlate && mouthPlate),
- },
- originals,
- };
-}
-
-function inflateOverlay(mesh, centre, distance) {
- const geometry = mesh.geometry;
- const position = geometry?.attributes?.position;
- if (!position) return;
- const normal = geometry.attributes.normal;
- const radial = new THREE.Vector3();
- const offset = new THREE.Vector3();
- const vertexNormal = new THREE.Vector3();
- for (let i = 0; i < position.count; i++) {
- radial.fromBufferAttribute(position, i).sub(centre);
- if (radial.lengthSq() < 1e-12) continue;
- offset.copy(radial).normalize();
- if (normal) {
- // The planes are double sided, so the stored normal may point either way.
- vertexNormal.fromBufferAttribute(normal, i);
- if (vertexNormal.lengthSq() > 1e-12) {
- if (vertexNormal.dot(offset) < 0) vertexNormal.negate();
- offset.copy(vertexNormal.normalize());
- }
- }
- position.setXYZ(
- i,
- position.getX(i) + offset.x * distance,
- position.getY(i) + offset.y * distance,
- position.getZ(i) + offset.z * distance,
- );
- }
- position.needsUpdate = true;
- geometry.computeBoundingBox();
- geometry.computeBoundingSphere();
-}
-
-/**
- * Make every triangle of a cylindrical UV island sit on one side of the seam.
- *
- * A cylinder unwrap cuts the surface open along one line (here the back of the
- * head). A triangle that spans the cut has its corners at u ~ 1 and u ~ 0, so
- * interpolating them drags the whole texture across the triangle - the streak
- * seen down the back. Wrapping the low corners up by one turn keeps each
- * triangle local; the texture's two edges are identical (the covering is drawn
- * right across), so the join is invisible.
- */
-function healCylinderSeam(mesh) {
- const source = mesh?.geometry;
- if (!source?.attributes?.uv) return;
- // Give every triangle its own corners first. The seam's low and high sides
- // share vertices, so editing a shared one would drag its neighbours too; a
- // non-indexed copy isolates each triangle and the fix cannot leak.
- const geometry = source.index ? source.toNonIndexed() : source;
- const uv = geometry.attributes.uv;
- for (let i = 0; i < uv.count; i += 3) {
- const top = Math.max(uv.getX(i), uv.getX(i + 1), uv.getX(i + 2));
- if (top - Math.min(uv.getX(i), uv.getX(i + 1), uv.getX(i + 2)) <= 0.5) continue;
- for (let k = 0; k < 3; k += 1) {
- if (uv.getX(i + k) < top - 0.5) uv.setX(i + k, uv.getX(i + k) + 1);
- }
- }
- uv.needsUpdate = true;
- mesh.geometry = geometry;
-}
diff --git a/bluebey-studio/src/outline.js b/bluebey-studio/src/outline.js
deleted file mode 100644
index 127d576..0000000
--- a/bluebey-studio/src/outline.js
+++ /dev/null
@@ -1,368 +0,0 @@
-import * as THREE from 'three';
-
-/**
- * A screen-space outline: the label version of "line art".
- *
- * The studio's other outline is an *inverted hull* - a copy of a mesh, expanded
- * along its normals, drawn back-faces-only. That is cheap and its line is
- * computed from the geometry, so it comes out smooth, but it cannot outline a
- * thin closed solid: a leaf blade is 0.026 units thick and the hull expands by
- * 0.022 in every direction, so the expanded front and back cross inside the leaf
- * and the line breaks up. No amount of mesh fixing removes that; it is the
- * technique.
- *
- * So the thin overlapping parts - the waist leaves, and the nose in the
- * line-art styles - are handed to this pass instead, and everything else keeps
- * its hull (see MODEL-GUIDE.md §5). This module renders the scene once into a
- * buffer holding a *label* per pixel, then draws a full-screen pass that inks
- * pixels where the labels disagree.
- *
- * ## Why labels, not coverage
- *
- * The parts want different lines:
- *
- * - a LEAF that runs into the body must NOT get a line along the intersection.
- * Such a line reads as the leaf sinking into the body, and the original
- * artwork does not draw one either - the leaf simply passes behind the body.
- * - the NOSE is the opposite. It is a bump sitting on the body, so the ring
- * where it meets the body *is* its outline. In a line drawing there is no
- * colour to read the nose by, so that ring is the only thing that shows it.
- *
- * A single "coverage" cannot express that difference, so each part writes a
- * label:
- *
- * 0 (paper) nothing is there
- * BEHIND something that is merely behind: it still writes depth, so it
- * hides what is behind *it*, but a leaf in front of it is still
- * outlined - a foot behind the skirt does not swallow the leaf's
- * edge. This is the default for meshes nobody claimed.
- * SOLID the body: the leaves *emerge* from it, so a leaf must not be
- * outlined where it meets it (that reads as the leaf sinking into
- * the body, and the original artwork draws no line there either).
- * LEAF a leaf
- * NOSE the nose
- *
- * The label is read with `NearestFilter`, because it is an identity, not a
- * colour: filtering it would blend two labels into a third value that means
- * neither.
- *
- * ## What this pass deliberately does not do
- *
- * It does not look for folds, and so it needs no normals at all - only the
- * labels. Surface shape was tried (a second buffer of filtered normals, with the
- * fold test gated to the leaves) and taken out again: it did bring back the
- * lines where one leaf lies over the next, but it cost a whole extra scene pass
- * and it read as a grainy speckle across the skirt, because a leaf is thin
- * enough (0.026) that its own rim is a 145-degree crease and the mesh arrives
- * with those edges split into separate vertices. The recipe is written up in
- * MODEL-GUIDE.md §5-2c if it is ever wanted back - for instance to make raised
- * leaf veins show.
- *
- * Callers must keep the outline hulls out of this pass: `exclude()` takes them
- * (see `outlineExclusion` in main.js). A hull is an expanded copy of a mesh, and
- * this pass swaps a front-side material onto everything it sees, so a hull would
- * paint an enlarged copy of the character's own label over all of it.
- *
- * A mesh that should *hide* parts of the character without being outlined itself
- * (the invisible wall) is handed to `occlude()` instead: it is drawn for its
- * depth alone, with a paper label, so the leaves and the nose buried behind it
- * get no label - and therefore no line.
- */
-
-// Taps around the pixel, averaged into a coverage. 16 gives a smooth ramp.
-const TAPS = 16;
-
-/** Written into the alpha channel; see the note above. */
-export const BEHIND_LABEL = 0.1;
-export const SOLID_LABEL = 0.4;
-export const LEAF_LABEL = 0.7;
-export const NOSE_LABEL = 1;
-
-const VERTEX = /* glsl */`
- varying vec2 vUv;
- void main() {
- vUv = uv;
- // The quad is already in clip space, so no camera maths is involved.
- gl_Position = vec4(position.xy, 0.0, 1.0);
- }
-`;
-
-const FRAGMENT = /* glsl */`
- uniform sampler2D uLabels;
- uniform vec2 uTexel;
- uniform vec3 uColor;
- uniform float uRadius;
- varying vec2 vUv;
-
- const float TAU = 6.28318530718;
-
- // The labels, read back out of the alpha channel (see the top of this file).
- // They are exact values, not a gradient, so the tests are simple comparisons.
- //
- // "paper" means "does not block a line": the untouched background (0) and the
- // parts that are merely behind (0.1). Only the body (0.4) blocks one.
- float paper(float a) { return 1.0 - step(0.2, a); }
- float drawn(float a) { return step(0.6, a); }
- float nose(float a) { return step(0.85, a); }
-
- void main() {
- vec4 here = texture2D(uLabels, vUv);
- float ink = 0.0;
-
- for (int i = 0; i < ${TAPS}; i++) {
- float angle = (float(i) / float(${TAPS})) * TAU;
- vec2 offset = vec2(cos(angle), sin(angle)) * uTexel * uRadius;
- vec4 there = texture2D(uLabels, vUv + offset);
-
- float edge = 0.0;
- // 1. a drawn part against the paper. A leaf against the *body* fires
- // nothing here, which is the point: the leaf simply passes behind it.
- edge = max(edge, max(drawn(here.a) * paper(there.a),
- drawn(there.a) * paper(here.a)));
- // 2. the nose against anything that is not the nose, so its whole ring -
- // including the part against the body - is inked.
- edge = max(edge, abs(nose(here.a) - nose(there.a)));
-
- ink += edge;
- }
-
- // Averaging the taps turns the flag into a coverage, and that is what takes
- // the steps out of the line: a pixel half over an edge gets half the ink, so
- // the line gets soft edges instead of landing on the pixel grid. It also
- // reads lighter than a hard band of the same width, which is what makes it
- // sit next to the hull's line without looking heavier.
- ink /= float(${TAPS});
- // The ramp starts at about three taps out of sixteen rather than at one, so an
- // isolated tap is not enough to ink a pixel; a real edge has half the taps
- // crossing it, so asking for a few costs nothing there.
- float line = smoothstep(0.19, 0.45, ink);
- if (line < 0.02) discard;
- gl_FragColor = vec4(uColor, line);
- }
-`;
-
-export function createScreenOutline({ renderer, scene, camera, width = 1280, height = 800 }) {
- const target = new THREE.WebGLRenderTarget(width, height, {
- // The label is an identity, not a shade: filtering would blend two labels into
- // a third value that means neither of them. The softness of the line comes
- // from spreading the taps in the shader, not from blurring this buffer.
- minFilter: THREE.NearestFilter,
- magFilter: THREE.NearestFilter,
- depthBuffer: true,
- // The alpha channel carries the label, so it must not be filled in.
- format: THREE.RGBAFormat,
- });
- target.texture.generateMipmaps = false;
-
- /**
- * The label each part writes into the alpha channel.
- *
- * `transparent: true` keeps three.js from defining `OPAQUE`, which would force
- * the alpha to 1, and `blending: NoBlending` makes the fragment *replace* the
- * pixel instead of blending into it, so the labels stay exact. `depthWrite`
- * stays on, which is what hides the parts that are behind something else.
- */
- const labelMaterials = new Map();
- function labelMaterialFor(label) {
- let material = labelMaterials.get(label);
- if (material) return material;
- material = new THREE.MeshBasicMaterial({
- color: 0xffffff,
- transparent: true,
- opacity: label,
- blending: THREE.NoBlending,
- depthWrite: true,
- });
- material.customProgramCacheKey = () => `bluebey-outline-label-${label}`;
- labelMaterials.set(label, material);
- return material;
- }
-
- const uniforms = {
- uLabels: { value: target.texture },
- uTexel: { value: new THREE.Vector2(1 / width, 1 / height) },
- uColor: { value: new THREE.Color('#2a1e33') },
- uRadius: { value: 1.4 },
- };
- const material = new THREE.ShaderMaterial({
- uniforms,
- vertexShader: VERTEX,
- fragmentShader: FRAGMENT,
- transparent: true,
- depthTest: false,
- depthWrite: false,
- toneMapped: false,
- });
-
- // A single quad in clip space, with the camera taken out of the equation.
- const quadScene = new THREE.Scene();
- const quadCamera = new THREE.Camera();
- quadScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material));
-
- /** Objects that must not appear in the outline (ground, shadows, gizmos). */
- const hidden = [];
- /** Meshes whose material (and render order) is borrowed for the label pass. */
- const swapped = [];
- const exclusion = new Set();
-
- /**
- * Meshes that only *occlude* in the label pass (the 見えない壁).
- *
- * They are drawn with a paper label and both sides, so their depth hides
- * whatever is behind them: a leaf or the nose buried in the wall then gets no
- * label at all, and so no line. Their own silhouette inks nothing either,
- * because paper against paper is not an edge - which is what keeps the wall
- * itself invisible instead of drawing a rectangle.
- */
- const occluders = new Set();
- let occluderMaterial = null;
-
- function occluderMaterialFor() {
- if (!occluderMaterial) {
- occluderMaterial = new THREE.MeshBasicMaterial({
- color: 0xffffff,
- transparent: true,
- opacity: 0, // paper: it never reads as a drawn part
- blending: THREE.NoBlending,
- depthWrite: true,
- side: THREE.DoubleSide, // the wall can be seen from either side
- });
- }
- return occluderMaterial;
- }
-
- /** Objects to draw in the label pass for their depth alone (see `occluders`). */
- function occlude(objects) {
- occluders.clear();
- for (const object of objects) if (object) occluders.add(object);
- }
-
- /**
- * When set, these meshes write the given label and everything else writes
- * BEHIND. Without it the whole scene shares one label.
- *
- * @type {Map|null}
- */
- let focus = null;
-
- function setSize(nextWidth, nextHeight) {
- const w = Math.max(1, Math.round(nextWidth));
- const h = Math.max(1, Math.round(nextHeight));
- if (target.width === w && target.height === h) return;
- target.setSize(w, h);
- uniforms.uTexel.value.set(1 / w, 1 / h);
- }
-
- /** Layers/objects to leave out of the label pass (ground, shadow, gizmo, hulls). */
- function exclude(objects) {
- exclusion.clear();
- for (const object of objects) if (object) exclusion.add(object);
- }
-
- /**
- * Hand the pass a split: these meshes get this label, and everything else is
- * drawn as BEHIND - present, so it still hides what is behind it, but not
- * blocking a leaf's outline. Callers should therefore name every part that a
- * leaf must not be outlined against (the body) as well as the outlined ones
- * (the leaves, the nose).
- *
- * `null` puts the whole scene back on one label.
- *
- * @param {Array<[THREE.Object3D, number]>|null} parts
- */
- function only(parts) {
- focus = parts ? new Map(parts) : null;
- }
-
- /**
- * Draw one frame. `baseRender` renders the scene the normal way; it is called
- * between the label pass and the ink so the ink lands on top of it.
- */
- function render(baseRender, options = {}) {
- const enabled = options.enabled !== false;
- if (!enabled) {
- baseRender();
- return;
- }
-
- uniforms.uColor.value.set(options.color ?? '#2a1e33');
- uniforms.uRadius.value = Math.max(0.6, options.radius ?? 1.4);
-
- // --- 1. the labels pass --------------------------------------------
- const previousOverride = scene.overrideMaterial;
- const previousClear = renderer.getClearColor(new THREE.Color());
- const previousAlpha = renderer.getClearAlpha();
- hidden.length = 0;
- swapped.length = 0;
- const hide = (object) => {
- if (object.visible) {
- hidden.push(object);
- object.visible = false;
- }
- };
- for (const object of exclusion) hide(object);
-
- if (focus) {
- // A per-mesh material, so the meshes outside `focus` can occlude without
- // contributing a line. (`overrideMaterial` would put one label on all of
- // them, and a leaf ending on a foot would then be read the same way as a
- // leaf ending on the body.)
- //
- // The occluders are also pushed to the front of the draw order. Every body
- // mesh sits at the same origin, so three.js would otherwise fall back to
- // insertion order and could draw a far-side leaf *before* the body that
- // hides it - and writing a label cannot erase what is already in the
- // buffer, only stop it being drawn. Drawn first, the depth test does it.
- scene.traverse((object) => {
- if (!object.isMesh || !object.visible) return;
- swapped.push([object, object.material, object.renderOrder]);
- if (occluders.has(object)) {
- // Depth only, drawn first: the wall hides the labels behind it.
- object.material = occluderMaterialFor();
- object.renderOrder = -1;
- return;
- }
- const label = focus.get(object) ?? BEHIND_LABEL;
- object.material = labelMaterialFor(label);
- if (label < 0.6) object.renderOrder = -1;
- });
- scene.overrideMaterial = null;
- } else {
- // No split given: nothing is outlined, so no label may read as "drawn".
- scene.overrideMaterial = labelMaterialFor(SOLID_LABEL);
- }
-
- renderer.setRenderTarget(target);
- renderer.setClearColor(0x000000, 0);
- renderer.clear(true, true, false);
- renderer.render(scene, options.camera ?? camera);
-
- renderer.setRenderTarget(null);
- scene.overrideMaterial = previousOverride;
- for (const object of hidden) object.visible = true;
- for (const [object, swappedMaterial, renderOrder] of swapped) {
- object.material = swappedMaterial;
- object.renderOrder = renderOrder;
- }
- renderer.setClearColor(previousClear, previousAlpha);
-
- // --- 2. the scene itself -------------------------------------------
- baseRender();
-
- // --- 3. the ink, straight over the top -----------------------------
- renderer.autoClear = false;
- renderer.render(quadScene, quadCamera);
- renderer.autoClear = true;
- }
-
- function dispose() {
- target.dispose();
- for (const material of labelMaterials.values()) material.dispose();
- labelMaterials.clear();
- material.dispose();
- occluderMaterial?.dispose();
- for (const child of quadScene.children) child.geometry.dispose();
- }
-
- return { render, setSize, exclude, occlude, only, uniforms, target, material, dispose };
-}
diff --git a/bluebey-studio/src/panel.js b/bluebey-studio/src/panel.js
deleted file mode 100644
index a6e971d..0000000
--- a/bluebey-studio/src/panel.js
+++ /dev/null
@@ -1,2506 +0,0 @@
-import {
- h, section as uiSection, subhead, hint, slider, check, segmented, buttons,
- colorField, xyPad, selectField, controlRow, tabs, details, toast, icon, svgIcon,
-} from './ui.js';
-import {
- FACE_PRESETS, POSE_PRESETS, defaultState, applyPatch,
-} from './presets.js';
-import { STYLE_DEFS } from './styles.js';
-import { ENVIRONMENTS } from './look.js';
-import { BACKGROUND_PRESETS, EFFECT_PRESETS } from './background.js';
-import { PROP_LIBRARY, applyPropFaceScale, applyPropText } from './props.js';
-import { HAT_LIBRARY } from './hats.js';
-import {
- GION_SHEETS, gionSheetUrl, cropFromMarquee, fitSheet, normalizeRect, clamp, DEFAULT_STAMP_WIDTH,
-} from './gion.js';
-
-/**
- * A small icon for each section heading, so the list a tab shows can be scanned
- * at a glance. Keyed by the section title; a title with no icon just shows text.
- */
-const SECTION_ICONS = {
- 'ポーズ例': 'M12 3.6a2.1 2.1 0 1 1 0 4.2 2.1 2.1 0 1 1 0-4.2ZM12 7.8v6.4M12 10.3 8.4 12.5M12 10.3l3.6 2.2M12 14.2 9 20.4M12 14.2l3 6.2',
- '全身とボーンのスライダー': 'M4 7h16M4 12h16M4 17h16M8 7h.01M15 12h.01M11 17h.01',
- '操作': 'M7 4l10 6-4.3 1.3L10.8 16 7 4Z',
- 'うごき': 'M8 5l10 7-10 7V5Z',
- 'プリセット': 'M12 3.6a8.4 8.4 0 1 1 0 16.8 8.4 8.4 0 1 1 0-16.8ZM8.9 9.8v1M15.1 9.8v1M8.3 13.9a4.7 4.7 0 0 0 7.4 0',
- '目': 'M2.5 12s3.6-6 9.5-6 9.5 6 9.5 6-3.6 6-9.5 6-9.5-6-9.5-6ZM12 14.6a2.6 2.6 0 1 0 0-5.2 2.6 2.6 0 0 0 0 5.2Z',
- '眼鏡・サングラス': 'M8 10.2a3.2 3.2 0 1 0 0 6.4 3.2 3.2 0 0 0 0-6.4ZM16 10.2a3.2 3.2 0 1 0 0 6.4 3.2 3.2 0 0 0 0-6.4ZM11.2 13h1.6M4.8 11.7 3 10.3M19.2 11.7 21 10.3',
- '鼻ちょうちん': 'M15 4.6a4.6 4.6 0 1 0 0 9.2 4.6 4.6 0 0 0 0-9.2ZM10.6 9.6 4.8 11.4',
- '口': 'M4.6 10c3 4.8 11.8 4.8 14.8 0',
- '見る先': 'M12 3.5v3M12 17.5v3M3.5 12h3M17.5 12h3M12 8.2a3.8 3.8 0 1 0 0 7.6 3.8 3.8 0 0 0 0-7.6Z',
- 'スタイル': 'M4 20l3.4-.8L20 6.6 17.4 4 4.8 16.6 4 20Z',
- '見た目の調整': 'M4 8h16M4 16h16M9.5 8h.01M15 16h.01',
- '背景': 'M4 4.5h16a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18V6A1.5 1.5 0 0 1 4 4.5ZM3 16l4-3.5 3.6 3 3.1-2.8 4.3 3.7M15.8 8.8h.01',
- '舞台': 'M4 20V9l8-5 8 5v11M4 20h16M9.5 20v-5h5v5',
- '画面・光': 'M12 7.5a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9ZM12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.2 5.2l1.4 1.4M17.4 17.4l1.4 1.4M18.8 5.2l-1.4 1.4M6.6 17.4 5.2 18.8',
- '画面の情報': 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM12 11v5M12 8h.01',
- '画像': 'M4 5h16v14H4zM4 15l5-4 4 3 4-3.5 3 2.5M15.5 9h.01',
- 'まんが': 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z',
- '共有': 'M8.2 10.6a2 2 0 1 0 0-3.2 2 2 0 0 0 0 3.2ZM17 6.4a2 2 0 1 0 0-3.2 2 2 0 0 0 0 3.2ZM17 20.8a2 2 0 1 0 0-3.2 2 2 0 0 0 0 3.2ZM10 10.4l5-2.6M10 13.6l5 2.6',
- 'テクスチャの下地を書き出す': 'M4 4h16v16H4zM4 9.3h16M4 14.7h16M9.3 4v16M14.7 4v16',
- '設定': 'M12 8.6a3.4 3.4 0 1 0 0 6.8 3.4 3.4 0 0 0 0-6.8ZM12 2.6v2.4M12 19v2.4M2.6 12H5M19 12h2.4M5.2 5.2l1.7 1.7M17.1 17.1l1.7 1.7M18.8 5.2l-1.7 1.7M6.9 17.1l-1.7 1.7',
-};
-
-/** `section`, with the heading icon looked up from its title. */
-const section = (parent, title, options) => uiSection(parent, title, { icon: SECTION_ICONS[title], ...options });
-
-/** Fallbacks for the fields an older settings file may predate. */
-const LOOK_AT_FALLBACKS = { x: 0, y: 2.6, z: 3, amount: 1 };
-
-/**
- * The licence reminder the three image-import buttons show before they open the
- * file picker. Kept in one place so the wording can change without hunting for
- * each button.
- */
-const IMAGE_LICENSE_NOTICE = '読み込む画像は、ご自身で権利をお持ちか、利用許諾のあるものに限ります。ライセンスをご確認ください。';
-
-/** True when the user confirmed they have the right to use the image. */
-function confirmImageLicense() {
- return window.confirm(IMAGE_LICENSE_NOTICE);
-}
-
-/**
- * The panel's multi-line field. The 看板 prop's lettering is the only caller, but
- * the shape (a control row wrapping a textarea) is worth keeping in one place.
- */
-function textArea({ label, value = '', rows = 3, onChange }) {
- const input = h('textarea', {
- rows,
- style: {
- width: '100%',
- boxSizing: 'border-box',
- resize: 'vertical',
- font: 'inherit',
- fontFamily: 'ui-monospace, "Hiragino Kaku Gothic ProN", "Noto Sans JP", monospace',
- fontSize: '11.5px',
- lineHeight: '1.5',
- padding: '6px 8px',
- border: '1px solid #e4dff0',
- borderRadius: '8px',
- background: '#fff',
- color: '#2b2433',
- },
- });
- input.value = value ?? '';
- input.addEventListener('input', () => onChange?.(input.value));
- return {
- el: controlRow(label, input, { wide: true }),
- // Assigning the same text again would jump the caret to the end, so only
- // write when the value actually differs.
- set(v) { const next = v ?? ''; if (input.value !== next) input.value = next; },
- get: () => input.value,
- };
-}
-
-/**
- * The placed prop groups, in state order.
- *
- * WHY this climbs the scene: the app owns the prop rebuild (see `applyProps` in
- * src/main.js) and hands `props.js` only the whole-prop scale, so a sign's
- * per-item `faceScale` has to be re-applied to the freshly built mesh by the one
- * place that knows that value - this panel. There is no direct scene reference
- * here, so this walks up from the model's container and collects the props: they
- * are the only objects tagged `userData.propId`, and the app adds them in order.
- */
-function placedPropGroups(app) {
- const scene = app.container?.parent;
- if (!scene) return [];
- const groups = [];
- scene.traverse((object) => {
- if (object.userData?.propId) groups.push(object);
- });
- return groups;
-}
-
-/**
- * Builds the whole control panel and wires it to the app state.
- *
- * Widgets never call the heavy "apply everything" path while they are being
- * dragged; they patch one scope and ask the app to refresh just that part.
- */
-export function buildPanel(app, root) {
- const state = app.state;
- const model = app.model;
- const rig = app.rig;
-
- const syncers = [];
- const boneSyncers = [];
- const sync = () => { for (const fn of syncers) fn(); };
- const syncBones = () => { for (const fn of boneSyncers) fn(); };
-
- /** Replace the state and re-read every widget (used by presets and loads). */
- const replace = (value, scope = 'all') => app.actions.applyState(value, { scope, sync: true });
-
- root.replaceChildren();
-
- // --- randomising ----------------------------------------------------------
- // Shared by 「顔をランダムに」 (face tab) and 「すべてをランダムに」 (the top bar).
- const rnd = (min, max) => min + (max - min) * Math.random();
- const pick = (list) => list[Math.floor(Math.random() * list.length)];
- const randomEye = () => ({
- open: +rnd(0.4, 1).toFixed(2),
- lookX: +rnd(-0.35, 0.35).toFixed(2),
- lookY: +rnd(-0.25, 0.25).toFixed(2),
- eyeX: Math.round(rnd(-16, 16)),
- });
- const randomGlasses = () => {
- const kind = pick(['glasses', 'sunglasses']);
- const dark = kind === 'sunglasses';
- return {
- enabled: true,
- kind,
- frameColor: pick(['#2a1e33', '#12101a', '#7a5c2e', '#c9c2d6']),
- lensColor: pick(['#2b2433', '#1a1620', '#3a2f4a', '#7a3b1e']),
- lensOpacity: dark ? +rnd(0.85, 1).toFixed(2) : +rnd(0, 0.35).toFixed(2),
- lensGap: Math.round(rnd(-24, 56)),
- frameWidth: +rnd(0.6, 1.8).toFixed(2),
- scale: +rnd(0.9, 1.15).toFixed(2),
- offsetY: Math.round(rnd(-18, 18)),
- tilt: Math.round(rnd(-12, 12)),
- };
- };
- const randomFacePatch = () => {
- return {
- eyes: {
- left: randomEye(),
- right: randomEye(),
- // 眼鏡・サングラスも要素に含める(かける/かけない、各種設定)。
- glasses: Math.random() < 0.5 ? { enabled: false } : randomGlasses(),
- },
- // The mouth takes the height and the tilt too, so the face really changes.
- mouth: {
- smile: +rnd(-1, 1.2).toFixed(2),
- // ときどき丸く開く口(O)にする。
- round: Math.random() < 0.3 ? +rnd(0.35, 1).toFixed(2) : 0,
- open: +rnd(0, 0.5).toFixed(2),
- width: +rnd(0.7, 1.3).toFixed(2),
- thickness: +rnd(0.7, 1.6).toFixed(2),
- tilt: Math.round(rnd(-12, 12)),
- offsetY: Math.round(rnd(-16, 16)),
- },
- };
- };
- const randomFace = () => replace({ face: randomFacePatch() }, 'face');
- const randomAll = () => {
- // 後ろを向く(180°)や大きく傾くポーズは避ける。見える顔が変わる範囲だけ。
- const poses = POSE_PRESETS.filter((preset) => {
- const m = preset.pose?.bones?.master ?? [0, 0, 0];
- return Math.abs(m[0]) <= 45 && Math.abs(m[1]) <= 40 && Math.abs(m[2]) <= 40;
- });
- const pose = pick(poses.length ? poses : POSE_PRESETS);
- app.actions.applyPosePreset?.(pose.id);
- // The viewer style (実写/フラット/線画), the body colours, and 左右反転 are
- // deliberately left alone.
- replace({ face: randomFacePatch() }, 'face');
- sync();
- };
-
- // A small action bar above the tabs: undo/redo apply to every tab, so they must
- // not be buried inside one of them. 「すべてをランダムに」 sits here too.
- const topBar = h('div', { class: 'panel-topbar' });
- const iconButton = (label, d, onClick) => {
- const button = h('button', { type: 'button', class: 'icon-btn', title: label, 'aria-label': label });
- button.append(icon(d));
- button.addEventListener('click', onClick);
- return button;
- };
- topBar.append(
- iconButton('元に戻す', 'M4.8 11.5h9.7a5.2 5.2 0 0 1 0 10.4h-3.6M4.8 11.5 9.6 6.7M4.8 11.5 9.6 16.3', () => app.actions.undo()),
- iconButton('やり直す', 'M19.2 11.5h-9.7a5.2 5.2 0 0 0 0 10.4h3.6M19.2 11.5 14.4 6.7M19.2 11.5 14.4 16.3', () => app.actions.redo()),
- iconButton('すべてをランダムに', 'M7.2 9.4a2.6 2.6 0 1 1 3.9 2.3c-.9.5-1.3 1.2-1.3 2.1v.4M9.2 17.4h.01M16.8 6.8v6.1M16.8 15.6h.01', () => randomAll()),
- );
- root.append(topBar);
-
- // Tabs instead of one long scroll: the panel used to be a wall of controls,
- // and everything is easier to find this way. Each tab is a small stroke icon
- // (its name is the tooltip and the accessible label) sitting on the top edge
- // of the content, so the row reads as tabs rather than buttons.
- const tabBar = tabs(root, [
- {
- id: 'pose',
- label: 'ポーズ',
- // A standing figure: head, body, arms and legs.
- icon: 'M12 3.6a2.1 2.1 0 1 1 0 4.2 2.1 2.1 0 1 1 0-4.2ZM12 7.8v6.4M12 10.3 8.4 12.5M12 10.3l3.6 2.2M12 14.2 9 20.4M12 14.2l3 6.2',
- },
- {
- id: 'face',
- label: '顔',
- // A smiley face: an outline, two eyes and a smile.
- icon: 'M12 3.6a8.4 8.4 0 1 1 0 16.8 8.4 8.4 0 1 1 0-16.8ZM8.9 9.8v1M15.1 9.8v1M8.3 13.9a4.7 4.7 0 0 0 7.4 0',
- },
- {
- id: 'view',
- label: '見た目',
- // A painter's palette: the outline, the thumb hole and four colour dots.
- icon: 'M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.9 0 1.6-.7 1.6-1.7 0-.4-.2-.8-.4-1.1-.3-.3-.4-.7-.4-1.1a1.6 1.6 0 0 1 1.7-1.7h2c3 0 5.5-2.5 5.5-5.6C22 6 17.5 2 12 2ZM13.5 6.5h.01M17.5 10.5h.01M8.5 7.5h.01M6.5 12.5h.01',
- },
- {
- id: 'scene',
- label: 'シーン',
- // A framed picture: background, stage and light all sit inside the frame.
- icon: 'M4 4.5h16a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18V6A1.5 1.5 0 0 1 4 4.5ZM3 16l4-3.5 3.6 3 3.1-2.8 4.3 3.7M15.8 8.8h.01',
- },
- {
- id: 'export',
- label: '書き出し',
- // A download arrow dropping into a tray.
- icon: 'M12 3.8v9.6M8.2 9.8 12 13.6l3.8-3.8M4.5 16.5v1.6a1.8 1.8 0 0 0 1.8 1.8h11.4a1.8 1.8 0 0 0 1.8-1.8v-1.6',
- },
- ]);
- const tabPose = tabBar.panels.pose;
- const tabFace = tabBar.panels.face;
- const tabView = tabBar.panels.view;
- const tabScene = tabBar.panels.scene;
- const tabExport = tabBar.panels.export;
-
- /* ------------------------------------------------------------------ pose */
-
- // ポーズ例: ready-made poses, because most people start by picking one.
- const posePresetSection = section(tabPose, 'ポーズ例');
- posePresetSection.add(buttons({
- items: POSE_PRESETS.map((preset) => ({
- id: preset.id,
- label: preset.label,
- onClick: () => applyPosePreset(preset),
- })),
- }));
-
- // 全身とボーンのスライダー: pick a bone, then turn it on the three axes.
- const poseSection = section(tabPose, '全身とボーンのスライダー');
-
- const boneList = h('div', { class: 'bone-list' });
- const boneButtons = new Map();
- for (const bone of model.bones) {
- const button = h('button', { type: 'button', class: 'bone-item' },
- h('span', { text: bone.label }),
- h('small', { text: bone.name }));
- button.addEventListener('click', () => rig.select(bone.name));
- boneButtons.set(bone.name, button);
- boneList.append(button);
- }
- poseSection.add(controlRow(null, boneList, { wide: true }));
-
- const axisSliders = {
- x: slider({ label: 'よこ(X)', min: -180, max: 180, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: (v) => setAxis('x', v) }),
- y: slider({ label: 'たて(Y)', min: -180, max: 180, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: (v) => setAxis('y', v) }),
- z: slider({ label: 'ねじり(Z)', min: -180, max: 180, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: (v) => setAxis('z', v) }),
- };
- for (const widget of Object.values(axisSliders)) poseSection.add(widget.el);
-
- function setAxis(axis, value) {
- const name = rig.selected;
- if (!name) return;
- const delta = rig.getDelta(name);
- delta[axis] = value;
- rig.setDelta(name, delta);
- app.actions.capturePose();
- app.needsRender = true;
- }
-
- poseSection.add(hint('ボーンは上の一覧から選びます。スライダーは「休めの姿勢からの差」です。'
- + 'ビューポートのぶるべーは、ドラッグで移動、Shift+ドラッグで向きを変えられます。'));
-
- // こまかい設定: the whole-body nudge and the rotation gizmo are tweaks, not the
- // main way in, so they stay folded away inside the bone section.
- const rootSliders = [
- { label: 'よこ移動', min: -20, max: 20 },
- { label: 'たて移動', min: -10, max: 20 },
- { label: 'おくゆき', min: -20, max: 20 },
- ].map(({ label, min, max }, index) => slider({
- label, min, max, step: 0.01, value: 0,
- format: (v) => v.toFixed(2),
- onInput: (v) => { state.pose.root[index] = v; app.needsRender = true; },
- }));
- const poseBox = details(poseSection.body, 'こまかい設定(ぜんたいをずらす・回転ギズモ)');
- for (const widget of rootSliders) poseBox.add(widget.el);
-
- const gizmoToggle = check({
- label: '回転ギズモを表示',
- // Off by default: the rings belong to the model's surface, so having them
- // drawn on top of the character is not what you want while composing a
- // picture. The 'g' key toggles it too.
- value: false,
- onChange: (value) => { rig.setGizmoVisible(value); app.needsRender = true; },
- });
- poseBox.add(controlRow(null, gizmoToggle.el, { wide: true }));
-
- // --- 操作 -----------------------------------------------------------------
- // Deliberately NOT wrapped in a collapsible section: 「ポーズを全部戻す」 and the
- // undo/redo pair are used often enough that they should be on screen without
- // opening anything first.
- tabPose.append(buttons({
- items: [
- { id: 'resetBone', label: 'このボーンを戻す', onClick: () => { rig.reset(rig.selected); app.actions.capturePose(); syncBones(); app.needsRender = true; } },
- { id: 'resetAll', label: 'ポーズを全部戻す', onClick: () => app.actions.resetPose() },
- { id: 'random', label: '少しランダムに', onClick: () => randomPose() },
- ],
- }).el);
- tabPose.append(hint('「元に戻す」「やり直す」は、パネル上部のアイコンにあります(どのタブでも使えます)。'));
-
- /** Accepts a preset object (from the buttons) or a preset id (from tools). */
- function applyPosePreset(presetOrId) {
- const preset = typeof presetOrId === 'string'
- ? POSE_PRESETS.find((item) => item.id === presetOrId)
- : presetOrId;
- if (!preset) return;
- // Some poses already raise the arms; leaving the てをふる motion running would
- // add to the arm the pose just set and fold the flipper over the face, so
- // switching to any pose stops that motion.
- if (state.anim.mode === 'wave') {
- state.anim.mode = 'off';
- app.animator.reset();
- app.needsRender = true;
- }
- state.pose = { bones: {}, root: [0, 0, 0] };
- applyPatch(state.pose, preset.pose ?? {});
- if (!state.pose.root) state.pose.root = [0, 0, 0];
- replace({}, 'pose');
- sync();
- }
-
- function randomPose() {
- const names = model.bones.map((bone) => bone.name);
- for (const name of names) {
- if (name === 'master') continue;
- const scale = name.startsWith('arm') ? 22 : 12;
- rig.setDelta(name, {
- x: (Math.random() - 0.5) * scale,
- y: (Math.random() - 0.5) * scale,
- z: (Math.random() - 0.5) * scale,
- });
- }
- app.actions.capturePose();
- syncBones();
- app.needsRender = true;
- }
-
- /* ------------------------------------------------------------------ face */
-
- let faceSection = section(tabFace, 'プリセット');
-
- // The face tab's sections are created here in the order they should read on
- // screen (プリセット -> 目 -> 見る先 -> 口 -> 眼鏡 -> 鼻ちょうちん -> 帽子),
- // but filled further down where the widgets are built: the DOM order is fixed
- // at creation.
- const faceTabSections = {
- eye: section(tabFace, '目'),
- look: section(tabFace, '見る先'),
- mouth: section(tabFace, '口'),
- glasses: section(tabFace, '眼鏡・サングラス'),
- snot: section(tabFace, '鼻ちょうちん'),
- hat: section(tabFace, '帽子'),
- };
-
- faceSection.add(buttons({
- items: FACE_PRESETS.map((preset) => ({
- id: preset.id,
- label: preset.label,
- onClick: () => applyFacePreset(preset.id),
- })),
- }));
-
- function applyFacePreset(id) {
- const preset = FACE_PRESETS.find((item) => item.id === id);
- if (!preset) return;
- state.face = defaultState().face;
- applyPatch(state.face, preset.face);
- app.actions.refresh('face');
- sync();
- }
-
- // --- eyes
- faceSection = faceTabSections.eye;
-
- /** Tear sliders, which are added to the こまかい設定 box once it exists. */
- const tearWidgets = [];
- // The link switch sits directly above the *left* eye's open slider, because that
- // is the slider it drives - it was previously below both eyes, where it looked
- // like it belonged to whatever came next.
- const linkedToggle = check({
- label: '左右を連動させる',
- value: true,
- onChange: (value) => {
- state.face.eyes.linked = value;
- if (value) {
- // Catch the two eyes up with each other, so switching the link on does
- // something visible even if they had drifted apart.
- state.face.eyes.right.open = state.face.eyes.left.open;
- state.face.eyes.right.closed = state.face.eyes.left.closed;
- state.face.eyes.right.closedLines = state.face.eyes.left.closedLines;
- state.face.eyes.right.irisShape = state.face.eyes.left.irisShape;
- state.face.eyes.right.shape = state.face.eyes.left.shape;
- // The lids are per eye now too, so link them along with the rest.
- state.face.eyes.right.lowerLid = state.face.eyes.left.lowerLid;
- state.face.eyes.right.lidShape = state.face.eyes.left.lidShape;
- state.face.eyes.right.lidWidth = state.face.eyes.left.lidWidth;
- state.face.eyes.right.lidTilt = state.face.eyes.left.lidTilt;
- state.face.eyes.right.lashes = state.face.eyes.left.lashes;
- state.face.eyes.right.lashAngle = state.face.eyes.left.lashAngle;
- state.face.eyes.right.lashPos = state.face.eyes.left.lashPos;
- state.face.eyes.right.white = state.face.eyes.left.white ?? null;
- state.face.eyes.right.highlight = state.face.eyes.left.highlight ?? null;
- state.face.eyes.right.lookX = state.face.eyes.left.lookX;
- state.face.eyes.right.lookY = state.face.eyes.left.lookY;
- // The tears mirror as a pair: the size and the height match, while the
- // sideways position and the tilt flip sign.
- state.face.eyes.right.tear = state.face.eyes.left.tear ?? 0;
- state.face.eyes.right.tearY = state.face.eyes.left.tearY ?? 0;
- state.face.eyes.right.tearX = -(state.face.eyes.left.tearX ?? 0);
- state.face.eyes.right.tearTilt = -(state.face.eyes.left.tearTilt ?? 0);
- }
- app.actions.refresh('face');
- sync();
- },
- });
- // The eye "shape" folds the old 閉じ方 (line/3/わらう) and the heart pupil into
- // one menu. `open`/`closed`/`irisShape` stay the stored fields; this reads and
- // writes them together so the menu always shows the current look.
- const eyeShapeOf = (eye) => {
- const shape = eye.shape;
- const explicitShut = shape === 'three' || shape === 'arch'
- || (typeof shape === 'string' && shape.startsWith('line'));
- if (explicitShut) return shape;
- if ((eye.open ?? 1) <= 0.02) {
- if (!shape) {
- if (eye.closed === 'three') return 'three';
- if (eye.closed === 'arch') return 'arch';
- return `line${Math.min(3, Math.max(1, Math.round(eye.closedLines ?? 1)))}`;
- }
- // An open/heart eye whose lid is all the way down is just a blink.
- return 'line1';
- }
- if (shape) return shape;
- return (eye.irisShape ?? 'circle') === 'heart' ? 'heart' : 'open';
- };
- const applyEyeShape = (eye, value) => {
- eye.shape = value;
- eye.irisShape = value === 'heart' ? 'heart' : 'circle';
- if (value === 'open' || value === 'heart') {
- eye.open = 1;
- // Clear any stale shut fields so a blink (open -> 0) shows a plain line and
- // never the 3 or the arch that was picked before.
- eye.closed = 'line';
- eye.closedLines = 1;
- return;
- }
- // A shut artwork is drawn as the eye's content with the lids open; the lid
- // height is its own slider (上まぶたの高さ), so selecting one opens the lid.
- eye.open = 1;
- if (value.startsWith('line')) {
- eye.closed = 'line';
- eye.closedLines = Number(value.slice(4)) || 1;
- return;
- }
- eye.closed = value;
- };
- const eyeWhiteOf = (eye) => {
- if (eye.white != null) return eye.white === true;
- // The usual: an open eye shows its white; a shut eye or a heart does not.
- return eyeShapeOf(eye) === 'open';
- };
- const eyeHighlightOf = (eye) => (eye.highlight != null
- ? eye.highlight === true
- : state.face.eyes.highlight !== false);
-
- const EYE_ICONS = {
- both: ' ',
- left: ' ',
- right: ' ',
- lid: ' ',
- };
- // Each eye group carries the same controls. Linked, one group ("両目") drives
- // both eyes; unlinked, a group per eye appears and the gaze pad doubles.
- const eyeGroup = (title, icon = null) => {
- const body = h('div', { class: 'eye-group-body' });
- const head = h('div', { class: 'eye-group-head' },
- icon ? svgIcon(EYE_ICONS[icon] ?? '') : null,
- h('span', { text: title }));
- const el = h('div', { class: 'eye-group' }, head, body);
- return {
- el,
- add: (child) => (body.append(child?.el ?? child), child),
- setVisible: (visible) => { el.style.display = visible ? '' : 'none'; },
- };
- };
-
- const makeEyeControls = (mode) => {
- const keys = mode === 'both' ? ['left', 'right'] : [mode];
- const primary = keys[0];
- const write = (fn) => { for (const k of keys) fn(state.face.eyes[k]); };
- const touched = () => { app.actions.refresh('face'); sync(); };
- const group = eyeGroup(mode === 'both' ? '両目' : (mode === 'left' ? '左目' : '右目'), mode);
-
- // --- まぶた (per eye) ------------------------------------------------
- // 上まぶたの高さ and 下まぶたの高さ share one opening, so each is held to the
- // other: the lower lid cannot rise past the upper, and the upper cannot drop
- // below the lower. The sliders' own limits are kept in step in `syncOne`.
- const lidSub = eyeGroup('まぶた', 'lid');
- const open = slider({
- label: '上まぶたの高さ', min: 0, max: 1, step: 0.01, value: 1,
- format: (v) => (v < 0.02 ? '閉じ' : v > 0.98 ? '開き' : `${Math.round(v * 100)}%`),
- onInput: (v) => {
- const floor = state.face.eyes[primary].lowerLid ?? state.face.eyes.lowerLid ?? 0;
- const vv = Math.max(v, floor);
- write((e) => { e.open = vv; });
- if (vv !== v) open.set(vv);
- touched();
- },
- });
- const lowerLid = slider({
- label: '下まぶたの高さ', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2),
- onInput: (v) => {
- const ceil = state.face.eyes[primary].open ?? 1;
- const vv = Math.min(v, ceil);
- write((e) => { e.lowerLid = vv; });
- if (vv !== v) lowerLid.set(vv);
- touched();
- },
- });
- const lidShape = segmented({
- label: 'まぶたの形',
- options: [
- { value: 'curve', label: '曲線' },
- { value: 'flat', label: '直線(平ら)' },
- ],
- value: 'curve',
- onChange: (v) => { write((e) => { e.lidShape = v; }); touched(); },
- });
- const lidWidth = slider({
- label: 'まぶたの太さ', min: 0, max: 30, step: 1, value: 14, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { write((e) => { e.lidWidth = v; }); touched(); },
- });
- const lidTilt = slider({
- label: 'まぶたの傾き', min: -40, max: 40, step: 1, value: 0, format: (v) => `${Math.round(v)}°`,
- onInput: (v) => { write((e) => { e.lidTilt = v; }); touched(); },
- });
- const lashes = check({
- label: 'まつげをつける',
- value: false,
- onChange: (v) => { write((e) => { e.lashes = v; }); touched(); },
- });
- const lashAngle = slider({
- label: 'まつげの角度', min: -80, max: 80, step: 1, value: 0, format: (v) => `${Math.round(v)}°`,
- onInput: (v) => { write((e) => { e.lashAngle = v; }); touched(); },
- });
- const lashPos = slider({
- label: 'まつげの位置', min: -80, max: 80, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { write((e) => { e.lashPos = v; }); touched(); },
- });
- lidSub.add(lidShape);
- lidSub.add(open);
- lidSub.add(lowerLid);
- lidSub.add(lidWidth);
- lidSub.add(lidTilt);
- lidSub.add(controlRow(null, lashes.el, { wide: true }));
- lidSub.add(lashAngle);
- lidSub.add(lashPos);
- group.add(lidSub);
- const openInput = open.el.querySelector('input');
- const lowerLidInput = lowerLid.el.querySelector('input');
-
- // 目の形: the artwork the eye shows. A shut shape is drawn with the lids open,
- // so 上まぶたの高さ can then be lowered onto it.
- const shape = segmented({
- label: '目の形',
- options: [
- { value: 'open', label: 'ふつう' },
- { value: 'line1', label: '1線' },
- { value: 'line2', label: '2線' },
- { value: 'line3', label: '3線' },
- { value: 'three', label: '3の目' },
- { value: 'arch', label: 'わらう' },
- { value: 'heart', label: 'ハート' },
- ],
- value: 'open',
- onChange: (v) => { write((e) => applyEyeShape(e, v)); touched(); },
- });
- // The heart colour only matters for the heart, so it sits right under 目の形
- // and shows only then.
- const heartColor = colorField({
- label: 'ハートの色', value: '#e0344f',
- swatches: ['#e0344f', '#ff5f8f', '#c2185b', '#150e1b'],
- onChange: (v) => { state.face.eyes.heartColor = v; touched(); },
- });
- // 大きさ: one knob for the iris, the heart and the shut artwork.
- const size = slider({
- label: '大きさ', min: 0.4, max: 1.8, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.eyes.irisScale = v; touched(); },
- });
- const threeFlip = segmented({
- label: '3の向き',
- options: [
- { value: 'normal', label: '3' },
- { value: 'flip', label: '3(逆)' },
- ],
- value: 'normal',
- onChange: (v) => { write((e) => { e.threeFlip = v === 'flip'; applyEyeShape(e, 'three'); }); touched(); },
- });
- const white = check({
- label: '白目を出す',
- value: true,
- onChange: (v) => { write((e) => { e.white = v; }); touched(); },
- });
- const highlight = check({
- label: '光彩(瞳の白い丸)を出す',
- value: true,
- onChange: (v) => { write((e) => { e.highlight = v; }); touched(); },
- });
- const pad = xyPad({
- value: { x: 0, y: 0 },
- center: 'eye',
- onChange: ({ x, y }) => { write((e) => { e.lookX = x; e.lookY = y; }); touched(); },
- });
- // 涙: per eye. Linked, one set drives both - size and height match, while the
- // sideways position and the tilt mirror so the pair stays symmetric.
- const tearOn = check({
- label: '涙を出す',
- value: false,
- onChange: (v) => { write((e) => { e.tearOn = v; }); touched(); },
- });
- const writeTear = (patch) => {
- Object.assign(state.face.eyes[primary], patch);
- if (mode === 'both') {
- const mirrored = { ...patch };
- if (patch.tearX != null) mirrored.tearX = -patch.tearX;
- if (patch.tearTilt != null) mirrored.tearTilt = -patch.tearTilt;
- Object.assign(state.face.eyes[primary === 'left' ? 'right' : 'left'], mirrored);
- }
- touched();
- };
- const tear = slider({
- label: '涙の大きさ', min: 0.3, max: 1.6, step: 0.01, value: 0.3, format: (v) => v.toFixed(2),
- onInput: (v) => writeTear({ tear: v }),
- });
- const tearY = slider({
- label: '涙の高さ', min: -80, max: 140, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => writeTear({ tearY: v }),
- });
- const tearX = slider({
- label: '涙のよこ位置', min: -150, max: 150, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => writeTear({ tearX: v }),
- });
- const tearTilt = slider({
- label: '涙の傾き', min: -60, max: 60, step: 1, value: 0, format: (v) => `${Math.round(v)}°`,
- onInput: (v) => writeTear({ tearTilt: v }),
- });
-
- group.add(shape);
- group.add(heartColor);
- group.add(size);
- group.add(threeFlip);
- group.add(controlRow(null, white.el, { wide: true }));
- group.add(controlRow(null, highlight.el, { wide: true }));
- group.add(controlRow(null, tearOn.el, { wide: true }));
- group.add(tear);
- group.add(tearY);
- group.add(tearX);
- group.add(tearTilt);
- group.add(controlRow('目線', h('div', { class: 'pad-wrap' }, pad.el,
- h('p', { class: 'hint', text: 'ドラッグで目線。ダブルクリックで正面に戻ります。' }))));
-
- const syncOne = () => {
- const e = state.face.eyes[primary];
- const shapeValue = eyeShapeOf(e);
- const lower = e.lowerLid ?? state.face.eyes.lowerLid ?? 0;
- const upper = e.open ?? 1;
- // Keep each lid's slider range from crossing the other before applying the
- // values, so the displayed thumb cannot sit past the other lid either.
- if (openInput) openInput.min = String(Math.min(lower, upper));
- if (lowerLidInput) lowerLidInput.max = String(Math.max(lower, upper));
- open.set(upper);
- lowerLid.set(lower);
- lidShape.set(e.lidShape ?? state.face.eyes.lidShape ?? 'curve');
- lidWidth.set(e.lidWidth ?? state.face.eyes.lidWidth ?? 14);
- lidTilt.set(e.lidTilt ?? state.face.eyes.lidTilt ?? 0);
- lashes.set((e.lashes ?? state.face.eyes.lashes) === true);
- const lashesOn = (e.lashes ?? state.face.eyes.lashes) === true;
- lashAngle.set(e.lashAngle ?? state.face.eyes.lashAngle ?? 0);
- lashPos.set(e.lashPos ?? state.face.eyes.lashPos ?? 0);
- lashAngle.el.style.display = lashesOn ? '' : 'none';
- lashPos.el.style.display = lashesOn ? '' : 'none';
- shape.set(shapeValue);
- heartColor.set(state.face.eyes.heartColor ?? '#e0344f');
- heartColor.el.style.display = shapeValue === 'heart' ? '' : 'none';
- size.set(state.face.eyes.irisScale ?? 1);
- threeFlip.set(e.threeFlip ? 'flip' : 'normal');
- threeFlip.el.style.display = shapeValue === 'three' ? '' : 'none';
- white.set(eyeWhiteOf(e));
- highlight.set(eyeHighlightOf(e));
- const tearShown = e.tearOn === true;
- tearOn.set(tearShown);
- for (const widget of [tear, tearY, tearX, tearTilt]) {
- widget.el.style.display = tearShown ? '' : 'none';
- }
- tear.set(e.tear ?? 0.3);
- tearY.set(e.tearY ?? 0);
- tearX.set(e.tearX ?? 0);
- tearTilt.set(e.tearTilt ?? 0);
- pad.set({ x: e.lookX ?? 0, y: e.lookY ?? 0 });
- };
- return { group, sync: syncOne };
- };
-
- const bothControls = makeEyeControls('both');
- const leftControls = makeEyeControls('left');
- const rightControls = makeEyeControls('right');
-
- faceSection.add(controlRow(null, linkedToggle.el, { wide: true }));
- faceSection.add(bothControls.group);
- faceSection.add(leftControls.group);
- faceSection.add(rightControls.group);
- faceSection.add(hint('「目の形」の「2線」「3線」は端点がつながった形になります。'
- + '閉じ目の形も、上まぶた・下まぶたで上下から隠せます。'));
-
- // 横位置 (per eye) rarely changes, so it gets its own small box.
- const eyeAdvancedWidgets = {};
- for (const key of ['left', 'right']) {
- const label = key === 'left' ? '左目' : '右目';
- const eyeX = slider({
- label: `${label}の横位置`, min: -200, max: 200, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (value) => { state.face.eyes[key].eyeX = value; app.actions.refresh('face'); },
- });
- eyeAdvancedWidgets[key] = { eyeX };
- tearWidgets.push(eyeX.el);
- }
-
- // One button to put every eye setting back to the artwork's defaults. The
- // artwork *source* is kept: resetting the numbers should not throw away a
- // hand-drawn image the user loaded.
- faceSection.add(buttons({
- items: [{
- id: 'eyesReset',
- label: '目の設定をリセット',
- onClick: () => {
- const source = state.face.eyes.source;
- state.face.eyes = { ...defaultState().face.eyes, source };
- app.actions.refresh('face');
- sync();
- },
- }],
- }));
-
- faceSection.add(hint('「まぶた」は目のグループごとに設定できます。「左右を連動させる」を入れると「両目」の設定が1つだけ出て、'
- + 'まぶた・目の形・大きさ・白目・光彩・目線が両目でそろいます。'
- + 'はずすと「左目」「右目」の設定がそれぞれに出て、目線のパッドも2つになります。'
- + '「左目」「右目」は、ぶるべー自身から見た左右です(正面から見ると、画面では左右が入れ替わって見えます)。'));
-
- const eyeColors = {
- white: colorField({ label: '白目', value: '#ffffff', onChange: (v) => { state.face.eyes.white = v; app.actions.refresh('face'); } }),
- iris: colorField({ label: '瞳', value: '#150e1b', onChange: (v) => { state.face.eyes.iris = v; app.actions.refresh('face'); } }),
- line: colorField({ label: 'まぶたの線', value: '#55386e', onChange: (v) => { state.face.eyes.line = v; app.actions.refresh('face'); } }),
- };
- const eyeColorBox = details(faceSection.body, '色(白目・瞳・まぶたの線)');
- for (const widget of Object.values(eyeColors)) eyeColorBox.add(widget.el);
-
- // 横位置 (per eye) is rarely used, so it stays in its own small box.
- const tearBox = details(faceSection.body, '横位置(左右べつ)');
- for (const el of tearWidgets) tearBox.add(el);
-
- // --- glasses / sunglasses (drawn into the same texture as the eyes)
- faceSection = faceTabSections.glasses;
- faceSection.add(hint('眼鏡やサングラスを、目と同じ絵にかけます。'
- + '線画のときはレンズを塗らず、フレームの線だけになります。'));
-
- // 眼鏡とサングラスの違いは、レンズの形だけではない。サングラスはレンズが不透明
- // なので、目を開けたままだと瞳が透けて見えてしまう。種類を変えたらその種類の
- // 既定値(レンズの色・濃さ・高さ)を一緒に持ってきて、サングラスのときは両目を
- // 閉じる。フレームや大きさには触らないので、作りこんだフレームは種類を変えても
- // 失われない。
- const GLASSES_KINDS = {
- glasses: { lensColor: '#2b2433', lensOpacity: 0.22, offsetY: 0 },
- sunglasses: { lensColor: '#1a1620', lensOpacity: 1, offsetY: -40 },
- };
- const wearSunglasses = () => {
- state.face.eyes.glasses.lensOpacity = 1;
- state.face.eyes.left.open = 0;
- state.face.eyes.right.open = 0;
- };
- const setGlassesKind = (kind) => {
- Object.assign(state.face.eyes.glasses, { kind, ...GLASSES_KINDS[kind] });
- // サングラス hides the eye behind an opaque lens, so both eyes shut with it;
- // 眼鏡's lens is clear, so switching back opens them again.
- const open = kind === 'sunglasses' ? 0 : 1;
- state.face.eyes.left.open = open;
- state.face.eyes.right.open = open;
- };
-
- const glassesToggle = check({
- label: '眼鏡をかける',
- value: false,
- onChange: (value) => {
- state.face.eyes.glasses.enabled = value;
- // A loaded state can already be on サングラス, so enabling the shades also
- // shuts the eyes rather than showing them through the opaque lens.
- if (value && state.face.eyes.glasses.kind === 'sunglasses') wearSunglasses();
- app.actions.refresh('face');
- sync();
- },
- });
- faceSection.add(controlRow(null, glassesToggle.el, { wide: true }));
-
- const glassesKindSegment = segmented({
- label: '種類',
- options: [
- { value: 'glasses', label: '眼鏡' },
- { value: 'sunglasses', label: 'サングラス' },
- ],
- value: 'glasses',
- onChange: (value) => {
- setGlassesKind(value);
- // Picking a kind means you mean to wear it, so put the glasses on rather
- // than leaving the checkbox off and nothing showing on the face.
- state.face.eyes.glasses.enabled = true;
- glassesToggle.set(true);
- app.actions.refresh('face');
- sync();
- },
- });
- faceSection.add(glassesKindSegment.el);
-
- const glassesSliders = {
- scale: slider({
- label: '大きさ', min: 0.7, max: 1.5, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.eyes.glasses.scale = v; app.actions.refresh('face'); },
- }),
- lensOpacity: slider({
- label: 'レンズの濃さ', min: 0, max: 1, step: 0.01, value: 0.22,
- format: (v) => (v <= 0.005 ? '透明' : v.toFixed(2)),
- onInput: (v) => { state.face.eyes.glasses.lensOpacity = v; app.actions.refresh('face'); },
- }),
- lensGap: slider({
- label: '左右レンズの間隔', min: -40, max: 120, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.glasses.lensGap = v; app.actions.refresh('face'); },
- }),
- frameWidth: slider({
- label: 'フレームの太さ', min: 0.2, max: 3, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.eyes.glasses.frameWidth = v; app.actions.refresh('face'); },
- }),
- offsetY: slider({
- label: '高さ', min: -60, max: 60, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.glasses.offsetY = v; app.actions.refresh('face'); },
- }),
- tilt: slider({
- label: '傾き', min: -30, max: 30, step: 1, value: 0, format: (v) => `${Math.round(v)}°`,
- onInput: (v) => { state.face.eyes.glasses.tilt = v; app.actions.refresh('face'); },
- }),
- };
- faceSection.add(glassesSliders.scale.el);
- faceSection.add(glassesSliders.lensOpacity.el);
- const glassesBox = details(faceSection.body, 'こまかい設定(フレーム・位置・色)');
- glassesBox.add(glassesSliders.frameWidth.el);
- glassesBox.add(glassesSliders.offsetY.el);
- glassesBox.add(glassesSliders.tilt.el);
- glassesBox.add(glassesSliders.lensGap.el);
- const glassesFrameColor = colorField({
- label: 'フレームの色', value: '#2a1e33',
- swatches: ['#2a1e33', '#12101a', '#7a5c2e', '#c9c2d6'],
- onChange: (v) => { state.face.eyes.glasses.frameColor = v; app.actions.refresh('face'); },
- });
- const glassesLensColor = colorField({
- label: 'レンズの色', value: '#2b2433',
- swatches: ['#2b2433', '#1a1620', '#3a2f4a', '#7a3b1e'],
- onChange: (v) => { state.face.eyes.glasses.lensColor = v; app.actions.refresh('face'); },
- });
- glassesBox.add(glassesFrameColor.el);
- glassesBox.add(glassesLensColor.el);
- glassesBox.add(hint('「レンズの濃さ」を 0 にするとレンズは透明になり、フレームだけの眼鏡になります。'
- + '「高さ」は眼鏡全体を上下に動かします(+で下がります)。'
- + '「傾き」は左右のレンズが一緒に傾きます。'));
- glassesBox.add(buttons({
- items: [
- {
- id: 'glasses',
- label: '眼鏡',
- onClick: () => {
- setGlassesKind('glasses');
- state.face.eyes.glasses.enabled = true;
- app.actions.refresh('face');
- sync();
- },
- },
- {
- id: 'sunglasses',
- label: 'サングラス',
- onClick: () => {
- setGlassesKind('sunglasses');
- state.face.eyes.glasses.enabled = true;
- app.actions.refresh('face');
- sync();
- },
- },
- ],
- }));
-
- // --- 鼻ちょうちん: the snot bubble of a sleeping face.
- faceSection = faceTabSections.snot;
- const snotToggle = check({
- label: '鼻ちょうちんを出す',
- value: false,
- onChange: (value) => { state.face.eyes.snot.enabled = value; app.actions.refresh('face'); },
- });
- faceSection.add(controlRow(null, snotToggle.el, { wide: true }));
- const snotSize = slider({
- label: '大きさ', min: 0.4, max: 2.2, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.eyes.snot.size = v; app.actions.refresh('face'); },
- });
- faceSection.add(snotSize.el);
- const snotBox = details(faceSection.body, 'こまかい設定(位置・色)');
- const snotSliders = {
- offsetX: slider({
- label: 'よこの位置', min: -160, max: 160, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.snot.offsetX = v; app.actions.refresh('face'); },
- }),
- offsetY: slider({
- label: 'たかさ', min: -160, max: 160, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.snot.offsetY = v; app.actions.refresh('face'); },
- }),
- offsetZ: slider({
- label: 'おくゆき', min: -10, max: 160, step: 1, value: 0, format: (v) => `${Math.round(v)}`,
- onInput: (v) => { state.face.eyes.snot.offsetZ = v; app.actions.refresh('face'); },
- }),
- };
- for (const widget of Object.values(snotSliders)) snotBox.add(widget.el);
- const snotColor = colorField({
- label: '泡の色', value: '#e3ecff',
- swatches: ['#e3ecff', '#ffffff', '#cfe0ff', '#bfe8d0'],
- onChange: (v) => { state.face.eyes.snot.color = v; app.actions.refresh('face'); },
- });
- snotBox.add(snotColor.el);
- snotBox.add(hint('寝ている顔の横に、鼻から出る泡を描きます。'
- + '「ねている」ポーズと組み合わせると、いねむりしているように見えます。'
- + '泡は鼻の横に出るので、位置は「よこの位置」「たかさ」で調整できます。'));
-
- // --- mouth
- faceSection = faceTabSections.mouth;
-
- const mouthToggle = check({
- label: '口を表示',
- value: true,
- onChange: (value) => { state.face.mouth.visible = value; app.actions.refresh('face'); },
- });
- faceSection.add(controlRow(null, mouthToggle.el, { wide: true }));
-
- // 口角の角度は「笑いの深さ」に比例して動かす(笑いの深さ 1 で -8°、-1 で 70°、
- // その間は直線)。笑いの深さを動かすと追従し、手で微調整したいときのために
- // スライダー自体は残してある。
- const cornerAngleFor = (smile) => Math.round(31 - 39 * Math.max(-1, Math.min(1.4, smile)));
-
- const mouthSliders = {
- smile: slider({ label: '曲げ', min: -1, max: 1.4, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.smile = v; state.face.mouth.cornerAngle = cornerAngleFor(v); app.actions.refresh('face'); sync(); } }),
- open: slider({ label: '開き', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.open = v; app.actions.refresh('face'); } }),
- round: slider({ label: 'Oの大きさ', min: 0, max: 1, step: 0.01, value: 0, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.round = v; app.actions.refresh('face'); } }),
- width: slider({ label: '幅', min: 0.2, max: 1.6, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.width = v; app.actions.refresh('face'); } }),
- thickness: slider({ label: '太さ', min: 0.2, max: 3, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.thickness = v; app.actions.refresh('face'); } }),
- tilt: slider({ label: '傾き', min: -30, max: 30, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: (v) => { state.face.mouth.tilt = v; app.actions.refresh('face'); } }),
- offsetY: slider({ label: '高さ', min: -140, max: 140, step: 1, value: 0, format: (v) => `${Math.round(v)}`, onInput: (v) => { state.face.mouth.offsetY = v; app.actions.refresh('face'); } }),
- tongue: slider({ label: '舌の大きさ', min: 0, max: 1.6, step: 0.01, value: 0.96, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.tongue = v; app.actions.refresh('face'); } }),
- tonguePos: slider({ label: '舌の位置', min: 0.05, max: 0.95, step: 0.01, value: 0.84, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.tonguePos = v; app.actions.refresh('face'); } }),
- corners: slider({ label: '口角の長さ', min: 0, max: 1.6, step: 0.01, value: 0.71, format: (v) => v.toFixed(2), onInput: (v) => { state.face.mouth.corners = v; app.actions.refresh('face'); } }),
- cornerAngle: slider({ label: '口角の角度', min: -70, max: 70, step: 1, value: -8, format: (v) => `${Math.round(v)}°`, onInput: (v) => { state.face.mouth.cornerAngle = v; app.actions.refresh('face'); } }),
- };
- // 口には「ふつうの口」と「丸く開く(O)」の2つの形があり、使うスライダーが
- // まったく違う。形を切り替えると、その形で使うものだけを出す。
- const mouthModeOf = (mouth) => (mouth.round > 0.004 ? 'round' : 'curve');
- const mouthRowKeys = {
- curve: ['smile', 'width', 'thickness', 'open', 'tongue', 'tonguePos', 'corners', 'cornerAngle', 'tilt', 'offsetY'],
- round: ['round', 'width', 'thickness', 'tongue', 'tilt', 'offsetY'],
- };
- // DOM order is fixed here; the mode only shows or hides rows.
- const mouthRowOrder = ['round', 'smile', 'width', 'thickness', 'open', 'tilt', 'offsetY', 'tongue', 'tonguePos', 'corners', 'cornerAngle'];
- const mouthMode = segmented({
- label: '口の形',
- options: [
- { value: 'curve', label: 'ふつうの口' },
- { value: 'round', label: '丸く開く(O)' },
- ],
- value: 'curve',
- onChange: (shape) => {
- if (shape === 'round') {
- if (!(state.face.mouth.round > 0.004)) state.face.mouth.round = 0.6;
- } else {
- state.face.mouth.round = 0;
- }
- app.actions.refresh('face');
- sync();
- },
- });
- faceSection.add(mouthMode);
- for (const key of mouthRowOrder) faceSection.add(mouthSliders[key].el);
- faceSection.add(hint('「口の形」を切り替えると、その形で使うスライダーだけが出ます。'
- + 'ふつうの口は「曲げ」と「開き」、丸く開く口(O)は「Oの大きさ」で形が決まります。'));
- faceSection.add(buttons({
- items: [{
- id: 'mouthreset',
- label: '口を初期値に戻す',
- onClick: () => {
- state.face.mouth = defaultState().face.mouth;
- app.actions.refresh('face');
- sync();
- },
- }],
- }));
-
- const mouthColors = {
- color: colorField({ label: '口の線', value: '#ff1a44', onChange: (v) => { state.face.mouth.color = v; app.actions.refresh('face'); } }),
- innerColor: colorField({ label: '口の中', value: '#4a0f1e', onChange: (v) => { state.face.mouth.innerColor = v; app.actions.refresh('face'); } }),
- tongueColor: colorField({ label: '舌', value: '#ff2d2d', onChange: (v) => { state.face.mouth.tongueColor = v; app.actions.refresh('face'); } }),
- cornerColor: colorField({ label: '口角', value: '#725497', onChange: (v) => { state.face.mouth.cornerColor = v; app.actions.refresh('face'); } }),
- };
- const mouthColorBox = details(faceSection.body, '色(口の線・中・舌・口角)');
- for (const widget of Object.values(mouthColors)) mouthColorBox.add(widget.el);
-
- /* ------------------------------------------------------ movement (pose tab) */
-
- // This is the pose tab's うごき section; the widgets sit here because they are
- // driven by the animator alongside the pose state.
- const moveSection = section(tabPose, 'うごき');
-
- const animWidgets = {
- blink: check({ label: 'まばたきする', value: true, onChange: (v) => { state.anim.blink = v; app.animator.reset(); } }),
- lookAround: check({ label: '目線がうごく', value: false, onChange: (v) => { state.anim.lookAround = v; app.animator.reset(); } }),
- };
-
- const motionSegment = segmented({
- label: 'からだのうごき',
- options: [
- { value: 'off', label: 'とまる' },
- { value: 'idle', label: 'ゆれる' },
- { value: 'walk', label: '歩く' },
- { value: 'wave', label: 'てをふる' },
- { value: 'sleep', label: 'ねている' },
- ],
- value: 'off',
- onChange: (value) => {
- state.anim.mode = value;
- // Sleeping always shows the snot bubble, so the motion reads without having
- // to switch the bubble on as well.
- if (value === 'sleep') {
- state.face.eyes.snot.enabled = true;
- snotToggle.set(true);
- app.actions.refresh('face');
- }
- app.animator.reset();
- app.needsRender = true;
- },
- });
- moveSection.add(motionSegment.el);
- for (const widget of Object.values(animWidgets)) moveSection.add(controlRow(null, widget.el, { wide: true }));
-
- moveSection.add(slider({
- label: 'まばたきの間隔', min: 1, max: 8, step: 0.1, value: 3.4, format: (v) => `${v.toFixed(1)}秒`,
- onInput: (v) => { state.anim.blinkInterval = v; },
- }));
- moveSection.add(slider({
- label: '動きの速さ', min: 0.2, max: 2, step: 0.05, value: 1, format: (v) => `${v.toFixed(2)}倍`,
- onInput: (v) => { state.anim.speed = v; },
- }));
-
- const recordButton = buttons({
- items: [{ id: 'record', label: '録画を開始', primary: true, onClick: () => app.actions.toggleRecording() }],
- });
- moveSection.add(recordButton.el);
- moveSection.add(hint('スペースキーで演出の開始・停止。録画はWebM(動画)として保存されます。'));
-
- // --- 見る先 (moved here, next to the face controls, from the pose tab) ----
- const lookAtSection = faceTabSections.look;
- // `lookAt` moves the eyes and the face only, so the face has to be rebuilt too.
- const refreshLookAt = () => {
- app.actions.refresh('lookAt');
- app.actions.refresh('face');
- sync();
- };
- const lookAtToggle = check({
- label: '指定した場所を見る',
- value: false,
- onChange: (value) => { state.lookAt.enabled = value; refreshLookAt(); },
- });
- lookAtSection.add(controlRow(null, lookAtToggle.el, { wide: true }));
- const lookAtSliders = {
- x: slider({
- label: 'よこ(X)', min: -6, max: 6, step: 0.05, value: 0, format: (v) => v.toFixed(2),
- onInput: (v) => { state.lookAt.x = v; refreshLookAt(); },
- }),
- y: slider({
- label: 'たかさ(Y)', min: 0, max: 5, step: 0.05, value: 2.6, format: (v) => v.toFixed(2),
- onInput: (v) => { state.lookAt.y = v; refreshLookAt(); },
- }),
- z: slider({
- label: 'おくゆき(Z)', min: -6, max: 6, step: 0.05, value: 3, format: (v) => v.toFixed(2),
- onInput: (v) => { state.lookAt.z = v; refreshLookAt(); },
- }),
- amount: slider({
- label: '強さ', min: 0, max: 1, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.lookAt.amount = v; refreshLookAt(); },
- }),
- };
- for (const widget of Object.values(lookAtSliders)) lookAtSection.add(widget.el);
- const turnBodyToggle = check({
- label: '体も向ける',
- value: false,
- onChange: (value) => { state.lookAt.turnBody = value; refreshLookAt(); },
- });
- lookAtSection.add(controlRow(null, turnBodyToggle.el, { wide: true }));
- lookAtSection.add(hint('ビューポートを**クリック**すると、その場所を見るように目が向きます(ドラッグと区別するため、動かさずに押して離してください)。数値でも同じ値を動かせます。'));
-
- /* ---------------------------------------------------------------- display */
-
- // The face tab's bottom line: a quick way out of whatever has been built up.
- tabFace.append(buttons({
- items: [
- { id: 'faceReset', label: '顔をリセット', onClick: () => replace({ face: defaultState().face }, 'face') },
- { id: 'faceRandom', label: '顔をランダムに', onClick: () => randomFace() },
- ],
- }).el);
-
- const displaySection = section(tabView, 'スタイル');
-
- const styleSegment = segmented({
- label: 'スタイル',
- options: STYLE_DEFS.map((style) => ({ value: style.value, label: style.label })),
- value: 'real',
- onChange: (value) => replace({ render: { style: value } }, 'render'),
- });
- displaySection.add(styleSegment.el);
- displaySection.add(hint('線画は輪郭線+白ぬりです(体に色がつきません)。線だけを他の絵に重ねたいときは、「書き出し」タブで「PNGの背景を透明にする」を入れてください。'));
-
- const outlineToggle = check({
- label: '輪郭線を出す', value: true,
- onChange: (value) => { state.render.outline = value; app.actions.refresh('render'); },
- });
- displaySection.add(controlRow(null, outlineToggle.el, { wide: true }));
- const outlineWidthSlider = slider({
- label: '線の太さ(体・手足)', min: 0, max: 0.08, step: 0.001, value: 0.022, format: (v) => v.toFixed(3),
- onInput: (v) => { state.render.outlineWidth = v; app.actions.refresh('render'); },
- });
- displaySection.add(outlineWidthSlider.el);
- const outlinePixelsSlider = slider({
- label: '線の太さ(葉っぱ・鼻)', min: 1, max: 5, step: 0.1, value: 2,
- format: (v) => `${v.toFixed(1)} px`,
- onInput: (v) => { state.render.outlinePixels = v; app.actions.refresh('render'); },
- });
- displaySection.add(outlinePixelsSlider.el);
- displaySection.add(colorField({
- label: '線の色', value: '#2a1e33',
- swatches: ['#2a1e33', '#111111', '#55386e', '#0f5c8c', '#8c4a0f'],
- onChange: (v) => { state.render.outlineColor = v; app.actions.refresh('render'); },
- }));
- displaySection.add(colorField({
- label: '紙の色', value: '#ffffff',
- swatches: ['#ffffff', '#fdf8ee', '#f4f0fb'],
- onChange: (v) => { state.render.paper = v; app.actions.refresh('render'); },
- }));
- const leafBodyLineCheck = check({
- label: '葉っぱと体の境目に線を出す', value: true,
- onChange: (v) => { state.render.leafBodyLine = v; app.actions.refresh('render'); },
- title: '線画では体も葉っぱも紙なので、切ると葉っぱと体の区別がつかなくなります。'
- + '「葉が体にめり込んで見える」のが気になるときに切ってください',
- });
- displaySection.add(controlRow(null, leafBodyLineCheck.el, { wide: true }));
- displaySection.add(hint('「葉っぱと体の境目に線を出す」を切ると、葉は体の後ろに回り込むだけになります(すっきりしますが、線画では葉と体が同じ白になって見分けにくくなります)。'));
- displaySection.add(buttons({
- items: [{
- id: 'resetStyle',
- label: 'プリセットのスタイルに戻す',
- onClick: () => replace({
- render: {
- style: 'real', outline: true, outlineWidth: 0.022, outlinePixels: 2,
- outlineColor: '#2a1e33', paper: '#ffffff', leafBodyLine: true,
- },
- }, 'render'),
- }],
- }));
-
- /* ------------------------------------------------------------- 帽子 (顔タブ) */
-
- const hatSection = faceTabSections.hat;
- hatSection.add(hint('ぶるべーの頭に帽子をかぶせます。「なし」を選ぶと外れます。'));
- const hatSegment = segmented({
- label: '帽子',
- options: HAT_LIBRARY.map((hat) => ({ value: hat.id, label: hat.label })),
- value: 'none',
- onChange: (value) => {
- state.face.hat.kind = value;
- // Picking one of the built-in hats (including なし) also clears any GLB hat
- // the user imported, so なし really removes the hat.
- state.face.hat.custom = false;
- app.actions.refresh('all');
- },
- });
- hatSection.add(hatSegment.el);
- syncers.push(() => hatSegment.set(state.face.hat?.custom === true ? 'none' : (state.face.hat?.kind ?? 'none')));
-
- // 帽子モデル(GLB)の取り込み。頭に乗るよう自動で位置と大きさを合わせる。
- const hatFilePicker = h('input', { type: 'file', accept: '.glb,.gltf,model/gltf-binary,model/gltf+json', style: { display: 'none' } });
- hatFilePicker.addEventListener('change', async () => {
- const file = hatFilePicker.files?.[0];
- hatFilePicker.value = '';
- if (!file) return;
- try {
- await app.actions.loadHatModel(file);
- toast(`帽子「${file.name}」を読み込みました`);
- } catch (error) {
- console.error(error);
- toast('帽子モデルを読み込めませんでした(GLBをお使いください)');
- }
- });
- hatSection.add(hatFilePicker);
- hatSection.add(buttons({
- items: [{ id: 'hatfile', label: '帽子モデルを読み込む(GLB)', onClick: () => hatFilePicker.click() }],
- }));
- hatSection.add(hint('GLBの帽子を頭に乗せます。大きさと位置は自動で合わせます。'));
-
- const hatBox = details(hatSection.body, '位置と傾き');
- const hatSliders = {
- height: slider({
- label: '高さ', min: -1.2, max: 1.2, step: 0.01, value: 0, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.hat.height = v; app.actions.refresh('all'); },
- }),
- x: slider({
- label: 'よこ位置', min: -1.2, max: 1.2, step: 0.01, value: 0, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.hat.x = v; app.actions.refresh('all'); },
- }),
- z: slider({
- label: 'おくゆき', min: -1.2, max: 1.2, step: 0.01, value: 0, format: (v) => v.toFixed(2),
- onInput: (v) => { state.face.hat.z = v; app.actions.refresh('all'); },
- }),
- tiltX: slider({
- label: '前後の傾き', min: -45, max: 45, step: 1, value: 0, format: (v) => `${Math.round(v)}°`,
- onInput: (v) => { state.face.hat.tiltX = v; app.actions.refresh('all'); },
- }),
- tiltZ: slider({
- label: '左右の傾き', min: -45, max: 45, step: 1, value: 0, format: (v) => `${Math.round(v)}°`,
- onInput: (v) => { state.face.hat.tiltZ = v; app.actions.refresh('all'); },
- }),
- };
- for (const widget of Object.values(hatSliders)) hatBox.add(widget.el);
- hatBox.add(hint('「高さ」「よこ位置」「おくゆき」で帽子を動かし、「前後の傾き」「左右の傾き」で斜めにかぶせられます。'));
- syncers.push(() => {
- hatSliders.height.set(state.face.hat?.height ?? 0);
- hatSliders.x.set(state.face.hat?.x ?? 0);
- hatSliders.z.set(state.face.hat?.z ?? 0);
- hatSliders.tiltX.set(state.face.hat?.tiltX ?? 0);
- hatSliders.tiltZ.set(state.face.hat?.tiltZ ?? 0);
- });
-
- /* ---------------------------------------------------------- 環境の映りこみ */
-
- // The body colours are fixed (see `render.colors` in presets.js), so the 体の色
- // section is gone. The strength of the environment reflection is still a look
- // setting, so it lives in 見た目の調整 below.
- const envIntensitySlider = slider({
- label: '環境の映りこみ', min: 0, max: 2, step: 0.05, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.render.envIntensity = v; app.actions.refresh('render'); },
- });
-
- /* -------------------------------------------------------------------- look */
-
- const lookSection = section(tabView, '見た目の調整');
- const mirrorToggle = check({
- label: '左右反転',
- value: false,
- onChange: (value) => { state.render.mirror = value; app.actions.refresh('render'); sync(); },
- });
- lookSection.add(controlRow(null, mirrorToggle.el, { wide: true }));
- lookSection.add(hint('体だけでなく、目や口の位置までふくめて反転します。文字と並べるときの向きを決めるときに使います。'));
-
- const shadowCheck = check({
- label: '影を落とす',
- value: true,
- onChange: (value) => { state.render.shadow = value; app.actions.refresh('render'); },
- });
- lookSection.add(controlRow(null, shadowCheck.el, { wide: true }));
- const shadowOpacitySlider = slider({
- label: '濃さ', min: 0, max: 0.6, step: 0.01, value: 0.22, format: (v) => v.toFixed(2),
- onInput: (v) => { state.render.shadowOpacity = v; app.actions.refresh('render'); },
- });
- const shadowSoftnessSlider = slider({
- label: 'ぼかし', min: 0, max: 3, step: 0.05, value: 1.6, format: (v) => v.toFixed(2),
- onInput: (v) => { state.render.shadowSoftness = v; app.actions.refresh('render'); },
- });
- lookSection.add(shadowOpacitySlider.el);
- lookSection.add(shadowSoftnessSlider.el);
- const contactShadowToggle = check({
- label: '接地影だけ',
- value: false,
- onChange: (value) => { state.render.contactShadow = value; app.actions.refresh('render'); },
- });
- lookSection.add(controlRow(null, contactShadowToggle.el, { wide: true }));
- const contactBlobToggle = check({
- label: '足もとの丸い影',
- value: false,
- onChange: (value) => { state.render.contactBlob = value; app.actions.refresh('render'); },
- });
- lookSection.add(controlRow(null, contactBlobToggle.el, { wide: true }));
- lookSection.add(hint('影の向きは「ひかり」の光の向き(パッド)にしたがって変わります。'
- + '「接地影だけ」は足もとの丸い影だけにします。'
- + '「足もとの丸い影」は、足もとに薄く敷く丸い影です(はじめは出ていません)。'));
- // 環境の映りこみ: the strength of the environment reflection, moved here from
- // the old 体の色 section (the body colours themselves are fixed now).
- lookSection.add(envIntensitySlider.el);
-
- const environmentSegment = segmented({
- label: '照明の種類',
- options: ENVIRONMENTS.map((env) => ({ value: env.value, label: env.label })),
- value: 'gradient',
- onChange: (value) => { state.render.environment = value; app.actions.refresh('render'); },
- });
- // 照明の種類は「シーン」タブの「画面・光」に置く(環境の映りこみの元を選ぶものなので)。
- lookSection.add(buttons({
- items: [{
- id: 'resetLook',
- label: 'プリセットに戻す',
- onClick: () => replace({
- render: {
- mirror: false, shadow: true, shadowOpacity: 0.22, shadowSoftness: 1.6,
- contactShadow: false, contactBlob: false, environment: 'gradient', envIntensity: 1,
- },
- }, 'render'),
- }],
- }));
-
- /* -------------------------------------------------------------- background */
-
- const backdropSection = section(tabScene, '背景');
- // 素材 is one button for two state values (a photo or an effect line). This
- // remembers which of the two was picked last, so leaving 素材 and coming back
- // returns to it instead of always jumping to the photos.
- let lastMaterial = 'preset';
- const backgroundModeSegment = segmented({
- label: '背景',
- options: [
- { value: 'solid', label: '単色' },
- { value: 'transparent', label: '透明' },
- { value: 'material', label: '素材' },
- { value: 'image', label: '自分の画像' },
- { value: 'camera', label: 'カメラ=AR' },
- ],
- value: 'solid',
- onChange: (value) => {
- if (value === 'camera') {
- // The action owns the stream, so let it decide and re-read the state after.
- Promise.resolve(app.actions.toggleCamera(true)).then(() => sync());
- return;
- }
- if (state.view.background === 'camera') app.actions.toggleCamera(false);
- state.view.background = value === 'material' ? lastMaterial : value;
- app.actions.refresh('view');
- sync();
- },
- });
- backdropSection.add(backgroundModeSegment.el);
- // 背景の色 only shows in the 単色 mode; it used to live in the スタイル section.
- const backgroundColour = colorField({
- label: '背景の色', value: '#ffffff',
- swatches: ['#ffffff', '#f4f0fb', '#d9f0ff', '#fff0f4', '#1b1230'],
- onChange: (v) => { state.view.backgroundColor = v; app.actions.refresh('view'); },
- });
- backdropSection.add(backgroundColour.el);
-
- const photoButtons = buttons({
- label: '写真',
- items: BACKGROUND_PRESETS.map((preset) => ({
- id: preset.name,
- label: preset.label,
- onClick: () => {
- state.view.backgroundPreset = preset.name;
- lastMaterial = 'preset';
- state.view.background = 'preset';
- app.actions.refresh('view');
- sync();
- },
- })),
- });
-
- const effectButtons = buttons({
- label: '効果線(まんが)',
- items: EFFECT_PRESETS.map((effect) => ({
- id: `effect-${effect.name}`,
- label: effect.label,
- onClick: () => {
- state.view.backgroundEffect = effect.name;
- lastMaterial = 'effect';
- state.view.background = 'effect';
- app.actions.refresh('view');
- sync();
- },
- })),
- });
-
- // The two rows are shown only while 素材 is the active mode (see the syncer
- // below); the border groups them so they read as one choice, not two.
- const materialGroup = h('div', {
- class: 'material-group',
- style: { display: 'none', paddingLeft: '8px', borderLeft: '2px solid #e4dff0' },
- }, photoButtons.el, effectButtons.el, hint('効果線はアプリが描いています(画像ファイルなし・ネット不要)。集中線=放射、落ち込み線=上から下、疾走線=左右です。'));
- backdropSection.add(materialGroup);
-
- const backgroundImagePicker = h('input', { type: 'file', accept: 'image/*', style: { display: 'none' } });
- backgroundImagePicker.addEventListener('change', async () => {
- const file = backgroundImagePicker.files?.[0];
- backgroundImagePicker.value = '';
- if (!file) return;
- try {
- const dataUrl = await app.backdrop.loadImageFile(file);
- state.view.backgroundImage = dataUrl;
- state.view.background = 'image';
- app.actions.refresh('view');
- sync();
- toast(`背景の画像「${file.name}」を読み込みました`);
- } catch (error) {
- console.error(error);
- toast('画像を読み込めませんでした(PNG / JPG をお使いください)');
- }
- });
- backdropSection.add(backgroundImagePicker);
- backdropSection.add(buttons({
- items: [{ id: 'bgimg', label: '画像を読み込む', onClick: () => { if (confirmImageLicense()) backgroundImagePicker.click(); } }],
- }));
-
- const backgroundFitSegment = segmented({
- label: '合わせ方',
- options: [
- { value: 'cover', label: '全体' },
- { value: 'contain', label: '収める' },
- { value: 'stretch', label: '伸ばす' },
- { value: 'tile', label: '並べる' },
- ],
- value: 'cover',
- onChange: (value) => { state.view.backgroundFit = value; app.actions.refresh('view'); },
- });
- const backgroundBlurSlider = slider({
- label: 'ぼかし', min: 0, max: 20, step: 0.5, value: 0, format: (v) => `${v.toFixed(1)}px`,
- onInput: (v) => { state.view.backgroundBlur = v; app.actions.refresh('view'); },
- });
- const backgroundDarkenSlider = slider({
- label: '暗さ', min: 0, max: 0.8, step: 0.01, value: 0, format: (v) => v.toFixed(2),
- onInput: (v) => { state.view.backgroundDarken = v; app.actions.refresh('view'); },
- });
- const backgroundScaleSlider = slider({
- label: '大きさ', min: 0.5, max: 2, step: 0.01, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.view.backgroundScale = v; app.actions.refresh('view'); },
- });
- backdropSection.add(backgroundFitSegment.el);
- backdropSection.add(backgroundBlurSlider.el);
- backdropSection.add(backgroundDarkenSlider.el);
- backdropSection.add(backgroundScaleSlider.el);
-
- const cameraFacingSegment = segmented({
- label: 'カメラ',
- options: [
- { value: 'user', label: '前' },
- { value: 'environment', label: '後ろ' },
- ],
- value: 'environment',
- onChange: (value) => {
- state.view.cameraFacing = value;
- // Only a live stream has to be restarted on the other lens.
- if (state.view.background === 'camera') {
- Promise.resolve(app.actions.toggleCamera(true)).then(() => sync());
- return;
- }
- app.actions.refresh('view');
- },
- });
- const cameraMirrorToggle = check({
- label: '左右反転',
- value: false,
- onChange: (value) => { state.view.cameraMirror = value; app.actions.refresh('view'); },
- });
- backdropSection.add(cameraFacingSegment.el);
- backdropSection.add(controlRow(null, cameraMirrorToggle.el, { wide: true }));
-
- // A gyro reading is a delta from the pose the device held when the switch was
- // turned on, so the camera does not jump when it starts listening.
- let gyroBase = null;
- let gyroOff = null;
- const gyroToggle = check({
- label: '端末を傾けて見る',
- value: false,
- onChange: (value) => {
- state.view.gyro = value;
- if (!value) {
- gyroBase = null;
- if (gyroOff) { gyroOff(); gyroOff = null; }
- return;
- }
- gyroBase = { azimuth: Number(state.view.azimuth) || 0, polar: Number(state.view.polar) || 76 };
- app.backdrop?.requestGyro?.();
- gyroOff = gyroOff ?? app.backdrop?.onGyro?.((reading) => {
- if (!gyroBase) return;
- const yaw = reading?.yaw ?? 0;
- const pitch = reading?.pitch ?? 0;
- state.view.azimuth = Math.max(-180, Math.min(180, gyroBase.azimuth - yaw));
- state.view.polar = Math.max(1, Math.min(179, gyroBase.polar - pitch));
- app.actions.refresh('view');
- }) ?? null;
- },
- });
- backdropSection.add(controlRow(null, gyroToggle.el, { wide: true }));
- backdropSection.add(hint('カメラ(AR)を使うには、https のサイトか localhost で開いてください。http のLANアドレスでは、どのブラウザでもカメラは使えません。使えるときは、URLバーのアイコンを「許可」にし、端末の設定でもアプリにカメラを許可してください。'));
- backdropSection.add(buttons({
- items: [{ id: 'bgshot', label: 'この背景で写真を撮る', primary: true, onClick: () => app.actions.savePNG() }],
- }));
-
- /* ------------------------------------------------------------------- props */
-
- // 小物: the things you put the character *among*. Each stage feature gets its
- // own top-level section rather than one shared 舞台 drawer, so they are easier
- // to find and open independently.
- const propSection = section(tabScene, '小物');
- propSection.add(buttons({
- label: '追加',
- items: PROP_LIBRARY.map((entry) => ({
- id: entry.id,
- label: entry.label,
- onClick: () => { app.actions.addProp(entry.id); sync(); },
- })),
- }));
- propSection.add(hint('小物はキャラクターのまわりに置かれ、背景と同じくそのまま写真に写ります。'
- + 'ビューポートで小物をドラッグすると移動できます(数値でも動かせます)。'));
- const propListBox = h('div', { class: 'prop-list' });
- propSection.add(controlRow(null, propListBox, { wide: true }));
-
- const propWidgets = [];
- let propShape = '';
-
- /**
- * Rebuild the list only when the *set* of props changes: a rebuild mid-drag
- * would replace the slider the user is holding. The handlers read the item by
- * index rather than keeping a reference, so a state swap (undo, load) is safe.
- */
- const renderProps = () => {
- const items = state.props?.items ?? [];
- const shape = items.map((item) => item.kind).join('|');
- if (shape === propShape && propWidgets.length === items.length) return;
- propShape = shape;
- propWidgets.length = 0;
- propListBox.replaceChildren();
- items.forEach((item, index) => {
- const label = PROP_LIBRARY.find((entry) => entry.id === item.kind)?.label ?? item.kind;
- const block = h('div', {
- class: 'prop-item',
- style: { borderTop: '1px dashed #e4dff0', paddingTop: '8px', display: 'grid', gap: '6px' },
- }, subhead(items.length > 1 ? `${label} ${index + 1}` : label));
- const patch = (key) => (value) => {
- const target = state.props?.items?.[index];
- if (target) target[key] = value;
- app.actions.refresh('render');
- sync();
- };
- const sliders = {
- x: slider({ label: 'よこ(X)', min: -4, max: 4, step: 0.05, value: 0, format: (v) => v.toFixed(2), onInput: patch('x') }),
- y: slider({ label: 'たかさ(Y)', min: 0, max: 2, step: 0.05, value: 0, format: (v) => v.toFixed(2), onInput: patch('y') }),
- z: slider({ label: 'おくゆき(Z)', min: -4, max: 4, step: 0.05, value: 0, format: (v) => v.toFixed(2), onInput: patch('z') }),
- rotX: slider({ label: '前後の傾き', min: -180, max: 180, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: patch('rotX') }),
- rotY: slider({ label: '回転(よこ)', min: -180, max: 180, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: patch('rotY') }),
- rotZ: slider({ label: '左右の傾き', min: -180, max: 180, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: patch('rotZ') }),
- scale: slider({ label: '大きさ', min: 0.4, max: 2, step: 0.01, value: 1, format: (v) => v.toFixed(2), onInput: patch('scale') }),
- };
- // The numbers are only a fallback for the drag, so they stay folded away.
- const numbers = details(block, '数値で調整');
- for (const widget of Object.values(sliders)) numbers.add(widget.el);
- // 看板 only: the writing area (face *and* frame) can be enlarged on its
- // own, so the posts and base stay put. The app rebuilds the props from the
- // state and carries only the whole-prop scale, so the props syncer
- // re-applies this to the fresh mesh (see `applyPropFaceScale`).
- let faceScale = null;
- let signText = null;
- if (item.kind === 'sign') {
- signText = textArea({
- label: '看板の文字(改行できます)',
- value: '',
- onChange: (value) => {
- const target = state.props?.items?.[index];
- if (target) target.text = value;
- // Paint straight onto the live face rather than rebuilding every prop
- // on each keystroke; a later rebuild re-applies it (see main.js).
- applyPropText(placedPropGroups(app)[index], value);
- app.needsRender = true;
- },
- });
- block.insertBefore(signText.el, numbers.el);
- faceScale = slider({
- label: '文字部分の大きさ', min: 0.4, max: 2.4, step: 0.01, value: 1,
- format: (v) => v.toFixed(2), onInput: patch('faceScale'),
- });
- block.insertBefore(faceScale.el, numbers.el);
- }
- block.append(buttons({
- items: [{
- id: 'remove',
- label: '削除',
- onClick: () => {
- state.props = state.props ?? { items: [] };
- state.props.items = (state.props.items ?? []).filter((_, i) => i !== index);
- app.actions.refresh('render');
- renderProps();
- sync();
- },
- }],
- }).el);
- propListBox.append(block);
- propWidgets.push({ index, sliders, faceScale, signText });
- });
- };
- renderProps();
-
- /* ------------------------------------------------------------- 擬音 */
-
- // 擬音: manga onomatopoeia as stickers over the picture. The sheets are dense,
- // so the user picks the crop with a marquee instead of the app slicing them
- // (see src/gion.js for why). Only the sheet name and the crop rectangle live in
- // the state - never the bitmap - so a shared link stays small.
- const gionSection = section(tabScene, '擬音(マンガのオノマトペ)');
- gionSection.add(hint('擬音(ドドド、バン!など)の画像から、四角く切り出したスタンプを画面に貼れます。'
- + '「擬音をえらぶ」で画像を開き、ドラッグで四角を描いて「追加」を押してください。'
- + '貼ったあとはビューポートでドラッグして動かせます。'));
-
- const gionSheetSelect = selectField({
- label: 'シート',
- options: GION_SHEETS.map((sheet) => ({ value: sheet.name, label: sheet.label })),
- value: GION_SHEETS[0]?.name ?? '',
- onChange: () => {},
- });
- if (GION_SHEETS.length) {
- gionSection.add(gionSheetSelect.el);
- gionSection.add(buttons({
- items: [{
- id: 'pick',
- label: '擬音をえらぶ',
- primary: true,
- onClick: () => openGionPicker(gionSheetSelect.get()),
- }],
- }));
- } else {
- // No sheet ships with the studio (see src/gion.js), so there is nothing to
- // pick yet - the section stays, but only with a note on how to add one.
- gionSection.add(hint('擬音の画像がまだありません。assets/manga-gion/ に画像を置き、'
- + 'src/gion.js の GION_SHEETS に登録すると使えます。'));
- }
-
- const gionListBox = h('div', { class: 'gion-list' });
- gionSection.add(controlRow(null, gionListBox, { wide: true }));
-
- let gionShape = '';
- const gionWidgets = [];
-
- /** Select a stamp: the viewport outlines it and the list highlights it. */
- const selectGion = (id) => {
- app.gionSelected = id ?? null;
- app.actions.refresh('gion');
- sync();
- };
-
- /**
- * A little canvas preview of a stamp's crop, drawn from the sheet once it has
- * loaded. The sheet itself is only fetched when a stamp first needs it.
- */
- const gionPreviews = new Map();
- function gionPreview(item) {
- const canvas = h('canvas', { class: 'gion-thumb', width: 84, height: 56 });
- const draw = (image) => {
- if (!image?.naturalWidth) return;
- const ctx = canvas.getContext('2d');
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- const scale = Math.min(canvas.width / item.sw, canvas.height / item.sh);
- const w = item.sw * scale;
- const h = item.sh * scale;
- ctx.drawImage(image, item.sx, item.sy, item.sw, item.sh,
- (canvas.width - w) / 2, (canvas.height - h) / 2, w, h);
- };
- const known = gionPreviews.get(item.sheet);
- if (known) {
- draw(known);
- } else {
- const image = new Image();
- image.onload = () => draw(image);
- image.src = gionSheetUrl(item.sheet);
- gionPreviews.set(item.sheet, image);
- }
- return canvas;
- }
-
- /** Rebuild the list only when the *set* of stamps changes (see `renderProps`). */
- const renderGion = () => {
- const items = state.gion?.items ?? [];
- const shape = items
- .map((item) => `${item.id}:${item.sheet}:${item.sx},${item.sy},${item.sw},${item.sh}`)
- .join('|');
- if (shape === gionShape && gionWidgets.length === items.length) return;
- gionShape = shape;
- gionWidgets.length = 0;
- gionListBox.replaceChildren();
- if (!items.some((item) => item.id === app.gionSelected)) app.gionSelected = null;
- if (items.length === 0) {
- gionListBox.append(h('p', { class: 'hint', text: 'まだ擬音は貼られていません。' }));
- return;
- }
- items.forEach((item, index) => {
- const label = GION_SHEETS.find((sheet) => sheet.name === item.sheet)?.label ?? item.sheet;
- const block = h('div', {
- class: 'gion-item',
- style: { borderTop: '1px dashed #e4dff0', paddingTop: '8px', display: 'grid', gap: '6px' },
- });
- const preview = gionPreview(item);
- preview.title = 'クリックで選択';
- preview.addEventListener('click', () => selectGion(item.id));
- const head = h('div', { class: 'gion-item-head' }, preview,
- subhead(`擬音 ${index + 1}(${label})`));
- block.append(head);
-
- const patch = (key) => (value) => {
- const target = state.gion?.items?.[index];
- if (target) target[key] = value;
- app.actions.refresh('gion');
- sync();
- };
- const sliders = {
- w: slider({
- label: '大きさ', min: 40, max: 900, step: 1, value: DEFAULT_STAMP_WIDTH,
- format: (v) => `${Math.round(v)}px`, onInput: patch('w'),
- }),
- rot: slider({
- label: '回転', min: -180, max: 180, step: 1, value: 0,
- format: (v) => `${Math.round(v)}°`, onInput: patch('rot'),
- }),
- };
- const flip = check({ label: '左右反転', value: false, onChange: patch('flip') });
- block.append(sliders.w.el, sliders.rot.el, controlRow(null, flip.el, { wide: true }));
- block.append(buttons({
- items: [
- { id: 'select', label: 'この擬音を選ぶ', onClick: () => selectGion(item.id) },
- {
- id: 'remove',
- label: '削除',
- onClick: () => {
- state.gion = state.gion ?? { items: [] };
- state.gion.items = (state.gion.items ?? []).filter((_, i) => i !== index);
- if (app.gionSelected === item.id) app.gionSelected = null;
- app.actions.refresh('gion');
- renderGion();
- sync();
- },
- },
- ],
- }).el);
- gionListBox.append(block);
- gionWidgets.push({ id: item.id, sliders, flip, block });
- });
- };
- renderGion();
-
- /**
- * The picker: a full-screen overlay showing the whole sheet, on which the user
- * drags a rectangle. "追加" turns that rectangle into a stamp in the middle of
- * the picture; Esc or キャンセル closes without adding anything.
- */
- const openGionPicker = (sheetName) => {
- const sheet = GION_SHEETS.find((entry) => entry.name === sheetName) ?? GION_SHEETS[0];
- if (!sheet) { toast('擬音のシートがありません'); return; }
-
- const image = h('img', { class: 'gion-picker-sheet', alt: sheet.label, src: gionSheetUrl(sheet.name) });
- const marquee = h('div', { class: 'gion-picker-marquee' });
- const board = h('div', { class: 'gion-picker-board' }, image, marquee);
- const addButton = h('button', { type: 'button', class: 'btn primary', text: '追加', disabled: true });
- const cancelButton = h('button', { type: 'button', class: 'btn', text: 'キャンセル' });
- const overlay = h('div', { class: 'gion-picker' },
- h('div', { class: 'gion-picker-bar' },
- h('div', { class: 'gion-picker-title', text: `「${sheet.label}」から擬音をえらぶ` }),
- h('div', { class: 'gion-picker-actions' },
- h('span', { class: 'hint', text: 'ドラッグで四角を描く/Escで閉じる' }),
- addButton,
- cancelButton)),
- board);
-
- /** Where the sheet is currently displayed, in board pixels. */
- let sheetRect = null;
- /** The marquee the user is drawing, or the last one drawn. */
- let current = null;
- let start = null;
-
- const layoutSheet = () => {
- if (!image.naturalWidth) return;
- const rect = fitSheet(image, { width: board.clientWidth, height: board.clientHeight }, { padding: 12 });
- image.style.left = `${rect.x}px`;
- image.style.top = `${rect.y}px`;
- image.style.width = `${rect.w}px`;
- image.style.height = `${rect.h}px`;
- sheetRect = rect;
- };
-
- const updateAdd = () => {
- addButton.disabled = !(current && current.w >= 6 && current.h >= 6 && sheetRect);
- };
-
- const drawMarquee = () => {
- if (!current) { marquee.style.display = 'none'; return; }
- marquee.style.display = 'block';
- marquee.style.left = `${current.x}px`;
- marquee.style.top = `${current.y}px`;
- marquee.style.width = `${current.w}px`;
- marquee.style.height = `${current.h}px`;
- };
-
- const pointIn = (event, bounds) => ({
- x: clamp(event.clientX - bounds.left, 0, bounds.width),
- y: clamp(event.clientY - bounds.top, 0, bounds.height),
- });
-
- board.addEventListener('pointerdown', (event) => {
- if (event.pointerType === 'mouse' && event.button !== 0) return;
- start = pointIn(event, board.getBoundingClientRect());
- current = null;
- drawMarquee();
- updateAdd();
- try { board.setPointerCapture(event.pointerId); } catch { /* a nicety, not a need */ }
- });
- board.addEventListener('pointermove', (event) => {
- if (!start) return;
- current = normalizeRect(start, pointIn(event, board.getBoundingClientRect()));
- drawMarquee();
- updateAdd();
- });
- const endMarquee = (event) => {
- if (!start) return;
- start = null;
- try { if (board.hasPointerCapture(event.pointerId)) board.releasePointerCapture(event.pointerId); } catch { /* done anyway */ }
- updateAdd();
- };
- board.addEventListener('pointerup', endMarquee);
- board.addEventListener('pointercancel', endMarquee);
- image.addEventListener('load', () => { layoutSheet(); updateAdd(); });
-
- addButton.addEventListener('click', () => {
- if (!current || !sheetRect) return;
- const crop = cropFromMarquee(current, sheetRect, image);
- app.actions.addGionStamp({ sheet: sheet.name, ...crop });
- close();
- });
- cancelButton.addEventListener('click', () => close());
- overlay.addEventListener('pointerdown', (event) => { if (event.target === overlay) close(); });
-
- const onKey = (event) => {
- if (event.key !== 'Escape') return;
- event.preventDefault();
- close();
- };
- const onResize = () => layoutSheet();
-
- let closed = false;
- const close = () => {
- if (closed) return;
- closed = true;
- document.removeEventListener('keydown', onKey);
- window.removeEventListener('resize', onResize);
- overlay.remove();
- };
-
- document.addEventListener('keydown', onKey);
- window.addEventListener('resize', onResize);
- document.body.append(overlay);
- // A cached sheet is already loaded, so the `load` event never fires for it.
- if (image.complete && image.naturalWidth) { layoutSheet(); updateAdd(); }
- };
-
- /* ------------------------------------------------------------- 見えない壁 */
-
- // 見えない壁: place up to two finite "boards" and bury the character in them, so
- // only the part in front still shows. A wall's position is kept *relative to the
- // character*, so moving ぶるべー carries its walls along (see src/clip.js).
- const wallSection = section(tabScene, '見えない壁');
- wallSection.add(hint('見えない板(有限の壁)を置くと、その向こう側が隠れます。'
- + 'ぶるべーを壁に埋め込むと、体の一部だけが見えるようになります。'
- + '板は2枚まで置けます。板の位置はぶるべーからの相対なので、ぶるべーを動かすと壁も一緒に動きます。'
- + '板そのものは透明です(下の「板を表示」をオンにしたときだけ見えます)。'));
-
- const WALL_SLOTS = [
- { key: 'wall', label: '壁 1' },
- { key: 'wall2', label: '壁 2' },
- ];
-
- const wallGroups = WALL_SLOTS.map(({ key, label }) => {
- const box = details(wallSection.body, label);
- const cfg = () => (state.render[key] = state.render[key] ?? defaultState().render[key]);
- const toggle = check({
- label: `${label}で一部を隠す`,
- value: false,
- onChange: (value) => { cfg().on = value; app.actions.refresh('render'); sync(); },
- });
- box.add(controlRow(null, toggle.el, { wide: true }));
- const sliders = {
- x: slider({ label: 'よこ(X)', min: -4, max: 4, step: 0.05, value: 0, format: (v) => v.toFixed(2), onInput: (v) => { cfg().x = v; app.actions.refresh('render'); sync(); } }),
- y: slider({ label: 'たかさ(Y)', min: -2, max: 4, step: 0.05, value: 0, format: (v) => v.toFixed(2), onInput: (v) => { cfg().y = v; app.actions.refresh('render'); sync(); } }),
- z: slider({ label: 'おくゆき(Z)', min: -4, max: 4, step: 0.05, value: 0, format: (v) => v.toFixed(2), onInput: (v) => { cfg().z = v; app.actions.refresh('render'); sync(); } }),
- yaw: slider({ label: '向き(左右)', min: -180, max: 180, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: (v) => { cfg().yaw = v; app.actions.refresh('render'); sync(); } }),
- tilt: slider({ label: '傾き(前後)', min: -90, max: 90, step: 1, value: 0, format: (v) => `${Math.round(v)}°`, onInput: (v) => { cfg().tilt = v; app.actions.refresh('render'); sync(); } }),
- size: slider({ label: '大きさ', min: 0.2, max: 8, step: 0.1, value: 1, format: (v) => v.toFixed(1), onInput: (v) => { cfg().size = v; app.actions.refresh('render'); sync(); } }),
- };
- for (const widget of Object.values(sliders)) box.add(widget.el);
-
- // The wall writes depth but no colour, so it hides what is behind it without
- // being visible. The guide is the tinted copy of that same rectangle, shown
- // only while placing - and it is also the switch that says "I am placing the
- // wall now", so with it off the wall cannot swallow an orbit drag.
- const guide = check({
- label: '板を表示(ビューポートでドラッグして動かせます)',
- value: false,
- onChange: (value) => { cfg().guide = value; app.actions.refresh('render'); sync(); },
- });
- box.add(controlRow(null, guide.el, { wide: true }));
- return { key, label, toggle, sliders, guide };
- });
-
- wallSection.add(hint('板は「大きさ」の四角い壁そのもので、**その後ろ側が隠れます**'
- + '(カメラから見て板の後ろになる部分が隠れるので、カメラを回すと見え方も変わります)。'
- + '「板を表示」を切ると板は完全に透明になり、カメラ操作のじゃまもしません。'
- + '「板を表示」した板は、ビューポートでドラッグしても動かせます。'));
- wallSection.add(buttons({
- items: [{
- id: 'wallReset',
- label: '見えない壁をリセット',
- onClick: () => {
- state.render.wall = defaultState().render.wall;
- state.render.wall2 = defaultState().render.wall2;
- app.actions.refresh('render');
- sync();
- },
- }],
- }));
-
- /* ------------------------------------------------------------------ light */
-
- // 画面・光: the light rig and the camera, which together decide how the picture
- // is lit and framed.
- const screenLightSection = section(tabScene, '画面・光');
- const lightSection = details(screenLightSection.body, 'ひかり');
- const lightPad = xyPad({
- value: { x: -0.2, y: 0.45 },
- onChange: ({ x, y }) => {
- state.render.lightAzimuth = x * 180;
- state.render.lightElevation = 5 + Math.max(0, y) * 80;
- app.actions.refresh('render');
- },
- });
- lightSection.add(controlRow('光の向き', h('div', { class: 'pad-wrap' },
- lightPad.el,
- h('p', { class: 'hint', text: 'ドラッグで光源の向きを変えられます。' }))));
- const lightIntensitySlider = slider({
- label: '明るさ', min: 0, max: 5, step: 0.05, value: 2.1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.render.lightIntensity = v; app.actions.refresh('render'); },
- });
- const ambientSlider = slider({
- label: '環境光', min: 0, max: 3, step: 0.05, value: 0.9, format: (v) => v.toFixed(2),
- onInput: (v) => { state.render.ambient = v; app.actions.refresh('render'); },
- });
- const exposureSlider = slider({
- label: '露光', min: 0.4, max: 2, step: 0.02, value: 1, format: (v) => v.toFixed(2),
- onInput: (v) => { state.render.exposure = v; app.actions.refresh('render'); },
- });
- const lightColorField = colorField({
- label: '光の色', value: '#ffffff',
- swatches: ['#ffffff', '#fff3d6', '#d6e8ff', '#ffd9c9', '#e6dcff'],
- onChange: (v) => { state.render.lightColor = v; app.actions.refresh('render'); },
- });
- const ambientColorField = colorField({
- label: '環境光の色', value: '#ffffff',
- swatches: ['#ffffff', '#d9d0f2', '#cfe6ff', '#ffe6cc', '#e8ffe0'],
- onChange: (v) => { state.render.ambientColor = v; app.actions.refresh('render'); },
- });
- lightSection.add(environmentSegment.el);
- lightSection.add(lightIntensitySlider.el);
- lightSection.add(ambientSlider.el);
- lightSection.add(lightColorField.el);
- lightSection.add(ambientColorField.el);
- lightSection.add(exposureSlider.el);
- lightSection.add(hint('「照明の種類」は環境の映りこみの元(グラデーション/部屋/なし)です。'
- + '「光の色」でライトの色、「環境光の色」で影側の色を変えられます。'
- + 'テクスチャ(環境)の映りこみの強さは「見た目」タブの「環境の映りこみ」で調整できます。'));
-
- /* ----------------------------------------------------------------- camera */
-
- const cameraSection = details(screenLightSection.body, 'カメラ');
-
- const projectionSegment = segmented({
- label: '映し方',
- options: [
- { value: 'persp', label: '遠近あり' },
- { value: 'ortho', label: '正投影' },
- ],
- value: 'persp',
- onChange: (value) => replace({ view: { projection: value } }, 'view'),
- });
- cameraSection.add(projectionSegment.el);
- cameraSection.add(hint('正投影にすると遠近のゆがみが消え、図面のような絵になります。'));
-
- cameraSection.add(buttons({
- label: '向き',
- items: [
- { id: 'front', label: '正面', onClick: () => app.actions.setCameraPreset('front') },
- { id: 'threeQuarter', label: '斜め', onClick: () => app.actions.setCameraPreset('threeQuarter') },
- { id: 'side', label: '横', onClick: () => app.actions.setCameraPreset('side') },
- { id: 'back', label: '後ろ', onClick: () => app.actions.setCameraPreset('back') },
- { id: 'top', label: '見下ろし', onClick: () => app.actions.setCameraPreset('top') },
- ],
- }));
-
- const cameraSliders = {
- azimuth: slider({
- label: '回転', min: -180, max: 180, step: 1, value: 0, format: (v) => `${Math.round(v)}°`,
- onInput: (v) => { state.view.azimuth = v; app.actions.refresh('view'); },
- }),
- polar: slider({
- label: '高さ', min: 1, max: 179, step: 1, value: 76, format: (v) => `${Math.round(v)}°`,
- onInput: (v) => { state.view.polar = v; app.actions.refresh('view'); },
- }),
- size: slider({
- label: '大きさ', min: 2, max: 200, step: 0.1, value: 12, format: (v) => v.toFixed(1),
- onInput: (v) => {
- if (state.view.projection === 'ortho') state.view.orthoHeight = v;
- else state.view.distance = v;
- app.actions.refresh('view');
- },
- }),
- targetY: slider({
- label: '見る高さ', min: 0, max: Math.max(2, model.size.y * 1.2), step: 0.05, value: model.size.y * 0.5, format: (v) => v.toFixed(2),
- onInput: (v) => { state.view.targetY = v; app.actions.refresh('view'); },
- }),
- };
- for (const widget of Object.values(cameraSliders)) cameraSection.add(widget.el);
-
- const autoRotateToggle = check({
- label: '自動で回す', value: false,
- onChange: (v) => { state.view.autoRotate = v; app.actions.refresh('view'); },
- });
- cameraSection.add(controlRow(null, autoRotateToggle.el, { wide: true }));
- cameraSection.add(slider({
- label: '回す速さ', min: 0.2, max: 4, step: 0.1, value: 0.8, format: (v) => v.toFixed(1),
- onInput: (v) => { state.view.autoRotateSpeed = v; app.actions.refresh('view'); },
- }));
-
- /* ------------------------------------------------------------------ export */
-
- const exportSection = section(tabExport, '画像');
-
- exportSection.add(segmented({
- label: 'PNGの大きさ',
- options: [
- { value: '1', label: '1倍' },
- { value: '2', label: '2倍' },
- { value: '3', label: '3倍' },
- { value: '4', label: '4倍' },
- ],
- value: '2',
- onChange: (value) => { state.render.pngScale = Number(value); },
- }));
- exportSection.add(buttons({
- items: [
- { id: 'png', label: 'PNGを保存', primary: true, onClick: () => app.actions.savePNG() },
- { id: 'copy', label: 'クリップボードにコピー', onClick: () => app.actions.copyPNG() },
- ],
- }));
- const transparentCheck = check({
- label: 'PNGの背景を透明にする', value: false,
- onChange: (v) => { state.render.pngTransparent = v; },
- });
- exportSection.add(transparentCheck.el);
- exportSection.add(hint('線画のときに透明にすると、体の白ぬりも透明になって線だけが残ります(他の絵に重ねられます)。ふつう・フラットでは体の色はそのまま残ります。'));
-
- // まんが(コマ)gets its own section: it is a different kind of output.
- const comicSection = section(tabExport, 'まんが');
- const storyScaleSegment = segmented({
- label: '書き出しの大きさ',
- options: [
- { value: '1', label: '1×(軽い)' },
- { value: '1.5', label: '1.5×' },
- { value: '2', label: '2×(きれい)' },
- ],
- value: '1.5',
- hint: 'コマ1枚の画素数は「いまの画面の大きさ × この倍率」です。'
- + '2× だと台紙のPNGがとても大きくなります(重いときは 1× か 1.5×)',
- onChange: (value) => { state.story.scale = Number(value); },
- });
- comicSection.add(storyScaleSegment.el);
- const storyColumnsSlider = slider({
- label: '列数', min: 1, max: 4, step: 1, value: 1, format: (v) => `${Math.round(v)}列`,
- onInput: (v) => { state.story.columns = Math.round(v); },
- });
- const storyGapSlider = slider({
- label: '間隔', min: 0, max: 40, step: 1, value: 12, format: (v) => `${Math.round(v)}px`,
- onInput: (v) => { state.story.gap = v; },
- });
- const storyBackgroundField = colorField({
- label: '台紙の色', value: '#ffffff',
- onChange: (v) => { state.story.sheetBackground = v; },
- });
- comicSection.add(storyColumnsSlider.el);
- comicSection.add(storyGapSlider.el);
- comicSection.add(storyBackgroundField.el);
- comicSection.add(buttons({
- items: [
- { id: 'addPanel', label: '今の状態をコマに追加', onClick: () => { app.actions.addStoryPanel(); sync(); } },
- { id: 'saveStory', label: 'まんがを書き出す', primary: true, onClick: () => app.actions.saveStory({ scale: state.story?.scale ?? 1.5 }) },
- ],
- }));
- comicSection.add(hint('2列以上で3コマ以上のときは、読む順にコーナーへ番号を付けます。'));
- const storyPanelBox = h('div', { class: 'story-panels', style: { display: 'grid', gap: '6px' } });
- comicSection.add(controlRow(null, storyPanelBox, { wide: true }));
- let storyShape = '';
-
- /** Bring back everything a panel saved: pose, face, 擬音 and the camera. */
- const recallStoryPanel = (index) => {
- const panel = state.story?.panels?.[index];
- if (!panel) return;
- // Replace the bones outright: a merged pose would keep bones the panel never had.
- state.pose = { bones: {}, root: [0, 0, 0] };
- app.actions.applyState({
- pose: panel.pose ?? {},
- face: panel.face ?? {},
- // The 擬音 on the shot. A panel saved before this existed clears them, so
- // recalling it shows what it actually recorded rather than a leftover.
- gion: panel.gion ?? { items: [] },
- // The camera it was recorded with, so the view comes back too. A panel
- // saved before this existed simply keeps the current camera.
- ...(panel.view ? { view: panel.view } : {}),
- }, { scope: 'all', sync: true });
- };
-
- const removeStoryPanel = (index) => {
- state.story = state.story ?? {};
- state.story.panels = (state.story.panels ?? []).filter((_, i) => i !== index);
- // The empty patch is only here to route the edit through the app's own apply
- // path: that is what records an undo step and re-reads the panel.
- app.actions.applyState({}, { scope: 'all', sync: true });
- };
-
- /**
- * Same rebuild policy as the prop list: only when the set of panels changes.
- *
- * A preview arrives a moment *after* its panel does (it needs a render), so its
- * version is part of the identity: when one turns up, the row is drawn again
- * with it.
- */
- const renderStoryPanels = () => {
- const panels = state.story?.panels ?? [];
- const thumbs = app.actions.storyThumbs?.() ?? null;
- const shape = `${panels.length}:${panels
- .map((panel) => `${panel.id ?? ''}:${thumbs?.get?.(panel.id)?.version ?? 0}`)
- .join('|')}`;
- if (shape === storyShape) return;
- storyShape = shape;
- storyPanelBox.replaceChildren();
- panels.forEach((panel, index) => {
- const thumb = thumbs?.get?.(panel.id)?.url;
- const preview = thumb
- ? h('img', {
- class: 'story-thumb',
- alt: panel.label ?? `コマ${index + 1}`,
- src: thumb,
- title: 'クリックでこのコマを呼び出す',
- style: {
- width: '100%', height: '76px', objectFit: 'contain', cursor: 'pointer',
- background: '#f3f0fa', border: '1px solid #e4dff0', borderRadius: '8px',
- },
- onClick: () => recallStoryPanel(index),
- })
- : h('div', {
- text: '(プレビューなし)',
- style: {
- padding: '6px 8px', borderRadius: '8px', background: '#f7f5fd',
- color: '#8a80a0', fontSize: '11px',
- },
- });
- const block = h('div', {
- class: 'story-panel',
- style: { borderTop: '1px dashed #e4dff0', paddingTop: '6px', display: 'grid', gap: '4px' },
- }, subhead(panel.label ?? `コマ${index + 1}`), preview);
- block.append(buttons({
- items: [
- { id: 'recall', label: 'このコマを呼び出す', onClick: () => recallStoryPanel(index) },
- { id: 'remove', label: '削除', onClick: () => removeStoryPanel(index) },
- ],
- }).el);
- storyPanelBox.append(block);
- });
- };
- renderStoryPanels();
-
- // The share section comes after まんが, so the reading order is 画像 → まんが →
- // 共有 (the order the outputs are usually wanted in).
- /* ------------------------------------------------------------ extra export */
-
- const extraExportSection = section(tabExport, '共有');
- extraExportSection.add(buttons({
- items: [
- { id: 'share-image', label: '画像をシェア', primary: true, onClick: () => app.actions.shareImage() },
- { id: 'share-x', label: 'Xで投稿', onClick: () => app.actions.shareImage('x') },
- { id: 'share-fb', label: 'Facebookでシェア', onClick: () => app.actions.shareImage('facebook') },
- { id: 'share-link', label: 'この見た目のリンクをコピー', onClick: () => app.actions.copyShareLink() },
- ],
- }));
- extraExportSection.add(hint('いまの画面を画像にして「#ぶるべースタジオ」を付けて投稿します。'
- + 'スマートフォンでは端末の共有画面が開きます。パソコンでは画像をコピーして投稿画面を開くので、貼り付けて投稿してください(Ctrl+V)。'));
-
- // 手描きの顔パーツを描くための下地。画面を切り取る他の書き出しと違って、これは
- // 「スタジオが読み込む大きさ・位置そのまま」の素材なので、別のセクションにして
- // 共有のあとに置く(素材を作る → 見せる、の順)。
- const faceMapSection = section(tabExport, 'テクスチャの下地を書き出す');
- faceMapSection.add(buttons({
- items: [
- { id: 'facemap-eyes', label: '目の下地を書き出す', onClick: () => app.actions.saveFaceMap('eyes') },
- { id: 'facemap-mouth', label: '口の下地を書き出す', onClick: () => app.actions.saveFaceMap('mouth') },
- ],
- }));
- faceMapSection.add(hint('書き出したPNGは、スタジオが読み込む大きさ・位置そのものです。'
- + 'そのまま絵を描いて「目の画像を読み込む(PNG)」や「口の画像を読み込む(PNG)」で読み込むと、'
- + '描いた場所にそのまま入ります。青い線は位置合わせの目安です。使う前に消すか、上から塗ってください。'));
-
- /* --------------------------------------------------------------- settings */
-
- const settingsSection = section(tabExport, '設定');
-
- settingsSection.add(buttons({
- items: [
- { id: 'save', label: '設定を保存(JSON)', onClick: () => app.actions.saveSettings() },
- { id: 'load', label: '設定を読み込む', onClick: () => settingsPicker.click() },
- ],
- }));
-
- const settingsPicker = h('input', { type: 'file', accept: '.json,application/json', style: { display: 'none' } });
- settingsPicker.addEventListener('change', async () => {
- const file = settingsPicker.files?.[0];
- settingsPicker.value = '';
- if (!file) return;
- try {
- const text = await file.text();
- app.actions.loadSettings(text);
- } catch (error) {
- console.error(error);
- toast('設定ファイルを読み込めませんでした');
- }
- });
- settingsSection.add(settingsPicker);
-
- /* -------------------------------------------------------------------- sync */
-
- // A tiny read-out of what the renderer is actually doing: if the picture ever
- // looks wrong or never appears, this says why (buffer size, quality, fps).
- const infoSection = section(tabScene, '画面の情報');
- const statsLine = h('p', { class: 'hint', text: '計測中…' });
- infoSection.add(statsLine);
- infoSection.add(hint('動きが重い環境では、負荷を下げるために画質(描画バッファの倍率)を自動で下げます。'));
-
- // シーン: the scene controls are built wherever they belong in the code, so put
- // them on the tab in the order the user meets them: what is behind the
- // character, what is beside it, then the light and the read-out.
- tabScene.append(
- backdropSection.el, propSection.el, gionSection.el, wallSection.el,
- screenLightSection.el, infoSection.el,
- );
-
- boneSyncers.push(() => {
- const name = rig.selected;
- for (const [key, button] of boneButtons) button.classList.toggle('active', key === name);
- const delta = name ? rig.getDelta(name) : { x: 0, y: 0, z: 0 };
- axisSliders.x.set(delta.x);
- axisSliders.y.set(delta.y);
- axisSliders.z.set(delta.z);
- const root = state.pose.root ?? [0, 0, 0];
- rootSliders.forEach((widget, index) => widget.set(root[index] ?? 0));
- });
-
- syncers.push(...boneSyncers);
-
- syncers.push(() => {
- const linked = state.face.eyes.linked !== false;
- bothControls.group.setVisible(linked);
- leftControls.group.setVisible(!linked);
- rightControls.group.setVisible(!linked);
- bothControls.sync();
- leftControls.sync();
- rightControls.sync();
-
- for (const key of ['left', 'right']) {
- const eye = state.face.eyes[key];
- const w = eyeAdvancedWidgets[key];
- w.eyeX.set(eye.eyeX ?? 0);
- }
-
- glassesToggle.set(state.face.eyes.glasses.enabled === true);
- glassesKindSegment.set(state.face.eyes.glasses.kind ?? 'glasses');
- for (const [key, widget] of Object.entries(glassesSliders)) widget.set(state.face.eyes.glasses[key]);
- glassesFrameColor.set(state.face.eyes.glasses.frameColor);
- glassesLensColor.set(state.face.eyes.glasses.lensColor);
- eyeColors.white.set(state.face.eyes.white);
- eyeColors.iris.set(state.face.eyes.iris);
- eyeColors.line.set(state.face.eyes.line);
- });
-
- syncers.push(() => {
- snotToggle.set(state.face.eyes.snot.enabled === true);
- snotSize.set(state.face.eyes.snot.size);
- for (const [key, widget] of Object.entries(snotSliders)) widget.set(state.face.eyes.snot[key]);
- snotColor.set(state.face.eyes.snot.color);
- });
-
- syncers.push(() => {
- mouthToggle.set(state.face.mouth.visible);
- for (const [key, widget] of Object.entries(mouthSliders)) widget.set(state.face.mouth[key]);
- for (const [key, widget] of Object.entries(mouthColors)) widget.set(state.face.mouth[key]);
- const mouthShape = mouthModeOf(state.face.mouth);
- mouthMode.set(mouthShape);
- const shown = new Set(mouthRowKeys[mouthShape]);
- for (const key of mouthRowOrder) mouthSliders[key].el.style.display = shown.has(key) ? '' : 'none';
- });
-
- syncers.push(() => {
- motionSegment.set(state.anim.mode ?? 'off');
- animWidgets.blink.set(state.anim.blink);
- animWidgets.lookAround.set(state.anim.lookAround);
- });
-
- syncers.push(() => {
- styleSegment.set(state.render.style);
- outlineToggle.set(state.render.outline);
- outlinePixelsSlider.set(state.render.outlinePixels ?? 2);
- leafBodyLineCheck.set(state.render.leafBodyLine !== false);
- transparentCheck.set(state.render.pngTransparent);
- lightPad.set({
- x: state.render.lightAzimuth / 180,
- y: Math.max(0, (state.render.lightElevation - 5) / 80),
- });
- lightIntensitySlider.set(state.render.lightIntensity ?? 2.1);
- ambientSlider.set(state.render.ambient ?? 0.9);
- lightColorField.set(state.render.lightColor ?? '#ffffff');
- ambientColorField.set(state.render.ambientColor ?? '#ffffff');
- exposureSlider.set(state.render.exposure ?? 1);
- const line = ['lineart', 'outline'].includes(state.render.style);
- outlineToggle.el.querySelector('input').disabled = line;
- });
-
- syncers.push(() => {
- projectionSegment.set(state.view.projection);
- cameraSliders.azimuth.set(state.view.azimuth);
- cameraSliders.polar.set(state.view.polar);
- const ortho = state.view.projection === 'ortho';
- cameraSliders.size.set(ortho ? (state.view.orthoHeight || 7.5) : (state.view.distance || 12));
- cameraSliders.targetY.set(state.view.targetY || model.size.y * 0.5);
- autoRotateToggle.set(state.view.autoRotate);
- });
-
- syncers.push(() => {
- lookAtToggle.set(state.lookAt?.enabled === true);
- for (const [key, widget] of Object.entries(lookAtSliders)) {
- widget.set(state.lookAt?.[key] ?? LOOK_AT_FALLBACKS[key]);
- }
- turnBodyToggle.set(state.lookAt?.turnBody === true);
- });
-
- syncers.push(() => {
- envIntensitySlider.set(state.render?.envIntensity ?? 1);
- });
-
- syncers.push(() => {
- mirrorToggle.set(state.render?.mirror === true);
- shadowCheck.set(state.render?.shadow !== false);
- shadowOpacitySlider.set(state.render?.shadowOpacity ?? 0.22);
- shadowSoftnessSlider.set(state.render?.shadowSoftness ?? 1.6);
- contactShadowToggle.set(state.render?.contactShadow === true);
- contactBlobToggle.set(state.render?.contactBlob === true);
- environmentSegment.set(state.render?.environment ?? 'gradient');
- });
-
- syncers.push(() => {
- const background = state.view?.background ?? 'solid';
- // 素材 is active for either half of it, and its rows show only then.
- const isMaterial = background === 'preset' || background === 'effect';
- backgroundModeSegment.set(isMaterial ? 'material' : background);
- materialGroup.style.display = isMaterial ? '' : 'none';
- backgroundColour.set(state.view?.backgroundColor ?? '#ffffff');
- backgroundFitSegment.set(state.view?.backgroundFit ?? 'cover');
- backgroundBlurSlider.set(state.view?.backgroundBlur ?? 0);
- backgroundDarkenSlider.set(state.view?.backgroundDarken ?? 0);
- backgroundScaleSlider.set(state.view?.backgroundScale ?? 1);
- cameraFacingSegment.set(state.view?.cameraFacing ?? 'environment');
- cameraMirrorToggle.set(state.view?.cameraMirror === true);
- gyroToggle.set(state.view?.gyro === true);
- });
-
- syncers.push(() => {
- renderProps();
- const items = state.props?.items ?? [];
- // A rebuild only carries the whole-prop scale, so re-apply each sign's face
- // size to the freshly built mesh here (see `placedPropGroups`).
- const groups = placedPropGroups(app);
- for (const entry of propWidgets) {
- const item = items[entry.index] ?? {};
- entry.sliders.x.set(item.x ?? 0);
- entry.sliders.y.set(item.y ?? 0);
- entry.sliders.z.set(item.z ?? 0);
- entry.sliders.rotX.set(item.rotX ?? 0);
- entry.sliders.rotY.set(item.rotY ?? 0);
- entry.sliders.rotZ.set(item.rotZ ?? 0);
- entry.sliders.scale.set(item.scale ?? 1);
- entry.faceScale?.set(item.faceScale ?? 1);
- applyPropFaceScale(groups[entry.index], item.faceScale);
- entry.signText?.set(item.text ?? '');
- }
- });
-
- syncers.push(() => {
- renderGion();
- const items = state.gion?.items ?? [];
- for (const entry of gionWidgets) {
- const item = items.find((candidate) => candidate.id === entry.id);
- if (!item) continue;
- entry.sliders.w.set(item.w ?? DEFAULT_STAMP_WIDTH);
- entry.sliders.rot.set(item.rot ?? 0);
- entry.flip.set(item.flip === true);
- entry.block.classList.toggle('active', item.id === app.gionSelected);
- }
- });
-
- syncers.push(() => {
- for (const group of wallGroups) {
- const wall = state.render?.[group.key] ?? defaultState().render[group.key];
- group.toggle.set(wall.on === true);
- for (const [key, widget] of Object.entries(group.sliders)) widget.set(wall[key] ?? (key === 'size' ? 1 : 0));
- group.guide.set(wall.guide === true);
- }
- });
-
- syncers.push(() => {
- storyScaleSegment.set(String(state.story?.scale ?? 1.5));
- storyColumnsSlider.set(state.story?.columns ?? 1);
- storyGapSlider.set(state.story?.gap ?? 12);
- storyBackgroundField.set(state.story?.sheetBackground ?? '#ffffff');
- renderStoryPanels();
- });
-
- app.actions.applyFacePreset = applyFacePreset;
- app.actions.applyPosePreset = applyPosePreset;
-
- sync();
-
- // The 'g' key toggles the gizmo behind the panel's back, so the checkbox
- // follows the rig rather than the other way round.
- syncers.push(() => gizmoToggle.set(rig.helper.visible === true));
-
- return {
- sync,
- onRigChanged: syncBones,
- /** Which 擬音 stamp the viewport should outline (a drag on it calls this). */
- selectGion(id) {
- app.gionSelected = id ?? null;
- sync();
- },
- /** Open the crop picker for a sheet (the 擬音 section's button calls this). */
- openGionPicker(sheet) {
- openGionPicker(sheet ?? gionSheetSelect.get());
- },
- setStats(text) { statsLine.textContent = text; },
- setRecording(recording) {
- recordButton.setLabel('record', recording ? '録画を停止' : '録画を開始');
- },
- };
-}
diff --git a/bluebey-studio/src/presets.js b/bluebey-studio/src/presets.js
deleted file mode 100644
index a31c952..0000000
--- a/bluebey-studio/src/presets.js
+++ /dev/null
@@ -1,668 +0,0 @@
-/**
- * The complete, serialisable studio state plus the preset libraries.
- *
- * Everything here is plain JSON: "設定を保存" writes exactly this object, and
- * "設定を読み込む" merges it back in, so a look can be archived and shared.
- *
- * Eye parameters are named after the character's own left/right, NOT the
- * viewer's: `left` is the eye on the model's +x side, which is the half of the
- * eye texture with u > 0.5. In a front view that eye appears on the right of
- * the screen.
- */
-
-/**
- * 見えない壁 (the invisible wall) settings. The studio offers two, so a corner (or
- * two opposite cuts) can be made. A wall's position is kept *relative to the
- * character*, so moving ぶるべー carries its walls along (see src/clip.js).
- */
-function wallDefaults() {
- return {
- on: false,
- x: 0, y: 0, z: 0, // a point the plane passes through, relative to the character
- yaw: 0, // turn about Y, degrees
- tilt: 0, // lean about X, degrees
- // The guide is the only part of the wall that is ever *drawn* (the wall
- // itself writes depth but no colour, so it hides what is behind it
- // without being visible). `guide` shows that rectangle while placing,
- // which is also when it can be dragged in the viewport.
- guide: false,
- size: 1, // the wall's size (a real width and height, not a clip)
- };
-}
-
-export function defaultState() {
- return {
- version: 1,
- view: {
- projection: 'persp', // persp | ortho
- fov: 30,
- azimuth: 0, // degrees, 0 = looking at the face
- polar: 76, // degrees from +y, 90 = level with the target
- distance: 0, // 0 = fit to the model automatically
- orthoHeight: 0, // 0 = fit to the model automatically
- targetY: 0, // 0 = centre of the model
- // The mouse orbit and pan write their own target back here (see
- // ViewRig.captureInto), so refreshing the view - or sharing a link - keeps
- // the camera the user actually framed.
- targetX: 0,
- targetZ: 0,
- autoRotate: false,
- autoRotateSpeed: 0.8,
- // solid | transparent | preset | image | effect | camera
- // preset = one of the CC0 backdrops in assets/backgrounds
- // image = something the user loaded (data URL)
- // effect = a procedurally drawn manga effect line (no file at all)
- // camera = the phone's camera, for the AR mode
- background: 'solid',
- backgroundColor: '#ffffff',
- backgroundPreset: 'autumn_park',
- backgroundEffect: 'focus', // focus | fall | speed | ellipse
- backgroundImage: null, // data URL, set by 「画像を読み込む」
- backgroundFit: 'cover', // cover | contain | stretch | tile
- backgroundBlur: 0, // px
- backgroundDarken: 0, // 0..1 dark veil, helps the character read
- backgroundScale: 1,
- backgroundOffset: { x: 0, y: 0 },
- // AR: which camera, and whether to mirror (front camera feels mirrored)
- cameraFacing: 'environment', // environment | user
- cameraMirror: false,
- gyro: false, // look around by turning the phone
- },
- render: {
- style: 'real', // real | flat | lineart | outline
- outline: true,
- outlineWidth: 0.022,
- outlineColor: '#2a1e33',
- paper: '#ffffff',
- shadow: true,
- // The soft round blob at the character's feet. Off by default: it sat at the
- // origin and stayed behind when the character was moved. See look.js.
- contactBlob: false,
- pngScale: 2,
- pngTransparent: false,
- svgWidth: 2048,
- lightAzimuth: -38,
- lightElevation: 40,
- lightIntensity: 2.1,
- ambient: 0.9,
- // The colour of the main light and of the ambient (sky) light.
- lightColor: '#ffffff',
- ambientColor: '#ffffff',
- exposure: 1,
- // The fixed body palette. ぶるべー keeps its colours, so there is no theme
- // picker and no per-part editor; the parts live here so the styles and the
- // model have one place to read them from.
- colors: {
- body: '#c8b0f0',
- accent: '#8a4fe0',
- nose: '#7a4fb0',
- leaf: '#a6dd6a',
- vein: '#7fbf3f',
- feet: '#7a4fb0',
- },
- // Mirror the whole character, for laying out a panel that faces its text.
- mirror: false,
- // Shadows: the soft blob under the feet, plus the cast shadow.
- shadowOpacity: 0.22,
- shadowSoftness: 1.6,
- shadowOffset: 0,
- contactShadow: false, // blob only, no cast shadow
- // Lighting environment. 'room' is three's RoomEnvironment; 'gradient' is
- // the tiny procedural sky the studio always had.
- environment: 'gradient', // gradient | room | none
- envIntensity: 1,
- // Hand-drawn line work (SVG export and the outline pass).
- handDrawn: 0, // 0 = off, 1 = a lot
- handDrawnSeed: 7,
- handDrawnScale: 40,
- handDrawnPasses: 1,
- // How the outline is drawn - the two methods take the parts they are good
- // at. See MODEL-GUIDE.md §5 and src/outline.js.
- //
- // 'screen' (the default) hands the leaves to the screen-space edge pass,
- // because a hull cannot outline a shell that thin: the expanded
- // front and back cross inside it and the line breaks up. Every
- // other part keeps its hull, whose line is computed from the
- // geometry and so comes out smooth, where the pixel grid would
- // make it stepped.
- // 'hull' the inverted-hull copy everywhere - cheap, and the way it was
- // done before, but the leaves then come out as a tangle.
- outlineMethod: 'screen',
- outlinePixels: 2.4, // 'screen' only: line width in screen pixels
- // Whether a leaf gets a line where it meets the *body*. There is no right
- // answer here, so it is a setting:
- // on - the body counts as paper, so the leaves are outlined where they
- // emerge from it. Without this a line drawing cannot tell the
- // leaves and the body apart at all (both are paper).
- // off - the body blocks the line, so the leaf simply passes behind it and
- // nothing is drawn along the meeting. Cleaner, but the leaves and
- // the body merge into one white shape.
- leafBodyLine: true,
- // 見えない壁 (the invisible wall). Up to two placeable rectangles that hide
- // whatever is behind them, so the character can be buried in a wall and
- // only the rest of the body shows. See src/clip.js.
- wall: wallDefaults(),
- wall2: wallDefaults(),
- },
- pose: {
- bones: {}, // { boneName: [x, y, z] } in degrees, rest = 0,0,0
- root: [0, 0, 0], // whole-body offset in model units
- },
- anim: {
- mode: 'off', // off | idle | walk
- blink: true,
- blinkInterval: 3.4, // seconds between blinks
- lookAround: false,
- speed: 1,
- },
- face: {
- // 帽子 (a hat on the head). `kind` is one of HAT_LIBRARY in src/hats.js.
- // `x`/`z` slide it sideways / forwards, `height` lifts it off the head;
- // `tiltX` leans it forward/back and `tiltZ` leans it sideways.
- hat: { kind: 'none', custom: false, x: 0, z: 0, height: -0.7, tiltX: 0, tiltZ: 0 },
- eyes: {
- source: 'parametric', // parametric | opened | closed | ... (hand-drawn)
- linked: true, // move both eyes together
- left: { shape: 'open', open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0.3, tearOn: false, tearY: 0, tearX: 0, tearTilt: 0, white: null, highlight: null, shapeScale: 1, lowerLid: null, lidShape: null, lidWidth: null, lidTilt: null, lashes: null, lashAngle: null, lashPos: null },
- right: { shape: 'open', open: 1, lookX: 0, lookY: 0, eyeX: 0, closed: 'line', closedLines: 1, irisShape: 'circle', threeFlip: false, tear: 0.3, tearOn: false, tearY: 0, tearX: 0, tearTilt: 0, white: null, highlight: null, shapeScale: 1, lowerLid: null, lidShape: null, lidWidth: null, lidTilt: null, lashes: null, lashAngle: null, lashPos: null },
- irisScale: 1,
- lookMax: 1.2,
- highlight: true,
- lidWidth: 14,
- lowerLid: 0,
- lashes: false,
- lashAngle: 0,
- lashPos: 0,
- // How the closing eyelid is drawn: `curve` is the soft, rounded lid; `flat`
- // is a straight lid edge (the hard, narrowed look). `lidTilt` rotates the
- // narrowed eye. See faceArt.js.
- lidShape: 'curve',
- lidTilt: 0,
- heartScale: 2,
- heartColor: '#e0344f',
- // The tears live on each eye (see `eyes.left.tear`), so one eye can cry
- // on its own; only the colour is shared.
- tearColor: '#8fd8ff',
- // 鼻ちょうちん: the snot bubble someone sleeps with. Drawn into the same
- // plate as the eyes, out to the side of the nose. Starts off.
- snot: {
- enabled: false,
- size: 1.23,
- offsetX: -23,
- offsetY: 0,
- offsetZ: 16,
- color: '#e3ecff',
- },
- // 眼鏡 / サングラス are drawn into the same texture as the eyes, so they
- // travel with the head instead of being a separate 3D mesh. Off by
- // default. `kind` only picks which lens defaults the panel offers; the
- // drawing itself reads the colours and opacities below.
- glasses: {
- enabled: false,
- kind: 'glasses', // glasses | sunglasses
- frameColor: '#2a1e33',
- frameWidth: 1, // multiplier on the frame stroke (EYE_LAYOUT.lidStroke)
- lensColor: '#2b2433',
- lensOpacity: 0.22, // 0..1; 眼鏡 is nearly clear, サングラス dark
- lensGap: 0, // px, + moves the two lenses further apart
- scale: 1, // lens size multiplier
- offsetY: 0, // px, artwork units (positive = down)
- tilt: 0, // degrees, both lenses turn together
- },
- white: '#ffffff',
- iris: '#150e1b',
- line: '#55386e',
- },
- mouth: {
- source: 'parametric', // parametric | original
- visible: true,
- smile: 1, // 0 = straight line, 1 = the original smile, <0 = frown
- width: 1, // the smile line's length (the chord); also the O's width
- thickness: 1,
- open: 0, // 0 = closed lips, 1 = wide open
- tilt: 0, // degrees
- offsetY: 0, // texture pixels
- tongue: 0.96, // 0 hides it; scales the whole tongue
- tonguePos: 0.84, // 0..1 along the smile line
- corners: 0.71, // length of the corner marks, 0 hides them
- cornerAngle: -8, // degrees, tilts the corner marks
- round: 0, // >0 turns the mouth into a round "O"
- color: '#ff1a44',
- innerColor: '#4a0f1e',
- tongueColor: '#ff2d2d',
- cornerColor: '#725497',
- line: '#55386e',
- },
- },
- // ----------------------------------------------------------------- look at
- // Where the character should look (world units).
- lookAt: {
- enabled: false,
- x: 0,
- y: 2.6,
- z: 3,
- turnBody: false, // also rotate the whole body a little
- amount: 1,
- // The turn and the gaze are *remembered* rather than derived, so switching
- // the mode (or just 体も向ける) off leaves the character where it is instead
- // of snapping it back to the front. See `applyLookAt` in main.js.
- bodyYawDeg: 0,
- freeze: null,
- },
- // ------------------------------------------------------------------- props
- // 小物. Each entry is a procedural prop from src/props.js.
- props: {
- items: [],
- },
- // -------------------------------------------------------------------- 擬音
- // マンガのオノマトペ (擬音) as stickers over the picture. Each item is a crop
- // of a sheet - `{ id, sheet, sx, sy, sw, sh, x, y, w, rot, flip }` - and only
- // the sheet *name* is stored, never the bitmap, so a shared link stays small
- // (see src/gion.js for the geometry).
- gion: {
- items: [],
- },
- // ------------------------------------------------------------------- story
- // 4コマ / 紙芝居. Each panel is a small state patch (pose, face).
- story: {
- columns: 1,
- gap: 12,
- padding: 20,
- sheetBackground: '#ffffff',
- // How big each captured panel is, as a multiple of the viewport. 2x doubles
- // each side, so the sheet PNG gets four times the pixels per panel.
- scale: 1.5,
- panels: [],
- },
- };
-}
-
-/** Expression presets. Each patches a copy of the default state. */
-export const FACE_PRESETS = [
- {
- id: 'normal',
- label: 'ふつう',
- face: {
- eyes: {
- left: { open: 1, lookX: 0, lookY: 0, closed: 'line' },
- right: { open: 1, lookX: 0, lookY: 0, closed: 'line' },
- irisScale: 1,
- highlight: true,
- },
- // Depth 1 = the hand-drawn original; the corners and tongue below are also
- // the settings that reproduce it (measured against the artwork).
- mouth: { smile: 1, width: 1, thickness: 1, open: 0, tilt: 0, offsetY: 0, tongue: 0.96, tonguePos: 0.84, corners: 0.71, cornerAngle: -8 },
- },
- },
- {
- id: 'laugh',
- label: 'にっこり',
- face: {
- eyes: {
- left: { shape: 'arch', open: 1, lookX: 0, lookY: 0, closed: 'arch' },
- right: { shape: 'arch', open: 1, lookX: 0, lookY: 0, closed: 'arch' },
- highlight: false,
- },
- // A laugh is a wide smile with squeezed-shut eyes. An *open* mouth on a
- // round mascot reads as a shout, so only びっくり keeps the round O.
- mouth: { smile: 1.15, width: 1.14, thickness: 1.05, open: 0, tilt: 0, offsetY: 0, round: 0, tongue: 0.96, corners: 1.25 },
- },
- },
- {
- id: 'eating',
- label: 'たべている',
- face: {
- eyes: {
- left: { open: 1, lookX: 0, lookY: 0.15, closed: 'line' },
- right: { open: 1, lookX: 0, lookY: 0.15, closed: 'line' },
- irisScale: 1,
- highlight: true,
- },
- // A small, cute round mouth (the other round mouth, with びっくり).
- mouth: { smile: 0.2, width: 0.8, thickness: 1.1, open: 0, tilt: 0, offsetY: 6, round: 0.58, tongue: 0.7, corners: 0 },
- },
- },
- {
- id: 'wink',
- label: 'ウインク',
- face: {
- eyes: {
- // The shut eye is the original `eyes-close-tight` artwork: three strokes
- // sharing one vertex, pointing at the nose.
- left: { shape: 'line3', open: 1, lookX: 0, lookY: 0, closed: 'line', closedLines: 3 },
- right: { open: 1, lookX: 0, lookY: 0, closed: 'line', closedLines: 1 },
- irisScale: 1,
- highlight: true,
- },
- mouth: { smile: 1.0, width: 1.02, thickness: 1, open: 0, tilt: 0, offsetY: 0, tongue: 0, corners: 1 },
- },
- },
- {
- id: 'surprised',
- label: 'びっくり',
- face: {
- eyes: {
- left: { open: 1, lookX: 0, lookY: 0, closed: 'line' },
- right: { open: 1, lookX: 0, lookY: 0, closed: 'line' },
- irisScale: 0.74,
- lookMax: 0.35,
- highlight: true,
- },
- mouth: { smile: 0.2, width: 0.85, thickness: 1.1, open: 0, tilt: 0, offsetY: 0, round: 0.62, tongue: 0.25, corners: 0 },
- },
- },
- {
- id: 'sleepy',
- label: 'ねぼけ',
- face: {
- eyes: {
- left: { open: 0.42, lookX: 0, lookY: -0.22, closed: 'line' },
- right: { open: 0.42, lookX: 0, lookY: -0.22, closed: 'line' },
- irisScale: 1,
- highlight: true,
- // A sleeper's snot bubble, with the defaults from the panel.
- snot: { enabled: true },
- },
- mouth: { smile: 0.62, width: 0.88, thickness: 1, open: 0, tilt: -3, offsetY: 6, tongue: 0, corners: 0.8 },
- },
- },
- {
- id: 'shy',
- label: 'てれ',
- face: {
- eyes: {
- // Blushing, eyes half shut and looking away.
- left: { open: 0.72, lookX: -0.4, lookY: 0.08, closed: 'line' },
- right: { open: 0.72, lookX: -0.4, lookY: 0.08, closed: 'line' },
- irisScale: 1,
- highlight: true,
- },
- mouth: { smile: 0.5, width: 0.82, thickness: 1.05, open: 0, tilt: 0, offsetY: 0, tongue: 0, corners: 0.5 },
- },
- },
- {
- id: 'sidelong',
- label: 'じと目',
- face: {
- eyes: {
- // Flat, narrowed lids tilted inwards - a sideways, unimpressed look.
- // 上まぶたの高さ is now independent of the lower lid, so `open` carries the
- // whole slit (it used to be measured from the raised lower lid).
- left: { open: 0.74, lookX: 0.4, lookY: -0.12, closed: 'line' },
- right: { open: 0.74, lookX: 0.4, lookY: -0.12, closed: 'line' },
- lowerLid: 0.42,
- lidShape: 'flat',
- lidTilt: 12,
- irisScale: 0.92,
- highlight: true,
- },
- mouth: { smile: 0.12, width: 0.78, thickness: 1.1, open: 0, tilt: 0, offsetY: 0, tongue: 0, corners: 0 },
- },
- },
- {
- id: 'love',
- label: 'すき',
- face: {
- eyes: {
- left: { shape: 'heart', open: 1, lookX: -0.1, lookY: -0.3, closed: 'line', irisShape: 'heart', white: false },
- right: { shape: 'heart', open: 1, lookX: 0.1, lookY: -0.3, closed: 'line', irisShape: 'heart', white: false },
- irisScale: 1,
- // The white behind a heart would poke out around its lobes, so hide it.
- heartColor: '#e0344f',
- highlight: false,
- },
- mouth: { smile: 1.15, width: 1, thickness: 1.05, open: 0.16, tilt: 4, offsetY: 0, tongue: 0.9, corners: 1.3 },
- },
- },
- {
- id: 'grumpy',
- label: 'むっと',
- face: {
- eyes: {
- left: { open: 0.6, lookX: 0, lookY: -0.12, closed: 'line', closedLines: 2 },
- right: { open: 0.6, lookX: 0, lookY: -0.12, closed: 'line', closedLines: 2 },
- irisScale: 0.86,
- highlight: true,
- },
- mouth: { smile: -0.62, width: 0.84, thickness: 1.15, open: 0, tilt: 0, offsetY: 0, tongue: 0, corners: 0 },
- },
- },
- {
- id: 'cry',
- label: 'ないてる',
- face: {
- eyes: {
- // Big tears that hang low and lean outwards - the classic "crying hard"
- // look. The sideways position and the tilt are mirrored between the eyes,
- // so the pair stays symmetric.
- left: { open: 0.84, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearOn: true, tearY: 26, tearTilt: -20 },
- right: { open: 0.84, lookX: 0, lookY: -0.3, closed: 'line', tear: 1.35, tearOn: true, tearY: 26, tearTilt: 20 },
- irisScale: 1.02,
- lowerLid: 0.42,
- highlight: true,
- },
- mouth: { smile: -0.5, width: 0.72, thickness: 1.25, open: 0, tilt: 0, offsetY: 0, tongue: 0, corners: 0 },
- },
- },
-];
-
-/**
- * Poses. `bones` holds per-bone XYZ degrees relative to the rest pose.
- *
- * The axes were measured (turn one bone 40° and watch the hand's centre move),
- * and the two sides are NOT the same. That is what used to send the left hand
- * behind while the right one came forward:
- *
- * arm / hand
- * X lift. Up on both sides, so the SAME value is symmetric.
- * Y roll about the flipper - which way the palm faces.
- * Z forward/back. +Z is BACK on the left and FORWARD on the right, so a
- * symmetric pose writes the right-hand Z with the opposite sign, while
- * a walk (opposite arms) writes the SAME sign on both.
- * legsupport
- * X out to the side and up (same value on both sides is symmetric)
- * Y twist (which way the toe points)
- * Z forward/back, mirrored like `arm`
- * It pivots at the middle of the body, so the feet travel a long way for
- * a small angle - keep it under about 15 degrees.
- * master
- * X lean forward Y spin around Z roll sideways
- *
- * `armsupport` still exists in the rig but it is a control bone that pivots at
- * the body's centre, so it swings a hand in a wide arc across the face. Every
- * preset below drives `arm`/`hand` instead.
- */
-export const POSE_PRESETS = [
- { id: 'stand', label: '立ち', pose: { bones: {} } },
- {
- id: 'attention',
- label: 'きおつけ',
- pose: {
- bones: {
- // Both flippers dropped to the sides. A negative X lowers the arm, and
- // the value is the same on both sides (only Z mirrors between them).
- 'arm.l': [-62, 0, 0],
- 'arm.r': [-62, 0, 0],
- },
- },
- },
- {
- id: 'bow',
- label: 'おじぎ',
- pose: { bones: { master: [22, 0, 0], 'arm.l': [-62, 0, 0], 'arm.r': [-62, 0, 0] } },
- },
- {
- id: 'deep-bow',
- label: 'ふかくおじぎ',
- pose: { bones: { master: [42, 0, 0], 'arm.l': [-62, 0, 0], 'arm.r': [-62, 0, 0] } },
- },
- {
- id: 'tilt',
- label: 'くびかしげ',
- pose: {
- bones: {
- master: [0, 0, 16],
- // The roll turns the whole body about its base, which swings the feet up
- // and down, so the legs take the difference. The right-hand leg (on the
- // left of the screen) is the one that is planted: it opens outwards a
- // little and keeps the sole down. The left-hand leg bends back behind it.
- 'legsupport.r': [8, 0, 0],
- 'legsupport.l': [-6, 0, 14],
- },
- },
- },
- {
- id: 'wave',
- label: 'てをふる',
- pose: {
- bones: {
- master: [0, 0, -4],
- // The left flipper goes up and slightly forward. Y is the roll about the
- // flipper's own length, which is what turns the palm to face outwards.
- 'arm.l': [84, 0, -6],
- 'hand.l': [0, -30, 0],
- },
- },
- },
- {
- id: 'banzai',
- label: 'ばんざい',
- pose: {
- bones: {
- master: [-8, 0, 0],
- // Both flippers as far up as the shoulder allows (the pivot sits at the
- // middle of the body, so they cannot reach above the head).
- 'arm.l': [106, 0, -12],
- 'arm.r': [106, 0, 12],
- },
- root: [0, 0.2, 0],
- },
- },
- {
- id: 'stretch',
- label: 'のびをする',
- pose: {
- bones: {
- master: [-16, 0, 0],
- 'arm.l': [112, 0, -8],
- 'arm.r': [112, 0, 8],
- },
- root: [0, 0.08, 0],
- },
- },
- {
- id: 'jump',
- label: 'ジャンプ',
- pose: {
- bones: {
- master: [-8, 0, 0],
- 'arm.l': [68, 0, -10],
- 'arm.r': [68, 0, 10],
- 'legsupport.l': [10, 0, 0],
- 'legsupport.r': [10, 0, 0],
- },
- root: [0, 0.62, 0],
- },
- },
- {
- id: 'leap',
- label: '大ジャンプ',
- pose: {
- bones: {
- master: [-18, 0, 0],
- 'arm.l': [92, 0, -12],
- 'arm.r': [92, 0, 12],
- 'legsupport.l': [14, 0, 0],
- 'legsupport.r': [14, 0, 0],
- },
- root: [0, 1.0, 0],
- },
- },
- {
- id: 'dance',
- label: 'おどる',
- pose: {
- bones: {
- master: [-4, 0, -12],
- // Deliberately uneven: the left flipper is up, the right one is out in
- // front, and the feet step the other way.
- 'arm.l': [78, 0, -14],
- 'arm.r': [14, 0, 34],
- 'legsupport.l': [0, 0, 8],
- 'legsupport.r': [0, 0, 8],
- },
- root: [0, 0.14, 0],
- },
- },
- {
- id: 'tip-over',
- label: 'こてん',
- pose: {
- bones: {
- master: [0, 0, 68],
- 'arm.l': [26, 0, -18],
- 'arm.r': [10, 0, 18],
- },
- root: [0, 0.25, 0],
- },
- },
- {
- id: 'lie',
- label: 'ねている',
- pose: {
- bones: {
- // Flat on its back: X tips the body backwards all the way over, so the
- // face looks up. The root lift keeps the back resting on the ground
- // rather than sinking through it (the body's depth becomes its height).
- master: [-88, 0, 0],
- // Both flippers rest against the body, loose.
- 'arm.l': [-18, 0, 0],
- 'arm.r': [-18, 0, 0],
- },
- root: [0, 1.5, 0],
- },
- },
- { id: 'side', label: 'よこむき', pose: { bones: { master: [0, 92, 0] } } },
- { id: 'turn', label: 'うしろむき', pose: { bones: { master: [0, 180, 0] } } },
- {
- id: 'present',
- label: '右を紹介',
- pose: {
- bones: {
- master: [6, -26, 0],
- // The right flipper sweeps forward to point at whatever is on its right
- // (+Z is forwards on the right); the left one just steps aside.
- 'arm.r': [16, 0, 52],
- 'arm.l': [10, 0, -6],
- },
- },
- },
- {
- id: 'present-left',
- label: '左を紹介',
- pose: {
- bones: {
- master: [6, 26, 0],
- 'arm.l': [16, 0, -52],
- 'arm.r': [10, 0, 6],
- },
- },
- },
-];
-
-/** Deep-merge `patch` into `target` (plain objects only). */
-export function applyPatch(target, patch) {
- for (const [key, value] of Object.entries(patch ?? {})) {
- if (value && typeof value === 'object' && !Array.isArray(value)
- && target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
- applyPatch(target[key], value);
- } else {
- target[key] = Array.isArray(value) ? value.slice() : value;
- }
- }
- return target;
-}
-
-export function cloneState(state) {
- return JSON.parse(JSON.stringify(state));
-}
diff --git a/bluebey-studio/src/props.js b/bluebey-studio/src/props.js
deleted file mode 100644
index 78525bd..0000000
--- a/bluebey-studio/src/props.js
+++ /dev/null
@@ -1,646 +0,0 @@
-import * as THREE from 'three';
-
-/**
- * Procedural 小物 (props) that can be placed on the stage next to the character:
- * a lectern, a desk, a microphone stand, a signboard, a potted plant and a
- * cardboard box.
- *
- * Everything is built from three's primitives instead of loaded from a file, for
- * three reasons. The character is about 4.15 units tall with its feet on y = 0
- * and its body a sphere of radius ~1.6 centred near y = 2.4, so every generator
- * below works from that scale, and every group's origin is its *base centre* -
- * dropping one at a ground position and rotating it about y is all the app has
- * to do. The studio also re-skins props with the same toon/flat materials and
- * the same inverted-hull outline as the body, so the geometry stays chunky: no
- * plate thinner than ~0.05 units (a thin plate's offset hull turns into a smear)
- * and no textures anywhere. And props must be identical on every run, so every
- * size, position and angle here is a literal - no Math.random().
- *
- * Colours are never hard-coded on a mesh. Each material is tagged with
- * `userData.part`, and `applyPropColors` fills the colour in from the app's
- * render.colors palette, which is what lets the colour-theme feature recolour a
- * prop that was built minutes ago. The mapping keeps lightness apart from hue,
- * so a prop still reads in the near-monochrome すみ theme:
- *
- * wood the body colour, darkened - painted wood, cardboard, soil
- * paper the body colour, nearly white - the blank sign face
- * accent the accent colour - legs, frames, pots, mic bodies
- * leaf the leaf colour - foliage
- * metal a fixed neutral grey - stands, booms, microphone heads
- *
- * `userData.shade` multiplies a part's colour, so one prop can use two tones of
- * the same part - a cardboard box and its darker inner flaps - without adding a
- * sixth palette entry.
- */
-
-/** Reused when something has to be aimed along a direction vector. */
-const UP = new THREE.Vector3(0, 1, 0);
-const WHITE = new THREE.Color(0xffffff);
-/** A fixed grey: a metal stand has to read in every theme, warm or grey. */
-const METAL_GREY = '#b9bec7';
-/** Enough of the palette to build with when the app has not sent one yet. */
-const FALLBACK_COLORS = { body: '#c8b0f0', accent: '#8a4fe0', leaf: '#a6dd6a' };
-
-const PART_NAMES = ['wood', 'metal', 'paper', 'accent', 'leaf'];
-
-const clamp01 = (value) => Math.min(1, Math.max(0, value));
-
-/** The lectern's reading surface tilts up towards the audience, i.e. towards +z. */
-const SLAB_TILT = -0.32;
-
-// ---------------------------------------------------------------------------
-// Building blocks
-// ---------------------------------------------------------------------------
-
-/**
- * A material per (part, shade) inside one prop. Sharing them keeps the draw
- * calls down and, more importantly, lets `disposeProp` release each one once.
- */
-function materialCache() {
- const cache = new Map();
- return (part, shade = 1) => {
- if (!PART_NAMES.includes(part)) throw new Error(`unknown prop material part: ${part}`);
- const key = `${part}:${shade}`;
- let material = cache.get(key);
- if (!material) {
- const metal = part === 'metal';
- material = new THREE.MeshStandardMaterial({
- color: 0xffffff, // filled in by applyPropColors
- roughness: 0.75,
- metalness: metal ? 0.6 : 0,
- });
- material.userData.part = part;
- material.userData.shade = shade;
- cache.set(key, material);
- }
- return material;
- };
-}
-
-/**
- * Wraps a generator so each prop gets its own material cache. The generator
- * receives `(group, material, options)`, fills the group and returns nothing.
- */
-function prop(generator) {
- return (options = {}) => {
- const group = new THREE.Group();
- generator(group, materialCache(), options);
- return group;
- };
-}
-
-/** Adds a mesh, matching the body's shadow flags, and returns it. */
-function addMesh(parent, geometry, material, x = 0, y = 0, z = 0) {
- const mesh = new THREE.Mesh(geometry, material);
- mesh.position.set(x, y, z);
- mesh.castShadow = true;
- mesh.receiveShadow = false;
- parent.add(mesh);
- return mesh;
-}
-
-/**
- * The 線画 fill a part takes, as how far it steps from the paper towards the ink
- * (see `addMesh` in src/styles.js). A prop is paper in 線画, so two parts of a prop
- * that meet flush - the pencil's lead in its wood, a can's label on its body - come
- * out as one white shape. Tagging one of them with a tone puts the seam back; it
- * has no effect on リアル / フラット, where the part's own colour draws it.
- */
-function tone(mesh, strength) {
- mesh.userData.tone = strength;
- return mesh;
-}
-
-/** A cuboid, sized as [width, height, depth] and centred on `at`. */
-function addBox(parent, material, size, at = [0, 0, 0]) {
- const geometry = new THREE.BoxGeometry(size[0], size[1], size[2]);
- return addMesh(parent, geometry, material, at[0], at[1], at[2]);
-}
-
-/** A cylinder (or cone, if the two radii differ) centred on `at`. */
-function addCylinder(parent, material, radiusTop, radiusBottom, height, at, segments = 12) {
- const geometry = new THREE.CylinderGeometry(radiusTop, radiusBottom, height, segments);
- return addMesh(parent, geometry, material, at[0], at[1], at[2]);
-}
-
-/** A rounded blob. The caller usually scales it into a leaf or an ellipsoid. */
-function addSphere(parent, material, radius, at, segments = [10, 8]) {
- const geometry = new THREE.SphereGeometry(radius, segments[0], segments[1]);
- return addMesh(parent, geometry, material, at[0], at[1], at[2]);
-}
-
-/** A rod from `from` to `to`: stems, booms and struts are all just this. */
-function addStrut(parent, material, from, to, radius, segments = 8) {
- const start = new THREE.Vector3(from[0], from[1], from[2]);
- const end = new THREE.Vector3(to[0], to[1], to[2]);
- const axis = end.clone().sub(start);
- const length = axis.length();
- const mesh = addMesh(
- parent,
- new THREE.CylinderGeometry(radius, radius, length, segments),
- material,
- (start.x + end.x) / 2,
- (start.y + end.y) / 2,
- (start.z + end.z) / 2,
- );
- mesh.quaternion.setFromUnitVectors(UP, axis.normalize());
- return mesh;
-}
-
-// ---------------------------------------------------------------------------
-// The props
-// ---------------------------------------------------------------------------
-
-/**
- * 演台. A foot plate, a pedestal, a slab that tilts up towards the audience and
- * a gooseneck microphone on the corner nearest the camera. The mic is on the
- * audience edge on purpose: on the far edge the slab would hide it from a
- * slightly-above camera.
- */
-const buildPodium = prop((group, material) => {
- addBox(group, material('accent'), [1.30, 0.10, 0.98], [0, 0.05, 0]);
- addBox(group, material('wood'), [1.10, 1.30, 0.78], [0, 0.75, 0]);
-
- const slab = addBox(group, material('wood'), [1.42, 0.12, 0.86], [0, 1.42, 0.10]);
- slab.rotation.x = SLAB_TILT;
- // A lip along the slab's low edge, so the silhouette reads as a lectern top.
- const lip = addBox(group, material('accent'), [1.02, 0.10, 0.10], [0, 1.498, 0.527]);
- lip.rotation.x = SLAB_TILT;
-
- const stem = addCylinder(group, material('metal'), 0.032, 0.032, 0.60, [-0.48, 1.80, 0.26], 8);
- stem.rotation.x = SLAB_TILT;
- const head = addSphere(group, material('metal'), 0.09, [-0.48, 2.11, 0.15], [10, 8]);
- head.scale.set(1, 1.25, 1);
-});
-
-/**
- * 机. A top, four legs and a pair of side rails. The legs and rails take the
- * darker `accent` tone so the table keeps its shape in a flat or greyscale
- * render, where a single flat colour would turn it into a silhouette.
- */
-const buildDesk = prop((group, material) => {
- addBox(group, material('wood'), [2.30, 0.12, 1.20], [0, 1.44, 0]);
- for (const x of [-1.00, 1.00]) {
- for (const z of [-0.46, 0.46]) {
- addBox(group, material('accent'), [0.14, 1.38, 0.14], [x, 0.69, z]);
- }
- addBox(group, material('accent'), [0.12, 0.10, 1.00], [x, 0.42, 0]);
- }
-});
-
-/**
- * マイク. A round weighted base, a pole, a short boom and a body with a grille
- * head, standing about as high as the character's hands.
- */
-const buildMic = prop((group, material) => {
- addCylinder(group, material('metal'), 0.40, 0.42, 0.10, [0, 0.05, 0], 16);
- addCylinder(group, material('metal'), 0.048, 0.058, 1.35, [0, 0.775, 0], 10);
- addStrut(group, material('metal'), [0, 1.42, 0], [0, 1.62, 0.28], 0.038);
-
- const body = addCylinder(group, material('accent'), 0.13, 0.13, 0.44, [0, 1.80, 0.36], 12);
- body.rotation.x = 0.30;
- const head = addSphere(group, material('metal'), 0.145, [0, 2.05, 0.42], [12, 8]);
- head.scale.set(1, 0.9, 1);
-});
-
-/**
- * 看板. A post, a frame and a blank face. The face is a named child tagged
- * `userData.canvasTexture`, so a later feature can paint a CanvasTexture onto it
- * without hunting for the writable mesh. The face *and* its frame are grouped,
- * so `applyPropFaceScale` can grow the whole board about the face's centre while
- * the post and base stay put. Both are thin boxes rather than planes: the outline
- * pass offsets geometry along its normals, and a zero-thickness plane gets a
- * visibly doubled rim.
- */
-const buildSign = prop((group, material) => {
- addBox(group, material('accent'), [0.52, 0.10, 0.52], [0, 0.05, 0]);
- addBox(group, material('wood'), [0.16, 1.62, 0.16], [0, 0.91, 0]);
-
- // The board is one group so the face and its frame scale together, about the
- // board's own centre - which is also the face's centre.
- const board = new THREE.Group();
- board.name = 'sign-board';
- board.userData.faceScaleGroup = true;
- board.position.set(0, 2.21, 0);
- group.add(board);
-
- addBox(board, material('accent'), [1.42, 0.98, 0.12], [0, 0, 0]);
- const face = addBox(board, material('paper'), [1.18, 0.76, 0.08], [0, 0, 0.06]);
- face.name = 'sign-face';
- face.userData.canvasTexture = true;
-});
-
-/** The branches of 観葉植物: where a stem ends, which way its leaf points. */
-const PLANT_BRANCHES = [
- { end: [0.05, 1.12, 0.04], dir: [0.10, 0.96, 0.14] },
- { end: [-0.30, 0.98, -0.06], dir: [-0.52, 0.76, -0.30] },
- { end: [0.28, 1.00, 0.14], dir: [0.48, 0.74, 0.40] },
- { end: [-0.40, 0.80, -0.16], dir: [-0.86, 0.36, -0.28] },
- { end: [0.38, 0.82, 0.16], dir: [0.88, 0.32, 0.26] },
- { end: [-0.04, 1.06, -0.20], dir: [-0.14, 0.95, -0.28] },
-];
-
-/**
- * 観葉植物. A tapered pot with a rim, dark soil, and leaves that are just
- * squashed spheres aimed along each branch's direction - the cheapest way to get
- * a soft, rounded leaf that still takes a clean outline.
- */
-const buildPlant = prop((group, material) => {
- addCylinder(group, material('accent'), 0.40, 0.28, 0.56, [0, 0.28, 0], 14);
- tone(addCylinder(group, material('accent', 0.88), 0.44, 0.42, 0.12, [0, 0.56, 0], 14), 0.18);
- tone(addCylinder(group, material('wood', 0.75), 0.37, 0.37, 0.06, [0, 0.61, 0], 14), 0.28);
-
- for (const branch of PLANT_BRANCHES) {
- addStrut(group, material('leaf', 0.6), [0, 0.56, 0], branch.end, 0.035);
-
- const direction = new THREE.Vector3(branch.dir[0], branch.dir[1], branch.dir[2]).normalize();
- const leaf = addSphere(group, material('leaf'), 0.30, [0, 0, 0], [8, 6]);
- leaf.scale.set(0.46, 1, 0.22);
- leaf.position.set(branch.end[0], branch.end[1], branch.end[2]).addScaledVector(direction, 0.26);
- leaf.quaternion.setFromUnitVectors(UP, direction);
- }
-});
-
-/**
- * 段ボール箱. The body and two lid flaps, plus a tape strip when the lid is
- * shut. The flaps are children of pivot groups sitting on the box's top edges,
- * so `open: true` is a single rotation each. The flaps are a darker shade of the
- * same wood part, which is what makes the box read as cardboard rather than a
- * solid block.
- */
-const buildBox = prop((group, material, options) => {
- const flapsDown = options.open !== true;
- addBox(group, material('wood'), [1.24, 1.00, 1.24], [0, 0.50, 0]);
-
- const back = new THREE.Group();
- back.position.set(0, 1.00, -0.62);
- back.rotation.x = flapsDown ? 0 : -1.05;
- group.add(back);
- tone(addBox(back, material('wood', 0.72), [1.20, 0.07, 0.62], [0, 0.035, 0.31]), 0.20);
-
- const front = new THREE.Group();
- front.position.set(0, 1.00, 0.62);
- front.rotation.x = flapsDown ? 0 : 1.05;
- group.add(front);
- tone(addBox(front, material('wood', 0.72), [1.20, 0.07, 0.62], [0, 0.035, -0.31]), 0.20);
-
- if (flapsDown) addBox(group, material('paper', 0.95), [0.18, 0.05, 1.26], [0, 1.095, 0]);
-});
-
-/**
- * 空いた缶詰. A short metal can, opened: a foot bead, the body, a label band and
- * a mouth rim, plus the single lid peeled back on a hinge at the back of the
- * mouth so it stands up behind the opening. It is deliberately
- * small next to the 4-unit-tall character - something to be pointed at rather
- * than stood behind - so every size here stays under a unit, and the whole can
- * leans a little so it does not read as a diagram. The label is the one coloured
- * part; everything else is the neutral `metal` grey, so it still reads as a tin.
- */
-const buildCan = prop((group, material) => {
- addCylinder(group, material('metal'), 0.153, 0.153, 0.05, [0, 0.025, 0], 16);
- addCylinder(group, material('metal'), 0.145, 0.145, 0.40, [0, 0.20, 0], 16);
- tone(addCylinder(group, material('accent', 0.85), 0.148, 0.148, 0.16, [0, 0.21, 0], 16), 0.20);
- addCylinder(group, material('metal'), 0.153, 0.153, 0.05, [0, 0.40, 0], 16);
- // A darker disc just inside the rim so the mouth reads as empty, not capped.
- tone(addCylinder(group, material('metal', 0.45), 0.132, 0.132, 0.012, [0, 0.422, 0], 16), 0.30);
-
- // The one lid, peeled back on a hinge at the back of the mouth so its
- // underside shows. The hinge sits *on* the can's back rim and the disc is
- // placed so its own rim passes through that same point (its centre is one
- // radius in front of the hinge), so opening the hinge rotates the lid about
- // its edge - the two rims stay joined instead of the lid floating in mid-air.
- // It is twisted slightly sideways so it does not read as a knob sitting
- // straight up on the can.
- const lidRadius = 0.132;
- const hinge = new THREE.Group();
- hinge.position.set(0, 0.42, -lidRadius);
- hinge.rotation.x = -1.05;
- hinge.rotation.z = 0.4;
- group.add(hinge);
- addCylinder(hinge, material('metal', 0.9), lidRadius, lidRadius, 0.018, [0, 0, lidRadius], 18);
-
- group.rotation.z = 0.05;
-});
-
-/**
- * ベッド. A wooden frame on four legs with a mattress, a headboard at the far end
- * and a pillow, sized so the character can be posed lying on it.
- */
-const buildBed = prop((group, material) => {
- // Sized to take the character lying down: the body is a sphere of radius ~1.6,
- // so the frame is a little over 3 units across and 4 units long.
- for (const x of [-1.55, 1.55]) {
- for (const z of [-1.85, 1.85]) {
- addBox(group, material('wood'), [0.22, 0.5, 0.22], [x, 0.25, z]);
- }
- }
- addBox(group, material('wood'), [3.5, 0.26, 4.3], [0, 0.63, 0]);
- // The headboard sits at the far (-z) end, away from the camera.
- addBox(group, material('wood'), [3.5, 1.5, 0.2], [0, 1.25, -2.15]);
- addBox(group, material('paper'), [3.3, 0.46, 4.1], [0, 0.99, 0]);
-
- // The pillow, slightly tipped up against the headboard.
- const pillow = addBox(group, material('paper', 0.9), [2.2, 0.34, 0.9], [0, 1.39, -1.5]);
- pillow.rotation.x = -0.08;
-});
-
-/**
- * 布団. A mattress laid on the floor, a pillow and a quilt folded over the lower
- * end.
- */
-const buildFuton = prop((group, material) => {
- addBox(group, material('paper'), [3.6, 0.3, 4.4], [0, 0.15, 0]);
- const pillow = addBox(group, material('paper', 0.9), [2.2, 0.3, 0.9], [0, 0.45, -1.6]);
- pillow.rotation.x = -0.06;
- addBox(group, material('accent', 0.8), [3.6, 0.24, 2.7], [0, 0.42, 0.85]);
-});
-
-// ---------------------------------------------------------------------------
-// The library
-// ---------------------------------------------------------------------------
-
-/**
- * `height` is the approximate height in units, measured from the base, so the UI
- * can offer a sensible scale. Where a prop has options, it is the height of the
- * default variant: an open cardboard box stands about a third taller again, so
- * the offered scale stays sensible either way.
- */
-/** 鉛筆: an oversized pencil standing on its eraser, point up. */
-const buildPencil = prop((group, material) => {
- const r = 0.13;
- // Eraser, ferrule and lead are each a band of the same width against the body,
- // so without a tone of their own they vanish into it in 線画.
- tone(addCylinder(group, material('accent', 0.85), r * 0.9, r * 0.98, 0.16, [0, 0.08, 0], 6), 0.20);
- tone(addCylinder(group, material('metal', 0.9), r, r, 0.12, [0, 0.22, 0], 6), 0.24);
- addCylinder(group, material('accent'), r, r, 1.30, [0, 0.93, 0], 6);
- addCylinder(group, material('wood'), 0, r, 0.30, [0, 1.73, 0], 6);
- // The lead's wide base sinks into the cone (whose tip is at y = 1.88), so the
- // two look joined, while its point still pokes out past the wood.
- tone(addCylinder(group, material('metal', 0.3), 0, r * 0.3, 0.12, [0, 1.86, 0], 6), 0.45);
-});
-
-/** コップ: a mug with a rim, a handle and a little drink inside. */
-const buildCup = prop((group, material) => {
- addCylinder(group, material('paper'), 0.34, 0.30, 0.66, [0, 0.33, 0], 18);
- addCylinder(group, material('paper', 0.86), 0.30, 0.30, 0.05, [0, 0.645, 0], 18);
- addCylinder(group, material('wood', 0.5), 0.28, 0.28, 0.02, [0, 0.61, 0], 18);
- addMesh(group, new THREE.TorusGeometry(0.16, 0.045, 8, 16), material('paper'), 0.36, 0.36, 0);
-});
-
-/** ノート: a fat pad of pages under a thin cover, bound along one long edge. */
-const buildNotebook = prop((group, material) => {
- const width = 1.5;
- const depth = 2.0;
- const pages = 0.18;
- const cover = 0.04;
- // The cover pokes out past the pages, so the notebook reads from above.
- addBox(group, material('wood', 0.6), [width + 0.08, cover, depth + 0.08], [0, cover / 2, 0]);
- addBox(group, material('paper'), [width, pages, depth], [0, cover + pages / 2, 0]);
- addBox(
- group,
- material('accent'),
- [0.16, pages + 0.04, depth + 0.02],
- [-width / 2 + 0.08, cover + pages / 2, 0],
- );
-});
-
-/** 消しゴム: a chunky two-tone block, a hard accent top on a soft white base. */
-const buildEraser = prop((group, material) => {
- const width = 0.9;
- const depth = 0.5;
- const half = 0.15;
- addBox(group, material('paper'), [width, half, depth], [0, half / 2, 0]);
- tone(addBox(group, material('accent', 0.9), [width, half, depth], [0, half + half / 2, 0]), 0.22);
-});
-
-export const PROP_LIBRARY = [
- { id: 'podium', label: '演台', height: 2.2, build: buildPodium },
- { id: 'desk', label: '机', height: 1.5, build: buildDesk },
- { id: 'mic', label: 'マイク', height: 2.2, build: buildMic },
- { id: 'sign', label: '看板', height: 2.7, build: buildSign },
- { id: 'plant', label: '観葉植物', height: 1.7, build: buildPlant },
- { id: 'box', label: '段ボール箱', height: 1.15, build: buildBox },
- { id: 'can', label: '空いた缶詰', height: 0.65, build: buildCan },
- { id: 'bed', label: 'ベッド', height: 2.0, build: buildBed },
- { id: 'futon', label: '布団', height: 0.65, build: buildFuton },
- { id: 'cup', label: 'コップ', height: 0.66, build: buildCup },
- { id: 'pencil', label: '鉛筆', height: 2.0, build: buildPencil },
- { id: 'notebook', label: 'ノート', height: 0.22, build: buildNotebook },
- { id: 'eraser', label: '消しゴム', height: 0.3, build: buildEraser },
-];
-
-/**
- * Where a freshly added prop goes. The character's body is a sphere of radius
- * ~1.6 around x = 0, z = 0, so anything closer than about 2.2 units would push
- * into it; each prop therefore sits 2.6-3.0 units out, in the quadrant that
- * suits its height, turned only slightly - a prop square to the camera reads as
- * a diagram, and one turned all the way shows its side.
- */
-export const PROP_DEFAULTS = {
- podium: { x: -2.1, y: 0, z: 1.9, rotY: 0.30, scale: 1 },
- desk: { x: -2.6, y: 0, z: 0.5, rotY: 0.40, scale: 1 },
- mic: { x: 2.0, y: 0, z: 1.8, rotY: -0.40, scale: 1 },
- // `faceScale` only means something for a 看板: it grows the writing area (the
- // face and its frame) together, so the post and base stay put. The panel owns
- // that control.
- sign: { x: 2.9, y: 0, z: 0.4, rotY: -0.25, scale: 1, faceScale: 1, text: '' },
- plant: { x: -2.0, y: 0, z: -1.8, rotY: 0.25, scale: 1 },
- box: { x: 2.0, y: 0, z: -1.7, rotY: 0.55, scale: 1 },
- can: { x: 1.7, y: 0, z: 1.5, rotY: -0.5, scale: 1 },
- bed: { x: 0, y: 0, z: -4.7, rotY: 0, scale: 1 },
- futon: { x: 4.2, y: 0, z: -0.6, rotY: -0.35, scale: 1 },
- pencil: { x: -1.4, y: 0, z: 2.6, rotY: 0.3, scale: 1 },
- cup: { x: 1.5, y: 0, z: 2.5, rotY: -0.4, scale: 1 },
- notebook: { x: -3.2, y: 0, z: -2.7, rotY: 0.35, scale: 1 },
- eraser: { x: 3.6, y: 0, z: 2.6, rotY: -0.45, scale: 1 },
-};
-
-/** The palette entry each part reads, derived from the app's colours. */
-function derivePalette(colors = {}) {
- const body = new THREE.Color(colors.body ?? FALLBACK_COLORS.body);
- const accent = new THREE.Color(colors.accent ?? FALLBACK_COLORS.accent);
- const leaf = new THREE.Color(colors.leaf ?? FALLBACK_COLORS.leaf);
- return {
- // Darkened rather than mixed towards black, so the surface keeps a hint of
- // the theme's hue instead of turning into a grey.
- wood: body.clone().multiplyScalar(0.72),
- paper: body.clone().lerp(WHITE, 0.85),
- accent,
- leaf,
- metal: new THREE.Color(METAL_GREY),
- };
-}
-
-/**
- * Builds one prop.
- *
- * @param {string} id one of PROP_LIBRARY's ids
- * @param {object} [options]
- * @param {object} [options.colors] the app's render.colors palette
- * @param {string} [options.outlineColor] kept on the group for the outline pass
- * @param {number} [options.scale] uniform scale for the whole prop
- * @returns {THREE.Group} the prop, origin at its base centre, facing +z
- */
-export function buildProp(id, options = {}) {
- const spec = PROP_LIBRARY.find((entry) => entry.id === id);
- if (!spec) {
- const known = PROP_LIBRARY.map((entry) => entry.id).join(', ');
- throw new Error(`unknown prop id ${JSON.stringify(id)}; known props: ${known}`);
- }
-
- const group = spec.build(options);
- group.name = `prop:${id}`;
- group.userData.propId = id;
- group.userData.outlineColor = options.outlineColor ?? null;
- if (Number.isFinite(options.scale) && options.scale !== 1) {
- group.scale.setScalar(options.scale);
- }
- return applyPropColors(group, options.colors);
-}
-
-/**
- * Recolours a built prop in place, for the colour-theme feature. Every mesh
- * material this module creates carries `userData.part`, so the walk only has to
- * look at those; anything else (a future prop built by hand) is left alone.
- */
-export function applyPropColors(group, colors = {}) {
- const palette = derivePalette(colors);
- const seen = new Set();
-
- group.traverse((object) => {
- const list = Array.isArray(object.material) ? object.material : [object.material];
- for (const material of list) {
- if (!material || seen.has(material)) continue;
- seen.add(material);
- const part = material.userData?.part;
- if (!part) continue;
- const shade = Number.isFinite(material.userData.shade) ? material.userData.shade : 1;
- material.color.copy(palette[part] ?? palette.wood).multiplyScalar(shade);
- material.color.r = clamp01(material.color.r);
- material.color.g = clamp01(material.color.g);
- material.color.b = clamp01(material.color.b);
- }
- });
-
- return group;
-}
-
-/**
- * The writing surface of a prop: the mesh tagged `userData.canvasTexture`, i.e.
- * the 看板's face. A prop without one returns `null`, so callers can treat every
- * prop the same.
- */
-export function propFace(group) {
- let face = null;
- group?.traverse?.((object) => {
- if (!face && object.userData?.canvasTexture) face = object;
- });
- return face;
-}
-
-/**
- * Scale the writing area - the 看板's face *and* the frame around it - about the
- * board's own centre, so the post and base stay where they are. A prop that tags
- * a group `faceScaleGroup` is scaled as a whole; anything else falls back to the
- * writable mesh alone. `addBox` puts a mesh's origin at the box centre and the
- * 看板's board group sits at the face centre, which is what makes this a scale
- * about the centre rather than about the prop's base.
- *
- * WHY this is applied from the panel: the app rebuilds every prop from the state
- * (`applyProps` in src/main.js) and hands `buildProp` only the whole-prop scale,
- * so a per-item `faceScale` has to be re-applied to the fresh mesh afterwards.
- */
-export function applyPropFaceScale(group, faceScale) {
- let target = null;
- group?.traverse?.((object) => {
- if (!target && object.userData?.faceScaleGroup) target = object;
- });
- target ??= propFace(group);
- if (!target) return;
- const value = Number(faceScale);
- const scale = Number.isFinite(value) ? Math.min(4, Math.max(0.2, value)) : 1;
- target.scale.setScalar(scale);
-}
-
-/**
- * Paint text onto a prop's writing surface (the 看板's face), or clear it.
- *
- * The face's material gets a `CanvasTexture`, drawn here with a system font stack
- * (no font file is needed, and Japanese falls back to whatever the device has).
- * Long lines and long blocks are shrunk to fit the board, and explicit newlines
- * are honoured. The material is per-prop (see `materialCache`), so the map never
- * leaks to another sign.
- */
-export function applyPropText(group, text) {
- if (typeof document === 'undefined') return; // Node (tests): no canvas
- const face = propFace(group);
- const material = face?.material;
- if (!material) return;
- const value = typeof text === 'string' ? text : '';
-
- if (!value.trim()) {
- if (material.map) {
- material.map.dispose();
- material.map = null;
- material.needsUpdate = true;
- }
- material.userData.signText = '';
- return;
- }
- if (material.userData.signText === value) return; // nothing changed
-
- const canvas = document.createElement('canvas');
- canvas.width = 512;
- canvas.height = 330;
- const ctx = canvas.getContext('2d');
- ctx.fillStyle = '#f4efe2';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.fillStyle = '#241a30';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
-
- const family = '"Hiragino Kaku Gothic ProN", "Yu Gothic", "Noto Sans JP", sans-serif';
- const lines = value.split(/\r\n|\r|\n/);
- const maxWidth = canvas.width * 0.86;
- let size = 150;
- const fits = () => {
- ctx.font = `700 ${size}px ${family}`;
- return lines.every((line) => ctx.measureText(line).width <= maxWidth)
- && lines.length * size * 1.15 <= canvas.height * 0.86;
- };
- while (size > 18 && !fits()) size -= 2;
- ctx.font = `700 ${size}px ${family}`;
- const step = size * 1.15;
- const startY = canvas.height / 2 - ((lines.length - 1) * step) / 2;
- lines.forEach((line, index) => ctx.fillText(line, canvas.width / 2, startY + index * step));
-
- const texture = new THREE.CanvasTexture(canvas);
- texture.colorSpace = THREE.SRGBColorSpace;
- texture.anisotropy = 4;
- if (material.map) material.map.dispose();
- material.map = texture;
- material.userData.signText = value;
- material.needsUpdate = true;
-}
-
-/** The world-space box the prop occupies; the app drops its contact shadow from it. */
-export function propBounds(group) {
- group.updateMatrixWorld(true);
- return new THREE.Box3().setFromObject(group);
-}
-
-/** Releases every geometry and material in the prop and empties the group. */
-export function disposeProp(group) {
- const geometries = new Set();
- const materials = new Set();
-
- group.traverse((object) => {
- if (object.geometry) geometries.add(object.geometry);
- const list = Array.isArray(object.material) ? object.material : [object.material];
- for (const material of list) if (material) materials.add(material);
- });
-
- for (const geometry of geometries) geometry.dispose();
- for (const material of materials) material.dispose();
- group.clear();
-}
diff --git a/bluebey-studio/src/rig.js b/bluebey-studio/src/rig.js
deleted file mode 100644
index e3c8a8e..0000000
--- a/bluebey-studio/src/rig.js
+++ /dev/null
@@ -1,212 +0,0 @@
-import * as THREE from 'three';
-import { TransformControls } from 'three/addons/controls/TransformControls.js';
-
-/**
- * Posing. Each bone is driven as a rotation *relative to its rest pose*, so
- * every bone reads 0/0/0 in the original stance and a saved pose stays valid
- * even if the model is re-exported with different bone orientations.
- *
- * `bone.quaternion = rest * delta` and `delta = rest⁻¹ * quaternion`, with the
- * delta expressed as XYZ Euler angles in degrees for the UI.
- */
-
-const RAD = Math.PI / 180;
-const DEG = 180 / Math.PI;
-
-/** three sanitises node names, so `armsupport.l` arrives as `armsupportl`. */
-const normalizeName = (name) => (name ?? '').replace(/[.\s]/g, '').toLowerCase();
-
-/**
- * The same angle shifted by whole turns to sit next to `reference`. On an exact
- * half-turn tie the lower (smaller-magnitude) value wins, which keeps the
- * canonical -180..180 reading when two are equally close.
- */
-const nearestAngle = (angle, reference) => {
- const turns = (reference - angle) / 360;
- const low = angle + 360 * Math.floor(turns);
- const high = angle + 360 * Math.ceil(turns);
- return Math.abs(low - reference) <= Math.abs(high - reference) ? low : high;
-};
-
-/** True when two quaternions describe the same rotation (double cover included). */
-const sameRotation = (a, b) => Math.abs(Math.abs(a.dot(b)) - 1) < 1e-6;
-
-export class Rig {
- constructor({ bones, scene, camera, domElement, orbit, pickTargets, onChange = null }) {
- this.bones = bones;
- this.byName = new Map();
- for (const entry of bones) {
- this.byName.set(entry.name, entry);
- const alias = normalizeName(entry.name);
- if (!this.byName.has(alias)) this.byName.set(alias, entry);
- }
- this.scene = scene;
- this.domElement = domElement;
- this.orbit = orbit;
- this.pickTargets = pickTargets;
- this.onChange = onChange;
- this.selected = null;
- // The delta last handed to (or written by) the UI, in degrees. An XYZ Euler
- // has several equivalent triples for one rotation, so getDelta returns the
- // one closest to this. That keeps a single-axis drag smooth through the
- // wrap at 90 deg (the middle axis) instead of jumping to the gimbal-flipped
- // twin, which made the sliders and the next setDelta spin the character.
- this.lastDelta = new Map();
-
- this.controls = new TransformControls(camera, domElement);
- this.controls.setMode('rotate');
- this.controls.setSpace('local');
- this.controls.setSize(0.8);
- // A *hidden* TransformControls still grabs the pointer in three: its picker
- // stays raycastable even when the helper is not drawn, so grabbing the body
- // (the rings sit on the `master` bone, right where you grab) silently turned
- // the character. Keep the controls disabled until the gizmo is actually
- // shown, so the mouse can only move the character (see setGizmoVisible).
- this.controls.enabled = false;
- this.helper = this.controls.getHelper();
- this.helper.visible = false;
- // Whether the user wants the gizmo at all. It starts OFF: the model is the
- // point, and the rings sat on top of it until you went looking for them.
- // Selecting a bone must not switch it back on by itself, so `select()`
- // consults this instead of always showing the helper.
- this.gizmoWanted = false;
- scene.add(this.helper);
-
- this.controls.addEventListener('dragging-changed', (event) => {
- this.orbit.enabled = !event.value;
- });
- this.controls.addEventListener('objectChange', () => {
- if (this.selected) this.onChange?.(this.selected);
- });
- }
-
- /* ------------------------------------------------------------ selection */
-
- select(name, { silent = false } = {}) {
- if (name && !this.byName.has(name)) return;
- this.selected = name ?? null;
- if (name) {
- this.controls.attach(this.byName.get(name).bone);
- this.helper.visible = this.gizmoWanted;
- } else {
- this.controls.detach();
- this.helper.visible = false;
- }
- // Only an actually-shown gizmo may take the pointer (see the constructor).
- this.controls.enabled = this.helper.visible;
- if (!silent) this.onChange?.(this.selected);
- }
-
- // Clicking the model no longer reaches for a bone: the mouse is for composing
- // (see the drag handling in src/main.js), and bones are picked from the panel's
- // bone list, which calls `select()` directly. So there is no pointer handler on
- // the canvas here at all.
-
- /* ----------------------------------------------------------------- pose */
-
- getDelta(name) {
- const entry = this.byName.get(name);
- if (!entry) return { x: 0, y: 0, z: 0 };
- const delta = entry.rest.clone().invert().multiply(entry.bone.quaternion);
- const base = new THREE.Euler().setFromQuaternion(delta, 'XYZ');
- const reference = this.lastDelta.get(entry) ?? { x: 0, y: 0, z: 0 };
-
- // For one rotation an XYZ Euler has a second solution - its "flipped" twin
- // `(x+180, 180-y, z+180)`. Three always returns the one whose middle angle
- // stays within +/-90, so past 90 the twin is what continues the drag; offer
- // both and keep whichever is closest to the last value. Near gimbal lock the
- // twin no longer reproduces the rotation, so it is dropped by the check.
- const candidates = [{ x: base.x, y: base.y, z: base.z }];
- const flipped = new THREE.Euler(base.x + Math.PI, Math.PI - base.y, base.z + Math.PI, 'XYZ');
- if (sameRotation(delta, new THREE.Quaternion().setFromEuler(flipped))) {
- candidates.push({ x: flipped.x, y: flipped.y, z: flipped.z });
- }
-
- let best = { x: 0, y: 0, z: 0 };
- let bestDistance = Infinity;
- for (const candidate of candidates) {
- const x = nearestAngle(candidate.x * DEG, reference.x);
- const y = nearestAngle(candidate.y * DEG, reference.y);
- const z = nearestAngle(candidate.z * DEG, reference.z);
- const distance = (x - reference.x) ** 2 + (y - reference.y) ** 2 + (z - reference.z) ** 2;
- if (distance < bestDistance) {
- bestDistance = distance;
- best = { x, y, z };
- }
- }
-
- this.lastDelta.set(entry, best);
- return { ...best };
- }
-
- setDelta(name, { x = 0, y = 0, z = 0 }) {
- const entry = this.byName.get(name);
- if (!entry) return;
- const delta = new THREE.Quaternion().setFromEuler(
- new THREE.Euler(x * RAD, y * RAD, z * RAD, 'XYZ'),
- );
- entry.bone.quaternion.copy(entry.rest).multiply(delta);
- // Remember exactly what the UI asked for so getDelta can hand it straight
- // back (this keeps applyPose/getPose an exact round-trip). Keyed by the bone
- // entry, not the name, so a raw name and its normalised alias agree.
- this.lastDelta.set(entry, { x, y, z });
- }
-
- /** `pose.bones` maps a bone name to `[x, y, z]` degrees; `pose.root` is a translation. */
- applyPose(pose = {}) {
- // Presets and saved files use the glTF bone names (`armsupport.l`) while
- // three sanitises them to `armsupportl`, so compare on the normalised form -
- // otherwise every preset that touches an arm or leg is silently ignored.
- const rotations = new Map();
- for (const [key, value] of Object.entries(pose.bones ?? {})) {
- rotations.set(normalizeName(key), value);
- }
- for (const entry of this.bones) {
- const value = rotations.get(normalizeName(entry.name));
- if (Array.isArray(value)) this.setDelta(entry.name, { x: value[0], y: value[1], z: value[2] });
- else this.setDelta(entry.name, { x: 0, y: 0, z: 0 });
- }
- }
-
- getPose({ onlyMoved = true } = {}) {
- const rotations = {};
- for (const entry of this.bones) {
- const { x, y, z } = this.getDelta(entry.name);
- const rounded = [round1(x), round1(y), round1(z)];
- if (onlyMoved && rounded.every((value) => value === 0)) continue;
- rotations[entry.name] = rounded;
- }
- return { bones: rotations };
- }
-
- reset(name) {
- if (name) {
- this.setDelta(name, { x: 0, y: 0, z: 0 });
- return;
- }
- for (const entry of this.bones) this.setDelta(entry.name, { x: 0, y: 0, z: 0 });
- }
-
- /** True when every bone sits at its rest rotation. */
- isRestPose() {
- return this.bones.every((entry) => {
- const { x, y, z } = this.getDelta(entry.name);
- return Math.abs(x) < 0.01 && Math.abs(y) < 0.01 && Math.abs(z) < 0.01;
- });
- }
-
- setGizmoVisible(visible) {
- this.gizmoWanted = visible !== false;
- this.helper.visible = this.gizmoWanted && this.selected != null;
- // A hidden gizmo must not grab the pointer (see the constructor).
- this.controls.enabled = this.helper.visible;
- }
-
- dispose() {
- this.controls.detach();
- this.controls.dispose();
- this.helper.removeFromParent();
- }
-}
-
-const round1 = (value) => Math.round(value * 10) / 10;
diff --git a/bluebey-studio/src/style.css b/bluebey-studio/src/style.css
deleted file mode 100644
index a147b1f..0000000
--- a/bluebey-studio/src/style.css
+++ /dev/null
@@ -1,880 +0,0 @@
-:root {
- --accent: #7b53d1;
- --accent-soft: #efe8fd;
- --accent-2: #b498ff;
- --text: #2b2433;
- --muted: #6f6682;
- --border: #e4dff0;
- --card: rgba(255, 255, 255, 0.94);
- --radius: 12px;
- --panel-width: 344px;
-}
-
-* { box-sizing: border-box; }
-
-html, body {
- margin: 0;
- height: 100%;
- height: 100vh;
- /* `dvh` tracks the space mobile browser chrome leaves; the lines above are
- the fallbacks for browsers that do not know it yet. */
- height: 100dvh;
- overflow: hidden;
-}
-
-body {
- font-family: system-ui, -apple-system, "Segoe UI", "Hiragino Kaku Gothic ProN", "Noto Sans JP", Meiryo, sans-serif;
- color: var(--text);
- background: radial-gradient(120% 120% at 30% 0%, #f7f5fc 0%, #ece8f4 60%, #e4e0ee 100%);
- -webkit-font-smoothing: antialiased;
-}
-
-/* ------------------------------------------------------------------ viewport */
-
-/*
- * The canvas gets its size from #stage, never from its own width/height
- * attributes: a canvas is a replaced element, so `width: auto` would make its
- * layout follow those attributes - and since the renderer writes them, that
- * becomes a resize feedback loop that leaves the picture blank.
- */
-#stage {
- position: fixed;
- inset: 0;
-}
-
-body:not(.panel-hidden) #stage {
- right: var(--panel-width);
-}
-
-#view {
- display: block;
- width: 100%;
- height: 100%;
- touch-action: none;
- outline: none;
-}
-
-/* ------------------------------------------------------------------- topbar */
-
-#topbar {
- position: fixed;
- top: 14px;
- left: 16px;
- z-index: 20;
- display: flex;
- align-items: center;
- gap: 14px;
- max-width: calc(100vw - var(--panel-width) - 48px);
-}
-
-#topbar h1 {
- margin: 0;
- font-size: 16px;
- font-weight: 700;
- letter-spacing: 0.02em;
- color: #4b3a72;
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
-}
-
-.topbar-actions { display: flex; flex-wrap: wrap; gap: 6px; }
-
-#topbar button {
- font: inherit;
- font-size: 12px;
- padding: 6px 11px;
- border-radius: 999px;
- border: 1px solid rgba(123, 83, 209, 0.22);
- background: rgba(255, 255, 255, 0.86);
- color: #5a4a7d;
- cursor: pointer;
- backdrop-filter: blur(6px);
- transition: background 0.15s, transform 0.1s;
-}
-
-#topbar button:hover { background: #fff; transform: translateY(-1px); }
-#topbar button:active { transform: translateY(0); }
-#topbar button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); }
-
-/* Shared by the mouse hint and the touch hint (see #viewport-hint-touch). */
-.viewport-hint {
- position: fixed;
- left: 18px;
- bottom: 14px;
- z-index: 10;
- margin: 0;
- font-size: 11.5px;
- color: #8a80a0;
- pointer-events: none;
-}
-
-/* Phones get a touch-specific hint instead of this mouse one; swapped in the
- small-screen media query at the end of the viewport/panel section below. */
-#viewport-hint-touch { display: none; }
-
-/* AR (camera) mode's shutter: a camera-app button over the viewport. It is a DOM
- layer, so it never appears in the exported PNG. */
-.ar-shutter {
- position: absolute;
- left: 50%;
- bottom: calc(22px + env(safe-area-inset-bottom));
- transform: translateX(-50%);
- z-index: 4;
- width: 64px;
- height: 64px;
- padding: 0;
- border-radius: 50%;
- border: 4px solid #ffffff;
- background: rgba(255, 255, 255, 0.35);
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
- cursor: pointer;
-}
-.ar-shutter::after {
- content: "";
- position: absolute;
- inset: 6px;
- border-radius: 50%;
- background: #ffffff;
-}
-.ar-shutter:active::after { background: #d9d2e8; }
-.ar-shutter[hidden] { display: none; }
-
-/* -------------------------------------------------------------------- panel */
-
-#panel {
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- width: var(--panel-width);
- padding: 14px 14px 40px;
- overflow-y: auto;
- overflow-x: hidden;
- background: var(--card);
- border-left: 1px solid var(--border);
- backdrop-filter: blur(10px);
- z-index: 30;
- scrollbar-width: thin;
-}
-
-#panel::-webkit-scrollbar { width: 9px; }
-#panel::-webkit-scrollbar-thumb { background: #d8d1e8; border-radius: 9px; }
-
-/* The phone sheet's grab handle is a real element (built in the app) so it can
- carry a drag; it is hidden everywhere else. */
-.panel-grip { display: none; }
-
-body.panel-hidden #panel { display: none; }
-
-body.panel-hidden #topbar { max-width: calc(100vw - 48px); }
-
-@media (max-width: 780px) {
- :root { --panel-width: min(88vw, 344px); }
- #topbar { max-width: calc(100vw - 60px); }
-}
-
-/*
- * Phones and other short touch screens. The panel becomes a bottom sheet over
- * the 3D view instead of a side column: the canvas keeps the full screen behind
- * it, so opening the sheet does not resize the render (and the ≡ button, which
- * already calls resizeViewport, is all the control needed).
- */
-@media (max-width: 768px), (max-height: 500px) and (pointer: coarse) {
- /* Full-screen canvas: the sheet floats over it, so its height never feeds
- back into the renderer. */
- #stage,
- body:not(.panel-hidden) #stage {
- inset: 0;
- right: 0;
- }
-
- #topbar {
- top: calc(8px + env(safe-area-inset-top));
- left: calc(10px + env(safe-area-inset-left));
- right: calc(10px + env(safe-area-inset-right));
- max-width: none;
- gap: 8px;
- }
-
- /* Let the title give way first (it can ellipsize); the buttons keep their
- labels and stay on one line. */
- #topbar h1 {
- font-size: 13px;
- white-space: nowrap;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .topbar-actions {
- flex: 1;
- flex-wrap: nowrap;
- justify-content: flex-end;
- gap: 6px;
- }
-
- /* Comfortable tap targets (>= 40px) in a row that still fits 360px. */
- #topbar button {
- font-size: 11px;
- padding: 9px 10px;
- min-height: 40px;
- }
-
- #btn-panel { min-width: 44px; }
-
- #viewport-hint { display: none; }
- #viewport-hint-touch {
- display: block;
- bottom: calc(14px + env(safe-area-inset-bottom));
- }
-
- #panel {
- top: auto;
- left: 0;
- right: 0;
- bottom: 0;
- width: auto;
- max-height: 65dvh;
- padding: 0 14px calc(20px + env(safe-area-inset-bottom));
- border-left: 0;
- border-top: 1px solid var(--border);
- border-radius: 18px 18px 0 0;
- box-shadow: 0 -6px 24px rgba(40, 20, 80, 0.18);
- }
-
- /* The grab handle: sticky at the top of the scroll area, so it stays grabbable
- however far the menu is scrolled, and `touch-action: none` so a drag on it
- resizes the sheet instead of scrolling it. */
- .panel-grip {
- display: flex;
- align-items: center;
- justify-content: center;
- position: sticky;
- top: 0;
- z-index: 6;
- height: 24px;
- margin: 0 -14px;
- background: var(--card);
- touch-action: none;
- cursor: grab;
- }
- .panel-grip::after {
- content: "";
- width: 44px;
- height: 5px;
- border-radius: 999px;
- background: #cfc6e4;
- }
- .panel-grip:active { cursor: grabbing; }
-
- body.panel-hidden #topbar { max-width: none; }
-
- /* The grab handle is sticky at the top of the sheet, so the action bar and the
- tabs start below it. */
- .panel-topbar { top: 24px; }
- .tabs { top: 58px; }
-}
-
-/* ----------------------------------------------------------------- sections */
-
-.tabs {
- position: sticky;
- /* Sits just below the sticky .panel-topbar (which is 34px tall). */
- top: 34px;
- z-index: 3;
- display: flex;
- align-items: flex-end;
- gap: 3px;
- padding: 2px 0 0;
- margin: -2px 0 8px;
- border-bottom: 2px solid var(--border);
- background: linear-gradient(var(--card) 72%, rgba(255, 255, 255, 0));
-}
-
-/* Each tab is a rounded-top tab sitting on the strip's baseline; only the icon
- shows, and the active one fills with the accent and covers the baseline. */
-.tab {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- font: inherit;
- padding: 9px 2px 7px;
- border: 1px solid var(--border);
- border-bottom: none;
- border-radius: 9px 9px 0 0;
- background: var(--accent-soft);
- color: var(--muted);
- cursor: pointer;
- white-space: nowrap;
-}
-
-.tab svg { display: block; width: 20px; height: 20px; }
-.tab:hover { color: var(--accent-2); border-color: var(--accent-2); }
-.tab.active {
- margin-bottom: -2px;
- padding-bottom: 9px;
- background: var(--accent);
- border-color: var(--accent);
- color: #fff;
-}
-.tab-panel[hidden] { display: none; }
-
-.details {
- border: 1px dashed var(--border);
- border-radius: 9px;
- padding: 0 9px;
- background: #fdfcff;
-}
-.details > summary {
- font-size: 11px;
- color: var(--muted);
- cursor: pointer;
- padding: 6px 0;
- list-style-position: inside;
-}
-.details[open] > summary { color: var(--accent); }
-.details-body { display: grid; gap: 8px; padding: 2px 0 9px; }
-
-.sec {
- background: #fff;
- border: 1px solid var(--border);
- border-radius: var(--radius);
- margin-bottom: 10px;
- overflow: hidden;
- box-shadow: 0 1px 2px rgba(53, 38, 90, 0.04);
-}
-
-.sec-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- width: 100%;
- padding: 10px 12px;
- margin: 0;
- border: 0;
- background: #fff;
- font: inherit;
- font-size: 12.5px;
- font-weight: 700;
- color: #4b3a72;
- text-align: left;
- cursor: pointer;
-}
-
-.sec-head .chev { color: var(--muted); font-size: 10px; transition: transform 0.18s; }
-.sec.closed .chev { transform: rotate(-90deg); }
-.sec-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
-.sec-title > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.sec-icon { flex: 0 0 auto; width: 16px; height: 16px; color: var(--accent); }
-.sec-body { padding: 2px 12px 12px; display: grid; gap: 9px; }
-.sec.closed .sec-body { display: none; }
-
-/* -------------------------------------------------------------------- rows */
-
-.row { display: grid; grid-template-columns: 82px minmax(0, 1fr); gap: 8px; align-items: center; }
-.row.wide { grid-template-columns: 1fr; gap: 5px; }
-.row > .label { font-size: 11.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.control { display: flex; align-items: center; gap: 7px; min-width: 0; }
-.hint { font-size: 11px; line-height: 1.55; color: var(--muted); }
-.hint b { color: #5a4a7d; }
-.row.is-off .control, .row.is-off > .label { opacity: 0.4; }
-.row.is-off .control input { cursor: default; }
-.subhead {
- font-size: 10.5px;
- font-weight: 700;
- letter-spacing: 0.06em;
- color: #8d80ab;
- text-transform: uppercase;
- margin-top: 3px;
-}
-.eye-group {
- border-top: 1px solid var(--border);
- margin-top: 4px;
- padding-top: 8px;
- display: grid;
- gap: 8px;
-}
-.eye-group-head {
- font-size: 11.5px;
- font-weight: 700;
- color: #6b5c92;
- display: flex;
- align-items: center;
- gap: 6px;
-}
-.eye-group-head .grp-icon { color: #8a7ab8; }
-.eye-group-body { display: grid; gap: 8px; }
-.select {
- flex: 1;
- min-width: 0;
- font: inherit;
- font-size: 11.5px;
- padding: 5px 6px;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: #fff;
- color: var(--text);
-}
-.stack { display: grid; gap: 6px; }
-.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
-.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
-
-input[type="range"] {
- flex: 1;
- min-width: 0;
- height: 18px;
- accent-color: var(--accent);
- cursor: pointer;
-}
-
-.num {
- flex: 0 0 auto;
- min-width: 40px;
- font-size: 11px;
- color: #5a4a7d;
- font-variant-numeric: tabular-nums;
- text-align: right;
-}
-
-.seg { display: inline-flex; flex-wrap: wrap; padding: 2px; gap: 2px; background: #f2eff9; border-radius: 999px; }
-.seg button {
- font: inherit;
- font-size: 11px;
- padding: 4px 9px;
- border: 0;
- border-radius: 999px;
- background: transparent;
- color: var(--muted);
- cursor: pointer;
- white-space: nowrap;
-}
-.seg button.active { background: #fff; color: var(--accent); box-shadow: 0 1px 3px rgba(53, 38, 90, 0.16); }
-
-.btn {
- font: inherit;
- font-size: 11.5px;
- padding: 6px 10px;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: #fff;
- color: var(--text);
- cursor: pointer;
- white-space: nowrap;
-}
-.btn:hover { border-color: var(--accent-2); }
-.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
-.btn.primary:hover { background: #6b45c2; }
-.btn:disabled { opacity: 0.45; cursor: default; }
-.buttons { display: flex; flex-wrap: wrap; gap: 6px; }
-
-.chk { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: #4b3a72; cursor: pointer; }
-.chk input { accent-color: var(--accent); width: 14px; height: 14px; }
-
-.pad-wrap { display: flex; gap: 10px; align-items: center; }
-.pad {
- position: relative;
- flex: 0 0 auto;
- width: 118px;
- height: 118px;
- border-radius: 14px;
- border: 1px solid var(--border);
- background:
- linear-gradient(#f0ecfa, #f0ecfa) 50% 0 / 1px 100% no-repeat,
- linear-gradient(#f0ecfa, #f0ecfa) 0 50% / 100% 1px no-repeat,
- #faf8ff;
- cursor: crosshair;
- touch-action: none;
-}
-.pad-dot {
- position: absolute;
- width: 13px;
- height: 13px;
- margin: -7px 0 0 -7px;
- border-radius: 50%;
- background: var(--accent);
- box-shadow: 0 1px 4px rgba(53, 38, 90, 0.35);
- pointer-events: none;
-}
-.pad-eye {
- position: absolute;
- inset: 0;
- margin: auto;
- width: 46px;
- height: 46px;
- border-radius: 50%;
- border: 1px dashed #d9d1ec;
- pointer-events: none;
-}
-
-/* Tail picker: a 3x3 grid where a dot's place is the direction it means, so
- the whole pad fits next to an 82px label even in a narrow panel. */
-.tail-pad {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- grid-template-rows: repeat(3, 1fr);
- place-items: center;
- flex: 0 0 auto;
- width: 128px;
- height: 128px;
- padding: 10px;
- border-radius: 14px;
- border: 1px solid var(--border);
- background: #faf8ff;
-}
-.tail-dot {
- width: 28px;
- height: 28px;
- padding: 0;
- border: 0;
- border-radius: 50%;
- background: #ded6f0;
- cursor: pointer;
- transition: background 0.12s, transform 0.1s;
-}
-.tail-dot:hover { background: var(--accent-2); transform: scale(1.1); }
-.tail-dot.active {
- background: var(--accent);
- box-shadow: 0 1px 4px rgba(53, 38, 90, 0.35);
-}
-
-.swatches { display: flex; gap: 5px; flex-wrap: wrap; }
-.swatch { width: 17px; height: 17px; border-radius: 5px; border: 1px solid rgba(43, 36, 51, 0.18); cursor: pointer; }
-input[type="color"] {
- width: 30px;
- height: 22px;
- padding: 0;
- border: 1px solid var(--border);
- border-radius: 6px;
- background: #fff;
- cursor: pointer;
-}
-
-.bone-list {
- max-height: 168px;
- overflow-y: auto;
- border: 1px solid var(--border);
- border-radius: 9px;
- padding: 4px;
- display: grid;
- gap: 2px;
- background: #fcfbff;
-}
-.bone-item {
- display: flex;
- justify-content: space-between;
- gap: 8px;
- padding: 4px 8px;
- border: 0;
- border-radius: 6px;
- background: transparent;
- font: inherit;
- font-size: 11.5px;
- color: var(--text);
- text-align: left;
- cursor: pointer;
-}
-.bone-item:hover { background: #f4f0fd; }
-.bone-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
-.bone-item small { color: var(--muted); font-size: 10.5px; }
-
-/* --------------------------------------------------------------------- 擬音 */
-
-/*
- * The stamps are painted onto the caption canvas, which is `pointer-events:
- * none`, so each one gets its own transparent drag box on top - the same trick
- * the bubbles use. The box follows the stamp's rotation. Its z-index sits below
- * the caption handles (3), because the bubbles are drawn over the stamps and so
- * must stay grabbable where the two overlap.
- */
-.gion-handle {
- position: absolute;
- display: none;
- cursor: move;
- touch-action: none;
- pointer-events: auto;
- z-index: 2;
- border: 1px dashed transparent;
- border-radius: 4px;
-}
-.gion-handle.selected {
- border-color: var(--accent);
- box-shadow: 0 0 0 2px rgba(123, 83, 209, 0.18);
-}
-
-.gion-list { display: grid; gap: 10px; }
-.gion-item-head { display: flex; align-items: center; gap: 8px; }
-.gion-thumb {
- flex: 0 0 auto;
- width: 84px;
- height: 56px;
- background: #f3f0fa;
- border: 1px solid var(--border);
- border-radius: 8px;
- cursor: pointer;
-}
-.gion-item.active .gion-thumb { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
-
-/* The picker covers the whole window, so even a big sheet has room to draw on. */
-.gion-picker {
- position: fixed;
- inset: 0;
- z-index: 95;
- display: flex;
- flex-direction: column;
- gap: 10px;
- padding: 16px;
- background: rgba(24, 16, 44, 0.86);
- backdrop-filter: blur(4px);
-}
-.gion-picker-bar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 14px;
- flex-wrap: wrap;
- color: #fff;
-}
-.gion-picker-title { font-size: 13.5px; font-weight: 700; }
-.gion-picker-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
-.gion-picker-actions .hint { color: #cfc6e6; }
-.gion-picker-board {
- position: relative;
- flex: 1;
- min-height: 0;
- overflow: hidden;
- border-radius: 12px;
- /* A checkerboard reads as "transparent", so a PNG sheet's holes are obvious. */
- background:
- linear-gradient(45deg, #2b2344 25%, transparent 25%) 0 0 / 22px 22px,
- linear-gradient(-45deg, #2b2344 25%, transparent 25%) 0 11px / 22px 22px,
- linear-gradient(45deg, transparent 75%, #2b2344 75%) 11px -11px / 22px 22px,
- linear-gradient(-45deg, transparent 75%, #2b2344 75%) -11px 0 / 22px 22px,
- #241d3a;
- cursor: crosshair;
- touch-action: none;
-}
-.gion-picker-sheet {
- position: absolute;
- display: block;
- user-select: none;
- -webkit-user-drag: none;
- pointer-events: none;
-}
-.gion-picker-marquee {
- position: absolute;
- display: none;
- border: 1.5px solid #fff;
- background: rgba(180, 152, 255, 0.28);
- box-shadow: 0 0 0 9999px rgba(20, 12, 40, 0.42);
- pointer-events: none;
-}
-
-/* -------------------------------------------------------------------- toast */
-
-#notice {
- position: fixed;
- left: 50%;
- top: 14px;
- transform: translateX(-50%);
- z-index: 95;
- max-width: min(560px, 90vw);
- padding: 10px 16px;
- border-radius: 12px;
- background: #fff4e5;
- border: 1px solid #f0c48a;
- color: #7a4a12;
- font-size: 12px;
- line-height: 1.65;
- white-space: pre-line;
- box-shadow: 0 6px 20px rgba(43, 36, 51, 0.15);
-}
-
-#notice[hidden] { display: none; }
-
-/* ------------------------------------------------ 起動時のご利用について */
-#start-notice {
- position: fixed;
- inset: 0;
- z-index: 96;
- display: grid;
- place-items: center;
- padding: 24px;
- background: rgba(43, 36, 51, 0.5);
- backdrop-filter: blur(3px);
-}
-#start-notice[hidden] { display: none; }
-
-.start-sheet {
- width: min(560px, 100%);
- max-height: 86vh;
- overflow: auto;
- padding: 22px 26px;
- border-radius: 16px;
- background: #fff;
- color: #2b2433;
- box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
- font-size: 13.5px;
- line-height: 1.75;
-}
-.start-sheet h2 { margin: 0 0 12px; font-size: 17px; }
-.start-sheet p { margin: 0 0 10px; }
-.start-sheet ul { margin: 0 0 12px; padding-left: 1.3em; }
-.start-actions {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- flex-wrap: wrap;
- margin-top: 16px;
-}
-.start-skip { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #6b5d7a; }
-.start-ok {
- border: 0;
- border-radius: 8px;
- padding: 9px 22px;
- background: #6b4ea8;
- color: #fff;
- font-size: 13.5px;
- cursor: pointer;
-}
-
-#toast {
- position: fixed;
- left: 50%;
- bottom: 26px;
- transform: translate(-50%, 14px);
- z-index: 70;
- padding: 8px 16px;
- border-radius: 999px;
- background: rgba(43, 36, 51, 0.9);
- color: #fff;
- font-size: 12px;
- opacity: 0;
- pointer-events: none;
- transition: opacity 0.2s, transform 0.2s;
-}
-#toast.show { opacity: 1; transform: translate(-50%, 0); }
-
-/* ------------------------------------------------------------------ loading */
-
-#loading {
- position: fixed;
- inset: 0;
- z-index: 80;
- display: grid;
- place-items: center;
- background: rgba(244, 242, 250, 0.94);
- transition: opacity 0.35s;
-}
-#loading.done { opacity: 0; pointer-events: none; }
-
-.loading-box { text-align: center; max-width: 520px; padding: 24px; }
-.loading-spinner {
- width: 34px;
- height: 34px;
- margin: 0 auto 14px;
- border-radius: 50%;
- border: 3px solid #e0d8f2;
- border-top-color: var(--accent);
- animation: spin 0.9s linear infinite;
-}
-@keyframes spin { to { transform: rotate(360deg); } }
-#loading-text { margin: 0; font-size: 13px; color: #5a4a7d; }
-#loading-error {
- margin: 12px 0 0;
- font-size: 12px;
- line-height: 1.7;
- color: #b3261e;
- white-space: pre-wrap;
- text-align: left;
-}
-
-/* --------------------------------------------------------------------- help */
-
-#help {
- position: fixed;
- inset: 0;
- z-index: 90;
- display: grid;
- place-items: center;
- padding: 24px;
- background: rgba(43, 36, 51, 0.42);
- backdrop-filter: blur(3px);
-}
-#help[hidden] { display: none; }
-
-.help-sheet {
- position: relative;
- width: min(760px, 100%);
- max-height: min(84vh, 900px);
- overflow-y: auto;
- padding: 26px 30px 32px;
- background: #fff;
- border-radius: 18px;
- box-shadow: 0 24px 60px rgba(20, 12, 40, 0.3);
- font-size: 13px;
- line-height: 1.75;
-}
-.help-sheet h2 { margin: 0 0 14px; font-size: 17px; color: #4b3a72; }
-.help-sheet h3 { margin: 20px 0 6px; font-size: 13.5px; color: var(--accent); }
-.help-sheet ul { margin: 0; padding-left: 1.25em; }
-.help-sheet li { margin: 3px 0; }
-.help-sheet code { background: #f3f0fa; padding: 1px 5px; border-radius: 5px; font-size: 12px; }
-.help-sheet .keys { list-style: none; padding: 0; display: grid; gap: 4px; }
-.help-close {
- position: absolute;
- top: 12px;
- right: 14px;
- width: 32px;
- height: 32px;
- border: 0;
- border-radius: 50%;
- background: #f3f0fa;
- color: #5a4a7d;
- font-size: 19px;
- line-height: 1;
- cursor: pointer;
-}
-.help-close:hover { background: #e8e2f7; }
-
-kbd {
- display: inline-block;
- min-width: 20px;
- padding: 1px 6px;
- border: 1px solid var(--border);
- border-bottom-width: 2px;
- border-radius: 5px;
- background: #faf9fe;
- font: inherit;
- font-size: 11px;
- text-align: center;
-}
-
-/* The always-visible action bar above the tabs: 元に戻す / やり直す / すべてランダム.
- Sticky, so it (and the tabs below it) stay on screen while the panel scrolls. */
-.panel-topbar {
- position: sticky;
- top: 0;
- z-index: 5;
- background: #fdfcff;
- display: flex;
- gap: 4px;
- justify-content: flex-end;
- padding: 4px 8px 2px;
-}
-.panel-topbar .icon-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 32px;
- height: 28px;
- padding: 0;
- border: 1px solid var(--border);
- border-radius: 8px;
- background: #fff;
- color: #2b2433;
- cursor: pointer;
-}
-.panel-topbar .icon-btn:hover { background: #f4f0fb; }
-.panel-topbar .icon-btn:active { transform: translateY(1px); }
diff --git a/bluebey-studio/src/styles.js b/bluebey-studio/src/styles.js
deleted file mode 100644
index 2e4f62d..0000000
--- a/bluebey-studio/src/styles.js
+++ /dev/null
@@ -1,457 +0,0 @@
-import * as THREE from 'three';
-
-/**
- * Render styles for the body meshes, plus the outline pass.
- *
- * real the PBR materials exactly as authored in the GLB
- * flat two-tone toon shading, the classic mascot look
- * lineart white paper + lines, the face artwork drawn as strokes
- * outline nothing but the lines, kept invisible via colorWrite = false so the
- * result composites on top of anything (used by the SVG export)
- *
- * The lines come from two systems, each used where it is strong (see
- * MODEL-GUIDE.md §5): every mesh gets an inverted-hull copy, and the thin
- * overlapping leaves are handed to the screen-space pass instead (outline.js),
- * which the caller arranges with `setHullHidden`.
- */
-
-export const STYLE_DEFS = [
- { value: 'real', label: 'リアル' },
- { value: 'flat', label: 'フラット' },
- { value: 'lineart', label: '線画' },
-];
-
-// 'outline' is still used internally by the SVG export (lines on a transparent
-// background), it is just no longer offered as a viewing style.
-const LINE_STYLES = new Set(['lineart', 'outline']);
-
-export const isLineStyle = (value) => LINE_STYLES.has(value);
-
-/** Parts that need no outline at all. */
-const OUTLINE_SKIP = new Set();
-
-/**
- * Parts whose outline is only useful in the line-art styles.
- *
- * Empty now: the leaves used to be listed here, but the line styles get their
- * leaf lines from the `Vein` faces themselves (see OUTLINE_SHADED_ONLY), so the
- * hull is the wrong tool for them in either direction.
- */
-const OUTLINE_LINE_ONLY = new Set();
-
-/**
- * Parts that should be outlined in the SHADED styles only.
- *
- * The inverted hull draws an outline by expanding a copy of the mesh along its
- * normals and rendering the back faces. That works on one big round body, but
- * the leaf skirt is a dozen thin shells lying on top of each other: each hull's
- * far side shows through its neighbours, which is what produced the faint
- * overlapping hairlines. In the SVG pass the leaves get their line from their
- * own `Vein` faces (filled with the ink colour) instead, so the hull is dropped
- * there and kept for the cartoon styles, where it does read as an edge.
- */
-const OUTLINE_SHADED_ONLY = new Set(['vein']);
-
-/**
- * Per-part outline thickness, as a fraction of the global width.
- *
- * The hull is offset by a fixed distance in world units, so the right factor
- * depends on how fine a part's mesh is compared with that offset:
- *
- * - the nose is only ~0.24 units across, and the offset that looks right on the
- * body reads as a thick ring on it, hence the small factor. In the line-art
- * styles it is the other way round (a hairline looks like dust on paper), so
- * the nose gets a fuller line there - see OUTLINE_SCALE_LINE.
- * - the leaves need a *larger* factor, which only shows up in the "hull only"
- * style: the 13 blades overlap and interpenetrate, so each one's expanded
- * shell cuts across its neighbours and the line breaks up. A thicker hull
- * merges those scratches back into a band. Making the leaves thicker does NOT
- * fix it - measured, see MODEL-GUIDE.md §5-4 - because the overlaps are the
- * blocker, not the leaf's own thickness.
- */
-const OUTLINE_SCALE = { nose: 0.4, leaf: 1.7, vein: 2.2 };
-
-/** Fuller lines for the paper-and-ink styles, where a hairline reads as dust. */
-const OUTLINE_SCALE_LINE = { nose: 0.9, leaf: 1.9, vein: 2.4 };
-
-/**
- * Parts whose outline should only survive where the part itself sticks out past
- * the body. Their hull is drawn first with depth testing off, so everything the
- * body covers paints over it: the nose then only gets a line in the views where
- * it actually pokes out of the silhouette, instead of a ring around it - which is
- * what the original artwork does, the nose reads by its own colour there.
- *
- * In the line-art styles there is no colour to read it by (the nose is paper on
- * paper), so this is switched off and the nose goes to the screen-space pass
- * instead - see `screenParts` in main.js.
- */
-const OUTLINE_SILHOUETTE_ONLY = new Set(['nose']);
-
-export class Styles {
- constructor({ meshes, paper = '#ffffff', outlineColor = '#2a1e33', outlineWidth = 0.022 }) {
- this.meshes = meshes;
- this.paper = paper;
- this.style = 'real';
- this.outlineEnabled = true;
- /**
- * Source meshes whose hull outline must stay off, because another method
- * (the screen-space pass) is drawing them. Empty = every hull is available.
- * Only the leaves ever land here: see MODEL-GUIDE.md §5.
- */
- this.hullHidden = new Set();
-
- this.originals = new Map();
- for (const mesh of meshes) {
- this.originals.set(mesh, mesh.material);
- mesh.castShadow = true;
- mesh.receiveShadow = false;
- }
-
- this.gradientMap = makeGradientMap();
- this.toonMaterials = new Map();
- this.paperMaterial = new THREE.MeshBasicMaterial({ color: paper, toneMapped: false });
- // A hat is not flat: in a line drawing a white hat on a white head merges
- // into one shape, so hats get a light tone instead of paper (see addMesh).
- // The tone is per-mesh (`toneMeshes`, mesh -> material) and the amount it
- // steps from the paper towards the ink is cached by strength in
- // `toneMaterials`, so a part on a same-coloured neighbour (a hat band, a
- // pencil's lead) can ask for a darker fill and its seam then reads.
- this.toneMaterials = new Map();
- /** Runtime meshes that want a tone fill rather than paper in 線画. */
- this.toneMeshes = new Map();
- this.invisibleMaterial = new THREE.MeshBasicMaterial({ colorWrite: false, depthWrite: true });
-
- this.outlineWidth = outlineWidth;
- this.outlineColor = outlineColor;
- /** scale -> { material, uniform }, so the width slider updates all of them. */
- this.hullMaterials = new Map();
- this.outlineMeshes = this.createOutlines();
- this.setStyle('real');
- }
-
- /**
- * The part kind, taken from the material the GLB shipped with.
- *
- * It must be read from `originals`, not from `mesh.material`: `setStyle`
- * replaces the materials, and the replacements (paper, ink, toon) carry no
- * name, so after the first swap every part would look like an unknown one and
- * the leaf/nose special cases would silently stop applying.
- */
- kindOf(mesh) {
- return (this.originals.get(mesh)?.name ?? '').toLowerCase();
- }
-
- hullMaterialFor(scale, overlay = false) {
- const key = `${scale}:${overlay ? 'overlay' : 'solid'}`;
- let entry = this.hullMaterials.get(key);
- if (!entry) {
- const uniform = { value: this.outlineWidth * scale };
- const material = makeHullMaterial(this.outlineColor, uniform);
- if (overlay) {
- // Depth-*tested*, but writing none: the hull is drawn before the body
- // (renderOrder -1), so the body still paints over its interior and the
- // nose only gets a line where it pokes out of the silhouette. Testing
- // (rather than ignoring) depth is what lets the 見えない壁 hide it too -
- // with the test off, a nose buried in the wall left a filled blob,
- // because nothing was left to paint over the hull's inside.
- material.depthTest = true;
- material.depthWrite = false;
- }
- entry = { uniform, material, scale };
- this.hullMaterials.set(key, entry);
- }
- return entry.material;
- }
-
- /**
- * The flat fill a runtime mesh wears instead of paper in 線画, cached by how far
- * it steps from the paper towards the ink. `userData.tone` on the mesh (set by
- * the builder) carries the amount; the hat default comes from `options.tone`.
- */
- toneMaterialFor(strength = DEFAULT_TONE) {
- const key = String(strength);
- let material = this.toneMaterials.get(key);
- if (!material) {
- material = new THREE.MeshBasicMaterial({
- color: toneOf(this.paper, strength),
- toneMapped: false,
- });
- material.userData.tone = strength;
- this.toneMaterials.set(key, material);
- }
- return material;
- }
-
- createOutlines() {
- const hulls = [];
- for (const mesh of this.meshes) {
- const kind = this.kindOf(mesh);
- if (OUTLINE_SKIP.has(kind)) continue;
- const overlay = OUTLINE_SILHOUETTE_ONLY.has(kind);
- const material = this.hullMaterialFor(this.scaleFor(kind), overlay);
- const hull = mesh.isSkinnedMesh
- ? new THREE.SkinnedMesh(mesh.geometry, material)
- : new THREE.Mesh(mesh.geometry, material);
- hull.name = `${mesh.name || 'mesh'}:outline`;
- hull.position.copy(mesh.position);
- hull.quaternion.copy(mesh.quaternion);
- hull.scale.copy(mesh.scale);
- hull.frustumCulled = false;
- hull.castShadow = false;
- hull.receiveShadow = false;
- if (overlay) hull.renderOrder = -1;
- hull.userData.kind = kind;
- hull.userData.source = mesh;
- hull.userData.lineOnly = OUTLINE_LINE_ONLY.has(kind);
- hull.userData.shadedOnly = OUTLINE_SHADED_ONLY.has(kind);
- if (hull.isSkinnedMesh) hull.bind(mesh.skeleton, mesh.bindMatrix);
- mesh.parent.add(hull);
- hulls.push(hull);
- }
- return hulls;
- }
-
- /**
- * Register a mesh that is built at runtime (a hat), so it follows the render
- * styles and gets an outline hull like the GLB parts. `removeMesh` undoes it.
- *
- * `options.tone` gives the mesh the light tone fill in 線画 instead of paper,
- * for parts whose shape would otherwise merge with the body (the hats). A
- * number instead of `true` picks how far to step towards the ink, and the
- * builder can override it per mesh with `userData.tone` - which is how a part
- * draws its seam against a same-coloured neighbour.
- *
- * `options.lineOnly` keeps the hull to the line-art styles, so a runtime mesh
- * (a hat, a prop) gets its ink outline in 線画 but none in リアル / フラット.
- */
- addMesh(mesh, options = {}) {
- if (!mesh || this.originals.has(mesh)) return;
- this.originals.set(mesh, mesh.material);
- this.meshes.push(mesh);
- const tone = mesh.userData.tone ?? options.tone;
- if (tone) this.toneMeshes.set(mesh, this.toneMaterialFor(tone === true ? DEFAULT_TONE : tone));
- const kind = this.kindOf(mesh);
- if (!OUTLINE_SKIP.has(kind)) {
- const overlay = OUTLINE_SILHOUETTE_ONLY.has(kind);
- const hull = new THREE.Mesh(mesh.geometry, this.hullMaterialFor(this.scaleFor(kind), overlay));
- hull.name = `${mesh.name || 'mesh'}:outline`;
- hull.position.copy(mesh.position);
- hull.quaternion.copy(mesh.quaternion);
- hull.scale.copy(mesh.scale);
- hull.frustumCulled = false;
- hull.castShadow = false;
- hull.receiveShadow = false;
- if (overlay) hull.renderOrder = -1;
- hull.userData.kind = kind;
- hull.userData.source = mesh;
- hull.userData.lineOnly = options.lineOnly === true || OUTLINE_LINE_ONLY.has(kind);
- hull.userData.shadedOnly = OUTLINE_SHADED_ONLY.has(kind);
- mesh.parent?.add(hull);
- this.outlineMeshes.push(hull);
- mesh.userData.hull = hull;
- }
- this.setStyle(this.style);
- }
-
- /** Take a runtime mesh (and its outline hull) back out. */
- removeMesh(mesh) {
- if (!mesh || !this.originals.has(mesh)) return;
- const original = this.originals.get(mesh);
- const hull = mesh.userData.hull;
- if (hull) {
- hull.removeFromParent();
- const i = this.outlineMeshes.indexOf(hull);
- if (i >= 0) this.outlineMeshes.splice(i, 1);
- delete mesh.userData.hull;
- }
- // Put the mesh's own material back before dropping it: while a line style is
- // on it is wearing the *shared* paper/tone material, and the caller is about
- // to dispose it - which would blank every part using it.
- if (original) mesh.material = original;
- this.originals.delete(mesh);
- this.toneMeshes.delete(mesh);
- const j = this.meshes.indexOf(mesh);
- if (j >= 0) this.meshes.splice(j, 1);
- }
-
- /** The outline factor for a part in the current style (line art wants more). */
- scaleFor(kind) {
- const line = LINE_STYLES.has(this.style);
- const table = line ? OUTLINE_SCALE_LINE : OUTLINE_SCALE;
- return table[kind] ?? OUTLINE_SCALE[kind] ?? 1;
- }
-
- setStyle(value) {
- this.style = value;
- const line = LINE_STYLES.has(value);
- for (const mesh of this.meshes) {
- const original = this.originals.get(mesh);
- let material;
- if (line) {
- // Paper on paper in `lineart`; a light tone for hats, which would
- // otherwise merge into the head; invisible in `outline`, which is the pass
- // the SVG trace reads. Either way every edge - the body, the nose, and
- // the leaves - is drawn by the screen-space outline, so no part needs a
- // material trick of its own any more.
- material = value === 'lineart'
- ? (this.toneMeshes.get(mesh) ?? this.paperMaterial)
- : this.invisibleMaterial;
- } else if (value === 'flat') {
- material = this.toonFor(original);
- } else {
- material = original;
- }
- if (mesh.material !== material) mesh.material = material;
- }
-
- // The nose only keeps a line where it pokes out of the body in the shaded
- // styles; in a line drawing it is handed to the screen-space pass instead
- // (see `screenParts` in main.js), because a hull cannot ring a bump that sits
- // flush on the surface it is drawn on - the expanded shell lands *inside* the
- // body and loses the depth test.
- for (const hull of this.outlineMeshes) {
- const kind = hull.userData.kind;
- const silhouetteOnly = OUTLINE_SILHOUETTE_ONLY.has(kind) && !line;
- hull.material = this.hullMaterialFor(this.scaleFor(kind), silhouetteOnly);
- hull.renderOrder = silhouetteOnly ? -1 : 0;
- }
-
- // Lines are the whole point of the line-art styles.
- this.setOutlineVisible(line ? true : this.outlineEnabled);
- }
-
- /**
- * Hand a set of parts to the screen-space outline, or take them back.
- *
- * The hull and the screen-space pass each have a shape they cannot draw: a hull
- * cannot outline a thin closed shell (the leaves), and the screen-space pass
- * draws a stepped line because it works on the pixel grid. So the leaves go to
- * the screen-space pass and everything else keeps its hull, which is drawn from
- * the geometry and therefore comes out smooth.
- */
- setHullHidden(meshes) {
- this.hullHidden = new Set(meshes ?? []);
- this.setStyle(this.style);
- }
-
- toonFor(original) {
- let material = this.toonMaterials.get(original);
- if (!material) {
- material = new THREE.MeshToonMaterial({
- color: original.color ? original.color.clone() : new THREE.Color(0xffffff),
- map: original.map ?? null,
- vertexColors: original.vertexColors === true,
- gradientMap: this.gradientMap,
- side: original.side,
- transparent: original.transparent === true,
- alphaTest: original.alphaTest ?? 0,
- depthWrite: original.depthWrite !== false,
- });
- this.toonMaterials.set(original, material);
- }
- return material;
- }
-
- setPaper(color) {
- this.paper = color;
- this.paperMaterial.color.set(color);
- for (const material of this.toneMaterials.values()) {
- material.color.copy(toneOf(color, material.userData.tone));
- }
- }
-
- setOutlineVisible(visible) {
- const lineMode = LINE_STYLES.has(this.style);
- for (const hull of this.outlineMeshes) {
- const lineOnly = hull.userData.lineOnly === true;
- const shadedOnly = hull.userData.shadedOnly === true;
- hull.visible = visible
- && (!lineOnly || lineMode)
- && !(shadedOnly && lineMode)
- && !this.hullHidden.has(hull.userData.source);
- }
- }
-
- setOutlineEnabled(enabled) {
- this.outlineEnabled = enabled;
- if (!LINE_STYLES.has(this.style)) this.setOutlineVisible(enabled);
- }
-
- setOutlineWidth(width) {
- this.outlineWidth = width;
- for (const entry of this.hullMaterials.values()) entry.uniform.value = width * entry.scale;
- }
-
- setOutlineColor(color) {
- this.outlineColor = color;
- for (const entry of this.hullMaterials.values()) entry.material.color.set(color);
- }
-
- /**
- * The toon materials are cached copies of the originals, so a colour theme that
- * edits `original.color` has to be copied across or the flat style keeps
- * showing the old colour. See src/look.js.
- */
- refreshColors() {
- for (const [mesh, original] of this.originals) {
- const toon = this.toonMaterials.get(original);
- if (toon && original.color) toon.color.copy(original.color);
- // The outline hull is a colour of its own, so it is left alone.
- void mesh;
- }
- return this;
- }
-
- dispose() {
- for (const hull of this.outlineMeshes) {
- hull.removeFromParent();
- hull.skeleton = null;
- }
- this.outlineMeshes = [];
- for (const entry of this.hullMaterials.values()) entry.material.dispose();
- this.hullMaterials.clear();
- this.paperMaterial.dispose();
- for (const material of this.toneMaterials.values()) material.dispose();
- this.toneMaterials.clear();
- this.invisibleMaterial.dispose();
- this.gradientMap.dispose();
- for (const material of this.toonMaterials.values()) material.dispose();
- this.toonMaterials.clear();
- }
-}
-
-/** How far the hat default steps from the paper towards the ink in 線画. */
-const DEFAULT_TONE = 0.14;
-
-/** A step towards the ink, for the flat fill a runtime mesh gets in 線画. */
-function toneOf(paper, strength = DEFAULT_TONE) {
- return new THREE.Color(paper).lerp(new THREE.Color('#2a1e33'), strength);
-}
-
-/** A 3-step ramp gives crisper cartoon bands than the shader's default. */
-function makeGradientMap() {
- const steps = new Uint8Array([90, 165, 255]);
- const texture = new THREE.DataTexture(steps, steps.length, 1, THREE.RedFormat);
- texture.minFilter = THREE.NearestFilter;
- texture.magFilter = THREE.NearestFilter;
- texture.generateMipmaps = false;
- texture.needsUpdate = true;
- return texture;
-}
-
-function makeHullMaterial(color, uniform) {
- const material = new THREE.MeshBasicMaterial({
- color,
- side: THREE.BackSide,
- toneMapped: false,
- });
- material.onBeforeCompile = (shader) => {
- shader.uniforms.uOutline = uniform;
- shader.vertexShader = `uniform float uOutline;\n${shader.vertexShader}`.replace(
- '#include ',
- '#include \n\ttransformed += normal * uOutline;',
- );
- };
- material.customProgramCacheKey = () => 'bluebey-outline-hull';
- return material;
-}
diff --git a/bluebey-studio/src/textOutlines.js b/bluebey-studio/src/textOutlines.js
deleted file mode 100644
index 334a92d..0000000
--- a/bluebey-studio/src/textOutlines.js
+++ /dev/null
@@ -1,432 +0,0 @@
-import opentype from 'opentype.js';
-
-/**
- * Caption text as outlines.
- *
- * A caption drawn with changes shape in every viewer, because the glyphs
- * come from whatever font that viewer happens to have. The studio ships one
- * subset font (M PLUS Rounded 1c) and this module turns the caption into plain
- * SVG paths instead, so an exported SVG looks the same everywhere and stays
- * editable as vector art.
- *
- * The module runs unchanged in the browser and in Node: it touches no DOM and no
- * Node built-in at module scope, so the tests can parse the font straight from
- * disk. `loadFont` is the only asynchronous export; everything else is a pure
- * function of the font, the text and the options.
- *
- * Widths are summed one character at a time, without kerning between them. That
- * costs a fraction of a pixel on Latin pairs but keeps measuring and wrapping in
- * exact agreement, which matters more for a short caption.
- */
-
-/** Families tried in order when a caption falls back to live . */
-const FALLBACK_FAMILIES = [
- 'M PLUS Rounded 1c',
- 'Hiragino Maru Gothic ProN',
- 'Yu Gothic',
- 'Meiryo',
- 'sans-serif',
-];
-
-/** CSS keywords that must stay unquoted inside a font stack. */
-const GENERIC_FAMILIES = new Set([
- 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
- 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'math', 'emoji',
- 'fangsong',
-]);
-
-/**
- * Closing marks and brackets that should not start a line.
- *
- * A full kinsoku table needs per-font metrics; this short list covers the marks
- * a caption actually uses, and dragging the preceding character down is enough
- * to fix the rest.
- */
-const CLOSING_PUNCTUATION = new Set([
- '、', '。', ',', '.', ':', ';', '!', '?',
- ')', ']', '}', '〕', '〉', '》', '」', '』', '】', '〗', '〙', '〟',
- '”', '’', '⦆', '»',
-]);
-
-const SPACE = /\s/;
-const LINE_BREAK = /\r\n|\r|\n/;
-
-const DEFAULT_FONT_SIZE = 16;
-const DEFAULT_LINE_HEIGHT = 1.4;
-
-/**
- * Parsed fonts, keyed by URL. The stored value is the in-flight promise, so two
- * callers asking for the same URL share one request instead of loading twice.
- */
-const fontCache = new Map();
-
-/** Family name of the most recent font from `loadFont`, read by `captionFontStack`. */
-let loadedFamily = null;
-
-/**
- * Load a font and remember it under `url`.
- *
- * Resolves to an `opentype.Font`; rejects if the font cannot be fetched or
- * parsed, and does not cache that failure, so a later call can retry.
- *
- * @param {string} url URL (browser) or file path (Node) of the font
- * @returns {Promise}
- */
-export async function loadFont(url) {
- if (!url) throw new Error('loadFont: a font URL is required');
- if (fontCache.has(url)) return fontCache.get(url);
-
- const pending = opentype.load(url).then((font) => {
- loadedFamily = familyNameOf(font) ?? loadedFamily;
- return font;
- });
- // A rejected promise left in the cache would make every retry fail forever.
- pending.catch(() => fontCache.delete(url));
- fontCache.set(url, pending);
- return pending;
-}
-
-/**
- * Synchronous twin of `loadFont` for tests and offline use.
- *
- * @param {ArrayBuffer} arrayBuffer font bytes; a typed-array view is accepted too
- * @returns {import('opentype.js').Font}
- */
-export function parseFont(arrayBuffer) {
- // `opentype.parse` needs a real ArrayBuffer, so unwrap a view (for example a
- // Node Buffer) before handing it over.
- const view = ArrayBuffer.isView(arrayBuffer) ? arrayBuffer : null;
- const buffer = view
- ? view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength)
- : arrayBuffer;
- return opentype.parse(buffer);
-}
-
-/**
- * Does the font really cover every character of `text`?
- *
- * Callers use this to decide between outlines and live text: a single missing
- * glyph means the caption would come out with a hole in it, so the answer is
- * then `false`. Newlines carry no glyph and are ignored.
- *
- * @param {import('opentype.js').Font} font
- * @param {string} text
- * @returns {boolean}
- */
-export function hasGlyphs(font, text) {
- if (!font || typeof text !== 'string') return false;
- for (const ch of text) {
- if (ch === '\n' || ch === '\r') continue;
- if (font.charToGlyphIndex(ch) === 0) return false; // 0 is .notdef
- }
- return true;
-}
-
-/**
- * Size one string in pixels.
- *
- * The width is the widest line, so the same call works for a single line and for
- * text that already contains breaks. `ascent` is above the baseline and
- * `descent` below it, both in the font's own sign convention.
- *
- * @param {import('opentype.js').Font} font
- * @param {string} text
- * @param {number} [fontSize]
- * @returns {{width: number, ascent: number, descent: number}}
- */
-export function measureText(font, text, fontSize = DEFAULT_FONT_SIZE) {
- const size = Number.isFinite(fontSize) ? fontSize : DEFAULT_FONT_SIZE;
- const scale = size / (font.unitsPerEm || 1000);
-
- let width = 0;
- for (const line of String(text ?? '').split(LINE_BREAK)) {
- width = Math.max(width, measureLine(font, line, size));
- }
- return { width, ascent: font.ascender * scale, descent: font.descender * scale };
-}
-
-/**
- * Wrap `text` into lines and report the block's size.
- *
- * Rules, in the order they apply:
- * - an explicit `\n` always ends a line;
- * - a Latin word breaks only at a space, never in the middle;
- * - CJK characters break anywhere, one break opportunity per character;
- * - a piece that is too wide on its own still gets a line (nothing is dropped);
- * - a line is not allowed to start with closing punctuation when dragging the
- * previous character down would avoid it.
- *
- * @param {import('opentype.js').Font} font
- * @param {string} text
- * @param {object} [options]
- * @param {number} [options.fontSize=16]
- * @param {number|null} [options.maxWidth=0] 0/null/undefined means "no wrapping"
- * @param {number} [options.lineHeight=1.4] multiplier of `fontSize`
- * @param {'left'|'center'|'right'} [options.align='left']
- * @returns {{
- * lines: Array<{text: string, width: number}>,
- * width: number, height: number, lineHeight: number,
- * fontSize: number, align: string,
- * }}
- */
-export function layoutText(font, text, options = {}) {
- const opts = options ?? {};
- const fontSize = Number.isFinite(opts.fontSize) ? opts.fontSize : DEFAULT_FONT_SIZE;
- const lineHeight = Number.isFinite(opts.lineHeight) ? opts.lineHeight : DEFAULT_LINE_HEIGHT;
- const align = opts.align === 'center' || opts.align === 'right' ? opts.align : 'left';
- const maxWidth = opts.maxWidth;
- const wrapWidth = Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : Infinity;
-
- const lines = [];
- for (const paragraph of String(text ?? '').split(LINE_BREAK)) {
- const atoms = tokenise(font, paragraph, fontSize);
- for (const wrapped of wrapAtoms(atoms, wrapWidth)) {
- lines.push({
- text: wrapped.map((atom) => atom.text).join(''),
- width: wrapped.reduce((sum, atom) => sum + atom.width, 0),
- });
- }
- }
-
- let width = 0;
- for (const line of lines) width = Math.max(width, line.width);
-
- const height = lines.length * fontSize * lineHeight;
- return { lines, width, height, lineHeight, fontSize, align };
-}
-
-/**
- * Convert a layout to one SVG path `d` string.
- *
- * `x`/`y` is the top-left corner of the text block. Each line sits on its own
- * baseline, placed with half-leading so a line box of `fontSize * lineHeight`
- * surrounds the glyphs evenly, and shifted sideways by `layout.align`.
- *
- * Characters the font does not cover are skipped rather than drawn as .notdef,
- * and a line whose outline cannot be built cleanly is dropped, so the result
- * never carries `NaN` into the document.
- *
- * @param {import('opentype.js').Font} font
- * @param {object} layout value returned by `layoutText`
- * @param {object} [options]
- * @param {number} [options.x=0]
- * @param {number} [options.y=0]
- * @param {number} [options.round=2] decimal places in the output
- * @returns {string}
- */
-export function textToPathData(font, layout, options = {}) {
- const opts = options ?? {};
- const x = Number.isFinite(opts.x) ? opts.x : 0;
- const y = Number.isFinite(opts.y) ? opts.y : 0;
- const round = Number.isFinite(opts.round) ? Math.max(0, Math.floor(opts.round)) : 2;
-
- if (!font || !layout || !Array.isArray(layout.lines) || layout.lines.length === 0) return '';
-
- const fontSize = Number.isFinite(layout.fontSize) ? layout.fontSize : DEFAULT_FONT_SIZE;
- const lineHeight = Number.isFinite(layout.lineHeight) ? layout.lineHeight : DEFAULT_LINE_HEIGHT;
- const blockWidth = Number.isFinite(layout.width) ? layout.width : 0;
-
- const step = fontSize * lineHeight;
- const scale = fontSize / (font.unitsPerEm || 1000);
- const ascent = font.ascender * scale;
- const descent = -font.descender * scale; // depth below the baseline, positive
- const halfLeading = (step - (ascent + descent)) / 2;
-
- const parts = [];
- for (let i = 0; i < layout.lines.length; i++) {
- const line = layout.lines[i];
- const drawable = drawableText(font, line.text);
- if (!drawable) continue;
-
- const baseline = y + i * step + halfLeading + ascent;
- const lineX = x + alignOffset(layout.align, blockWidth, line.width);
- const d = font.getPath(drawable, lineX, baseline, fontSize).toPathData(round);
- if (!d || d.includes('NaN') || d.includes('Infinity')) continue;
- parts.push(d);
- }
- return parts.join(' ');
-}
-
-/**
- * The CSS `font-family` the app should use for live `` or canvas captions.
- *
- * Starts with the family of the most recently loaded font, so the fallback text
- * looks as close as possible to the outlines, and ends with Japanese-safe
- * families that exist on the machines the studio runs on.
- *
- * @returns {string}
- */
-export function captionFontStack() {
- const families = [loadedFamily ?? FALLBACK_FAMILIES[0]];
- const seen = new Set(families.map((name) => name.toLowerCase()));
- for (const name of FALLBACK_FAMILIES) {
- if (seen.has(name.toLowerCase())) continue;
- seen.add(name.toLowerCase());
- families.push(name);
- }
- return families.map(cssFamily).join(', ');
-}
-
-// --- internals ---------------------------------------------------------------
-
-/** Width of one line of text, in pixels. */
-function measureLine(font, line, fontSize) {
- let width = 0;
- for (const ch of line) width += font.getAdvanceWidth(ch, fontSize);
- return width;
-}
-
-/** Family name of a font, or `null` when it does not carry one. */
-function familyNameOf(font) {
- const names = font?.names?.fontFamily;
- if (!names) return null;
- // A parsed font stores one entry per language tag; a font built from scratch
- // stores a plain string. Accept both.
- const value = typeof names === 'string' ? names : names.en ?? Object.values(names)[0];
- return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
-}
-
-/** Only the characters the font can actually draw, newlines removed. */
-function drawableText(font, text) {
- let out = '';
- for (const ch of String(text ?? '')) {
- if (ch === '\n' || ch === '\r') continue;
- if (font.charToGlyphIndex(ch) === 0) continue; // no outline to draw
- out += ch;
- }
- return out;
-}
-
-/** Sideways shift of a line inside the block, for the block's alignment. */
-function alignOffset(align, blockWidth, lineWidth) {
- const slack = blockWidth - lineWidth;
- if (align === 'center') return slack / 2;
- if (align === 'right') return slack;
- return 0;
-}
-
-/** Quote a family for CSS unless it is a generic keyword. */
-function cssFamily(name) {
- if (GENERIC_FAMILIES.has(name.toLowerCase())) return name;
- return `'${name.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
-}
-
-/** `true` for scripts that may break between any two characters. */
-function isCjk(ch) {
- const c = ch.codePointAt(0);
- return (
- (c >= 0x1100 && c <= 0x11ff) || // Hangul Jamo
- (c >= 0x2e80 && c <= 0x303f) || // radicals, CJK punctuation
- (c >= 0x3040 && c <= 0x30ff) || // hiragana, katakana
- (c >= 0x3130 && c <= 0x318f) || // Hangul compatibility Jamo
- (c >= 0x3400 && c <= 0x4dbf) || // CJK extension A
- (c >= 0x4e00 && c <= 0x9fff) || // CJK unified ideographs
- (c >= 0xa960 && c <= 0xa97f) || // Hangul Jamo extended A
- (c >= 0xac00 && c <= 0xd7ff) || // Hangul syllables
- (c >= 0xf900 && c <= 0xfaff) || // CJK compatibility ideographs
- (c >= 0xfe10 && c <= 0xfe4f) || // vertical and compatibility forms
- (c >= 0xff00 && c <= 0xffef) || // fullwidth and halfwidth forms
- (c >= 0x1f200 && c <= 0x1f2ff) || // enclosed ideographic supplement
- (c >= 0x20000 && c <= 0x2fa1f) // CJK extensions B onwards
- );
-}
-
-/** One breakable piece of text together with its advance width. */
-function makeAtom(font, text, fontSize, space, closing) {
- return { text, width: font.getAdvanceWidth(text, fontSize), space, closing };
-}
-
-/**
- * Split a paragraph into the smallest pieces a line may break between.
- *
- * A Latin run stays one atom so it can never be split; every CJK character is
- * its own atom so a break may fall on either side of it.
- *
- * @returns {Array<{text: string, width: number, space: boolean, closing: boolean}>}
- */
-function tokenise(font, paragraph, fontSize) {
- const atoms = [];
- let word = null;
-
- const endWord = () => {
- if (word !== null) {
- atoms.push(word);
- word = null;
- }
- };
-
- for (const ch of paragraph) {
- if (SPACE.test(ch)) {
- endWord();
- atoms.push(makeAtom(font, ch, fontSize, true, false));
- } else if (isCjk(ch)) {
- endWord();
- atoms.push(makeAtom(font, ch, fontSize, false, CLOSING_PUNCTUATION.has(ch)));
- } else {
- if (word === null) word = makeAtom(font, '', fontSize, false, false);
- word.text += ch;
- word.width += font.getAdvanceWidth(ch, fontSize);
- }
- }
- endWord();
- return atoms;
-}
-
-/**
- * Greedy line breaker over atoms. Always returns at least one line, so an empty
- * paragraph comes back as one empty line and explicit breaks are preserved.
- *
- * @param {Array} atoms
- * @param {number} maxWidth `Infinity` disables wrapping
- * @returns {Array>}
- */
-function wrapAtoms(atoms, maxWidth) {
- const lines = [];
- let current = [];
-
- const currentWidth = () => current.reduce((sum, atom) => sum + atom.width, 0);
-
- const finish = () => {
- // A break swallows the spaces next to it, so no line ends or starts blank.
- while (current.length > 0 && current[current.length - 1].space) current.pop();
- while (current.length > 0 && current[0].space) current.shift();
- lines.push(current);
- current = [];
- };
-
- for (const atom of atoms) {
- if (current.length === 0) {
- if (atom.space) continue; // never start a line with a space
- current.push(atom);
- continue;
- }
- if (currentWidth() + atom.width <= maxWidth) {
- current.push(atom);
- continue;
- }
-
- // The atom does not fit. Keep closing punctuation off the start of the next
- // line by dragging the previous character down, but only when that leaves
- // something behind and the pair still respects the maximum width.
- const carried = current[current.length - 1];
- const visible = current.filter((a) => !a.space);
- if (
- atom.closing &&
- visible.length > 1 &&
- !carried.space &&
- carried.width + atom.width <= maxWidth
- ) {
- current.pop();
- finish();
- current.push(carried);
- } else {
- finish();
- }
- // The break already stands in for a space, so it does not start the new line.
- if (atom.space) continue;
- current.push(atom);
- }
-
- finish();
- return lines;
-}
diff --git a/bluebey-studio/src/trace.js b/bluebey-studio/src/trace.js
deleted file mode 100644
index 9b61d6d..0000000
--- a/bluebey-studio/src/trace.js
+++ /dev/null
@@ -1,410 +0,0 @@
-/**
- * Zero-dependency marching-squares contour tracer.
- *
- * Runs unchanged in the browser and in Node: the module has no imports at all,
- * never touches the DOM and never writes to the console.
- *
- * Coordinate system
- * -----------------
- * Pixel (x, y) is the unit square whose top-left corner sits at (x, y), so the
- * centre of pixel (x, y) is at (x + 0.5, y + 0.5) and y grows downwards
- * (row 0 is the top row of the input).
- *
- * Contours
- * --------
- * Every returned contour is a *closed* polyline of sub-pixel points; the first
- * point is not repeated at the end. Contours are oriented so that the inside of
- * the shape (the region where the sample is at or above the threshold) lies to
- * the left of the direction of travel. See `contourArea` for the sign this
- * implies.
- */
-
-const EDGE_TOP = 0;
-const EDGE_RIGHT = 1;
-const EDGE_BOTTOM = 2;
-const EDGE_LEFT = 3;
-
-/**
- * Directed segments emitted by each unambiguous marching-squares case.
- *
- * The case index is built from the cell corners as
- * `tl | tr << 1 | br << 2 | bl << 3`. Entries are flat `[from, to, from, to]`
- * pairs of edge ids; the direction keeps the inside region on the left.
- */
-const CELL_SEGMENTS = [
- null, // 0 - nothing inside
- [EDGE_LEFT, EDGE_TOP, -1, -1], // 1 - top-left only
- [EDGE_TOP, EDGE_RIGHT, -1, -1], // 2 - top-right only
- [EDGE_LEFT, EDGE_RIGHT, -1, -1], // 3 - top row
- [EDGE_RIGHT, EDGE_BOTTOM, -1, -1], // 4 - bottom-right only
- null, // 5 - saddle (top-left + bottom-right)
- [EDGE_TOP, EDGE_BOTTOM, -1, -1], // 6 - right column
- [EDGE_LEFT, EDGE_BOTTOM, -1, -1], // 7 - everything but bottom-left
- [EDGE_BOTTOM, EDGE_LEFT, -1, -1], // 8 - bottom-left only
- [EDGE_BOTTOM, EDGE_TOP, -1, -1], // 9 - left column
- null, // 10 - saddle (top-right + bottom-left)
- [EDGE_BOTTOM, EDGE_RIGHT, -1, -1], // 11 - everything but bottom-right
- [EDGE_RIGHT, EDGE_LEFT, -1, -1], // 12 - bottom row
- [EDGE_RIGHT, EDGE_TOP, -1, -1], // 13 - everything but top-right
- [EDGE_TOP, EDGE_LEFT, -1, -1], // 14 - everything but top-left
- null, // 15 - everything inside
-];
-
-// Case 5 (top-left + bottom-right inside). When the centre of the cell is
-// inside, the two inside corners are joined through the middle and the two
-// outside corners are separated; otherwise the inside corners are separated.
-const SADDLE_5_CONNECTED = [EDGE_LEFT, EDGE_BOTTOM, EDGE_RIGHT, EDGE_TOP];
-const SADDLE_5_SPLIT = [EDGE_LEFT, EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM];
-
-// Case 10 (top-right + bottom-left inside): the mirror image of case 5.
-const SADDLE_10_CONNECTED = [EDGE_TOP, EDGE_LEFT, EDGE_BOTTOM, EDGE_RIGHT];
-const SADDLE_10_SPLIT = [EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_LEFT];
-
-const DUPLICATE_EPS = 1e-9;
-
-/**
- * Signed area of a closed polyline, via the shoelace formula. The polyline is
- * implicitly closed (the last point is joined back to the first), so an open
- * ring is fine.
- *
- * Sign convention: this is the plain shoelace sum `Σ (x_i·y_{i+1} − x_{i+1}·y_i) / 2`
- * evaluated in the tracer's y-down pixel coordinates. A ring that runs
- * clockwise *as seen on screen* is therefore positive, and a counter-clockwise
- * one is negative. Because `traceAlphaContours` keeps the inside on the left,
- * the outer boundary of a filled region comes out negative and a hole in it
- * comes out positive.
- *
- * @param {Array<{x: number, y: number}>} points
- * @returns {number} signed area in square pixels
- */
-export function contourArea(points) {
- const n = points.length;
- if (!points || n < 3) return 0;
- let sum = 0;
- for (let i = 0; i < n; i++) {
- const a = points[i];
- const b = i + 1 === n ? points[0] : points[i + 1];
- sum += a.x * b.y - b.x * a.y;
- }
- return sum / 2;
-}
-
-/**
- * Build an SVG path `d` attribute with one closed subpath per contour.
- *
- * `mapPoint(x, y)` returns the `[X, Y]` pair written to the output, which is
- * what makes it possible to flip the y axis or apply a scale without touching
- * the tracer. Coordinates are rounded to `decimals` places.
- *
- * @param {Array>} contours
- * @param {(x: number, y: number) => [number, number]} mapPoint
- * @param {number} [decimals]
- * @returns {string}
- */
-export function contoursToPathData(contours, mapPoint, decimals = 2) {
- const places = Math.max(0, Math.floor(decimals));
- const factor = Math.pow(10, places);
- const parts = [];
- for (let c = 0; c < contours.length; c++) {
- const points = contours[c];
- if (!points || points.length < 2) continue;
- for (let i = 0; i < points.length; i++) {
- const mapped = mapPoint(points[i].x, points[i].y);
- // Rounding before formatting keeps `-0.00` out of the output.
- const rx = Math.round(mapped[0] * factor) / factor;
- const ry = Math.round(mapped[1] * factor) / factor;
- parts.push((i === 0 ? 'M' : 'L') + rx.toFixed(places) + ' ' + ry.toFixed(places));
- }
- parts.push('Z');
- }
- return parts.join(' ');
-}
-
-// --- closed-ring simplification helpers ------------------------------------
-
-/** Drop points that repeat their predecessor (including across the wrap). */
-function removeConsecutiveDuplicates(points) {
- const out = [];
- for (let i = 0; i < points.length; i++) {
- const p = points[i];
- const last = out[out.length - 1];
- if (last && Math.abs(last.x - p.x) <= DUPLICATE_EPS && Math.abs(last.y - p.y) <= DUPLICATE_EPS) {
- continue;
- }
- out.push(p);
- }
- while (out.length > 1) {
- const first = out[0];
- const last = out[out.length - 1];
- if (Math.abs(first.x - last.x) <= DUPLICATE_EPS && Math.abs(first.y - last.y) <= DUPLICATE_EPS) {
- out.pop();
- } else {
- break;
- }
- }
- return out;
-}
-
-/** Drop points that sit on the straight segment between their two neighbours. */
-function removeCollinear(points) {
- let list = points;
- let changed = true;
- while (changed && list.length > 3) {
- changed = false;
- const n = list.length;
- const out = [];
- for (let i = 0; i < n; i++) {
- const a = list[i === 0 ? n - 1 : i - 1];
- const b = list[i];
- const c = list[i + 1 === n ? 0 : i + 1];
- const abx = b.x - a.x;
- const aby = b.y - a.y;
- const bcx = c.x - b.x;
- const bcy = c.y - b.y;
- const cross = abx * bcy - aby * bcx;
- // |cross| / (|ab| * |bc|) is sin(turn angle); a small value means a
- // straight-through point, which carries no shape information.
- const scale = Math.sqrt((abx * abx + aby * aby) * (bcx * bcx + bcy * bcy));
- const straight = scale <= DUPLICATE_EPS || Math.abs(cross) <= 1e-9 * scale;
- if (straight && abx * bcx + aby * bcy >= 0) {
- changed = true;
- } else {
- out.push(b);
- }
- }
- list = out;
- }
- return list;
-}
-
-/**
- * Iterative Douglas–Peucker for an *open* polyline. The two end points are
- * always kept; the recursion uses an explicit stack so long contours cannot
- * overflow the call stack.
- */
-function douglasPeucker(points, tolerance) {
- const n = points.length;
- if (n <= 2) return points.slice();
- const keep = new Uint8Array(n);
- keep[0] = 1;
- keep[n - 1] = 1;
- const stack = [0, n - 1];
- while (stack.length > 0) {
- const i1 = stack.pop();
- const i0 = stack.pop();
- if (i1 <= i0 + 1) continue;
- const a = points[i0];
- const b = points[i1];
- const dx = b.x - a.x;
- const dy = b.y - a.y;
- const len = Math.sqrt(dx * dx + dy * dy);
- let maxDistance = -1;
- let maxIndex = -1;
- if (len <= DUPLICATE_EPS) {
- // Degenerate segment: fall back to the distance from the anchor point.
- for (let i = i0 + 1; i < i1; i++) {
- const px = points[i].x - a.x;
- const py = points[i].y - a.y;
- const d = Math.sqrt(px * px + py * py);
- if (d > maxDistance) {
- maxDistance = d;
- maxIndex = i;
- }
- }
- } else {
- for (let i = i0 + 1; i < i1; i++) {
- const p = points[i];
- const d = Math.abs(dy * (p.x - a.x) - dx * (p.y - a.y)) / len;
- if (d > maxDistance) {
- maxDistance = d;
- maxIndex = i;
- }
- }
- }
- if (maxDistance > tolerance && maxIndex > i0) {
- keep[maxIndex] = 1;
- stack.push(i0, maxIndex, maxIndex, i1);
- }
- }
- const out = [];
- for (let i = 0; i < n; i++) {
- if (keep[i]) out.push(points[i]);
- }
- return out;
-}
-
-/**
- * Simplify a closed ring, wrap-around segment included.
- *
- * The ring is cut at the point farthest from `points[0]`, which gives two open
- * polylines that together cover every segment of the loop exactly once; each
- * half is then simplified with Douglas–Peucker and the halves are stitched
- * back together (without duplicating the shared anchors).
- */
-function simplifyClosedRing(points, tolerance) {
- const n = points.length;
- if (n <= 3) return points.slice();
- let far = 0;
- let farDistance = -1;
- const first = points[0];
- for (let i = 1; i < n; i++) {
- const dx = points[i].x - first.x;
- const dy = points[i].y - first.y;
- const d = dx * dx + dy * dy;
- if (d > farDistance) {
- farDistance = d;
- far = i;
- }
- }
- // A ring whose points all coincide carries no shape; leave it to minArea.
- if (far <= 0 || farDistance <= DUPLICATE_EPS) return points.slice();
-
- const head = douglasPeucker(points.slice(0, far + 1), tolerance);
- const tail = douglasPeucker(points.slice(far).concat([first]), tolerance);
-
- // `head` ends and `tail` starts on the same anchor, and both end on
- // `points[0]`; drop the duplicated join so every point appears once.
- return head.slice(0, -1).concat(tail.slice(0, -1));
-}
-
-/**
- * Trace the iso-contour of a scalar field at `options.threshold`.
- *
- * @param {ArrayLike} alpha width*height samples, row-major, row 0 on top
- * @param {number} width
- * @param {number} height
- * @param {object} [options]
- * @param {number} [options.threshold=0.5] inside when `alpha/255 >= threshold`
- * @param {number} [options.simplifyTolerance=0.35] Douglas-Peucker tolerance, px
- * @param {number} [options.minArea=2] drop rings smaller than this, px²
- * @returns {Array>}
- */
-export function traceAlphaContours(alpha, width, height, options = {}) {
- const threshold = options.threshold ?? 0.5;
- const tolerance = options.simplifyTolerance ?? 0.35;
- const minArea = options.minArea ?? 2;
-
- const w = Math.floor(width);
- const h = Math.floor(height);
- if (!alpha || w < 1 || h < 1 || alpha.length < w * h) return [];
- if (w < 2 && h < 2) return [];
-
- // Work on a signed field, padded with a 1px outside border: `s >= 0` is
- // inside. The padding guarantees every crossing is strictly interior, so the
- // marching always yields closed rings and never touches the array edges.
- // The border holds the same value a fully transparent pixel maps to, which
- // makes shapes that run off the image close exactly on the image edge.
- const pw = w + 2;
- const ph = h + 2;
- const s = new Float32Array(pw * ph);
- s.fill(-threshold);
- for (let y = 0; y < h; y++) {
- const src = y * w;
- const dst = (y + 1) * pw + 1;
- for (let x = 0; x < w; x++) s[dst + x] = alpha[src + x] / 255 - threshold;
- }
- const sample = (x, y) => s[y * pw + x];
-
- // Crossing points, keyed by the grid edge they sit on. Both cells sharing an
- // edge call these with the same sample pair in the same order (top→bottom,
- // left→right), so the coordinates come out bit-identical and can be matched
- // by key alone.
- const points = new Map();
- function crossing(key, a, b, x0, y0, dx, dy) {
- let p = points.get(key);
- if (p === undefined) {
- const t = a / (a - b);
- p = { x: x0 + dx * t, y: y0 + dy * t };
- points.set(key, p);
- }
- return p;
- }
- // Horizontal edge of the padded grid at row `py`, spanning columns px..px+1.
- const hKey = (px, py) => `h:${px}:${py}`;
- const hPoint = (px, py) => crossing(hKey(px, py), sample(px, py), sample(px + 1, py), px - 0.5, py - 0.5, 1, 0);
- // Vertical edge of the padded grid at column `px`, spanning rows py..py+1.
- const vKey = (px, py) => `v:${px}:${py}`;
- const vPoint = (px, py) => crossing(vKey(px, py), sample(px, py), sample(px, py + 1), px - 0.5, py - 0.5, 0, 1);
-
- const edgeKey = [
- (px, py) => hKey(px, py), // EDGE_TOP
- (px, py) => vKey(px + 1, py), // EDGE_RIGHT
- (px, py) => hKey(px, py + 1), // EDGE_BOTTOM
- (px, py) => vKey(px, py), // EDGE_LEFT
- ];
- const edgePoint = [
- (px, py) => hPoint(px, py), // EDGE_TOP
- (px, py) => vPoint(px + 1, py), // EDGE_RIGHT
- (px, py) => hPoint(px, py + 1), // EDGE_BOTTOM
- (px, py) => vPoint(px, py), // EDGE_LEFT
- ];
-
- // Directed segments: `from` -> `to`, inside region on the left of travel.
- const fromKeys = [];
- const toKeys = [];
- const fromPoints = [];
-
- for (let py = 0; py < ph - 1; py++) {
- for (let px = 0; px < pw - 1; px++) {
- const tl = sample(px, py);
- const tr = sample(px + 1, py);
- const br = sample(px + 1, py + 1);
- const bl = sample(px, py + 1);
- const inside = (v) => (v >= 0 ? 1 : 0);
- const code = inside(tl) | (inside(tr) << 1) | (inside(br) << 2) | (inside(bl) << 3);
- let segments = CELL_SEGMENTS[code];
- if (code === 5) {
- // With the cell centre inside, the two inside corners join through the
- // middle; otherwise each is cut off on its own.
- segments = (tl + tr + br + bl) / 4 >= 0 ? SADDLE_5_CONNECTED : SADDLE_5_SPLIT;
- } else if (code === 10) {
- segments = (tl + tr + br + bl) / 4 >= 0 ? SADDLE_10_CONNECTED : SADDLE_10_SPLIT;
- }
- if (!segments) continue;
- for (let i = 0; i < segments.length; i += 2) {
- const a = segments[i];
- const b = segments[i + 1];
- if (a < 0 || b < 0) continue; // padding of the single-segment cases
- fromKeys.push(edgeKey[a](px, py));
- fromPoints.push(edgePoint[a](px, py));
- toKeys.push(edgeKey[b](px, py));
- edgePoint[b](px, py); // make sure the shared crossing exists
- }
- }
- }
-
- // Every crossing has exactly one incoming and one outgoing segment, so the
- // segments can be walked into closed rings without any ambiguity.
- const outgoing = new Map();
- for (let i = 0; i < fromKeys.length; i++) outgoing.set(fromKeys[i], i);
-
- const segmentCount = fromKeys.length;
- const used = new Uint8Array(segmentCount);
- const contours = [];
-
- for (let start = 0; start < segmentCount; start++) {
- if (used[start]) continue;
- const ring = [];
- let current = start;
- for (let guard = 0; guard <= segmentCount; guard++) {
- used[current] = 1;
- ring.push(fromPoints[current]);
- const next = outgoing.get(toKeys[current]);
- if (next === undefined || next === start) break;
- if (used[next]) break;
- current = next;
- }
- if (ring.length >= 3) contours.push(ring);
- }
-
- const result = [];
- for (let i = 0; i < contours.length; i++) {
- let ring = removeConsecutiveDuplicates(contours[i]);
- ring = simplifyClosedRing(ring, tolerance);
- ring = removeCollinear(ring);
- if (ring.length < 3) continue;
- if (Math.abs(contourArea(ring)) < minArea) continue;
- result.push(ring);
- }
- return result;
-}
\ No newline at end of file
diff --git a/bluebey-studio/src/ui.js b/bluebey-studio/src/ui.js
deleted file mode 100644
index 91e852b..0000000
--- a/bluebey-studio/src/ui.js
+++ /dev/null
@@ -1,367 +0,0 @@
-/**
- * Small DOM widget kit for the control panel: hyperscript, collapsible sections
- * and a handful of labelled controls. Everything returns `{ el, set, get }` so
- * callers can push state back into the widgets when presets are applied.
- */
-
-export function h(tag, props = {}, ...children) {
- const node = document.createElement(tag);
- for (const [key, value] of Object.entries(props ?? {})) {
- if (value == null || value === false) continue;
- if (key === 'class') node.className = value;
- else if (key === 'text') node.textContent = value;
- else if (key === 'style' && typeof value === 'object') Object.assign(node.style, value);
- else if (key === 'dataset' && typeof value === 'object') Object.assign(node.dataset, value);
- else if (key.startsWith('on') && typeof value === 'function') node.addEventListener(key.slice(2).toLowerCase(), value);
- else if (value === true) node.setAttribute(key, '');
- else node.setAttribute(key, String(value));
- }
- for (const child of children.flat()) {
- if (child == null || child === false) continue;
- node.append(child instanceof Node ? child : document.createTextNode(String(child)));
- }
- return node;
-}
-
-const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
-
-let toastTimer = 0;
-
-export function toast(message) {
- const node = document.getElementById('toast');
- if (!node) return;
- node.textContent = message;
- node.classList.add('show');
- clearTimeout(toastTimer);
- toastTimer = setTimeout(() => node.classList.remove('show'), 2400);
-}
-
-export function section(parent, title, { open = false, icon = null } = {}) {
- const body = h('div', { class: 'sec-body' });
- const head = h('button', { type: 'button', class: 'sec-head' },
- h('span', { class: 'sec-title' },
- icon ? secIcon(icon) : null,
- h('span', { text: title })),
- h('span', { class: 'chev', text: '▾' }));
- const el = h('section', { class: `sec${open ? '' : ' closed'}` }, head, body);
- head.addEventListener('click', () => el.classList.toggle('closed'));
- parent.append(el);
- // `add` accepts either an element or a widget object (`{ el }`), so callers
- // cannot accidentally append `[object Object]`.
- return { el, body, add: (child) => (body.append(child?.el ?? child), child), setOpen: (open) => el.classList.toggle('closed', !open) };
-}
-
-/** A label + control row. Pass `label` as null for a full-width control. */
-export function controlRow(label, control, { wide = false } = {}) {
- if (label == null || wide) return h('div', { class: 'row wide' }, label ? h('span', { class: 'label', text: label }) : null, control);
- return h('div', { class: 'row' },
- h('span', { class: 'label', text: label, title: label }),
- control);
-}
-
-/**
- * A tab's optional icon: one 24x24 stroke path, drawn in `currentColor` so it
- * follows the tab's text colour (and turns white when the tab is active).
- */
-function tabIcon(d) {
- const SVG_NS = 'http://www.w3.org/2000/svg';
- const svg = document.createElementNS(SVG_NS, 'svg');
- svg.setAttribute('viewBox', '0 0 24 24');
- svg.setAttribute('width', '18');
- svg.setAttribute('height', '18');
- svg.setAttribute('fill', 'none');
- svg.setAttribute('stroke', 'currentColor');
- svg.setAttribute('stroke-width', '1.8');
- svg.setAttribute('stroke-linecap', 'round');
- svg.setAttribute('stroke-linejoin', 'round');
- svg.style.verticalAlign = 'baseline';
- svg.style.flex = '0 0 auto';
- const path = document.createElementNS(SVG_NS, 'path');
- path.setAttribute('d', d);
- svg.append(path);
- return svg;
-}
-
-/** A small stroke icon built from raw SVG markup (see the icons in panel.js). */
-export function svgIcon(inner, { size = 16, viewBox = '0 0 24 24', className = 'grp-icon' } = {}) {
- const SVG_NS = 'http://www.w3.org/2000/svg';
- const svg = document.createElementNS(SVG_NS, 'svg');
- svg.setAttribute('viewBox', viewBox);
- svg.setAttribute('width', String(size));
- svg.setAttribute('height', String(size));
- svg.setAttribute('fill', 'none');
- svg.setAttribute('stroke', 'currentColor');
- svg.setAttribute('stroke-width', '1.8');
- svg.setAttribute('stroke-linecap', 'round');
- svg.setAttribute('stroke-linejoin', 'round');
- svg.setAttribute('class', className);
- svg.innerHTML = inner;
- return svg;
-}
-
-function secIcon(d) {
- const svg = tabIcon(d);
- svg.setAttribute('class', 'sec-icon');
- svg.setAttribute('width', '16');
- svg.setAttribute('height', '16');
- svg.style.verticalAlign = 'middle';
- return svg;
-}
-
-/**
- * A standalone 24x24 stroke icon (the same drawing as a tab's), for a plain
- * button that has no label of its own.
- */
-export function icon(d, size = 18) {
- const svg = tabIcon(d);
- svg.setAttribute('width', String(size));
- svg.setAttribute('height', String(size));
- return svg;
-}
-
-/**
- * Tabbed container: returns the per-tab panels to fill in. When a definition
- * carries an `icon` (an SVG path `d`) the tab shows only that icon, and the
- * label becomes its tooltip and accessible name; without one the label shows.
- */
-export function tabs(parent, definitions) {
- const bar = h('div', { class: 'tabs' });
- const buttons = new Map();
- const panels = {};
- let active = definitions[0]?.id ?? null;
-
- const select = (id) => {
- if (!panels[id]) return;
- active = id;
- for (const [key, button] of buttons) button.classList.toggle('active', key === id);
- for (const [key, panel] of Object.entries(panels)) panel.hidden = key !== id;
- };
-
- for (const definition of definitions) {
- const button = h('button', { type: 'button', class: 'tab' });
- if (definition.icon) {
- button.append(tabIcon(definition.icon));
- button.title = definition.label;
- button.setAttribute('aria-label', definition.label);
- } else {
- button.append(h('span', { class: 'tab-label', text: definition.label }));
- }
- button.addEventListener('click', () => select(definition.id));
- buttons.set(definition.id, button);
- bar.append(button);
- panels[definition.id] = h('div', { class: 'tab-panel' });
- }
-
- parent.append(bar);
- for (const definition of definitions) parent.append(panels[definition.id]);
- select(active);
- return { select, panels, get active() { return active; } };
-}
-
-/** A collapsed "more options" block, using the native details element. */
-export function details(parent, summary, { open = false } = {}) {
- const body = h('div', { class: 'details-body' });
- const el = h('details', { class: 'details' }, h('summary', { text: summary }), body);
- if (open) el.open = true;
- parent.append(el);
- return { el, body, add: (child) => (body.append(child?.el ?? child), child) };
-}
-
-export function subhead(text) {
- return h('div', { class: 'subhead', text });
-}
-
-export function hint(...lines) {
- return h('p', { class: 'hint' }, ...lines.flat().map((line) => (line instanceof Node ? line : line)));
-}
-
-export function slider({ label, min = 0, max = 1, step = 0.01, value = 0, format, onInput, onCommit, wide = false }) {
- const input = h('input', { type: 'range', min, max, step, value });
- const num = h('span', { class: 'num' });
- const fmt = format ?? ((v) => (step >= 1 ? String(Math.round(v)) : v.toFixed(2)));
- const sync = () => { num.textContent = fmt(Number(input.value)); };
- input.addEventListener('input', () => { sync(); onInput?.(Number(input.value)); });
- input.addEventListener('change', () => onCommit?.(Number(input.value)));
- sync();
- const control = h('div', { class: 'control' }, input, num);
- return {
- el: controlRow(label, control, { wide }),
- set(v) { input.value = String(v); sync(); },
- get: () => Number(input.value),
- };
-}
-
-export function check({ label, value = false, onChange, title }) {
- const input = h('input', { type: 'checkbox' });
- input.checked = !!value;
- input.addEventListener('change', () => onChange?.(input.checked));
- const el = h('label', { class: 'chk', title: title ?? '' }, input, h('span', { text: label }));
- return { el, set: (v) => { input.checked = !!v; }, get: () => input.checked };
-}
-
-export function segmented({ label, options, value, onChange, wide = false }) {
- const el = h('div', { class: 'seg' });
- const nodes = new Map();
- let current = value;
- const sync = () => { for (const [key, node] of nodes) node.classList.toggle('active', key === current); };
- const build = (list) => {
- el.replaceChildren();
- nodes.clear();
- for (const option of list) {
- const node = h('button', { type: 'button', title: option.title ?? option.label, text: option.label });
- node.addEventListener('click', () => {
- if (current === option.value) return;
- current = option.value;
- sync();
- onChange?.(option.value);
- });
- nodes.set(option.value, node);
- el.append(node);
- }
- sync();
- };
- build(options);
- return {
- el: controlRow(label, el, { wide }),
- set(v) { current = v; sync(); },
- get: () => current,
- setOptions: build,
- };
-}
-
-export function buttons({ label, items, wide = true }) {
- const el = h('div', { class: 'buttons' });
- const nodes = new Map();
- for (const item of items) {
- const node = h('button', {
- type: 'button',
- class: `btn${item.primary ? ' primary' : ''}`,
- title: item.title ?? '',
- text: item.label,
- });
- node.addEventListener('click', () => item.onClick?.());
- const key = item.id ?? item.label;
- nodes.set(key, node);
- el.append(node);
- }
- return {
- el: controlRow(label, el, { wide }),
- button: (key) => nodes.get(key)?.el,
- setDisabled(key, disabled) { const node = nodes.get(key); if (node) node.disabled = disabled; },
- setLabel(key, text) { const node = nodes.get(key); if (node) node.textContent = text; },
- };
-}
-
-export function colorField({ label, value = '#ffffff', onChange, swatches = [], wide = false }) {
- const input = h('input', { type: 'color', value });
- input.addEventListener('input', () => onChange?.(input.value));
- const control = h('div', { class: 'control' }, input);
- if (swatches.length) {
- const strip = h('div', { class: 'swatches' });
- for (const color of swatches) {
- const chip = h('button', { type: 'button', class: 'swatch', title: color, style: { background: color } });
- chip.addEventListener('click', () => { input.value = color; onChange?.(color); });
- strip.append(chip);
- }
- control.append(strip);
- }
- return {
- el: controlRow(label, control, { wide }),
- set(v) { input.value = v; },
- get: () => input.value,
- };
-}
-
-/**
- * Two-axis drag pad. `value` is `{ x, y }` with both components in -1..1 and
- * `y` positive upwards (screen-like, but flipped so up = up).
- */
-export function xyPad({ value = { x: 0, y: 0 }, onChange, onCommit, center = null }) {
- const dot = h('span', { class: 'pad-dot' });
- const el = h('div', { class: 'pad' }, center === 'eye' ? h('span', { class: 'pad-eye' }) : null, dot);
- let current = { x: value.x ?? 0, y: value.y ?? 0 };
- let dragging = false;
-
- const place = () => {
- dot.style.left = `${((current.x + 1) / 2) * 100}%`;
- dot.style.top = `${((1 - (current.y + 1) / 2)) * 100}%`;
- };
- const fromEvent = (event) => {
- const rect = el.getBoundingClientRect();
- const x = clamp(((event.clientX - rect.left) / rect.width) * 2 - 1, -1, 1);
- const y = clamp(1 - ((event.clientY - rect.top) / rect.height) * 2, -1, 1);
- current = { x, y };
- place();
- onChange?.({ ...current });
- };
-
- el.addEventListener('pointerdown', (event) => {
- dragging = true;
- el.setPointerCapture(event.pointerId);
- fromEvent(event);
- });
- el.addEventListener('pointermove', (event) => { if (dragging) fromEvent(event); });
- const end = (event) => {
- if (!dragging) return;
- dragging = false;
- if (el.hasPointerCapture(event.pointerId)) el.releasePointerCapture(event.pointerId);
- onCommit?.({ ...current });
- };
- el.addEventListener('pointerup', end);
- el.addEventListener('pointercancel', end);
- el.addEventListener('dblclick', () => { current = { x: 0, y: 0 }; place(); onChange?.({ ...current }); onCommit?.({ ...current }); });
-
- place();
- return {
- el,
- set(v) { current = { x: v.x ?? 0, y: v.y ?? 0 }; place(); },
- get: () => ({ ...current }),
- };
-}
-
-/**
- * Tail-direction picker: a 3x3 grid of dots whose *position* is the direction,
- * which reads at a glance in a way a flat row of "左上/右上" buttons does not.
- * The centre dot means no tail. Same `{ el, set, get }` contract as the rest.
- */
-export function tailPad({ label, value = 'left', onChange }) {
- // Row-major, so the array order is what the user sees on screen.
- const CELLS = [
- ['topLeft', '左上'], ['top', '上'], ['topRight', '右上'],
- ['left', '左'], ['none', 'なし'], ['right', '右'],
- ['bottomLeft', '左下'], ['bottom', '下'], ['bottomRight', '右下'],
- ];
- const el = h('div', { class: 'tail-pad' });
- const nodes = new Map();
- let current = value;
- const sync = () => { for (const [key, node] of nodes) node.classList.toggle('active', key === current); };
- for (const [cell, text] of CELLS) {
- const node = h('button', { type: 'button', class: 'tail-dot', title: text, 'aria-label': text });
- node.addEventListener('click', () => {
- if (current === cell) return;
- current = cell;
- sync();
- onChange?.(cell);
- });
- nodes.set(cell, node);
- el.append(node);
- }
- sync();
- return {
- el: controlRow(label, el),
- set(v) { current = v; sync(); },
- get: () => current,
- };
-}
-
-export function selectField({ label, options, value, onChange, wide = false }) {
- const select = h('select', { class: 'select' });
- for (const option of options) select.append(h('option', { value: option.value, text: option.label }));
- select.value = value;
- select.addEventListener('change', () => onChange?.(select.value));
- return {
- el: controlRow(label, select, { wide }),
- set(v) { select.value = v; },
- get: () => select.value,
- };
-}
diff --git a/bluebey-studio/src/urlState.js b/bluebey-studio/src/urlState.js
deleted file mode 100644
index d6fb6bd..0000000
--- a/bluebey-studio/src/urlState.js
+++ /dev/null
@@ -1,237 +0,0 @@
-/**
- * Shareable links: the whole look, packed into the URL fragment.
- *
- * A studio session is a lot of state, and "save a JSON file and send it" is a
- * poor answer to "how do I show you what I made" - one link you can paste into
- * chat is much better. So the state is JSON-encoded, deflated and
- * base64url-encoded into something short enough to sit in a `#` fragment.
- *
- * Three details make it survive the trip:
- *
- * - `view.backgroundImage` can be a multi-megabyte data URL (a photo the user
- * loaded). No link can carry that, so `stripForUrl` replaces just that one
- * field with `null` and keeps everything else - caption text, story panels,
- * props. Nothing else is ever dropped.
- *
- * - The payload starts with a version tag ('1' = raw deflate, '0' = plain
- * bytes) so the decoder knows whether to inflate. A browser without
- * `CompressionStream` falls back to the uncompressed form rather than
- * failing to produce a link at all.
- *
- * - base64url uses `-` and `_` and carries no `=` padding, so the fragment is
- * safe to paste into chat, Markdown or HTML without being mangled or escaped.
- *
- * `decodeState` is deliberately total: any malformed, truncated or hostile input
- * returns `null` rather than throwing, because it is fed whatever came out of
- * the address bar.
- *
- * Most of a session is still at its default, and a default costs bytes on every
- * link. `pruneDefaults` drops exactly the fields that equal `defaultState()`, and
- * `restoreDefaults` merges what is left back onto a fresh default, so the link
- * carries only what the user actually changed. The pruning is lossless and the
- * decoder still accepts the older, full-state links.
- */
-
-import { applyPatch, defaultState } from './presets.js';
-
-/** Above this length a `data:` background image is a photo, not a link. */
-const MAX_INLINE_IMAGE = 2048;
-
-/** Version tags. '1' is the deflated body, '0' the fallback plain body. */
-const TAG_DEFLATE = '1';
-const TAG_PLAIN = '0';
-
-const BASE64URL = /^[A-Za-z0-9_-]+$/;
-
-/**
- * A copy of `state` that is safe to put in a URL. Only the inline background
- * image is dropped (replaced with `null`), and only when it is a `data:` URL
- * long enough to blow up the link.
- */
-export function stripForUrl(state) {
- // A JSON round-trip gives the copy for free and drops anything that could not
- // be serialised anyway, so the link and the live state cannot diverge.
- const copy = JSON.parse(JSON.stringify(state ?? null));
- if (!copy || typeof copy !== 'object' || Array.isArray(copy)) return copy;
-
- const image = copy.view && copy.view.backgroundImage;
- if (typeof image === 'string' && image.startsWith('data:') && image.length > MAX_INLINE_IMAGE) {
- copy.view.backgroundImage = null;
- }
- return copy;
-}
-
-/** Plain objects only: not `null`, not an array. */
-function isPlainObject(value) {
- return value !== null && typeof value === 'object' && !Array.isArray(value);
-}
-
-/** Structural equality for the JSON-shaped values this module deals in. */
-function deepEqual(a, b) {
- if (a === b) return true;
- if (typeof a !== typeof b || a === null || b === null) return false;
- if (typeof a !== 'object') return Number.isNaN(a) && Number.isNaN(b);
- if (Array.isArray(a) !== Array.isArray(b)) return false;
- if (Array.isArray(a)) {
- return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
- }
- const aKeys = Object.keys(a);
- const bKeys = Object.keys(b);
- if (aKeys.length !== bKeys.length) return false;
- return aKeys.every((key) => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key]));
-}
-
-/** A detached copy of a JSON-shaped value. */
-function cloneValue(value) {
- return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
-}
-
-/** Marks a value that equals its default, so its key can be left out entirely. */
-const OMIT = Symbol('urlState.omit');
-
-/**
- * `value` with every sub-tree that deep-equals the matching `def` removed.
- * Arrays are kept whole - never pruned element by element - because that is how
- * `applyPatch` replaces them; objects are pruned key by key. `OMIT` means the
- * value matched its default and can be dropped from the parent.
- */
-function pruneValue(value, def) {
- if (deepEqual(value, def)) return OMIT;
- if (isPlainObject(value)) {
- if (!isPlainObject(def)) return cloneValue(value);
- const out = {};
- for (const [key, child] of Object.entries(value)) {
- const pruned = pruneValue(child, def[key]);
- if (pruned !== OMIT) out[key] = pruned;
- }
- return Object.keys(out).length === 0 ? OMIT : out;
- }
- return Array.isArray(value) ? cloneValue(value) : value;
-}
-
-/**
- * A copy of `state` with every value equal to `defaultState()` omitted. Lossless:
- * `restoreDefaults` merges the result onto a fresh default. Arrays that differ
- * from the default are carried whole so `applyPatch` can replace them.
- */
-export function pruneDefaults(state) {
- const pruned = pruneValue(state, defaultState());
- return pruned === OMIT ? {} : pruned;
-}
-
-/**
- * The inverse of `pruneDefaults`: a full state, with every field the partial does
- * not mention left at its default. Merging a previously full state is idempotent,
- * so links made by the older encoder still decode.
- */
-export function restoreDefaults(partial) {
- return applyPatch(defaultState(), partial);
-}
-
-/** True when the encoded fragment is bigger than a URL can comfortably hold. */
-export function isTooLong(text, limit = 1800) {
- return typeof text === 'string' && text.length > limit;
-}
-
-/**
- * base64url, without padding. Chunked so a large payload does not blow the
- * argument limit of `String.fromCharCode`.
- */
-function toBase64Url(bytes) {
- let binary = '';
- const chunk = 0x8000;
- for (let i = 0; i < bytes.length; i += chunk) {
- binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
- }
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
-}
-
-/** The inverse of `toBase64Url`. Throws on text that is not valid base64. */
-function fromBase64Url(text) {
- const base64 = text.replace(/-/g, '+').replace(/_/g, '/');
- const pad = (4 - (base64.length % 4)) % 4;
- const binary = atob(base64 + '='.repeat(pad));
- const bytes = new Uint8Array(binary.length);
- for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
- return bytes;
-}
-
-/** Read a whole ReadableStream into one Uint8Array. */
-async function drain(stream) {
- const chunks = [];
- let length = 0;
- for await (const chunk of stream) {
- const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
- chunks.push(bytes);
- length += bytes.length;
- }
- const out = new Uint8Array(length);
- let offset = 0;
- for (const bytes of chunks) {
- out.set(bytes, offset);
- offset += bytes.length;
- }
- return out;
-}
-
-async function deflateRaw(bytes) {
- const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream('deflate-raw'));
- return drain(stream);
-}
-
-async function inflateRaw(bytes) {
- const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('deflate-raw'));
- return drain(stream);
-}
-
-/**
- * Serialise, deflate and base64url-encode a state. The result is a fragment
- * payload: it survives a URL, a chat message and an HTML attribute.
- *
- * @param {object} state the studio state
- * @returns {Promise} a URL-safe string (tag + base64url)
- */
-export async function encodeState(state) {
- const json = JSON.stringify(pruneDefaults(stripForUrl(state)));
- const bytes = new TextEncoder().encode(json);
-
- if (typeof CompressionStream === 'function') {
- try {
- return TAG_DEFLATE + toBase64Url(await deflateRaw(bytes));
- } catch {
- // Fall through: an unusable compressing stream should not cost the link.
- }
- }
- return TAG_PLAIN + toBase64Url(bytes);
-}
-
-/**
- * The inverse of `encodeState`. Never throws: a bad tag, bad base64, a failed
- * inflate or JSON that is not a plain object all come back as `null`.
- *
- * @param {string} text the fragment payload
- * @returns {Promise<{ state: object } | null>}
- */
-export async function decodeState(text) {
- try {
- if (typeof text !== 'string' || text.length < 2) return null;
-
- const tag = text[0];
- if (tag !== TAG_DEFLATE && tag !== TAG_PLAIN) return null;
-
- const body = text.slice(1);
- if (!BASE64URL.test(body)) return null;
-
- let bytes = fromBase64Url(body);
- if (tag === TAG_DEFLATE) {
- if (typeof DecompressionStream !== 'function') return null;
- bytes = await inflateRaw(bytes);
- }
-
- const parsed = JSON.parse(new TextDecoder().decode(bytes));
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
- return { state: restoreDefaults(parsed) };
- } catch {
- return null;
- }
-}
diff --git a/bluebey-studio/src/zip.js b/bluebey-studio/src/zip.js
deleted file mode 100644
index 21a1955..0000000
--- a/bluebey-studio/src/zip.js
+++ /dev/null
@@ -1,218 +0,0 @@
-/**
- * Store-only ZIP writer.
- *
- * The studio exports a whole batch of images at once (one PNG per comic panel
- * or sticker), and a single download is far nicer to handle than a dozen files,
- * so the batch is packed into one archive here.
- *
- * PNG payloads are already DEFLATE-compressed, so running deflate over them a
- * second time costs time and saves nothing measurable: every entry is stored
- * verbatim (compression method 0, `compressedSize === uncompressedSize`) and no
- * data descriptor is needed.
- *
- * The module has no imports, never touches the DOM beyond `Blob`, and stamps
- * entries with a fixed DOS date unless one is passed in, so the same input
- * always produces a byte-for-byte identical archive. That keeps the output safe
- * to cache, diff, hash and test.
- *
- * Archive layout (every integer little-endian, no extra fields, no comments, no
- * directory entries):
- *
- * [local file header + stored data] one per file
- * [central directory entry] one per file
- * [end of central directory record]
- */
-
-/** Signature of a local file header ("PK\x03\x04"). */
-const LOCAL_SIGNATURE = 0x04034b50;
-/** Signature of a central directory entry ("PK\x01\x02"). */
-const CENTRAL_SIGNATURE = 0x02014b50;
-/** Signature of the end of central directory record ("PK\x05\x06"). */
-const EOCD_SIGNATURE = 0x06054b50;
-
-/** ZIP 2.0 is the oldest version that covers everything this writer emits. */
-const VERSION_NEEDED = 20;
-/** General purpose bit 11: the entry name is UTF-8, not CP437. */
-const FLAG_UTF8 = 0x0800;
-/** Compression method 0: stored. */
-const METHOD_STORE = 0;
-
-/** Largest value a 32-bit ZIP field can hold. Nothing may reach 4 GiB. */
-const MAX_FIELD = 0xffffffff;
-/** Largest value a 16-bit ZIP field can hold (entry count, name length). */
-const MAX_SHORT = 0xffff;
-
-const LOCAL_HEADER_SIZE = 30;
-const CENTRAL_HEADER_SIZE = 46;
-const EOCD_SIZE = 22;
-
-/** 1980-01-01 00:00 in DOS form: year offset 0, month 1, day 1, midnight. */
-const DEFAULT_TIME = 0;
-const DEFAULT_DATE = (1 << 5) | 1;
-
-/**
- * CRC-32 (polynomial 0xEDB88320, reflected), the checksum every ZIP entry must
- * carry. The table is built once at module load; eight table-driven bits per
- * byte is fast enough that a several-megabyte PNG batch stays imperceptible.
- */
-const CRC_TABLE = (() => {
- const table = new Uint32Array(256);
- for (let i = 0; i < 256; i++) {
- let c = i;
- for (let bit = 0; bit < 8; bit++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
- table[i] = c >>> 0;
- }
- return table;
-})();
-
-/**
- * @param {Uint8Array} bytes
- * @returns {number} unsigned 32-bit CRC-32
- */
-export function crc32(bytes) {
- let c = 0xffffffff;
- for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
- return (c ^ 0xffffffff) >>> 0;
-}
-
-/** Clip a Date into the DOS date/time pair stored in the headers. */
-function toDosDateTime(date) {
- const year = date.getFullYear();
- // The DOS epoch starts in 1980; anything older is clamped to the epoch.
- if (year < 1980) return { time: DEFAULT_TIME, date: DEFAULT_DATE };
- const time = (date.getHours() << 11) | (date.getMinutes() << 5) | (date.getSeconds() >> 1);
- const day = ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
- return { time: time & MAX_SHORT, date: day & MAX_SHORT };
-}
-
-/**
- * Entry names live inside the archive, where the separator is always "/": a
- * backslash (what Windows paths use) would be taken as part of the name, and a
- * leading slash would look like an absolute path to some extractors.
- */
-function normalizeName(name) {
- return String(name ?? '')
- .replace(/\\/g, '/')
- .replace(/^\/+/, '');
-}
-
-/** Accept any byte source, plus plain strings meaning UTF-8 text. */
-function toBytes(data, encoder) {
- if (typeof data === 'string') return encoder.encode(data);
- if (data instanceof Uint8Array) return data;
- if (ArrayBuffer.isView(data)) {
- return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
- }
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
- throw new TypeError('ZIP: file data must be a Uint8Array, an ArrayBuffer or a string');
-}
-
-function writeU16(view, offset, value) {
- view.setUint16(offset, value & MAX_SHORT, true);
-}
-
-function writeU32(view, offset, value) {
- view.setUint32(offset, value >>> 0, true);
-}
-
-/**
- * Pack `files` into a stored (uncompressed) ZIP archive.
- *
- * @param {Array<{name: string, data: Uint8Array | string}>} files
- * a string `data` is encoded as UTF-8 text
- * @param {object} [options]
- * @param {Date} [options.date]
- * timestamp for every entry; defaults to 1980-01-01 00:00 so that the same
- * input always yields the same bytes
- * @returns {Blob} an `application/zip` blob
- */
-export function createZip(files, { date } = {}) {
- const encoder = new TextEncoder();
- const stamp = date ? toDosDateTime(date) : { time: DEFAULT_TIME, date: DEFAULT_DATE };
-
- // First pass: normalise the input and measure it, so the output buffer can be
- // allocated exactly once instead of being grown and copied.
- const entries = [];
- let localTotal = 0;
- let centralTotal = 0;
-
- for (const file of files ?? []) {
- const nameBytes = encoder.encode(normalizeName(file.name));
- const data = toBytes(file.data, encoder);
-
- if (data.length > MAX_FIELD) {
- throw new Error('ZIP: a file is 4 GiB or larger; zip64 is not supported');
- }
- if (nameBytes.length > MAX_SHORT) {
- throw new Error('ZIP: a file name is longer than 65535 bytes');
- }
-
- const length = LOCAL_HEADER_SIZE + nameBytes.length + data.length;
- localTotal += length;
- centralTotal += CENTRAL_HEADER_SIZE + nameBytes.length;
- entries.push({ nameBytes, data, crc: crc32(data), length, offset: 0 });
- }
-
- if (entries.length > MAX_SHORT) {
- throw new Error('ZIP: more than 65535 files; zip64 is not supported');
- }
- if (localTotal + centralTotal + EOCD_SIZE > MAX_FIELD) {
- throw new Error('ZIP: the archive is 4 GiB or larger; zip64 is not supported');
- }
-
- const bytes = new Uint8Array(localTotal + centralTotal + EOCD_SIZE);
- const view = new DataView(bytes.buffer);
-
- let offset = 0;
- for (const entry of entries) {
- entry.offset = offset;
- writeU32(view, offset + 0, LOCAL_SIGNATURE);
- writeU16(view, offset + 4, VERSION_NEEDED);
- writeU16(view, offset + 6, FLAG_UTF8);
- writeU16(view, offset + 8, METHOD_STORE);
- writeU16(view, offset + 10, stamp.time);
- writeU16(view, offset + 12, stamp.date);
- writeU32(view, offset + 14, entry.crc);
- writeU32(view, offset + 18, entry.data.length);
- writeU32(view, offset + 22, entry.data.length);
- writeU16(view, offset + 26, entry.nameBytes.length);
- writeU16(view, offset + 28, 0); // extra field length
- bytes.set(entry.nameBytes, offset + LOCAL_HEADER_SIZE);
- bytes.set(entry.data, offset + LOCAL_HEADER_SIZE + entry.nameBytes.length);
- offset += entry.length;
- }
-
- const centralOffset = offset;
- for (const entry of entries) {
- writeU32(view, offset + 0, CENTRAL_SIGNATURE);
- writeU16(view, offset + 4, VERSION_NEEDED); // version made by (host 0 = MS-DOS)
- writeU16(view, offset + 6, VERSION_NEEDED);
- writeU16(view, offset + 8, FLAG_UTF8);
- writeU16(view, offset + 10, METHOD_STORE);
- writeU16(view, offset + 12, stamp.time);
- writeU16(view, offset + 14, stamp.date);
- writeU32(view, offset + 16, entry.crc);
- writeU32(view, offset + 20, entry.data.length);
- writeU32(view, offset + 24, entry.data.length);
- writeU16(view, offset + 28, entry.nameBytes.length);
- writeU16(view, offset + 30, 0); // extra field length
- writeU16(view, offset + 32, 0); // file comment length
- writeU16(view, offset + 34, 0); // disk number start
- writeU16(view, offset + 36, 0); // internal attributes
- writeU32(view, offset + 38, 0); // external attributes
- writeU32(view, offset + 42, entry.offset);
- bytes.set(entry.nameBytes, offset + CENTRAL_HEADER_SIZE);
- offset += CENTRAL_HEADER_SIZE + entry.nameBytes.length;
- }
-
- writeU32(view, offset + 0, EOCD_SIGNATURE);
- writeU16(view, offset + 4, 0); // number of this disk
- writeU16(view, offset + 6, 0); // disk holding the central directory
- writeU16(view, offset + 8, entries.length);
- writeU16(view, offset + 10, entries.length);
- writeU32(view, offset + 12, offset - centralOffset);
- writeU32(view, offset + 16, centralOffset);
- writeU16(view, offset + 20, 0); // archive comment length
-
- return new Blob([bytes], { type: 'application/zip' });
-}
diff --git a/bluebey-studio/vendor/lib/opentype.module.js b/bluebey-studio/vendor/lib/opentype.module.js
deleted file mode 100644
index 3bbee16..0000000
--- a/bluebey-studio/vendor/lib/opentype.module.js
+++ /dev/null
@@ -1,14460 +0,0 @@
-/**
- * https://opentype.js.org v1.3.4 | (c) Frederik De Bleser and other contributors | MIT License | Uses tiny-inflate by Devon Govett and string.prototype.codepointat polyfill by Mathias Bynens
- */
-
-/*! https://mths.be/codepointat v0.2.0 by @mathias */
-if (!String.prototype.codePointAt) {
- (function() {
- var defineProperty = (function() {
- // IE 8 only supports `Object.defineProperty` on DOM elements
- try {
- var object = {};
- var $defineProperty = Object.defineProperty;
- var result = $defineProperty(object, object, object) && $defineProperty;
- } catch(error) {}
- return result;
- }());
- var codePointAt = function(position) {
- if (this == null) {
- throw TypeError();
- }
- var string = String(this);
- var size = string.length;
- // `ToInteger`
- var index = position ? Number(position) : 0;
- if (index != index) { // better `isNaN`
- index = 0;
- }
- // Account for out-of-bounds indices:
- if (index < 0 || index >= size) {
- return undefined;
- }
- // Get the first code unit
- var first = string.charCodeAt(index);
- var second;
- if ( // check if it’s the start of a surrogate pair
- first >= 0xD800 && first <= 0xDBFF && // high surrogate
- size > index + 1 // there is a next code unit
- ) {
- second = string.charCodeAt(index + 1);
- if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate
- // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
- return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
- }
- }
- return first;
- };
- if (defineProperty) {
- defineProperty(String.prototype, 'codePointAt', {
- 'value': codePointAt,
- 'configurable': true,
- 'writable': true
- });
- } else {
- String.prototype.codePointAt = codePointAt;
- }
- }());
-}
-
-var TINF_OK = 0;
-var TINF_DATA_ERROR = -3;
-
-function Tree() {
- this.table = new Uint16Array(16); /* table of code length counts */
- this.trans = new Uint16Array(288); /* code -> symbol translation table */
-}
-
-function Data(source, dest) {
- this.source = source;
- this.sourceIndex = 0;
- this.tag = 0;
- this.bitcount = 0;
-
- this.dest = dest;
- this.destLen = 0;
-
- this.ltree = new Tree(); /* dynamic length/symbol tree */
- this.dtree = new Tree(); /* dynamic distance tree */
-}
-
-/* --------------------------------------------------- *
- * -- uninitialized global data (static structures) -- *
- * --------------------------------------------------- */
-
-var sltree = new Tree();
-var sdtree = new Tree();
-
-/* extra bits and base tables for length codes */
-var length_bits = new Uint8Array(30);
-var length_base = new Uint16Array(30);
-
-/* extra bits and base tables for distance codes */
-var dist_bits = new Uint8Array(30);
-var dist_base = new Uint16Array(30);
-
-/* special ordering of code length codes */
-var clcidx = new Uint8Array([
- 16, 17, 18, 0, 8, 7, 9, 6,
- 10, 5, 11, 4, 12, 3, 13, 2,
- 14, 1, 15
-]);
-
-/* used by tinf_decode_trees, avoids allocations every call */
-var code_tree = new Tree();
-var lengths = new Uint8Array(288 + 32);
-
-/* ----------------------- *
- * -- utility functions -- *
- * ----------------------- */
-
-/* build extra bits and base tables */
-function tinf_build_bits_base(bits, base, delta, first) {
- var i, sum;
-
- /* build bits table */
- for (i = 0; i < delta; ++i) { bits[i] = 0; }
- for (i = 0; i < 30 - delta; ++i) { bits[i + delta] = i / delta | 0; }
-
- /* build base table */
- for (sum = first, i = 0; i < 30; ++i) {
- base[i] = sum;
- sum += 1 << bits[i];
- }
-}
-
-/* build the fixed huffman trees */
-function tinf_build_fixed_trees(lt, dt) {
- var i;
-
- /* build fixed length tree */
- for (i = 0; i < 7; ++i) { lt.table[i] = 0; }
-
- lt.table[7] = 24;
- lt.table[8] = 152;
- lt.table[9] = 112;
-
- for (i = 0; i < 24; ++i) { lt.trans[i] = 256 + i; }
- for (i = 0; i < 144; ++i) { lt.trans[24 + i] = i; }
- for (i = 0; i < 8; ++i) { lt.trans[24 + 144 + i] = 280 + i; }
- for (i = 0; i < 112; ++i) { lt.trans[24 + 144 + 8 + i] = 144 + i; }
-
- /* build fixed distance tree */
- for (i = 0; i < 5; ++i) { dt.table[i] = 0; }
-
- dt.table[5] = 32;
-
- for (i = 0; i < 32; ++i) { dt.trans[i] = i; }
-}
-
-/* given an array of code lengths, build a tree */
-var offs = new Uint16Array(16);
-
-function tinf_build_tree(t, lengths, off, num) {
- var i, sum;
-
- /* clear code length count table */
- for (i = 0; i < 16; ++i) { t.table[i] = 0; }
-
- /* scan symbol lengths, and sum code length counts */
- for (i = 0; i < num; ++i) { t.table[lengths[off + i]]++; }
-
- t.table[0] = 0;
-
- /* compute offset table for distribution sort */
- for (sum = 0, i = 0; i < 16; ++i) {
- offs[i] = sum;
- sum += t.table[i];
- }
-
- /* create code->symbol translation table (symbols sorted by code) */
- for (i = 0; i < num; ++i) {
- if (lengths[off + i]) { t.trans[offs[lengths[off + i]]++] = i; }
- }
-}
-
-/* ---------------------- *
- * -- decode functions -- *
- * ---------------------- */
-
-/* get one bit from source stream */
-function tinf_getbit(d) {
- /* check if tag is empty */
- if (!d.bitcount--) {
- /* load next tag */
- d.tag = d.source[d.sourceIndex++];
- d.bitcount = 7;
- }
-
- /* shift bit out of tag */
- var bit = d.tag & 1;
- d.tag >>>= 1;
-
- return bit;
-}
-
-/* read a num bit value from a stream and add base */
-function tinf_read_bits(d, num, base) {
- if (!num)
- { return base; }
-
- while (d.bitcount < 24) {
- d.tag |= d.source[d.sourceIndex++] << d.bitcount;
- d.bitcount += 8;
- }
-
- var val = d.tag & (0xffff >>> (16 - num));
- d.tag >>>= num;
- d.bitcount -= num;
- return val + base;
-}
-
-/* given a data stream and a tree, decode a symbol */
-function tinf_decode_symbol(d, t) {
- while (d.bitcount < 24) {
- d.tag |= d.source[d.sourceIndex++] << d.bitcount;
- d.bitcount += 8;
- }
-
- var sum = 0, cur = 0, len = 0;
- var tag = d.tag;
-
- /* get more bits while code value is above sum */
- do {
- cur = 2 * cur + (tag & 1);
- tag >>>= 1;
- ++len;
-
- sum += t.table[len];
- cur -= t.table[len];
- } while (cur >= 0);
-
- d.tag = tag;
- d.bitcount -= len;
-
- return t.trans[sum + cur];
-}
-
-/* given a data stream, decode dynamic trees from it */
-function tinf_decode_trees(d, lt, dt) {
- var hlit, hdist, hclen;
- var i, num, length;
-
- /* get 5 bits HLIT (257-286) */
- hlit = tinf_read_bits(d, 5, 257);
-
- /* get 5 bits HDIST (1-32) */
- hdist = tinf_read_bits(d, 5, 1);
-
- /* get 4 bits HCLEN (4-19) */
- hclen = tinf_read_bits(d, 4, 4);
-
- for (i = 0; i < 19; ++i) { lengths[i] = 0; }
-
- /* read code lengths for code length alphabet */
- for (i = 0; i < hclen; ++i) {
- /* get 3 bits code length (0-7) */
- var clen = tinf_read_bits(d, 3, 0);
- lengths[clcidx[i]] = clen;
- }
-
- /* build code length tree */
- tinf_build_tree(code_tree, lengths, 0, 19);
-
- /* decode code lengths for the dynamic trees */
- for (num = 0; num < hlit + hdist;) {
- var sym = tinf_decode_symbol(d, code_tree);
-
- switch (sym) {
- case 16:
- /* copy previous code length 3-6 times (read 2 bits) */
- var prev = lengths[num - 1];
- for (length = tinf_read_bits(d, 2, 3); length; --length) {
- lengths[num++] = prev;
- }
- break;
- case 17:
- /* repeat code length 0 for 3-10 times (read 3 bits) */
- for (length = tinf_read_bits(d, 3, 3); length; --length) {
- lengths[num++] = 0;
- }
- break;
- case 18:
- /* repeat code length 0 for 11-138 times (read 7 bits) */
- for (length = tinf_read_bits(d, 7, 11); length; --length) {
- lengths[num++] = 0;
- }
- break;
- default:
- /* values 0-15 represent the actual code lengths */
- lengths[num++] = sym;
- break;
- }
- }
-
- /* build dynamic trees */
- tinf_build_tree(lt, lengths, 0, hlit);
- tinf_build_tree(dt, lengths, hlit, hdist);
-}
-
-/* ----------------------------- *
- * -- block inflate functions -- *
- * ----------------------------- */
-
-/* given a stream and two trees, inflate a block of data */
-function tinf_inflate_block_data(d, lt, dt) {
- while (1) {
- var sym = tinf_decode_symbol(d, lt);
-
- /* check for end of block */
- if (sym === 256) {
- return TINF_OK;
- }
-
- if (sym < 256) {
- d.dest[d.destLen++] = sym;
- } else {
- var length, dist, offs;
- var i;
-
- sym -= 257;
-
- /* possibly get more bits from length code */
- length = tinf_read_bits(d, length_bits[sym], length_base[sym]);
-
- dist = tinf_decode_symbol(d, dt);
-
- /* possibly get more bits from distance code */
- offs = d.destLen - tinf_read_bits(d, dist_bits[dist], dist_base[dist]);
-
- /* copy match */
- for (i = offs; i < offs + length; ++i) {
- d.dest[d.destLen++] = d.dest[i];
- }
- }
- }
-}
-
-/* inflate an uncompressed block of data */
-function tinf_inflate_uncompressed_block(d) {
- var length, invlength;
- var i;
-
- /* unread from bitbuffer */
- while (d.bitcount > 8) {
- d.sourceIndex--;
- d.bitcount -= 8;
- }
-
- /* get length */
- length = d.source[d.sourceIndex + 1];
- length = 256 * length + d.source[d.sourceIndex];
-
- /* get one's complement of length */
- invlength = d.source[d.sourceIndex + 3];
- invlength = 256 * invlength + d.source[d.sourceIndex + 2];
-
- /* check length */
- if (length !== (~invlength & 0x0000ffff))
- { return TINF_DATA_ERROR; }
-
- d.sourceIndex += 4;
-
- /* copy block */
- for (i = length; i; --i)
- { d.dest[d.destLen++] = d.source[d.sourceIndex++]; }
-
- /* make sure we start next block on a byte boundary */
- d.bitcount = 0;
-
- return TINF_OK;
-}
-
-/* inflate stream from source to dest */
-function tinf_uncompress(source, dest) {
- var d = new Data(source, dest);
- var bfinal, btype, res;
-
- do {
- /* read final block flag */
- bfinal = tinf_getbit(d);
-
- /* read block type (2 bits) */
- btype = tinf_read_bits(d, 2, 0);
-
- /* decompress block */
- switch (btype) {
- case 0:
- /* decompress uncompressed block */
- res = tinf_inflate_uncompressed_block(d);
- break;
- case 1:
- /* decompress block with fixed huffman trees */
- res = tinf_inflate_block_data(d, sltree, sdtree);
- break;
- case 2:
- /* decompress block with dynamic huffman trees */
- tinf_decode_trees(d, d.ltree, d.dtree);
- res = tinf_inflate_block_data(d, d.ltree, d.dtree);
- break;
- default:
- res = TINF_DATA_ERROR;
- }
-
- if (res !== TINF_OK)
- { throw new Error('Data error'); }
-
- } while (!bfinal);
-
- if (d.destLen < d.dest.length) {
- if (typeof d.dest.slice === 'function')
- { return d.dest.slice(0, d.destLen); }
- else
- { return d.dest.subarray(0, d.destLen); }
- }
-
- return d.dest;
-}
-
-/* -------------------- *
- * -- initialization -- *
- * -------------------- */
-
-/* build fixed huffman trees */
-tinf_build_fixed_trees(sltree, sdtree);
-
-/* build extra bits and base tables */
-tinf_build_bits_base(length_bits, length_base, 4, 3);
-tinf_build_bits_base(dist_bits, dist_base, 2, 1);
-
-/* fix a special case */
-length_bits[28] = 0;
-length_base[28] = 258;
-
-var tinyInflate = tinf_uncompress;
-
-// The Bounding Box object
-
-function derive(v0, v1, v2, v3, t) {
- return Math.pow(1 - t, 3) * v0 +
- 3 * Math.pow(1 - t, 2) * t * v1 +
- 3 * (1 - t) * Math.pow(t, 2) * v2 +
- Math.pow(t, 3) * v3;
-}
-/**
- * A bounding box is an enclosing box that describes the smallest measure within which all the points lie.
- * It is used to calculate the bounding box of a glyph or text path.
- *
- * On initialization, x1/y1/x2/y2 will be NaN. Check if the bounding box is empty using `isEmpty()`.
- *
- * @exports opentype.BoundingBox
- * @class
- * @constructor
- */
-function BoundingBox() {
- this.x1 = Number.NaN;
- this.y1 = Number.NaN;
- this.x2 = Number.NaN;
- this.y2 = Number.NaN;
-}
-
-/**
- * Returns true if the bounding box is empty, that is, no points have been added to the box yet.
- */
-BoundingBox.prototype.isEmpty = function() {
- return isNaN(this.x1) || isNaN(this.y1) || isNaN(this.x2) || isNaN(this.y2);
-};
-
-/**
- * Add the point to the bounding box.
- * The x1/y1/x2/y2 coordinates of the bounding box will now encompass the given point.
- * @param {number} x - The X coordinate of the point.
- * @param {number} y - The Y coordinate of the point.
- */
-BoundingBox.prototype.addPoint = function(x, y) {
- if (typeof x === 'number') {
- if (isNaN(this.x1) || isNaN(this.x2)) {
- this.x1 = x;
- this.x2 = x;
- }
- if (x < this.x1) {
- this.x1 = x;
- }
- if (x > this.x2) {
- this.x2 = x;
- }
- }
- if (typeof y === 'number') {
- if (isNaN(this.y1) || isNaN(this.y2)) {
- this.y1 = y;
- this.y2 = y;
- }
- if (y < this.y1) {
- this.y1 = y;
- }
- if (y > this.y2) {
- this.y2 = y;
- }
- }
-};
-
-/**
- * Add a X coordinate to the bounding box.
- * This extends the bounding box to include the X coordinate.
- * This function is used internally inside of addBezier.
- * @param {number} x - The X coordinate of the point.
- */
-BoundingBox.prototype.addX = function(x) {
- this.addPoint(x, null);
-};
-
-/**
- * Add a Y coordinate to the bounding box.
- * This extends the bounding box to include the Y coordinate.
- * This function is used internally inside of addBezier.
- * @param {number} y - The Y coordinate of the point.
- */
-BoundingBox.prototype.addY = function(y) {
- this.addPoint(null, y);
-};
-
-/**
- * Add a Bézier curve to the bounding box.
- * This extends the bounding box to include the entire Bézier.
- * @param {number} x0 - The starting X coordinate.
- * @param {number} y0 - The starting Y coordinate.
- * @param {number} x1 - The X coordinate of the first control point.
- * @param {number} y1 - The Y coordinate of the first control point.
- * @param {number} x2 - The X coordinate of the second control point.
- * @param {number} y2 - The Y coordinate of the second control point.
- * @param {number} x - The ending X coordinate.
- * @param {number} y - The ending Y coordinate.
- */
-BoundingBox.prototype.addBezier = function(x0, y0, x1, y1, x2, y2, x, y) {
- // This code is based on http://nishiohirokazu.blogspot.com/2009/06/how-to-calculate-bezier-curves-bounding.html
- // and https://github.com/icons8/svg-path-bounding-box
-
- var p0 = [x0, y0];
- var p1 = [x1, y1];
- var p2 = [x2, y2];
- var p3 = [x, y];
-
- this.addPoint(x0, y0);
- this.addPoint(x, y);
-
- for (var i = 0; i <= 1; i++) {
- var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i];
- var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i];
- var c = 3 * p1[i] - 3 * p0[i];
-
- if (a === 0) {
- if (b === 0) { continue; }
- var t = -c / b;
- if (0 < t && t < 1) {
- if (i === 0) { this.addX(derive(p0[i], p1[i], p2[i], p3[i], t)); }
- if (i === 1) { this.addY(derive(p0[i], p1[i], p2[i], p3[i], t)); }
- }
- continue;
- }
-
- var b2ac = Math.pow(b, 2) - 4 * c * a;
- if (b2ac < 0) { continue; }
- var t1 = (-b + Math.sqrt(b2ac)) / (2 * a);
- if (0 < t1 && t1 < 1) {
- if (i === 0) { this.addX(derive(p0[i], p1[i], p2[i], p3[i], t1)); }
- if (i === 1) { this.addY(derive(p0[i], p1[i], p2[i], p3[i], t1)); }
- }
- var t2 = (-b - Math.sqrt(b2ac)) / (2 * a);
- if (0 < t2 && t2 < 1) {
- if (i === 0) { this.addX(derive(p0[i], p1[i], p2[i], p3[i], t2)); }
- if (i === 1) { this.addY(derive(p0[i], p1[i], p2[i], p3[i], t2)); }
- }
- }
-};
-
-/**
- * Add a quadratic curve to the bounding box.
- * This extends the bounding box to include the entire quadratic curve.
- * @param {number} x0 - The starting X coordinate.
- * @param {number} y0 - The starting Y coordinate.
- * @param {number} x1 - The X coordinate of the control point.
- * @param {number} y1 - The Y coordinate of the control point.
- * @param {number} x - The ending X coordinate.
- * @param {number} y - The ending Y coordinate.
- */
-BoundingBox.prototype.addQuad = function(x0, y0, x1, y1, x, y) {
- var cp1x = x0 + 2 / 3 * (x1 - x0);
- var cp1y = y0 + 2 / 3 * (y1 - y0);
- var cp2x = cp1x + 1 / 3 * (x - x0);
- var cp2y = cp1y + 1 / 3 * (y - y0);
- this.addBezier(x0, y0, cp1x, cp1y, cp2x, cp2y, x, y);
-};
-
-// Geometric objects
-
-/**
- * A bézier path containing a set of path commands similar to a SVG path.
- * Paths can be drawn on a context using `draw`.
- * @exports opentype.Path
- * @class
- * @constructor
- */
-function Path() {
- this.commands = [];
- this.fill = 'black';
- this.stroke = null;
- this.strokeWidth = 1;
-}
-
-/**
- * @param {number} x
- * @param {number} y
- */
-Path.prototype.moveTo = function(x, y) {
- this.commands.push({
- type: 'M',
- x: x,
- y: y
- });
-};
-
-/**
- * @param {number} x
- * @param {number} y
- */
-Path.prototype.lineTo = function(x, y) {
- this.commands.push({
- type: 'L',
- x: x,
- y: y
- });
-};
-
-/**
- * Draws cubic curve
- * @function
- * curveTo
- * @memberof opentype.Path.prototype
- * @param {number} x1 - x of control 1
- * @param {number} y1 - y of control 1
- * @param {number} x2 - x of control 2
- * @param {number} y2 - y of control 2
- * @param {number} x - x of path point
- * @param {number} y - y of path point
- */
-
-/**
- * Draws cubic curve
- * @function
- * bezierCurveTo
- * @memberof opentype.Path.prototype
- * @param {number} x1 - x of control 1
- * @param {number} y1 - y of control 1
- * @param {number} x2 - x of control 2
- * @param {number} y2 - y of control 2
- * @param {number} x - x of path point
- * @param {number} y - y of path point
- * @see curveTo
- */
-Path.prototype.curveTo = Path.prototype.bezierCurveTo = function(x1, y1, x2, y2, x, y) {
- this.commands.push({
- type: 'C',
- x1: x1,
- y1: y1,
- x2: x2,
- y2: y2,
- x: x,
- y: y
- });
-};
-
-/**
- * Draws quadratic curve
- * @function
- * quadraticCurveTo
- * @memberof opentype.Path.prototype
- * @param {number} x1 - x of control
- * @param {number} y1 - y of control
- * @param {number} x - x of path point
- * @param {number} y - y of path point
- */
-
-/**
- * Draws quadratic curve
- * @function
- * quadTo
- * @memberof opentype.Path.prototype
- * @param {number} x1 - x of control
- * @param {number} y1 - y of control
- * @param {number} x - x of path point
- * @param {number} y - y of path point
- */
-Path.prototype.quadTo = Path.prototype.quadraticCurveTo = function(x1, y1, x, y) {
- this.commands.push({
- type: 'Q',
- x1: x1,
- y1: y1,
- x: x,
- y: y
- });
-};
-
-/**
- * Closes the path
- * @function closePath
- * @memberof opentype.Path.prototype
- */
-
-/**
- * Close the path
- * @function close
- * @memberof opentype.Path.prototype
- */
-Path.prototype.close = Path.prototype.closePath = function() {
- this.commands.push({
- type: 'Z'
- });
-};
-
-/**
- * Add the given path or list of commands to the commands of this path.
- * @param {Array} pathOrCommands - another opentype.Path, an opentype.BoundingBox, or an array of commands.
- */
-Path.prototype.extend = function(pathOrCommands) {
- if (pathOrCommands.commands) {
- pathOrCommands = pathOrCommands.commands;
- } else if (pathOrCommands instanceof BoundingBox) {
- var box = pathOrCommands;
- this.moveTo(box.x1, box.y1);
- this.lineTo(box.x2, box.y1);
- this.lineTo(box.x2, box.y2);
- this.lineTo(box.x1, box.y2);
- this.close();
- return;
- }
-
- Array.prototype.push.apply(this.commands, pathOrCommands);
-};
-
-/**
- * Calculate the bounding box of the path.
- * @returns {opentype.BoundingBox}
- */
-Path.prototype.getBoundingBox = function() {
- var box = new BoundingBox();
-
- var startX = 0;
- var startY = 0;
- var prevX = 0;
- var prevY = 0;
- for (var i = 0; i < this.commands.length; i++) {
- var cmd = this.commands[i];
- switch (cmd.type) {
- case 'M':
- box.addPoint(cmd.x, cmd.y);
- startX = prevX = cmd.x;
- startY = prevY = cmd.y;
- break;
- case 'L':
- box.addPoint(cmd.x, cmd.y);
- prevX = cmd.x;
- prevY = cmd.y;
- break;
- case 'Q':
- box.addQuad(prevX, prevY, cmd.x1, cmd.y1, cmd.x, cmd.y);
- prevX = cmd.x;
- prevY = cmd.y;
- break;
- case 'C':
- box.addBezier(prevX, prevY, cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y);
- prevX = cmd.x;
- prevY = cmd.y;
- break;
- case 'Z':
- prevX = startX;
- prevY = startY;
- break;
- default:
- throw new Error('Unexpected path command ' + cmd.type);
- }
- }
- if (box.isEmpty()) {
- box.addPoint(0, 0);
- }
- return box;
-};
-
-/**
- * Draw the path to a 2D context.
- * @param {CanvasRenderingContext2D} ctx - A 2D drawing context.
- */
-Path.prototype.draw = function(ctx) {
- ctx.beginPath();
- for (var i = 0; i < this.commands.length; i += 1) {
- var cmd = this.commands[i];
- if (cmd.type === 'M') {
- ctx.moveTo(cmd.x, cmd.y);
- } else if (cmd.type === 'L') {
- ctx.lineTo(cmd.x, cmd.y);
- } else if (cmd.type === 'C') {
- ctx.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y);
- } else if (cmd.type === 'Q') {
- ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y);
- } else if (cmd.type === 'Z') {
- ctx.closePath();
- }
- }
-
- if (this.fill) {
- ctx.fillStyle = this.fill;
- ctx.fill();
- }
-
- if (this.stroke) {
- ctx.strokeStyle = this.stroke;
- ctx.lineWidth = this.strokeWidth;
- ctx.stroke();
- }
-};
-
-/**
- * Convert the Path to a string of path data instructions
- * See http://www.w3.org/TR/SVG/paths.html#PathData
- * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values
- * @return {string}
- */
-Path.prototype.toPathData = function(decimalPlaces) {
- decimalPlaces = decimalPlaces !== undefined ? decimalPlaces : 2;
-
- function floatToString(v) {
- if (Math.round(v) === v) {
- return '' + Math.round(v);
- } else {
- return v.toFixed(decimalPlaces);
- }
- }
-
- function packValues() {
- var arguments$1 = arguments;
-
- var s = '';
- for (var i = 0; i < arguments.length; i += 1) {
- var v = arguments$1[i];
- if (v >= 0 && i > 0) {
- s += ' ';
- }
-
- s += floatToString(v);
- }
-
- return s;
- }
-
- var d = '';
- for (var i = 0; i < this.commands.length; i += 1) {
- var cmd = this.commands[i];
- if (cmd.type === 'M') {
- d += 'M' + packValues(cmd.x, cmd.y);
- } else if (cmd.type === 'L') {
- d += 'L' + packValues(cmd.x, cmd.y);
- } else if (cmd.type === 'C') {
- d += 'C' + packValues(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y);
- } else if (cmd.type === 'Q') {
- d += 'Q' + packValues(cmd.x1, cmd.y1, cmd.x, cmd.y);
- } else if (cmd.type === 'Z') {
- d += 'Z';
- }
- }
-
- return d;
-};
-
-/**
- * Convert the path to an SVG element, as a string.
- * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values
- * @return {string}
- */
-Path.prototype.toSVG = function(decimalPlaces) {
- var svg = ' ';
- return svg;
-};
-
-/**
- * Convert the path to a DOM element.
- * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values
- * @return {SVGPathElement}
- */
-Path.prototype.toDOMElement = function(decimalPlaces) {
- var temporaryPath = this.toPathData(decimalPlaces);
- var newPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
-
- newPath.setAttribute('d', temporaryPath);
-
- return newPath;
-};
-
-// Run-time checking of preconditions.
-
-function fail(message) {
- throw new Error(message);
-}
-
-// Precondition function that checks if the given predicate is true.
-// If not, it will throw an error.
-function argument(predicate, message) {
- if (!predicate) {
- fail(message);
- }
-}
-var check = { fail: fail, argument: argument, assert: argument };
-
-// Data types used in the OpenType font file.
-
-var LIMIT16 = 32768; // The limit at which a 16-bit number switches signs == 2^15
-var LIMIT32 = 2147483648; // The limit at which a 32-bit number switches signs == 2 ^ 31
-
-/**
- * @exports opentype.decode
- * @class
- */
-var decode = {};
-/**
- * @exports opentype.encode
- * @class
- */
-var encode = {};
-/**
- * @exports opentype.sizeOf
- * @class
- */
-var sizeOf = {};
-
-// Return a function that always returns the same value.
-function constant(v) {
- return function() {
- return v;
- };
-}
-
-// OpenType data types //////////////////////////////////////////////////////
-
-/**
- * Convert an 8-bit unsigned integer to a list of 1 byte.
- * @param {number}
- * @returns {Array}
- */
-encode.BYTE = function(v) {
- check.argument(v >= 0 && v <= 255, 'Byte value should be between 0 and 255.');
- return [v];
-};
-/**
- * @constant
- * @type {number}
- */
-sizeOf.BYTE = constant(1);
-
-/**
- * Convert a 8-bit signed integer to a list of 1 byte.
- * @param {string}
- * @returns {Array}
- */
-encode.CHAR = function(v) {
- return [v.charCodeAt(0)];
-};
-
-/**
- * @constant
- * @type {number}
- */
-sizeOf.CHAR = constant(1);
-
-/**
- * Convert an ASCII string to a list of bytes.
- * @param {string}
- * @returns {Array}
- */
-encode.CHARARRAY = function(v) {
- if (typeof v === 'undefined') {
- v = '';
- console.warn('Undefined CHARARRAY encountered and treated as an empty string. This is probably caused by a missing glyph name.');
- }
- var b = [];
- for (var i = 0; i < v.length; i += 1) {
- b[i] = v.charCodeAt(i);
- }
-
- return b;
-};
-
-/**
- * @param {Array}
- * @returns {number}
- */
-sizeOf.CHARARRAY = function(v) {
- if (typeof v === 'undefined') {
- return 0;
- }
- return v.length;
-};
-
-/**
- * Convert a 16-bit unsigned integer to a list of 2 bytes.
- * @param {number}
- * @returns {Array}
- */
-encode.USHORT = function(v) {
- return [(v >> 8) & 0xFF, v & 0xFF];
-};
-
-/**
- * @constant
- * @type {number}
- */
-sizeOf.USHORT = constant(2);
-
-/**
- * Convert a 16-bit signed integer to a list of 2 bytes.
- * @param {number}
- * @returns {Array}
- */
-encode.SHORT = function(v) {
- // Two's complement
- if (v >= LIMIT16) {
- v = -(2 * LIMIT16 - v);
- }
-
- return [(v >> 8) & 0xFF, v & 0xFF];
-};
-
-/**
- * @constant
- * @type {number}
- */
-sizeOf.SHORT = constant(2);
-
-/**
- * Convert a 24-bit unsigned integer to a list of 3 bytes.
- * @param {number}
- * @returns {Array}
- */
-encode.UINT24 = function(v) {
- return [(v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
-};
-
-/**
- * @constant
- * @type {number}
- */
-sizeOf.UINT24 = constant(3);
-
-/**
- * Convert a 32-bit unsigned integer to a list of 4 bytes.
- * @param {number}
- * @returns {Array}
- */
-encode.ULONG = function(v) {
- return [(v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
-};
-
-/**
- * @constant
- * @type {number}
- */
-sizeOf.ULONG = constant(4);
-
-/**
- * Convert a 32-bit unsigned integer to a list of 4 bytes.
- * @param {number}
- * @returns {Array}
- */
-encode.LONG = function(v) {
- // Two's complement
- if (v >= LIMIT32) {
- v = -(2 * LIMIT32 - v);
- }
-
- return [(v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
-};
-
-/**
- * @constant
- * @type {number}
- */
-sizeOf.LONG = constant(4);
-
-encode.FIXED = encode.ULONG;
-sizeOf.FIXED = sizeOf.ULONG;
-
-encode.FWORD = encode.SHORT;
-sizeOf.FWORD = sizeOf.SHORT;
-
-encode.UFWORD = encode.USHORT;
-sizeOf.UFWORD = sizeOf.USHORT;
-
-/**
- * Convert a 32-bit Apple Mac timestamp integer to a list of 8 bytes, 64-bit timestamp.
- * @param {number}
- * @returns {Array}
- */
-encode.LONGDATETIME = function(v) {
- return [0, 0, 0, 0, (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
-};
-
-/**
- * @constant
- * @type {number}
- */
-sizeOf.LONGDATETIME = constant(8);
-
-/**
- * Convert a 4-char tag to a list of 4 bytes.
- * @param {string}
- * @returns {Array}
- */
-encode.TAG = function(v) {
- check.argument(v.length === 4, 'Tag should be exactly 4 ASCII characters.');
- return [v.charCodeAt(0),
- v.charCodeAt(1),
- v.charCodeAt(2),
- v.charCodeAt(3)];
-};
-
-/**
- * @constant
- * @type {number}
- */
-sizeOf.TAG = constant(4);
-
-// CFF data types ///////////////////////////////////////////////////////////
-
-encode.Card8 = encode.BYTE;
-sizeOf.Card8 = sizeOf.BYTE;
-
-encode.Card16 = encode.USHORT;
-sizeOf.Card16 = sizeOf.USHORT;
-
-encode.OffSize = encode.BYTE;
-sizeOf.OffSize = sizeOf.BYTE;
-
-encode.SID = encode.USHORT;
-sizeOf.SID = sizeOf.USHORT;
-
-// Convert a numeric operand or charstring number to a variable-size list of bytes.
-/**
- * Convert a numeric operand or charstring number to a variable-size list of bytes.
- * @param {number}
- * @returns {Array}
- */
-encode.NUMBER = function(v) {
- if (v >= -107 && v <= 107) {
- return [v + 139];
- } else if (v >= 108 && v <= 1131) {
- v = v - 108;
- return [(v >> 8) + 247, v & 0xFF];
- } else if (v >= -1131 && v <= -108) {
- v = -v - 108;
- return [(v >> 8) + 251, v & 0xFF];
- } else if (v >= -32768 && v <= 32767) {
- return encode.NUMBER16(v);
- } else {
- return encode.NUMBER32(v);
- }
-};
-
-/**
- * @param {number}
- * @returns {number}
- */
-sizeOf.NUMBER = function(v) {
- return encode.NUMBER(v).length;
-};
-
-/**
- * Convert a signed number between -32768 and +32767 to a three-byte value.
- * This ensures we always use three bytes, but is not the most compact format.
- * @param {number}
- * @returns {Array}
- */
-encode.NUMBER16 = function(v) {
- return [28, (v >> 8) & 0xFF, v & 0xFF];
-};
-
-/**
- * @constant
- * @type {number}
- */
-sizeOf.NUMBER16 = constant(3);
-
-/**
- * Convert a signed number between -(2^31) and +(2^31-1) to a five-byte value.
- * This is useful if you want to be sure you always use four bytes,
- * at the expense of wasting a few bytes for smaller numbers.
- * @param {number}
- * @returns {Array}
- */
-encode.NUMBER32 = function(v) {
- return [29, (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF];
-};
-
-/**
- * @constant
- * @type {number}
- */
-sizeOf.NUMBER32 = constant(5);
-
-/**
- * @param {number}
- * @returns {Array}
- */
-encode.REAL = function(v) {
- var value = v.toString();
-
- // Some numbers use an epsilon to encode the value. (e.g. JavaScript will store 0.0000001 as 1e-7)
- // This code converts it back to a number without the epsilon.
- var m = /\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/.exec(value);
- if (m) {
- var epsilon = parseFloat('1e' + ((m[2] ? +m[2] : 0) + m[1].length));
- value = (Math.round(v * epsilon) / epsilon).toString();
- }
-
- var nibbles = '';
- for (var i = 0, ii = value.length; i < ii; i += 1) {
- var c = value[i];
- if (c === 'e') {
- nibbles += value[++i] === '-' ? 'c' : 'b';
- } else if (c === '.') {
- nibbles += 'a';
- } else if (c === '-') {
- nibbles += 'e';
- } else {
- nibbles += c;
- }
- }
-
- nibbles += (nibbles.length & 1) ? 'f' : 'ff';
- var out = [30];
- for (var i$1 = 0, ii$1 = nibbles.length; i$1 < ii$1; i$1 += 2) {
- out.push(parseInt(nibbles.substr(i$1, 2), 16));
- }
-
- return out;
-};
-
-/**
- * @param {number}
- * @returns {number}
- */
-sizeOf.REAL = function(v) {
- return encode.REAL(v).length;
-};
-
-encode.NAME = encode.CHARARRAY;
-sizeOf.NAME = sizeOf.CHARARRAY;
-
-encode.STRING = encode.CHARARRAY;
-sizeOf.STRING = sizeOf.CHARARRAY;
-
-/**
- * @param {DataView} data
- * @param {number} offset
- * @param {number} numBytes
- * @returns {string}
- */
-decode.UTF8 = function(data, offset, numBytes) {
- var codePoints = [];
- var numChars = numBytes;
- for (var j = 0; j < numChars; j++, offset += 1) {
- codePoints[j] = data.getUint8(offset);
- }
-
- return String.fromCharCode.apply(null, codePoints);
-};
-
-/**
- * @param {DataView} data
- * @param {number} offset
- * @param {number} numBytes
- * @returns {string}
- */
-decode.UTF16 = function(data, offset, numBytes) {
- var codePoints = [];
- var numChars = numBytes / 2;
- for (var j = 0; j < numChars; j++, offset += 2) {
- codePoints[j] = data.getUint16(offset);
- }
-
- return String.fromCharCode.apply(null, codePoints);
-};
-
-/**
- * Convert a JavaScript string to UTF16-BE.
- * @param {string}
- * @returns {Array}
- */
-encode.UTF16 = function(v) {
- var b = [];
- for (var i = 0; i < v.length; i += 1) {
- var codepoint = v.charCodeAt(i);
- b[b.length] = (codepoint >> 8) & 0xFF;
- b[b.length] = codepoint & 0xFF;
- }
-
- return b;
-};
-
-/**
- * @param {string}
- * @returns {number}
- */
-sizeOf.UTF16 = function(v) {
- return v.length * 2;
-};
-
-// Data for converting old eight-bit Macintosh encodings to Unicode.
-// This representation is optimized for decoding; encoding is slower
-// and needs more memory. The assumption is that all opentype.js users
-// want to open fonts, but saving a font will be comparatively rare
-// so it can be more expensive. Keyed by IANA character set name.
-//
-// Python script for generating these strings:
-//
-// s = u''.join([chr(c).decode('mac_greek') for c in range(128, 256)])
-// print(s.encode('utf-8'))
-/**
- * @private
- */
-var eightBitMacEncodings = {
- 'x-mac-croatian': // Python: 'mac_croatian'
- 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®Š™´¨≠ŽØ∞±≤≥∆µ∂∑∏š∫ªºΩžø' +
- '¿¡¬√ƒ≈ƫȅ ÀÃÕŒœĐ—“”‘’÷◊©⁄€‹›Æ»–·‚„‰ÂćÁčÈÍÎÏÌÓÔđÒÚÛÙıˆ˜¯πË˚¸Êæˇ',
- 'x-mac-cyrillic': // Python: 'mac_cyrillic'
- 'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ†°Ґ£§•¶І®©™Ђђ≠Ѓѓ∞±≤≥іµґЈЄєЇїЉљЊњ' +
- 'јЅ¬√ƒ≈∆«»… ЋћЌќѕ–—“”‘’÷„ЎўЏџ№Ёёяабвгдежзийклмнопрстуфхцчшщъыьэю',
- 'x-mac-gaelic': // http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/GAELIC.TXT
- 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØḂ±≤≥ḃĊċḊḋḞḟĠġṀæø' +
- 'ṁṖṗɼƒſṠ«»… ÀÃÕŒœ–—“”‘’ṡẛÿŸṪ€‹›Ŷŷṫ·Ỳỳ⁊ÂÊÁËÈÍÎÏÌÓÔ♣ÒÚÛÙıÝýŴŵẄẅẀẁẂẃ',
- 'x-mac-greek': // Python: 'mac_greek'
- 'Ĺ²É³ÖÜ΅àâä΄¨çéèê룙î‰ôö¦€ùûü†ΓΔΘΛΞΠß®©ΣΪ§≠°·Α±≤≥¥ΒΕΖΗΙΚΜΦΫΨΩ' +
- 'άΝ¬ΟΡ≈Τ«»… ΥΧΆΈœ–―“”‘’÷ΉΊΌΎέήίόΏύαβψδεφγηιξκλμνοπώρστθωςχυζϊϋΐΰ\u00AD',
- 'x-mac-icelandic': // Python: 'mac_iceland'
- 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûüݰ¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' +
- '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€ÐðÞþý·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ',
- 'x-mac-inuit': // http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/INUIT.TXT
- 'ᐃᐄᐅᐆᐊᐋᐱᐲᐳᐴᐸᐹᑉᑎᑏᑐᑑᑕᑖᑦᑭᑮᑯᑰᑲᑳᒃᒋᒌᒍᒎᒐᒑ°ᒡᒥᒦ•¶ᒧ®©™ᒨᒪᒫᒻᓂᓃᓄᓅᓇᓈᓐᓯᓰᓱᓲᓴᓵᔅᓕᓖᓗ' +
- 'ᓘᓚᓛᓪᔨᔩᔪᔫᔭ… ᔮᔾᕕᕖᕗ–—“”‘’ᕘᕙᕚᕝᕆᕇᕈᕉᕋᕌᕐᕿᖀᖁᖂᖃᖄᖅᖏᖐᖑᖒᖓᖔᖕᙱᙲᙳᙴᙵᙶᖖᖠᖡᖢᖣᖤᖥᖦᕼŁł',
- 'x-mac-ce': // Python: 'mac_latin2'
- 'ÄĀāÉĄÖÜáąČäčĆć鏟ĎíďĒēĖóėôöõúĚěü†°Ę£§•¶ß®©™ę¨≠ģĮįĪ≤≥īĶ∂∑łĻļĽľĹĺŅ' +
- 'ņѬ√ńŇ∆«»… ňŐÕőŌ–—“”‘’÷◊ōŔŕŘ‹›řŖŗŠ‚„šŚśÁŤťÍŽžŪÓÔūŮÚůŰűŲųÝýķŻŁżĢˇ',
- macintosh: // Python: 'mac_roman'
- 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' +
- '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›fifl‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ',
- 'x-mac-romanian': // Python: 'mac_romanian'
- 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ĂȘ∞±≤≥¥µ∂∑∏π∫ªºΩăș' +
- '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›Țț‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ',
- 'x-mac-turkish': // Python: 'mac_turkish'
- 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' +
- '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸĞğİıŞş‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙˆ˜¯˘˙˚¸˝˛ˇ'
-};
-
-/**
- * Decodes an old-style Macintosh string. Returns either a Unicode JavaScript
- * string, or 'undefined' if the encoding is unsupported. For example, we do
- * not support Chinese, Japanese or Korean because these would need large
- * mapping tables.
- * @param {DataView} dataView
- * @param {number} offset
- * @param {number} dataLength
- * @param {string} encoding
- * @returns {string}
- */
-decode.MACSTRING = function(dataView, offset, dataLength, encoding) {
- var table = eightBitMacEncodings[encoding];
- if (table === undefined) {
- return undefined;
- }
-
- var result = '';
- for (var i = 0; i < dataLength; i++) {
- var c = dataView.getUint8(offset + i);
- // In all eight-bit Mac encodings, the characters 0x00..0x7F are
- // mapped to U+0000..U+007F; we only need to look up the others.
- if (c <= 0x7F) {
- result += String.fromCharCode(c);
- } else {
- result += table[c & 0x7F];
- }
- }
-
- return result;
-};
-
-// Helper function for encode.MACSTRING. Returns a dictionary for mapping
-// Unicode character codes to their 8-bit MacOS equivalent. This table
-// is not exactly a super cheap data structure, but we do not care because
-// encoding Macintosh strings is only rarely needed in typical applications.
-var macEncodingTableCache = typeof WeakMap === 'function' && new WeakMap();
-var macEncodingCacheKeys;
-var getMacEncodingTable = function (encoding) {
- // Since we use encoding as a cache key for WeakMap, it has to be
- // a String object and not a literal. And at least on NodeJS 2.10.1,
- // WeakMap requires that the same String instance is passed for cache hits.
- if (!macEncodingCacheKeys) {
- macEncodingCacheKeys = {};
- for (var e in eightBitMacEncodings) {
- /*jshint -W053 */ // Suppress "Do not use String as a constructor."
- macEncodingCacheKeys[e] = new String(e);
- }
- }
-
- var cacheKey = macEncodingCacheKeys[encoding];
- if (cacheKey === undefined) {
- return undefined;
- }
-
- // We can't do "if (cache.has(key)) {return cache.get(key)}" here:
- // since garbage collection may run at any time, it could also kick in
- // between the calls to cache.has() and cache.get(). In that case,
- // we would return 'undefined' even though we do support the encoding.
- if (macEncodingTableCache) {
- var cachedTable = macEncodingTableCache.get(cacheKey);
- if (cachedTable !== undefined) {
- return cachedTable;
- }
- }
-
- var decodingTable = eightBitMacEncodings[encoding];
- if (decodingTable === undefined) {
- return undefined;
- }
-
- var encodingTable = {};
- for (var i = 0; i < decodingTable.length; i++) {
- encodingTable[decodingTable.charCodeAt(i)] = i + 0x80;
- }
-
- if (macEncodingTableCache) {
- macEncodingTableCache.set(cacheKey, encodingTable);
- }
-
- return encodingTable;
-};
-
-/**
- * Encodes an old-style Macintosh string. Returns a byte array upon success.
- * If the requested encoding is unsupported, or if the input string contains
- * a character that cannot be expressed in the encoding, the function returns
- * 'undefined'.
- * @param {string} str
- * @param {string} encoding
- * @returns {Array}
- */
-encode.MACSTRING = function(str, encoding) {
- var table = getMacEncodingTable(encoding);
- if (table === undefined) {
- return undefined;
- }
-
- var result = [];
- for (var i = 0; i < str.length; i++) {
- var c = str.charCodeAt(i);
-
- // In all eight-bit Mac encodings, the characters 0x00..0x7F are
- // mapped to U+0000..U+007F; we only need to look up the others.
- if (c >= 0x80) {
- c = table[c];
- if (c === undefined) {
- // str contains a Unicode character that cannot be encoded
- // in the requested encoding.
- return undefined;
- }
- }
- result[i] = c;
- // result.push(c);
- }
-
- return result;
-};
-
-/**
- * @param {string} str
- * @param {string} encoding
- * @returns {number}
- */
-sizeOf.MACSTRING = function(str, encoding) {
- var b = encode.MACSTRING(str, encoding);
- if (b !== undefined) {
- return b.length;
- } else {
- return 0;
- }
-};
-
-// Helper for encode.VARDELTAS
-function isByteEncodable(value) {
- return value >= -128 && value <= 127;
-}
-
-// Helper for encode.VARDELTAS
-function encodeVarDeltaRunAsZeroes(deltas, pos, result) {
- var runLength = 0;
- var numDeltas = deltas.length;
- while (pos < numDeltas && runLength < 64 && deltas[pos] === 0) {
- ++pos;
- ++runLength;
- }
- result.push(0x80 | (runLength - 1));
- return pos;
-}
-
-// Helper for encode.VARDELTAS
-function encodeVarDeltaRunAsBytes(deltas, offset, result) {
- var runLength = 0;
- var numDeltas = deltas.length;
- var pos = offset;
- while (pos < numDeltas && runLength < 64) {
- var value = deltas[pos];
- if (!isByteEncodable(value)) {
- break;
- }
-
- // Within a byte-encoded run of deltas, a single zero is best
- // stored literally as 0x00 value. However, if we have two or
- // more zeroes in a sequence, it is better to start a new run.
- // Fore example, the sequence of deltas [15, 15, 0, 15, 15]
- // becomes 6 bytes (04 0F 0F 00 0F 0F) when storing the zero
- // within the current run, but 7 bytes (01 0F 0F 80 01 0F 0F)
- // when starting a new run.
- if (value === 0 && pos + 1 < numDeltas && deltas[pos + 1] === 0) {
- break;
- }
-
- ++pos;
- ++runLength;
- }
- result.push(runLength - 1);
- for (var i = offset; i < pos; ++i) {
- result.push((deltas[i] + 256) & 0xff);
- }
- return pos;
-}
-
-// Helper for encode.VARDELTAS
-function encodeVarDeltaRunAsWords(deltas, offset, result) {
- var runLength = 0;
- var numDeltas = deltas.length;
- var pos = offset;
- while (pos < numDeltas && runLength < 64) {
- var value = deltas[pos];
-
- // Within a word-encoded run of deltas, it is easiest to start
- // a new run (with a different encoding) whenever we encounter
- // a zero value. For example, the sequence [0x6666, 0, 0x7777]
- // needs 7 bytes when storing the zero inside the current run
- // (42 66 66 00 00 77 77), and equally 7 bytes when starting a
- // new run (40 66 66 80 40 77 77).
- if (value === 0) {
- break;
- }
-
- // Within a word-encoded run of deltas, a single value in the
- // range (-128..127) should be encoded within the current run
- // because it is more compact. For example, the sequence
- // [0x6666, 2, 0x7777] becomes 7 bytes when storing the value
- // literally (42 66 66 00 02 77 77), but 8 bytes when starting
- // a new run (40 66 66 00 02 40 77 77).
- if (isByteEncodable(value) && pos + 1 < numDeltas && isByteEncodable(deltas[pos + 1])) {
- break;
- }
-
- ++pos;
- ++runLength;
- }
- result.push(0x40 | (runLength - 1));
- for (var i = offset; i < pos; ++i) {
- var val = deltas[i];
- result.push(((val + 0x10000) >> 8) & 0xff, (val + 0x100) & 0xff);
- }
- return pos;
-}
-
-/**
- * Encode a list of variation adjustment deltas.
- *
- * Variation adjustment deltas are used in ‘gvar’ and ‘cvar’ tables.
- * They indicate how points (in ‘gvar’) or values (in ‘cvar’) get adjusted
- * when generating instances of variation fonts.
- *
- * @see https://www.microsoft.com/typography/otspec/gvar.htm
- * @see https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
- * @param {Array}
- * @return {Array}
- */
-encode.VARDELTAS = function(deltas) {
- var pos = 0;
- var result = [];
- while (pos < deltas.length) {
- var value = deltas[pos];
- if (value === 0) {
- pos = encodeVarDeltaRunAsZeroes(deltas, pos, result);
- } else if (value >= -128 && value <= 127) {
- pos = encodeVarDeltaRunAsBytes(deltas, pos, result);
- } else {
- pos = encodeVarDeltaRunAsWords(deltas, pos, result);
- }
- }
- return result;
-};
-
-// Convert a list of values to a CFF INDEX structure.
-// The values should be objects containing name / type / value.
-/**
- * @param {Array} l
- * @returns {Array}
- */
-encode.INDEX = function(l) {
- //var offset, offsets, offsetEncoder, encodedOffsets, encodedOffset, data,
- // i, v;
- // Because we have to know which data type to use to encode the offsets,
- // we have to go through the values twice: once to encode the data and
- // calculate the offsets, then again to encode the offsets using the fitting data type.
- var offset = 1; // First offset is always 1.
- var offsets = [offset];
- var data = [];
- for (var i = 0; i < l.length; i += 1) {
- var v = encode.OBJECT(l[i]);
- Array.prototype.push.apply(data, v);
- offset += v.length;
- offsets.push(offset);
- }
-
- if (data.length === 0) {
- return [0, 0];
- }
-
- var encodedOffsets = [];
- var offSize = (1 + Math.floor(Math.log(offset) / Math.log(2)) / 8) | 0;
- var offsetEncoder = [undefined, encode.BYTE, encode.USHORT, encode.UINT24, encode.ULONG][offSize];
- for (var i$1 = 0; i$1 < offsets.length; i$1 += 1) {
- var encodedOffset = offsetEncoder(offsets[i$1]);
- Array.prototype.push.apply(encodedOffsets, encodedOffset);
- }
-
- return Array.prototype.concat(encode.Card16(l.length),
- encode.OffSize(offSize),
- encodedOffsets,
- data);
-};
-
-/**
- * @param {Array}
- * @returns {number}
- */
-sizeOf.INDEX = function(v) {
- return encode.INDEX(v).length;
-};
-
-/**
- * Convert an object to a CFF DICT structure.
- * The keys should be numeric.
- * The values should be objects containing name / type / value.
- * @param {Object} m
- * @returns {Array}
- */
-encode.DICT = function(m) {
- var d = [];
- var keys = Object.keys(m);
- var length = keys.length;
-
- for (var i = 0; i < length; i += 1) {
- // Object.keys() return string keys, but our keys are always numeric.
- var k = parseInt(keys[i], 0);
- var v = m[k];
- // Value comes before the key.
- d = d.concat(encode.OPERAND(v.value, v.type));
- d = d.concat(encode.OPERATOR(k));
- }
-
- return d;
-};
-
-/**
- * @param {Object}
- * @returns {number}
- */
-sizeOf.DICT = function(m) {
- return encode.DICT(m).length;
-};
-
-/**
- * @param {number}
- * @returns {Array}
- */
-encode.OPERATOR = function(v) {
- if (v < 1200) {
- return [v];
- } else {
- return [12, v - 1200];
- }
-};
-
-/**
- * @param {Array} v
- * @param {string}
- * @returns {Array}
- */
-encode.OPERAND = function(v, type) {
- var d = [];
- if (Array.isArray(type)) {
- for (var i = 0; i < type.length; i += 1) {
- check.argument(v.length === type.length, 'Not enough arguments given for type' + type);
- d = d.concat(encode.OPERAND(v[i], type[i]));
- }
- } else {
- if (type === 'SID') {
- d = d.concat(encode.NUMBER(v));
- } else if (type === 'offset') {
- // We make it easy for ourselves and always encode offsets as
- // 4 bytes. This makes offset calculation for the top dict easier.
- d = d.concat(encode.NUMBER32(v));
- } else if (type === 'number') {
- d = d.concat(encode.NUMBER(v));
- } else if (type === 'real') {
- d = d.concat(encode.REAL(v));
- } else {
- throw new Error('Unknown operand type ' + type);
- // FIXME Add support for booleans
- }
- }
-
- return d;
-};
-
-encode.OP = encode.BYTE;
-sizeOf.OP = sizeOf.BYTE;
-
-// memoize charstring encoding using WeakMap if available
-var wmm = typeof WeakMap === 'function' && new WeakMap();
-
-/**
- * Convert a list of CharString operations to bytes.
- * @param {Array}
- * @returns {Array}
- */
-encode.CHARSTRING = function(ops) {
- // See encode.MACSTRING for why we don't do "if (wmm && wmm.has(ops))".
- if (wmm) {
- var cachedValue = wmm.get(ops);
- if (cachedValue !== undefined) {
- return cachedValue;
- }
- }
-
- var d = [];
- var length = ops.length;
-
- for (var i = 0; i < length; i += 1) {
- var op = ops[i];
- d = d.concat(encode[op.type](op.value));
- }
-
- if (wmm) {
- wmm.set(ops, d);
- }
-
- return d;
-};
-
-/**
- * @param {Array}
- * @returns {number}
- */
-sizeOf.CHARSTRING = function(ops) {
- return encode.CHARSTRING(ops).length;
-};
-
-// Utility functions ////////////////////////////////////////////////////////
-
-/**
- * Convert an object containing name / type / value to bytes.
- * @param {Object}
- * @returns {Array}
- */
-encode.OBJECT = function(v) {
- var encodingFunction = encode[v.type];
- check.argument(encodingFunction !== undefined, 'No encoding function for type ' + v.type);
- return encodingFunction(v.value);
-};
-
-/**
- * @param {Object}
- * @returns {number}
- */
-sizeOf.OBJECT = function(v) {
- var sizeOfFunction = sizeOf[v.type];
- check.argument(sizeOfFunction !== undefined, 'No sizeOf function for type ' + v.type);
- return sizeOfFunction(v.value);
-};
-
-/**
- * Convert a table object to bytes.
- * A table contains a list of fields containing the metadata (name, type and default value).
- * The table itself has the field values set as attributes.
- * @param {opentype.Table}
- * @returns {Array}
- */
-encode.TABLE = function(table) {
- var d = [];
- var length = table.fields.length;
- var subtables = [];
- var subtableOffsets = [];
-
- for (var i = 0; i < length; i += 1) {
- var field = table.fields[i];
- var encodingFunction = encode[field.type];
- check.argument(encodingFunction !== undefined, 'No encoding function for field type ' + field.type + ' (' + field.name + ')');
- var value = table[field.name];
- if (value === undefined) {
- value = field.value;
- }
-
- var bytes = encodingFunction(value);
-
- if (field.type === 'TABLE') {
- subtableOffsets.push(d.length);
- d = d.concat([0, 0]);
- subtables.push(bytes);
- } else {
- d = d.concat(bytes);
- }
- }
-
- for (var i$1 = 0; i$1 < subtables.length; i$1 += 1) {
- var o = subtableOffsets[i$1];
- var offset = d.length;
- check.argument(offset < 65536, 'Table ' + table.tableName + ' too big.');
- d[o] = offset >> 8;
- d[o + 1] = offset & 0xff;
- d = d.concat(subtables[i$1]);
- }
-
- return d;
-};
-
-/**
- * @param {opentype.Table}
- * @returns {number}
- */
-sizeOf.TABLE = function(table) {
- var numBytes = 0;
- var length = table.fields.length;
-
- for (var i = 0; i < length; i += 1) {
- var field = table.fields[i];
- var sizeOfFunction = sizeOf[field.type];
- check.argument(sizeOfFunction !== undefined, 'No sizeOf function for field type ' + field.type + ' (' + field.name + ')');
- var value = table[field.name];
- if (value === undefined) {
- value = field.value;
- }
-
- numBytes += sizeOfFunction(value);
-
- // Subtables take 2 more bytes for offsets.
- if (field.type === 'TABLE') {
- numBytes += 2;
- }
- }
-
- return numBytes;
-};
-
-encode.RECORD = encode.TABLE;
-sizeOf.RECORD = sizeOf.TABLE;
-
-// Merge in a list of bytes.
-encode.LITERAL = function(v) {
- return v;
-};
-
-sizeOf.LITERAL = function(v) {
- return v.length;
-};
-
-// Table metadata
-
-/**
- * @exports opentype.Table
- * @class
- * @param {string} tableName
- * @param {Array} fields
- * @param {Object} options
- * @constructor
- */
-function Table(tableName, fields, options) {
- // For coverage tables with coverage format 2, we do not want to add the coverage data directly to the table object,
- // as this will result in wrong encoding order of the coverage data on serialization to bytes.
- // The fallback of using the field values directly when not present on the table is handled in types.encode.TABLE() already.
- if (fields.length && (fields[0].name !== 'coverageFormat' || fields[0].value === 1)) {
- for (var i = 0; i < fields.length; i += 1) {
- var field = fields[i];
- this[field.name] = field.value;
- }
- }
-
- this.tableName = tableName;
- this.fields = fields;
- if (options) {
- var optionKeys = Object.keys(options);
- for (var i$1 = 0; i$1 < optionKeys.length; i$1 += 1) {
- var k = optionKeys[i$1];
- var v = options[k];
- if (this[k] !== undefined) {
- this[k] = v;
- }
- }
- }
-}
-
-/**
- * Encodes the table and returns an array of bytes
- * @return {Array}
- */
-Table.prototype.encode = function() {
- return encode.TABLE(this);
-};
-
-/**
- * Get the size of the table.
- * @return {number}
- */
-Table.prototype.sizeOf = function() {
- return sizeOf.TABLE(this);
-};
-
-/**
- * @private
- */
-function ushortList(itemName, list, count) {
- if (count === undefined) {
- count = list.length;
- }
- var fields = new Array(list.length + 1);
- fields[0] = {name: itemName + 'Count', type: 'USHORT', value: count};
- for (var i = 0; i < list.length; i++) {
- fields[i + 1] = {name: itemName + i, type: 'USHORT', value: list[i]};
- }
- return fields;
-}
-
-/**
- * @private
- */
-function tableList(itemName, records, itemCallback) {
- var count = records.length;
- var fields = new Array(count + 1);
- fields[0] = {name: itemName + 'Count', type: 'USHORT', value: count};
- for (var i = 0; i < count; i++) {
- fields[i + 1] = {name: itemName + i, type: 'TABLE', value: itemCallback(records[i], i)};
- }
- return fields;
-}
-
-/**
- * @private
- */
-function recordList(itemName, records, itemCallback) {
- var count = records.length;
- var fields = [];
- fields[0] = {name: itemName + 'Count', type: 'USHORT', value: count};
- for (var i = 0; i < count; i++) {
- fields = fields.concat(itemCallback(records[i], i));
- }
- return fields;
-}
-
-// Common Layout Tables
-
-/**
- * @exports opentype.Coverage
- * @class
- * @param {opentype.Table}
- * @constructor
- * @extends opentype.Table
- */
-function Coverage(coverageTable) {
- if (coverageTable.format === 1) {
- Table.call(this, 'coverageTable',
- [{name: 'coverageFormat', type: 'USHORT', value: 1}]
- .concat(ushortList('glyph', coverageTable.glyphs))
- );
- } else if (coverageTable.format === 2) {
- Table.call(this, 'coverageTable',
- [{name: 'coverageFormat', type: 'USHORT', value: 2}]
- .concat(recordList('rangeRecord', coverageTable.ranges, function(RangeRecord) {
- return [
- {name: 'startGlyphID', type: 'USHORT', value: RangeRecord.start},
- {name: 'endGlyphID', type: 'USHORT', value: RangeRecord.end},
- {name: 'startCoverageIndex', type: 'USHORT', value: RangeRecord.index} ];
- }))
- );
- } else {
- check.assert(false, 'Coverage format must be 1 or 2.');
- }
-}
-Coverage.prototype = Object.create(Table.prototype);
-Coverage.prototype.constructor = Coverage;
-
-function ScriptList(scriptListTable) {
- Table.call(this, 'scriptListTable',
- recordList('scriptRecord', scriptListTable, function(scriptRecord, i) {
- var script = scriptRecord.script;
- var defaultLangSys = script.defaultLangSys;
- check.assert(!!defaultLangSys, 'Unable to write GSUB: script ' + scriptRecord.tag + ' has no default language system.');
- return [
- {name: 'scriptTag' + i, type: 'TAG', value: scriptRecord.tag},
- {name: 'script' + i, type: 'TABLE', value: new Table('scriptTable', [
- {name: 'defaultLangSys', type: 'TABLE', value: new Table('defaultLangSys', [
- {name: 'lookupOrder', type: 'USHORT', value: 0},
- {name: 'reqFeatureIndex', type: 'USHORT', value: defaultLangSys.reqFeatureIndex}]
- .concat(ushortList('featureIndex', defaultLangSys.featureIndexes)))}
- ].concat(recordList('langSys', script.langSysRecords, function(langSysRecord, i) {
- var langSys = langSysRecord.langSys;
- return [
- {name: 'langSysTag' + i, type: 'TAG', value: langSysRecord.tag},
- {name: 'langSys' + i, type: 'TABLE', value: new Table('langSys', [
- {name: 'lookupOrder', type: 'USHORT', value: 0},
- {name: 'reqFeatureIndex', type: 'USHORT', value: langSys.reqFeatureIndex}
- ].concat(ushortList('featureIndex', langSys.featureIndexes)))}
- ];
- })))}
- ];
- })
- );
-}
-ScriptList.prototype = Object.create(Table.prototype);
-ScriptList.prototype.constructor = ScriptList;
-
-/**
- * @exports opentype.FeatureList
- * @class
- * @param {opentype.Table}
- * @constructor
- * @extends opentype.Table
- */
-function FeatureList(featureListTable) {
- Table.call(this, 'featureListTable',
- recordList('featureRecord', featureListTable, function(featureRecord, i) {
- var feature = featureRecord.feature;
- return [
- {name: 'featureTag' + i, type: 'TAG', value: featureRecord.tag},
- {name: 'feature' + i, type: 'TABLE', value: new Table('featureTable', [
- {name: 'featureParams', type: 'USHORT', value: feature.featureParams} ].concat(ushortList('lookupListIndex', feature.lookupListIndexes)))}
- ];
- })
- );
-}
-FeatureList.prototype = Object.create(Table.prototype);
-FeatureList.prototype.constructor = FeatureList;
-
-/**
- * @exports opentype.LookupList
- * @class
- * @param {opentype.Table}
- * @param {Object}
- * @constructor
- * @extends opentype.Table
- */
-function LookupList(lookupListTable, subtableMakers) {
- Table.call(this, 'lookupListTable', tableList('lookup', lookupListTable, function(lookupTable) {
- var subtableCallback = subtableMakers[lookupTable.lookupType];
- check.assert(!!subtableCallback, 'Unable to write GSUB lookup type ' + lookupTable.lookupType + ' tables.');
- return new Table('lookupTable', [
- {name: 'lookupType', type: 'USHORT', value: lookupTable.lookupType},
- {name: 'lookupFlag', type: 'USHORT', value: lookupTable.lookupFlag}
- ].concat(tableList('subtable', lookupTable.subtables, subtableCallback)));
- }));
-}
-LookupList.prototype = Object.create(Table.prototype);
-LookupList.prototype.constructor = LookupList;
-
-// Record = same as Table, but inlined (a Table has an offset and its data is further in the stream)
-// Don't use offsets inside Records (probable bug), only in Tables.
-var table = {
- Table: Table,
- Record: Table,
- Coverage: Coverage,
- ScriptList: ScriptList,
- FeatureList: FeatureList,
- LookupList: LookupList,
- ushortList: ushortList,
- tableList: tableList,
- recordList: recordList,
-};
-
-// Parsing utility functions
-
-// Retrieve an unsigned byte from the DataView.
-function getByte(dataView, offset) {
- return dataView.getUint8(offset);
-}
-
-// Retrieve an unsigned 16-bit short from the DataView.
-// The value is stored in big endian.
-function getUShort(dataView, offset) {
- return dataView.getUint16(offset, false);
-}
-
-// Retrieve a signed 16-bit short from the DataView.
-// The value is stored in big endian.
-function getShort(dataView, offset) {
- return dataView.getInt16(offset, false);
-}
-
-// Retrieve an unsigned 32-bit long from the DataView.
-// The value is stored in big endian.
-function getULong(dataView, offset) {
- return dataView.getUint32(offset, false);
-}
-
-// Retrieve a 32-bit signed fixed-point number (16.16) from the DataView.
-// The value is stored in big endian.
-function getFixed(dataView, offset) {
- var decimal = dataView.getInt16(offset, false);
- var fraction = dataView.getUint16(offset + 2, false);
- return decimal + fraction / 65535;
-}
-
-// Retrieve a 4-character tag from the DataView.
-// Tags are used to identify tables.
-function getTag(dataView, offset) {
- var tag = '';
- for (var i = offset; i < offset + 4; i += 1) {
- tag += String.fromCharCode(dataView.getInt8(i));
- }
-
- return tag;
-}
-
-// Retrieve an offset from the DataView.
-// Offsets are 1 to 4 bytes in length, depending on the offSize argument.
-function getOffset(dataView, offset, offSize) {
- var v = 0;
- for (var i = 0; i < offSize; i += 1) {
- v <<= 8;
- v += dataView.getUint8(offset + i);
- }
-
- return v;
-}
-
-// Retrieve a number of bytes from start offset to the end offset from the DataView.
-function getBytes(dataView, startOffset, endOffset) {
- var bytes = [];
- for (var i = startOffset; i < endOffset; i += 1) {
- bytes.push(dataView.getUint8(i));
- }
-
- return bytes;
-}
-
-// Convert the list of bytes to a string.
-function bytesToString(bytes) {
- var s = '';
- for (var i = 0; i < bytes.length; i += 1) {
- s += String.fromCharCode(bytes[i]);
- }
-
- return s;
-}
-
-var typeOffsets = {
- byte: 1,
- uShort: 2,
- short: 2,
- uLong: 4,
- fixed: 4,
- longDateTime: 8,
- tag: 4
-};
-
-// A stateful parser that changes the offset whenever a value is retrieved.
-// The data is a DataView.
-function Parser(data, offset) {
- this.data = data;
- this.offset = offset;
- this.relativeOffset = 0;
-}
-
-Parser.prototype.parseByte = function() {
- var v = this.data.getUint8(this.offset + this.relativeOffset);
- this.relativeOffset += 1;
- return v;
-};
-
-Parser.prototype.parseChar = function() {
- var v = this.data.getInt8(this.offset + this.relativeOffset);
- this.relativeOffset += 1;
- return v;
-};
-
-Parser.prototype.parseCard8 = Parser.prototype.parseByte;
-
-Parser.prototype.parseUShort = function() {
- var v = this.data.getUint16(this.offset + this.relativeOffset);
- this.relativeOffset += 2;
- return v;
-};
-
-Parser.prototype.parseCard16 = Parser.prototype.parseUShort;
-Parser.prototype.parseSID = Parser.prototype.parseUShort;
-Parser.prototype.parseOffset16 = Parser.prototype.parseUShort;
-
-Parser.prototype.parseShort = function() {
- var v = this.data.getInt16(this.offset + this.relativeOffset);
- this.relativeOffset += 2;
- return v;
-};
-
-Parser.prototype.parseF2Dot14 = function() {
- var v = this.data.getInt16(this.offset + this.relativeOffset) / 16384;
- this.relativeOffset += 2;
- return v;
-};
-
-Parser.prototype.parseULong = function() {
- var v = getULong(this.data, this.offset + this.relativeOffset);
- this.relativeOffset += 4;
- return v;
-};
-
-Parser.prototype.parseOffset32 = Parser.prototype.parseULong;
-
-Parser.prototype.parseFixed = function() {
- var v = getFixed(this.data, this.offset + this.relativeOffset);
- this.relativeOffset += 4;
- return v;
-};
-
-Parser.prototype.parseString = function(length) {
- var dataView = this.data;
- var offset = this.offset + this.relativeOffset;
- var string = '';
- this.relativeOffset += length;
- for (var i = 0; i < length; i++) {
- string += String.fromCharCode(dataView.getUint8(offset + i));
- }
-
- return string;
-};
-
-Parser.prototype.parseTag = function() {
- return this.parseString(4);
-};
-
-// LONGDATETIME is a 64-bit integer.
-// JavaScript and unix timestamps traditionally use 32 bits, so we
-// only take the last 32 bits.
-// + Since until 2038 those bits will be filled by zeros we can ignore them.
-Parser.prototype.parseLongDateTime = function() {
- var v = getULong(this.data, this.offset + this.relativeOffset + 4);
- // Subtract seconds between 01/01/1904 and 01/01/1970
- // to convert Apple Mac timestamp to Standard Unix timestamp
- v -= 2082844800;
- this.relativeOffset += 8;
- return v;
-};
-
-Parser.prototype.parseVersion = function(minorBase) {
- var major = getUShort(this.data, this.offset + this.relativeOffset);
-
- // How to interpret the minor version is very vague in the spec. 0x5000 is 5, 0x1000 is 1
- // Default returns the correct number if minor = 0xN000 where N is 0-9
- // Set minorBase to 1 for tables that use minor = N where N is 0-9
- var minor = getUShort(this.data, this.offset + this.relativeOffset + 2);
- this.relativeOffset += 4;
- if (minorBase === undefined) { minorBase = 0x1000; }
- return major + minor / minorBase / 10;
-};
-
-Parser.prototype.skip = function(type, amount) {
- if (amount === undefined) {
- amount = 1;
- }
-
- this.relativeOffset += typeOffsets[type] * amount;
-};
-
-///// Parsing lists and records ///////////////////////////////
-
-// Parse a list of 32 bit unsigned integers.
-Parser.prototype.parseULongList = function(count) {
- if (count === undefined) { count = this.parseULong(); }
- var offsets = new Array(count);
- var dataView = this.data;
- var offset = this.offset + this.relativeOffset;
- for (var i = 0; i < count; i++) {
- offsets[i] = dataView.getUint32(offset);
- offset += 4;
- }
-
- this.relativeOffset += count * 4;
- return offsets;
-};
-
-// Parse a list of 16 bit unsigned integers. The length of the list can be read on the stream
-// or provided as an argument.
-Parser.prototype.parseOffset16List =
-Parser.prototype.parseUShortList = function(count) {
- if (count === undefined) { count = this.parseUShort(); }
- var offsets = new Array(count);
- var dataView = this.data;
- var offset = this.offset + this.relativeOffset;
- for (var i = 0; i < count; i++) {
- offsets[i] = dataView.getUint16(offset);
- offset += 2;
- }
-
- this.relativeOffset += count * 2;
- return offsets;
-};
-
-// Parses a list of 16 bit signed integers.
-Parser.prototype.parseShortList = function(count) {
- var list = new Array(count);
- var dataView = this.data;
- var offset = this.offset + this.relativeOffset;
- for (var i = 0; i < count; i++) {
- list[i] = dataView.getInt16(offset);
- offset += 2;
- }
-
- this.relativeOffset += count * 2;
- return list;
-};
-
-// Parses a list of bytes.
-Parser.prototype.parseByteList = function(count) {
- var list = new Array(count);
- var dataView = this.data;
- var offset = this.offset + this.relativeOffset;
- for (var i = 0; i < count; i++) {
- list[i] = dataView.getUint8(offset++);
- }
-
- this.relativeOffset += count;
- return list;
-};
-
-/**
- * Parse a list of items.
- * Record count is optional, if omitted it is read from the stream.
- * itemCallback is one of the Parser methods.
- */
-Parser.prototype.parseList = function(count, itemCallback) {
- if (!itemCallback) {
- itemCallback = count;
- count = this.parseUShort();
- }
- var list = new Array(count);
- for (var i = 0; i < count; i++) {
- list[i] = itemCallback.call(this);
- }
- return list;
-};
-
-Parser.prototype.parseList32 = function(count, itemCallback) {
- if (!itemCallback) {
- itemCallback = count;
- count = this.parseULong();
- }
- var list = new Array(count);
- for (var i = 0; i < count; i++) {
- list[i] = itemCallback.call(this);
- }
- return list;
-};
-
-/**
- * Parse a list of records.
- * Record count is optional, if omitted it is read from the stream.
- * Example of recordDescription: { sequenceIndex: Parser.uShort, lookupListIndex: Parser.uShort }
- */
-Parser.prototype.parseRecordList = function(count, recordDescription) {
- // If the count argument is absent, read it in the stream.
- if (!recordDescription) {
- recordDescription = count;
- count = this.parseUShort();
- }
- var records = new Array(count);
- var fields = Object.keys(recordDescription);
- for (var i = 0; i < count; i++) {
- var rec = {};
- for (var j = 0; j < fields.length; j++) {
- var fieldName = fields[j];
- var fieldType = recordDescription[fieldName];
- rec[fieldName] = fieldType.call(this);
- }
- records[i] = rec;
- }
- return records;
-};
-
-Parser.prototype.parseRecordList32 = function(count, recordDescription) {
- // If the count argument is absent, read it in the stream.
- if (!recordDescription) {
- recordDescription = count;
- count = this.parseULong();
- }
- var records = new Array(count);
- var fields = Object.keys(recordDescription);
- for (var i = 0; i < count; i++) {
- var rec = {};
- for (var j = 0; j < fields.length; j++) {
- var fieldName = fields[j];
- var fieldType = recordDescription[fieldName];
- rec[fieldName] = fieldType.call(this);
- }
- records[i] = rec;
- }
- return records;
-};
-
-// Parse a data structure into an object
-// Example of description: { sequenceIndex: Parser.uShort, lookupListIndex: Parser.uShort }
-Parser.prototype.parseStruct = function(description) {
- if (typeof description === 'function') {
- return description.call(this);
- } else {
- var fields = Object.keys(description);
- var struct = {};
- for (var j = 0; j < fields.length; j++) {
- var fieldName = fields[j];
- var fieldType = description[fieldName];
- struct[fieldName] = fieldType.call(this);
- }
- return struct;
- }
-};
-
-/**
- * Parse a GPOS valueRecord
- * https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record
- * valueFormat is optional, if omitted it is read from the stream.
- */
-Parser.prototype.parseValueRecord = function(valueFormat) {
- if (valueFormat === undefined) {
- valueFormat = this.parseUShort();
- }
- if (valueFormat === 0) {
- // valueFormat2 in kerning pairs is most often 0
- // in this case return undefined instead of an empty object, to save space
- return;
- }
- var valueRecord = {};
-
- if (valueFormat & 0x0001) { valueRecord.xPlacement = this.parseShort(); }
- if (valueFormat & 0x0002) { valueRecord.yPlacement = this.parseShort(); }
- if (valueFormat & 0x0004) { valueRecord.xAdvance = this.parseShort(); }
- if (valueFormat & 0x0008) { valueRecord.yAdvance = this.parseShort(); }
-
- // Device table (non-variable font) / VariationIndex table (variable font) not supported
- // https://docs.microsoft.com/fr-fr/typography/opentype/spec/chapter2#devVarIdxTbls
- if (valueFormat & 0x0010) { valueRecord.xPlaDevice = undefined; this.parseShort(); }
- if (valueFormat & 0x0020) { valueRecord.yPlaDevice = undefined; this.parseShort(); }
- if (valueFormat & 0x0040) { valueRecord.xAdvDevice = undefined; this.parseShort(); }
- if (valueFormat & 0x0080) { valueRecord.yAdvDevice = undefined; this.parseShort(); }
-
- return valueRecord;
-};
-
-/**
- * Parse a list of GPOS valueRecords
- * https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record
- * valueFormat and valueCount are read from the stream.
- */
-Parser.prototype.parseValueRecordList = function() {
- var valueFormat = this.parseUShort();
- var valueCount = this.parseUShort();
- var values = new Array(valueCount);
- for (var i = 0; i < valueCount; i++) {
- values[i] = this.parseValueRecord(valueFormat);
- }
- return values;
-};
-
-Parser.prototype.parsePointer = function(description) {
- var structOffset = this.parseOffset16();
- if (structOffset > 0) {
- // NULL offset => return undefined
- return new Parser(this.data, this.offset + structOffset).parseStruct(description);
- }
- return undefined;
-};
-
-Parser.prototype.parsePointer32 = function(description) {
- var structOffset = this.parseOffset32();
- if (structOffset > 0) {
- // NULL offset => return undefined
- return new Parser(this.data, this.offset + structOffset).parseStruct(description);
- }
- return undefined;
-};
-
-/**
- * Parse a list of offsets to lists of 16-bit integers,
- * or a list of offsets to lists of offsets to any kind of items.
- * If itemCallback is not provided, a list of list of UShort is assumed.
- * If provided, itemCallback is called on each item and must parse the item.
- * See examples in tables/gsub.js
- */
-Parser.prototype.parseListOfLists = function(itemCallback) {
- var offsets = this.parseOffset16List();
- var count = offsets.length;
- var relativeOffset = this.relativeOffset;
- var list = new Array(count);
- for (var i = 0; i < count; i++) {
- var start = offsets[i];
- if (start === 0) {
- // NULL offset
- // Add i as owned property to list. Convenient with assert.
- list[i] = undefined;
- continue;
- }
- this.relativeOffset = start;
- if (itemCallback) {
- var subOffsets = this.parseOffset16List();
- var subList = new Array(subOffsets.length);
- for (var j = 0; j < subOffsets.length; j++) {
- this.relativeOffset = start + subOffsets[j];
- subList[j] = itemCallback.call(this);
- }
- list[i] = subList;
- } else {
- list[i] = this.parseUShortList();
- }
- }
- this.relativeOffset = relativeOffset;
- return list;
-};
-
-///// Complex tables parsing //////////////////////////////////
-
-// Parse a coverage table in a GSUB, GPOS or GDEF table.
-// https://www.microsoft.com/typography/OTSPEC/chapter2.htm
-// parser.offset must point to the start of the table containing the coverage.
-Parser.prototype.parseCoverage = function() {
- var startOffset = this.offset + this.relativeOffset;
- var format = this.parseUShort();
- var count = this.parseUShort();
- if (format === 1) {
- return {
- format: 1,
- glyphs: this.parseUShortList(count)
- };
- } else if (format === 2) {
- var ranges = new Array(count);
- for (var i = 0; i < count; i++) {
- ranges[i] = {
- start: this.parseUShort(),
- end: this.parseUShort(),
- index: this.parseUShort()
- };
- }
- return {
- format: 2,
- ranges: ranges
- };
- }
- throw new Error('0x' + startOffset.toString(16) + ': Coverage format must be 1 or 2.');
-};
-
-// Parse a Class Definition Table in a GSUB, GPOS or GDEF table.
-// https://www.microsoft.com/typography/OTSPEC/chapter2.htm
-Parser.prototype.parseClassDef = function() {
- var startOffset = this.offset + this.relativeOffset;
- var format = this.parseUShort();
- if (format === 1) {
- return {
- format: 1,
- startGlyph: this.parseUShort(),
- classes: this.parseUShortList()
- };
- } else if (format === 2) {
- return {
- format: 2,
- ranges: this.parseRecordList({
- start: Parser.uShort,
- end: Parser.uShort,
- classId: Parser.uShort
- })
- };
- }
- throw new Error('0x' + startOffset.toString(16) + ': ClassDef format must be 1 or 2.');
-};
-
-///// Static methods ///////////////////////////////////
-// These convenience methods can be used as callbacks and should be called with "this" context set to a Parser instance.
-
-Parser.list = function(count, itemCallback) {
- return function() {
- return this.parseList(count, itemCallback);
- };
-};
-
-Parser.list32 = function(count, itemCallback) {
- return function() {
- return this.parseList32(count, itemCallback);
- };
-};
-
-Parser.recordList = function(count, recordDescription) {
- return function() {
- return this.parseRecordList(count, recordDescription);
- };
-};
-
-Parser.recordList32 = function(count, recordDescription) {
- return function() {
- return this.parseRecordList32(count, recordDescription);
- };
-};
-
-Parser.pointer = function(description) {
- return function() {
- return this.parsePointer(description);
- };
-};
-
-Parser.pointer32 = function(description) {
- return function() {
- return this.parsePointer32(description);
- };
-};
-
-Parser.tag = Parser.prototype.parseTag;
-Parser.byte = Parser.prototype.parseByte;
-Parser.uShort = Parser.offset16 = Parser.prototype.parseUShort;
-Parser.uShortList = Parser.prototype.parseUShortList;
-Parser.uLong = Parser.offset32 = Parser.prototype.parseULong;
-Parser.uLongList = Parser.prototype.parseULongList;
-Parser.struct = Parser.prototype.parseStruct;
-Parser.coverage = Parser.prototype.parseCoverage;
-Parser.classDef = Parser.prototype.parseClassDef;
-
-///// Script, Feature, Lookup lists ///////////////////////////////////////////////
-// https://www.microsoft.com/typography/OTSPEC/chapter2.htm
-
-var langSysTable = {
- reserved: Parser.uShort,
- reqFeatureIndex: Parser.uShort,
- featureIndexes: Parser.uShortList
-};
-
-Parser.prototype.parseScriptList = function() {
- return this.parsePointer(Parser.recordList({
- tag: Parser.tag,
- script: Parser.pointer({
- defaultLangSys: Parser.pointer(langSysTable),
- langSysRecords: Parser.recordList({
- tag: Parser.tag,
- langSys: Parser.pointer(langSysTable)
- })
- })
- })) || [];
-};
-
-Parser.prototype.parseFeatureList = function() {
- return this.parsePointer(Parser.recordList({
- tag: Parser.tag,
- feature: Parser.pointer({
- featureParams: Parser.offset16,
- lookupListIndexes: Parser.uShortList
- })
- })) || [];
-};
-
-Parser.prototype.parseLookupList = function(lookupTableParsers) {
- return this.parsePointer(Parser.list(Parser.pointer(function() {
- var lookupType = this.parseUShort();
- check.argument(1 <= lookupType && lookupType <= 9, 'GPOS/GSUB lookup type ' + lookupType + ' unknown.');
- var lookupFlag = this.parseUShort();
- var useMarkFilteringSet = lookupFlag & 0x10;
- return {
- lookupType: lookupType,
- lookupFlag: lookupFlag,
- subtables: this.parseList(Parser.pointer(lookupTableParsers[lookupType])),
- markFilteringSet: useMarkFilteringSet ? this.parseUShort() : undefined
- };
- }))) || [];
-};
-
-Parser.prototype.parseFeatureVariationsList = function() {
- return this.parsePointer32(function() {
- var majorVersion = this.parseUShort();
- var minorVersion = this.parseUShort();
- check.argument(majorVersion === 1 && minorVersion < 1, 'GPOS/GSUB feature variations table unknown.');
- var featureVariations = this.parseRecordList32({
- conditionSetOffset: Parser.offset32,
- featureTableSubstitutionOffset: Parser.offset32
- });
- return featureVariations;
- }) || [];
-};
-
-var parse = {
- getByte: getByte,
- getCard8: getByte,
- getUShort: getUShort,
- getCard16: getUShort,
- getShort: getShort,
- getULong: getULong,
- getFixed: getFixed,
- getTag: getTag,
- getOffset: getOffset,
- getBytes: getBytes,
- bytesToString: bytesToString,
- Parser: Parser,
-};
-
-// The `cmap` table stores the mappings from characters to glyphs.
-
-function parseCmapTableFormat12(cmap, p) {
- //Skip reserved.
- p.parseUShort();
-
- // Length in bytes of the sub-tables.
- cmap.length = p.parseULong();
- cmap.language = p.parseULong();
-
- var groupCount;
- cmap.groupCount = groupCount = p.parseULong();
- cmap.glyphIndexMap = {};
-
- for (var i = 0; i < groupCount; i += 1) {
- var startCharCode = p.parseULong();
- var endCharCode = p.parseULong();
- var startGlyphId = p.parseULong();
-
- for (var c = startCharCode; c <= endCharCode; c += 1) {
- cmap.glyphIndexMap[c] = startGlyphId;
- startGlyphId++;
- }
- }
-}
-
-function parseCmapTableFormat4(cmap, p, data, start, offset) {
- // Length in bytes of the sub-tables.
- cmap.length = p.parseUShort();
- cmap.language = p.parseUShort();
-
- // segCount is stored x 2.
- var segCount;
- cmap.segCount = segCount = p.parseUShort() >> 1;
-
- // Skip searchRange, entrySelector, rangeShift.
- p.skip('uShort', 3);
-
- // The "unrolled" mapping from character codes to glyph indices.
- cmap.glyphIndexMap = {};
- var endCountParser = new parse.Parser(data, start + offset + 14);
- var startCountParser = new parse.Parser(data, start + offset + 16 + segCount * 2);
- var idDeltaParser = new parse.Parser(data, start + offset + 16 + segCount * 4);
- var idRangeOffsetParser = new parse.Parser(data, start + offset + 16 + segCount * 6);
- var glyphIndexOffset = start + offset + 16 + segCount * 8;
- for (var i = 0; i < segCount - 1; i += 1) {
- var glyphIndex = (void 0);
- var endCount = endCountParser.parseUShort();
- var startCount = startCountParser.parseUShort();
- var idDelta = idDeltaParser.parseShort();
- var idRangeOffset = idRangeOffsetParser.parseUShort();
- for (var c = startCount; c <= endCount; c += 1) {
- if (idRangeOffset !== 0) {
- // The idRangeOffset is relative to the current position in the idRangeOffset array.
- // Take the current offset in the idRangeOffset array.
- glyphIndexOffset = (idRangeOffsetParser.offset + idRangeOffsetParser.relativeOffset - 2);
-
- // Add the value of the idRangeOffset, which will move us into the glyphIndex array.
- glyphIndexOffset += idRangeOffset;
-
- // Then add the character index of the current segment, multiplied by 2 for USHORTs.
- glyphIndexOffset += (c - startCount) * 2;
- glyphIndex = parse.getUShort(data, glyphIndexOffset);
- if (glyphIndex !== 0) {
- glyphIndex = (glyphIndex + idDelta) & 0xFFFF;
- }
- } else {
- glyphIndex = (c + idDelta) & 0xFFFF;
- }
-
- cmap.glyphIndexMap[c] = glyphIndex;
- }
- }
-}
-
-// Parse the `cmap` table. This table stores the mappings from characters to glyphs.
-// There are many available formats, but we only support the Windows format 4 and 12.
-// This function returns a `CmapEncoding` object or null if no supported format could be found.
-function parseCmapTable(data, start) {
- var cmap = {};
- cmap.version = parse.getUShort(data, start);
- check.argument(cmap.version === 0, 'cmap table version should be 0.');
-
- // The cmap table can contain many sub-tables, each with their own format.
- // We're only interested in a "platform 0" (Unicode format) and "platform 3" (Windows format) table.
- cmap.numTables = parse.getUShort(data, start + 2);
- var offset = -1;
- for (var i = cmap.numTables - 1; i >= 0; i -= 1) {
- var platformId = parse.getUShort(data, start + 4 + (i * 8));
- var encodingId = parse.getUShort(data, start + 4 + (i * 8) + 2);
- if ((platformId === 3 && (encodingId === 0 || encodingId === 1 || encodingId === 10)) ||
- (platformId === 0 && (encodingId === 0 || encodingId === 1 || encodingId === 2 || encodingId === 3 || encodingId === 4))) {
- offset = parse.getULong(data, start + 4 + (i * 8) + 4);
- break;
- }
- }
-
- if (offset === -1) {
- // There is no cmap table in the font that we support.
- throw new Error('No valid cmap sub-tables found.');
- }
-
- var p = new parse.Parser(data, start + offset);
- cmap.format = p.parseUShort();
-
- if (cmap.format === 12) {
- parseCmapTableFormat12(cmap, p);
- } else if (cmap.format === 4) {
- parseCmapTableFormat4(cmap, p, data, start, offset);
- } else {
- throw new Error('Only format 4 and 12 cmap tables are supported (found format ' + cmap.format + ').');
- }
-
- return cmap;
-}
-
-function addSegment(t, code, glyphIndex) {
- t.segments.push({
- end: code,
- start: code,
- delta: -(code - glyphIndex),
- offset: 0,
- glyphIndex: glyphIndex
- });
-}
-
-function addTerminatorSegment(t) {
- t.segments.push({
- end: 0xFFFF,
- start: 0xFFFF,
- delta: 1,
- offset: 0
- });
-}
-
-// Make cmap table, format 4 by default, 12 if needed only
-function makeCmapTable(glyphs) {
- // Plan 0 is the base Unicode Plan but emojis, for example are on another plan, and needs cmap 12 format (with 32bit)
- var isPlan0Only = true;
- var i;
-
- // Check if we need to add cmap format 12 or if format 4 only is fine
- for (i = glyphs.length - 1; i > 0; i -= 1) {
- var g = glyphs.get(i);
- if (g.unicode > 65535) {
- console.log('Adding CMAP format 12 (needed!)');
- isPlan0Only = false;
- break;
- }
- }
-
- var cmapTable = [
- {name: 'version', type: 'USHORT', value: 0},
- {name: 'numTables', type: 'USHORT', value: isPlan0Only ? 1 : 2},
-
- // CMAP 4 header
- {name: 'platformID', type: 'USHORT', value: 3},
- {name: 'encodingID', type: 'USHORT', value: 1},
- {name: 'offset', type: 'ULONG', value: isPlan0Only ? 12 : (12 + 8)}
- ];
-
- if (!isPlan0Only)
- { cmapTable = cmapTable.concat([
- // CMAP 12 header
- {name: 'cmap12PlatformID', type: 'USHORT', value: 3}, // We encode only for PlatformID = 3 (Windows) because it is supported everywhere
- {name: 'cmap12EncodingID', type: 'USHORT', value: 10},
- {name: 'cmap12Offset', type: 'ULONG', value: 0}
- ]); }
-
- cmapTable = cmapTable.concat([
- // CMAP 4 Subtable
- {name: 'format', type: 'USHORT', value: 4},
- {name: 'cmap4Length', type: 'USHORT', value: 0},
- {name: 'language', type: 'USHORT', value: 0},
- {name: 'segCountX2', type: 'USHORT', value: 0},
- {name: 'searchRange', type: 'USHORT', value: 0},
- {name: 'entrySelector', type: 'USHORT', value: 0},
- {name: 'rangeShift', type: 'USHORT', value: 0}
- ]);
-
- var t = new table.Table('cmap', cmapTable);
-
- t.segments = [];
- for (i = 0; i < glyphs.length; i += 1) {
- var glyph = glyphs.get(i);
- for (var j = 0; j < glyph.unicodes.length; j += 1) {
- addSegment(t, glyph.unicodes[j], i);
- }
-
- t.segments = t.segments.sort(function (a, b) {
- return a.start - b.start;
- });
- }
-
- addTerminatorSegment(t);
-
- var segCount = t.segments.length;
- var segCountToRemove = 0;
-
- // CMAP 4
- // Set up parallel segment arrays.
- var endCounts = [];
- var startCounts = [];
- var idDeltas = [];
- var idRangeOffsets = [];
- var glyphIds = [];
-
- // CMAP 12
- var cmap12Groups = [];
-
- // Reminder this loop is not following the specification at 100%
- // The specification -> find suites of characters and make a group
- // Here we're doing one group for each letter
- // Doing as the spec can save 8 times (or more) space
- for (i = 0; i < segCount; i += 1) {
- var segment = t.segments[i];
-
- // CMAP 4
- if (segment.end <= 65535 && segment.start <= 65535) {
- endCounts = endCounts.concat({name: 'end_' + i, type: 'USHORT', value: segment.end});
- startCounts = startCounts.concat({name: 'start_' + i, type: 'USHORT', value: segment.start});
- idDeltas = idDeltas.concat({name: 'idDelta_' + i, type: 'SHORT', value: segment.delta});
- idRangeOffsets = idRangeOffsets.concat({name: 'idRangeOffset_' + i, type: 'USHORT', value: segment.offset});
- if (segment.glyphId !== undefined) {
- glyphIds = glyphIds.concat({name: 'glyph_' + i, type: 'USHORT', value: segment.glyphId});
- }
- } else {
- // Skip Unicode > 65535 (16bit unsigned max) for CMAP 4, will be added in CMAP 12
- segCountToRemove += 1;
- }
-
- // CMAP 12
- // Skip Terminator Segment
- if (!isPlan0Only && segment.glyphIndex !== undefined) {
- cmap12Groups = cmap12Groups.concat({name: 'cmap12Start_' + i, type: 'ULONG', value: segment.start});
- cmap12Groups = cmap12Groups.concat({name: 'cmap12End_' + i, type: 'ULONG', value: segment.end});
- cmap12Groups = cmap12Groups.concat({name: 'cmap12Glyph_' + i, type: 'ULONG', value: segment.glyphIndex});
- }
- }
-
- // CMAP 4 Subtable
- t.segCountX2 = (segCount - segCountToRemove) * 2;
- t.searchRange = Math.pow(2, Math.floor(Math.log((segCount - segCountToRemove)) / Math.log(2))) * 2;
- t.entrySelector = Math.log(t.searchRange / 2) / Math.log(2);
- t.rangeShift = t.segCountX2 - t.searchRange;
-
- t.fields = t.fields.concat(endCounts);
- t.fields.push({name: 'reservedPad', type: 'USHORT', value: 0});
- t.fields = t.fields.concat(startCounts);
- t.fields = t.fields.concat(idDeltas);
- t.fields = t.fields.concat(idRangeOffsets);
- t.fields = t.fields.concat(glyphIds);
-
- t.cmap4Length = 14 + // Subtable header
- endCounts.length * 2 +
- 2 + // reservedPad
- startCounts.length * 2 +
- idDeltas.length * 2 +
- idRangeOffsets.length * 2 +
- glyphIds.length * 2;
-
- if (!isPlan0Only) {
- // CMAP 12 Subtable
- var cmap12Length = 16 + // Subtable header
- cmap12Groups.length * 4;
-
- t.cmap12Offset = 12 + (2 * 2) + 4 + t.cmap4Length;
- t.fields = t.fields.concat([
- {name: 'cmap12Format', type: 'USHORT', value: 12},
- {name: 'cmap12Reserved', type: 'USHORT', value: 0},
- {name: 'cmap12Length', type: 'ULONG', value: cmap12Length},
- {name: 'cmap12Language', type: 'ULONG', value: 0},
- {name: 'cmap12nGroups', type: 'ULONG', value: cmap12Groups.length / 3}
- ]);
-
- t.fields = t.fields.concat(cmap12Groups);
- }
-
- return t;
-}
-
-var cmap = { parse: parseCmapTable, make: makeCmapTable };
-
-// Glyph encoding
-
-var cffStandardStrings = [
- '.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright',
- 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two',
- 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater',
- 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
- 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore',
- 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
- 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', 'exclamdown', 'cent', 'sterling',
- 'fraction', 'yen', 'florin', 'section', 'currency', 'quotesingle', 'quotedblleft', 'guillemotleft',
- 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', 'daggerdbl', 'periodcentered', 'paragraph',
- 'bullet', 'quotesinglbase', 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand',
- 'questiondown', 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'dieresis', 'ring',
- 'cedilla', 'hungarumlaut', 'ogonek', 'caron', 'emdash', 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE',
- 'ordmasculine', 'ae', 'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior', 'logicalnot', 'mu',
- 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn', 'onequarter', 'divide', 'brokenbar', 'degree', 'thorn',
- 'threequarters', 'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior', 'copyright',
- 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring', 'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex',
- 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute', 'Ocircumflex',
- 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute', 'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute',
- 'Ydieresis', 'Zcaron', 'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', 'ccedilla', 'eacute',
- 'ecircumflex', 'edieresis', 'egrave', 'iacute', 'icircumflex', 'idieresis', 'igrave', 'ntilde', 'oacute',
- 'ocircumflex', 'odieresis', 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', 'udieresis', 'ugrave',
- 'yacute', 'ydieresis', 'zcaron', 'exclamsmall', 'Hungarumlautsmall', 'dollaroldstyle', 'dollarsuperior',
- 'ampersandsmall', 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', '266 ff', 'onedotenleader',
- 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle',
- 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'commasuperior', 'threequartersemdash', 'periodsuperior',
- 'questionsmall', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', 'isuperior', 'lsuperior',
- 'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior', 'tsuperior', 'ff', 'ffi', 'ffl',
- 'parenleftinferior', 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall',
- 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall',
- 'Msmall', 'Nsmall', 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall',
- 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', 'exclamdownsmall',
- 'centoldstyle', 'Lslashsmall', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', 'Brevesmall', 'Caronsmall',
- 'Dotaccentsmall', 'Macronsmall', 'figuredash', 'hypheninferior', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall',
- 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds',
- 'zerosuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior',
- 'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior',
- 'seveninferior', 'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior',
- 'commainferior', 'Agravesmall', 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall',
- 'Aringsmall', 'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall',
- 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall',
- 'Oacutesmall', 'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall',
- 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', 'Ydieresissmall', '001.000',
- '001.001', '001.002', '001.003', 'Black', 'Bold', 'Book', 'Light', 'Medium', 'Regular', 'Roman', 'Semibold'];
-
-var cffStandardEncoding = [
- '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
- '', '', '', '', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright',
- 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two',
- 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater',
- 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
- 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore',
- 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
- 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', '', '', '', '', '', '', '', '',
- '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
- 'exclamdown', 'cent', 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency', 'quotesingle',
- 'quotedblleft', 'guillemotleft', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', '', 'endash', 'dagger',
- 'daggerdbl', 'periodcentered', '', 'paragraph', 'bullet', 'quotesinglbase', 'quotedblbase', 'quotedblright',
- 'guillemotright', 'ellipsis', 'perthousand', '', 'questiondown', '', 'grave', 'acute', 'circumflex', 'tilde',
- 'macron', 'breve', 'dotaccent', 'dieresis', '', 'ring', 'cedilla', '', 'hungarumlaut', 'ogonek', 'caron',
- 'emdash', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'AE', '', 'ordfeminine', '', '', '',
- '', 'Lslash', 'Oslash', 'OE', 'ordmasculine', '', '', '', '', '', 'ae', '', '', '', 'dotlessi', '', '',
- 'lslash', 'oslash', 'oe', 'germandbls'];
-
-var cffExpertEncoding = [
- '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
- '', '', '', '', 'space', 'exclamsmall', 'Hungarumlautsmall', '', 'dollaroldstyle', 'dollarsuperior',
- 'ampersandsmall', 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader',
- 'comma', 'hyphen', 'period', 'fraction', 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle',
- 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'colon',
- 'semicolon', 'commasuperior', 'threequartersemdash', 'periodsuperior', 'questionsmall', '', 'asuperior',
- 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', '', '', 'isuperior', '', '', 'lsuperior', 'msuperior',
- 'nsuperior', 'osuperior', '', '', 'rsuperior', 'ssuperior', 'tsuperior', '', 'ff', 'fi', 'fl', 'ffi', 'ffl',
- 'parenleftinferior', '', 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall',
- 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall',
- 'Msmall', 'Nsmall', 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall',
- 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', '', '', '', '', '', '', '',
- '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
- 'exclamdownsmall', 'centoldstyle', 'Lslashsmall', '', '', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall',
- 'Brevesmall', 'Caronsmall', '', 'Dotaccentsmall', '', '', 'Macronsmall', '', '', 'figuredash', 'hypheninferior',
- '', '', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', '', '', '', 'onequarter', 'onehalf', 'threequarters',
- 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', '',
- '', 'zerosuperior', 'onesuperior', 'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior',
- 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior', 'oneinferior', 'twoinferior',
- 'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior', 'eightinferior',
- 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior', 'commainferior', 'Agravesmall',
- 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall', 'AEsmall', 'Ccedillasmall',
- 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall', 'Igravesmall', 'Iacutesmall',
- 'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall',
- 'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', 'Uacutesmall',
- 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', 'Ydieresissmall'];
-
-var standardNames = [
- '.notdef', '.null', 'nonmarkingreturn', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent',
- 'ampersand', 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash',
- 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less',
- 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
- 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright',
- 'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
- 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde',
- 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', 'Ntilde', 'Odieresis', 'Udieresis', 'aacute', 'agrave',
- 'acircumflex', 'adieresis', 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', 'edieresis',
- 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', 'oacute', 'ograve', 'ocircumflex', 'odieresis',
- 'otilde', 'uacute', 'ugrave', 'ucircumflex', 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section',
- 'bullet', 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', 'acute', 'dieresis', 'notequal',
- 'AE', 'Oslash', 'infinity', 'plusminus', 'lessequal', 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation',
- 'product', 'pi', 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', 'oslash', 'questiondown',
- 'exclamdown', 'logicalnot', 'radical', 'florin', 'approxequal', 'Delta', 'guillemotleft', 'guillemotright',
- 'ellipsis', 'nonbreakingspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash', 'emdash', 'quotedblleft',
- 'quotedblright', 'quoteleft', 'quoteright', 'divide', 'lozenge', 'ydieresis', 'Ydieresis', 'fraction',
- 'currency', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered', 'quotesinglbase',
- 'quotedblbase', 'perthousand', 'Acircumflex', 'Ecircumflex', 'Aacute', 'Edieresis', 'Egrave', 'Iacute',
- 'Icircumflex', 'Idieresis', 'Igrave', 'Oacute', 'Ocircumflex', 'apple', 'Ograve', 'Uacute', 'Ucircumflex',
- 'Ugrave', 'dotlessi', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut',
- 'ogonek', 'caron', 'Lslash', 'lslash', 'Scaron', 'scaron', 'Zcaron', 'zcaron', 'brokenbar', 'Eth', 'eth',
- 'Yacute', 'yacute', 'Thorn', 'thorn', 'minus', 'multiply', 'onesuperior', 'twosuperior', 'threesuperior',
- 'onehalf', 'onequarter', 'threequarters', 'franc', 'Gbreve', 'gbreve', 'Idotaccent', 'Scedilla', 'scedilla',
- 'Cacute', 'cacute', 'Ccaron', 'ccaron', 'dcroat'];
-
-/**
- * This is the encoding used for fonts created from scratch.
- * It loops through all glyphs and finds the appropriate unicode value.
- * Since it's linear time, other encodings will be faster.
- * @exports opentype.DefaultEncoding
- * @class
- * @constructor
- * @param {opentype.Font}
- */
-function DefaultEncoding(font) {
- this.font = font;
-}
-
-DefaultEncoding.prototype.charToGlyphIndex = function(c) {
- var code = c.codePointAt(0);
- var glyphs = this.font.glyphs;
- if (glyphs) {
- for (var i = 0; i < glyphs.length; i += 1) {
- var glyph = glyphs.get(i);
- for (var j = 0; j < glyph.unicodes.length; j += 1) {
- if (glyph.unicodes[j] === code) {
- return i;
- }
- }
- }
- }
- return null;
-};
-
-/**
- * @exports opentype.CmapEncoding
- * @class
- * @constructor
- * @param {Object} cmap - a object with the cmap encoded data
- */
-function CmapEncoding(cmap) {
- this.cmap = cmap;
-}
-
-/**
- * @param {string} c - the character
- * @return {number} The glyph index.
- */
-CmapEncoding.prototype.charToGlyphIndex = function(c) {
- return this.cmap.glyphIndexMap[c.codePointAt(0)] || 0;
-};
-
-/**
- * @exports opentype.CffEncoding
- * @class
- * @constructor
- * @param {string} encoding - The encoding
- * @param {Array} charset - The character set.
- */
-function CffEncoding(encoding, charset) {
- this.encoding = encoding;
- this.charset = charset;
-}
-
-/**
- * @param {string} s - The character
- * @return {number} The index.
- */
-CffEncoding.prototype.charToGlyphIndex = function(s) {
- var code = s.codePointAt(0);
- var charName = this.encoding[code];
- return this.charset.indexOf(charName);
-};
-
-/**
- * @exports opentype.GlyphNames
- * @class
- * @constructor
- * @param {Object} post
- */
-function GlyphNames(post) {
- switch (post.version) {
- case 1:
- this.names = standardNames.slice();
- break;
- case 2:
- this.names = new Array(post.numberOfGlyphs);
- for (var i = 0; i < post.numberOfGlyphs; i++) {
- if (post.glyphNameIndex[i] < standardNames.length) {
- this.names[i] = standardNames[post.glyphNameIndex[i]];
- } else {
- this.names[i] = post.names[post.glyphNameIndex[i] - standardNames.length];
- }
- }
-
- break;
- case 2.5:
- this.names = new Array(post.numberOfGlyphs);
- for (var i$1 = 0; i$1 < post.numberOfGlyphs; i$1++) {
- this.names[i$1] = standardNames[i$1 + post.glyphNameIndex[i$1]];
- }
-
- break;
- case 3:
- this.names = [];
- break;
- default:
- this.names = [];
- break;
- }
-}
-
-/**
- * Gets the index of a glyph by name.
- * @param {string} name - The glyph name
- * @return {number} The index
- */
-GlyphNames.prototype.nameToGlyphIndex = function(name) {
- return this.names.indexOf(name);
-};
-
-/**
- * @param {number} gid
- * @return {string}
- */
-GlyphNames.prototype.glyphIndexToName = function(gid) {
- return this.names[gid];
-};
-
-function addGlyphNamesAll(font) {
- var glyph;
- var glyphIndexMap = font.tables.cmap.glyphIndexMap;
- var charCodes = Object.keys(glyphIndexMap);
-
- for (var i = 0; i < charCodes.length; i += 1) {
- var c = charCodes[i];
- var glyphIndex = glyphIndexMap[c];
- glyph = font.glyphs.get(glyphIndex);
- glyph.addUnicode(parseInt(c));
- }
-
- for (var i$1 = 0; i$1 < font.glyphs.length; i$1 += 1) {
- glyph = font.glyphs.get(i$1);
- if (font.cffEncoding) {
- if (font.isCIDFont) {
- glyph.name = 'gid' + i$1;
- } else {
- glyph.name = font.cffEncoding.charset[i$1];
- }
- } else if (font.glyphNames.names) {
- glyph.name = font.glyphNames.glyphIndexToName(i$1);
- }
- }
-}
-
-function addGlyphNamesToUnicodeMap(font) {
- font._IndexToUnicodeMap = {};
-
- var glyphIndexMap = font.tables.cmap.glyphIndexMap;
- var charCodes = Object.keys(glyphIndexMap);
-
- for (var i = 0; i < charCodes.length; i += 1) {
- var c = charCodes[i];
- var glyphIndex = glyphIndexMap[c];
- if (font._IndexToUnicodeMap[glyphIndex] === undefined) {
- font._IndexToUnicodeMap[glyphIndex] = {
- unicodes: [parseInt(c)]
- };
- } else {
- font._IndexToUnicodeMap[glyphIndex].unicodes.push(parseInt(c));
- }
- }
-}
-
-/**
- * @alias opentype.addGlyphNames
- * @param {opentype.Font}
- * @param {Object}
- */
-function addGlyphNames(font, opt) {
- if (opt.lowMemory) {
- addGlyphNamesToUnicodeMap(font);
- } else {
- addGlyphNamesAll(font);
- }
-}
-
-// Drawing utility functions.
-
-// Draw a line on the given context from point `x1,y1` to point `x2,y2`.
-function line(ctx, x1, y1, x2, y2) {
- ctx.beginPath();
- ctx.moveTo(x1, y1);
- ctx.lineTo(x2, y2);
- ctx.stroke();
-}
-
-var draw = { line: line };
-
-// The Glyph object
-// import glyf from './tables/glyf' Can't be imported here, because it's a circular dependency
-
-function getPathDefinition(glyph, path) {
- var _path = path || new Path();
- return {
- configurable: true,
-
- get: function() {
- if (typeof _path === 'function') {
- _path = _path();
- }
-
- return _path;
- },
-
- set: function(p) {
- _path = p;
- }
- };
-}
-/**
- * @typedef GlyphOptions
- * @type Object
- * @property {string} [name] - The glyph name
- * @property {number} [unicode]
- * @property {Array} [unicodes]
- * @property {number} [xMin]
- * @property {number} [yMin]
- * @property {number} [xMax]
- * @property {number} [yMax]
- * @property {number} [advanceWidth]
- */
-
-// A Glyph is an individual mark that often corresponds to a character.
-// Some glyphs, such as ligatures, are a combination of many characters.
-// Glyphs are the basic building blocks of a font.
-//
-// The `Glyph` class contains utility methods for drawing the path and its points.
-/**
- * @exports opentype.Glyph
- * @class
- * @param {GlyphOptions}
- * @constructor
- */
-function Glyph(options) {
- // By putting all the code on a prototype function (which is only declared once)
- // we reduce the memory requirements for larger fonts by some 2%
- this.bindConstructorValues(options);
-}
-
-/**
- * @param {GlyphOptions}
- */
-Glyph.prototype.bindConstructorValues = function(options) {
- this.index = options.index || 0;
-
- // These three values cannot be deferred for memory optimization:
- this.name = options.name || null;
- this.unicode = options.unicode || undefined;
- this.unicodes = options.unicodes || options.unicode !== undefined ? [options.unicode] : [];
-
- // But by binding these values only when necessary, we reduce can
- // the memory requirements by almost 3% for larger fonts.
- if ('xMin' in options) {
- this.xMin = options.xMin;
- }
-
- if ('yMin' in options) {
- this.yMin = options.yMin;
- }
-
- if ('xMax' in options) {
- this.xMax = options.xMax;
- }
-
- if ('yMax' in options) {
- this.yMax = options.yMax;
- }
-
- if ('advanceWidth' in options) {
- this.advanceWidth = options.advanceWidth;
- }
-
- // The path for a glyph is the most memory intensive, and is bound as a value
- // with a getter/setter to ensure we actually do path parsing only once the
- // path is actually needed by anything.
- Object.defineProperty(this, 'path', getPathDefinition(this, options.path));
-};
-
-/**
- * @param {number}
- */
-Glyph.prototype.addUnicode = function(unicode) {
- if (this.unicodes.length === 0) {
- this.unicode = unicode;
- }
-
- this.unicodes.push(unicode);
-};
-
-/**
- * Calculate the minimum bounding box for this glyph.
- * @return {opentype.BoundingBox}
- */
-Glyph.prototype.getBoundingBox = function() {
- return this.path.getBoundingBox();
-};
-
-/**
- * Convert the glyph to a Path we can draw on a drawing context.
- * @param {number} [x=0] - Horizontal position of the beginning of the text.
- * @param {number} [y=0] - Vertical position of the *baseline* of the text.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- * @param {Object=} options - xScale, yScale to stretch the glyph.
- * @param {opentype.Font} if hinting is to be used, the font
- * @return {opentype.Path}
- */
-Glyph.prototype.getPath = function(x, y, fontSize, options, font) {
- x = x !== undefined ? x : 0;
- y = y !== undefined ? y : 0;
- fontSize = fontSize !== undefined ? fontSize : 72;
- var commands;
- var hPoints;
- if (!options) { options = { }; }
- var xScale = options.xScale;
- var yScale = options.yScale;
-
- if (options.hinting && font && font.hinting) {
- // in case of hinting, the hinting engine takes care
- // of scaling the points (not the path) before hinting.
- hPoints = this.path && font.hinting.exec(this, fontSize);
- // in case the hinting engine failed hPoints is undefined
- // and thus reverts to plain rending
- }
-
- if (hPoints) {
- // Call font.hinting.getCommands instead of `glyf.getPath(hPoints).commands` to avoid a circular dependency
- commands = font.hinting.getCommands(hPoints);
- x = Math.round(x);
- y = Math.round(y);
- // TODO in case of hinting xyScaling is not yet supported
- xScale = yScale = 1;
- } else {
- commands = this.path.commands;
- var scale = 1 / (this.path.unitsPerEm || 1000) * fontSize;
- if (xScale === undefined) { xScale = scale; }
- if (yScale === undefined) { yScale = scale; }
- }
-
- var p = new Path();
- for (var i = 0; i < commands.length; i += 1) {
- var cmd = commands[i];
- if (cmd.type === 'M') {
- p.moveTo(x + (cmd.x * xScale), y + (-cmd.y * yScale));
- } else if (cmd.type === 'L') {
- p.lineTo(x + (cmd.x * xScale), y + (-cmd.y * yScale));
- } else if (cmd.type === 'Q') {
- p.quadraticCurveTo(x + (cmd.x1 * xScale), y + (-cmd.y1 * yScale),
- x + (cmd.x * xScale), y + (-cmd.y * yScale));
- } else if (cmd.type === 'C') {
- p.curveTo(x + (cmd.x1 * xScale), y + (-cmd.y1 * yScale),
- x + (cmd.x2 * xScale), y + (-cmd.y2 * yScale),
- x + (cmd.x * xScale), y + (-cmd.y * yScale));
- } else if (cmd.type === 'Z') {
- p.closePath();
- }
- }
-
- return p;
-};
-
-/**
- * Split the glyph into contours.
- * This function is here for backwards compatibility, and to
- * provide raw access to the TrueType glyph outlines.
- * @return {Array}
- */
-Glyph.prototype.getContours = function() {
- if (this.points === undefined) {
- return [];
- }
-
- var contours = [];
- var currentContour = [];
- for (var i = 0; i < this.points.length; i += 1) {
- var pt = this.points[i];
- currentContour.push(pt);
- if (pt.lastPointOfContour) {
- contours.push(currentContour);
- currentContour = [];
- }
- }
-
- check.argument(currentContour.length === 0, 'There are still points left in the current contour.');
- return contours;
-};
-
-/**
- * Calculate the xMin/yMin/xMax/yMax/lsb/rsb for a Glyph.
- * @return {Object}
- */
-Glyph.prototype.getMetrics = function() {
- var commands = this.path.commands;
- var xCoords = [];
- var yCoords = [];
- for (var i = 0; i < commands.length; i += 1) {
- var cmd = commands[i];
- if (cmd.type !== 'Z') {
- xCoords.push(cmd.x);
- yCoords.push(cmd.y);
- }
-
- if (cmd.type === 'Q' || cmd.type === 'C') {
- xCoords.push(cmd.x1);
- yCoords.push(cmd.y1);
- }
-
- if (cmd.type === 'C') {
- xCoords.push(cmd.x2);
- yCoords.push(cmd.y2);
- }
- }
-
- var metrics = {
- xMin: Math.min.apply(null, xCoords),
- yMin: Math.min.apply(null, yCoords),
- xMax: Math.max.apply(null, xCoords),
- yMax: Math.max.apply(null, yCoords),
- leftSideBearing: this.leftSideBearing
- };
-
- if (!isFinite(metrics.xMin)) {
- metrics.xMin = 0;
- }
-
- if (!isFinite(metrics.xMax)) {
- metrics.xMax = this.advanceWidth;
- }
-
- if (!isFinite(metrics.yMin)) {
- metrics.yMin = 0;
- }
-
- if (!isFinite(metrics.yMax)) {
- metrics.yMax = 0;
- }
-
- metrics.rightSideBearing = this.advanceWidth - metrics.leftSideBearing - (metrics.xMax - metrics.xMin);
- return metrics;
-};
-
-/**
- * Draw the glyph on the given context.
- * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
- * @param {number} [x=0] - Horizontal position of the beginning of the text.
- * @param {number} [y=0] - Vertical position of the *baseline* of the text.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- * @param {Object=} options - xScale, yScale to stretch the glyph.
- */
-Glyph.prototype.draw = function(ctx, x, y, fontSize, options) {
- this.getPath(x, y, fontSize, options).draw(ctx);
-};
-
-/**
- * Draw the points of the glyph.
- * On-curve points will be drawn in blue, off-curve points will be drawn in red.
- * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
- * @param {number} [x=0] - Horizontal position of the beginning of the text.
- * @param {number} [y=0] - Vertical position of the *baseline* of the text.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- */
-Glyph.prototype.drawPoints = function(ctx, x, y, fontSize) {
- function drawCircles(l, x, y, scale) {
- ctx.beginPath();
- for (var j = 0; j < l.length; j += 1) {
- ctx.moveTo(x + (l[j].x * scale), y + (l[j].y * scale));
- ctx.arc(x + (l[j].x * scale), y + (l[j].y * scale), 2, 0, Math.PI * 2, false);
- }
-
- ctx.closePath();
- ctx.fill();
- }
-
- x = x !== undefined ? x : 0;
- y = y !== undefined ? y : 0;
- fontSize = fontSize !== undefined ? fontSize : 24;
- var scale = 1 / this.path.unitsPerEm * fontSize;
-
- var blueCircles = [];
- var redCircles = [];
- var path = this.path;
- for (var i = 0; i < path.commands.length; i += 1) {
- var cmd = path.commands[i];
- if (cmd.x !== undefined) {
- blueCircles.push({x: cmd.x, y: -cmd.y});
- }
-
- if (cmd.x1 !== undefined) {
- redCircles.push({x: cmd.x1, y: -cmd.y1});
- }
-
- if (cmd.x2 !== undefined) {
- redCircles.push({x: cmd.x2, y: -cmd.y2});
- }
- }
-
- ctx.fillStyle = 'blue';
- drawCircles(blueCircles, x, y, scale);
- ctx.fillStyle = 'red';
- drawCircles(redCircles, x, y, scale);
-};
-
-/**
- * Draw lines indicating important font measurements.
- * Black lines indicate the origin of the coordinate system (point 0,0).
- * Blue lines indicate the glyph bounding box.
- * Green line indicates the advance width of the glyph.
- * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
- * @param {number} [x=0] - Horizontal position of the beginning of the text.
- * @param {number} [y=0] - Vertical position of the *baseline* of the text.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- */
-Glyph.prototype.drawMetrics = function(ctx, x, y, fontSize) {
- var scale;
- x = x !== undefined ? x : 0;
- y = y !== undefined ? y : 0;
- fontSize = fontSize !== undefined ? fontSize : 24;
- scale = 1 / this.path.unitsPerEm * fontSize;
- ctx.lineWidth = 1;
-
- // Draw the origin
- ctx.strokeStyle = 'black';
- draw.line(ctx, x, -10000, x, 10000);
- draw.line(ctx, -10000, y, 10000, y);
-
- // This code is here due to memory optimization: by not using
- // defaults in the constructor, we save a notable amount of memory.
- var xMin = this.xMin || 0;
- var yMin = this.yMin || 0;
- var xMax = this.xMax || 0;
- var yMax = this.yMax || 0;
- var advanceWidth = this.advanceWidth || 0;
-
- // Draw the glyph box
- ctx.strokeStyle = 'blue';
- draw.line(ctx, x + (xMin * scale), -10000, x + (xMin * scale), 10000);
- draw.line(ctx, x + (xMax * scale), -10000, x + (xMax * scale), 10000);
- draw.line(ctx, -10000, y + (-yMin * scale), 10000, y + (-yMin * scale));
- draw.line(ctx, -10000, y + (-yMax * scale), 10000, y + (-yMax * scale));
-
- // Draw the advance width
- ctx.strokeStyle = 'green';
- draw.line(ctx, x + (advanceWidth * scale), -10000, x + (advanceWidth * scale), 10000);
-};
-
-// The GlyphSet object
-
-// Define a property on the glyph that depends on the path being loaded.
-function defineDependentProperty(glyph, externalName, internalName) {
- Object.defineProperty(glyph, externalName, {
- get: function() {
- // Request the path property to make sure the path is loaded.
- glyph.path; // jshint ignore:line
- return glyph[internalName];
- },
- set: function(newValue) {
- glyph[internalName] = newValue;
- },
- enumerable: true,
- configurable: true
- });
-}
-
-/**
- * A GlyphSet represents all glyphs available in the font, but modelled using
- * a deferred glyph loader, for retrieving glyphs only once they are absolutely
- * necessary, to keep the memory footprint down.
- * @exports opentype.GlyphSet
- * @class
- * @param {opentype.Font}
- * @param {Array}
- */
-function GlyphSet(font, glyphs) {
- this.font = font;
- this.glyphs = {};
- if (Array.isArray(glyphs)) {
- for (var i = 0; i < glyphs.length; i++) {
- var glyph = glyphs[i];
- glyph.path.unitsPerEm = font.unitsPerEm;
- this.glyphs[i] = glyph;
- }
- }
-
- this.length = (glyphs && glyphs.length) || 0;
-}
-
-/**
- * @param {number} index
- * @return {opentype.Glyph}
- */
-GlyphSet.prototype.get = function(index) {
- // this.glyphs[index] is 'undefined' when low memory mode is on. glyph is pushed on request only.
- if (this.glyphs[index] === undefined) {
- this.font._push(index);
- if (typeof this.glyphs[index] === 'function') {
- this.glyphs[index] = this.glyphs[index]();
- }
-
- var glyph = this.glyphs[index];
- var unicodeObj = this.font._IndexToUnicodeMap[index];
-
- if (unicodeObj) {
- for (var j = 0; j < unicodeObj.unicodes.length; j++)
- { glyph.addUnicode(unicodeObj.unicodes[j]); }
- }
-
- if (this.font.cffEncoding) {
- if (this.font.isCIDFont) {
- glyph.name = 'gid' + index;
- } else {
- glyph.name = this.font.cffEncoding.charset[index];
- }
- } else if (this.font.glyphNames.names) {
- glyph.name = this.font.glyphNames.glyphIndexToName(index);
- }
-
- this.glyphs[index].advanceWidth = this.font._hmtxTableData[index].advanceWidth;
- this.glyphs[index].leftSideBearing = this.font._hmtxTableData[index].leftSideBearing;
- } else {
- if (typeof this.glyphs[index] === 'function') {
- this.glyphs[index] = this.glyphs[index]();
- }
- }
-
- return this.glyphs[index];
-};
-
-/**
- * @param {number} index
- * @param {Object}
- */
-GlyphSet.prototype.push = function(index, loader) {
- this.glyphs[index] = loader;
- this.length++;
-};
-
-/**
- * @alias opentype.glyphLoader
- * @param {opentype.Font} font
- * @param {number} index
- * @return {opentype.Glyph}
- */
-function glyphLoader(font, index) {
- return new Glyph({index: index, font: font});
-}
-
-/**
- * Generate a stub glyph that can be filled with all metadata *except*
- * the "points" and "path" properties, which must be loaded only once
- * the glyph's path is actually requested for text shaping.
- * @alias opentype.ttfGlyphLoader
- * @param {opentype.Font} font
- * @param {number} index
- * @param {Function} parseGlyph
- * @param {Object} data
- * @param {number} position
- * @param {Function} buildPath
- * @return {opentype.Glyph}
- */
-function ttfGlyphLoader(font, index, parseGlyph, data, position, buildPath) {
- return function() {
- var glyph = new Glyph({index: index, font: font});
-
- glyph.path = function() {
- parseGlyph(glyph, data, position);
- var path = buildPath(font.glyphs, glyph);
- path.unitsPerEm = font.unitsPerEm;
- return path;
- };
-
- defineDependentProperty(glyph, 'xMin', '_xMin');
- defineDependentProperty(glyph, 'xMax', '_xMax');
- defineDependentProperty(glyph, 'yMin', '_yMin');
- defineDependentProperty(glyph, 'yMax', '_yMax');
-
- return glyph;
- };
-}
-/**
- * @alias opentype.cffGlyphLoader
- * @param {opentype.Font} font
- * @param {number} index
- * @param {Function} parseCFFCharstring
- * @param {string} charstring
- * @return {opentype.Glyph}
- */
-function cffGlyphLoader(font, index, parseCFFCharstring, charstring) {
- return function() {
- var glyph = new Glyph({index: index, font: font});
-
- glyph.path = function() {
- var path = parseCFFCharstring(font, glyph, charstring);
- path.unitsPerEm = font.unitsPerEm;
- return path;
- };
-
- return glyph;
- };
-}
-
-var glyphset = { GlyphSet: GlyphSet, glyphLoader: glyphLoader, ttfGlyphLoader: ttfGlyphLoader, cffGlyphLoader: cffGlyphLoader };
-
-// The `CFF` table contains the glyph outlines in PostScript format.
-
-// Custom equals function that can also check lists.
-function equals(a, b) {
- if (a === b) {
- return true;
- } else if (Array.isArray(a) && Array.isArray(b)) {
- if (a.length !== b.length) {
- return false;
- }
-
- for (var i = 0; i < a.length; i += 1) {
- if (!equals(a[i], b[i])) {
- return false;
- }
- }
-
- return true;
- } else {
- return false;
- }
-}
-
-// Subroutines are encoded using the negative half of the number space.
-// See type 2 chapter 4.7 "Subroutine operators".
-function calcCFFSubroutineBias(subrs) {
- var bias;
- if (subrs.length < 1240) {
- bias = 107;
- } else if (subrs.length < 33900) {
- bias = 1131;
- } else {
- bias = 32768;
- }
-
- return bias;
-}
-
-// Parse a `CFF` INDEX array.
-// An index array consists of a list of offsets, then a list of objects at those offsets.
-function parseCFFIndex(data, start, conversionFn) {
- var offsets = [];
- var objects = [];
- var count = parse.getCard16(data, start);
- var objectOffset;
- var endOffset;
- if (count !== 0) {
- var offsetSize = parse.getByte(data, start + 2);
- objectOffset = start + ((count + 1) * offsetSize) + 2;
- var pos = start + 3;
- for (var i = 0; i < count + 1; i += 1) {
- offsets.push(parse.getOffset(data, pos, offsetSize));
- pos += offsetSize;
- }
-
- // The total size of the index array is 4 header bytes + the value of the last offset.
- endOffset = objectOffset + offsets[count];
- } else {
- endOffset = start + 2;
- }
-
- for (var i$1 = 0; i$1 < offsets.length - 1; i$1 += 1) {
- var value = parse.getBytes(data, objectOffset + offsets[i$1], objectOffset + offsets[i$1 + 1]);
- if (conversionFn) {
- value = conversionFn(value);
- }
-
- objects.push(value);
- }
-
- return {objects: objects, startOffset: start, endOffset: endOffset};
-}
-
-function parseCFFIndexLowMemory(data, start) {
- var offsets = [];
- var count = parse.getCard16(data, start);
- var objectOffset;
- var endOffset;
- if (count !== 0) {
- var offsetSize = parse.getByte(data, start + 2);
- objectOffset = start + ((count + 1) * offsetSize) + 2;
- var pos = start + 3;
- for (var i = 0; i < count + 1; i += 1) {
- offsets.push(parse.getOffset(data, pos, offsetSize));
- pos += offsetSize;
- }
-
- // The total size of the index array is 4 header bytes + the value of the last offset.
- endOffset = objectOffset + offsets[count];
- } else {
- endOffset = start + 2;
- }
-
- return {offsets: offsets, startOffset: start, endOffset: endOffset};
-}
-function getCffIndexObject(i, offsets, data, start, conversionFn) {
- var count = parse.getCard16(data, start);
- var objectOffset = 0;
- if (count !== 0) {
- var offsetSize = parse.getByte(data, start + 2);
- objectOffset = start + ((count + 1) * offsetSize) + 2;
- }
-
- var value = parse.getBytes(data, objectOffset + offsets[i], objectOffset + offsets[i + 1]);
- if (conversionFn) {
- value = conversionFn(value);
- }
- return value;
-}
-
-// Parse a `CFF` DICT real value.
-function parseFloatOperand(parser) {
- var s = '';
- var eof = 15;
- var lookup = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'E', 'E-', null, '-'];
- while (true) {
- var b = parser.parseByte();
- var n1 = b >> 4;
- var n2 = b & 15;
-
- if (n1 === eof) {
- break;
- }
-
- s += lookup[n1];
-
- if (n2 === eof) {
- break;
- }
-
- s += lookup[n2];
- }
-
- return parseFloat(s);
-}
-
-// Parse a `CFF` DICT operand.
-function parseOperand(parser, b0) {
- var b1;
- var b2;
- var b3;
- var b4;
- if (b0 === 28) {
- b1 = parser.parseByte();
- b2 = parser.parseByte();
- return b1 << 8 | b2;
- }
-
- if (b0 === 29) {
- b1 = parser.parseByte();
- b2 = parser.parseByte();
- b3 = parser.parseByte();
- b4 = parser.parseByte();
- return b1 << 24 | b2 << 16 | b3 << 8 | b4;
- }
-
- if (b0 === 30) {
- return parseFloatOperand(parser);
- }
-
- if (b0 >= 32 && b0 <= 246) {
- return b0 - 139;
- }
-
- if (b0 >= 247 && b0 <= 250) {
- b1 = parser.parseByte();
- return (b0 - 247) * 256 + b1 + 108;
- }
-
- if (b0 >= 251 && b0 <= 254) {
- b1 = parser.parseByte();
- return -(b0 - 251) * 256 - b1 - 108;
- }
-
- throw new Error('Invalid b0 ' + b0);
-}
-
-// Convert the entries returned by `parseDict` to a proper dictionary.
-// If a value is a list of one, it is unpacked.
-function entriesToObject(entries) {
- var o = {};
- for (var i = 0; i < entries.length; i += 1) {
- var key = entries[i][0];
- var values = entries[i][1];
- var value = (void 0);
- if (values.length === 1) {
- value = values[0];
- } else {
- value = values;
- }
-
- if (o.hasOwnProperty(key) && !isNaN(o[key])) {
- throw new Error('Object ' + o + ' already has key ' + key);
- }
-
- o[key] = value;
- }
-
- return o;
-}
-
-// Parse a `CFF` DICT object.
-// A dictionary contains key-value pairs in a compact tokenized format.
-function parseCFFDict(data, start, size) {
- start = start !== undefined ? start : 0;
- var parser = new parse.Parser(data, start);
- var entries = [];
- var operands = [];
- size = size !== undefined ? size : data.length;
-
- while (parser.relativeOffset < size) {
- var op = parser.parseByte();
-
- // The first byte for each dict item distinguishes between operator (key) and operand (value).
- // Values <= 21 are operators.
- if (op <= 21) {
- // Two-byte operators have an initial escape byte of 12.
- if (op === 12) {
- op = 1200 + parser.parseByte();
- }
-
- entries.push([op, operands]);
- operands = [];
- } else {
- // Since the operands (values) come before the operators (keys), we store all operands in a list
- // until we encounter an operator.
- operands.push(parseOperand(parser, op));
- }
- }
-
- return entriesToObject(entries);
-}
-
-// Given a String Index (SID), return the value of the string.
-// Strings below index 392 are standard CFF strings and are not encoded in the font.
-function getCFFString(strings, index) {
- if (index <= 390) {
- index = cffStandardStrings[index];
- } else {
- index = strings[index - 391];
- }
-
- return index;
-}
-
-// Interpret a dictionary and return a new dictionary with readable keys and values for missing entries.
-// This function takes `meta` which is a list of objects containing `operand`, `name` and `default`.
-function interpretDict(dict, meta, strings) {
- var newDict = {};
- var value;
-
- // Because we also want to include missing values, we start out from the meta list
- // and lookup values in the dict.
- for (var i = 0; i < meta.length; i += 1) {
- var m = meta[i];
-
- if (Array.isArray(m.type)) {
- var values = [];
- values.length = m.type.length;
- for (var j = 0; j < m.type.length; j++) {
- value = dict[m.op] !== undefined ? dict[m.op][j] : undefined;
- if (value === undefined) {
- value = m.value !== undefined && m.value[j] !== undefined ? m.value[j] : null;
- }
- if (m.type[j] === 'SID') {
- value = getCFFString(strings, value);
- }
- values[j] = value;
- }
- newDict[m.name] = values;
- } else {
- value = dict[m.op];
- if (value === undefined) {
- value = m.value !== undefined ? m.value : null;
- }
-
- if (m.type === 'SID') {
- value = getCFFString(strings, value);
- }
- newDict[m.name] = value;
- }
- }
-
- return newDict;
-}
-
-// Parse the CFF header.
-function parseCFFHeader(data, start) {
- var header = {};
- header.formatMajor = parse.getCard8(data, start);
- header.formatMinor = parse.getCard8(data, start + 1);
- header.size = parse.getCard8(data, start + 2);
- header.offsetSize = parse.getCard8(data, start + 3);
- header.startOffset = start;
- header.endOffset = start + 4;
- return header;
-}
-
-var TOP_DICT_META = [
- {name: 'version', op: 0, type: 'SID'},
- {name: 'notice', op: 1, type: 'SID'},
- {name: 'copyright', op: 1200, type: 'SID'},
- {name: 'fullName', op: 2, type: 'SID'},
- {name: 'familyName', op: 3, type: 'SID'},
- {name: 'weight', op: 4, type: 'SID'},
- {name: 'isFixedPitch', op: 1201, type: 'number', value: 0},
- {name: 'italicAngle', op: 1202, type: 'number', value: 0},
- {name: 'underlinePosition', op: 1203, type: 'number', value: -100},
- {name: 'underlineThickness', op: 1204, type: 'number', value: 50},
- {name: 'paintType', op: 1205, type: 'number', value: 0},
- {name: 'charstringType', op: 1206, type: 'number', value: 2},
- {
- name: 'fontMatrix',
- op: 1207,
- type: ['real', 'real', 'real', 'real', 'real', 'real'],
- value: [0.001, 0, 0, 0.001, 0, 0]
- },
- {name: 'uniqueId', op: 13, type: 'number'},
- {name: 'fontBBox', op: 5, type: ['number', 'number', 'number', 'number'], value: [0, 0, 0, 0]},
- {name: 'strokeWidth', op: 1208, type: 'number', value: 0},
- {name: 'xuid', op: 14, type: [], value: null},
- {name: 'charset', op: 15, type: 'offset', value: 0},
- {name: 'encoding', op: 16, type: 'offset', value: 0},
- {name: 'charStrings', op: 17, type: 'offset', value: 0},
- {name: 'private', op: 18, type: ['number', 'offset'], value: [0, 0]},
- {name: 'ros', op: 1230, type: ['SID', 'SID', 'number']},
- {name: 'cidFontVersion', op: 1231, type: 'number', value: 0},
- {name: 'cidFontRevision', op: 1232, type: 'number', value: 0},
- {name: 'cidFontType', op: 1233, type: 'number', value: 0},
- {name: 'cidCount', op: 1234, type: 'number', value: 8720},
- {name: 'uidBase', op: 1235, type: 'number'},
- {name: 'fdArray', op: 1236, type: 'offset'},
- {name: 'fdSelect', op: 1237, type: 'offset'},
- {name: 'fontName', op: 1238, type: 'SID'}
-];
-
-var PRIVATE_DICT_META = [
- {name: 'subrs', op: 19, type: 'offset', value: 0},
- {name: 'defaultWidthX', op: 20, type: 'number', value: 0},
- {name: 'nominalWidthX', op: 21, type: 'number', value: 0}
-];
-
-// Parse the CFF top dictionary. A CFF table can contain multiple fonts, each with their own top dictionary.
-// The top dictionary contains the essential metadata for the font, together with the private dictionary.
-function parseCFFTopDict(data, strings) {
- var dict = parseCFFDict(data, 0, data.byteLength);
- return interpretDict(dict, TOP_DICT_META, strings);
-}
-
-// Parse the CFF private dictionary. We don't fully parse out all the values, only the ones we need.
-function parseCFFPrivateDict(data, start, size, strings) {
- var dict = parseCFFDict(data, start, size);
- return interpretDict(dict, PRIVATE_DICT_META, strings);
-}
-
-// Returns a list of "Top DICT"s found using an INDEX list.
-// Used to read both the usual high-level Top DICTs and also the FDArray
-// discovered inside CID-keyed fonts. When a Top DICT has a reference to
-// a Private DICT that is read and saved into the Top DICT.
-//
-// In addition to the expected/optional values as outlined in TOP_DICT_META
-// the following values might be saved into the Top DICT.
-//
-// _subrs [] array of local CFF subroutines from Private DICT
-// _subrsBias bias value computed from number of subroutines
-// (see calcCFFSubroutineBias() and parseCFFCharstring())
-// _defaultWidthX default widths for CFF characters
-// _nominalWidthX bias added to width embedded within glyph description
-//
-// _privateDict saved copy of parsed Private DICT from Top DICT
-function gatherCFFTopDicts(data, start, cffIndex, strings) {
- var topDictArray = [];
- for (var iTopDict = 0; iTopDict < cffIndex.length; iTopDict += 1) {
- var topDictData = new DataView(new Uint8Array(cffIndex[iTopDict]).buffer);
- var topDict = parseCFFTopDict(topDictData, strings);
- topDict._subrs = [];
- topDict._subrsBias = 0;
- topDict._defaultWidthX = 0;
- topDict._nominalWidthX = 0;
- var privateSize = topDict.private[0];
- var privateOffset = topDict.private[1];
- if (privateSize !== 0 && privateOffset !== 0) {
- var privateDict = parseCFFPrivateDict(data, privateOffset + start, privateSize, strings);
- topDict._defaultWidthX = privateDict.defaultWidthX;
- topDict._nominalWidthX = privateDict.nominalWidthX;
- if (privateDict.subrs !== 0) {
- var subrOffset = privateOffset + privateDict.subrs;
- var subrIndex = parseCFFIndex(data, subrOffset + start);
- topDict._subrs = subrIndex.objects;
- topDict._subrsBias = calcCFFSubroutineBias(topDict._subrs);
- }
- topDict._privateDict = privateDict;
- }
- topDictArray.push(topDict);
- }
- return topDictArray;
-}
-
-// Parse the CFF charset table, which contains internal names for all the glyphs.
-// This function will return a list of glyph names.
-// See Adobe TN #5176 chapter 13, "Charsets".
-function parseCFFCharset(data, start, nGlyphs, strings) {
- var sid;
- var count;
- var parser = new parse.Parser(data, start);
-
- // The .notdef glyph is not included, so subtract 1.
- nGlyphs -= 1;
- var charset = ['.notdef'];
-
- var format = parser.parseCard8();
- if (format === 0) {
- for (var i = 0; i < nGlyphs; i += 1) {
- sid = parser.parseSID();
- charset.push(getCFFString(strings, sid));
- }
- } else if (format === 1) {
- while (charset.length <= nGlyphs) {
- sid = parser.parseSID();
- count = parser.parseCard8();
- for (var i$1 = 0; i$1 <= count; i$1 += 1) {
- charset.push(getCFFString(strings, sid));
- sid += 1;
- }
- }
- } else if (format === 2) {
- while (charset.length <= nGlyphs) {
- sid = parser.parseSID();
- count = parser.parseCard16();
- for (var i$2 = 0; i$2 <= count; i$2 += 1) {
- charset.push(getCFFString(strings, sid));
- sid += 1;
- }
- }
- } else {
- throw new Error('Unknown charset format ' + format);
- }
-
- return charset;
-}
-
-// Parse the CFF encoding data. Only one encoding can be specified per font.
-// See Adobe TN #5176 chapter 12, "Encodings".
-function parseCFFEncoding(data, start, charset) {
- var code;
- var enc = {};
- var parser = new parse.Parser(data, start);
- var format = parser.parseCard8();
- if (format === 0) {
- var nCodes = parser.parseCard8();
- for (var i = 0; i < nCodes; i += 1) {
- code = parser.parseCard8();
- enc[code] = i;
- }
- } else if (format === 1) {
- var nRanges = parser.parseCard8();
- code = 1;
- for (var i$1 = 0; i$1 < nRanges; i$1 += 1) {
- var first = parser.parseCard8();
- var nLeft = parser.parseCard8();
- for (var j = first; j <= first + nLeft; j += 1) {
- enc[j] = code;
- code += 1;
- }
- }
- } else {
- throw new Error('Unknown encoding format ' + format);
- }
-
- return new CffEncoding(enc, charset);
-}
-
-// Take in charstring code and return a Glyph object.
-// The encoding is described in the Type 2 Charstring Format
-// https://www.microsoft.com/typography/OTSPEC/charstr2.htm
-function parseCFFCharstring(font, glyph, code) {
- var c1x;
- var c1y;
- var c2x;
- var c2y;
- var p = new Path();
- var stack = [];
- var nStems = 0;
- var haveWidth = false;
- var open = false;
- var x = 0;
- var y = 0;
- var subrs;
- var subrsBias;
- var defaultWidthX;
- var nominalWidthX;
- if (font.isCIDFont) {
- var fdIndex = font.tables.cff.topDict._fdSelect[glyph.index];
- var fdDict = font.tables.cff.topDict._fdArray[fdIndex];
- subrs = fdDict._subrs;
- subrsBias = fdDict._subrsBias;
- defaultWidthX = fdDict._defaultWidthX;
- nominalWidthX = fdDict._nominalWidthX;
- } else {
- subrs = font.tables.cff.topDict._subrs;
- subrsBias = font.tables.cff.topDict._subrsBias;
- defaultWidthX = font.tables.cff.topDict._defaultWidthX;
- nominalWidthX = font.tables.cff.topDict._nominalWidthX;
- }
- var width = defaultWidthX;
-
- function newContour(x, y) {
- if (open) {
- p.closePath();
- }
-
- p.moveTo(x, y);
- open = true;
- }
-
- function parseStems() {
- var hasWidthArg;
-
- // The number of stem operators on the stack is always even.
- // If the value is uneven, that means a width is specified.
- hasWidthArg = stack.length % 2 !== 0;
- if (hasWidthArg && !haveWidth) {
- width = stack.shift() + nominalWidthX;
- }
-
- nStems += stack.length >> 1;
- stack.length = 0;
- haveWidth = true;
- }
-
- function parse(code) {
- var b1;
- var b2;
- var b3;
- var b4;
- var codeIndex;
- var subrCode;
- var jpx;
- var jpy;
- var c3x;
- var c3y;
- var c4x;
- var c4y;
-
- var i = 0;
- while (i < code.length) {
- var v = code[i];
- i += 1;
- switch (v) {
- case 1: // hstem
- parseStems();
- break;
- case 3: // vstem
- parseStems();
- break;
- case 4: // vmoveto
- if (stack.length > 1 && !haveWidth) {
- width = stack.shift() + nominalWidthX;
- haveWidth = true;
- }
-
- y += stack.pop();
- newContour(x, y);
- break;
- case 5: // rlineto
- while (stack.length > 0) {
- x += stack.shift();
- y += stack.shift();
- p.lineTo(x, y);
- }
-
- break;
- case 6: // hlineto
- while (stack.length > 0) {
- x += stack.shift();
- p.lineTo(x, y);
- if (stack.length === 0) {
- break;
- }
-
- y += stack.shift();
- p.lineTo(x, y);
- }
-
- break;
- case 7: // vlineto
- while (stack.length > 0) {
- y += stack.shift();
- p.lineTo(x, y);
- if (stack.length === 0) {
- break;
- }
-
- x += stack.shift();
- p.lineTo(x, y);
- }
-
- break;
- case 8: // rrcurveto
- while (stack.length > 0) {
- c1x = x + stack.shift();
- c1y = y + stack.shift();
- c2x = c1x + stack.shift();
- c2y = c1y + stack.shift();
- x = c2x + stack.shift();
- y = c2y + stack.shift();
- p.curveTo(c1x, c1y, c2x, c2y, x, y);
- }
-
- break;
- case 10: // callsubr
- codeIndex = stack.pop() + subrsBias;
- subrCode = subrs[codeIndex];
- if (subrCode) {
- parse(subrCode);
- }
-
- break;
- case 11: // return
- return;
- case 12: // flex operators
- v = code[i];
- i += 1;
- switch (v) {
- case 35: // flex
- // |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd flex (12 35) |-
- c1x = x + stack.shift(); // dx1
- c1y = y + stack.shift(); // dy1
- c2x = c1x + stack.shift(); // dx2
- c2y = c1y + stack.shift(); // dy2
- jpx = c2x + stack.shift(); // dx3
- jpy = c2y + stack.shift(); // dy3
- c3x = jpx + stack.shift(); // dx4
- c3y = jpy + stack.shift(); // dy4
- c4x = c3x + stack.shift(); // dx5
- c4y = c3y + stack.shift(); // dy5
- x = c4x + stack.shift(); // dx6
- y = c4y + stack.shift(); // dy6
- stack.shift(); // flex depth
- p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
- p.curveTo(c3x, c3y, c4x, c4y, x, y);
- break;
- case 34: // hflex
- // |- dx1 dx2 dy2 dx3 dx4 dx5 dx6 hflex (12 34) |-
- c1x = x + stack.shift(); // dx1
- c1y = y; // dy1
- c2x = c1x + stack.shift(); // dx2
- c2y = c1y + stack.shift(); // dy2
- jpx = c2x + stack.shift(); // dx3
- jpy = c2y; // dy3
- c3x = jpx + stack.shift(); // dx4
- c3y = c2y; // dy4
- c4x = c3x + stack.shift(); // dx5
- c4y = y; // dy5
- x = c4x + stack.shift(); // dx6
- p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
- p.curveTo(c3x, c3y, c4x, c4y, x, y);
- break;
- case 36: // hflex1
- // |- dx1 dy1 dx2 dy2 dx3 dx4 dx5 dy5 dx6 hflex1 (12 36) |-
- c1x = x + stack.shift(); // dx1
- c1y = y + stack.shift(); // dy1
- c2x = c1x + stack.shift(); // dx2
- c2y = c1y + stack.shift(); // dy2
- jpx = c2x + stack.shift(); // dx3
- jpy = c2y; // dy3
- c3x = jpx + stack.shift(); // dx4
- c3y = c2y; // dy4
- c4x = c3x + stack.shift(); // dx5
- c4y = c3y + stack.shift(); // dy5
- x = c4x + stack.shift(); // dx6
- p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
- p.curveTo(c3x, c3y, c4x, c4y, x, y);
- break;
- case 37: // flex1
- // |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6 flex1 (12 37) |-
- c1x = x + stack.shift(); // dx1
- c1y = y + stack.shift(); // dy1
- c2x = c1x + stack.shift(); // dx2
- c2y = c1y + stack.shift(); // dy2
- jpx = c2x + stack.shift(); // dx3
- jpy = c2y + stack.shift(); // dy3
- c3x = jpx + stack.shift(); // dx4
- c3y = jpy + stack.shift(); // dy4
- c4x = c3x + stack.shift(); // dx5
- c4y = c3y + stack.shift(); // dy5
- if (Math.abs(c4x - x) > Math.abs(c4y - y)) {
- x = c4x + stack.shift();
- } else {
- y = c4y + stack.shift();
- }
-
- p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy);
- p.curveTo(c3x, c3y, c4x, c4y, x, y);
- break;
- default:
- console.log('Glyph ' + glyph.index + ': unknown operator ' + 1200 + v);
- stack.length = 0;
- }
- break;
- case 14: // endchar
- if (stack.length > 0 && !haveWidth) {
- width = stack.shift() + nominalWidthX;
- haveWidth = true;
- }
-
- if (open) {
- p.closePath();
- open = false;
- }
-
- break;
- case 18: // hstemhm
- parseStems();
- break;
- case 19: // hintmask
- case 20: // cntrmask
- parseStems();
- i += (nStems + 7) >> 3;
- break;
- case 21: // rmoveto
- if (stack.length > 2 && !haveWidth) {
- width = stack.shift() + nominalWidthX;
- haveWidth = true;
- }
-
- y += stack.pop();
- x += stack.pop();
- newContour(x, y);
- break;
- case 22: // hmoveto
- if (stack.length > 1 && !haveWidth) {
- width = stack.shift() + nominalWidthX;
- haveWidth = true;
- }
-
- x += stack.pop();
- newContour(x, y);
- break;
- case 23: // vstemhm
- parseStems();
- break;
- case 24: // rcurveline
- while (stack.length > 2) {
- c1x = x + stack.shift();
- c1y = y + stack.shift();
- c2x = c1x + stack.shift();
- c2y = c1y + stack.shift();
- x = c2x + stack.shift();
- y = c2y + stack.shift();
- p.curveTo(c1x, c1y, c2x, c2y, x, y);
- }
-
- x += stack.shift();
- y += stack.shift();
- p.lineTo(x, y);
- break;
- case 25: // rlinecurve
- while (stack.length > 6) {
- x += stack.shift();
- y += stack.shift();
- p.lineTo(x, y);
- }
-
- c1x = x + stack.shift();
- c1y = y + stack.shift();
- c2x = c1x + stack.shift();
- c2y = c1y + stack.shift();
- x = c2x + stack.shift();
- y = c2y + stack.shift();
- p.curveTo(c1x, c1y, c2x, c2y, x, y);
- break;
- case 26: // vvcurveto
- if (stack.length % 2) {
- x += stack.shift();
- }
-
- while (stack.length > 0) {
- c1x = x;
- c1y = y + stack.shift();
- c2x = c1x + stack.shift();
- c2y = c1y + stack.shift();
- x = c2x;
- y = c2y + stack.shift();
- p.curveTo(c1x, c1y, c2x, c2y, x, y);
- }
-
- break;
- case 27: // hhcurveto
- if (stack.length % 2) {
- y += stack.shift();
- }
-
- while (stack.length > 0) {
- c1x = x + stack.shift();
- c1y = y;
- c2x = c1x + stack.shift();
- c2y = c1y + stack.shift();
- x = c2x + stack.shift();
- y = c2y;
- p.curveTo(c1x, c1y, c2x, c2y, x, y);
- }
-
- break;
- case 28: // shortint
- b1 = code[i];
- b2 = code[i + 1];
- stack.push(((b1 << 24) | (b2 << 16)) >> 16);
- i += 2;
- break;
- case 29: // callgsubr
- codeIndex = stack.pop() + font.gsubrsBias;
- subrCode = font.gsubrs[codeIndex];
- if (subrCode) {
- parse(subrCode);
- }
-
- break;
- case 30: // vhcurveto
- while (stack.length > 0) {
- c1x = x;
- c1y = y + stack.shift();
- c2x = c1x + stack.shift();
- c2y = c1y + stack.shift();
- x = c2x + stack.shift();
- y = c2y + (stack.length === 1 ? stack.shift() : 0);
- p.curveTo(c1x, c1y, c2x, c2y, x, y);
- if (stack.length === 0) {
- break;
- }
-
- c1x = x + stack.shift();
- c1y = y;
- c2x = c1x + stack.shift();
- c2y = c1y + stack.shift();
- y = c2y + stack.shift();
- x = c2x + (stack.length === 1 ? stack.shift() : 0);
- p.curveTo(c1x, c1y, c2x, c2y, x, y);
- }
-
- break;
- case 31: // hvcurveto
- while (stack.length > 0) {
- c1x = x + stack.shift();
- c1y = y;
- c2x = c1x + stack.shift();
- c2y = c1y + stack.shift();
- y = c2y + stack.shift();
- x = c2x + (stack.length === 1 ? stack.shift() : 0);
- p.curveTo(c1x, c1y, c2x, c2y, x, y);
- if (stack.length === 0) {
- break;
- }
-
- c1x = x;
- c1y = y + stack.shift();
- c2x = c1x + stack.shift();
- c2y = c1y + stack.shift();
- x = c2x + stack.shift();
- y = c2y + (stack.length === 1 ? stack.shift() : 0);
- p.curveTo(c1x, c1y, c2x, c2y, x, y);
- }
-
- break;
- default:
- if (v < 32) {
- console.log('Glyph ' + glyph.index + ': unknown operator ' + v);
- } else if (v < 247) {
- stack.push(v - 139);
- } else if (v < 251) {
- b1 = code[i];
- i += 1;
- stack.push((v - 247) * 256 + b1 + 108);
- } else if (v < 255) {
- b1 = code[i];
- i += 1;
- stack.push(-(v - 251) * 256 - b1 - 108);
- } else {
- b1 = code[i];
- b2 = code[i + 1];
- b3 = code[i + 2];
- b4 = code[i + 3];
- i += 4;
- stack.push(((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) / 65536);
- }
- }
- }
- }
-
- parse(code);
-
- glyph.advanceWidth = width;
- return p;
-}
-
-function parseCFFFDSelect(data, start, nGlyphs, fdArrayCount) {
- var fdSelect = [];
- var fdIndex;
- var parser = new parse.Parser(data, start);
- var format = parser.parseCard8();
- if (format === 0) {
- // Simple list of nGlyphs elements
- for (var iGid = 0; iGid < nGlyphs; iGid++) {
- fdIndex = parser.parseCard8();
- if (fdIndex >= fdArrayCount) {
- throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')');
- }
- fdSelect.push(fdIndex);
- }
- } else if (format === 3) {
- // Ranges
- var nRanges = parser.parseCard16();
- var first = parser.parseCard16();
- if (first !== 0) {
- throw new Error('CFF Table CID Font FDSelect format 3 range has bad initial GID ' + first);
- }
- var next;
- for (var iRange = 0; iRange < nRanges; iRange++) {
- fdIndex = parser.parseCard8();
- next = parser.parseCard16();
- if (fdIndex >= fdArrayCount) {
- throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')');
- }
- if (next > nGlyphs) {
- throw new Error('CFF Table CID Font FDSelect format 3 range has bad GID ' + next);
- }
- for (; first < next; first++) {
- fdSelect.push(fdIndex);
- }
- first = next;
- }
- if (next !== nGlyphs) {
- throw new Error('CFF Table CID Font FDSelect format 3 range has bad final GID ' + next);
- }
- } else {
- throw new Error('CFF Table CID Font FDSelect table has unsupported format ' + format);
- }
- return fdSelect;
-}
-
-// Parse the `CFF` table, which contains the glyph outlines in PostScript format.
-function parseCFFTable(data, start, font, opt) {
- font.tables.cff = {};
- var header = parseCFFHeader(data, start);
- var nameIndex = parseCFFIndex(data, header.endOffset, parse.bytesToString);
- var topDictIndex = parseCFFIndex(data, nameIndex.endOffset);
- var stringIndex = parseCFFIndex(data, topDictIndex.endOffset, parse.bytesToString);
- var globalSubrIndex = parseCFFIndex(data, stringIndex.endOffset);
- font.gsubrs = globalSubrIndex.objects;
- font.gsubrsBias = calcCFFSubroutineBias(font.gsubrs);
-
- var topDictArray = gatherCFFTopDicts(data, start, topDictIndex.objects, stringIndex.objects);
- if (topDictArray.length !== 1) {
- throw new Error('CFF table has too many fonts in \'FontSet\' - count of fonts NameIndex.length = ' + topDictArray.length);
- }
-
- var topDict = topDictArray[0];
- font.tables.cff.topDict = topDict;
-
- if (topDict._privateDict) {
- font.defaultWidthX = topDict._privateDict.defaultWidthX;
- font.nominalWidthX = topDict._privateDict.nominalWidthX;
- }
-
- if (topDict.ros[0] !== undefined && topDict.ros[1] !== undefined) {
- font.isCIDFont = true;
- }
-
- if (font.isCIDFont) {
- var fdArrayOffset = topDict.fdArray;
- var fdSelectOffset = topDict.fdSelect;
- if (fdArrayOffset === 0 || fdSelectOffset === 0) {
- throw new Error('Font is marked as a CID font, but FDArray and/or FDSelect information is missing');
- }
- fdArrayOffset += start;
- var fdArrayIndex = parseCFFIndex(data, fdArrayOffset);
- var fdArray = gatherCFFTopDicts(data, start, fdArrayIndex.objects, stringIndex.objects);
- topDict._fdArray = fdArray;
- fdSelectOffset += start;
- topDict._fdSelect = parseCFFFDSelect(data, fdSelectOffset, font.numGlyphs, fdArray.length);
- }
-
- var privateDictOffset = start + topDict.private[1];
- var privateDict = parseCFFPrivateDict(data, privateDictOffset, topDict.private[0], stringIndex.objects);
- font.defaultWidthX = privateDict.defaultWidthX;
- font.nominalWidthX = privateDict.nominalWidthX;
-
- if (privateDict.subrs !== 0) {
- var subrOffset = privateDictOffset + privateDict.subrs;
- var subrIndex = parseCFFIndex(data, subrOffset);
- font.subrs = subrIndex.objects;
- font.subrsBias = calcCFFSubroutineBias(font.subrs);
- } else {
- font.subrs = [];
- font.subrsBias = 0;
- }
-
- // Offsets in the top dict are relative to the beginning of the CFF data, so add the CFF start offset.
- var charStringsIndex;
- if (opt.lowMemory) {
- charStringsIndex = parseCFFIndexLowMemory(data, start + topDict.charStrings);
- font.nGlyphs = charStringsIndex.offsets.length;
- } else {
- charStringsIndex = parseCFFIndex(data, start + topDict.charStrings);
- font.nGlyphs = charStringsIndex.objects.length;
- }
-
- var charset = parseCFFCharset(data, start + topDict.charset, font.nGlyphs, stringIndex.objects);
- if (topDict.encoding === 0) {
- // Standard encoding
- font.cffEncoding = new CffEncoding(cffStandardEncoding, charset);
- } else if (topDict.encoding === 1) {
- // Expert encoding
- font.cffEncoding = new CffEncoding(cffExpertEncoding, charset);
- } else {
- font.cffEncoding = parseCFFEncoding(data, start + topDict.encoding, charset);
- }
-
- // Prefer the CMAP encoding to the CFF encoding.
- font.encoding = font.encoding || font.cffEncoding;
-
- font.glyphs = new glyphset.GlyphSet(font);
- if (opt.lowMemory) {
- font._push = function(i) {
- var charString = getCffIndexObject(i, charStringsIndex.offsets, data, start + topDict.charStrings);
- font.glyphs.push(i, glyphset.cffGlyphLoader(font, i, parseCFFCharstring, charString));
- };
- } else {
- for (var i = 0; i < font.nGlyphs; i += 1) {
- var charString = charStringsIndex.objects[i];
- font.glyphs.push(i, glyphset.cffGlyphLoader(font, i, parseCFFCharstring, charString));
- }
- }
-}
-
-// Convert a string to a String ID (SID).
-// The list of strings is modified in place.
-function encodeString(s, strings) {
- var sid;
-
- // Is the string in the CFF standard strings?
- var i = cffStandardStrings.indexOf(s);
- if (i >= 0) {
- sid = i;
- }
-
- // Is the string already in the string index?
- i = strings.indexOf(s);
- if (i >= 0) {
- sid = i + cffStandardStrings.length;
- } else {
- sid = cffStandardStrings.length + strings.length;
- strings.push(s);
- }
-
- return sid;
-}
-
-function makeHeader() {
- return new table.Record('Header', [
- {name: 'major', type: 'Card8', value: 1},
- {name: 'minor', type: 'Card8', value: 0},
- {name: 'hdrSize', type: 'Card8', value: 4},
- {name: 'major', type: 'Card8', value: 1}
- ]);
-}
-
-function makeNameIndex(fontNames) {
- var t = new table.Record('Name INDEX', [
- {name: 'names', type: 'INDEX', value: []}
- ]);
- t.names = [];
- for (var i = 0; i < fontNames.length; i += 1) {
- t.names.push({name: 'name_' + i, type: 'NAME', value: fontNames[i]});
- }
-
- return t;
-}
-
-// Given a dictionary's metadata, create a DICT structure.
-function makeDict(meta, attrs, strings) {
- var m = {};
- for (var i = 0; i < meta.length; i += 1) {
- var entry = meta[i];
- var value = attrs[entry.name];
- if (value !== undefined && !equals(value, entry.value)) {
- if (entry.type === 'SID') {
- value = encodeString(value, strings);
- }
-
- m[entry.op] = {name: entry.name, type: entry.type, value: value};
- }
- }
-
- return m;
-}
-
-// The Top DICT houses the global font attributes.
-function makeTopDict(attrs, strings) {
- var t = new table.Record('Top DICT', [
- {name: 'dict', type: 'DICT', value: {}}
- ]);
- t.dict = makeDict(TOP_DICT_META, attrs, strings);
- return t;
-}
-
-function makeTopDictIndex(topDict) {
- var t = new table.Record('Top DICT INDEX', [
- {name: 'topDicts', type: 'INDEX', value: []}
- ]);
- t.topDicts = [{name: 'topDict_0', type: 'TABLE', value: topDict}];
- return t;
-}
-
-function makeStringIndex(strings) {
- var t = new table.Record('String INDEX', [
- {name: 'strings', type: 'INDEX', value: []}
- ]);
- t.strings = [];
- for (var i = 0; i < strings.length; i += 1) {
- t.strings.push({name: 'string_' + i, type: 'STRING', value: strings[i]});
- }
-
- return t;
-}
-
-function makeGlobalSubrIndex() {
- // Currently we don't use subroutines.
- return new table.Record('Global Subr INDEX', [
- {name: 'subrs', type: 'INDEX', value: []}
- ]);
-}
-
-function makeCharsets(glyphNames, strings) {
- var t = new table.Record('Charsets', [
- {name: 'format', type: 'Card8', value: 0}
- ]);
- for (var i = 0; i < glyphNames.length; i += 1) {
- var glyphName = glyphNames[i];
- var glyphSID = encodeString(glyphName, strings);
- t.fields.push({name: 'glyph_' + i, type: 'SID', value: glyphSID});
- }
-
- return t;
-}
-
-function glyphToOps(glyph) {
- var ops = [];
- var path = glyph.path;
- ops.push({name: 'width', type: 'NUMBER', value: glyph.advanceWidth});
- var x = 0;
- var y = 0;
- for (var i = 0; i < path.commands.length; i += 1) {
- var dx = (void 0);
- var dy = (void 0);
- var cmd = path.commands[i];
- if (cmd.type === 'Q') {
- // CFF only supports bézier curves, so convert the quad to a bézier.
- var _13 = 1 / 3;
- var _23 = 2 / 3;
-
- // We're going to create a new command so we don't change the original path.
- // Since all coordinates are relative, we round() them ASAP to avoid propagating errors.
- cmd = {
- type: 'C',
- x: cmd.x,
- y: cmd.y,
- x1: Math.round(_13 * x + _23 * cmd.x1),
- y1: Math.round(_13 * y + _23 * cmd.y1),
- x2: Math.round(_13 * cmd.x + _23 * cmd.x1),
- y2: Math.round(_13 * cmd.y + _23 * cmd.y1)
- };
- }
-
- if (cmd.type === 'M') {
- dx = Math.round(cmd.x - x);
- dy = Math.round(cmd.y - y);
- ops.push({name: 'dx', type: 'NUMBER', value: dx});
- ops.push({name: 'dy', type: 'NUMBER', value: dy});
- ops.push({name: 'rmoveto', type: 'OP', value: 21});
- x = Math.round(cmd.x);
- y = Math.round(cmd.y);
- } else if (cmd.type === 'L') {
- dx = Math.round(cmd.x - x);
- dy = Math.round(cmd.y - y);
- ops.push({name: 'dx', type: 'NUMBER', value: dx});
- ops.push({name: 'dy', type: 'NUMBER', value: dy});
- ops.push({name: 'rlineto', type: 'OP', value: 5});
- x = Math.round(cmd.x);
- y = Math.round(cmd.y);
- } else if (cmd.type === 'C') {
- var dx1 = Math.round(cmd.x1 - x);
- var dy1 = Math.round(cmd.y1 - y);
- var dx2 = Math.round(cmd.x2 - cmd.x1);
- var dy2 = Math.round(cmd.y2 - cmd.y1);
- dx = Math.round(cmd.x - cmd.x2);
- dy = Math.round(cmd.y - cmd.y2);
- ops.push({name: 'dx1', type: 'NUMBER', value: dx1});
- ops.push({name: 'dy1', type: 'NUMBER', value: dy1});
- ops.push({name: 'dx2', type: 'NUMBER', value: dx2});
- ops.push({name: 'dy2', type: 'NUMBER', value: dy2});
- ops.push({name: 'dx', type: 'NUMBER', value: dx});
- ops.push({name: 'dy', type: 'NUMBER', value: dy});
- ops.push({name: 'rrcurveto', type: 'OP', value: 8});
- x = Math.round(cmd.x);
- y = Math.round(cmd.y);
- }
-
- // Contours are closed automatically.
- }
-
- ops.push({name: 'endchar', type: 'OP', value: 14});
- return ops;
-}
-
-function makeCharStringsIndex(glyphs) {
- var t = new table.Record('CharStrings INDEX', [
- {name: 'charStrings', type: 'INDEX', value: []}
- ]);
-
- for (var i = 0; i < glyphs.length; i += 1) {
- var glyph = glyphs.get(i);
- var ops = glyphToOps(glyph);
- t.charStrings.push({name: glyph.name, type: 'CHARSTRING', value: ops});
- }
-
- return t;
-}
-
-function makePrivateDict(attrs, strings) {
- var t = new table.Record('Private DICT', [
- {name: 'dict', type: 'DICT', value: {}}
- ]);
- t.dict = makeDict(PRIVATE_DICT_META, attrs, strings);
- return t;
-}
-
-function makeCFFTable(glyphs, options) {
- var t = new table.Table('CFF ', [
- {name: 'header', type: 'RECORD'},
- {name: 'nameIndex', type: 'RECORD'},
- {name: 'topDictIndex', type: 'RECORD'},
- {name: 'stringIndex', type: 'RECORD'},
- {name: 'globalSubrIndex', type: 'RECORD'},
- {name: 'charsets', type: 'RECORD'},
- {name: 'charStringsIndex', type: 'RECORD'},
- {name: 'privateDict', type: 'RECORD'}
- ]);
-
- var fontScale = 1 / options.unitsPerEm;
- // We use non-zero values for the offsets so that the DICT encodes them.
- // This is important because the size of the Top DICT plays a role in offset calculation,
- // and the size shouldn't change after we've written correct offsets.
- var attrs = {
- version: options.version,
- fullName: options.fullName,
- familyName: options.familyName,
- weight: options.weightName,
- fontBBox: options.fontBBox || [0, 0, 0, 0],
- fontMatrix: [fontScale, 0, 0, fontScale, 0, 0],
- charset: 999,
- encoding: 0,
- charStrings: 999,
- private: [0, 999]
- };
-
- var privateAttrs = {};
-
- var glyphNames = [];
- var glyph;
-
- // Skip first glyph (.notdef)
- for (var i = 1; i < glyphs.length; i += 1) {
- glyph = glyphs.get(i);
- glyphNames.push(glyph.name);
- }
-
- var strings = [];
-
- t.header = makeHeader();
- t.nameIndex = makeNameIndex([options.postScriptName]);
- var topDict = makeTopDict(attrs, strings);
- t.topDictIndex = makeTopDictIndex(topDict);
- t.globalSubrIndex = makeGlobalSubrIndex();
- t.charsets = makeCharsets(glyphNames, strings);
- t.charStringsIndex = makeCharStringsIndex(glyphs);
- t.privateDict = makePrivateDict(privateAttrs, strings);
-
- // Needs to come at the end, to encode all custom strings used in the font.
- t.stringIndex = makeStringIndex(strings);
-
- var startOffset = t.header.sizeOf() +
- t.nameIndex.sizeOf() +
- t.topDictIndex.sizeOf() +
- t.stringIndex.sizeOf() +
- t.globalSubrIndex.sizeOf();
- attrs.charset = startOffset;
-
- // We use the CFF standard encoding; proper encoding will be handled in cmap.
- attrs.encoding = 0;
- attrs.charStrings = attrs.charset + t.charsets.sizeOf();
- attrs.private[1] = attrs.charStrings + t.charStringsIndex.sizeOf();
-
- // Recreate the Top DICT INDEX with the correct offsets.
- topDict = makeTopDict(attrs, strings);
- t.topDictIndex = makeTopDictIndex(topDict);
-
- return t;
-}
-
-var cff = { parse: parseCFFTable, make: makeCFFTable };
-
-// The `head` table contains global information about the font.
-
-// Parse the header `head` table
-function parseHeadTable(data, start) {
- var head = {};
- var p = new parse.Parser(data, start);
- head.version = p.parseVersion();
- head.fontRevision = Math.round(p.parseFixed() * 1000) / 1000;
- head.checkSumAdjustment = p.parseULong();
- head.magicNumber = p.parseULong();
- check.argument(head.magicNumber === 0x5F0F3CF5, 'Font header has wrong magic number.');
- head.flags = p.parseUShort();
- head.unitsPerEm = p.parseUShort();
- head.created = p.parseLongDateTime();
- head.modified = p.parseLongDateTime();
- head.xMin = p.parseShort();
- head.yMin = p.parseShort();
- head.xMax = p.parseShort();
- head.yMax = p.parseShort();
- head.macStyle = p.parseUShort();
- head.lowestRecPPEM = p.parseUShort();
- head.fontDirectionHint = p.parseShort();
- head.indexToLocFormat = p.parseShort();
- head.glyphDataFormat = p.parseShort();
- return head;
-}
-
-function makeHeadTable(options) {
- // Apple Mac timestamp epoch is 01/01/1904 not 01/01/1970
- var timestamp = Math.round(new Date().getTime() / 1000) + 2082844800;
- var createdTimestamp = timestamp;
-
- if (options.createdTimestamp) {
- createdTimestamp = options.createdTimestamp + 2082844800;
- }
-
- return new table.Table('head', [
- {name: 'version', type: 'FIXED', value: 0x00010000},
- {name: 'fontRevision', type: 'FIXED', value: 0x00010000},
- {name: 'checkSumAdjustment', type: 'ULONG', value: 0},
- {name: 'magicNumber', type: 'ULONG', value: 0x5F0F3CF5},
- {name: 'flags', type: 'USHORT', value: 0},
- {name: 'unitsPerEm', type: 'USHORT', value: 1000},
- {name: 'created', type: 'LONGDATETIME', value: createdTimestamp},
- {name: 'modified', type: 'LONGDATETIME', value: timestamp},
- {name: 'xMin', type: 'SHORT', value: 0},
- {name: 'yMin', type: 'SHORT', value: 0},
- {name: 'xMax', type: 'SHORT', value: 0},
- {name: 'yMax', type: 'SHORT', value: 0},
- {name: 'macStyle', type: 'USHORT', value: 0},
- {name: 'lowestRecPPEM', type: 'USHORT', value: 0},
- {name: 'fontDirectionHint', type: 'SHORT', value: 2},
- {name: 'indexToLocFormat', type: 'SHORT', value: 0},
- {name: 'glyphDataFormat', type: 'SHORT', value: 0}
- ], options);
-}
-
-var head = { parse: parseHeadTable, make: makeHeadTable };
-
-// The `hhea` table contains information for horizontal layout.
-
-// Parse the horizontal header `hhea` table
-function parseHheaTable(data, start) {
- var hhea = {};
- var p = new parse.Parser(data, start);
- hhea.version = p.parseVersion();
- hhea.ascender = p.parseShort();
- hhea.descender = p.parseShort();
- hhea.lineGap = p.parseShort();
- hhea.advanceWidthMax = p.parseUShort();
- hhea.minLeftSideBearing = p.parseShort();
- hhea.minRightSideBearing = p.parseShort();
- hhea.xMaxExtent = p.parseShort();
- hhea.caretSlopeRise = p.parseShort();
- hhea.caretSlopeRun = p.parseShort();
- hhea.caretOffset = p.parseShort();
- p.relativeOffset += 8;
- hhea.metricDataFormat = p.parseShort();
- hhea.numberOfHMetrics = p.parseUShort();
- return hhea;
-}
-
-function makeHheaTable(options) {
- return new table.Table('hhea', [
- {name: 'version', type: 'FIXED', value: 0x00010000},
- {name: 'ascender', type: 'FWORD', value: 0},
- {name: 'descender', type: 'FWORD', value: 0},
- {name: 'lineGap', type: 'FWORD', value: 0},
- {name: 'advanceWidthMax', type: 'UFWORD', value: 0},
- {name: 'minLeftSideBearing', type: 'FWORD', value: 0},
- {name: 'minRightSideBearing', type: 'FWORD', value: 0},
- {name: 'xMaxExtent', type: 'FWORD', value: 0},
- {name: 'caretSlopeRise', type: 'SHORT', value: 1},
- {name: 'caretSlopeRun', type: 'SHORT', value: 0},
- {name: 'caretOffset', type: 'SHORT', value: 0},
- {name: 'reserved1', type: 'SHORT', value: 0},
- {name: 'reserved2', type: 'SHORT', value: 0},
- {name: 'reserved3', type: 'SHORT', value: 0},
- {name: 'reserved4', type: 'SHORT', value: 0},
- {name: 'metricDataFormat', type: 'SHORT', value: 0},
- {name: 'numberOfHMetrics', type: 'USHORT', value: 0}
- ], options);
-}
-
-var hhea = { parse: parseHheaTable, make: makeHheaTable };
-
-// The `hmtx` table contains the horizontal metrics for all glyphs.
-
-function parseHmtxTableAll(data, start, numMetrics, numGlyphs, glyphs) {
- var advanceWidth;
- var leftSideBearing;
- var p = new parse.Parser(data, start);
- for (var i = 0; i < numGlyphs; i += 1) {
- // If the font is monospaced, only one entry is needed. This last entry applies to all subsequent glyphs.
- if (i < numMetrics) {
- advanceWidth = p.parseUShort();
- leftSideBearing = p.parseShort();
- }
-
- var glyph = glyphs.get(i);
- glyph.advanceWidth = advanceWidth;
- glyph.leftSideBearing = leftSideBearing;
- }
-}
-
-function parseHmtxTableOnLowMemory(font, data, start, numMetrics, numGlyphs) {
- font._hmtxTableData = {};
-
- var advanceWidth;
- var leftSideBearing;
- var p = new parse.Parser(data, start);
- for (var i = 0; i < numGlyphs; i += 1) {
- // If the font is monospaced, only one entry is needed. This last entry applies to all subsequent glyphs.
- if (i < numMetrics) {
- advanceWidth = p.parseUShort();
- leftSideBearing = p.parseShort();
- }
-
- font._hmtxTableData[i] = {
- advanceWidth: advanceWidth,
- leftSideBearing: leftSideBearing
- };
- }
-}
-
-// Parse the `hmtx` table, which contains the horizontal metrics for all glyphs.
-// This function augments the glyph array, adding the advanceWidth and leftSideBearing to each glyph.
-function parseHmtxTable(font, data, start, numMetrics, numGlyphs, glyphs, opt) {
- if (opt.lowMemory)
- { parseHmtxTableOnLowMemory(font, data, start, numMetrics, numGlyphs); }
- else
- { parseHmtxTableAll(data, start, numMetrics, numGlyphs, glyphs); }
-}
-
-function makeHmtxTable(glyphs) {
- var t = new table.Table('hmtx', []);
- for (var i = 0; i < glyphs.length; i += 1) {
- var glyph = glyphs.get(i);
- var advanceWidth = glyph.advanceWidth || 0;
- var leftSideBearing = glyph.leftSideBearing || 0;
- t.fields.push({name: 'advanceWidth_' + i, type: 'USHORT', value: advanceWidth});
- t.fields.push({name: 'leftSideBearing_' + i, type: 'SHORT', value: leftSideBearing});
- }
-
- return t;
-}
-
-var hmtx = { parse: parseHmtxTable, make: makeHmtxTable };
-
-// The `ltag` table stores IETF BCP-47 language tags. It allows supporting
-
-function makeLtagTable(tags) {
- var result = new table.Table('ltag', [
- {name: 'version', type: 'ULONG', value: 1},
- {name: 'flags', type: 'ULONG', value: 0},
- {name: 'numTags', type: 'ULONG', value: tags.length}
- ]);
-
- var stringPool = '';
- var stringPoolOffset = 12 + tags.length * 4;
- for (var i = 0; i < tags.length; ++i) {
- var pos = stringPool.indexOf(tags[i]);
- if (pos < 0) {
- pos = stringPool.length;
- stringPool += tags[i];
- }
-
- result.fields.push({name: 'offset ' + i, type: 'USHORT', value: stringPoolOffset + pos});
- result.fields.push({name: 'length ' + i, type: 'USHORT', value: tags[i].length});
- }
-
- result.fields.push({name: 'stringPool', type: 'CHARARRAY', value: stringPool});
- return result;
-}
-
-function parseLtagTable(data, start) {
- var p = new parse.Parser(data, start);
- var tableVersion = p.parseULong();
- check.argument(tableVersion === 1, 'Unsupported ltag table version.');
- // The 'ltag' specification does not define any flags; skip the field.
- p.skip('uLong', 1);
- var numTags = p.parseULong();
-
- var tags = [];
- for (var i = 0; i < numTags; i++) {
- var tag = '';
- var offset = start + p.parseUShort();
- var length = p.parseUShort();
- for (var j = offset; j < offset + length; ++j) {
- tag += String.fromCharCode(data.getInt8(j));
- }
-
- tags.push(tag);
- }
-
- return tags;
-}
-
-var ltag = { make: makeLtagTable, parse: parseLtagTable };
-
-// The `maxp` table establishes the memory requirements for the font.
-
-// Parse the maximum profile `maxp` table.
-function parseMaxpTable(data, start) {
- var maxp = {};
- var p = new parse.Parser(data, start);
- maxp.version = p.parseVersion();
- maxp.numGlyphs = p.parseUShort();
- if (maxp.version === 1.0) {
- maxp.maxPoints = p.parseUShort();
- maxp.maxContours = p.parseUShort();
- maxp.maxCompositePoints = p.parseUShort();
- maxp.maxCompositeContours = p.parseUShort();
- maxp.maxZones = p.parseUShort();
- maxp.maxTwilightPoints = p.parseUShort();
- maxp.maxStorage = p.parseUShort();
- maxp.maxFunctionDefs = p.parseUShort();
- maxp.maxInstructionDefs = p.parseUShort();
- maxp.maxStackElements = p.parseUShort();
- maxp.maxSizeOfInstructions = p.parseUShort();
- maxp.maxComponentElements = p.parseUShort();
- maxp.maxComponentDepth = p.parseUShort();
- }
-
- return maxp;
-}
-
-function makeMaxpTable(numGlyphs) {
- return new table.Table('maxp', [
- {name: 'version', type: 'FIXED', value: 0x00005000},
- {name: 'numGlyphs', type: 'USHORT', value: numGlyphs}
- ]);
-}
-
-var maxp = { parse: parseMaxpTable, make: makeMaxpTable };
-
-// The `name` naming table.
-
-// NameIDs for the name table.
-var nameTableNames = [
- 'copyright', // 0
- 'fontFamily', // 1
- 'fontSubfamily', // 2
- 'uniqueID', // 3
- 'fullName', // 4
- 'version', // 5
- 'postScriptName', // 6
- 'trademark', // 7
- 'manufacturer', // 8
- 'designer', // 9
- 'description', // 10
- 'manufacturerURL', // 11
- 'designerURL', // 12
- 'license', // 13
- 'licenseURL', // 14
- 'reserved', // 15
- 'preferredFamily', // 16
- 'preferredSubfamily', // 17
- 'compatibleFullName', // 18
- 'sampleText', // 19
- 'postScriptFindFontName', // 20
- 'wwsFamily', // 21
- 'wwsSubfamily' // 22
-];
-
-var macLanguages = {
- 0: 'en',
- 1: 'fr',
- 2: 'de',
- 3: 'it',
- 4: 'nl',
- 5: 'sv',
- 6: 'es',
- 7: 'da',
- 8: 'pt',
- 9: 'no',
- 10: 'he',
- 11: 'ja',
- 12: 'ar',
- 13: 'fi',
- 14: 'el',
- 15: 'is',
- 16: 'mt',
- 17: 'tr',
- 18: 'hr',
- 19: 'zh-Hant',
- 20: 'ur',
- 21: 'hi',
- 22: 'th',
- 23: 'ko',
- 24: 'lt',
- 25: 'pl',
- 26: 'hu',
- 27: 'es',
- 28: 'lv',
- 29: 'se',
- 30: 'fo',
- 31: 'fa',
- 32: 'ru',
- 33: 'zh',
- 34: 'nl-BE',
- 35: 'ga',
- 36: 'sq',
- 37: 'ro',
- 38: 'cz',
- 39: 'sk',
- 40: 'si',
- 41: 'yi',
- 42: 'sr',
- 43: 'mk',
- 44: 'bg',
- 45: 'uk',
- 46: 'be',
- 47: 'uz',
- 48: 'kk',
- 49: 'az-Cyrl',
- 50: 'az-Arab',
- 51: 'hy',
- 52: 'ka',
- 53: 'mo',
- 54: 'ky',
- 55: 'tg',
- 56: 'tk',
- 57: 'mn-CN',
- 58: 'mn',
- 59: 'ps',
- 60: 'ks',
- 61: 'ku',
- 62: 'sd',
- 63: 'bo',
- 64: 'ne',
- 65: 'sa',
- 66: 'mr',
- 67: 'bn',
- 68: 'as',
- 69: 'gu',
- 70: 'pa',
- 71: 'or',
- 72: 'ml',
- 73: 'kn',
- 74: 'ta',
- 75: 'te',
- 76: 'si',
- 77: 'my',
- 78: 'km',
- 79: 'lo',
- 80: 'vi',
- 81: 'id',
- 82: 'tl',
- 83: 'ms',
- 84: 'ms-Arab',
- 85: 'am',
- 86: 'ti',
- 87: 'om',
- 88: 'so',
- 89: 'sw',
- 90: 'rw',
- 91: 'rn',
- 92: 'ny',
- 93: 'mg',
- 94: 'eo',
- 128: 'cy',
- 129: 'eu',
- 130: 'ca',
- 131: 'la',
- 132: 'qu',
- 133: 'gn',
- 134: 'ay',
- 135: 'tt',
- 136: 'ug',
- 137: 'dz',
- 138: 'jv',
- 139: 'su',
- 140: 'gl',
- 141: 'af',
- 142: 'br',
- 143: 'iu',
- 144: 'gd',
- 145: 'gv',
- 146: 'ga',
- 147: 'to',
- 148: 'el-polyton',
- 149: 'kl',
- 150: 'az',
- 151: 'nn'
-};
-
-// MacOS language ID → MacOS script ID
-//
-// Note that the script ID is not sufficient to determine what encoding
-// to use in TrueType files. For some languages, MacOS used a modification
-// of a mainstream script. For example, an Icelandic name would be stored
-// with smRoman in the TrueType naming table, but the actual encoding
-// is a special Icelandic version of the normal Macintosh Roman encoding.
-// As another example, Inuktitut uses an 8-bit encoding for Canadian Aboriginal
-// Syllables but MacOS had run out of available script codes, so this was
-// done as a (pretty radical) "modification" of Ethiopic.
-//
-// http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/Readme.txt
-var macLanguageToScript = {
- 0: 0, // langEnglish → smRoman
- 1: 0, // langFrench → smRoman
- 2: 0, // langGerman → smRoman
- 3: 0, // langItalian → smRoman
- 4: 0, // langDutch → smRoman
- 5: 0, // langSwedish → smRoman
- 6: 0, // langSpanish → smRoman
- 7: 0, // langDanish → smRoman
- 8: 0, // langPortuguese → smRoman
- 9: 0, // langNorwegian → smRoman
- 10: 5, // langHebrew → smHebrew
- 11: 1, // langJapanese → smJapanese
- 12: 4, // langArabic → smArabic
- 13: 0, // langFinnish → smRoman
- 14: 6, // langGreek → smGreek
- 15: 0, // langIcelandic → smRoman (modified)
- 16: 0, // langMaltese → smRoman
- 17: 0, // langTurkish → smRoman (modified)
- 18: 0, // langCroatian → smRoman (modified)
- 19: 2, // langTradChinese → smTradChinese
- 20: 4, // langUrdu → smArabic
- 21: 9, // langHindi → smDevanagari
- 22: 21, // langThai → smThai
- 23: 3, // langKorean → smKorean
- 24: 29, // langLithuanian → smCentralEuroRoman
- 25: 29, // langPolish → smCentralEuroRoman
- 26: 29, // langHungarian → smCentralEuroRoman
- 27: 29, // langEstonian → smCentralEuroRoman
- 28: 29, // langLatvian → smCentralEuroRoman
- 29: 0, // langSami → smRoman
- 30: 0, // langFaroese → smRoman (modified)
- 31: 4, // langFarsi → smArabic (modified)
- 32: 7, // langRussian → smCyrillic
- 33: 25, // langSimpChinese → smSimpChinese
- 34: 0, // langFlemish → smRoman
- 35: 0, // langIrishGaelic → smRoman (modified)
- 36: 0, // langAlbanian → smRoman
- 37: 0, // langRomanian → smRoman (modified)
- 38: 29, // langCzech → smCentralEuroRoman
- 39: 29, // langSlovak → smCentralEuroRoman
- 40: 0, // langSlovenian → smRoman (modified)
- 41: 5, // langYiddish → smHebrew
- 42: 7, // langSerbian → smCyrillic
- 43: 7, // langMacedonian → smCyrillic
- 44: 7, // langBulgarian → smCyrillic
- 45: 7, // langUkrainian → smCyrillic (modified)
- 46: 7, // langByelorussian → smCyrillic
- 47: 7, // langUzbek → smCyrillic
- 48: 7, // langKazakh → smCyrillic
- 49: 7, // langAzerbaijani → smCyrillic
- 50: 4, // langAzerbaijanAr → smArabic
- 51: 24, // langArmenian → smArmenian
- 52: 23, // langGeorgian → smGeorgian
- 53: 7, // langMoldavian → smCyrillic
- 54: 7, // langKirghiz → smCyrillic
- 55: 7, // langTajiki → smCyrillic
- 56: 7, // langTurkmen → smCyrillic
- 57: 27, // langMongolian → smMongolian
- 58: 7, // langMongolianCyr → smCyrillic
- 59: 4, // langPashto → smArabic
- 60: 4, // langKurdish → smArabic
- 61: 4, // langKashmiri → smArabic
- 62: 4, // langSindhi → smArabic
- 63: 26, // langTibetan → smTibetan
- 64: 9, // langNepali → smDevanagari
- 65: 9, // langSanskrit → smDevanagari
- 66: 9, // langMarathi → smDevanagari
- 67: 13, // langBengali → smBengali
- 68: 13, // langAssamese → smBengali
- 69: 11, // langGujarati → smGujarati
- 70: 10, // langPunjabi → smGurmukhi
- 71: 12, // langOriya → smOriya
- 72: 17, // langMalayalam → smMalayalam
- 73: 16, // langKannada → smKannada
- 74: 14, // langTamil → smTamil
- 75: 15, // langTelugu → smTelugu
- 76: 18, // langSinhalese → smSinhalese
- 77: 19, // langBurmese → smBurmese
- 78: 20, // langKhmer → smKhmer
- 79: 22, // langLao → smLao
- 80: 30, // langVietnamese → smVietnamese
- 81: 0, // langIndonesian → smRoman
- 82: 0, // langTagalog → smRoman
- 83: 0, // langMalayRoman → smRoman
- 84: 4, // langMalayArabic → smArabic
- 85: 28, // langAmharic → smEthiopic
- 86: 28, // langTigrinya → smEthiopic
- 87: 28, // langOromo → smEthiopic
- 88: 0, // langSomali → smRoman
- 89: 0, // langSwahili → smRoman
- 90: 0, // langKinyarwanda → smRoman
- 91: 0, // langRundi → smRoman
- 92: 0, // langNyanja → smRoman
- 93: 0, // langMalagasy → smRoman
- 94: 0, // langEsperanto → smRoman
- 128: 0, // langWelsh → smRoman (modified)
- 129: 0, // langBasque → smRoman
- 130: 0, // langCatalan → smRoman
- 131: 0, // langLatin → smRoman
- 132: 0, // langQuechua → smRoman
- 133: 0, // langGuarani → smRoman
- 134: 0, // langAymara → smRoman
- 135: 7, // langTatar → smCyrillic
- 136: 4, // langUighur → smArabic
- 137: 26, // langDzongkha → smTibetan
- 138: 0, // langJavaneseRom → smRoman
- 139: 0, // langSundaneseRom → smRoman
- 140: 0, // langGalician → smRoman
- 141: 0, // langAfrikaans → smRoman
- 142: 0, // langBreton → smRoman (modified)
- 143: 28, // langInuktitut → smEthiopic (modified)
- 144: 0, // langScottishGaelic → smRoman (modified)
- 145: 0, // langManxGaelic → smRoman (modified)
- 146: 0, // langIrishGaelicScript → smRoman (modified)
- 147: 0, // langTongan → smRoman
- 148: 6, // langGreekAncient → smRoman
- 149: 0, // langGreenlandic → smRoman
- 150: 0, // langAzerbaijanRoman → smRoman
- 151: 0 // langNynorsk → smRoman
-};
-
-// While Microsoft indicates a region/country for all its language
-// IDs, we omit the region code if it's equal to the "most likely
-// region subtag" according to Unicode CLDR. For scripts, we omit
-// the subtag if it is equal to the Suppress-Script entry in the
-// IANA language subtag registry for IETF BCP 47.
-//
-// For example, Microsoft states that its language code 0x041A is
-// Croatian in Croatia. We transform this to the BCP 47 language code 'hr'
-// and not 'hr-HR' because Croatia is the default country for Croatian,
-// according to Unicode CLDR. As another example, Microsoft states
-// that 0x101A is Croatian (Latin) in Bosnia-Herzegovina. We transform
-// this to 'hr-BA' and not 'hr-Latn-BA' because Latin is the default script
-// for the Croatian language, according to IANA.
-//
-// http://www.unicode.org/cldr/charts/latest/supplemental/likely_subtags.html
-// http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
-var windowsLanguages = {
- 0x0436: 'af',
- 0x041C: 'sq',
- 0x0484: 'gsw',
- 0x045E: 'am',
- 0x1401: 'ar-DZ',
- 0x3C01: 'ar-BH',
- 0x0C01: 'ar',
- 0x0801: 'ar-IQ',
- 0x2C01: 'ar-JO',
- 0x3401: 'ar-KW',
- 0x3001: 'ar-LB',
- 0x1001: 'ar-LY',
- 0x1801: 'ary',
- 0x2001: 'ar-OM',
- 0x4001: 'ar-QA',
- 0x0401: 'ar-SA',
- 0x2801: 'ar-SY',
- 0x1C01: 'aeb',
- 0x3801: 'ar-AE',
- 0x2401: 'ar-YE',
- 0x042B: 'hy',
- 0x044D: 'as',
- 0x082C: 'az-Cyrl',
- 0x042C: 'az',
- 0x046D: 'ba',
- 0x042D: 'eu',
- 0x0423: 'be',
- 0x0845: 'bn',
- 0x0445: 'bn-IN',
- 0x201A: 'bs-Cyrl',
- 0x141A: 'bs',
- 0x047E: 'br',
- 0x0402: 'bg',
- 0x0403: 'ca',
- 0x0C04: 'zh-HK',
- 0x1404: 'zh-MO',
- 0x0804: 'zh',
- 0x1004: 'zh-SG',
- 0x0404: 'zh-TW',
- 0x0483: 'co',
- 0x041A: 'hr',
- 0x101A: 'hr-BA',
- 0x0405: 'cs',
- 0x0406: 'da',
- 0x048C: 'prs',
- 0x0465: 'dv',
- 0x0813: 'nl-BE',
- 0x0413: 'nl',
- 0x0C09: 'en-AU',
- 0x2809: 'en-BZ',
- 0x1009: 'en-CA',
- 0x2409: 'en-029',
- 0x4009: 'en-IN',
- 0x1809: 'en-IE',
- 0x2009: 'en-JM',
- 0x4409: 'en-MY',
- 0x1409: 'en-NZ',
- 0x3409: 'en-PH',
- 0x4809: 'en-SG',
- 0x1C09: 'en-ZA',
- 0x2C09: 'en-TT',
- 0x0809: 'en-GB',
- 0x0409: 'en',
- 0x3009: 'en-ZW',
- 0x0425: 'et',
- 0x0438: 'fo',
- 0x0464: 'fil',
- 0x040B: 'fi',
- 0x080C: 'fr-BE',
- 0x0C0C: 'fr-CA',
- 0x040C: 'fr',
- 0x140C: 'fr-LU',
- 0x180C: 'fr-MC',
- 0x100C: 'fr-CH',
- 0x0462: 'fy',
- 0x0456: 'gl',
- 0x0437: 'ka',
- 0x0C07: 'de-AT',
- 0x0407: 'de',
- 0x1407: 'de-LI',
- 0x1007: 'de-LU',
- 0x0807: 'de-CH',
- 0x0408: 'el',
- 0x046F: 'kl',
- 0x0447: 'gu',
- 0x0468: 'ha',
- 0x040D: 'he',
- 0x0439: 'hi',
- 0x040E: 'hu',
- 0x040F: 'is',
- 0x0470: 'ig',
- 0x0421: 'id',
- 0x045D: 'iu',
- 0x085D: 'iu-Latn',
- 0x083C: 'ga',
- 0x0434: 'xh',
- 0x0435: 'zu',
- 0x0410: 'it',
- 0x0810: 'it-CH',
- 0x0411: 'ja',
- 0x044B: 'kn',
- 0x043F: 'kk',
- 0x0453: 'km',
- 0x0486: 'quc',
- 0x0487: 'rw',
- 0x0441: 'sw',
- 0x0457: 'kok',
- 0x0412: 'ko',
- 0x0440: 'ky',
- 0x0454: 'lo',
- 0x0426: 'lv',
- 0x0427: 'lt',
- 0x082E: 'dsb',
- 0x046E: 'lb',
- 0x042F: 'mk',
- 0x083E: 'ms-BN',
- 0x043E: 'ms',
- 0x044C: 'ml',
- 0x043A: 'mt',
- 0x0481: 'mi',
- 0x047A: 'arn',
- 0x044E: 'mr',
- 0x047C: 'moh',
- 0x0450: 'mn',
- 0x0850: 'mn-CN',
- 0x0461: 'ne',
- 0x0414: 'nb',
- 0x0814: 'nn',
- 0x0482: 'oc',
- 0x0448: 'or',
- 0x0463: 'ps',
- 0x0415: 'pl',
- 0x0416: 'pt',
- 0x0816: 'pt-PT',
- 0x0446: 'pa',
- 0x046B: 'qu-BO',
- 0x086B: 'qu-EC',
- 0x0C6B: 'qu',
- 0x0418: 'ro',
- 0x0417: 'rm',
- 0x0419: 'ru',
- 0x243B: 'smn',
- 0x103B: 'smj-NO',
- 0x143B: 'smj',
- 0x0C3B: 'se-FI',
- 0x043B: 'se',
- 0x083B: 'se-SE',
- 0x203B: 'sms',
- 0x183B: 'sma-NO',
- 0x1C3B: 'sms',
- 0x044F: 'sa',
- 0x1C1A: 'sr-Cyrl-BA',
- 0x0C1A: 'sr',
- 0x181A: 'sr-Latn-BA',
- 0x081A: 'sr-Latn',
- 0x046C: 'nso',
- 0x0432: 'tn',
- 0x045B: 'si',
- 0x041B: 'sk',
- 0x0424: 'sl',
- 0x2C0A: 'es-AR',
- 0x400A: 'es-BO',
- 0x340A: 'es-CL',
- 0x240A: 'es-CO',
- 0x140A: 'es-CR',
- 0x1C0A: 'es-DO',
- 0x300A: 'es-EC',
- 0x440A: 'es-SV',
- 0x100A: 'es-GT',
- 0x480A: 'es-HN',
- 0x080A: 'es-MX',
- 0x4C0A: 'es-NI',
- 0x180A: 'es-PA',
- 0x3C0A: 'es-PY',
- 0x280A: 'es-PE',
- 0x500A: 'es-PR',
-
- // Microsoft has defined two different language codes for
- // “Spanish with modern sorting” and “Spanish with traditional
- // sorting”. This makes sense for collation APIs, and it would be
- // possible to express this in BCP 47 language tags via Unicode
- // extensions (eg., es-u-co-trad is Spanish with traditional
- // sorting). However, for storing names in fonts, the distinction
- // does not make sense, so we give “es” in both cases.
- 0x0C0A: 'es',
- 0x040A: 'es',
-
- 0x540A: 'es-US',
- 0x380A: 'es-UY',
- 0x200A: 'es-VE',
- 0x081D: 'sv-FI',
- 0x041D: 'sv',
- 0x045A: 'syr',
- 0x0428: 'tg',
- 0x085F: 'tzm',
- 0x0449: 'ta',
- 0x0444: 'tt',
- 0x044A: 'te',
- 0x041E: 'th',
- 0x0451: 'bo',
- 0x041F: 'tr',
- 0x0442: 'tk',
- 0x0480: 'ug',
- 0x0422: 'uk',
- 0x042E: 'hsb',
- 0x0420: 'ur',
- 0x0843: 'uz-Cyrl',
- 0x0443: 'uz',
- 0x042A: 'vi',
- 0x0452: 'cy',
- 0x0488: 'wo',
- 0x0485: 'sah',
- 0x0478: 'ii',
- 0x046A: 'yo'
-};
-
-// Returns a IETF BCP 47 language code, for example 'zh-Hant'
-// for 'Chinese in the traditional script'.
-function getLanguageCode(platformID, languageID, ltag) {
- switch (platformID) {
- case 0: // Unicode
- if (languageID === 0xFFFF) {
- return 'und';
- } else if (ltag) {
- return ltag[languageID];
- }
-
- break;
-
- case 1: // Macintosh
- return macLanguages[languageID];
-
- case 3: // Windows
- return windowsLanguages[languageID];
- }
-
- return undefined;
-}
-
-var utf16 = 'utf-16';
-
-// MacOS script ID → encoding. This table stores the default case,
-// which can be overridden by macLanguageEncodings.
-var macScriptEncodings = {
- 0: 'macintosh', // smRoman
- 1: 'x-mac-japanese', // smJapanese
- 2: 'x-mac-chinesetrad', // smTradChinese
- 3: 'x-mac-korean', // smKorean
- 6: 'x-mac-greek', // smGreek
- 7: 'x-mac-cyrillic', // smCyrillic
- 9: 'x-mac-devanagai', // smDevanagari
- 10: 'x-mac-gurmukhi', // smGurmukhi
- 11: 'x-mac-gujarati', // smGujarati
- 12: 'x-mac-oriya', // smOriya
- 13: 'x-mac-bengali', // smBengali
- 14: 'x-mac-tamil', // smTamil
- 15: 'x-mac-telugu', // smTelugu
- 16: 'x-mac-kannada', // smKannada
- 17: 'x-mac-malayalam', // smMalayalam
- 18: 'x-mac-sinhalese', // smSinhalese
- 19: 'x-mac-burmese', // smBurmese
- 20: 'x-mac-khmer', // smKhmer
- 21: 'x-mac-thai', // smThai
- 22: 'x-mac-lao', // smLao
- 23: 'x-mac-georgian', // smGeorgian
- 24: 'x-mac-armenian', // smArmenian
- 25: 'x-mac-chinesesimp', // smSimpChinese
- 26: 'x-mac-tibetan', // smTibetan
- 27: 'x-mac-mongolian', // smMongolian
- 28: 'x-mac-ethiopic', // smEthiopic
- 29: 'x-mac-ce', // smCentralEuroRoman
- 30: 'x-mac-vietnamese', // smVietnamese
- 31: 'x-mac-extarabic' // smExtArabic
-};
-
-// MacOS language ID → encoding. This table stores the exceptional
-// cases, which override macScriptEncodings. For writing MacOS naming
-// tables, we need to emit a MacOS script ID. Therefore, we cannot
-// merge macScriptEncodings into macLanguageEncodings.
-//
-// http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/Readme.txt
-var macLanguageEncodings = {
- 15: 'x-mac-icelandic', // langIcelandic
- 17: 'x-mac-turkish', // langTurkish
- 18: 'x-mac-croatian', // langCroatian
- 24: 'x-mac-ce', // langLithuanian
- 25: 'x-mac-ce', // langPolish
- 26: 'x-mac-ce', // langHungarian
- 27: 'x-mac-ce', // langEstonian
- 28: 'x-mac-ce', // langLatvian
- 30: 'x-mac-icelandic', // langFaroese
- 37: 'x-mac-romanian', // langRomanian
- 38: 'x-mac-ce', // langCzech
- 39: 'x-mac-ce', // langSlovak
- 40: 'x-mac-ce', // langSlovenian
- 143: 'x-mac-inuit', // langInuktitut
- 146: 'x-mac-gaelic' // langIrishGaelicScript
-};
-
-function getEncoding(platformID, encodingID, languageID) {
- switch (platformID) {
- case 0: // Unicode
- return utf16;
-
- case 1: // Apple Macintosh
- return macLanguageEncodings[languageID] || macScriptEncodings[encodingID];
-
- case 3: // Microsoft Windows
- if (encodingID === 1 || encodingID === 10) {
- return utf16;
- }
-
- break;
- }
-
- return undefined;
-}
-
-// Parse the naming `name` table.
-// FIXME: Format 1 additional fields are not supported yet.
-// ltag is the content of the `ltag' table, such as ['en', 'zh-Hans', 'de-CH-1904'].
-function parseNameTable(data, start, ltag) {
- var name = {};
- var p = new parse.Parser(data, start);
- var format = p.parseUShort();
- var count = p.parseUShort();
- var stringOffset = p.offset + p.parseUShort();
- for (var i = 0; i < count; i++) {
- var platformID = p.parseUShort();
- var encodingID = p.parseUShort();
- var languageID = p.parseUShort();
- var nameID = p.parseUShort();
- var property = nameTableNames[nameID] || nameID;
- var byteLength = p.parseUShort();
- var offset = p.parseUShort();
- var language = getLanguageCode(platformID, languageID, ltag);
- var encoding = getEncoding(platformID, encodingID, languageID);
- if (encoding !== undefined && language !== undefined) {
- var text = (void 0);
- if (encoding === utf16) {
- text = decode.UTF16(data, stringOffset + offset, byteLength);
- } else {
- text = decode.MACSTRING(data, stringOffset + offset, byteLength, encoding);
- }
-
- if (text) {
- var translations = name[property];
- if (translations === undefined) {
- translations = name[property] = {};
- }
-
- translations[language] = text;
- }
- }
- }
-
- var langTagCount = 0;
- if (format === 1) {
- // FIXME: Also handle Microsoft's 'name' table 1.
- langTagCount = p.parseUShort();
- }
-
- return name;
-}
-
-// {23: 'foo'} → {'foo': 23}
-// ['bar', 'baz'] → {'bar': 0, 'baz': 1}
-function reverseDict(dict) {
- var result = {};
- for (var key in dict) {
- result[dict[key]] = parseInt(key);
- }
-
- return result;
-}
-
-function makeNameRecord(platformID, encodingID, languageID, nameID, length, offset) {
- return new table.Record('NameRecord', [
- {name: 'platformID', type: 'USHORT', value: platformID},
- {name: 'encodingID', type: 'USHORT', value: encodingID},
- {name: 'languageID', type: 'USHORT', value: languageID},
- {name: 'nameID', type: 'USHORT', value: nameID},
- {name: 'length', type: 'USHORT', value: length},
- {name: 'offset', type: 'USHORT', value: offset}
- ]);
-}
-
-// Finds the position of needle in haystack, or -1 if not there.
-// Like String.indexOf(), but for arrays.
-function findSubArray(needle, haystack) {
- var needleLength = needle.length;
- var limit = haystack.length - needleLength + 1;
-
- loop:
- for (var pos = 0; pos < limit; pos++) {
- for (; pos < limit; pos++) {
- for (var k = 0; k < needleLength; k++) {
- if (haystack[pos + k] !== needle[k]) {
- continue loop;
- }
- }
-
- return pos;
- }
- }
-
- return -1;
-}
-
-function addStringToPool(s, pool) {
- var offset = findSubArray(s, pool);
- if (offset < 0) {
- offset = pool.length;
- var i = 0;
- var len = s.length;
- for (; i < len; ++i) {
- pool.push(s[i]);
- }
-
- }
-
- return offset;
-}
-
-function makeNameTable(names, ltag) {
- var nameID;
- var nameIDs = [];
-
- var namesWithNumericKeys = {};
- var nameTableIds = reverseDict(nameTableNames);
- for (var key in names) {
- var id = nameTableIds[key];
- if (id === undefined) {
- id = key;
- }
-
- nameID = parseInt(id);
-
- if (isNaN(nameID)) {
- throw new Error('Name table entry "' + key + '" does not exist, see nameTableNames for complete list.');
- }
-
- namesWithNumericKeys[nameID] = names[key];
- nameIDs.push(nameID);
- }
-
- var macLanguageIds = reverseDict(macLanguages);
- var windowsLanguageIds = reverseDict(windowsLanguages);
-
- var nameRecords = [];
- var stringPool = [];
-
- for (var i = 0; i < nameIDs.length; i++) {
- nameID = nameIDs[i];
- var translations = namesWithNumericKeys[nameID];
- for (var lang in translations) {
- var text = translations[lang];
-
- // For MacOS, we try to emit the name in the form that was introduced
- // in the initial version of the TrueType spec (in the late 1980s).
- // However, this can fail for various reasons: the requested BCP 47
- // language code might not have an old-style Mac equivalent;
- // we might not have a codec for the needed character encoding;
- // or the name might contain characters that cannot be expressed
- // in the old-style Macintosh encoding. In case of failure, we emit
- // the name in a more modern fashion (Unicode encoding with BCP 47
- // language tags) that is recognized by MacOS 10.5, released in 2009.
- // If fonts were only read by operating systems, we could simply
- // emit all names in the modern form; this would be much easier.
- // However, there are many applications and libraries that read
- // 'name' tables directly, and these will usually only recognize
- // the ancient form (silently skipping the unrecognized names).
- var macPlatform = 1; // Macintosh
- var macLanguage = macLanguageIds[lang];
- var macScript = macLanguageToScript[macLanguage];
- var macEncoding = getEncoding(macPlatform, macScript, macLanguage);
- var macName = encode.MACSTRING(text, macEncoding);
- if (macName === undefined) {
- macPlatform = 0; // Unicode
- macLanguage = ltag.indexOf(lang);
- if (macLanguage < 0) {
- macLanguage = ltag.length;
- ltag.push(lang);
- }
-
- macScript = 4; // Unicode 2.0 and later
- macName = encode.UTF16(text);
- }
-
- var macNameOffset = addStringToPool(macName, stringPool);
- nameRecords.push(makeNameRecord(macPlatform, macScript, macLanguage,
- nameID, macName.length, macNameOffset));
-
- var winLanguage = windowsLanguageIds[lang];
- if (winLanguage !== undefined) {
- var winName = encode.UTF16(text);
- var winNameOffset = addStringToPool(winName, stringPool);
- nameRecords.push(makeNameRecord(3, 1, winLanguage,
- nameID, winName.length, winNameOffset));
- }
- }
- }
-
- nameRecords.sort(function(a, b) {
- return ((a.platformID - b.platformID) ||
- (a.encodingID - b.encodingID) ||
- (a.languageID - b.languageID) ||
- (a.nameID - b.nameID));
- });
-
- var t = new table.Table('name', [
- {name: 'format', type: 'USHORT', value: 0},
- {name: 'count', type: 'USHORT', value: nameRecords.length},
- {name: 'stringOffset', type: 'USHORT', value: 6 + nameRecords.length * 12}
- ]);
-
- for (var r = 0; r < nameRecords.length; r++) {
- t.fields.push({name: 'record_' + r, type: 'RECORD', value: nameRecords[r]});
- }
-
- t.fields.push({name: 'strings', type: 'LITERAL', value: stringPool});
- return t;
-}
-
-var _name = { parse: parseNameTable, make: makeNameTable };
-
-// The `OS/2` table contains metrics required in OpenType fonts.
-
-var unicodeRanges = [
- {begin: 0x0000, end: 0x007F}, // Basic Latin
- {begin: 0x0080, end: 0x00FF}, // Latin-1 Supplement
- {begin: 0x0100, end: 0x017F}, // Latin Extended-A
- {begin: 0x0180, end: 0x024F}, // Latin Extended-B
- {begin: 0x0250, end: 0x02AF}, // IPA Extensions
- {begin: 0x02B0, end: 0x02FF}, // Spacing Modifier Letters
- {begin: 0x0300, end: 0x036F}, // Combining Diacritical Marks
- {begin: 0x0370, end: 0x03FF}, // Greek and Coptic
- {begin: 0x2C80, end: 0x2CFF}, // Coptic
- {begin: 0x0400, end: 0x04FF}, // Cyrillic
- {begin: 0x0530, end: 0x058F}, // Armenian
- {begin: 0x0590, end: 0x05FF}, // Hebrew
- {begin: 0xA500, end: 0xA63F}, // Vai
- {begin: 0x0600, end: 0x06FF}, // Arabic
- {begin: 0x07C0, end: 0x07FF}, // NKo
- {begin: 0x0900, end: 0x097F}, // Devanagari
- {begin: 0x0980, end: 0x09FF}, // Bengali
- {begin: 0x0A00, end: 0x0A7F}, // Gurmukhi
- {begin: 0x0A80, end: 0x0AFF}, // Gujarati
- {begin: 0x0B00, end: 0x0B7F}, // Oriya
- {begin: 0x0B80, end: 0x0BFF}, // Tamil
- {begin: 0x0C00, end: 0x0C7F}, // Telugu
- {begin: 0x0C80, end: 0x0CFF}, // Kannada
- {begin: 0x0D00, end: 0x0D7F}, // Malayalam
- {begin: 0x0E00, end: 0x0E7F}, // Thai
- {begin: 0x0E80, end: 0x0EFF}, // Lao
- {begin: 0x10A0, end: 0x10FF}, // Georgian
- {begin: 0x1B00, end: 0x1B7F}, // Balinese
- {begin: 0x1100, end: 0x11FF}, // Hangul Jamo
- {begin: 0x1E00, end: 0x1EFF}, // Latin Extended Additional
- {begin: 0x1F00, end: 0x1FFF}, // Greek Extended
- {begin: 0x2000, end: 0x206F}, // General Punctuation
- {begin: 0x2070, end: 0x209F}, // Superscripts And Subscripts
- {begin: 0x20A0, end: 0x20CF}, // Currency Symbol
- {begin: 0x20D0, end: 0x20FF}, // Combining Diacritical Marks For Symbols
- {begin: 0x2100, end: 0x214F}, // Letterlike Symbols
- {begin: 0x2150, end: 0x218F}, // Number Forms
- {begin: 0x2190, end: 0x21FF}, // Arrows
- {begin: 0x2200, end: 0x22FF}, // Mathematical Operators
- {begin: 0x2300, end: 0x23FF}, // Miscellaneous Technical
- {begin: 0x2400, end: 0x243F}, // Control Pictures
- {begin: 0x2440, end: 0x245F}, // Optical Character Recognition
- {begin: 0x2460, end: 0x24FF}, // Enclosed Alphanumerics
- {begin: 0x2500, end: 0x257F}, // Box Drawing
- {begin: 0x2580, end: 0x259F}, // Block Elements
- {begin: 0x25A0, end: 0x25FF}, // Geometric Shapes
- {begin: 0x2600, end: 0x26FF}, // Miscellaneous Symbols
- {begin: 0x2700, end: 0x27BF}, // Dingbats
- {begin: 0x3000, end: 0x303F}, // CJK Symbols And Punctuation
- {begin: 0x3040, end: 0x309F}, // Hiragana
- {begin: 0x30A0, end: 0x30FF}, // Katakana
- {begin: 0x3100, end: 0x312F}, // Bopomofo
- {begin: 0x3130, end: 0x318F}, // Hangul Compatibility Jamo
- {begin: 0xA840, end: 0xA87F}, // Phags-pa
- {begin: 0x3200, end: 0x32FF}, // Enclosed CJK Letters And Months
- {begin: 0x3300, end: 0x33FF}, // CJK Compatibility
- {begin: 0xAC00, end: 0xD7AF}, // Hangul Syllables
- {begin: 0xD800, end: 0xDFFF}, // Non-Plane 0 *
- {begin: 0x10900, end: 0x1091F}, // Phoenicia
- {begin: 0x4E00, end: 0x9FFF}, // CJK Unified Ideographs
- {begin: 0xE000, end: 0xF8FF}, // Private Use Area (plane 0)
- {begin: 0x31C0, end: 0x31EF}, // CJK Strokes
- {begin: 0xFB00, end: 0xFB4F}, // Alphabetic Presentation Forms
- {begin: 0xFB50, end: 0xFDFF}, // Arabic Presentation Forms-A
- {begin: 0xFE20, end: 0xFE2F}, // Combining Half Marks
- {begin: 0xFE10, end: 0xFE1F}, // Vertical Forms
- {begin: 0xFE50, end: 0xFE6F}, // Small Form Variants
- {begin: 0xFE70, end: 0xFEFF}, // Arabic Presentation Forms-B
- {begin: 0xFF00, end: 0xFFEF}, // Halfwidth And Fullwidth Forms
- {begin: 0xFFF0, end: 0xFFFF}, // Specials
- {begin: 0x0F00, end: 0x0FFF}, // Tibetan
- {begin: 0x0700, end: 0x074F}, // Syriac
- {begin: 0x0780, end: 0x07BF}, // Thaana
- {begin: 0x0D80, end: 0x0DFF}, // Sinhala
- {begin: 0x1000, end: 0x109F}, // Myanmar
- {begin: 0x1200, end: 0x137F}, // Ethiopic
- {begin: 0x13A0, end: 0x13FF}, // Cherokee
- {begin: 0x1400, end: 0x167F}, // Unified Canadian Aboriginal Syllabics
- {begin: 0x1680, end: 0x169F}, // Ogham
- {begin: 0x16A0, end: 0x16FF}, // Runic
- {begin: 0x1780, end: 0x17FF}, // Khmer
- {begin: 0x1800, end: 0x18AF}, // Mongolian
- {begin: 0x2800, end: 0x28FF}, // Braille Patterns
- {begin: 0xA000, end: 0xA48F}, // Yi Syllables
- {begin: 0x1700, end: 0x171F}, // Tagalog
- {begin: 0x10300, end: 0x1032F}, // Old Italic
- {begin: 0x10330, end: 0x1034F}, // Gothic
- {begin: 0x10400, end: 0x1044F}, // Deseret
- {begin: 0x1D000, end: 0x1D0FF}, // Byzantine Musical Symbols
- {begin: 0x1D400, end: 0x1D7FF}, // Mathematical Alphanumeric Symbols
- {begin: 0xFF000, end: 0xFFFFD}, // Private Use (plane 15)
- {begin: 0xFE00, end: 0xFE0F}, // Variation Selectors
- {begin: 0xE0000, end: 0xE007F}, // Tags
- {begin: 0x1900, end: 0x194F}, // Limbu
- {begin: 0x1950, end: 0x197F}, // Tai Le
- {begin: 0x1980, end: 0x19DF}, // New Tai Lue
- {begin: 0x1A00, end: 0x1A1F}, // Buginese
- {begin: 0x2C00, end: 0x2C5F}, // Glagolitic
- {begin: 0x2D30, end: 0x2D7F}, // Tifinagh
- {begin: 0x4DC0, end: 0x4DFF}, // Yijing Hexagram Symbols
- {begin: 0xA800, end: 0xA82F}, // Syloti Nagri
- {begin: 0x10000, end: 0x1007F}, // Linear B Syllabary
- {begin: 0x10140, end: 0x1018F}, // Ancient Greek Numbers
- {begin: 0x10380, end: 0x1039F}, // Ugaritic
- {begin: 0x103A0, end: 0x103DF}, // Old Persian
- {begin: 0x10450, end: 0x1047F}, // Shavian
- {begin: 0x10480, end: 0x104AF}, // Osmanya
- {begin: 0x10800, end: 0x1083F}, // Cypriot Syllabary
- {begin: 0x10A00, end: 0x10A5F}, // Kharoshthi
- {begin: 0x1D300, end: 0x1D35F}, // Tai Xuan Jing Symbols
- {begin: 0x12000, end: 0x123FF}, // Cuneiform
- {begin: 0x1D360, end: 0x1D37F}, // Counting Rod Numerals
- {begin: 0x1B80, end: 0x1BBF}, // Sundanese
- {begin: 0x1C00, end: 0x1C4F}, // Lepcha
- {begin: 0x1C50, end: 0x1C7F}, // Ol Chiki
- {begin: 0xA880, end: 0xA8DF}, // Saurashtra
- {begin: 0xA900, end: 0xA92F}, // Kayah Li
- {begin: 0xA930, end: 0xA95F}, // Rejang
- {begin: 0xAA00, end: 0xAA5F}, // Cham
- {begin: 0x10190, end: 0x101CF}, // Ancient Symbols
- {begin: 0x101D0, end: 0x101FF}, // Phaistos Disc
- {begin: 0x102A0, end: 0x102DF}, // Carian
- {begin: 0x1F030, end: 0x1F09F} // Domino Tiles
-];
-
-function getUnicodeRange(unicode) {
- for (var i = 0; i < unicodeRanges.length; i += 1) {
- var range = unicodeRanges[i];
- if (unicode >= range.begin && unicode < range.end) {
- return i;
- }
- }
-
- return -1;
-}
-
-// Parse the OS/2 and Windows metrics `OS/2` table
-function parseOS2Table(data, start) {
- var os2 = {};
- var p = new parse.Parser(data, start);
- os2.version = p.parseUShort();
- os2.xAvgCharWidth = p.parseShort();
- os2.usWeightClass = p.parseUShort();
- os2.usWidthClass = p.parseUShort();
- os2.fsType = p.parseUShort();
- os2.ySubscriptXSize = p.parseShort();
- os2.ySubscriptYSize = p.parseShort();
- os2.ySubscriptXOffset = p.parseShort();
- os2.ySubscriptYOffset = p.parseShort();
- os2.ySuperscriptXSize = p.parseShort();
- os2.ySuperscriptYSize = p.parseShort();
- os2.ySuperscriptXOffset = p.parseShort();
- os2.ySuperscriptYOffset = p.parseShort();
- os2.yStrikeoutSize = p.parseShort();
- os2.yStrikeoutPosition = p.parseShort();
- os2.sFamilyClass = p.parseShort();
- os2.panose = [];
- for (var i = 0; i < 10; i++) {
- os2.panose[i] = p.parseByte();
- }
-
- os2.ulUnicodeRange1 = p.parseULong();
- os2.ulUnicodeRange2 = p.parseULong();
- os2.ulUnicodeRange3 = p.parseULong();
- os2.ulUnicodeRange4 = p.parseULong();
- os2.achVendID = String.fromCharCode(p.parseByte(), p.parseByte(), p.parseByte(), p.parseByte());
- os2.fsSelection = p.parseUShort();
- os2.usFirstCharIndex = p.parseUShort();
- os2.usLastCharIndex = p.parseUShort();
- os2.sTypoAscender = p.parseShort();
- os2.sTypoDescender = p.parseShort();
- os2.sTypoLineGap = p.parseShort();
- os2.usWinAscent = p.parseUShort();
- os2.usWinDescent = p.parseUShort();
- if (os2.version >= 1) {
- os2.ulCodePageRange1 = p.parseULong();
- os2.ulCodePageRange2 = p.parseULong();
- }
-
- if (os2.version >= 2) {
- os2.sxHeight = p.parseShort();
- os2.sCapHeight = p.parseShort();
- os2.usDefaultChar = p.parseUShort();
- os2.usBreakChar = p.parseUShort();
- os2.usMaxContent = p.parseUShort();
- }
-
- return os2;
-}
-
-function makeOS2Table(options) {
- return new table.Table('OS/2', [
- {name: 'version', type: 'USHORT', value: 0x0003},
- {name: 'xAvgCharWidth', type: 'SHORT', value: 0},
- {name: 'usWeightClass', type: 'USHORT', value: 0},
- {name: 'usWidthClass', type: 'USHORT', value: 0},
- {name: 'fsType', type: 'USHORT', value: 0},
- {name: 'ySubscriptXSize', type: 'SHORT', value: 650},
- {name: 'ySubscriptYSize', type: 'SHORT', value: 699},
- {name: 'ySubscriptXOffset', type: 'SHORT', value: 0},
- {name: 'ySubscriptYOffset', type: 'SHORT', value: 140},
- {name: 'ySuperscriptXSize', type: 'SHORT', value: 650},
- {name: 'ySuperscriptYSize', type: 'SHORT', value: 699},
- {name: 'ySuperscriptXOffset', type: 'SHORT', value: 0},
- {name: 'ySuperscriptYOffset', type: 'SHORT', value: 479},
- {name: 'yStrikeoutSize', type: 'SHORT', value: 49},
- {name: 'yStrikeoutPosition', type: 'SHORT', value: 258},
- {name: 'sFamilyClass', type: 'SHORT', value: 0},
- {name: 'bFamilyType', type: 'BYTE', value: 0},
- {name: 'bSerifStyle', type: 'BYTE', value: 0},
- {name: 'bWeight', type: 'BYTE', value: 0},
- {name: 'bProportion', type: 'BYTE', value: 0},
- {name: 'bContrast', type: 'BYTE', value: 0},
- {name: 'bStrokeVariation', type: 'BYTE', value: 0},
- {name: 'bArmStyle', type: 'BYTE', value: 0},
- {name: 'bLetterform', type: 'BYTE', value: 0},
- {name: 'bMidline', type: 'BYTE', value: 0},
- {name: 'bXHeight', type: 'BYTE', value: 0},
- {name: 'ulUnicodeRange1', type: 'ULONG', value: 0},
- {name: 'ulUnicodeRange2', type: 'ULONG', value: 0},
- {name: 'ulUnicodeRange3', type: 'ULONG', value: 0},
- {name: 'ulUnicodeRange4', type: 'ULONG', value: 0},
- {name: 'achVendID', type: 'CHARARRAY', value: 'XXXX'},
- {name: 'fsSelection', type: 'USHORT', value: 0},
- {name: 'usFirstCharIndex', type: 'USHORT', value: 0},
- {name: 'usLastCharIndex', type: 'USHORT', value: 0},
- {name: 'sTypoAscender', type: 'SHORT', value: 0},
- {name: 'sTypoDescender', type: 'SHORT', value: 0},
- {name: 'sTypoLineGap', type: 'SHORT', value: 0},
- {name: 'usWinAscent', type: 'USHORT', value: 0},
- {name: 'usWinDescent', type: 'USHORT', value: 0},
- {name: 'ulCodePageRange1', type: 'ULONG', value: 0},
- {name: 'ulCodePageRange2', type: 'ULONG', value: 0},
- {name: 'sxHeight', type: 'SHORT', value: 0},
- {name: 'sCapHeight', type: 'SHORT', value: 0},
- {name: 'usDefaultChar', type: 'USHORT', value: 0},
- {name: 'usBreakChar', type: 'USHORT', value: 0},
- {name: 'usMaxContext', type: 'USHORT', value: 0}
- ], options);
-}
-
-var os2 = { parse: parseOS2Table, make: makeOS2Table, unicodeRanges: unicodeRanges, getUnicodeRange: getUnicodeRange };
-
-// The `post` table stores additional PostScript information, such as glyph names.
-
-// Parse the PostScript `post` table
-function parsePostTable(data, start) {
- var post = {};
- var p = new parse.Parser(data, start);
- post.version = p.parseVersion();
- post.italicAngle = p.parseFixed();
- post.underlinePosition = p.parseShort();
- post.underlineThickness = p.parseShort();
- post.isFixedPitch = p.parseULong();
- post.minMemType42 = p.parseULong();
- post.maxMemType42 = p.parseULong();
- post.minMemType1 = p.parseULong();
- post.maxMemType1 = p.parseULong();
- switch (post.version) {
- case 1:
- post.names = standardNames.slice();
- break;
- case 2:
- post.numberOfGlyphs = p.parseUShort();
- post.glyphNameIndex = new Array(post.numberOfGlyphs);
- for (var i = 0; i < post.numberOfGlyphs; i++) {
- post.glyphNameIndex[i] = p.parseUShort();
- }
-
- post.names = [];
- for (var i$1 = 0; i$1 < post.numberOfGlyphs; i$1++) {
- if (post.glyphNameIndex[i$1] >= standardNames.length) {
- var nameLength = p.parseChar();
- post.names.push(p.parseString(nameLength));
- }
- }
-
- break;
- case 2.5:
- post.numberOfGlyphs = p.parseUShort();
- post.offset = new Array(post.numberOfGlyphs);
- for (var i$2 = 0; i$2 < post.numberOfGlyphs; i$2++) {
- post.offset[i$2] = p.parseChar();
- }
-
- break;
- }
- return post;
-}
-
-function makePostTable() {
- return new table.Table('post', [
- {name: 'version', type: 'FIXED', value: 0x00030000},
- {name: 'italicAngle', type: 'FIXED', value: 0},
- {name: 'underlinePosition', type: 'FWORD', value: 0},
- {name: 'underlineThickness', type: 'FWORD', value: 0},
- {name: 'isFixedPitch', type: 'ULONG', value: 0},
- {name: 'minMemType42', type: 'ULONG', value: 0},
- {name: 'maxMemType42', type: 'ULONG', value: 0},
- {name: 'minMemType1', type: 'ULONG', value: 0},
- {name: 'maxMemType1', type: 'ULONG', value: 0}
- ]);
-}
-
-var post = { parse: parsePostTable, make: makePostTable };
-
-// The `GSUB` table contains ligatures, among other things.
-
-var subtableParsers = new Array(9); // subtableParsers[0] is unused
-
-// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#SS
-subtableParsers[1] = function parseLookup1() {
- var start = this.offset + this.relativeOffset;
- var substFormat = this.parseUShort();
- if (substFormat === 1) {
- return {
- substFormat: 1,
- coverage: this.parsePointer(Parser.coverage),
- deltaGlyphId: this.parseUShort()
- };
- } else if (substFormat === 2) {
- return {
- substFormat: 2,
- coverage: this.parsePointer(Parser.coverage),
- substitute: this.parseOffset16List()
- };
- }
- check.assert(false, '0x' + start.toString(16) + ': lookup type 1 format must be 1 or 2.');
-};
-
-// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#MS
-subtableParsers[2] = function parseLookup2() {
- var substFormat = this.parseUShort();
- check.argument(substFormat === 1, 'GSUB Multiple Substitution Subtable identifier-format must be 1');
- return {
- substFormat: substFormat,
- coverage: this.parsePointer(Parser.coverage),
- sequences: this.parseListOfLists()
- };
-};
-
-// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#AS
-subtableParsers[3] = function parseLookup3() {
- var substFormat = this.parseUShort();
- check.argument(substFormat === 1, 'GSUB Alternate Substitution Subtable identifier-format must be 1');
- return {
- substFormat: substFormat,
- coverage: this.parsePointer(Parser.coverage),
- alternateSets: this.parseListOfLists()
- };
-};
-
-// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#LS
-subtableParsers[4] = function parseLookup4() {
- var substFormat = this.parseUShort();
- check.argument(substFormat === 1, 'GSUB ligature table identifier-format must be 1');
- return {
- substFormat: substFormat,
- coverage: this.parsePointer(Parser.coverage),
- ligatureSets: this.parseListOfLists(function() {
- return {
- ligGlyph: this.parseUShort(),
- components: this.parseUShortList(this.parseUShort() - 1)
- };
- })
- };
-};
-
-var lookupRecordDesc = {
- sequenceIndex: Parser.uShort,
- lookupListIndex: Parser.uShort
-};
-
-// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#CSF
-subtableParsers[5] = function parseLookup5() {
- var start = this.offset + this.relativeOffset;
- var substFormat = this.parseUShort();
-
- if (substFormat === 1) {
- return {
- substFormat: substFormat,
- coverage: this.parsePointer(Parser.coverage),
- ruleSets: this.parseListOfLists(function() {
- var glyphCount = this.parseUShort();
- var substCount = this.parseUShort();
- return {
- input: this.parseUShortList(glyphCount - 1),
- lookupRecords: this.parseRecordList(substCount, lookupRecordDesc)
- };
- })
- };
- } else if (substFormat === 2) {
- return {
- substFormat: substFormat,
- coverage: this.parsePointer(Parser.coverage),
- classDef: this.parsePointer(Parser.classDef),
- classSets: this.parseListOfLists(function() {
- var glyphCount = this.parseUShort();
- var substCount = this.parseUShort();
- return {
- classes: this.parseUShortList(glyphCount - 1),
- lookupRecords: this.parseRecordList(substCount, lookupRecordDesc)
- };
- })
- };
- } else if (substFormat === 3) {
- var glyphCount = this.parseUShort();
- var substCount = this.parseUShort();
- return {
- substFormat: substFormat,
- coverages: this.parseList(glyphCount, Parser.pointer(Parser.coverage)),
- lookupRecords: this.parseRecordList(substCount, lookupRecordDesc)
- };
- }
- check.assert(false, '0x' + start.toString(16) + ': lookup type 5 format must be 1, 2 or 3.');
-};
-
-// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#CC
-subtableParsers[6] = function parseLookup6() {
- var start = this.offset + this.relativeOffset;
- var substFormat = this.parseUShort();
- if (substFormat === 1) {
- return {
- substFormat: 1,
- coverage: this.parsePointer(Parser.coverage),
- chainRuleSets: this.parseListOfLists(function() {
- return {
- backtrack: this.parseUShortList(),
- input: this.parseUShortList(this.parseShort() - 1),
- lookahead: this.parseUShortList(),
- lookupRecords: this.parseRecordList(lookupRecordDesc)
- };
- })
- };
- } else if (substFormat === 2) {
- return {
- substFormat: 2,
- coverage: this.parsePointer(Parser.coverage),
- backtrackClassDef: this.parsePointer(Parser.classDef),
- inputClassDef: this.parsePointer(Parser.classDef),
- lookaheadClassDef: this.parsePointer(Parser.classDef),
- chainClassSet: this.parseListOfLists(function() {
- return {
- backtrack: this.parseUShortList(),
- input: this.parseUShortList(this.parseShort() - 1),
- lookahead: this.parseUShortList(),
- lookupRecords: this.parseRecordList(lookupRecordDesc)
- };
- })
- };
- } else if (substFormat === 3) {
- return {
- substFormat: 3,
- backtrackCoverage: this.parseList(Parser.pointer(Parser.coverage)),
- inputCoverage: this.parseList(Parser.pointer(Parser.coverage)),
- lookaheadCoverage: this.parseList(Parser.pointer(Parser.coverage)),
- lookupRecords: this.parseRecordList(lookupRecordDesc)
- };
- }
- check.assert(false, '0x' + start.toString(16) + ': lookup type 6 format must be 1, 2 or 3.');
-};
-
-// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#ES
-subtableParsers[7] = function parseLookup7() {
- // Extension Substitution subtable
- var substFormat = this.parseUShort();
- check.argument(substFormat === 1, 'GSUB Extension Substitution subtable identifier-format must be 1');
- var extensionLookupType = this.parseUShort();
- var extensionParser = new Parser(this.data, this.offset + this.parseULong());
- return {
- substFormat: 1,
- lookupType: extensionLookupType,
- extension: subtableParsers[extensionLookupType].call(extensionParser)
- };
-};
-
-// https://www.microsoft.com/typography/OTSPEC/GSUB.htm#RCCS
-subtableParsers[8] = function parseLookup8() {
- var substFormat = this.parseUShort();
- check.argument(substFormat === 1, 'GSUB Reverse Chaining Contextual Single Substitution Subtable identifier-format must be 1');
- return {
- substFormat: substFormat,
- coverage: this.parsePointer(Parser.coverage),
- backtrackCoverage: this.parseList(Parser.pointer(Parser.coverage)),
- lookaheadCoverage: this.parseList(Parser.pointer(Parser.coverage)),
- substitutes: this.parseUShortList()
- };
-};
-
-// https://www.microsoft.com/typography/OTSPEC/gsub.htm
-function parseGsubTable(data, start) {
- start = start || 0;
- var p = new Parser(data, start);
- var tableVersion = p.parseVersion(1);
- check.argument(tableVersion === 1 || tableVersion === 1.1, 'Unsupported GSUB table version.');
- if (tableVersion === 1) {
- return {
- version: tableVersion,
- scripts: p.parseScriptList(),
- features: p.parseFeatureList(),
- lookups: p.parseLookupList(subtableParsers)
- };
- } else {
- return {
- version: tableVersion,
- scripts: p.parseScriptList(),
- features: p.parseFeatureList(),
- lookups: p.parseLookupList(subtableParsers),
- variations: p.parseFeatureVariationsList()
- };
- }
-
-}
-
-// GSUB Writing //////////////////////////////////////////////
-var subtableMakers = new Array(9);
-
-subtableMakers[1] = function makeLookup1(subtable) {
- if (subtable.substFormat === 1) {
- return new table.Table('substitutionTable', [
- {name: 'substFormat', type: 'USHORT', value: 1},
- {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)},
- {name: 'deltaGlyphID', type: 'USHORT', value: subtable.deltaGlyphId}
- ]);
- } else {
- return new table.Table('substitutionTable', [
- {name: 'substFormat', type: 'USHORT', value: 2},
- {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
- ].concat(table.ushortList('substitute', subtable.substitute)));
- }
-};
-
-subtableMakers[2] = function makeLookup2(subtable) {
- check.assert(subtable.substFormat === 1, 'Lookup type 2 substFormat must be 1.');
- return new table.Table('substitutionTable', [
- {name: 'substFormat', type: 'USHORT', value: 1},
- {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
- ].concat(table.tableList('seqSet', subtable.sequences, function(sequenceSet) {
- return new table.Table('sequenceSetTable', table.ushortList('sequence', sequenceSet));
- })));
-};
-
-subtableMakers[3] = function makeLookup3(subtable) {
- check.assert(subtable.substFormat === 1, 'Lookup type 3 substFormat must be 1.');
- return new table.Table('substitutionTable', [
- {name: 'substFormat', type: 'USHORT', value: 1},
- {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
- ].concat(table.tableList('altSet', subtable.alternateSets, function(alternateSet) {
- return new table.Table('alternateSetTable', table.ushortList('alternate', alternateSet));
- })));
-};
-
-subtableMakers[4] = function makeLookup4(subtable) {
- check.assert(subtable.substFormat === 1, 'Lookup type 4 substFormat must be 1.');
- return new table.Table('substitutionTable', [
- {name: 'substFormat', type: 'USHORT', value: 1},
- {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
- ].concat(table.tableList('ligSet', subtable.ligatureSets, function(ligatureSet) {
- return new table.Table('ligatureSetTable', table.tableList('ligature', ligatureSet, function(ligature) {
- return new table.Table('ligatureTable',
- [{name: 'ligGlyph', type: 'USHORT', value: ligature.ligGlyph}]
- .concat(table.ushortList('component', ligature.components, ligature.components.length + 1))
- );
- }));
- })));
-};
-
-subtableMakers[6] = function makeLookup6(subtable) {
- if (subtable.substFormat === 1) {
- var returnTable = new table.Table('chainContextTable', [
- {name: 'substFormat', type: 'USHORT', value: subtable.substFormat},
- {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}
- ].concat(table.tableList('chainRuleSet', subtable.chainRuleSets, function(chainRuleSet) {
- return new table.Table('chainRuleSetTable', table.tableList('chainRule', chainRuleSet, function(chainRule) {
- var tableData = table.ushortList('backtrackGlyph', chainRule.backtrack, chainRule.backtrack.length)
- .concat(table.ushortList('inputGlyph', chainRule.input, chainRule.input.length + 1))
- .concat(table.ushortList('lookaheadGlyph', chainRule.lookahead, chainRule.lookahead.length))
- .concat(table.ushortList('substitution', [], chainRule.lookupRecords.length));
-
- chainRule.lookupRecords.forEach(function (record, i) {
- tableData = tableData
- .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex})
- .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex});
- });
- return new table.Table('chainRuleTable', tableData);
- }));
- })));
- return returnTable;
- } else if (subtable.substFormat === 2) {
- check.assert(false, 'lookup type 6 format 2 is not yet supported.');
- } else if (subtable.substFormat === 3) {
- var tableData = [
- {name: 'substFormat', type: 'USHORT', value: subtable.substFormat} ];
-
- tableData.push({name: 'backtrackGlyphCount', type: 'USHORT', value: subtable.backtrackCoverage.length});
- subtable.backtrackCoverage.forEach(function (coverage, i) {
- tableData.push({name: 'backtrackCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)});
- });
- tableData.push({name: 'inputGlyphCount', type: 'USHORT', value: subtable.inputCoverage.length});
- subtable.inputCoverage.forEach(function (coverage, i) {
- tableData.push({name: 'inputCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)});
- });
- tableData.push({name: 'lookaheadGlyphCount', type: 'USHORT', value: subtable.lookaheadCoverage.length});
- subtable.lookaheadCoverage.forEach(function (coverage, i) {
- tableData.push({name: 'lookaheadCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)});
- });
-
- tableData.push({name: 'substitutionCount', type: 'USHORT', value: subtable.lookupRecords.length});
- subtable.lookupRecords.forEach(function (record, i) {
- tableData = tableData
- .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex})
- .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex});
- });
-
- var returnTable$1 = new table.Table('chainContextTable', tableData);
-
- return returnTable$1;
- }
-
- check.assert(false, 'lookup type 6 format must be 1, 2 or 3.');
-};
-
-function makeGsubTable(gsub) {
- return new table.Table('GSUB', [
- {name: 'version', type: 'ULONG', value: 0x10000},
- {name: 'scripts', type: 'TABLE', value: new table.ScriptList(gsub.scripts)},
- {name: 'features', type: 'TABLE', value: new table.FeatureList(gsub.features)},
- {name: 'lookups', type: 'TABLE', value: new table.LookupList(gsub.lookups, subtableMakers)}
- ]);
-}
-
-var gsub = { parse: parseGsubTable, make: makeGsubTable };
-
-// The `GPOS` table contains kerning pairs, among other things.
-
-// Parse the metadata `meta` table.
-// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6meta.html
-function parseMetaTable(data, start) {
- var p = new parse.Parser(data, start);
- var tableVersion = p.parseULong();
- check.argument(tableVersion === 1, 'Unsupported META table version.');
- p.parseULong(); // flags - currently unused and set to 0
- p.parseULong(); // tableOffset
- var numDataMaps = p.parseULong();
-
- var tags = {};
- for (var i = 0; i < numDataMaps; i++) {
- var tag = p.parseTag();
- var dataOffset = p.parseULong();
- var dataLength = p.parseULong();
- var text = decode.UTF8(data, start + dataOffset, dataLength);
-
- tags[tag] = text;
- }
- return tags;
-}
-
-function makeMetaTable(tags) {
- var numTags = Object.keys(tags).length;
- var stringPool = '';
- var stringPoolOffset = 16 + numTags * 12;
-
- var result = new table.Table('meta', [
- {name: 'version', type: 'ULONG', value: 1},
- {name: 'flags', type: 'ULONG', value: 0},
- {name: 'offset', type: 'ULONG', value: stringPoolOffset},
- {name: 'numTags', type: 'ULONG', value: numTags}
- ]);
-
- for (var tag in tags) {
- var pos = stringPool.length;
- stringPool += tags[tag];
-
- result.fields.push({name: 'tag ' + tag, type: 'TAG', value: tag});
- result.fields.push({name: 'offset ' + tag, type: 'ULONG', value: stringPoolOffset + pos});
- result.fields.push({name: 'length ' + tag, type: 'ULONG', value: tags[tag].length});
- }
-
- result.fields.push({name: 'stringPool', type: 'CHARARRAY', value: stringPool});
-
- return result;
-}
-
-var meta = { parse: parseMetaTable, make: makeMetaTable };
-
-// The `sfnt` wrapper provides organization for the tables in the font.
-
-function log2(v) {
- return Math.log(v) / Math.log(2) | 0;
-}
-
-function computeCheckSum(bytes) {
- while (bytes.length % 4 !== 0) {
- bytes.push(0);
- }
-
- var sum = 0;
- for (var i = 0; i < bytes.length; i += 4) {
- sum += (bytes[i] << 24) +
- (bytes[i + 1] << 16) +
- (bytes[i + 2] << 8) +
- (bytes[i + 3]);
- }
-
- sum %= Math.pow(2, 32);
- return sum;
-}
-
-function makeTableRecord(tag, checkSum, offset, length) {
- return new table.Record('Table Record', [
- {name: 'tag', type: 'TAG', value: tag !== undefined ? tag : ''},
- {name: 'checkSum', type: 'ULONG', value: checkSum !== undefined ? checkSum : 0},
- {name: 'offset', type: 'ULONG', value: offset !== undefined ? offset : 0},
- {name: 'length', type: 'ULONG', value: length !== undefined ? length : 0}
- ]);
-}
-
-function makeSfntTable(tables) {
- var sfnt = new table.Table('sfnt', [
- {name: 'version', type: 'TAG', value: 'OTTO'},
- {name: 'numTables', type: 'USHORT', value: 0},
- {name: 'searchRange', type: 'USHORT', value: 0},
- {name: 'entrySelector', type: 'USHORT', value: 0},
- {name: 'rangeShift', type: 'USHORT', value: 0}
- ]);
- sfnt.tables = tables;
- sfnt.numTables = tables.length;
- var highestPowerOf2 = Math.pow(2, log2(sfnt.numTables));
- sfnt.searchRange = 16 * highestPowerOf2;
- sfnt.entrySelector = log2(highestPowerOf2);
- sfnt.rangeShift = sfnt.numTables * 16 - sfnt.searchRange;
-
- var recordFields = [];
- var tableFields = [];
-
- var offset = sfnt.sizeOf() + (makeTableRecord().sizeOf() * sfnt.numTables);
- while (offset % 4 !== 0) {
- offset += 1;
- tableFields.push({name: 'padding', type: 'BYTE', value: 0});
- }
-
- for (var i = 0; i < tables.length; i += 1) {
- var t = tables[i];
- check.argument(t.tableName.length === 4, 'Table name' + t.tableName + ' is invalid.');
- var tableLength = t.sizeOf();
- var tableRecord = makeTableRecord(t.tableName, computeCheckSum(t.encode()), offset, tableLength);
- recordFields.push({name: tableRecord.tag + ' Table Record', type: 'RECORD', value: tableRecord});
- tableFields.push({name: t.tableName + ' table', type: 'RECORD', value: t});
- offset += tableLength;
- check.argument(!isNaN(offset), 'Something went wrong calculating the offset.');
- while (offset % 4 !== 0) {
- offset += 1;
- tableFields.push({name: 'padding', type: 'BYTE', value: 0});
- }
- }
-
- // Table records need to be sorted alphabetically.
- recordFields.sort(function(r1, r2) {
- if (r1.value.tag > r2.value.tag) {
- return 1;
- } else {
- return -1;
- }
- });
-
- sfnt.fields = sfnt.fields.concat(recordFields);
- sfnt.fields = sfnt.fields.concat(tableFields);
- return sfnt;
-}
-
-// Get the metrics for a character. If the string has more than one character
-// this function returns metrics for the first available character.
-// You can provide optional fallback metrics if no characters are available.
-function metricsForChar(font, chars, notFoundMetrics) {
- for (var i = 0; i < chars.length; i += 1) {
- var glyphIndex = font.charToGlyphIndex(chars[i]);
- if (glyphIndex > 0) {
- var glyph = font.glyphs.get(glyphIndex);
- return glyph.getMetrics();
- }
- }
-
- return notFoundMetrics;
-}
-
-function average(vs) {
- var sum = 0;
- for (var i = 0; i < vs.length; i += 1) {
- sum += vs[i];
- }
-
- return sum / vs.length;
-}
-
-// Convert the font object to a SFNT data structure.
-// This structure contains all the necessary tables and metadata to create a binary OTF file.
-function fontToSfntTable(font) {
- var xMins = [];
- var yMins = [];
- var xMaxs = [];
- var yMaxs = [];
- var advanceWidths = [];
- var leftSideBearings = [];
- var rightSideBearings = [];
- var firstCharIndex;
- var lastCharIndex = 0;
- var ulUnicodeRange1 = 0;
- var ulUnicodeRange2 = 0;
- var ulUnicodeRange3 = 0;
- var ulUnicodeRange4 = 0;
-
- for (var i = 0; i < font.glyphs.length; i += 1) {
- var glyph = font.glyphs.get(i);
- var unicode = glyph.unicode | 0;
-
- if (isNaN(glyph.advanceWidth)) {
- throw new Error('Glyph ' + glyph.name + ' (' + i + '): advanceWidth is not a number.');
- }
-
- if (firstCharIndex > unicode || firstCharIndex === undefined) {
- // ignore .notdef char
- if (unicode > 0) {
- firstCharIndex = unicode;
- }
- }
-
- if (lastCharIndex < unicode) {
- lastCharIndex = unicode;
- }
-
- var position = os2.getUnicodeRange(unicode);
- if (position < 32) {
- ulUnicodeRange1 |= 1 << position;
- } else if (position < 64) {
- ulUnicodeRange2 |= 1 << position - 32;
- } else if (position < 96) {
- ulUnicodeRange3 |= 1 << position - 64;
- } else if (position < 123) {
- ulUnicodeRange4 |= 1 << position - 96;
- } else {
- throw new Error('Unicode ranges bits > 123 are reserved for internal usage');
- }
- // Skip non-important characters.
- if (glyph.name === '.notdef') { continue; }
- var metrics = glyph.getMetrics();
- xMins.push(metrics.xMin);
- yMins.push(metrics.yMin);
- xMaxs.push(metrics.xMax);
- yMaxs.push(metrics.yMax);
- leftSideBearings.push(metrics.leftSideBearing);
- rightSideBearings.push(metrics.rightSideBearing);
- advanceWidths.push(glyph.advanceWidth);
- }
-
- var globals = {
- xMin: Math.min.apply(null, xMins),
- yMin: Math.min.apply(null, yMins),
- xMax: Math.max.apply(null, xMaxs),
- yMax: Math.max.apply(null, yMaxs),
- advanceWidthMax: Math.max.apply(null, advanceWidths),
- advanceWidthAvg: average(advanceWidths),
- minLeftSideBearing: Math.min.apply(null, leftSideBearings),
- maxLeftSideBearing: Math.max.apply(null, leftSideBearings),
- minRightSideBearing: Math.min.apply(null, rightSideBearings)
- };
- globals.ascender = font.ascender;
- globals.descender = font.descender;
-
- var headTable = head.make({
- flags: 3, // 00000011 (baseline for font at y=0; left sidebearing point at x=0)
- unitsPerEm: font.unitsPerEm,
- xMin: globals.xMin,
- yMin: globals.yMin,
- xMax: globals.xMax,
- yMax: globals.yMax,
- lowestRecPPEM: 3,
- createdTimestamp: font.createdTimestamp
- });
-
- var hheaTable = hhea.make({
- ascender: globals.ascender,
- descender: globals.descender,
- advanceWidthMax: globals.advanceWidthMax,
- minLeftSideBearing: globals.minLeftSideBearing,
- minRightSideBearing: globals.minRightSideBearing,
- xMaxExtent: globals.maxLeftSideBearing + (globals.xMax - globals.xMin),
- numberOfHMetrics: font.glyphs.length
- });
-
- var maxpTable = maxp.make(font.glyphs.length);
-
- var os2Table = os2.make(Object.assign({
- xAvgCharWidth: Math.round(globals.advanceWidthAvg),
- usFirstCharIndex: firstCharIndex,
- usLastCharIndex: lastCharIndex,
- ulUnicodeRange1: ulUnicodeRange1,
- ulUnicodeRange2: ulUnicodeRange2,
- ulUnicodeRange3: ulUnicodeRange3,
- ulUnicodeRange4: ulUnicodeRange4,
- // See http://typophile.com/node/13081 for more info on vertical metrics.
- // We get metrics for typical characters (such as "x" for xHeight).
- // We provide some fallback characters if characters are unavailable: their
- // ordering was chosen experimentally.
- sTypoAscender: globals.ascender,
- sTypoDescender: globals.descender,
- sTypoLineGap: 0,
- usWinAscent: globals.yMax,
- usWinDescent: Math.abs(globals.yMin),
- ulCodePageRange1: 1, // FIXME: hard-code Latin 1 support for now
- sxHeight: metricsForChar(font, 'xyvw', {yMax: Math.round(globals.ascender / 2)}).yMax,
- sCapHeight: metricsForChar(font, 'HIKLEFJMNTZBDPRAGOQSUVWXY', globals).yMax,
- usDefaultChar: font.hasChar(' ') ? 32 : 0, // Use space as the default character, if available.
- usBreakChar: font.hasChar(' ') ? 32 : 0, // Use space as the break character, if available.
- }, font.tables.os2));
-
- var hmtxTable = hmtx.make(font.glyphs);
- var cmapTable = cmap.make(font.glyphs);
-
- var englishFamilyName = font.getEnglishName('fontFamily');
- var englishStyleName = font.getEnglishName('fontSubfamily');
- var englishFullName = englishFamilyName + ' ' + englishStyleName;
- var postScriptName = font.getEnglishName('postScriptName');
- if (!postScriptName) {
- postScriptName = englishFamilyName.replace(/\s/g, '') + '-' + englishStyleName;
- }
-
- var names = {};
- for (var n in font.names) {
- names[n] = font.names[n];
- }
-
- if (!names.uniqueID) {
- names.uniqueID = {en: font.getEnglishName('manufacturer') + ':' + englishFullName};
- }
-
- if (!names.postScriptName) {
- names.postScriptName = {en: postScriptName};
- }
-
- if (!names.preferredFamily) {
- names.preferredFamily = font.names.fontFamily;
- }
-
- if (!names.preferredSubfamily) {
- names.preferredSubfamily = font.names.fontSubfamily;
- }
-
- var languageTags = [];
- var nameTable = _name.make(names, languageTags);
- var ltagTable = (languageTags.length > 0 ? ltag.make(languageTags) : undefined);
-
- var postTable = post.make();
- var cffTable = cff.make(font.glyphs, {
- version: font.getEnglishName('version'),
- fullName: englishFullName,
- familyName: englishFamilyName,
- weightName: englishStyleName,
- postScriptName: postScriptName,
- unitsPerEm: font.unitsPerEm,
- fontBBox: [0, globals.yMin, globals.ascender, globals.advanceWidthMax]
- });
-
- var metaTable = (font.metas && Object.keys(font.metas).length > 0) ? meta.make(font.metas) : undefined;
-
- // The order does not matter because makeSfntTable() will sort them.
- var tables = [headTable, hheaTable, maxpTable, os2Table, nameTable, cmapTable, postTable, cffTable, hmtxTable];
- if (ltagTable) {
- tables.push(ltagTable);
- }
- // Optional tables
- if (font.tables.gsub) {
- tables.push(gsub.make(font.tables.gsub));
- }
- if (metaTable) {
- tables.push(metaTable);
- }
-
- var sfntTable = makeSfntTable(tables);
-
- // Compute the font's checkSum and store it in head.checkSumAdjustment.
- var bytes = sfntTable.encode();
- var checkSum = computeCheckSum(bytes);
- var tableFields = sfntTable.fields;
- var checkSumAdjusted = false;
- for (var i$1 = 0; i$1 < tableFields.length; i$1 += 1) {
- if (tableFields[i$1].name === 'head table') {
- tableFields[i$1].value.checkSumAdjustment = 0xB1B0AFBA - checkSum;
- checkSumAdjusted = true;
- break;
- }
- }
-
- if (!checkSumAdjusted) {
- throw new Error('Could not find head table with checkSum to adjust.');
- }
-
- return sfntTable;
-}
-
-var sfnt = { make: makeSfntTable, fontToTable: fontToSfntTable, computeCheckSum: computeCheckSum };
-
-// The Layout object is the prototype of Substitution objects, and provides
-
-function searchTag(arr, tag) {
- /* jshint bitwise: false */
- var imin = 0;
- var imax = arr.length - 1;
- while (imin <= imax) {
- var imid = (imin + imax) >>> 1;
- var val = arr[imid].tag;
- if (val === tag) {
- return imid;
- } else if (val < tag) {
- imin = imid + 1;
- } else { imax = imid - 1; }
- }
- // Not found: return -1-insertion point
- return -imin - 1;
-}
-
-function binSearch(arr, value) {
- /* jshint bitwise: false */
- var imin = 0;
- var imax = arr.length - 1;
- while (imin <= imax) {
- var imid = (imin + imax) >>> 1;
- var val = arr[imid];
- if (val === value) {
- return imid;
- } else if (val < value) {
- imin = imid + 1;
- } else { imax = imid - 1; }
- }
- // Not found: return -1-insertion point
- return -imin - 1;
-}
-
-// binary search in a list of ranges (coverage, class definition)
-function searchRange(ranges, value) {
- // jshint bitwise: false
- var range;
- var imin = 0;
- var imax = ranges.length - 1;
- while (imin <= imax) {
- var imid = (imin + imax) >>> 1;
- range = ranges[imid];
- var start = range.start;
- if (start === value) {
- return range;
- } else if (start < value) {
- imin = imid + 1;
- } else { imax = imid - 1; }
- }
- if (imin > 0) {
- range = ranges[imin - 1];
- if (value > range.end) { return 0; }
- return range;
- }
-}
-
-/**
- * @exports opentype.Layout
- * @class
- */
-function Layout(font, tableName) {
- this.font = font;
- this.tableName = tableName;
-}
-
-Layout.prototype = {
-
- /**
- * Binary search an object by "tag" property
- * @instance
- * @function searchTag
- * @memberof opentype.Layout
- * @param {Array} arr
- * @param {string} tag
- * @return {number}
- */
- searchTag: searchTag,
-
- /**
- * Binary search in a list of numbers
- * @instance
- * @function binSearch
- * @memberof opentype.Layout
- * @param {Array} arr
- * @param {number} value
- * @return {number}
- */
- binSearch: binSearch,
-
- /**
- * Get or create the Layout table (GSUB, GPOS etc).
- * @param {boolean} create - Whether to create a new one.
- * @return {Object} The GSUB or GPOS table.
- */
- getTable: function(create) {
- var layout = this.font.tables[this.tableName];
- if (!layout && create) {
- layout = this.font.tables[this.tableName] = this.createDefaultTable();
- }
- return layout;
- },
-
- /**
- * Returns all scripts in the substitution table.
- * @instance
- * @return {Array}
- */
- getScriptNames: function() {
- var layout = this.getTable();
- if (!layout) { return []; }
- return layout.scripts.map(function(script) {
- return script.tag;
- });
- },
-
- /**
- * Returns the best bet for a script name.
- * Returns 'DFLT' if it exists.
- * If not, returns 'latn' if it exists.
- * If neither exist, returns undefined.
- */
- getDefaultScriptName: function() {
- var layout = this.getTable();
- if (!layout) { return; }
- var hasLatn = false;
- for (var i = 0; i < layout.scripts.length; i++) {
- var name = layout.scripts[i].tag;
- if (name === 'DFLT') { return name; }
- if (name === 'latn') { hasLatn = true; }
- }
- if (hasLatn) { return 'latn'; }
- },
-
- /**
- * Returns all LangSysRecords in the given script.
- * @instance
- * @param {string} [script='DFLT']
- * @param {boolean} create - forces the creation of this script table if it doesn't exist.
- * @return {Object} An object with tag and script properties.
- */
- getScriptTable: function(script, create) {
- var layout = this.getTable(create);
- if (layout) {
- script = script || 'DFLT';
- var scripts = layout.scripts;
- var pos = searchTag(layout.scripts, script);
- if (pos >= 0) {
- return scripts[pos].script;
- } else if (create) {
- var scr = {
- tag: script,
- script: {
- defaultLangSys: {reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: []},
- langSysRecords: []
- }
- };
- scripts.splice(-1 - pos, 0, scr);
- return scr.script;
- }
- }
- },
-
- /**
- * Returns a language system table
- * @instance
- * @param {string} [script='DFLT']
- * @param {string} [language='dlft']
- * @param {boolean} create - forces the creation of this langSysTable if it doesn't exist.
- * @return {Object}
- */
- getLangSysTable: function(script, language, create) {
- var scriptTable = this.getScriptTable(script, create);
- if (scriptTable) {
- if (!language || language === 'dflt' || language === 'DFLT') {
- return scriptTable.defaultLangSys;
- }
- var pos = searchTag(scriptTable.langSysRecords, language);
- if (pos >= 0) {
- return scriptTable.langSysRecords[pos].langSys;
- } else if (create) {
- var langSysRecord = {
- tag: language,
- langSys: {reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: []}
- };
- scriptTable.langSysRecords.splice(-1 - pos, 0, langSysRecord);
- return langSysRecord.langSys;
- }
- }
- },
-
- /**
- * Get a specific feature table.
- * @instance
- * @param {string} [script='DFLT']
- * @param {string} [language='dlft']
- * @param {string} feature - One of the codes listed at https://www.microsoft.com/typography/OTSPEC/featurelist.htm
- * @param {boolean} create - forces the creation of the feature table if it doesn't exist.
- * @return {Object}
- */
- getFeatureTable: function(script, language, feature, create) {
- var langSysTable = this.getLangSysTable(script, language, create);
- if (langSysTable) {
- var featureRecord;
- var featIndexes = langSysTable.featureIndexes;
- var allFeatures = this.font.tables[this.tableName].features;
- // The FeatureIndex array of indices is in arbitrary order,
- // even if allFeatures is sorted alphabetically by feature tag.
- for (var i = 0; i < featIndexes.length; i++) {
- featureRecord = allFeatures[featIndexes[i]];
- if (featureRecord.tag === feature) {
- return featureRecord.feature;
- }
- }
- if (create) {
- var index = allFeatures.length;
- // Automatic ordering of features would require to shift feature indexes in the script list.
- check.assert(index === 0 || feature >= allFeatures[index - 1].tag, 'Features must be added in alphabetical order.');
- featureRecord = {
- tag: feature,
- feature: { params: 0, lookupListIndexes: [] }
- };
- allFeatures.push(featureRecord);
- featIndexes.push(index);
- return featureRecord.feature;
- }
- }
- },
-
- /**
- * Get the lookup tables of a given type for a script/language/feature.
- * @instance
- * @param {string} [script='DFLT']
- * @param {string} [language='dlft']
- * @param {string} feature - 4-letter feature code
- * @param {number} lookupType - 1 to 9
- * @param {boolean} create - forces the creation of the lookup table if it doesn't exist, with no subtables.
- * @return {Object[]}
- */
- getLookupTables: function(script, language, feature, lookupType, create) {
- var featureTable = this.getFeatureTable(script, language, feature, create);
- var tables = [];
- if (featureTable) {
- var lookupTable;
- var lookupListIndexes = featureTable.lookupListIndexes;
- var allLookups = this.font.tables[this.tableName].lookups;
- // lookupListIndexes are in no particular order, so use naive search.
- for (var i = 0; i < lookupListIndexes.length; i++) {
- lookupTable = allLookups[lookupListIndexes[i]];
- if (lookupTable.lookupType === lookupType) {
- tables.push(lookupTable);
- }
- }
- if (tables.length === 0 && create) {
- lookupTable = {
- lookupType: lookupType,
- lookupFlag: 0,
- subtables: [],
- markFilteringSet: undefined
- };
- var index = allLookups.length;
- allLookups.push(lookupTable);
- lookupListIndexes.push(index);
- return [lookupTable];
- }
- }
- return tables;
- },
-
- /**
- * Find a glyph in a class definition table
- * https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table
- * @param {object} classDefTable - an OpenType Layout class definition table
- * @param {number} glyphIndex - the index of the glyph to find
- * @returns {number} -1 if not found
- */
- getGlyphClass: function(classDefTable, glyphIndex) {
- switch (classDefTable.format) {
- case 1:
- if (classDefTable.startGlyph <= glyphIndex && glyphIndex < classDefTable.startGlyph + classDefTable.classes.length) {
- return classDefTable.classes[glyphIndex - classDefTable.startGlyph];
- }
- return 0;
- case 2:
- var range = searchRange(classDefTable.ranges, glyphIndex);
- return range ? range.classId : 0;
- }
- },
-
- /**
- * Find a glyph in a coverage table
- * https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-table
- * @param {object} coverageTable - an OpenType Layout coverage table
- * @param {number} glyphIndex - the index of the glyph to find
- * @returns {number} -1 if not found
- */
- getCoverageIndex: function(coverageTable, glyphIndex) {
- switch (coverageTable.format) {
- case 1:
- var index = binSearch(coverageTable.glyphs, glyphIndex);
- return index >= 0 ? index : -1;
- case 2:
- var range = searchRange(coverageTable.ranges, glyphIndex);
- return range ? range.index + glyphIndex - range.start : -1;
- }
- },
-
- /**
- * Returns the list of glyph indexes of a coverage table.
- * Format 1: the list is stored raw
- * Format 2: compact list as range records.
- * @instance
- * @param {Object} coverageTable
- * @return {Array}
- */
- expandCoverage: function(coverageTable) {
- if (coverageTable.format === 1) {
- return coverageTable.glyphs;
- } else {
- var glyphs = [];
- var ranges = coverageTable.ranges;
- for (var i = 0; i < ranges.length; i++) {
- var range = ranges[i];
- var start = range.start;
- var end = range.end;
- for (var j = start; j <= end; j++) {
- glyphs.push(j);
- }
- }
- return glyphs;
- }
- }
-
-};
-
-// The Position object provides utility methods to manipulate
-
-/**
- * @exports opentype.Position
- * @class
- * @extends opentype.Layout
- * @param {opentype.Font}
- * @constructor
- */
-function Position(font) {
- Layout.call(this, font, 'gpos');
-}
-
-Position.prototype = Layout.prototype;
-
-/**
- * Init some data for faster and easier access later.
- */
-Position.prototype.init = function() {
- var script = this.getDefaultScriptName();
- this.defaultKerningTables = this.getKerningTables(script);
-};
-
-/**
- * Find a glyph pair in a list of lookup tables of type 2 and retrieve the xAdvance kerning value.
- *
- * @param {integer} leftIndex - left glyph index
- * @param {integer} rightIndex - right glyph index
- * @returns {integer}
- */
-Position.prototype.getKerningValue = function(kerningLookups, leftIndex, rightIndex) {
- for (var i = 0; i < kerningLookups.length; i++) {
- var subtables = kerningLookups[i].subtables;
- for (var j = 0; j < subtables.length; j++) {
- var subtable = subtables[j];
- var covIndex = this.getCoverageIndex(subtable.coverage, leftIndex);
- if (covIndex < 0) { continue; }
- switch (subtable.posFormat) {
- case 1:
- // Search Pair Adjustment Positioning Format 1
- var pairSet = subtable.pairSets[covIndex];
- for (var k = 0; k < pairSet.length; k++) {
- var pair = pairSet[k];
- if (pair.secondGlyph === rightIndex) {
- return pair.value1 && pair.value1.xAdvance || 0;
- }
- }
- break; // left glyph found, not right glyph - try next subtable
- case 2:
- // Search Pair Adjustment Positioning Format 2
- var class1 = this.getGlyphClass(subtable.classDef1, leftIndex);
- var class2 = this.getGlyphClass(subtable.classDef2, rightIndex);
- var pair$1 = subtable.classRecords[class1][class2];
- return pair$1.value1 && pair$1.value1.xAdvance || 0;
- }
- }
- }
- return 0;
-};
-
-/**
- * List all kerning lookup tables.
- *
- * @param {string} [script='DFLT'] - use font.position.getDefaultScriptName() for a better default value
- * @param {string} [language='dflt']
- * @return {object[]} The list of kerning lookup tables (may be empty), or undefined if there is no GPOS table (and we should use the kern table)
- */
-Position.prototype.getKerningTables = function(script, language) {
- if (this.font.tables.gpos) {
- return this.getLookupTables(script, language, 'kern', 2);
- }
-};
-
-// The Substitution object provides utility methods to manipulate
-
-/**
- * @exports opentype.Substitution
- * @class
- * @extends opentype.Layout
- * @param {opentype.Font}
- * @constructor
- */
-function Substitution(font) {
- Layout.call(this, font, 'gsub');
-}
-
-// Check if 2 arrays of primitives are equal.
-function arraysEqual(ar1, ar2) {
- var n = ar1.length;
- if (n !== ar2.length) { return false; }
- for (var i = 0; i < n; i++) {
- if (ar1[i] !== ar2[i]) { return false; }
- }
- return true;
-}
-
-// Find the first subtable of a lookup table in a particular format.
-function getSubstFormat(lookupTable, format, defaultSubtable) {
- var subtables = lookupTable.subtables;
- for (var i = 0; i < subtables.length; i++) {
- var subtable = subtables[i];
- if (subtable.substFormat === format) {
- return subtable;
- }
- }
- if (defaultSubtable) {
- subtables.push(defaultSubtable);
- return defaultSubtable;
- }
- return undefined;
-}
-
-Substitution.prototype = Layout.prototype;
-
-/**
- * Create a default GSUB table.
- * @return {Object} gsub - The GSUB table.
- */
-Substitution.prototype.createDefaultTable = function() {
- // Generate a default empty GSUB table with just a DFLT script and dflt lang sys.
- return {
- version: 1,
- scripts: [{
- tag: 'DFLT',
- script: {
- defaultLangSys: { reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: [] },
- langSysRecords: []
- }
- }],
- features: [],
- lookups: []
- };
-};
-
-/**
- * List all single substitutions (lookup type 1) for a given script, language, and feature.
- * @param {string} [script='DFLT']
- * @param {string} [language='dflt']
- * @param {string} feature - 4-character feature name ('aalt', 'salt', 'ss01'...)
- * @return {Array} substitutions - The list of substitutions.
- */
-Substitution.prototype.getSingle = function(feature, script, language) {
- var substitutions = [];
- var lookupTables = this.getLookupTables(script, language, feature, 1);
- for (var idx = 0; idx < lookupTables.length; idx++) {
- var subtables = lookupTables[idx].subtables;
- for (var i = 0; i < subtables.length; i++) {
- var subtable = subtables[i];
- var glyphs = this.expandCoverage(subtable.coverage);
- var j = (void 0);
- if (subtable.substFormat === 1) {
- var delta = subtable.deltaGlyphId;
- for (j = 0; j < glyphs.length; j++) {
- var glyph = glyphs[j];
- substitutions.push({ sub: glyph, by: glyph + delta });
- }
- } else {
- var substitute = subtable.substitute;
- for (j = 0; j < glyphs.length; j++) {
- substitutions.push({ sub: glyphs[j], by: substitute[j] });
- }
- }
- }
- }
- return substitutions;
-};
-
-/**
- * List all multiple substitutions (lookup type 2) for a given script, language, and feature.
- * @param {string} [script='DFLT']
- * @param {string} [language='dflt']
- * @param {string} feature - 4-character feature name ('ccmp', 'stch')
- * @return {Array} substitutions - The list of substitutions.
- */
-Substitution.prototype.getMultiple = function(feature, script, language) {
- var substitutions = [];
- var lookupTables = this.getLookupTables(script, language, feature, 2);
- for (var idx = 0; idx < lookupTables.length; idx++) {
- var subtables = lookupTables[idx].subtables;
- for (var i = 0; i < subtables.length; i++) {
- var subtable = subtables[i];
- var glyphs = this.expandCoverage(subtable.coverage);
- var j = (void 0);
-
- for (j = 0; j < glyphs.length; j++) {
- var glyph = glyphs[j];
- var replacements = subtable.sequences[j];
- substitutions.push({ sub: glyph, by: replacements });
- }
- }
- }
- return substitutions;
-};
-
-/**
- * List all alternates (lookup type 3) for a given script, language, and feature.
- * @param {string} [script='DFLT']
- * @param {string} [language='dflt']
- * @param {string} feature - 4-character feature name ('aalt', 'salt'...)
- * @return {Array} alternates - The list of alternates
- */
-Substitution.prototype.getAlternates = function(feature, script, language) {
- var alternates = [];
- var lookupTables = this.getLookupTables(script, language, feature, 3);
- for (var idx = 0; idx < lookupTables.length; idx++) {
- var subtables = lookupTables[idx].subtables;
- for (var i = 0; i < subtables.length; i++) {
- var subtable = subtables[i];
- var glyphs = this.expandCoverage(subtable.coverage);
- var alternateSets = subtable.alternateSets;
- for (var j = 0; j < glyphs.length; j++) {
- alternates.push({ sub: glyphs[j], by: alternateSets[j] });
- }
- }
- }
- return alternates;
-};
-
-/**
- * List all ligatures (lookup type 4) for a given script, language, and feature.
- * The result is an array of ligature objects like { sub: [ids], by: id }
- * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
- * @param {string} [script='DFLT']
- * @param {string} [language='dflt']
- * @return {Array} ligatures - The list of ligatures.
- */
-Substitution.prototype.getLigatures = function(feature, script, language) {
- var ligatures = [];
- var lookupTables = this.getLookupTables(script, language, feature, 4);
- for (var idx = 0; idx < lookupTables.length; idx++) {
- var subtables = lookupTables[idx].subtables;
- for (var i = 0; i < subtables.length; i++) {
- var subtable = subtables[i];
- var glyphs = this.expandCoverage(subtable.coverage);
- var ligatureSets = subtable.ligatureSets;
- for (var j = 0; j < glyphs.length; j++) {
- var startGlyph = glyphs[j];
- var ligSet = ligatureSets[j];
- for (var k = 0; k < ligSet.length; k++) {
- var lig = ligSet[k];
- ligatures.push({
- sub: [startGlyph].concat(lig.components),
- by: lig.ligGlyph
- });
- }
- }
- }
- }
- return ligatures;
-};
-
-/**
- * Add or modify a single substitution (lookup type 1)
- * Format 2, more flexible, is always used.
- * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
- * @param {Object} substitution - { sub: id, by: id } (format 1 is not supported)
- * @param {string} [script='DFLT']
- * @param {string} [language='dflt']
- */
-Substitution.prototype.addSingle = function(feature, substitution, script, language) {
- var lookupTable = this.getLookupTables(script, language, feature, 1, true)[0];
- var subtable = getSubstFormat(lookupTable, 2, { // lookup type 1 subtable, format 2, coverage format 1
- substFormat: 2,
- coverage: {format: 1, glyphs: []},
- substitute: []
- });
- check.assert(subtable.coverage.format === 1, 'Single: unable to modify coverage table format ' + subtable.coverage.format);
- var coverageGlyph = substitution.sub;
- var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
- if (pos < 0) {
- pos = -1 - pos;
- subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
- subtable.substitute.splice(pos, 0, 0);
- }
- subtable.substitute[pos] = substitution.by;
-};
-
-/**
- * Add or modify a multiple substitution (lookup type 2)
- * @param {string} feature - 4-letter feature name ('ccmp', 'stch')
- * @param {Object} substitution - { sub: id, by: [id] } for format 2.
- * @param {string} [script='DFLT']
- * @param {string} [language='dflt']
- */
-Substitution.prototype.addMultiple = function(feature, substitution, script, language) {
- check.assert(substitution.by instanceof Array && substitution.by.length > 1, 'Multiple: "by" must be an array of two or more ids');
- var lookupTable = this.getLookupTables(script, language, feature, 2, true)[0];
- var subtable = getSubstFormat(lookupTable, 1, { // lookup type 2 subtable, format 1, coverage format 1
- substFormat: 1,
- coverage: {format: 1, glyphs: []},
- sequences: []
- });
- check.assert(subtable.coverage.format === 1, 'Multiple: unable to modify coverage table format ' + subtable.coverage.format);
- var coverageGlyph = substitution.sub;
- var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
- if (pos < 0) {
- pos = -1 - pos;
- subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
- subtable.sequences.splice(pos, 0, 0);
- }
- subtable.sequences[pos] = substitution.by;
-};
-
-/**
- * Add or modify an alternate substitution (lookup type 3)
- * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
- * @param {Object} substitution - { sub: id, by: [ids] }
- * @param {string} [script='DFLT']
- * @param {string} [language='dflt']
- */
-Substitution.prototype.addAlternate = function(feature, substitution, script, language) {
- var lookupTable = this.getLookupTables(script, language, feature, 3, true)[0];
- var subtable = getSubstFormat(lookupTable, 1, { // lookup type 3 subtable, format 1, coverage format 1
- substFormat: 1,
- coverage: {format: 1, glyphs: []},
- alternateSets: []
- });
- check.assert(subtable.coverage.format === 1, 'Alternate: unable to modify coverage table format ' + subtable.coverage.format);
- var coverageGlyph = substitution.sub;
- var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
- if (pos < 0) {
- pos = -1 - pos;
- subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
- subtable.alternateSets.splice(pos, 0, 0);
- }
- subtable.alternateSets[pos] = substitution.by;
-};
-
-/**
- * Add a ligature (lookup type 4)
- * Ligatures with more components must be stored ahead of those with fewer components in order to be found
- * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...)
- * @param {Object} ligature - { sub: [ids], by: id }
- * @param {string} [script='DFLT']
- * @param {string} [language='dflt']
- */
-Substitution.prototype.addLigature = function(feature, ligature, script, language) {
- var lookupTable = this.getLookupTables(script, language, feature, 4, true)[0];
- var subtable = lookupTable.subtables[0];
- if (!subtable) {
- subtable = { // lookup type 4 subtable, format 1, coverage format 1
- substFormat: 1,
- coverage: { format: 1, glyphs: [] },
- ligatureSets: []
- };
- lookupTable.subtables[0] = subtable;
- }
- check.assert(subtable.coverage.format === 1, 'Ligature: unable to modify coverage table format ' + subtable.coverage.format);
- var coverageGlyph = ligature.sub[0];
- var ligComponents = ligature.sub.slice(1);
- var ligatureTable = {
- ligGlyph: ligature.by,
- components: ligComponents
- };
- var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph);
- if (pos >= 0) {
- // ligatureSet already exists
- var ligatureSet = subtable.ligatureSets[pos];
- for (var i = 0; i < ligatureSet.length; i++) {
- // If ligature already exists, return.
- if (arraysEqual(ligatureSet[i].components, ligComponents)) {
- return;
- }
- }
- // ligature does not exist: add it.
- ligatureSet.push(ligatureTable);
- } else {
- // Create a new ligatureSet and add coverage for the first glyph.
- pos = -1 - pos;
- subtable.coverage.glyphs.splice(pos, 0, coverageGlyph);
- subtable.ligatureSets.splice(pos, 0, [ligatureTable]);
- }
-};
-
-/**
- * List all feature data for a given script and language.
- * @param {string} feature - 4-letter feature name
- * @param {string} [script='DFLT']
- * @param {string} [language='dflt']
- * @return {Array} substitutions - The list of substitutions.
- */
-Substitution.prototype.getFeature = function(feature, script, language) {
- if (/ss\d\d/.test(feature)) {
- // ss01 - ss20
- return this.getSingle(feature, script, language);
- }
- switch (feature) {
- case 'aalt':
- case 'salt':
- return this.getSingle(feature, script, language)
- .concat(this.getAlternates(feature, script, language));
- case 'dlig':
- case 'liga':
- case 'rlig':
- return this.getLigatures(feature, script, language);
- case 'ccmp':
- return this.getMultiple(feature, script, language)
- .concat(this.getLigatures(feature, script, language));
- case 'stch':
- return this.getMultiple(feature, script, language);
- }
- return undefined;
-};
-
-/**
- * Add a substitution to a feature for a given script and language.
- * @param {string} feature - 4-letter feature name
- * @param {Object} sub - the substitution to add (an object like { sub: id or [ids], by: id or [ids] })
- * @param {string} [script='DFLT']
- * @param {string} [language='dflt']
- */
-Substitution.prototype.add = function(feature, sub, script, language) {
- if (/ss\d\d/.test(feature)) {
- // ss01 - ss20
- return this.addSingle(feature, sub, script, language);
- }
- switch (feature) {
- case 'aalt':
- case 'salt':
- if (typeof sub.by === 'number') {
- return this.addSingle(feature, sub, script, language);
- }
- return this.addAlternate(feature, sub, script, language);
- case 'dlig':
- case 'liga':
- case 'rlig':
- return this.addLigature(feature, sub, script, language);
- case 'ccmp':
- if (sub.by instanceof Array) {
- return this.addMultiple(feature, sub, script, language);
- }
- return this.addLigature(feature, sub, script, language);
- }
- return undefined;
-};
-
-function isBrowser() {
- return typeof window !== 'undefined';
-}
-
-function nodeBufferToArrayBuffer(buffer) {
- var ab = new ArrayBuffer(buffer.length);
- var view = new Uint8Array(ab);
- for (var i = 0; i < buffer.length; ++i) {
- view[i] = buffer[i];
- }
-
- return ab;
-}
-
-function arrayBufferToNodeBuffer(ab) {
- var buffer = new Buffer(ab.byteLength);
- var view = new Uint8Array(ab);
- for (var i = 0; i < buffer.length; ++i) {
- buffer[i] = view[i];
- }
-
- return buffer;
-}
-
-function checkArgument(expression, message) {
- if (!expression) {
- throw message;
- }
-}
-
-// The `glyf` table describes the glyphs in TrueType outline format.
-
-// Parse the coordinate data for a glyph.
-function parseGlyphCoordinate(p, flag, previousValue, shortVectorBitMask, sameBitMask) {
- var v;
- if ((flag & shortVectorBitMask) > 0) {
- // The coordinate is 1 byte long.
- v = p.parseByte();
- // The `same` bit is re-used for short values to signify the sign of the value.
- if ((flag & sameBitMask) === 0) {
- v = -v;
- }
-
- v = previousValue + v;
- } else {
- // The coordinate is 2 bytes long.
- // If the `same` bit is set, the coordinate is the same as the previous coordinate.
- if ((flag & sameBitMask) > 0) {
- v = previousValue;
- } else {
- // Parse the coordinate as a signed 16-bit delta value.
- v = previousValue + p.parseShort();
- }
- }
-
- return v;
-}
-
-// Parse a TrueType glyph.
-function parseGlyph(glyph, data, start) {
- var p = new parse.Parser(data, start);
- glyph.numberOfContours = p.parseShort();
- glyph._xMin = p.parseShort();
- glyph._yMin = p.parseShort();
- glyph._xMax = p.parseShort();
- glyph._yMax = p.parseShort();
- var flags;
- var flag;
-
- if (glyph.numberOfContours > 0) {
- // This glyph is not a composite.
- var endPointIndices = glyph.endPointIndices = [];
- for (var i = 0; i < glyph.numberOfContours; i += 1) {
- endPointIndices.push(p.parseUShort());
- }
-
- glyph.instructionLength = p.parseUShort();
- glyph.instructions = [];
- for (var i$1 = 0; i$1 < glyph.instructionLength; i$1 += 1) {
- glyph.instructions.push(p.parseByte());
- }
-
- var numberOfCoordinates = endPointIndices[endPointIndices.length - 1] + 1;
- flags = [];
- for (var i$2 = 0; i$2 < numberOfCoordinates; i$2 += 1) {
- flag = p.parseByte();
- flags.push(flag);
- // If bit 3 is set, we repeat this flag n times, where n is the next byte.
- if ((flag & 8) > 0) {
- var repeatCount = p.parseByte();
- for (var j = 0; j < repeatCount; j += 1) {
- flags.push(flag);
- i$2 += 1;
- }
- }
- }
-
- check.argument(flags.length === numberOfCoordinates, 'Bad flags.');
-
- if (endPointIndices.length > 0) {
- var points = [];
- var point;
- // X/Y coordinates are relative to the previous point, except for the first point which is relative to 0,0.
- if (numberOfCoordinates > 0) {
- for (var i$3 = 0; i$3 < numberOfCoordinates; i$3 += 1) {
- flag = flags[i$3];
- point = {};
- point.onCurve = !!(flag & 1);
- point.lastPointOfContour = endPointIndices.indexOf(i$3) >= 0;
- points.push(point);
- }
-
- var px = 0;
- for (var i$4 = 0; i$4 < numberOfCoordinates; i$4 += 1) {
- flag = flags[i$4];
- point = points[i$4];
- point.x = parseGlyphCoordinate(p, flag, px, 2, 16);
- px = point.x;
- }
-
- var py = 0;
- for (var i$5 = 0; i$5 < numberOfCoordinates; i$5 += 1) {
- flag = flags[i$5];
- point = points[i$5];
- point.y = parseGlyphCoordinate(p, flag, py, 4, 32);
- py = point.y;
- }
- }
-
- glyph.points = points;
- } else {
- glyph.points = [];
- }
- } else if (glyph.numberOfContours === 0) {
- glyph.points = [];
- } else {
- glyph.isComposite = true;
- glyph.points = [];
- glyph.components = [];
- var moreComponents = true;
- while (moreComponents) {
- flags = p.parseUShort();
- var component = {
- glyphIndex: p.parseUShort(),
- xScale: 1,
- scale01: 0,
- scale10: 0,
- yScale: 1,
- dx: 0,
- dy: 0
- };
- if ((flags & 1) > 0) {
- // The arguments are words
- if ((flags & 2) > 0) {
- // values are offset
- component.dx = p.parseShort();
- component.dy = p.parseShort();
- } else {
- // values are matched points
- component.matchedPoints = [p.parseUShort(), p.parseUShort()];
- }
-
- } else {
- // The arguments are bytes
- if ((flags & 2) > 0) {
- // values are offset
- component.dx = p.parseChar();
- component.dy = p.parseChar();
- } else {
- // values are matched points
- component.matchedPoints = [p.parseByte(), p.parseByte()];
- }
- }
-
- if ((flags & 8) > 0) {
- // We have a scale
- component.xScale = component.yScale = p.parseF2Dot14();
- } else if ((flags & 64) > 0) {
- // We have an X / Y scale
- component.xScale = p.parseF2Dot14();
- component.yScale = p.parseF2Dot14();
- } else if ((flags & 128) > 0) {
- // We have a 2x2 transformation
- component.xScale = p.parseF2Dot14();
- component.scale01 = p.parseF2Dot14();
- component.scale10 = p.parseF2Dot14();
- component.yScale = p.parseF2Dot14();
- }
-
- glyph.components.push(component);
- moreComponents = !!(flags & 32);
- }
- if (flags & 0x100) {
- // We have instructions
- glyph.instructionLength = p.parseUShort();
- glyph.instructions = [];
- for (var i$6 = 0; i$6 < glyph.instructionLength; i$6 += 1) {
- glyph.instructions.push(p.parseByte());
- }
- }
- }
-}
-
-// Transform an array of points and return a new array.
-function transformPoints(points, transform) {
- var newPoints = [];
- for (var i = 0; i < points.length; i += 1) {
- var pt = points[i];
- var newPt = {
- x: transform.xScale * pt.x + transform.scale01 * pt.y + transform.dx,
- y: transform.scale10 * pt.x + transform.yScale * pt.y + transform.dy,
- onCurve: pt.onCurve,
- lastPointOfContour: pt.lastPointOfContour
- };
- newPoints.push(newPt);
- }
-
- return newPoints;
-}
-
-function getContours(points) {
- var contours = [];
- var currentContour = [];
- for (var i = 0; i < points.length; i += 1) {
- var pt = points[i];
- currentContour.push(pt);
- if (pt.lastPointOfContour) {
- contours.push(currentContour);
- currentContour = [];
- }
- }
-
- check.argument(currentContour.length === 0, 'There are still points left in the current contour.');
- return contours;
-}
-
-// Convert the TrueType glyph outline to a Path.
-function getPath(points) {
- var p = new Path();
- if (!points) {
- return p;
- }
-
- var contours = getContours(points);
-
- for (var contourIndex = 0; contourIndex < contours.length; ++contourIndex) {
- var contour = contours[contourIndex];
-
- var prev = null;
- var curr = contour[contour.length - 1];
- var next = contour[0];
-
- if (curr.onCurve) {
- p.moveTo(curr.x, curr.y);
- } else {
- if (next.onCurve) {
- p.moveTo(next.x, next.y);
- } else {
- // If both first and last points are off-curve, start at their middle.
- var start = {x: (curr.x + next.x) * 0.5, y: (curr.y + next.y) * 0.5};
- p.moveTo(start.x, start.y);
- }
- }
-
- for (var i = 0; i < contour.length; ++i) {
- prev = curr;
- curr = next;
- next = contour[(i + 1) % contour.length];
-
- if (curr.onCurve) {
- // This is a straight line.
- p.lineTo(curr.x, curr.y);
- } else {
- var prev2 = prev;
- var next2 = next;
-
- if (!prev.onCurve) {
- prev2 = { x: (curr.x + prev.x) * 0.5, y: (curr.y + prev.y) * 0.5 };
- }
-
- if (!next.onCurve) {
- next2 = { x: (curr.x + next.x) * 0.5, y: (curr.y + next.y) * 0.5 };
- }
-
- p.quadraticCurveTo(curr.x, curr.y, next2.x, next2.y);
- }
- }
-
- p.closePath();
- }
- return p;
-}
-
-function buildPath(glyphs, glyph) {
- if (glyph.isComposite) {
- for (var j = 0; j < glyph.components.length; j += 1) {
- var component = glyph.components[j];
- var componentGlyph = glyphs.get(component.glyphIndex);
- // Force the ttfGlyphLoader to parse the glyph.
- componentGlyph.getPath();
- if (componentGlyph.points) {
- var transformedPoints = (void 0);
- if (component.matchedPoints === undefined) {
- // component positioned by offset
- transformedPoints = transformPoints(componentGlyph.points, component);
- } else {
- // component positioned by matched points
- if ((component.matchedPoints[0] > glyph.points.length - 1) ||
- (component.matchedPoints[1] > componentGlyph.points.length - 1)) {
- throw Error('Matched points out of range in ' + glyph.name);
- }
- var firstPt = glyph.points[component.matchedPoints[0]];
- var secondPt = componentGlyph.points[component.matchedPoints[1]];
- var transform = {
- xScale: component.xScale, scale01: component.scale01,
- scale10: component.scale10, yScale: component.yScale,
- dx: 0, dy: 0
- };
- secondPt = transformPoints([secondPt], transform)[0];
- transform.dx = firstPt.x - secondPt.x;
- transform.dy = firstPt.y - secondPt.y;
- transformedPoints = transformPoints(componentGlyph.points, transform);
- }
- glyph.points = glyph.points.concat(transformedPoints);
- }
- }
- }
-
- return getPath(glyph.points);
-}
-
-function parseGlyfTableAll(data, start, loca, font) {
- var glyphs = new glyphset.GlyphSet(font);
-
- // The last element of the loca table is invalid.
- for (var i = 0; i < loca.length - 1; i += 1) {
- var offset = loca[i];
- var nextOffset = loca[i + 1];
- if (offset !== nextOffset) {
- glyphs.push(i, glyphset.ttfGlyphLoader(font, i, parseGlyph, data, start + offset, buildPath));
- } else {
- glyphs.push(i, glyphset.glyphLoader(font, i));
- }
- }
-
- return glyphs;
-}
-
-function parseGlyfTableOnLowMemory(data, start, loca, font) {
- var glyphs = new glyphset.GlyphSet(font);
-
- font._push = function(i) {
- var offset = loca[i];
- var nextOffset = loca[i + 1];
- if (offset !== nextOffset) {
- glyphs.push(i, glyphset.ttfGlyphLoader(font, i, parseGlyph, data, start + offset, buildPath));
- } else {
- glyphs.push(i, glyphset.glyphLoader(font, i));
- }
- };
-
- return glyphs;
-}
-
-// Parse all the glyphs according to the offsets from the `loca` table.
-function parseGlyfTable(data, start, loca, font, opt) {
- if (opt.lowMemory)
- { return parseGlyfTableOnLowMemory(data, start, loca, font); }
- else
- { return parseGlyfTableAll(data, start, loca, font); }
-}
-
-var glyf = { getPath: getPath, parse: parseGlyfTable};
-
-/* A TrueType font hinting interpreter.
-*
-* (c) 2017 Axel Kittenberger
-*
-* This interpreter has been implemented according to this documentation:
-* https://developer.apple.com/fonts/TrueType-Reference-Manual/RM05/Chap5.html
-*
-* According to the documentation F24DOT6 values are used for pixels.
-* That means calculation is 1/64 pixel accurate and uses integer operations.
-* However, Javascript has floating point operations by default and only
-* those are available. One could make a case to simulate the 1/64 accuracy
-* exactly by truncating after every division operation
-* (for example with << 0) to get pixel exactly results as other TrueType
-* implementations. It may make sense since some fonts are pixel optimized
-* by hand using DELTAP instructions. The current implementation doesn't
-* and rather uses full floating point precision.
-*
-* xScale, yScale and rotation is currently ignored.
-*
-* A few non-trivial instructions are missing as I didn't encounter yet
-* a font that used them to test a possible implementation.
-*
-* Some fonts seem to use undocumented features regarding the twilight zone.
-* Only some of them are implemented as they were encountered.
-*
-* The exports.DEBUG statements are removed on the minified distribution file.
-*/
-
-var instructionTable;
-var exec;
-var execGlyph;
-var execComponent;
-
-/*
-* Creates a hinting object.
-*
-* There ought to be exactly one
-* for each truetype font that is used for hinting.
-*/
-function Hinting(font) {
- // the font this hinting object is for
- this.font = font;
-
- this.getCommands = function (hPoints) {
- return glyf.getPath(hPoints).commands;
- };
-
- // cached states
- this._fpgmState =
- this._prepState =
- undefined;
-
- // errorState
- // 0 ... all okay
- // 1 ... had an error in a glyf,
- // continue working but stop spamming
- // the console
- // 2 ... error at prep, stop hinting at this ppem
- // 3 ... error at fpeg, stop hinting for this font at all
- this._errorState = 0;
-}
-
-/*
-* Not rounding.
-*/
-function roundOff(v) {
- return v;
-}
-
-/*
-* Rounding to grid.
-*/
-function roundToGrid(v) {
- //Rounding in TT is supposed to "symmetrical around zero"
- return Math.sign(v) * Math.round(Math.abs(v));
-}
-
-/*
-* Rounding to double grid.
-*/
-function roundToDoubleGrid(v) {
- return Math.sign(v) * Math.round(Math.abs(v * 2)) / 2;
-}
-
-/*
-* Rounding to half grid.
-*/
-function roundToHalfGrid(v) {
- return Math.sign(v) * (Math.round(Math.abs(v) + 0.5) - 0.5);
-}
-
-/*
-* Rounding to up to grid.
-*/
-function roundUpToGrid(v) {
- return Math.sign(v) * Math.ceil(Math.abs(v));
-}
-
-/*
-* Rounding to down to grid.
-*/
-function roundDownToGrid(v) {
- return Math.sign(v) * Math.floor(Math.abs(v));
-}
-
-/*
-* Super rounding.
-*/
-var roundSuper = function (v) {
- var period = this.srPeriod;
- var phase = this.srPhase;
- var threshold = this.srThreshold;
- var sign = 1;
-
- if (v < 0) {
- v = -v;
- sign = -1;
- }
-
- v += threshold - phase;
-
- v = Math.trunc(v / period) * period;
-
- v += phase;
-
- // according to http://xgridfit.sourceforge.net/round.html
- if (v < 0) { return phase * sign; }
-
- return v * sign;
-};
-
-/*
-* Unit vector of x-axis.
-*/
-var xUnitVector = {
- x: 1,
-
- y: 0,
-
- axis: 'x',
-
- // Gets the projected distance between two points.
- // o1/o2 ... if true, respective original position is used.
- distance: function (p1, p2, o1, o2) {
- return (o1 ? p1.xo : p1.x) - (o2 ? p2.xo : p2.x);
- },
-
- // Moves point p so the moved position has the same relative
- // position to the moved positions of rp1 and rp2 than the
- // original positions had.
- //
- // See APPENDIX on INTERPOLATE at the bottom of this file.
- interpolate: function (p, rp1, rp2, pv) {
- var do1;
- var do2;
- var doa1;
- var doa2;
- var dm1;
- var dm2;
- var dt;
-
- if (!pv || pv === this) {
- do1 = p.xo - rp1.xo;
- do2 = p.xo - rp2.xo;
- dm1 = rp1.x - rp1.xo;
- dm2 = rp2.x - rp2.xo;
- doa1 = Math.abs(do1);
- doa2 = Math.abs(do2);
- dt = doa1 + doa2;
-
- if (dt === 0) {
- p.x = p.xo + (dm1 + dm2) / 2;
- return;
- }
-
- p.x = p.xo + (dm1 * doa2 + dm2 * doa1) / dt;
- return;
- }
-
- do1 = pv.distance(p, rp1, true, true);
- do2 = pv.distance(p, rp2, true, true);
- dm1 = pv.distance(rp1, rp1, false, true);
- dm2 = pv.distance(rp2, rp2, false, true);
- doa1 = Math.abs(do1);
- doa2 = Math.abs(do2);
- dt = doa1 + doa2;
-
- if (dt === 0) {
- xUnitVector.setRelative(p, p, (dm1 + dm2) / 2, pv, true);
- return;
- }
-
- xUnitVector.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true);
- },
-
- // Slope of line normal to this
- normalSlope: Number.NEGATIVE_INFINITY,
-
- // Sets the point 'p' relative to point 'rp'
- // by the distance 'd'.
- //
- // See APPENDIX on SETRELATIVE at the bottom of this file.
- //
- // p ... point to set
- // rp ... reference point
- // d ... distance on projection vector
- // pv ... projection vector (undefined = this)
- // org ... if true, uses the original position of rp as reference.
- setRelative: function (p, rp, d, pv, org) {
- if (!pv || pv === this) {
- p.x = (org ? rp.xo : rp.x) + d;
- return;
- }
-
- var rpx = org ? rp.xo : rp.x;
- var rpy = org ? rp.yo : rp.y;
- var rpdx = rpx + d * pv.x;
- var rpdy = rpy + d * pv.y;
-
- p.x = rpdx + (p.y - rpdy) / pv.normalSlope;
- },
-
- // Slope of vector line.
- slope: 0,
-
- // Touches the point p.
- touch: function (p) {
- p.xTouched = true;
- },
-
- // Tests if a point p is touched.
- touched: function (p) {
- return p.xTouched;
- },
-
- // Untouches the point p.
- untouch: function (p) {
- p.xTouched = false;
- }
-};
-
-/*
-* Unit vector of y-axis.
-*/
-var yUnitVector = {
- x: 0,
-
- y: 1,
-
- axis: 'y',
-
- // Gets the projected distance between two points.
- // o1/o2 ... if true, respective original position is used.
- distance: function (p1, p2, o1, o2) {
- return (o1 ? p1.yo : p1.y) - (o2 ? p2.yo : p2.y);
- },
-
- // Moves point p so the moved position has the same relative
- // position to the moved positions of rp1 and rp2 than the
- // original positions had.
- //
- // See APPENDIX on INTERPOLATE at the bottom of this file.
- interpolate: function (p, rp1, rp2, pv) {
- var do1;
- var do2;
- var doa1;
- var doa2;
- var dm1;
- var dm2;
- var dt;
-
- if (!pv || pv === this) {
- do1 = p.yo - rp1.yo;
- do2 = p.yo - rp2.yo;
- dm1 = rp1.y - rp1.yo;
- dm2 = rp2.y - rp2.yo;
- doa1 = Math.abs(do1);
- doa2 = Math.abs(do2);
- dt = doa1 + doa2;
-
- if (dt === 0) {
- p.y = p.yo + (dm1 + dm2) / 2;
- return;
- }
-
- p.y = p.yo + (dm1 * doa2 + dm2 * doa1) / dt;
- return;
- }
-
- do1 = pv.distance(p, rp1, true, true);
- do2 = pv.distance(p, rp2, true, true);
- dm1 = pv.distance(rp1, rp1, false, true);
- dm2 = pv.distance(rp2, rp2, false, true);
- doa1 = Math.abs(do1);
- doa2 = Math.abs(do2);
- dt = doa1 + doa2;
-
- if (dt === 0) {
- yUnitVector.setRelative(p, p, (dm1 + dm2) / 2, pv, true);
- return;
- }
-
- yUnitVector.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true);
- },
-
- // Slope of line normal to this.
- normalSlope: 0,
-
- // Sets the point 'p' relative to point 'rp'
- // by the distance 'd'
- //
- // See APPENDIX on SETRELATIVE at the bottom of this file.
- //
- // p ... point to set
- // rp ... reference point
- // d ... distance on projection vector
- // pv ... projection vector (undefined = this)
- // org ... if true, uses the original position of rp as reference.
- setRelative: function (p, rp, d, pv, org) {
- if (!pv || pv === this) {
- p.y = (org ? rp.yo : rp.y) + d;
- return;
- }
-
- var rpx = org ? rp.xo : rp.x;
- var rpy = org ? rp.yo : rp.y;
- var rpdx = rpx + d * pv.x;
- var rpdy = rpy + d * pv.y;
-
- p.y = rpdy + pv.normalSlope * (p.x - rpdx);
- },
-
- // Slope of vector line.
- slope: Number.POSITIVE_INFINITY,
-
- // Touches the point p.
- touch: function (p) {
- p.yTouched = true;
- },
-
- // Tests if a point p is touched.
- touched: function (p) {
- return p.yTouched;
- },
-
- // Untouches the point p.
- untouch: function (p) {
- p.yTouched = false;
- }
-};
-
-Object.freeze(xUnitVector);
-Object.freeze(yUnitVector);
-
-/*
-* Creates a unit vector that is not x- or y-axis.
-*/
-function UnitVector(x, y) {
- this.x = x;
- this.y = y;
- this.axis = undefined;
- this.slope = y / x;
- this.normalSlope = -x / y;
- Object.freeze(this);
-}
-
-/*
-* Gets the projected distance between two points.
-* o1/o2 ... if true, respective original position is used.
-*/
-UnitVector.prototype.distance = function(p1, p2, o1, o2) {
- return (
- this.x * xUnitVector.distance(p1, p2, o1, o2) +
- this.y * yUnitVector.distance(p1, p2, o1, o2)
- );
-};
-
-/*
-* Moves point p so the moved position has the same relative
-* position to the moved positions of rp1 and rp2 than the
-* original positions had.
-*
-* See APPENDIX on INTERPOLATE at the bottom of this file.
-*/
-UnitVector.prototype.interpolate = function(p, rp1, rp2, pv) {
- var dm1;
- var dm2;
- var do1;
- var do2;
- var doa1;
- var doa2;
- var dt;
-
- do1 = pv.distance(p, rp1, true, true);
- do2 = pv.distance(p, rp2, true, true);
- dm1 = pv.distance(rp1, rp1, false, true);
- dm2 = pv.distance(rp2, rp2, false, true);
- doa1 = Math.abs(do1);
- doa2 = Math.abs(do2);
- dt = doa1 + doa2;
-
- if (dt === 0) {
- this.setRelative(p, p, (dm1 + dm2) / 2, pv, true);
- return;
- }
-
- this.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true);
-};
-
-/*
-* Sets the point 'p' relative to point 'rp'
-* by the distance 'd'
-*
-* See APPENDIX on SETRELATIVE at the bottom of this file.
-*
-* p ... point to set
-* rp ... reference point
-* d ... distance on projection vector
-* pv ... projection vector (undefined = this)
-* org ... if true, uses the original position of rp as reference.
-*/
-UnitVector.prototype.setRelative = function(p, rp, d, pv, org) {
- pv = pv || this;
-
- var rpx = org ? rp.xo : rp.x;
- var rpy = org ? rp.yo : rp.y;
- var rpdx = rpx + d * pv.x;
- var rpdy = rpy + d * pv.y;
-
- var pvns = pv.normalSlope;
- var fvs = this.slope;
-
- var px = p.x;
- var py = p.y;
-
- p.x = (fvs * px - pvns * rpdx + rpdy - py) / (fvs - pvns);
- p.y = fvs * (p.x - px) + py;
-};
-
-/*
-* Touches the point p.
-*/
-UnitVector.prototype.touch = function(p) {
- p.xTouched = true;
- p.yTouched = true;
-};
-
-/*
-* Returns a unit vector with x/y coordinates.
-*/
-function getUnitVector(x, y) {
- var d = Math.sqrt(x * x + y * y);
-
- x /= d;
- y /= d;
-
- if (x === 1 && y === 0) { return xUnitVector; }
- else if (x === 0 && y === 1) { return yUnitVector; }
- else { return new UnitVector(x, y); }
-}
-
-/*
-* Creates a point in the hinting engine.
-*/
-function HPoint(
- x,
- y,
- lastPointOfContour,
- onCurve
-) {
- this.x = this.xo = Math.round(x * 64) / 64; // hinted x value and original x-value
- this.y = this.yo = Math.round(y * 64) / 64; // hinted y value and original y-value
-
- this.lastPointOfContour = lastPointOfContour;
- this.onCurve = onCurve;
- this.prevPointOnContour = undefined;
- this.nextPointOnContour = undefined;
- this.xTouched = false;
- this.yTouched = false;
-
- Object.preventExtensions(this);
-}
-
-/*
-* Returns the next touched point on the contour.
-*
-* v ... unit vector to test touch axis.
-*/
-HPoint.prototype.nextTouched = function(v) {
- var p = this.nextPointOnContour;
-
- while (!v.touched(p) && p !== this) { p = p.nextPointOnContour; }
-
- return p;
-};
-
-/*
-* Returns the previous touched point on the contour
-*
-* v ... unit vector to test touch axis.
-*/
-HPoint.prototype.prevTouched = function(v) {
- var p = this.prevPointOnContour;
-
- while (!v.touched(p) && p !== this) { p = p.prevPointOnContour; }
-
- return p;
-};
-
-/*
-* The zero point.
-*/
-var HPZero = Object.freeze(new HPoint(0, 0));
-
-/*
-* The default state of the interpreter.
-*
-* Note: Freezing the defaultState and then deriving from it
-* makes the V8 Javascript engine going awkward,
-* so this is avoided, albeit the defaultState shouldn't
-* ever change.
-*/
-var defaultState = {
- cvCutIn: 17 / 16, // control value cut in
- deltaBase: 9,
- deltaShift: 0.125,
- loop: 1, // loops some instructions
- minDis: 1, // minimum distance
- autoFlip: true
-};
-
-/*
-* The current state of the interpreter.
-*
-* env ... 'fpgm' or 'prep' or 'glyf'
-* prog ... the program
-*/
-function State(env, prog) {
- this.env = env;
- this.stack = [];
- this.prog = prog;
-
- switch (env) {
- case 'glyf' :
- this.zp0 = this.zp1 = this.zp2 = 1;
- this.rp0 = this.rp1 = this.rp2 = 0;
- /* fall through */
- case 'prep' :
- this.fv = this.pv = this.dpv = xUnitVector;
- this.round = roundToGrid;
- }
-}
-
-/*
-* Executes a glyph program.
-*
-* This does the hinting for each glyph.
-*
-* Returns an array of moved points.
-*
-* glyph: the glyph to hint
-* ppem: the size the glyph is rendered for
-*/
-Hinting.prototype.exec = function(glyph, ppem) {
- if (typeof ppem !== 'number') {
- throw new Error('Point size is not a number!');
- }
-
- // Received a fatal error, don't do any hinting anymore.
- if (this._errorState > 2) { return; }
-
- var font = this.font;
- var prepState = this._prepState;
-
- if (!prepState || prepState.ppem !== ppem) {
- var fpgmState = this._fpgmState;
-
- if (!fpgmState) {
- // Executes the fpgm state.
- // This is used by fonts to define functions.
- State.prototype = defaultState;
-
- fpgmState =
- this._fpgmState =
- new State('fpgm', font.tables.fpgm);
-
- fpgmState.funcs = [ ];
- fpgmState.font = font;
-
- if (exports.DEBUG) {
- console.log('---EXEC FPGM---');
- fpgmState.step = -1;
- }
-
- try {
- exec(fpgmState);
- } catch (e) {
- console.log('Hinting error in FPGM:' + e);
- this._errorState = 3;
- return;
- }
- }
-
- // Executes the prep program for this ppem setting.
- // This is used by fonts to set cvt values
- // depending on to be rendered font size.
-
- State.prototype = fpgmState;
- prepState =
- this._prepState =
- new State('prep', font.tables.prep);
-
- prepState.ppem = ppem;
-
- // Creates a copy of the cvt table
- // and scales it to the current ppem setting.
- var oCvt = font.tables.cvt;
- if (oCvt) {
- var cvt = prepState.cvt = new Array(oCvt.length);
- var scale = ppem / font.unitsPerEm;
- for (var c = 0; c < oCvt.length; c++) {
- cvt[c] = oCvt[c] * scale;
- }
- } else {
- prepState.cvt = [];
- }
-
- if (exports.DEBUG) {
- console.log('---EXEC PREP---');
- prepState.step = -1;
- }
-
- try {
- exec(prepState);
- } catch (e) {
- if (this._errorState < 2) {
- console.log('Hinting error in PREP:' + e);
- }
- this._errorState = 2;
- }
- }
-
- if (this._errorState > 1) { return; }
-
- try {
- return execGlyph(glyph, prepState);
- } catch (e) {
- if (this._errorState < 1) {
- console.log('Hinting error:' + e);
- console.log('Note: further hinting errors are silenced');
- }
- this._errorState = 1;
- return undefined;
- }
-};
-
-/*
-* Executes the hinting program for a glyph.
-*/
-execGlyph = function(glyph, prepState) {
- // original point positions
- var xScale = prepState.ppem / prepState.font.unitsPerEm;
- var yScale = xScale;
- var components = glyph.components;
- var contours;
- var gZone;
- var state;
-
- State.prototype = prepState;
- if (!components) {
- state = new State('glyf', glyph.instructions);
- if (exports.DEBUG) {
- console.log('---EXEC GLYPH---');
- state.step = -1;
- }
- execComponent(glyph, state, xScale, yScale);
- gZone = state.gZone;
- } else {
- var font = prepState.font;
- gZone = [];
- contours = [];
- for (var i = 0; i < components.length; i++) {
- var c = components[i];
- var cg = font.glyphs.get(c.glyphIndex);
-
- state = new State('glyf', cg.instructions);
-
- if (exports.DEBUG) {
- console.log('---EXEC COMP ' + i + '---');
- state.step = -1;
- }
-
- execComponent(cg, state, xScale, yScale);
- // appends the computed points to the result array
- // post processes the component points
- var dx = Math.round(c.dx * xScale);
- var dy = Math.round(c.dy * yScale);
- var gz = state.gZone;
- var cc = state.contours;
- for (var pi = 0; pi < gz.length; pi++) {
- var p = gz[pi];
- p.xTouched = p.yTouched = false;
- p.xo = p.x = p.x + dx;
- p.yo = p.y = p.y + dy;
- }
-
- var gLen = gZone.length;
- gZone.push.apply(gZone, gz);
- for (var j = 0; j < cc.length; j++) {
- contours.push(cc[j] + gLen);
- }
- }
-
- if (glyph.instructions && !state.inhibitGridFit) {
- // the composite has instructions on its own
- state = new State('glyf', glyph.instructions);
-
- state.gZone = state.z0 = state.z1 = state.z2 = gZone;
-
- state.contours = contours;
-
- // note: HPZero cannot be used here, since
- // the point might be modified
- gZone.push(
- new HPoint(0, 0),
- new HPoint(Math.round(glyph.advanceWidth * xScale), 0)
- );
-
- if (exports.DEBUG) {
- console.log('---EXEC COMPOSITE---');
- state.step = -1;
- }
-
- exec(state);
-
- gZone.length -= 2;
- }
- }
-
- return gZone;
-};
-
-/*
-* Executes the hinting program for a component of a multi-component glyph
-* or of the glyph itself for a non-component glyph.
-*/
-execComponent = function(glyph, state, xScale, yScale)
-{
- var points = glyph.points || [];
- var pLen = points.length;
- var gZone = state.gZone = state.z0 = state.z1 = state.z2 = [];
- var contours = state.contours = [];
-
- // Scales the original points and
- // makes copies for the hinted points.
- var cp; // current point
- for (var i = 0; i < pLen; i++) {
- cp = points[i];
-
- gZone[i] = new HPoint(
- cp.x * xScale,
- cp.y * yScale,
- cp.lastPointOfContour,
- cp.onCurve
- );
- }
-
- // Chain links the contours.
- var sp; // start point
- var np; // next point
-
- for (var i$1 = 0; i$1 < pLen; i$1++) {
- cp = gZone[i$1];
-
- if (!sp) {
- sp = cp;
- contours.push(i$1);
- }
-
- if (cp.lastPointOfContour) {
- cp.nextPointOnContour = sp;
- sp.prevPointOnContour = cp;
- sp = undefined;
- } else {
- np = gZone[i$1 + 1];
- cp.nextPointOnContour = np;
- np.prevPointOnContour = cp;
- }
- }
-
- if (state.inhibitGridFit) { return; }
-
- if (exports.DEBUG) {
- console.log('PROCESSING GLYPH', state.stack);
- for (var i$2 = 0; i$2 < pLen; i$2++) {
- console.log(i$2, gZone[i$2].x, gZone[i$2].y);
- }
- }
-
- gZone.push(
- new HPoint(0, 0),
- new HPoint(Math.round(glyph.advanceWidth * xScale), 0)
- );
-
- exec(state);
-
- // Removes the extra points.
- gZone.length -= 2;
-
- if (exports.DEBUG) {
- console.log('FINISHED GLYPH', state.stack);
- for (var i$3 = 0; i$3 < pLen; i$3++) {
- console.log(i$3, gZone[i$3].x, gZone[i$3].y);
- }
- }
-};
-
-/*
-* Executes the program loaded in state.
-*/
-exec = function(state) {
- var prog = state.prog;
-
- if (!prog) { return; }
-
- var pLen = prog.length;
- var ins;
-
- for (state.ip = 0; state.ip < pLen; state.ip++) {
- if (exports.DEBUG) { state.step++; }
- ins = instructionTable[prog[state.ip]];
-
- if (!ins) {
- throw new Error(
- 'unknown instruction: 0x' +
- Number(prog[state.ip]).toString(16)
- );
- }
-
- ins(state);
-
- // very extensive debugging for each step
- /*
- if (exports.DEBUG) {
- var da;
- if (state.gZone) {
- da = [];
- for (let i = 0; i < state.gZone.length; i++)
- {
- da.push(i + ' ' +
- state.gZone[i].x * 64 + ' ' +
- state.gZone[i].y * 64 + ' ' +
- (state.gZone[i].xTouched ? 'x' : '') +
- (state.gZone[i].yTouched ? 'y' : '')
- );
- }
- console.log('GZ', da);
- }
-
- if (state.tZone) {
- da = [];
- for (let i = 0; i < state.tZone.length; i++) {
- da.push(i + ' ' +
- state.tZone[i].x * 64 + ' ' +
- state.tZone[i].y * 64 + ' ' +
- (state.tZone[i].xTouched ? 'x' : '') +
- (state.tZone[i].yTouched ? 'y' : '')
- );
- }
- console.log('TZ', da);
- }
-
- if (state.stack.length > 10) {
- console.log(
- state.stack.length,
- '...', state.stack.slice(state.stack.length - 10)
- );
- } else {
- console.log(state.stack.length, state.stack);
- }
- }
- */
- }
-};
-
-/*
-* Initializes the twilight zone.
-*
-* This is only done if a SZPx instruction
-* refers to the twilight zone.
-*/
-function initTZone(state)
-{
- var tZone = state.tZone = new Array(state.gZone.length);
-
- // no idea if this is actually correct...
- for (var i = 0; i < tZone.length; i++)
- {
- tZone[i] = new HPoint(0, 0);
- }
-}
-
-/*
-* Skips the instruction pointer ahead over an IF/ELSE block.
-* handleElse .. if true breaks on matching ELSE
-*/
-function skip(state, handleElse)
-{
- var prog = state.prog;
- var ip = state.ip;
- var nesting = 1;
- var ins;
-
- do {
- ins = prog[++ip];
- if (ins === 0x58) // IF
- { nesting++; }
- else if (ins === 0x59) // EIF
- { nesting--; }
- else if (ins === 0x40) // NPUSHB
- { ip += prog[ip + 1] + 1; }
- else if (ins === 0x41) // NPUSHW
- { ip += 2 * prog[ip + 1] + 1; }
- else if (ins >= 0xB0 && ins <= 0xB7) // PUSHB
- { ip += ins - 0xB0 + 1; }
- else if (ins >= 0xB8 && ins <= 0xBF) // PUSHW
- { ip += (ins - 0xB8 + 1) * 2; }
- else if (handleElse && nesting === 1 && ins === 0x1B) // ELSE
- { break; }
- } while (nesting > 0);
-
- state.ip = ip;
-}
-
-/*----------------------------------------------------------*
-* And then a lot of instructions... *
-*----------------------------------------------------------*/
-
-// SVTCA[a] Set freedom and projection Vectors To Coordinate Axis
-// 0x00-0x01
-function SVTCA(v, state) {
- if (exports.DEBUG) { console.log(state.step, 'SVTCA[' + v.axis + ']'); }
-
- state.fv = state.pv = state.dpv = v;
-}
-
-// SPVTCA[a] Set Projection Vector to Coordinate Axis
-// 0x02-0x03
-function SPVTCA(v, state) {
- if (exports.DEBUG) { console.log(state.step, 'SPVTCA[' + v.axis + ']'); }
-
- state.pv = state.dpv = v;
-}
-
-// SFVTCA[a] Set Freedom Vector to Coordinate Axis
-// 0x04-0x05
-function SFVTCA(v, state) {
- if (exports.DEBUG) { console.log(state.step, 'SFVTCA[' + v.axis + ']'); }
-
- state.fv = v;
-}
-
-// SPVTL[a] Set Projection Vector To Line
-// 0x06-0x07
-function SPVTL(a, state) {
- var stack = state.stack;
- var p2i = stack.pop();
- var p1i = stack.pop();
- var p2 = state.z2[p2i];
- var p1 = state.z1[p1i];
-
- if (exports.DEBUG) { console.log('SPVTL[' + a + ']', p2i, p1i); }
-
- var dx;
- var dy;
-
- if (!a) {
- dx = p1.x - p2.x;
- dy = p1.y - p2.y;
- } else {
- dx = p2.y - p1.y;
- dy = p1.x - p2.x;
- }
-
- state.pv = state.dpv = getUnitVector(dx, dy);
-}
-
-// SFVTL[a] Set Freedom Vector To Line
-// 0x08-0x09
-function SFVTL(a, state) {
- var stack = state.stack;
- var p2i = stack.pop();
- var p1i = stack.pop();
- var p2 = state.z2[p2i];
- var p1 = state.z1[p1i];
-
- if (exports.DEBUG) { console.log('SFVTL[' + a + ']', p2i, p1i); }
-
- var dx;
- var dy;
-
- if (!a) {
- dx = p1.x - p2.x;
- dy = p1.y - p2.y;
- } else {
- dx = p2.y - p1.y;
- dy = p1.x - p2.x;
- }
-
- state.fv = getUnitVector(dx, dy);
-}
-
-// SPVFS[] Set Projection Vector From Stack
-// 0x0A
-function SPVFS(state) {
- var stack = state.stack;
- var y = stack.pop();
- var x = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SPVFS[]', y, x); }
-
- state.pv = state.dpv = getUnitVector(x, y);
-}
-
-// SFVFS[] Set Freedom Vector From Stack
-// 0x0B
-function SFVFS(state) {
- var stack = state.stack;
- var y = stack.pop();
- var x = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SPVFS[]', y, x); }
-
- state.fv = getUnitVector(x, y);
-}
-
-// GPV[] Get Projection Vector
-// 0x0C
-function GPV(state) {
- var stack = state.stack;
- var pv = state.pv;
-
- if (exports.DEBUG) { console.log(state.step, 'GPV[]'); }
-
- stack.push(pv.x * 0x4000);
- stack.push(pv.y * 0x4000);
-}
-
-// GFV[] Get Freedom Vector
-// 0x0C
-function GFV(state) {
- var stack = state.stack;
- var fv = state.fv;
-
- if (exports.DEBUG) { console.log(state.step, 'GFV[]'); }
-
- stack.push(fv.x * 0x4000);
- stack.push(fv.y * 0x4000);
-}
-
-// SFVTPV[] Set Freedom Vector To Projection Vector
-// 0x0E
-function SFVTPV(state) {
- state.fv = state.pv;
-
- if (exports.DEBUG) { console.log(state.step, 'SFVTPV[]'); }
-}
-
-// ISECT[] moves point p to the InterSECTion of two lines
-// 0x0F
-function ISECT(state)
-{
- var stack = state.stack;
- var pa0i = stack.pop();
- var pa1i = stack.pop();
- var pb0i = stack.pop();
- var pb1i = stack.pop();
- var pi = stack.pop();
- var z0 = state.z0;
- var z1 = state.z1;
- var pa0 = z0[pa0i];
- var pa1 = z0[pa1i];
- var pb0 = z1[pb0i];
- var pb1 = z1[pb1i];
- var p = state.z2[pi];
-
- if (exports.DEBUG) { console.log('ISECT[], ', pa0i, pa1i, pb0i, pb1i, pi); }
-
- // math from
- // en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
-
- var x1 = pa0.x;
- var y1 = pa0.y;
- var x2 = pa1.x;
- var y2 = pa1.y;
- var x3 = pb0.x;
- var y3 = pb0.y;
- var x4 = pb1.x;
- var y4 = pb1.y;
-
- var div = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
- var f1 = x1 * y2 - y1 * x2;
- var f2 = x3 * y4 - y3 * x4;
-
- p.x = (f1 * (x3 - x4) - f2 * (x1 - x2)) / div;
- p.y = (f1 * (y3 - y4) - f2 * (y1 - y2)) / div;
-}
-
-// SRP0[] Set Reference Point 0
-// 0x10
-function SRP0(state) {
- state.rp0 = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SRP0[]', state.rp0); }
-}
-
-// SRP1[] Set Reference Point 1
-// 0x11
-function SRP1(state) {
- state.rp1 = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SRP1[]', state.rp1); }
-}
-
-// SRP1[] Set Reference Point 2
-// 0x12
-function SRP2(state) {
- state.rp2 = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SRP2[]', state.rp2); }
-}
-
-// SZP0[] Set Zone Pointer 0
-// 0x13
-function SZP0(state) {
- var n = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SZP0[]', n); }
-
- state.zp0 = n;
-
- switch (n) {
- case 0:
- if (!state.tZone) { initTZone(state); }
- state.z0 = state.tZone;
- break;
- case 1 :
- state.z0 = state.gZone;
- break;
- default :
- throw new Error('Invalid zone pointer');
- }
-}
-
-// SZP1[] Set Zone Pointer 1
-// 0x14
-function SZP1(state) {
- var n = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SZP1[]', n); }
-
- state.zp1 = n;
-
- switch (n) {
- case 0:
- if (!state.tZone) { initTZone(state); }
- state.z1 = state.tZone;
- break;
- case 1 :
- state.z1 = state.gZone;
- break;
- default :
- throw new Error('Invalid zone pointer');
- }
-}
-
-// SZP2[] Set Zone Pointer 2
-// 0x15
-function SZP2(state) {
- var n = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SZP2[]', n); }
-
- state.zp2 = n;
-
- switch (n) {
- case 0:
- if (!state.tZone) { initTZone(state); }
- state.z2 = state.tZone;
- break;
- case 1 :
- state.z2 = state.gZone;
- break;
- default :
- throw new Error('Invalid zone pointer');
- }
-}
-
-// SZPS[] Set Zone PointerS
-// 0x16
-function SZPS(state) {
- var n = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SZPS[]', n); }
-
- state.zp0 = state.zp1 = state.zp2 = n;
-
- switch (n) {
- case 0:
- if (!state.tZone) { initTZone(state); }
- state.z0 = state.z1 = state.z2 = state.tZone;
- break;
- case 1 :
- state.z0 = state.z1 = state.z2 = state.gZone;
- break;
- default :
- throw new Error('Invalid zone pointer');
- }
-}
-
-// SLOOP[] Set LOOP variable
-// 0x17
-function SLOOP(state) {
- state.loop = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SLOOP[]', state.loop); }
-}
-
-// RTG[] Round To Grid
-// 0x18
-function RTG(state) {
- if (exports.DEBUG) { console.log(state.step, 'RTG[]'); }
-
- state.round = roundToGrid;
-}
-
-// RTHG[] Round To Half Grid
-// 0x19
-function RTHG(state) {
- if (exports.DEBUG) { console.log(state.step, 'RTHG[]'); }
-
- state.round = roundToHalfGrid;
-}
-
-// SMD[] Set Minimum Distance
-// 0x1A
-function SMD(state) {
- var d = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SMD[]', d); }
-
- state.minDis = d / 0x40;
-}
-
-// ELSE[] ELSE clause
-// 0x1B
-function ELSE(state) {
- // This instruction has been reached by executing a then branch
- // so it just skips ahead until matching EIF.
- //
- // In case the IF was negative the IF[] instruction already
- // skipped forward over the ELSE[]
-
- if (exports.DEBUG) { console.log(state.step, 'ELSE[]'); }
-
- skip(state, false);
-}
-
-// JMPR[] JuMP Relative
-// 0x1C
-function JMPR(state) {
- var o = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'JMPR[]', o); }
-
- // A jump by 1 would do nothing.
- state.ip += o - 1;
-}
-
-// SCVTCI[] Set Control Value Table Cut-In
-// 0x1D
-function SCVTCI(state) {
- var n = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SCVTCI[]', n); }
-
- state.cvCutIn = n / 0x40;
-}
-
-// DUP[] DUPlicate top stack element
-// 0x20
-function DUP(state) {
- var stack = state.stack;
-
- if (exports.DEBUG) { console.log(state.step, 'DUP[]'); }
-
- stack.push(stack[stack.length - 1]);
-}
-
-// POP[] POP top stack element
-// 0x21
-function POP(state) {
- if (exports.DEBUG) { console.log(state.step, 'POP[]'); }
-
- state.stack.pop();
-}
-
-// CLEAR[] CLEAR the stack
-// 0x22
-function CLEAR(state) {
- if (exports.DEBUG) { console.log(state.step, 'CLEAR[]'); }
-
- state.stack.length = 0;
-}
-
-// SWAP[] SWAP the top two elements on the stack
-// 0x23
-function SWAP(state) {
- var stack = state.stack;
-
- var a = stack.pop();
- var b = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SWAP[]'); }
-
- stack.push(a);
- stack.push(b);
-}
-
-// DEPTH[] DEPTH of the stack
-// 0x24
-function DEPTH(state) {
- var stack = state.stack;
-
- if (exports.DEBUG) { console.log(state.step, 'DEPTH[]'); }
-
- stack.push(stack.length);
-}
-
-// LOOPCALL[] LOOPCALL function
-// 0x2A
-function LOOPCALL(state) {
- var stack = state.stack;
- var fn = stack.pop();
- var c = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'LOOPCALL[]', fn, c); }
-
- // saves callers program
- var cip = state.ip;
- var cprog = state.prog;
-
- state.prog = state.funcs[fn];
-
- // executes the function
- for (var i = 0; i < c; i++) {
- exec(state);
-
- if (exports.DEBUG) { console.log(
- ++state.step,
- i + 1 < c ? 'next loopcall' : 'done loopcall',
- i
- ); }
- }
-
- // restores the callers program
- state.ip = cip;
- state.prog = cprog;
-}
-
-// CALL[] CALL function
-// 0x2B
-function CALL(state) {
- var fn = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'CALL[]', fn); }
-
- // saves callers program
- var cip = state.ip;
- var cprog = state.prog;
-
- state.prog = state.funcs[fn];
-
- // executes the function
- exec(state);
-
- // restores the callers program
- state.ip = cip;
- state.prog = cprog;
-
- if (exports.DEBUG) { console.log(++state.step, 'returning from', fn); }
-}
-
-// CINDEX[] Copy the INDEXed element to the top of the stack
-// 0x25
-function CINDEX(state) {
- var stack = state.stack;
- var k = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'CINDEX[]', k); }
-
- // In case of k == 1, it copies the last element after popping
- // thus stack.length - k.
- stack.push(stack[stack.length - k]);
-}
-
-// MINDEX[] Move the INDEXed element to the top of the stack
-// 0x26
-function MINDEX(state) {
- var stack = state.stack;
- var k = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'MINDEX[]', k); }
-
- stack.push(stack.splice(stack.length - k, 1)[0]);
-}
-
-// FDEF[] Function DEFinition
-// 0x2C
-function FDEF(state) {
- if (state.env !== 'fpgm') { throw new Error('FDEF not allowed here'); }
- var stack = state.stack;
- var prog = state.prog;
- var ip = state.ip;
-
- var fn = stack.pop();
- var ipBegin = ip;
-
- if (exports.DEBUG) { console.log(state.step, 'FDEF[]', fn); }
-
- while (prog[++ip] !== 0x2D){ }
-
- state.ip = ip;
- state.funcs[fn] = prog.slice(ipBegin + 1, ip);
-}
-
-// MDAP[a] Move Direct Absolute Point
-// 0x2E-0x2F
-function MDAP(round, state) {
- var pi = state.stack.pop();
- var p = state.z0[pi];
- var fv = state.fv;
- var pv = state.pv;
-
- if (exports.DEBUG) { console.log(state.step, 'MDAP[' + round + ']', pi); }
-
- var d = pv.distance(p, HPZero);
-
- if (round) { d = state.round(d); }
-
- fv.setRelative(p, HPZero, d, pv);
- fv.touch(p);
-
- state.rp0 = state.rp1 = pi;
-}
-
-// IUP[a] Interpolate Untouched Points through the outline
-// 0x30
-function IUP(v, state) {
- var z2 = state.z2;
- var pLen = z2.length - 2;
- var cp;
- var pp;
- var np;
-
- if (exports.DEBUG) { console.log(state.step, 'IUP[' + v.axis + ']'); }
-
- for (var i = 0; i < pLen; i++) {
- cp = z2[i]; // current point
-
- // if this point has been touched go on
- if (v.touched(cp)) { continue; }
-
- pp = cp.prevTouched(v);
-
- // no point on the contour has been touched?
- if (pp === cp) { continue; }
-
- np = cp.nextTouched(v);
-
- if (pp === np) {
- // only one point on the contour has been touched
- // so simply moves the point like that
-
- v.setRelative(cp, cp, v.distance(pp, pp, false, true), v, true);
- }
-
- v.interpolate(cp, pp, np, v);
- }
-}
-
-// SHP[] SHift Point using reference point
-// 0x32-0x33
-function SHP(a, state) {
- var stack = state.stack;
- var rpi = a ? state.rp1 : state.rp2;
- var rp = (a ? state.z0 : state.z1)[rpi];
- var fv = state.fv;
- var pv = state.pv;
- var loop = state.loop;
- var z2 = state.z2;
-
- while (loop--)
- {
- var pi = stack.pop();
- var p = z2[pi];
-
- var d = pv.distance(rp, rp, false, true);
- fv.setRelative(p, p, d, pv);
- fv.touch(p);
-
- if (exports.DEBUG) {
- console.log(
- state.step,
- (state.loop > 1 ?
- 'loop ' + (state.loop - loop) + ': ' :
- ''
- ) +
- 'SHP[' + (a ? 'rp1' : 'rp2') + ']', pi
- );
- }
- }
-
- state.loop = 1;
-}
-
-// SHC[] SHift Contour using reference point
-// 0x36-0x37
-function SHC(a, state) {
- var stack = state.stack;
- var rpi = a ? state.rp1 : state.rp2;
- var rp = (a ? state.z0 : state.z1)[rpi];
- var fv = state.fv;
- var pv = state.pv;
- var ci = stack.pop();
- var sp = state.z2[state.contours[ci]];
- var p = sp;
-
- if (exports.DEBUG) { console.log(state.step, 'SHC[' + a + ']', ci); }
-
- var d = pv.distance(rp, rp, false, true);
-
- do {
- if (p !== rp) { fv.setRelative(p, p, d, pv); }
- p = p.nextPointOnContour;
- } while (p !== sp);
-}
-
-// SHZ[] SHift Zone using reference point
-// 0x36-0x37
-function SHZ(a, state) {
- var stack = state.stack;
- var rpi = a ? state.rp1 : state.rp2;
- var rp = (a ? state.z0 : state.z1)[rpi];
- var fv = state.fv;
- var pv = state.pv;
-
- var e = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SHZ[' + a + ']', e); }
-
- var z;
- switch (e) {
- case 0 : z = state.tZone; break;
- case 1 : z = state.gZone; break;
- default : throw new Error('Invalid zone');
- }
-
- var p;
- var d = pv.distance(rp, rp, false, true);
- var pLen = z.length - 2;
- for (var i = 0; i < pLen; i++)
- {
- p = z[i];
- fv.setRelative(p, p, d, pv);
- //if (p !== rp) fv.setRelative(p, p, d, pv);
- }
-}
-
-// SHPIX[] SHift point by a PIXel amount
-// 0x38
-function SHPIX(state) {
- var stack = state.stack;
- var loop = state.loop;
- var fv = state.fv;
- var d = stack.pop() / 0x40;
- var z2 = state.z2;
-
- while (loop--) {
- var pi = stack.pop();
- var p = z2[pi];
-
- if (exports.DEBUG) {
- console.log(
- state.step,
- (state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') +
- 'SHPIX[]', pi, d
- );
- }
-
- fv.setRelative(p, p, d);
- fv.touch(p);
- }
-
- state.loop = 1;
-}
-
-// IP[] Interpolate Point
-// 0x39
-function IP(state) {
- var stack = state.stack;
- var rp1i = state.rp1;
- var rp2i = state.rp2;
- var loop = state.loop;
- var rp1 = state.z0[rp1i];
- var rp2 = state.z1[rp2i];
- var fv = state.fv;
- var pv = state.dpv;
- var z2 = state.z2;
-
- while (loop--) {
- var pi = stack.pop();
- var p = z2[pi];
-
- if (exports.DEBUG) {
- console.log(
- state.step,
- (state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') +
- 'IP[]', pi, rp1i, '<->', rp2i
- );
- }
-
- fv.interpolate(p, rp1, rp2, pv);
-
- fv.touch(p);
- }
-
- state.loop = 1;
-}
-
-// MSIRP[a] Move Stack Indirect Relative Point
-// 0x3A-0x3B
-function MSIRP(a, state) {
- var stack = state.stack;
- var d = stack.pop() / 64;
- var pi = stack.pop();
- var p = state.z1[pi];
- var rp0 = state.z0[state.rp0];
- var fv = state.fv;
- var pv = state.pv;
-
- fv.setRelative(p, rp0, d, pv);
- fv.touch(p);
-
- if (exports.DEBUG) { console.log(state.step, 'MSIRP[' + a + ']', d, pi); }
-
- state.rp1 = state.rp0;
- state.rp2 = pi;
- if (a) { state.rp0 = pi; }
-}
-
-// ALIGNRP[] Align to reference point.
-// 0x3C
-function ALIGNRP(state) {
- var stack = state.stack;
- var rp0i = state.rp0;
- var rp0 = state.z0[rp0i];
- var loop = state.loop;
- var fv = state.fv;
- var pv = state.pv;
- var z1 = state.z1;
-
- while (loop--) {
- var pi = stack.pop();
- var p = z1[pi];
-
- if (exports.DEBUG) {
- console.log(
- state.step,
- (state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') +
- 'ALIGNRP[]', pi
- );
- }
-
- fv.setRelative(p, rp0, 0, pv);
- fv.touch(p);
- }
-
- state.loop = 1;
-}
-
-// RTG[] Round To Double Grid
-// 0x3D
-function RTDG(state) {
- if (exports.DEBUG) { console.log(state.step, 'RTDG[]'); }
-
- state.round = roundToDoubleGrid;
-}
-
-// MIAP[a] Move Indirect Absolute Point
-// 0x3E-0x3F
-function MIAP(round, state) {
- var stack = state.stack;
- var n = stack.pop();
- var pi = stack.pop();
- var p = state.z0[pi];
- var fv = state.fv;
- var pv = state.pv;
- var cv = state.cvt[n];
-
- if (exports.DEBUG) {
- console.log(
- state.step,
- 'MIAP[' + round + ']',
- n, '(', cv, ')', pi
- );
- }
-
- var d = pv.distance(p, HPZero);
-
- if (round) {
- if (Math.abs(d - cv) < state.cvCutIn) { d = cv; }
-
- d = state.round(d);
- }
-
- fv.setRelative(p, HPZero, d, pv);
-
- if (state.zp0 === 0) {
- p.xo = p.x;
- p.yo = p.y;
- }
-
- fv.touch(p);
-
- state.rp0 = state.rp1 = pi;
-}
-
-// NPUSB[] PUSH N Bytes
-// 0x40
-function NPUSHB(state) {
- var prog = state.prog;
- var ip = state.ip;
- var stack = state.stack;
-
- var n = prog[++ip];
-
- if (exports.DEBUG) { console.log(state.step, 'NPUSHB[]', n); }
-
- for (var i = 0; i < n; i++) { stack.push(prog[++ip]); }
-
- state.ip = ip;
-}
-
-// NPUSHW[] PUSH N Words
-// 0x41
-function NPUSHW(state) {
- var ip = state.ip;
- var prog = state.prog;
- var stack = state.stack;
- var n = prog[++ip];
-
- if (exports.DEBUG) { console.log(state.step, 'NPUSHW[]', n); }
-
- for (var i = 0; i < n; i++) {
- var w = (prog[++ip] << 8) | prog[++ip];
- if (w & 0x8000) { w = -((w ^ 0xffff) + 1); }
- stack.push(w);
- }
-
- state.ip = ip;
-}
-
-// WS[] Write Store
-// 0x42
-function WS(state) {
- var stack = state.stack;
- var store = state.store;
-
- if (!store) { store = state.store = []; }
-
- var v = stack.pop();
- var l = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'WS', v, l); }
-
- store[l] = v;
-}
-
-// RS[] Read Store
-// 0x43
-function RS(state) {
- var stack = state.stack;
- var store = state.store;
-
- var l = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'RS', l); }
-
- var v = (store && store[l]) || 0;
-
- stack.push(v);
-}
-
-// WCVTP[] Write Control Value Table in Pixel units
-// 0x44
-function WCVTP(state) {
- var stack = state.stack;
-
- var v = stack.pop();
- var l = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'WCVTP', v, l); }
-
- state.cvt[l] = v / 0x40;
-}
-
-// RCVT[] Read Control Value Table entry
-// 0x45
-function RCVT(state) {
- var stack = state.stack;
- var cvte = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'RCVT', cvte); }
-
- stack.push(state.cvt[cvte] * 0x40);
-}
-
-// GC[] Get Coordinate projected onto the projection vector
-// 0x46-0x47
-function GC(a, state) {
- var stack = state.stack;
- var pi = stack.pop();
- var p = state.z2[pi];
-
- if (exports.DEBUG) { console.log(state.step, 'GC[' + a + ']', pi); }
-
- stack.push(state.dpv.distance(p, HPZero, a, false) * 0x40);
-}
-
-// MD[a] Measure Distance
-// 0x49-0x4A
-function MD(a, state) {
- var stack = state.stack;
- var pi2 = stack.pop();
- var pi1 = stack.pop();
- var p2 = state.z1[pi2];
- var p1 = state.z0[pi1];
- var d = state.dpv.distance(p1, p2, a, a);
-
- if (exports.DEBUG) { console.log(state.step, 'MD[' + a + ']', pi2, pi1, '->', d); }
-
- state.stack.push(Math.round(d * 64));
-}
-
-// MPPEM[] Measure Pixels Per EM
-// 0x4B
-function MPPEM(state) {
- if (exports.DEBUG) { console.log(state.step, 'MPPEM[]'); }
- state.stack.push(state.ppem);
-}
-
-// FLIPON[] set the auto FLIP Boolean to ON
-// 0x4D
-function FLIPON(state) {
- if (exports.DEBUG) { console.log(state.step, 'FLIPON[]'); }
- state.autoFlip = true;
-}
-
-// LT[] Less Than
-// 0x50
-function LT(state) {
- var stack = state.stack;
- var e2 = stack.pop();
- var e1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'LT[]', e2, e1); }
-
- stack.push(e1 < e2 ? 1 : 0);
-}
-
-// LTEQ[] Less Than or EQual
-// 0x53
-function LTEQ(state) {
- var stack = state.stack;
- var e2 = stack.pop();
- var e1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'LTEQ[]', e2, e1); }
-
- stack.push(e1 <= e2 ? 1 : 0);
-}
-
-// GTEQ[] Greater Than
-// 0x52
-function GT(state) {
- var stack = state.stack;
- var e2 = stack.pop();
- var e1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'GT[]', e2, e1); }
-
- stack.push(e1 > e2 ? 1 : 0);
-}
-
-// GTEQ[] Greater Than or EQual
-// 0x53
-function GTEQ(state) {
- var stack = state.stack;
- var e2 = stack.pop();
- var e1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'GTEQ[]', e2, e1); }
-
- stack.push(e1 >= e2 ? 1 : 0);
-}
-
-// EQ[] EQual
-// 0x54
-function EQ(state) {
- var stack = state.stack;
- var e2 = stack.pop();
- var e1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'EQ[]', e2, e1); }
-
- stack.push(e2 === e1 ? 1 : 0);
-}
-
-// NEQ[] Not EQual
-// 0x55
-function NEQ(state) {
- var stack = state.stack;
- var e2 = stack.pop();
- var e1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'NEQ[]', e2, e1); }
-
- stack.push(e2 !== e1 ? 1 : 0);
-}
-
-// ODD[] ODD
-// 0x56
-function ODD(state) {
- var stack = state.stack;
- var n = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'ODD[]', n); }
-
- stack.push(Math.trunc(n) % 2 ? 1 : 0);
-}
-
-// EVEN[] EVEN
-// 0x57
-function EVEN(state) {
- var stack = state.stack;
- var n = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'EVEN[]', n); }
-
- stack.push(Math.trunc(n) % 2 ? 0 : 1);
-}
-
-// IF[] IF test
-// 0x58
-function IF(state) {
- var test = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'IF[]', test); }
-
- // if test is true it just continues
- // if not the ip is skipped until matching ELSE or EIF
- if (!test) {
- skip(state, true);
-
- if (exports.DEBUG) { console.log(state.step, 'EIF[]'); }
- }
-}
-
-// EIF[] End IF
-// 0x59
-function EIF(state) {
- // this can be reached normally when
- // executing an else branch.
- // -> just ignore it
-
- if (exports.DEBUG) { console.log(state.step, 'EIF[]'); }
-}
-
-// AND[] logical AND
-// 0x5A
-function AND(state) {
- var stack = state.stack;
- var e2 = stack.pop();
- var e1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'AND[]', e2, e1); }
-
- stack.push(e2 && e1 ? 1 : 0);
-}
-
-// OR[] logical OR
-// 0x5B
-function OR(state) {
- var stack = state.stack;
- var e2 = stack.pop();
- var e1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'OR[]', e2, e1); }
-
- stack.push(e2 || e1 ? 1 : 0);
-}
-
-// NOT[] logical NOT
-// 0x5C
-function NOT(state) {
- var stack = state.stack;
- var e = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'NOT[]', e); }
-
- stack.push(e ? 0 : 1);
-}
-
-// DELTAP1[] DELTA exception P1
-// DELTAP2[] DELTA exception P2
-// DELTAP3[] DELTA exception P3
-// 0x5D, 0x71, 0x72
-function DELTAP123(b, state) {
- var stack = state.stack;
- var n = stack.pop();
- var fv = state.fv;
- var pv = state.pv;
- var ppem = state.ppem;
- var base = state.deltaBase + (b - 1) * 16;
- var ds = state.deltaShift;
- var z0 = state.z0;
-
- if (exports.DEBUG) { console.log(state.step, 'DELTAP[' + b + ']', n, stack); }
-
- for (var i = 0; i < n; i++) {
- var pi = stack.pop();
- var arg = stack.pop();
- var appem = base + ((arg & 0xF0) >> 4);
- if (appem !== ppem) { continue; }
-
- var mag = (arg & 0x0F) - 8;
- if (mag >= 0) { mag++; }
- if (exports.DEBUG) { console.log(state.step, 'DELTAPFIX', pi, 'by', mag * ds); }
-
- var p = z0[pi];
- fv.setRelative(p, p, mag * ds, pv);
- }
-}
-
-// SDB[] Set Delta Base in the graphics state
-// 0x5E
-function SDB(state) {
- var stack = state.stack;
- var n = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SDB[]', n); }
-
- state.deltaBase = n;
-}
-
-// SDS[] Set Delta Shift in the graphics state
-// 0x5F
-function SDS(state) {
- var stack = state.stack;
- var n = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SDS[]', n); }
-
- state.deltaShift = Math.pow(0.5, n);
-}
-
-// ADD[] ADD
-// 0x60
-function ADD(state) {
- var stack = state.stack;
- var n2 = stack.pop();
- var n1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'ADD[]', n2, n1); }
-
- stack.push(n1 + n2);
-}
-
-// SUB[] SUB
-// 0x61
-function SUB(state) {
- var stack = state.stack;
- var n2 = stack.pop();
- var n1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SUB[]', n2, n1); }
-
- stack.push(n1 - n2);
-}
-
-// DIV[] DIV
-// 0x62
-function DIV(state) {
- var stack = state.stack;
- var n2 = stack.pop();
- var n1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'DIV[]', n2, n1); }
-
- stack.push(n1 * 64 / n2);
-}
-
-// MUL[] MUL
-// 0x63
-function MUL(state) {
- var stack = state.stack;
- var n2 = stack.pop();
- var n1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'MUL[]', n2, n1); }
-
- stack.push(n1 * n2 / 64);
-}
-
-// ABS[] ABSolute value
-// 0x64
-function ABS(state) {
- var stack = state.stack;
- var n = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'ABS[]', n); }
-
- stack.push(Math.abs(n));
-}
-
-// NEG[] NEGate
-// 0x65
-function NEG(state) {
- var stack = state.stack;
- var n = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'NEG[]', n); }
-
- stack.push(-n);
-}
-
-// FLOOR[] FLOOR
-// 0x66
-function FLOOR(state) {
- var stack = state.stack;
- var n = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'FLOOR[]', n); }
-
- stack.push(Math.floor(n / 0x40) * 0x40);
-}
-
-// CEILING[] CEILING
-// 0x67
-function CEILING(state) {
- var stack = state.stack;
- var n = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'CEILING[]', n); }
-
- stack.push(Math.ceil(n / 0x40) * 0x40);
-}
-
-// ROUND[ab] ROUND value
-// 0x68-0x6B
-function ROUND(dt, state) {
- var stack = state.stack;
- var n = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'ROUND[]'); }
-
- stack.push(state.round(n / 0x40) * 0x40);
-}
-
-// WCVTF[] Write Control Value Table in Funits
-// 0x70
-function WCVTF(state) {
- var stack = state.stack;
- var v = stack.pop();
- var l = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'WCVTF[]', v, l); }
-
- state.cvt[l] = v * state.ppem / state.font.unitsPerEm;
-}
-
-// DELTAC1[] DELTA exception C1
-// DELTAC2[] DELTA exception C2
-// DELTAC3[] DELTA exception C3
-// 0x73, 0x74, 0x75
-function DELTAC123(b, state) {
- var stack = state.stack;
- var n = stack.pop();
- var ppem = state.ppem;
- var base = state.deltaBase + (b - 1) * 16;
- var ds = state.deltaShift;
-
- if (exports.DEBUG) { console.log(state.step, 'DELTAC[' + b + ']', n, stack); }
-
- for (var i = 0; i < n; i++) {
- var c = stack.pop();
- var arg = stack.pop();
- var appem = base + ((arg & 0xF0) >> 4);
- if (appem !== ppem) { continue; }
-
- var mag = (arg & 0x0F) - 8;
- if (mag >= 0) { mag++; }
-
- var delta = mag * ds;
-
- if (exports.DEBUG) { console.log(state.step, 'DELTACFIX', c, 'by', delta); }
-
- state.cvt[c] += delta;
- }
-}
-
-// SROUND[] Super ROUND
-// 0x76
-function SROUND(state) {
- var n = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'SROUND[]', n); }
-
- state.round = roundSuper;
-
- var period;
-
- switch (n & 0xC0) {
- case 0x00:
- period = 0.5;
- break;
- case 0x40:
- period = 1;
- break;
- case 0x80:
- period = 2;
- break;
- default:
- throw new Error('invalid SROUND value');
- }
-
- state.srPeriod = period;
-
- switch (n & 0x30) {
- case 0x00:
- state.srPhase = 0;
- break;
- case 0x10:
- state.srPhase = 0.25 * period;
- break;
- case 0x20:
- state.srPhase = 0.5 * period;
- break;
- case 0x30:
- state.srPhase = 0.75 * period;
- break;
- default: throw new Error('invalid SROUND value');
- }
-
- n &= 0x0F;
-
- if (n === 0) { state.srThreshold = 0; }
- else { state.srThreshold = (n / 8 - 0.5) * period; }
-}
-
-// S45ROUND[] Super ROUND 45 degrees
-// 0x77
-function S45ROUND(state) {
- var n = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'S45ROUND[]', n); }
-
- state.round = roundSuper;
-
- var period;
-
- switch (n & 0xC0) {
- case 0x00:
- period = Math.sqrt(2) / 2;
- break;
- case 0x40:
- period = Math.sqrt(2);
- break;
- case 0x80:
- period = 2 * Math.sqrt(2);
- break;
- default:
- throw new Error('invalid S45ROUND value');
- }
-
- state.srPeriod = period;
-
- switch (n & 0x30) {
- case 0x00:
- state.srPhase = 0;
- break;
- case 0x10:
- state.srPhase = 0.25 * period;
- break;
- case 0x20:
- state.srPhase = 0.5 * period;
- break;
- case 0x30:
- state.srPhase = 0.75 * period;
- break;
- default:
- throw new Error('invalid S45ROUND value');
- }
-
- n &= 0x0F;
-
- if (n === 0) { state.srThreshold = 0; }
- else { state.srThreshold = (n / 8 - 0.5) * period; }
-}
-
-// ROFF[] Round Off
-// 0x7A
-function ROFF(state) {
- if (exports.DEBUG) { console.log(state.step, 'ROFF[]'); }
-
- state.round = roundOff;
-}
-
-// RUTG[] Round Up To Grid
-// 0x7C
-function RUTG(state) {
- if (exports.DEBUG) { console.log(state.step, 'RUTG[]'); }
-
- state.round = roundUpToGrid;
-}
-
-// RDTG[] Round Down To Grid
-// 0x7D
-function RDTG(state) {
- if (exports.DEBUG) { console.log(state.step, 'RDTG[]'); }
-
- state.round = roundDownToGrid;
-}
-
-// SCANCTRL[] SCAN conversion ConTRoL
-// 0x85
-function SCANCTRL(state) {
- var n = state.stack.pop();
-
- // ignored by opentype.js
-
- if (exports.DEBUG) { console.log(state.step, 'SCANCTRL[]', n); }
-}
-
-// SDPVTL[a] Set Dual Projection Vector To Line
-// 0x86-0x87
-function SDPVTL(a, state) {
- var stack = state.stack;
- var p2i = stack.pop();
- var p1i = stack.pop();
- var p2 = state.z2[p2i];
- var p1 = state.z1[p1i];
-
- if (exports.DEBUG) { console.log(state.step, 'SDPVTL[' + a + ']', p2i, p1i); }
-
- var dx;
- var dy;
-
- if (!a) {
- dx = p1.x - p2.x;
- dy = p1.y - p2.y;
- } else {
- dx = p2.y - p1.y;
- dy = p1.x - p2.x;
- }
-
- state.dpv = getUnitVector(dx, dy);
-}
-
-// GETINFO[] GET INFOrmation
-// 0x88
-function GETINFO(state) {
- var stack = state.stack;
- var sel = stack.pop();
- var r = 0;
-
- if (exports.DEBUG) { console.log(state.step, 'GETINFO[]', sel); }
-
- // v35 as in no subpixel hinting
- if (sel & 0x01) { r = 35; }
-
- // TODO rotation and stretch currently not supported
- // and thus those GETINFO are always 0.
-
- // opentype.js is always gray scaling
- if (sel & 0x20) { r |= 0x1000; }
-
- stack.push(r);
-}
-
-// ROLL[] ROLL the top three stack elements
-// 0x8A
-function ROLL(state) {
- var stack = state.stack;
- var a = stack.pop();
- var b = stack.pop();
- var c = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'ROLL[]'); }
-
- stack.push(b);
- stack.push(a);
- stack.push(c);
-}
-
-// MAX[] MAXimum of top two stack elements
-// 0x8B
-function MAX(state) {
- var stack = state.stack;
- var e2 = stack.pop();
- var e1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'MAX[]', e2, e1); }
-
- stack.push(Math.max(e1, e2));
-}
-
-// MIN[] MINimum of top two stack elements
-// 0x8C
-function MIN(state) {
- var stack = state.stack;
- var e2 = stack.pop();
- var e1 = stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'MIN[]', e2, e1); }
-
- stack.push(Math.min(e1, e2));
-}
-
-// SCANTYPE[] SCANTYPE
-// 0x8D
-function SCANTYPE(state) {
- var n = state.stack.pop();
- // ignored by opentype.js
- if (exports.DEBUG) { console.log(state.step, 'SCANTYPE[]', n); }
-}
-
-// INSTCTRL[] INSTCTRL
-// 0x8D
-function INSTCTRL(state) {
- var s = state.stack.pop();
- var v = state.stack.pop();
-
- if (exports.DEBUG) { console.log(state.step, 'INSTCTRL[]', s, v); }
-
- switch (s) {
- case 1 : state.inhibitGridFit = !!v; return;
- case 2 : state.ignoreCvt = !!v; return;
- default: throw new Error('invalid INSTCTRL[] selector');
- }
-}
-
-// PUSHB[abc] PUSH Bytes
-// 0xB0-0xB7
-function PUSHB(n, state) {
- var stack = state.stack;
- var prog = state.prog;
- var ip = state.ip;
-
- if (exports.DEBUG) { console.log(state.step, 'PUSHB[' + n + ']'); }
-
- for (var i = 0; i < n; i++) { stack.push(prog[++ip]); }
-
- state.ip = ip;
-}
-
-// PUSHW[abc] PUSH Words
-// 0xB8-0xBF
-function PUSHW(n, state) {
- var ip = state.ip;
- var prog = state.prog;
- var stack = state.stack;
-
- if (exports.DEBUG) { console.log(state.ip, 'PUSHW[' + n + ']'); }
-
- for (var i = 0; i < n; i++) {
- var w = (prog[++ip] << 8) | prog[++ip];
- if (w & 0x8000) { w = -((w ^ 0xffff) + 1); }
- stack.push(w);
- }
-
- state.ip = ip;
-}
-
-// MDRP[abcde] Move Direct Relative Point
-// 0xD0-0xEF
-// (if indirect is 0)
-//
-// and
-//
-// MIRP[abcde] Move Indirect Relative Point
-// 0xE0-0xFF
-// (if indirect is 1)
-
-function MDRP_MIRP(indirect, setRp0, keepD, ro, dt, state) {
- var stack = state.stack;
- var cvte = indirect && stack.pop();
- var pi = stack.pop();
- var rp0i = state.rp0;
- var rp = state.z0[rp0i];
- var p = state.z1[pi];
-
- var md = state.minDis;
- var fv = state.fv;
- var pv = state.dpv;
- var od; // original distance
- var d; // moving distance
- var sign; // sign of distance
- var cv;
-
- d = od = pv.distance(p, rp, true, true);
- sign = d >= 0 ? 1 : -1; // Math.sign would be 0 in case of 0
-
- // TODO consider autoFlip
- d = Math.abs(d);
-
- if (indirect) {
- cv = state.cvt[cvte];
-
- if (ro && Math.abs(d - cv) < state.cvCutIn) { d = cv; }
- }
-
- if (keepD && d < md) { d = md; }
-
- if (ro) { d = state.round(d); }
-
- fv.setRelative(p, rp, sign * d, pv);
- fv.touch(p);
-
- if (exports.DEBUG) {
- console.log(
- state.step,
- (indirect ? 'MIRP[' : 'MDRP[') +
- (setRp0 ? 'M' : 'm') +
- (keepD ? '>' : '_') +
- (ro ? 'R' : '_') +
- (dt === 0 ? 'Gr' : (dt === 1 ? 'Bl' : (dt === 2 ? 'Wh' : ''))) +
- ']',
- indirect ?
- cvte + '(' + state.cvt[cvte] + ',' + cv + ')' :
- '',
- pi,
- '(d =', od, '->', sign * d, ')'
- );
- }
-
- state.rp1 = state.rp0;
- state.rp2 = pi;
- if (setRp0) { state.rp0 = pi; }
-}
-
-/*
-* The instruction table.
-*/
-instructionTable = [
- /* 0x00 */ SVTCA.bind(undefined, yUnitVector),
- /* 0x01 */ SVTCA.bind(undefined, xUnitVector),
- /* 0x02 */ SPVTCA.bind(undefined, yUnitVector),
- /* 0x03 */ SPVTCA.bind(undefined, xUnitVector),
- /* 0x04 */ SFVTCA.bind(undefined, yUnitVector),
- /* 0x05 */ SFVTCA.bind(undefined, xUnitVector),
- /* 0x06 */ SPVTL.bind(undefined, 0),
- /* 0x07 */ SPVTL.bind(undefined, 1),
- /* 0x08 */ SFVTL.bind(undefined, 0),
- /* 0x09 */ SFVTL.bind(undefined, 1),
- /* 0x0A */ SPVFS,
- /* 0x0B */ SFVFS,
- /* 0x0C */ GPV,
- /* 0x0D */ GFV,
- /* 0x0E */ SFVTPV,
- /* 0x0F */ ISECT,
- /* 0x10 */ SRP0,
- /* 0x11 */ SRP1,
- /* 0x12 */ SRP2,
- /* 0x13 */ SZP0,
- /* 0x14 */ SZP1,
- /* 0x15 */ SZP2,
- /* 0x16 */ SZPS,
- /* 0x17 */ SLOOP,
- /* 0x18 */ RTG,
- /* 0x19 */ RTHG,
- /* 0x1A */ SMD,
- /* 0x1B */ ELSE,
- /* 0x1C */ JMPR,
- /* 0x1D */ SCVTCI,
- /* 0x1E */ undefined, // TODO SSWCI
- /* 0x1F */ undefined, // TODO SSW
- /* 0x20 */ DUP,
- /* 0x21 */ POP,
- /* 0x22 */ CLEAR,
- /* 0x23 */ SWAP,
- /* 0x24 */ DEPTH,
- /* 0x25 */ CINDEX,
- /* 0x26 */ MINDEX,
- /* 0x27 */ undefined, // TODO ALIGNPTS
- /* 0x28 */ undefined,
- /* 0x29 */ undefined, // TODO UTP
- /* 0x2A */ LOOPCALL,
- /* 0x2B */ CALL,
- /* 0x2C */ FDEF,
- /* 0x2D */ undefined, // ENDF (eaten by FDEF)
- /* 0x2E */ MDAP.bind(undefined, 0),
- /* 0x2F */ MDAP.bind(undefined, 1),
- /* 0x30 */ IUP.bind(undefined, yUnitVector),
- /* 0x31 */ IUP.bind(undefined, xUnitVector),
- /* 0x32 */ SHP.bind(undefined, 0),
- /* 0x33 */ SHP.bind(undefined, 1),
- /* 0x34 */ SHC.bind(undefined, 0),
- /* 0x35 */ SHC.bind(undefined, 1),
- /* 0x36 */ SHZ.bind(undefined, 0),
- /* 0x37 */ SHZ.bind(undefined, 1),
- /* 0x38 */ SHPIX,
- /* 0x39 */ IP,
- /* 0x3A */ MSIRP.bind(undefined, 0),
- /* 0x3B */ MSIRP.bind(undefined, 1),
- /* 0x3C */ ALIGNRP,
- /* 0x3D */ RTDG,
- /* 0x3E */ MIAP.bind(undefined, 0),
- /* 0x3F */ MIAP.bind(undefined, 1),
- /* 0x40 */ NPUSHB,
- /* 0x41 */ NPUSHW,
- /* 0x42 */ WS,
- /* 0x43 */ RS,
- /* 0x44 */ WCVTP,
- /* 0x45 */ RCVT,
- /* 0x46 */ GC.bind(undefined, 0),
- /* 0x47 */ GC.bind(undefined, 1),
- /* 0x48 */ undefined, // TODO SCFS
- /* 0x49 */ MD.bind(undefined, 0),
- /* 0x4A */ MD.bind(undefined, 1),
- /* 0x4B */ MPPEM,
- /* 0x4C */ undefined, // TODO MPS
- /* 0x4D */ FLIPON,
- /* 0x4E */ undefined, // TODO FLIPOFF
- /* 0x4F */ undefined, // TODO DEBUG
- /* 0x50 */ LT,
- /* 0x51 */ LTEQ,
- /* 0x52 */ GT,
- /* 0x53 */ GTEQ,
- /* 0x54 */ EQ,
- /* 0x55 */ NEQ,
- /* 0x56 */ ODD,
- /* 0x57 */ EVEN,
- /* 0x58 */ IF,
- /* 0x59 */ EIF,
- /* 0x5A */ AND,
- /* 0x5B */ OR,
- /* 0x5C */ NOT,
- /* 0x5D */ DELTAP123.bind(undefined, 1),
- /* 0x5E */ SDB,
- /* 0x5F */ SDS,
- /* 0x60 */ ADD,
- /* 0x61 */ SUB,
- /* 0x62 */ DIV,
- /* 0x63 */ MUL,
- /* 0x64 */ ABS,
- /* 0x65 */ NEG,
- /* 0x66 */ FLOOR,
- /* 0x67 */ CEILING,
- /* 0x68 */ ROUND.bind(undefined, 0),
- /* 0x69 */ ROUND.bind(undefined, 1),
- /* 0x6A */ ROUND.bind(undefined, 2),
- /* 0x6B */ ROUND.bind(undefined, 3),
- /* 0x6C */ undefined, // TODO NROUND[ab]
- /* 0x6D */ undefined, // TODO NROUND[ab]
- /* 0x6E */ undefined, // TODO NROUND[ab]
- /* 0x6F */ undefined, // TODO NROUND[ab]
- /* 0x70 */ WCVTF,
- /* 0x71 */ DELTAP123.bind(undefined, 2),
- /* 0x72 */ DELTAP123.bind(undefined, 3),
- /* 0x73 */ DELTAC123.bind(undefined, 1),
- /* 0x74 */ DELTAC123.bind(undefined, 2),
- /* 0x75 */ DELTAC123.bind(undefined, 3),
- /* 0x76 */ SROUND,
- /* 0x77 */ S45ROUND,
- /* 0x78 */ undefined, // TODO JROT[]
- /* 0x79 */ undefined, // TODO JROF[]
- /* 0x7A */ ROFF,
- /* 0x7B */ undefined,
- /* 0x7C */ RUTG,
- /* 0x7D */ RDTG,
- /* 0x7E */ POP, // actually SANGW, supposed to do only a pop though
- /* 0x7F */ POP, // actually AA, supposed to do only a pop though
- /* 0x80 */ undefined, // TODO FLIPPT
- /* 0x81 */ undefined, // TODO FLIPRGON
- /* 0x82 */ undefined, // TODO FLIPRGOFF
- /* 0x83 */ undefined,
- /* 0x84 */ undefined,
- /* 0x85 */ SCANCTRL,
- /* 0x86 */ SDPVTL.bind(undefined, 0),
- /* 0x87 */ SDPVTL.bind(undefined, 1),
- /* 0x88 */ GETINFO,
- /* 0x89 */ undefined, // TODO IDEF
- /* 0x8A */ ROLL,
- /* 0x8B */ MAX,
- /* 0x8C */ MIN,
- /* 0x8D */ SCANTYPE,
- /* 0x8E */ INSTCTRL,
- /* 0x8F */ undefined,
- /* 0x90 */ undefined,
- /* 0x91 */ undefined,
- /* 0x92 */ undefined,
- /* 0x93 */ undefined,
- /* 0x94 */ undefined,
- /* 0x95 */ undefined,
- /* 0x96 */ undefined,
- /* 0x97 */ undefined,
- /* 0x98 */ undefined,
- /* 0x99 */ undefined,
- /* 0x9A */ undefined,
- /* 0x9B */ undefined,
- /* 0x9C */ undefined,
- /* 0x9D */ undefined,
- /* 0x9E */ undefined,
- /* 0x9F */ undefined,
- /* 0xA0 */ undefined,
- /* 0xA1 */ undefined,
- /* 0xA2 */ undefined,
- /* 0xA3 */ undefined,
- /* 0xA4 */ undefined,
- /* 0xA5 */ undefined,
- /* 0xA6 */ undefined,
- /* 0xA7 */ undefined,
- /* 0xA8 */ undefined,
- /* 0xA9 */ undefined,
- /* 0xAA */ undefined,
- /* 0xAB */ undefined,
- /* 0xAC */ undefined,
- /* 0xAD */ undefined,
- /* 0xAE */ undefined,
- /* 0xAF */ undefined,
- /* 0xB0 */ PUSHB.bind(undefined, 1),
- /* 0xB1 */ PUSHB.bind(undefined, 2),
- /* 0xB2 */ PUSHB.bind(undefined, 3),
- /* 0xB3 */ PUSHB.bind(undefined, 4),
- /* 0xB4 */ PUSHB.bind(undefined, 5),
- /* 0xB5 */ PUSHB.bind(undefined, 6),
- /* 0xB6 */ PUSHB.bind(undefined, 7),
- /* 0xB7 */ PUSHB.bind(undefined, 8),
- /* 0xB8 */ PUSHW.bind(undefined, 1),
- /* 0xB9 */ PUSHW.bind(undefined, 2),
- /* 0xBA */ PUSHW.bind(undefined, 3),
- /* 0xBB */ PUSHW.bind(undefined, 4),
- /* 0xBC */ PUSHW.bind(undefined, 5),
- /* 0xBD */ PUSHW.bind(undefined, 6),
- /* 0xBE */ PUSHW.bind(undefined, 7),
- /* 0xBF */ PUSHW.bind(undefined, 8),
- /* 0xC0 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 0),
- /* 0xC1 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 1),
- /* 0xC2 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 2),
- /* 0xC3 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 3),
- /* 0xC4 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 0),
- /* 0xC5 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 1),
- /* 0xC6 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 2),
- /* 0xC7 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 3),
- /* 0xC8 */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 0),
- /* 0xC9 */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 1),
- /* 0xCA */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 2),
- /* 0xCB */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 3),
- /* 0xCC */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 0),
- /* 0xCD */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 1),
- /* 0xCE */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 2),
- /* 0xCF */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 3),
- /* 0xD0 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 0),
- /* 0xD1 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 1),
- /* 0xD2 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 2),
- /* 0xD3 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 3),
- /* 0xD4 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 0),
- /* 0xD5 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 1),
- /* 0xD6 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 2),
- /* 0xD7 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 3),
- /* 0xD8 */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 0),
- /* 0xD9 */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 1),
- /* 0xDA */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 2),
- /* 0xDB */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 3),
- /* 0xDC */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 0),
- /* 0xDD */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 1),
- /* 0xDE */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 2),
- /* 0xDF */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 3),
- /* 0xE0 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 0),
- /* 0xE1 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 1),
- /* 0xE2 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 2),
- /* 0xE3 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 3),
- /* 0xE4 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 0),
- /* 0xE5 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 1),
- /* 0xE6 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 2),
- /* 0xE7 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 3),
- /* 0xE8 */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 0),
- /* 0xE9 */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 1),
- /* 0xEA */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 2),
- /* 0xEB */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 3),
- /* 0xEC */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 0),
- /* 0xED */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 1),
- /* 0xEE */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 2),
- /* 0xEF */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 3),
- /* 0xF0 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 0),
- /* 0xF1 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 1),
- /* 0xF2 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 2),
- /* 0xF3 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 3),
- /* 0xF4 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 0),
- /* 0xF5 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 1),
- /* 0xF6 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 2),
- /* 0xF7 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 3),
- /* 0xF8 */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 0),
- /* 0xF9 */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 1),
- /* 0xFA */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 2),
- /* 0xFB */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 3),
- /* 0xFC */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 0),
- /* 0xFD */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 1),
- /* 0xFE */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 2),
- /* 0xFF */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 3)
-];
-
-/*****************************
- Mathematical Considerations
-******************************
-
-fv ... refers to freedom vector
-pv ... refers to projection vector
-rp ... refers to reference point
-p ... refers to to point being operated on
-d ... refers to distance
-
-SETRELATIVE:
-============
-
-case freedom vector == x-axis:
-------------------------------
-
- (pv)
- .-'
- rpd .-'
- .-*
- d .-'90°'
- .-' '
- .-' '
- *-' ' b
- rp '
- '
- '
- p *----------*-------------- (fv)
- pm
-
- rpdx = rpx + d * pv.x
- rpdy = rpy + d * pv.y
-
- equation of line b
-
- y - rpdy = pvns * (x- rpdx)
-
- y = p.y
-
- x = rpdx + ( p.y - rpdy ) / pvns
-
-
-case freedom vector == y-axis:
-------------------------------
-
- * pm
- |\
- | \
- | \
- | \
- | \
- | \
- | \
- | \
- | \
- | \ b
- | \
- | \
- | \ .-' (pv)
- | 90° \.-'
- | .-'* rpd
- | .-'
- * *-' d
- p rp
-
- rpdx = rpx + d * pv.x
- rpdy = rpy + d * pv.y
-
- equation of line b:
- pvns ... normal slope to pv
-
- y - rpdy = pvns * (x - rpdx)
-
- x = p.x
-
- y = rpdy + pvns * (p.x - rpdx)
-
-
-
-generic case:
--------------
-
-
- .'(fv)
- .'
- .* pm
- .' !
- .' .
- .' !
- .' . b
- .' !
- * .
- p !
- 90° . ... (pv)
- ...-*-'''
- ...---''' rpd
- ...---''' d
- *--'''
- rp
-
- rpdx = rpx + d * pv.x
- rpdy = rpy + d * pv.y
-
- equation of line b:
- pvns... normal slope to pv
-
- y - rpdy = pvns * (x - rpdx)
-
- equation of freedom vector line:
- fvs ... slope of freedom vector (=fy/fx)
-
- y - py = fvs * (x - px)
-
-
- on pm both equations are true for same x/y
-
- y - rpdy = pvns * (x - rpdx)
-
- y - py = fvs * (x - px)
-
- form to y and set equal:
-
- pvns * (x - rpdx) + rpdy = fvs * (x - px) + py
-
- expand:
-
- pvns * x - pvns * rpdx + rpdy = fvs * x - fvs * px + py
-
- switch:
-
- fvs * x - fvs * px + py = pvns * x - pvns * rpdx + rpdy
-
- solve for x:
-
- fvs * x - pvns * x = fvs * px - pvns * rpdx - py + rpdy
-
-
-
- fvs * px - pvns * rpdx + rpdy - py
- x = -----------------------------------
- fvs - pvns
-
- and:
-
- y = fvs * (x - px) + py
-
-
-
-INTERPOLATE:
-============
-
-Examples of point interpolation.
-
-The weight of the movement of the reference point gets bigger
-the further the other reference point is away, thus the safest
-option (that is avoiding 0/0 divisions) is to weight the
-original distance of the other point by the sum of both distances.
-
-If the sum of both distances is 0, then move the point by the
-arithmetic average of the movement of both reference points.
-
-
-
-
- (+6)
- rp1o *---->*rp1
- . . (+12)
- . . rp2o *---------->* rp2
- . . . .
- . . . .
- . 10 20 . .
- |.........|...................| .
- . . .
- . . (+8) .
- po *------>*p .
- . . .
- . 12 . 24 .
- |...........|.......................|
- 36
-
-
--------
-
-
-
- (+10)
- rp1o *-------->*rp1
- . . (-10)
- . . rp2 *<---------* rpo2
- . . . .
- . . . .
- . 10 . 30 . .
- |.........|.............................|
- . .
- . (+5) .
- po *--->* p .
- . . .
- . . 20 .
- |....|..............|
- 5 15
-
-
--------
-
-
- (+10)
- rp1o *-------->*rp1
- . .
- . .
- rp2o *-------->*rp2
-
-
- (+10)
- po *-------->* p
-
--------
-
-
- (+10)
- rp1o *-------->*rp1
- . .
- . .(+30)
- rp2o *---------------------------->*rp2
-
-
- (+25)
- po *----------------------->* p
-
-
-
-vim: set ts=4 sw=4 expandtab:
-*****/
-
-/**
- * Converts a string into a list of tokens.
- */
-
-/**
- * Create a new token
- * @param {string} char a single char
- */
-function Token(char) {
- this.char = char;
- this.state = {};
- this.activeState = null;
-}
-
-/**
- * Create a new context range
- * @param {number} startIndex range start index
- * @param {number} endOffset range end index offset
- * @param {string} contextName owner context name
- */
-function ContextRange(startIndex, endOffset, contextName) {
- this.contextName = contextName;
- this.startIndex = startIndex;
- this.endOffset = endOffset;
-}
-
-/**
- * Check context start and end
- * @param {string} contextName a unique context name
- * @param {function} checkStart a predicate function the indicates a context's start
- * @param {function} checkEnd a predicate function the indicates a context's end
- */
-function ContextChecker(contextName, checkStart, checkEnd) {
- this.contextName = contextName;
- this.openRange = null;
- this.ranges = [];
- this.checkStart = checkStart;
- this.checkEnd = checkEnd;
-}
-
-/**
- * @typedef ContextParams
- * @type Object
- * @property {array} context context items
- * @property {number} currentIndex current item index
- */
-
-/**
- * Create a context params
- * @param {array} context a list of items
- * @param {number} currentIndex current item index
- */
-function ContextParams(context, currentIndex) {
- this.context = context;
- this.index = currentIndex;
- this.length = context.length;
- this.current = context[currentIndex];
- this.backtrack = context.slice(0, currentIndex);
- this.lookahead = context.slice(currentIndex + 1);
-}
-
-/**
- * Create an event instance
- * @param {string} eventId event unique id
- */
-function Event(eventId) {
- this.eventId = eventId;
- this.subscribers = [];
-}
-
-/**
- * Initialize a core events and auto subscribe required event handlers
- * @param {any} events an object that enlists core events handlers
- */
-function initializeCoreEvents(events) {
- var this$1 = this;
-
- var coreEvents = [
- 'start', 'end', 'next', 'newToken', 'contextStart',
- 'contextEnd', 'insertToken', 'removeToken', 'removeRange',
- 'replaceToken', 'replaceRange', 'composeRUD', 'updateContextsRanges'
- ];
-
- coreEvents.forEach(function (eventId) {
- Object.defineProperty(this$1.events, eventId, {
- value: new Event(eventId)
- });
- });
-
- if (!!events) {
- coreEvents.forEach(function (eventId) {
- var event = events[eventId];
- if (typeof event === 'function') {
- this$1.events[eventId].subscribe(event);
- }
- });
- }
- var requiresContextUpdate = [
- 'insertToken', 'removeToken', 'removeRange',
- 'replaceToken', 'replaceRange', 'composeRUD'
- ];
- requiresContextUpdate.forEach(function (eventId) {
- this$1.events[eventId].subscribe(
- this$1.updateContextsRanges
- );
- });
-}
-
-/**
- * Converts a string into a list of tokens
- * @param {any} events tokenizer core events
- */
-function Tokenizer(events) {
- this.tokens = [];
- this.registeredContexts = {};
- this.contextCheckers = [];
- this.events = {};
- this.registeredModifiers = [];
-
- initializeCoreEvents.call(this, events);
-}
-
-/**
- * Sets the state of a token, usually called by a state modifier.
- * @param {string} key state item key
- * @param {any} value state item value
- */
-Token.prototype.setState = function(key, value) {
- this.state[key] = value;
- this.activeState = { key: key, value: this.state[key] };
- return this.activeState;
-};
-
-Token.prototype.getState = function (stateId) {
- return this.state[stateId] || null;
-};
-
-/**
- * Checks if an index exists in the tokens list.
- * @param {number} index token index
- */
-Tokenizer.prototype.inboundIndex = function(index) {
- return index >= 0 && index < this.tokens.length;
-};
-
-/**
- * Compose and apply a list of operations (replace, update, delete)
- * @param {array} RUDs replace, update and delete operations
- * TODO: Perf. Optimization (lengthBefore === lengthAfter ? dispatch once)
- */
-Tokenizer.prototype.composeRUD = function (RUDs) {
- var this$1 = this;
-
- var silent = true;
- var state = RUDs.map(function (RUD) { return (
- this$1[RUD[0]].apply(this$1, RUD.slice(1).concat(silent))
- ); });
- var hasFAILObject = function (obj) { return (
- typeof obj === 'object' &&
- obj.hasOwnProperty('FAIL')
- ); };
- if (state.every(hasFAILObject)) {
- return {
- FAIL: "composeRUD: one or more operations hasn't completed successfully",
- report: state.filter(hasFAILObject)
- };
- }
- this.dispatch('composeRUD', [state.filter(function (op) { return !hasFAILObject(op); })]);
-};
-
-/**
- * Replace a range of tokens with a list of tokens
- * @param {number} startIndex range start index
- * @param {number} offset range offset
- * @param {token} tokens a list of tokens to replace
- * @param {boolean} silent dispatch events and update context ranges
- */
-Tokenizer.prototype.replaceRange = function (startIndex, offset, tokens, silent) {
- offset = offset !== null ? offset : this.tokens.length;
- var isTokenType = tokens.every(function (token) { return token instanceof Token; });
- if (!isNaN(startIndex) && this.inboundIndex(startIndex) && isTokenType) {
- var replaced = this.tokens.splice.apply(
- this.tokens, [startIndex, offset].concat(tokens)
- );
- if (!silent) { this.dispatch('replaceToken', [startIndex, offset, tokens]); }
- return [replaced, tokens];
- } else {
- return { FAIL: 'replaceRange: invalid tokens or startIndex.' };
- }
-};
-
-/**
- * Replace a token with another token
- * @param {number} index token index
- * @param {token} token a token to replace
- * @param {boolean} silent dispatch events and update context ranges
- */
-Tokenizer.prototype.replaceToken = function (index, token, silent) {
- if (!isNaN(index) && this.inboundIndex(index) && token instanceof Token) {
- var replaced = this.tokens.splice(index, 1, token);
- if (!silent) { this.dispatch('replaceToken', [index, token]); }
- return [replaced[0], token];
- } else {
- return { FAIL: 'replaceToken: invalid token or index.' };
- }
-};
-
-/**
- * Removes a range of tokens
- * @param {number} startIndex range start index
- * @param {number} offset range offset
- * @param {boolean} silent dispatch events and update context ranges
- */
-Tokenizer.prototype.removeRange = function(startIndex, offset, silent) {
- offset = !isNaN(offset) ? offset : this.tokens.length;
- var tokens = this.tokens.splice(startIndex, offset);
- if (!silent) { this.dispatch('removeRange', [tokens, startIndex, offset]); }
- return tokens;
-};
-
-/**
- * Remove a token at a certain index
- * @param {number} index token index
- * @param {boolean} silent dispatch events and update context ranges
- */
-Tokenizer.prototype.removeToken = function(index, silent) {
- if (!isNaN(index) && this.inboundIndex(index)) {
- var token = this.tokens.splice(index, 1);
- if (!silent) { this.dispatch('removeToken', [token, index]); }
- return token;
- } else {
- return { FAIL: 'removeToken: invalid token index.' };
- }
-};
-
-/**
- * Insert a list of tokens at a certain index
- * @param {array} tokens a list of tokens to insert
- * @param {number} index insert the list of tokens at index
- * @param {boolean} silent dispatch events and update context ranges
- */
-Tokenizer.prototype.insertToken = function (tokens, index, silent) {
- var tokenType = tokens.every(
- function (token) { return token instanceof Token; }
- );
- if (tokenType) {
- this.tokens.splice.apply(
- this.tokens, [index, 0].concat(tokens)
- );
- if (!silent) { this.dispatch('insertToken', [tokens, index]); }
- return tokens;
- } else {
- return { FAIL: 'insertToken: invalid token(s).' };
- }
-};
-
-/**
- * A state modifier that is called on 'newToken' event
- * @param {string} modifierId state modifier id
- * @param {function} condition a predicate function that returns true or false
- * @param {function} modifier a function to update token state
- */
-Tokenizer.prototype.registerModifier = function(modifierId, condition, modifier) {
- this.events.newToken.subscribe(function(token, contextParams) {
- var conditionParams = [token, contextParams];
- var canApplyModifier = (
- condition === null ||
- condition.apply(this, conditionParams) === true
- );
- var modifierParams = [token, contextParams];
- if (canApplyModifier) {
- var newStateValue = modifier.apply(this, modifierParams);
- token.setState(modifierId, newStateValue);
- }
- });
- this.registeredModifiers.push(modifierId);
-};
-
-/**
- * Subscribe a handler to an event
- * @param {function} eventHandler an event handler function
- */
-Event.prototype.subscribe = function (eventHandler) {
- if (typeof eventHandler === 'function') {
- return ((this.subscribers.push(eventHandler)) - 1);
- } else {
- return { FAIL: ("invalid '" + (this.eventId) + "' event handler")};
- }
-};
-
-/**
- * Unsubscribe an event handler
- * @param {string} subsId subscription id
- */
-Event.prototype.unsubscribe = function (subsId) {
- this.subscribers.splice(subsId, 1);
-};
-
-/**
- * Sets context params current value index
- * @param {number} index context params current value index
- */
-ContextParams.prototype.setCurrentIndex = function(index) {
- this.index = index;
- this.current = this.context[index];
- this.backtrack = this.context.slice(0, index);
- this.lookahead = this.context.slice(index + 1);
-};
-
-/**
- * Get an item at an offset from the current value
- * example (current value is 3):
- * 1 2 [3] 4 5 | items values
- * -2 -1 0 1 2 | offset values
- * @param {number} offset an offset from current value index
- */
-ContextParams.prototype.get = function (offset) {
- switch (true) {
- case (offset === 0):
- return this.current;
- case (offset < 0 && Math.abs(offset) <= this.backtrack.length):
- return this.backtrack.slice(offset)[0];
- case (offset > 0 && offset <= this.lookahead.length):
- return this.lookahead[offset - 1];
- default:
- return null;
- }
-};
-
-/**
- * Converts a context range into a string value
- * @param {contextRange} range a context range
- */
-Tokenizer.prototype.rangeToText = function (range) {
- if (range instanceof ContextRange) {
- return (
- this.getRangeTokens(range)
- .map(function (token) { return token.char; }).join('')
- );
- }
-};
-
-/**
- * Converts all tokens into a string
- */
-Tokenizer.prototype.getText = function () {
- return this.tokens.map(function (token) { return token.char; }).join('');
-};
-
-/**
- * Get a context by name
- * @param {string} contextName context name to get
- */
-Tokenizer.prototype.getContext = function (contextName) {
- var context = this.registeredContexts[contextName];
- return !!context ? context : null;
-};
-
-/**
- * Subscribes a new event handler to an event
- * @param {string} eventName event name to subscribe to
- * @param {function} eventHandler a function to be invoked on event
- */
-Tokenizer.prototype.on = function(eventName, eventHandler) {
- var event = this.events[eventName];
- if (!!event) {
- return event.subscribe(eventHandler);
- } else {
- return null;
- }
-};
-
-/**
- * Dispatches an event
- * @param {string} eventName event name
- * @param {any} args event handler arguments
- */
-Tokenizer.prototype.dispatch = function(eventName, args) {
- var this$1 = this;
-
- var event = this.events[eventName];
- if (event instanceof Event) {
- event.subscribers.forEach(function (subscriber) {
- subscriber.apply(this$1, args || []);
- });
- }
-};
-
-/**
- * Register a new context checker
- * @param {string} contextName a unique context name
- * @param {function} contextStartCheck a predicate function that returns true on context start
- * @param {function} contextEndCheck a predicate function that returns true on context end
- * TODO: call tokenize on registration to update context ranges with the new context.
- */
-Tokenizer.prototype.registerContextChecker = function(contextName, contextStartCheck, contextEndCheck) {
- if (!!this.getContext(contextName)) { return {
- FAIL:
- ("context name '" + contextName + "' is already registered.")
- }; }
- if (typeof contextStartCheck !== 'function') { return {
- FAIL:
- "missing context start check."
- }; }
- if (typeof contextEndCheck !== 'function') { return {
- FAIL:
- "missing context end check."
- }; }
- var contextCheckers = new ContextChecker(
- contextName, contextStartCheck, contextEndCheck
- );
- this.registeredContexts[contextName] = contextCheckers;
- this.contextCheckers.push(contextCheckers);
- return contextCheckers;
-};
-
-/**
- * Gets a context range tokens
- * @param {contextRange} range a context range
- */
-Tokenizer.prototype.getRangeTokens = function(range) {
- var endIndex = range.startIndex + range.endOffset;
- return [].concat(
- this.tokens
- .slice(range.startIndex, endIndex)
- );
-};
-
-/**
- * Gets the ranges of a context
- * @param {string} contextName context name
- */
-Tokenizer.prototype.getContextRanges = function(contextName) {
- var context = this.getContext(contextName);
- if (!!context) {
- return context.ranges;
- } else {
- return { FAIL: ("context checker '" + contextName + "' is not registered.") };
- }
-};
-
-/**
- * Resets context ranges to run context update
- */
-Tokenizer.prototype.resetContextsRanges = function () {
- var registeredContexts = this.registeredContexts;
- for (var contextName in registeredContexts) {
- if (registeredContexts.hasOwnProperty(contextName)) {
- var context = registeredContexts[contextName];
- context.ranges = [];
- }
- }
-};
-
-/**
- * Updates context ranges
- */
-Tokenizer.prototype.updateContextsRanges = function () {
- this.resetContextsRanges();
- var chars = this.tokens.map(function (token) { return token.char; });
- for (var i = 0; i < chars.length; i++) {
- var contextParams = new ContextParams(chars, i);
- this.runContextCheck(contextParams);
- }
- this.dispatch('updateContextsRanges', [this.registeredContexts]);
-};
-
-/**
- * Sets the end offset of an open range
- * @param {number} offset range end offset
- * @param {string} contextName context name
- */
-Tokenizer.prototype.setEndOffset = function (offset, contextName) {
- var startIndex = this.getContext(contextName).openRange.startIndex;
- var range = new ContextRange(startIndex, offset, contextName);
- var ranges = this.getContext(contextName).ranges;
- range.rangeId = contextName + "." + (ranges.length);
- ranges.push(range);
- this.getContext(contextName).openRange = null;
- return range;
-};
-
-/**
- * Runs a context check on the current context
- * @param {contextParams} contextParams current context params
- */
-Tokenizer.prototype.runContextCheck = function(contextParams) {
- var this$1 = this;
-
- var index = contextParams.index;
- this.contextCheckers.forEach(function (contextChecker) {
- var contextName = contextChecker.contextName;
- var openRange = this$1.getContext(contextName).openRange;
- if (!openRange && contextChecker.checkStart(contextParams)) {
- openRange = new ContextRange(index, null, contextName);
- this$1.getContext(contextName).openRange = openRange;
- this$1.dispatch('contextStart', [contextName, index]);
- }
- if (!!openRange && contextChecker.checkEnd(contextParams)) {
- var offset = (index - openRange.startIndex) + 1;
- var range = this$1.setEndOffset(offset, contextName);
- this$1.dispatch('contextEnd', [contextName, range]);
- }
- });
-};
-
-/**
- * Converts a text into a list of tokens
- * @param {string} text a text to tokenize
- */
-Tokenizer.prototype.tokenize = function (text) {
- this.tokens = [];
- this.resetContextsRanges();
- var chars = Array.from(text);
- this.dispatch('start');
- for (var i = 0; i < chars.length; i++) {
- var char = chars[i];
- var contextParams = new ContextParams(chars, i);
- this.dispatch('next', [contextParams]);
- this.runContextCheck(contextParams);
- var token = new Token(char);
- this.tokens.push(token);
- this.dispatch('newToken', [token, contextParams]);
- }
- this.dispatch('end', [this.tokens]);
- return this.tokens;
-};
-
-// ╭─┄┄┄────────────────────────┄─────────────────────────────────────────────╮
-// ┊ Character Class Assertions ┊ Checks if a char belongs to a certain class ┊
-// ╰─╾──────────────────────────┄─────────────────────────────────────────────╯
-// jscs:disable maximumLineLength
-/**
- * Check if a char is Arabic
- * @param {string} c a single char
- */
-function isArabicChar(c) {
- return /[\u0600-\u065F\u066A-\u06D2\u06FA-\u06FF]/.test(c);
-}
-
-/**
- * Check if a char is an isolated arabic char
- * @param {string} c a single char
- */
-function isIsolatedArabicChar(char) {
- return /[\u0630\u0690\u0621\u0631\u0661\u0671\u0622\u0632\u0672\u0692\u06C2\u0623\u0673\u0693\u06C3\u0624\u0694\u06C4\u0625\u0675\u0695\u06C5\u06E5\u0676\u0696\u06C6\u0627\u0677\u0697\u06C7\u0648\u0688\u0698\u06C8\u0689\u0699\u06C9\u068A\u06CA\u066B\u068B\u06CB\u068C\u068D\u06CD\u06FD\u068E\u06EE\u06FE\u062F\u068F\u06CF\u06EF]/.test(char);
-}
-
-/**
- * Check if a char is an Arabic Tashkeel char
- * @param {string} c a single char
- */
-function isTashkeelArabicChar(char) {
- return /[\u0600-\u0605\u060C-\u060E\u0610-\u061B\u061E\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED]/.test(char);
-}
-
-/**
- * Check if a char is Latin
- * @param {string} c a single char
- */
-function isLatinChar(c) {
- return /[A-z]/.test(c);
-}
-
-/**
- * Check if a char is whitespace char
- * @param {string} c a single char
- */
-function isWhiteSpace(c) {
- return /\s/.test(c);
-}
-
-/**
- * Query a feature by some of it's properties to lookup a glyph substitution.
- */
-
-/**
- * Create feature query instance
- * @param {Font} font opentype font instance
- */
-function FeatureQuery(font) {
- this.font = font;
- this.features = {};
-}
-
-/**
- * @typedef SubstitutionAction
- * @type Object
- * @property {number} id substitution type
- * @property {string} tag feature tag
- * @property {any} substitution substitution value(s)
- */
-
-/**
- * Create a substitution action instance
- * @param {SubstitutionAction} action
- */
-function SubstitutionAction(action) {
- this.id = action.id;
- this.tag = action.tag;
- this.substitution = action.substitution;
-}
-
-/**
- * Lookup a coverage table
- * @param {number} glyphIndex glyph index
- * @param {CoverageTable} coverage coverage table
- */
-function lookupCoverage(glyphIndex, coverage) {
- if (!glyphIndex) { return -1; }
- switch (coverage.format) {
- case 1:
- return coverage.glyphs.indexOf(glyphIndex);
-
- case 2:
- var ranges = coverage.ranges;
- for (var i = 0; i < ranges.length; i++) {
- var range = ranges[i];
- if (glyphIndex >= range.start && glyphIndex <= range.end) {
- var offset = glyphIndex - range.start;
- return range.index + offset;
- }
- }
- break;
- default:
- return -1; // not found
- }
- return -1;
-}
-
-/**
- * Handle a single substitution - format 1
- * @param {ContextParams} contextParams context params to lookup
- */
-function singleSubstitutionFormat1(glyphIndex, subtable) {
- var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage);
- if (substituteIndex === -1) { return null; }
- return glyphIndex + subtable.deltaGlyphId;
-}
-
-/**
- * Handle a single substitution - format 2
- * @param {ContextParams} contextParams context params to lookup
- */
-function singleSubstitutionFormat2(glyphIndex, subtable) {
- var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage);
- if (substituteIndex === -1) { return null; }
- return subtable.substitute[substituteIndex];
-}
-
-/**
- * Lookup a list of coverage tables
- * @param {any} coverageList a list of coverage tables
- * @param {ContextParams} contextParams context params to lookup
- */
-function lookupCoverageList(coverageList, contextParams) {
- var lookupList = [];
- for (var i = 0; i < coverageList.length; i++) {
- var coverage = coverageList[i];
- var glyphIndex = contextParams.current;
- glyphIndex = Array.isArray(glyphIndex) ? glyphIndex[0] : glyphIndex;
- var lookupIndex = lookupCoverage(glyphIndex, coverage);
- if (lookupIndex !== -1) {
- lookupList.push(lookupIndex);
- }
- }
- if (lookupList.length !== coverageList.length) { return -1; }
- return lookupList;
-}
-
-/**
- * Handle chaining context substitution - format 3
- * @param {ContextParams} contextParams context params to lookup
- */
-function chainingSubstitutionFormat3(contextParams, subtable) {
- var lookupsCount = (
- subtable.inputCoverage.length +
- subtable.lookaheadCoverage.length +
- subtable.backtrackCoverage.length
- );
- if (contextParams.context.length < lookupsCount) { return []; }
- // INPUT LOOKUP //
- var inputLookups = lookupCoverageList(
- subtable.inputCoverage, contextParams
- );
- if (inputLookups === -1) { return []; }
- // LOOKAHEAD LOOKUP //
- var lookaheadOffset = subtable.inputCoverage.length - 1;
- if (contextParams.lookahead.length < subtable.lookaheadCoverage.length) { return []; }
- var lookaheadContext = contextParams.lookahead.slice(lookaheadOffset);
- while (lookaheadContext.length && isTashkeelArabicChar(lookaheadContext[0].char)) {
- lookaheadContext.shift();
- }
- var lookaheadParams = new ContextParams(lookaheadContext, 0);
- var lookaheadLookups = lookupCoverageList(
- subtable.lookaheadCoverage, lookaheadParams
- );
- // BACKTRACK LOOKUP //
- var backtrackContext = [].concat(contextParams.backtrack);
- backtrackContext.reverse();
- while (backtrackContext.length && isTashkeelArabicChar(backtrackContext[0].char)) {
- backtrackContext.shift();
- }
- if (backtrackContext.length < subtable.backtrackCoverage.length) { return []; }
- var backtrackParams = new ContextParams(backtrackContext, 0);
- var backtrackLookups = lookupCoverageList(
- subtable.backtrackCoverage, backtrackParams
- );
- var contextRulesMatch = (
- inputLookups.length === subtable.inputCoverage.length &&
- lookaheadLookups.length === subtable.lookaheadCoverage.length &&
- backtrackLookups.length === subtable.backtrackCoverage.length
- );
- var substitutions = [];
- if (contextRulesMatch) {
- for (var i = 0; i < subtable.lookupRecords.length; i++) {
- var lookupRecord = subtable.lookupRecords[i];
- var lookupListIndex = lookupRecord.lookupListIndex;
- var lookupTable = this.getLookupByIndex(lookupListIndex);
- for (var s = 0; s < lookupTable.subtables.length; s++) {
- var subtable$1 = lookupTable.subtables[s];
- var lookup = this.getLookupMethod(lookupTable, subtable$1);
- var substitutionType = this.getSubstitutionType(lookupTable, subtable$1);
- if (substitutionType === '12') {
- for (var n = 0; n < inputLookups.length; n++) {
- var glyphIndex = contextParams.get(n);
- var substitution = lookup(glyphIndex);
- if (substitution) { substitutions.push(substitution); }
- }
- }
- }
- }
- }
- return substitutions;
-}
-
-/**
- * Handle ligature substitution - format 1
- * @param {ContextParams} contextParams context params to lookup
- */
-function ligatureSubstitutionFormat1(contextParams, subtable) {
- // COVERAGE LOOKUP //
- var glyphIndex = contextParams.current;
- var ligSetIndex = lookupCoverage(glyphIndex, subtable.coverage);
- if (ligSetIndex === -1) { return null; }
- // COMPONENTS LOOKUP
- // (!) note, components are ordered in the written direction.
- var ligature;
- var ligatureSet = subtable.ligatureSets[ligSetIndex];
- for (var s = 0; s < ligatureSet.length; s++) {
- ligature = ligatureSet[s];
- for (var l = 0; l < ligature.components.length; l++) {
- var lookaheadItem = contextParams.lookahead[l];
- var component = ligature.components[l];
- if (lookaheadItem !== component) { break; }
- if (l === ligature.components.length - 1) { return ligature; }
- }
- }
- return null;
-}
-
-/**
- * Handle decomposition substitution - format 1
- * @param {number} glyphIndex glyph index
- * @param {any} subtable subtable
- */
-function decompositionSubstitutionFormat1(glyphIndex, subtable) {
- var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage);
- if (substituteIndex === -1) { return null; }
- return subtable.sequences[substituteIndex];
-}
-
-/**
- * Get default script features indexes
- */
-FeatureQuery.prototype.getDefaultScriptFeaturesIndexes = function () {
- var scripts = this.font.tables.gsub.scripts;
- for (var s = 0; s < scripts.length; s++) {
- var script = scripts[s];
- if (script.tag === 'DFLT') { return (
- script.script.defaultLangSys.featureIndexes
- ); }
- }
- return [];
-};
-
-/**
- * Get feature indexes of a specific script
- * @param {string} scriptTag script tag
- */
-FeatureQuery.prototype.getScriptFeaturesIndexes = function(scriptTag) {
- var tables = this.font.tables;
- if (!tables.gsub) { return []; }
- if (!scriptTag) { return this.getDefaultScriptFeaturesIndexes(); }
- var scripts = this.font.tables.gsub.scripts;
- for (var i = 0; i < scripts.length; i++) {
- var script = scripts[i];
- if (script.tag === scriptTag && script.script.defaultLangSys) {
- return script.script.defaultLangSys.featureIndexes;
- } else {
- var langSysRecords = script.langSysRecords;
- if (!!langSysRecords) {
- for (var j = 0; j < langSysRecords.length; j++) {
- var langSysRecord = langSysRecords[j];
- if (langSysRecord.tag === scriptTag) {
- var langSys = langSysRecord.langSys;
- return langSys.featureIndexes;
- }
- }
- }
- }
- }
- return this.getDefaultScriptFeaturesIndexes();
-};
-
-/**
- * Map a feature tag to a gsub feature
- * @param {any} features gsub features
- * @param {string} scriptTag script tag
- */
-FeatureQuery.prototype.mapTagsToFeatures = function (features, scriptTag) {
- var tags = {};
- for (var i = 0; i < features.length; i++) {
- var tag = features[i].tag;
- var feature = features[i].feature;
- tags[tag] = feature;
- }
- this.features[scriptTag].tags = tags;
-};
-
-/**
- * Get features of a specific script
- * @param {string} scriptTag script tag
- */
-FeatureQuery.prototype.getScriptFeatures = function (scriptTag) {
- var features = this.features[scriptTag];
- if (this.features.hasOwnProperty(scriptTag)) { return features; }
- var featuresIndexes = this.getScriptFeaturesIndexes(scriptTag);
- if (!featuresIndexes) { return null; }
- var gsub = this.font.tables.gsub;
- features = featuresIndexes.map(function (index) { return gsub.features[index]; });
- this.features[scriptTag] = features;
- this.mapTagsToFeatures(features, scriptTag);
- return features;
-};
-
-/**
- * Get substitution type
- * @param {any} lookupTable lookup table
- * @param {any} subtable subtable
- */
-FeatureQuery.prototype.getSubstitutionType = function(lookupTable, subtable) {
- var lookupType = lookupTable.lookupType.toString();
- var substFormat = subtable.substFormat.toString();
- return lookupType + substFormat;
-};
-
-/**
- * Get lookup method
- * @param {any} lookupTable lookup table
- * @param {any} subtable subtable
- */
-FeatureQuery.prototype.getLookupMethod = function(lookupTable, subtable) {
- var this$1 = this;
-
- var substitutionType = this.getSubstitutionType(lookupTable, subtable);
- switch (substitutionType) {
- case '11':
- return function (glyphIndex) { return singleSubstitutionFormat1.apply(
- this$1, [glyphIndex, subtable]
- ); };
- case '12':
- return function (glyphIndex) { return singleSubstitutionFormat2.apply(
- this$1, [glyphIndex, subtable]
- ); };
- case '63':
- return function (contextParams) { return chainingSubstitutionFormat3.apply(
- this$1, [contextParams, subtable]
- ); };
- case '41':
- return function (contextParams) { return ligatureSubstitutionFormat1.apply(
- this$1, [contextParams, subtable]
- ); };
- case '21':
- return function (glyphIndex) { return decompositionSubstitutionFormat1.apply(
- this$1, [glyphIndex, subtable]
- ); };
- default:
- throw new Error(
- "lookupType: " + (lookupTable.lookupType) + " - " +
- "substFormat: " + (subtable.substFormat) + " " +
- "is not yet supported"
- );
- }
-};
-
-/**
- * [ LOOKUP TYPES ]
- * -------------------------------
- * Single 1;
- * Multiple 2;
- * Alternate 3;
- * Ligature 4;
- * Context 5;
- * ChainingContext 6;
- * ExtensionSubstitution 7;
- * ReverseChainingContext 8;
- * -------------------------------
- *
- */
-
-/**
- * @typedef FQuery
- * @type Object
- * @param {string} tag feature tag
- * @param {string} script feature script
- * @param {ContextParams} contextParams context params
- */
-
-/**
- * Lookup a feature using a query parameters
- * @param {FQuery} query feature query
- */
-FeatureQuery.prototype.lookupFeature = function (query) {
- var contextParams = query.contextParams;
- var currentIndex = contextParams.index;
- var feature = this.getFeature({
- tag: query.tag, script: query.script
- });
- if (!feature) { return new Error(
- "font '" + (this.font.names.fullName.en) + "' " +
- "doesn't support feature '" + (query.tag) + "' " +
- "for script '" + (query.script) + "'."
- ); }
- var lookups = this.getFeatureLookups(feature);
- var substitutions = [].concat(contextParams.context);
- for (var l = 0; l < lookups.length; l++) {
- var lookupTable = lookups[l];
- var subtables = this.getLookupSubtables(lookupTable);
- for (var s = 0; s < subtables.length; s++) {
- var subtable = subtables[s];
- var substType = this.getSubstitutionType(lookupTable, subtable);
- var lookup = this.getLookupMethod(lookupTable, subtable);
- var substitution = (void 0);
- switch (substType) {
- case '11':
- substitution = lookup(contextParams.current);
- if (substitution) {
- substitutions.splice(currentIndex, 1, new SubstitutionAction({
- id: 11, tag: query.tag, substitution: substitution
- }));
- }
- break;
- case '12':
- substitution = lookup(contextParams.current);
- if (substitution) {
- substitutions.splice(currentIndex, 1, new SubstitutionAction({
- id: 12, tag: query.tag, substitution: substitution
- }));
- }
- break;
- case '63':
- substitution = lookup(contextParams);
- if (Array.isArray(substitution) && substitution.length) {
- substitutions.splice(currentIndex, 1, new SubstitutionAction({
- id: 63, tag: query.tag, substitution: substitution
- }));
- }
- break;
- case '41':
- substitution = lookup(contextParams);
- if (substitution) {
- substitutions.splice(currentIndex, 1, new SubstitutionAction({
- id: 41, tag: query.tag, substitution: substitution
- }));
- }
- break;
- case '21':
- substitution = lookup(contextParams.current);
- if (substitution) {
- substitutions.splice(currentIndex, 1, new SubstitutionAction({
- id: 21, tag: query.tag, substitution: substitution
- }));
- }
- break;
- }
- contextParams = new ContextParams(substitutions, currentIndex);
- if (Array.isArray(substitution) && !substitution.length) { continue; }
- substitution = null;
- }
- }
- return substitutions.length ? substitutions : null;
-};
-
-/**
- * Checks if a font supports a specific features
- * @param {FQuery} query feature query object
- */
-FeatureQuery.prototype.supports = function (query) {
- if (!query.script) { return false; }
- this.getScriptFeatures(query.script);
- var supportedScript = this.features.hasOwnProperty(query.script);
- if (!query.tag) { return supportedScript; }
- var supportedFeature = (
- this.features[query.script].some(function (feature) { return feature.tag === query.tag; })
- );
- return supportedScript && supportedFeature;
-};
-
-/**
- * Get lookup table subtables
- * @param {any} lookupTable lookup table
- */
-FeatureQuery.prototype.getLookupSubtables = function (lookupTable) {
- return lookupTable.subtables || null;
-};
-
-/**
- * Get lookup table by index
- * @param {number} index lookup table index
- */
-FeatureQuery.prototype.getLookupByIndex = function (index) {
- var lookups = this.font.tables.gsub.lookups;
- return lookups[index] || null;
-};
-
-/**
- * Get lookup tables for a feature
- * @param {string} feature
- */
-FeatureQuery.prototype.getFeatureLookups = function (feature) {
- // TODO: memoize
- return feature.lookupListIndexes.map(this.getLookupByIndex.bind(this));
-};
-
-/**
- * Query a feature by it's properties
- * @param {any} query an object that describes the properties of a query
- */
-FeatureQuery.prototype.getFeature = function getFeature(query) {
- if (!this.font) { return { FAIL: "No font was found"}; }
- if (!this.features.hasOwnProperty(query.script)) {
- this.getScriptFeatures(query.script);
- }
- var scriptFeatures = this.features[query.script];
- if (!scriptFeatures) { return (
- { FAIL: ("No feature for script " + (query.script))}
- ); }
- if (!scriptFeatures.tags[query.tag]) { return null; }
- return this.features[query.script].tags[query.tag];
-};
-
-/**
- * Arabic word context checkers
- */
-
-function arabicWordStartCheck(contextParams) {
- var char = contextParams.current;
- var prevChar = contextParams.get(-1);
- return (
- // ? arabic first char
- (prevChar === null && isArabicChar(char)) ||
- // ? arabic char preceded with a non arabic char
- (!isArabicChar(prevChar) && isArabicChar(char))
- );
-}
-
-function arabicWordEndCheck(contextParams) {
- var nextChar = contextParams.get(1);
- return (
- // ? last arabic char
- (nextChar === null) ||
- // ? next char is not arabic
- (!isArabicChar(nextChar))
- );
-}
-
-var arabicWordCheck = {
- startCheck: arabicWordStartCheck,
- endCheck: arabicWordEndCheck
-};
-
-/**
- * Arabic sentence context checkers
- */
-
-function arabicSentenceStartCheck(contextParams) {
- var char = contextParams.current;
- var prevChar = contextParams.get(-1);
- return (
- // ? an arabic char preceded with a non arabic char
- (isArabicChar(char) || isTashkeelArabicChar(char)) &&
- !isArabicChar(prevChar)
- );
-}
-
-function arabicSentenceEndCheck(contextParams) {
- var nextChar = contextParams.get(1);
- switch (true) {
- case nextChar === null:
- return true;
- case (!isArabicChar(nextChar) && !isTashkeelArabicChar(nextChar)):
- var nextIsWhitespace = isWhiteSpace(nextChar);
- if (!nextIsWhitespace) { return true; }
- if (nextIsWhitespace) {
- var arabicCharAhead = false;
- arabicCharAhead = (
- contextParams.lookahead.some(
- function (c) { return isArabicChar(c) || isTashkeelArabicChar(c); }
- )
- );
- if (!arabicCharAhead) { return true; }
- }
- break;
- default:
- return false;
- }
-}
-
-var arabicSentenceCheck = {
- startCheck: arabicSentenceStartCheck,
- endCheck: arabicSentenceEndCheck
-};
-
-/**
- * Apply single substitution format 1
- * @param {Array} substitutions substitutions
- * @param {any} tokens a list of tokens
- * @param {number} index token index
- */
-function singleSubstitutionFormat1$1(action, tokens, index) {
- tokens[index].setState(action.tag, action.substitution);
-}
-
-/**
- * Apply single substitution format 2
- * @param {Array} substitutions substitutions
- * @param {any} tokens a list of tokens
- * @param {number} index token index
- */
-function singleSubstitutionFormat2$1(action, tokens, index) {
- tokens[index].setState(action.tag, action.substitution);
-}
-
-/**
- * Apply chaining context substitution format 3
- * @param {Array} substitutions substitutions
- * @param {any} tokens a list of tokens
- * @param {number} index token index
- */
-function chainingSubstitutionFormat3$1(action, tokens, index) {
- action.substitution.forEach(function (subst, offset) {
- var token = tokens[index + offset];
- token.setState(action.tag, subst);
- });
-}
-
-/**
- * Apply ligature substitution format 1
- * @param {Array} substitutions substitutions
- * @param {any} tokens a list of tokens
- * @param {number} index token index
- */
-function ligatureSubstitutionFormat1$1(action, tokens, index) {
- var token = tokens[index];
- token.setState(action.tag, action.substitution.ligGlyph);
- var compsCount = action.substitution.components.length;
- for (var i = 0; i < compsCount; i++) {
- token = tokens[index + i + 1];
- token.setState('deleted', true);
- }
-}
-
-/**
- * Supported substitutions
- */
-var SUBSTITUTIONS = {
- 11: singleSubstitutionFormat1$1,
- 12: singleSubstitutionFormat2$1,
- 63: chainingSubstitutionFormat3$1,
- 41: ligatureSubstitutionFormat1$1
-};
-
-/**
- * Apply substitutions to a list of tokens
- * @param {Array} substitutions substitutions
- * @param {any} tokens a list of tokens
- * @param {number} index token index
- */
-function applySubstitution(action, tokens, index) {
- if (action instanceof SubstitutionAction && SUBSTITUTIONS[action.id]) {
- SUBSTITUTIONS[action.id](action, tokens, index);
- }
-}
-
-/**
- * Apply Arabic presentation forms to a range of tokens
- */
-
-/**
- * Check if a char can be connected to it's preceding char
- * @param {ContextParams} charContextParams context params of a char
- */
-function willConnectPrev(charContextParams) {
- var backtrack = [].concat(charContextParams.backtrack);
- for (var i = backtrack.length - 1; i >= 0; i--) {
- var prevChar = backtrack[i];
- var isolated = isIsolatedArabicChar(prevChar);
- var tashkeel = isTashkeelArabicChar(prevChar);
- if (!isolated && !tashkeel) { return true; }
- if (isolated) { return false; }
- }
- return false;
-}
-
-/**
- * Check if a char can be connected to it's proceeding char
- * @param {ContextParams} charContextParams context params of a char
- */
-function willConnectNext(charContextParams) {
- if (isIsolatedArabicChar(charContextParams.current)) { return false; }
- for (var i = 0; i < charContextParams.lookahead.length; i++) {
- var nextChar = charContextParams.lookahead[i];
- var tashkeel = isTashkeelArabicChar(nextChar);
- if (!tashkeel) { return true; }
- }
- return false;
-}
-
-/**
- * Apply arabic presentation forms to a list of tokens
- * @param {ContextRange} range a range of tokens
- */
-function arabicPresentationForms(range) {
- var this$1 = this;
-
- var script = 'arab';
- var tags = this.featuresTags[script];
- var tokens = this.tokenizer.getRangeTokens(range);
- if (tokens.length === 1) { return; }
- var contextParams = new ContextParams(
- tokens.map(function (token) { return token.getState('glyphIndex'); }
- ), 0);
- var charContextParams = new ContextParams(
- tokens.map(function (token) { return token.char; }
- ), 0);
- tokens.forEach(function (token, index) {
- if (isTashkeelArabicChar(token.char)) { return; }
- contextParams.setCurrentIndex(index);
- charContextParams.setCurrentIndex(index);
- var CONNECT = 0; // 2 bits 00 (10: can connect next) (01: can connect prev)
- if (willConnectPrev(charContextParams)) { CONNECT |= 1; }
- if (willConnectNext(charContextParams)) { CONNECT |= 2; }
- var tag;
- switch (CONNECT) {
- case 1: (tag = 'fina'); break;
- case 2: (tag = 'init'); break;
- case 3: (tag = 'medi'); break;
- }
- if (tags.indexOf(tag) === -1) { return; }
- var substitutions = this$1.query.lookupFeature({
- tag: tag, script: script, contextParams: contextParams
- });
- if (substitutions instanceof Error) { return console.info(substitutions.message); }
- substitutions.forEach(function (action, index) {
- if (action instanceof SubstitutionAction) {
- applySubstitution(action, tokens, index);
- contextParams.context[index] = action.substitution;
- }
- });
- });
-}
-
-/**
- * Apply Arabic required ligatures feature to a range of tokens
- */
-
-/**
- * Update context params
- * @param {any} tokens a list of tokens
- * @param {number} index current item index
- */
-function getContextParams(tokens, index) {
- var context = tokens.map(function (token) { return token.activeState.value; });
- return new ContextParams(context, index || 0);
-}
-
-/**
- * Apply Arabic required ligatures to a context range
- * @param {ContextRange} range a range of tokens
- */
-function arabicRequiredLigatures(range) {
- var this$1 = this;
-
- var script = 'arab';
- var tokens = this.tokenizer.getRangeTokens(range);
- var contextParams = getContextParams(tokens);
- contextParams.context.forEach(function (glyphIndex, index) {
- contextParams.setCurrentIndex(index);
- var substitutions = this$1.query.lookupFeature({
- tag: 'rlig', script: script, contextParams: contextParams
- });
- if (substitutions.length) {
- substitutions.forEach(
- function (action) { return applySubstitution(action, tokens, index); }
- );
- contextParams = getContextParams(tokens);
- }
- });
-}
-
-/**
- * Latin word context checkers
- */
-
-function latinWordStartCheck(contextParams) {
- var char = contextParams.current;
- var prevChar = contextParams.get(-1);
- return (
- // ? latin first char
- (prevChar === null && isLatinChar(char)) ||
- // ? latin char preceded with a non latin char
- (!isLatinChar(prevChar) && isLatinChar(char))
- );
-}
-
-function latinWordEndCheck(contextParams) {
- var nextChar = contextParams.get(1);
- return (
- // ? last latin char
- (nextChar === null) ||
- // ? next char is not latin
- (!isLatinChar(nextChar))
- );
-}
-
-var latinWordCheck = {
- startCheck: latinWordStartCheck,
- endCheck: latinWordEndCheck
-};
-
-/**
- * Apply Latin ligature feature to a range of tokens
- */
-
-/**
- * Update context params
- * @param {any} tokens a list of tokens
- * @param {number} index current item index
- */
-function getContextParams$1(tokens, index) {
- var context = tokens.map(function (token) { return token.activeState.value; });
- return new ContextParams(context, index || 0);
-}
-
-/**
- * Apply Arabic required ligatures to a context range
- * @param {ContextRange} range a range of tokens
- */
-function latinLigature(range) {
- var this$1 = this;
-
- var script = 'latn';
- var tokens = this.tokenizer.getRangeTokens(range);
- var contextParams = getContextParams$1(tokens);
- contextParams.context.forEach(function (glyphIndex, index) {
- contextParams.setCurrentIndex(index);
- var substitutions = this$1.query.lookupFeature({
- tag: 'liga', script: script, contextParams: contextParams
- });
- if (substitutions.length) {
- substitutions.forEach(
- function (action) { return applySubstitution(action, tokens, index); }
- );
- contextParams = getContextParams$1(tokens);
- }
- });
-}
-
-/**
- * Infer bidirectional properties for a given text and apply
- * the corresponding layout rules.
- */
-
-/**
- * Create Bidi. features
- * @param {string} baseDir text base direction. value either 'ltr' or 'rtl'
- */
-function Bidi(baseDir) {
- this.baseDir = baseDir || 'ltr';
- this.tokenizer = new Tokenizer();
- this.featuresTags = {};
-}
-
-/**
- * Sets Bidi text
- * @param {string} text a text input
- */
-Bidi.prototype.setText = function (text) {
- this.text = text;
-};
-
-/**
- * Store essential context checks:
- * arabic word check for applying gsub features
- * arabic sentence check for adjusting arabic layout
- */
-Bidi.prototype.contextChecks = ({
- latinWordCheck: latinWordCheck,
- arabicWordCheck: arabicWordCheck,
- arabicSentenceCheck: arabicSentenceCheck
-});
-
-/**
- * Register arabic word check
- */
-function registerContextChecker(checkId) {
- var check = this.contextChecks[(checkId + "Check")];
- return this.tokenizer.registerContextChecker(
- checkId, check.startCheck, check.endCheck
- );
-}
-
-/**
- * Perform pre tokenization procedure then
- * tokenize text input
- */
-function tokenizeText() {
- registerContextChecker.call(this, 'latinWord');
- registerContextChecker.call(this, 'arabicWord');
- registerContextChecker.call(this, 'arabicSentence');
- return this.tokenizer.tokenize(this.text);
-}
-
-/**
- * Reverse arabic sentence layout
- * TODO: check base dir before applying adjustments - priority low
- */
-function reverseArabicSentences() {
- var this$1 = this;
-
- var ranges = this.tokenizer.getContextRanges('arabicSentence');
- ranges.forEach(function (range) {
- var rangeTokens = this$1.tokenizer.getRangeTokens(range);
- this$1.tokenizer.replaceRange(
- range.startIndex,
- range.endOffset,
- rangeTokens.reverse()
- );
- });
-}
-
-/**
- * Register supported features tags
- * @param {script} script script tag
- * @param {Array} tags features tags list
- */
-Bidi.prototype.registerFeatures = function (script, tags) {
- var this$1 = this;
-
- var supportedTags = tags.filter(
- function (tag) { return this$1.query.supports({script: script, tag: tag}); }
- );
- if (!this.featuresTags.hasOwnProperty(script)) {
- this.featuresTags[script] = supportedTags;
- } else {
- this.featuresTags[script] =
- this.featuresTags[script].concat(supportedTags);
- }
-};
-
-/**
- * Apply GSUB features
- * @param {Array} tagsList a list of features tags
- * @param {string} script a script tag
- * @param {Font} font opentype font instance
- */
-Bidi.prototype.applyFeatures = function (font, features) {
- if (!font) { throw new Error(
- 'No valid font was provided to apply features'
- ); }
- if (!this.query) { this.query = new FeatureQuery(font); }
- for (var f = 0; f < features.length; f++) {
- var feature = features[f];
- if (!this.query.supports({script: feature.script})) { continue; }
- this.registerFeatures(feature.script, feature.tags);
- }
-};
-
-/**
- * Register a state modifier
- * @param {string} modifierId state modifier id
- * @param {function} condition a predicate function that returns true or false
- * @param {function} modifier a modifier function to set token state
- */
-Bidi.prototype.registerModifier = function (modifierId, condition, modifier) {
- this.tokenizer.registerModifier(modifierId, condition, modifier);
-};
-
-/**
- * Check if 'glyphIndex' is registered
- */
-function checkGlyphIndexStatus() {
- if (this.tokenizer.registeredModifiers.indexOf('glyphIndex') === -1) {
- throw new Error(
- 'glyphIndex modifier is required to apply ' +
- 'arabic presentation features.'
- );
- }
-}
-
-/**
- * Apply arabic presentation forms features
- */
-function applyArabicPresentationForms() {
- var this$1 = this;
-
- var script = 'arab';
- if (!this.featuresTags.hasOwnProperty(script)) { return; }
- checkGlyphIndexStatus.call(this);
- var ranges = this.tokenizer.getContextRanges('arabicWord');
- ranges.forEach(function (range) {
- arabicPresentationForms.call(this$1, range);
- });
-}
-
-/**
- * Apply required arabic ligatures
- */
-function applyArabicRequireLigatures() {
- var this$1 = this;
-
- var script = 'arab';
- if (!this.featuresTags.hasOwnProperty(script)) { return; }
- var tags = this.featuresTags[script];
- if (tags.indexOf('rlig') === -1) { return; }
- checkGlyphIndexStatus.call(this);
- var ranges = this.tokenizer.getContextRanges('arabicWord');
- ranges.forEach(function (range) {
- arabicRequiredLigatures.call(this$1, range);
- });
-}
-
-/**
- * Apply required arabic ligatures
- */
-function applyLatinLigatures() {
- var this$1 = this;
-
- var script = 'latn';
- if (!this.featuresTags.hasOwnProperty(script)) { return; }
- var tags = this.featuresTags[script];
- if (tags.indexOf('liga') === -1) { return; }
- checkGlyphIndexStatus.call(this);
- var ranges = this.tokenizer.getContextRanges('latinWord');
- ranges.forEach(function (range) {
- latinLigature.call(this$1, range);
- });
-}
-
-/**
- * Check if a context is registered
- * @param {string} contextId context id
- */
-Bidi.prototype.checkContextReady = function (contextId) {
- return !!this.tokenizer.getContext(contextId);
-};
-
-/**
- * Apply features to registered contexts
- */
-Bidi.prototype.applyFeaturesToContexts = function () {
- if (this.checkContextReady('arabicWord')) {
- applyArabicPresentationForms.call(this);
- applyArabicRequireLigatures.call(this);
- }
- if (this.checkContextReady('latinWord')) {
- applyLatinLigatures.call(this);
- }
- if (this.checkContextReady('arabicSentence')) {
- reverseArabicSentences.call(this);
- }
-};
-
-/**
- * process text input
- * @param {string} text an input text
- */
-Bidi.prototype.processText = function(text) {
- if (!this.text || this.text !== text) {
- this.setText(text);
- tokenizeText.call(this);
- this.applyFeaturesToContexts();
- }
-};
-
-/**
- * Process a string of text to identify and adjust
- * bidirectional text entities.
- * @param {string} text input text
- */
-Bidi.prototype.getBidiText = function (text) {
- this.processText(text);
- return this.tokenizer.getText();
-};
-
-/**
- * Get the current state index of each token
- * @param {text} text an input text
- */
-Bidi.prototype.getTextGlyphs = function (text) {
- this.processText(text);
- var indexes = [];
- for (var i = 0; i < this.tokenizer.tokens.length; i++) {
- var token = this.tokenizer.tokens[i];
- if (token.state.deleted) { continue; }
- var index = token.activeState.value;
- indexes.push(Array.isArray(index) ? index[0] : index);
- }
- return indexes;
-};
-
-// The Font object
-
-/**
- * @typedef FontOptions
- * @type Object
- * @property {Boolean} empty - whether to create a new empty font
- * @property {string} familyName
- * @property {string} styleName
- * @property {string=} fullName
- * @property {string=} postScriptName
- * @property {string=} designer
- * @property {string=} designerURL
- * @property {string=} manufacturer
- * @property {string=} manufacturerURL
- * @property {string=} license
- * @property {string=} licenseURL
- * @property {string=} version
- * @property {string=} description
- * @property {string=} copyright
- * @property {string=} trademark
- * @property {Number} unitsPerEm
- * @property {Number} ascender
- * @property {Number} descender
- * @property {Number} createdTimestamp
- * @property {string=} weightClass
- * @property {string=} widthClass
- * @property {string=} fsSelection
- */
-
-/**
- * A Font represents a loaded OpenType font file.
- * It contains a set of glyphs and methods to draw text on a drawing context,
- * or to get a path representing the text.
- * @exports opentype.Font
- * @class
- * @param {FontOptions}
- * @constructor
- */
-function Font(options) {
- options = options || {};
- options.tables = options.tables || {};
-
- if (!options.empty) {
- // Check that we've provided the minimum set of names.
- checkArgument(options.familyName, 'When creating a new Font object, familyName is required.');
- checkArgument(options.styleName, 'When creating a new Font object, styleName is required.');
- checkArgument(options.unitsPerEm, 'When creating a new Font object, unitsPerEm is required.');
- checkArgument(options.ascender, 'When creating a new Font object, ascender is required.');
- checkArgument(options.descender <= 0, 'When creating a new Font object, negative descender value is required.');
-
- // OS X will complain if the names are empty, so we put a single space everywhere by default.
- this.names = {
- fontFamily: {en: options.familyName || ' '},
- fontSubfamily: {en: options.styleName || ' '},
- fullName: {en: options.fullName || options.familyName + ' ' + options.styleName},
- // postScriptName may not contain any whitespace
- postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')},
- designer: {en: options.designer || ' '},
- designerURL: {en: options.designerURL || ' '},
- manufacturer: {en: options.manufacturer || ' '},
- manufacturerURL: {en: options.manufacturerURL || ' '},
- license: {en: options.license || ' '},
- licenseURL: {en: options.licenseURL || ' '},
- version: {en: options.version || 'Version 0.1'},
- description: {en: options.description || ' '},
- copyright: {en: options.copyright || ' '},
- trademark: {en: options.trademark || ' '}
- };
- this.unitsPerEm = options.unitsPerEm || 1000;
- this.ascender = options.ascender;
- this.descender = options.descender;
- this.createdTimestamp = options.createdTimestamp;
- this.tables = Object.assign(options.tables, {
- os2: Object.assign({
- usWeightClass: options.weightClass || this.usWeightClasses.MEDIUM,
- usWidthClass: options.widthClass || this.usWidthClasses.MEDIUM,
- fsSelection: options.fsSelection || this.fsSelectionValues.REGULAR,
- }, options.tables.os2)
- });
- }
-
- this.supported = true; // Deprecated: parseBuffer will throw an error if font is not supported.
- this.glyphs = new glyphset.GlyphSet(this, options.glyphs || []);
- this.encoding = new DefaultEncoding(this);
- this.position = new Position(this);
- this.substitution = new Substitution(this);
- this.tables = this.tables || {};
-
- // needed for low memory mode only.
- this._push = null;
- this._hmtxTableData = {};
-
- Object.defineProperty(this, 'hinting', {
- get: function() {
- if (this._hinting) { return this._hinting; }
- if (this.outlinesFormat === 'truetype') {
- return (this._hinting = new Hinting(this));
- }
- }
- });
-}
-
-/**
- * Check if the font has a glyph for the given character.
- * @param {string}
- * @return {Boolean}
- */
-Font.prototype.hasChar = function(c) {
- return this.encoding.charToGlyphIndex(c) !== null;
-};
-
-/**
- * Convert the given character to a single glyph index.
- * Note that this function assumes that there is a one-to-one mapping between
- * the given character and a glyph; for complex scripts this might not be the case.
- * @param {string}
- * @return {Number}
- */
-Font.prototype.charToGlyphIndex = function(s) {
- return this.encoding.charToGlyphIndex(s);
-};
-
-/**
- * Convert the given character to a single Glyph object.
- * Note that this function assumes that there is a one-to-one mapping between
- * the given character and a glyph; for complex scripts this might not be the case.
- * @param {string}
- * @return {opentype.Glyph}
- */
-Font.prototype.charToGlyph = function(c) {
- var glyphIndex = this.charToGlyphIndex(c);
- var glyph = this.glyphs.get(glyphIndex);
- if (!glyph) {
- // .notdef
- glyph = this.glyphs.get(0);
- }
-
- return glyph;
-};
-
-/**
- * Update features
- * @param {any} options features options
- */
-Font.prototype.updateFeatures = function (options) {
- // TODO: update all features options not only 'latn'.
- return this.defaultRenderOptions.features.map(function (feature) {
- if (feature.script === 'latn') {
- return {
- script: 'latn',
- tags: feature.tags.filter(function (tag) { return options[tag]; })
- };
- } else {
- return feature;
- }
- });
-};
-
-/**
- * Convert the given text to a list of Glyph objects.
- * Note that there is no strict one-to-one mapping between characters and
- * glyphs, so the list of returned glyphs can be larger or smaller than the
- * length of the given string.
- * @param {string}
- * @param {GlyphRenderOptions} [options]
- * @return {opentype.Glyph[]}
- */
-Font.prototype.stringToGlyphs = function(s, options) {
- var this$1 = this;
-
-
- var bidi = new Bidi();
-
- // Create and register 'glyphIndex' state modifier
- var charToGlyphIndexMod = function (token) { return this$1.charToGlyphIndex(token.char); };
- bidi.registerModifier('glyphIndex', null, charToGlyphIndexMod);
-
- // roll-back to default features
- var features = options ?
- this.updateFeatures(options.features) :
- this.defaultRenderOptions.features;
-
- bidi.applyFeatures(this, features);
-
- var indexes = bidi.getTextGlyphs(s);
-
- var length = indexes.length;
-
- // convert glyph indexes to glyph objects
- var glyphs = new Array(length);
- var notdef = this.glyphs.get(0);
- for (var i = 0; i < length; i += 1) {
- glyphs[i] = this.glyphs.get(indexes[i]) || notdef;
- }
- return glyphs;
-};
-
-/**
- * @param {string}
- * @return {Number}
- */
-Font.prototype.nameToGlyphIndex = function(name) {
- return this.glyphNames.nameToGlyphIndex(name);
-};
-
-/**
- * @param {string}
- * @return {opentype.Glyph}
- */
-Font.prototype.nameToGlyph = function(name) {
- var glyphIndex = this.nameToGlyphIndex(name);
- var glyph = this.glyphs.get(glyphIndex);
- if (!glyph) {
- // .notdef
- glyph = this.glyphs.get(0);
- }
-
- return glyph;
-};
-
-/**
- * @param {Number}
- * @return {String}
- */
-Font.prototype.glyphIndexToName = function(gid) {
- if (!this.glyphNames.glyphIndexToName) {
- return '';
- }
-
- return this.glyphNames.glyphIndexToName(gid);
-};
-
-/**
- * Retrieve the value of the kerning pair between the left glyph (or its index)
- * and the right glyph (or its index). If no kerning pair is found, return 0.
- * The kerning value gets added to the advance width when calculating the spacing
- * between glyphs.
- * For GPOS kerning, this method uses the default script and language, which covers
- * most use cases. To have greater control, use font.position.getKerningValue .
- * @param {opentype.Glyph} leftGlyph
- * @param {opentype.Glyph} rightGlyph
- * @return {Number}
- */
-Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) {
- leftGlyph = leftGlyph.index || leftGlyph;
- rightGlyph = rightGlyph.index || rightGlyph;
- var gposKerning = this.position.defaultKerningTables;
- if (gposKerning) {
- return this.position.getKerningValue(gposKerning, leftGlyph, rightGlyph);
- }
- // "kern" table
- return this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0;
-};
-
-/**
- * @typedef GlyphRenderOptions
- * @type Object
- * @property {string} [script] - script used to determine which features to apply. By default, 'DFLT' or 'latn' is used.
- * See https://www.microsoft.com/typography/otspec/scripttags.htm
- * @property {string} [language='dflt'] - language system used to determine which features to apply.
- * See https://www.microsoft.com/typography/developers/opentype/languagetags.aspx
- * @property {boolean} [kerning=true] - whether to include kerning values
- * @property {object} [features] - OpenType Layout feature tags. Used to enable or disable the features of the given script/language system.
- * See https://www.microsoft.com/typography/otspec/featuretags.htm
- */
-Font.prototype.defaultRenderOptions = {
- kerning: true,
- features: [
- /**
- * these 4 features are required to render Arabic text properly
- * and shouldn't be turned off when rendering arabic text.
- */
- { script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] },
- { script: 'latn', tags: ['liga', 'rlig'] }
- ]
-};
-
-/**
- * Helper function that invokes the given callback for each glyph in the given text.
- * The callback gets `(glyph, x, y, fontSize, options)`.* @param {string} text
- * @param {string} text - The text to apply.
- * @param {number} [x=0] - Horizontal position of the beginning of the text.
- * @param {number} [y=0] - Vertical position of the *baseline* of the text.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- * @param {GlyphRenderOptions=} options
- * @param {Function} callback
- */
-Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback) {
- x = x !== undefined ? x : 0;
- y = y !== undefined ? y : 0;
- fontSize = fontSize !== undefined ? fontSize : 72;
- options = Object.assign({}, this.defaultRenderOptions, options);
- var fontScale = 1 / this.unitsPerEm * fontSize;
- var glyphs = this.stringToGlyphs(text, options);
- var kerningLookups;
- if (options.kerning) {
- var script = options.script || this.position.getDefaultScriptName();
- kerningLookups = this.position.getKerningTables(script, options.language);
- }
- for (var i = 0; i < glyphs.length; i += 1) {
- var glyph = glyphs[i];
- callback.call(this, glyph, x, y, fontSize, options);
- if (glyph.advanceWidth) {
- x += glyph.advanceWidth * fontScale;
- }
-
- if (options.kerning && i < glyphs.length - 1) {
- // We should apply position adjustment lookups in a more generic way.
- // Here we only use the xAdvance value.
- var kerningValue = kerningLookups ?
- this.position.getKerningValue(kerningLookups, glyph.index, glyphs[i + 1].index) :
- this.getKerningValue(glyph, glyphs[i + 1]);
- x += kerningValue * fontScale;
- }
-
- if (options.letterSpacing) {
- x += options.letterSpacing * fontSize;
- } else if (options.tracking) {
- x += (options.tracking / 1000) * fontSize;
- }
- }
- return x;
-};
-
-/**
- * Create a Path object that represents the given text.
- * @param {string} text - The text to create.
- * @param {number} [x=0] - Horizontal position of the beginning of the text.
- * @param {number} [y=0] - Vertical position of the *baseline* of the text.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- * @param {GlyphRenderOptions=} options
- * @return {opentype.Path}
- */
-Font.prototype.getPath = function(text, x, y, fontSize, options) {
- var fullPath = new Path();
- this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
- var glyphPath = glyph.getPath(gX, gY, gFontSize, options, this);
- fullPath.extend(glyphPath);
- });
- return fullPath;
-};
-
-/**
- * Create an array of Path objects that represent the glyphs of a given text.
- * @param {string} text - The text to create.
- * @param {number} [x=0] - Horizontal position of the beginning of the text.
- * @param {number} [y=0] - Vertical position of the *baseline* of the text.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- * @param {GlyphRenderOptions=} options
- * @return {opentype.Path[]}
- */
-Font.prototype.getPaths = function(text, x, y, fontSize, options) {
- var glyphPaths = [];
- this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
- var glyphPath = glyph.getPath(gX, gY, gFontSize, options, this);
- glyphPaths.push(glyphPath);
- });
-
- return glyphPaths;
-};
-
-/**
- * Returns the advance width of a text.
- *
- * This is something different than Path.getBoundingBox() as for example a
- * suffixed whitespace increases the advanceWidth but not the bounding box
- * or an overhanging letter like a calligraphic 'f' might have a quite larger
- * bounding box than its advance width.
- *
- * This corresponds to canvas2dContext.measureText(text).width
- *
- * @param {string} text - The text to create.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- * @param {GlyphRenderOptions=} options
- * @return advance width
- */
-Font.prototype.getAdvanceWidth = function(text, fontSize, options) {
- return this.forEachGlyph(text, 0, 0, fontSize, options, function() {});
-};
-
-/**
- * Draw the text on the given drawing context.
- * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
- * @param {string} text - The text to create.
- * @param {number} [x=0] - Horizontal position of the beginning of the text.
- * @param {number} [y=0] - Vertical position of the *baseline* of the text.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- * @param {GlyphRenderOptions=} options
- */
-Font.prototype.draw = function(ctx, text, x, y, fontSize, options) {
- this.getPath(text, x, y, fontSize, options).draw(ctx);
-};
-
-/**
- * Draw the points of all glyphs in the text.
- * On-curve points will be drawn in blue, off-curve points will be drawn in red.
- * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
- * @param {string} text - The text to create.
- * @param {number} [x=0] - Horizontal position of the beginning of the text.
- * @param {number} [y=0] - Vertical position of the *baseline* of the text.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- * @param {GlyphRenderOptions=} options
- */
-Font.prototype.drawPoints = function(ctx, text, x, y, fontSize, options) {
- this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
- glyph.drawPoints(ctx, gX, gY, gFontSize);
- });
-};
-
-/**
- * Draw lines indicating important font measurements for all glyphs in the text.
- * Black lines indicate the origin of the coordinate system (point 0,0).
- * Blue lines indicate the glyph bounding box.
- * Green line indicates the advance width of the glyph.
- * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
- * @param {string} text - The text to create.
- * @param {number} [x=0] - Horizontal position of the beginning of the text.
- * @param {number} [y=0] - Vertical position of the *baseline* of the text.
- * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
- * @param {GlyphRenderOptions=} options
- */
-Font.prototype.drawMetrics = function(ctx, text, x, y, fontSize, options) {
- this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
- glyph.drawMetrics(ctx, gX, gY, gFontSize);
- });
-};
-
-/**
- * @param {string}
- * @return {string}
- */
-Font.prototype.getEnglishName = function(name) {
- var translations = this.names[name];
- if (translations) {
- return translations.en;
- }
-};
-
-/**
- * Validate
- */
-Font.prototype.validate = function() {
- var _this = this;
-
- function assert(predicate, message) {
- }
-
- function assertNamePresent(name) {
- var englishName = _this.getEnglishName(name);
- assert(englishName && englishName.trim().length > 0);
- }
-
- // Identification information
- assertNamePresent('fontFamily');
- assertNamePresent('weightName');
- assertNamePresent('manufacturer');
- assertNamePresent('copyright');
- assertNamePresent('version');
-
- // Dimension information
- assert(this.unitsPerEm > 0);
-};
-
-/**
- * Convert the font object to a SFNT data structure.
- * This structure contains all the necessary tables and metadata to create a binary OTF file.
- * @return {opentype.Table}
- */
-Font.prototype.toTables = function() {
- return sfnt.fontToTable(this);
-};
-/**
- * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.
- */
-Font.prototype.toBuffer = function() {
- console.warn('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.');
- return this.toArrayBuffer();
-};
-/**
- * Converts a `opentype.Font` into an `ArrayBuffer`
- * @return {ArrayBuffer}
- */
-Font.prototype.toArrayBuffer = function() {
- var sfntTable = this.toTables();
- var bytes = sfntTable.encode();
- var buffer = new ArrayBuffer(bytes.length);
- var intArray = new Uint8Array(buffer);
- for (var i = 0; i < bytes.length; i++) {
- intArray[i] = bytes[i];
- }
-
- return buffer;
-};
-
-/**
- * Initiate a download of the OpenType font.
- */
-Font.prototype.download = function(fileName) {
- var familyName = this.getEnglishName('fontFamily');
- var styleName = this.getEnglishName('fontSubfamily');
- fileName = fileName || familyName.replace(/\s/g, '') + '-' + styleName + '.otf';
- var arrayBuffer = this.toArrayBuffer();
-
- if (isBrowser()) {
- window.URL = window.URL || window.webkitURL;
-
- if (window.URL) {
- var dataView = new DataView(arrayBuffer);
- var blob = new Blob([dataView], {type: 'font/opentype'});
-
- var link = document.createElement('a');
- link.href = window.URL.createObjectURL(blob);
- link.download = fileName;
-
- var event = document.createEvent('MouseEvents');
- event.initEvent('click', true, false);
- link.dispatchEvent(event);
- } else {
- console.warn('Font file could not be downloaded. Try using a different browser.');
- }
- } else {
- var fs = require('fs');
- var buffer = arrayBufferToNodeBuffer(arrayBuffer);
- fs.writeFileSync(fileName, buffer);
- }
-};
-/**
- * @private
- */
-Font.prototype.fsSelectionValues = {
- ITALIC: 0x001, //1
- UNDERSCORE: 0x002, //2
- NEGATIVE: 0x004, //4
- OUTLINED: 0x008, //8
- STRIKEOUT: 0x010, //16
- BOLD: 0x020, //32
- REGULAR: 0x040, //64
- USER_TYPO_METRICS: 0x080, //128
- WWS: 0x100, //256
- OBLIQUE: 0x200 //512
-};
-
-/**
- * @private
- */
-Font.prototype.usWidthClasses = {
- ULTRA_CONDENSED: 1,
- EXTRA_CONDENSED: 2,
- CONDENSED: 3,
- SEMI_CONDENSED: 4,
- MEDIUM: 5,
- SEMI_EXPANDED: 6,
- EXPANDED: 7,
- EXTRA_EXPANDED: 8,
- ULTRA_EXPANDED: 9
-};
-
-/**
- * @private
- */
-Font.prototype.usWeightClasses = {
- THIN: 100,
- EXTRA_LIGHT: 200,
- LIGHT: 300,
- NORMAL: 400,
- MEDIUM: 500,
- SEMI_BOLD: 600,
- BOLD: 700,
- EXTRA_BOLD: 800,
- BLACK: 900
-};
-
-// The `fvar` table stores font variation axes and instances.
-
-function addName(name, names) {
- var nameString = JSON.stringify(name);
- var nameID = 256;
- for (var nameKey in names) {
- var n = parseInt(nameKey);
- if (!n || n < 256) {
- continue;
- }
-
- if (JSON.stringify(names[nameKey]) === nameString) {
- return n;
- }
-
- if (nameID <= n) {
- nameID = n + 1;
- }
- }
-
- names[nameID] = name;
- return nameID;
-}
-
-function makeFvarAxis(n, axis, names) {
- var nameID = addName(axis.name, names);
- return [
- {name: 'tag_' + n, type: 'TAG', value: axis.tag},
- {name: 'minValue_' + n, type: 'FIXED', value: axis.minValue << 16},
- {name: 'defaultValue_' + n, type: 'FIXED', value: axis.defaultValue << 16},
- {name: 'maxValue_' + n, type: 'FIXED', value: axis.maxValue << 16},
- {name: 'flags_' + n, type: 'USHORT', value: 0},
- {name: 'nameID_' + n, type: 'USHORT', value: nameID}
- ];
-}
-
-function parseFvarAxis(data, start, names) {
- var axis = {};
- var p = new parse.Parser(data, start);
- axis.tag = p.parseTag();
- axis.minValue = p.parseFixed();
- axis.defaultValue = p.parseFixed();
- axis.maxValue = p.parseFixed();
- p.skip('uShort', 1); // reserved for flags; no values defined
- axis.name = names[p.parseUShort()] || {};
- return axis;
-}
-
-function makeFvarInstance(n, inst, axes, names) {
- var nameID = addName(inst.name, names);
- var fields = [
- {name: 'nameID_' + n, type: 'USHORT', value: nameID},
- {name: 'flags_' + n, type: 'USHORT', value: 0}
- ];
-
- for (var i = 0; i < axes.length; ++i) {
- var axisTag = axes[i].tag;
- fields.push({
- name: 'axis_' + n + ' ' + axisTag,
- type: 'FIXED',
- value: inst.coordinates[axisTag] << 16
- });
- }
-
- return fields;
-}
-
-function parseFvarInstance(data, start, axes, names) {
- var inst = {};
- var p = new parse.Parser(data, start);
- inst.name = names[p.parseUShort()] || {};
- p.skip('uShort', 1); // reserved for flags; no values defined
-
- inst.coordinates = {};
- for (var i = 0; i < axes.length; ++i) {
- inst.coordinates[axes[i].tag] = p.parseFixed();
- }
-
- return inst;
-}
-
-function makeFvarTable(fvar, names) {
- var result = new table.Table('fvar', [
- {name: 'version', type: 'ULONG', value: 0x10000},
- {name: 'offsetToData', type: 'USHORT', value: 0},
- {name: 'countSizePairs', type: 'USHORT', value: 2},
- {name: 'axisCount', type: 'USHORT', value: fvar.axes.length},
- {name: 'axisSize', type: 'USHORT', value: 20},
- {name: 'instanceCount', type: 'USHORT', value: fvar.instances.length},
- {name: 'instanceSize', type: 'USHORT', value: 4 + fvar.axes.length * 4}
- ]);
- result.offsetToData = result.sizeOf();
-
- for (var i = 0; i < fvar.axes.length; i++) {
- result.fields = result.fields.concat(makeFvarAxis(i, fvar.axes[i], names));
- }
-
- for (var j = 0; j < fvar.instances.length; j++) {
- result.fields = result.fields.concat(makeFvarInstance(j, fvar.instances[j], fvar.axes, names));
- }
-
- return result;
-}
-
-function parseFvarTable(data, start, names) {
- var p = new parse.Parser(data, start);
- var tableVersion = p.parseULong();
- check.argument(tableVersion === 0x00010000, 'Unsupported fvar table version.');
- var offsetToData = p.parseOffset16();
- // Skip countSizePairs.
- p.skip('uShort', 1);
- var axisCount = p.parseUShort();
- var axisSize = p.parseUShort();
- var instanceCount = p.parseUShort();
- var instanceSize = p.parseUShort();
-
- var axes = [];
- for (var i = 0; i < axisCount; i++) {
- axes.push(parseFvarAxis(data, start + offsetToData + i * axisSize, names));
- }
-
- var instances = [];
- var instanceStart = start + offsetToData + axisCount * axisSize;
- for (var j = 0; j < instanceCount; j++) {
- instances.push(parseFvarInstance(data, instanceStart + j * instanceSize, axes, names));
- }
-
- return {axes: axes, instances: instances};
-}
-
-var fvar = { make: makeFvarTable, parse: parseFvarTable };
-
-// The `GDEF` table contains various glyph properties
-
-var attachList = function() {
- return {
- coverage: this.parsePointer(Parser.coverage),
- attachPoints: this.parseList(Parser.pointer(Parser.uShortList))
- };
-};
-
-var caretValue = function() {
- var format = this.parseUShort();
- check.argument(format === 1 || format === 2 || format === 3,
- 'Unsupported CaretValue table version.');
- if (format === 1) {
- return { coordinate: this.parseShort() };
- } else if (format === 2) {
- return { pointindex: this.parseShort() };
- } else if (format === 3) {
- // Device / Variation Index tables unsupported
- return { coordinate: this.parseShort() };
- }
-};
-
-var ligGlyph = function() {
- return this.parseList(Parser.pointer(caretValue));
-};
-
-var ligCaretList = function() {
- return {
- coverage: this.parsePointer(Parser.coverage),
- ligGlyphs: this.parseList(Parser.pointer(ligGlyph))
- };
-};
-
-var markGlyphSets = function() {
- this.parseUShort(); // Version
- return this.parseList(Parser.pointer(Parser.coverage));
-};
-
-function parseGDEFTable(data, start) {
- start = start || 0;
- var p = new Parser(data, start);
- var tableVersion = p.parseVersion(1);
- check.argument(tableVersion === 1 || tableVersion === 1.2 || tableVersion === 1.3,
- 'Unsupported GDEF table version.');
- var gdef = {
- version: tableVersion,
- classDef: p.parsePointer(Parser.classDef),
- attachList: p.parsePointer(attachList),
- ligCaretList: p.parsePointer(ligCaretList),
- markAttachClassDef: p.parsePointer(Parser.classDef)
- };
- if (tableVersion >= 1.2) {
- gdef.markGlyphSets = p.parsePointer(markGlyphSets);
- }
- return gdef;
-}
-var gdef = { parse: parseGDEFTable };
-
-// The `GPOS` table contains kerning pairs, among other things.
-
-var subtableParsers$1 = new Array(10); // subtableParsers[0] is unused
-
-// https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-1-single-adjustment-positioning-subtable
-// this = Parser instance
-subtableParsers$1[1] = function parseLookup1() {
- var start = this.offset + this.relativeOffset;
- var posformat = this.parseUShort();
- if (posformat === 1) {
- return {
- posFormat: 1,
- coverage: this.parsePointer(Parser.coverage),
- value: this.parseValueRecord()
- };
- } else if (posformat === 2) {
- return {
- posFormat: 2,
- coverage: this.parsePointer(Parser.coverage),
- values: this.parseValueRecordList()
- };
- }
- check.assert(false, '0x' + start.toString(16) + ': GPOS lookup type 1 format must be 1 or 2.');
-};
-
-// https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-2-pair-adjustment-positioning-subtable
-subtableParsers$1[2] = function parseLookup2() {
- var start = this.offset + this.relativeOffset;
- var posFormat = this.parseUShort();
- check.assert(posFormat === 1 || posFormat === 2, '0x' + start.toString(16) + ': GPOS lookup type 2 format must be 1 or 2.');
- var coverage = this.parsePointer(Parser.coverage);
- var valueFormat1 = this.parseUShort();
- var valueFormat2 = this.parseUShort();
- if (posFormat === 1) {
- // Adjustments for Glyph Pairs
- return {
- posFormat: posFormat,
- coverage: coverage,
- valueFormat1: valueFormat1,
- valueFormat2: valueFormat2,
- pairSets: this.parseList(Parser.pointer(Parser.list(function() {
- return { // pairValueRecord
- secondGlyph: this.parseUShort(),
- value1: this.parseValueRecord(valueFormat1),
- value2: this.parseValueRecord(valueFormat2)
- };
- })))
- };
- } else if (posFormat === 2) {
- var classDef1 = this.parsePointer(Parser.classDef);
- var classDef2 = this.parsePointer(Parser.classDef);
- var class1Count = this.parseUShort();
- var class2Count = this.parseUShort();
- return {
- // Class Pair Adjustment
- posFormat: posFormat,
- coverage: coverage,
- valueFormat1: valueFormat1,
- valueFormat2: valueFormat2,
- classDef1: classDef1,
- classDef2: classDef2,
- class1Count: class1Count,
- class2Count: class2Count,
- classRecords: this.parseList(class1Count, Parser.list(class2Count, function() {
- return {
- value1: this.parseValueRecord(valueFormat1),
- value2: this.parseValueRecord(valueFormat2)
- };
- }))
- };
- }
-};
-
-subtableParsers$1[3] = function parseLookup3() { return { error: 'GPOS Lookup 3 not supported' }; };
-subtableParsers$1[4] = function parseLookup4() { return { error: 'GPOS Lookup 4 not supported' }; };
-subtableParsers$1[5] = function parseLookup5() { return { error: 'GPOS Lookup 5 not supported' }; };
-subtableParsers$1[6] = function parseLookup6() { return { error: 'GPOS Lookup 6 not supported' }; };
-subtableParsers$1[7] = function parseLookup7() { return { error: 'GPOS Lookup 7 not supported' }; };
-subtableParsers$1[8] = function parseLookup8() { return { error: 'GPOS Lookup 8 not supported' }; };
-subtableParsers$1[9] = function parseLookup9() { return { error: 'GPOS Lookup 9 not supported' }; };
-
-// https://docs.microsoft.com/en-us/typography/opentype/spec/gpos
-function parseGposTable(data, start) {
- start = start || 0;
- var p = new Parser(data, start);
- var tableVersion = p.parseVersion(1);
- check.argument(tableVersion === 1 || tableVersion === 1.1, 'Unsupported GPOS table version ' + tableVersion);
-
- if (tableVersion === 1) {
- return {
- version: tableVersion,
- scripts: p.parseScriptList(),
- features: p.parseFeatureList(),
- lookups: p.parseLookupList(subtableParsers$1)
- };
- } else {
- return {
- version: tableVersion,
- scripts: p.parseScriptList(),
- features: p.parseFeatureList(),
- lookups: p.parseLookupList(subtableParsers$1),
- variations: p.parseFeatureVariationsList()
- };
- }
-
-}
-
-// GPOS Writing //////////////////////////////////////////////
-// NOT SUPPORTED
-var subtableMakers$1 = new Array(10);
-
-function makeGposTable(gpos) {
- return new table.Table('GPOS', [
- {name: 'version', type: 'ULONG', value: 0x10000},
- {name: 'scripts', type: 'TABLE', value: new table.ScriptList(gpos.scripts)},
- {name: 'features', type: 'TABLE', value: new table.FeatureList(gpos.features)},
- {name: 'lookups', type: 'TABLE', value: new table.LookupList(gpos.lookups, subtableMakers$1)}
- ]);
-}
-
-var gpos = { parse: parseGposTable, make: makeGposTable };
-
-// The `kern` table contains kerning pairs.
-
-function parseWindowsKernTable(p) {
- var pairs = {};
- // Skip nTables.
- p.skip('uShort');
- var subtableVersion = p.parseUShort();
- check.argument(subtableVersion === 0, 'Unsupported kern sub-table version.');
- // Skip subtableLength, subtableCoverage
- p.skip('uShort', 2);
- var nPairs = p.parseUShort();
- // Skip searchRange, entrySelector, rangeShift.
- p.skip('uShort', 3);
- for (var i = 0; i < nPairs; i += 1) {
- var leftIndex = p.parseUShort();
- var rightIndex = p.parseUShort();
- var value = p.parseShort();
- pairs[leftIndex + ',' + rightIndex] = value;
- }
- return pairs;
-}
-
-function parseMacKernTable(p) {
- var pairs = {};
- // The Mac kern table stores the version as a fixed (32 bits) but we only loaded the first 16 bits.
- // Skip the rest.
- p.skip('uShort');
- var nTables = p.parseULong();
- //check.argument(nTables === 1, 'Only 1 subtable is supported (got ' + nTables + ').');
- if (nTables > 1) {
- console.warn('Only the first kern subtable is supported.');
- }
- p.skip('uLong');
- var coverage = p.parseUShort();
- var subtableVersion = coverage & 0xFF;
- p.skip('uShort');
- if (subtableVersion === 0) {
- var nPairs = p.parseUShort();
- // Skip searchRange, entrySelector, rangeShift.
- p.skip('uShort', 3);
- for (var i = 0; i < nPairs; i += 1) {
- var leftIndex = p.parseUShort();
- var rightIndex = p.parseUShort();
- var value = p.parseShort();
- pairs[leftIndex + ',' + rightIndex] = value;
- }
- }
- return pairs;
-}
-
-// Parse the `kern` table which contains kerning pairs.
-function parseKernTable(data, start) {
- var p = new parse.Parser(data, start);
- var tableVersion = p.parseUShort();
- if (tableVersion === 0) {
- return parseWindowsKernTable(p);
- } else if (tableVersion === 1) {
- return parseMacKernTable(p);
- } else {
- throw new Error('Unsupported kern table version (' + tableVersion + ').');
- }
-}
-
-var kern = { parse: parseKernTable };
-
-// The `loca` table stores the offsets to the locations of the glyphs in the font.
-
-// Parse the `loca` table. This table stores the offsets to the locations of the glyphs in the font,
-// relative to the beginning of the glyphData table.
-// The number of glyphs stored in the `loca` table is specified in the `maxp` table (under numGlyphs)
-// The loca table has two versions: a short version where offsets are stored as uShorts, and a long
-// version where offsets are stored as uLongs. The `head` table specifies which version to use
-// (under indexToLocFormat).
-function parseLocaTable(data, start, numGlyphs, shortVersion) {
- var p = new parse.Parser(data, start);
- var parseFn = shortVersion ? p.parseUShort : p.parseULong;
- // There is an extra entry after the last index element to compute the length of the last glyph.
- // That's why we use numGlyphs + 1.
- var glyphOffsets = [];
- for (var i = 0; i < numGlyphs + 1; i += 1) {
- var glyphOffset = parseFn.call(p);
- if (shortVersion) {
- // The short table version stores the actual offset divided by 2.
- glyphOffset *= 2;
- }
-
- glyphOffsets.push(glyphOffset);
- }
-
- return glyphOffsets;
-}
-
-var loca = { parse: parseLocaTable };
-
-// opentype.js
-
-/**
- * The opentype library.
- * @namespace opentype
- */
-
-// File loaders /////////////////////////////////////////////////////////
-/**
- * Loads a font from a file. The callback throws an error message as the first parameter if it fails
- * and the font as an ArrayBuffer in the second parameter if it succeeds.
- * @param {string} path - The path of the file
- * @param {Function} callback - The function to call when the font load completes
- */
-function loadFromFile(path, callback) {
- var fs = require('fs');
- fs.readFile(path, function(err, buffer) {
- if (err) {
- return callback(err.message);
- }
-
- callback(null, nodeBufferToArrayBuffer(buffer));
- });
-}
-/**
- * Loads a font from a URL. The callback throws an error message as the first parameter if it fails
- * and the font as an ArrayBuffer in the second parameter if it succeeds.
- * @param {string} url - The URL of the font file.
- * @param {Function} callback - The function to call when the font load completes
- */
-function loadFromUrl(url, callback) {
- var request = new XMLHttpRequest();
- request.open('get', url, true);
- request.responseType = 'arraybuffer';
- request.onload = function() {
- if (request.response) {
- return callback(null, request.response);
- } else {
- return callback('Font could not be loaded: ' + request.statusText);
- }
- };
-
- request.onerror = function () {
- callback('Font could not be loaded');
- };
-
- request.send();
-}
-
-// Table Directory Entries //////////////////////////////////////////////
-/**
- * Parses OpenType table entries.
- * @param {DataView}
- * @param {Number}
- * @return {Object[]}
- */
-function parseOpenTypeTableEntries(data, numTables) {
- var tableEntries = [];
- var p = 12;
- for (var i = 0; i < numTables; i += 1) {
- var tag = parse.getTag(data, p);
- var checksum = parse.getULong(data, p + 4);
- var offset = parse.getULong(data, p + 8);
- var length = parse.getULong(data, p + 12);
- tableEntries.push({tag: tag, checksum: checksum, offset: offset, length: length, compression: false});
- p += 16;
- }
-
- return tableEntries;
-}
-
-/**
- * Parses WOFF table entries.
- * @param {DataView}
- * @param {Number}
- * @return {Object[]}
- */
-function parseWOFFTableEntries(data, numTables) {
- var tableEntries = [];
- var p = 44; // offset to the first table directory entry.
- for (var i = 0; i < numTables; i += 1) {
- var tag = parse.getTag(data, p);
- var offset = parse.getULong(data, p + 4);
- var compLength = parse.getULong(data, p + 8);
- var origLength = parse.getULong(data, p + 12);
- var compression = (void 0);
- if (compLength < origLength) {
- compression = 'WOFF';
- } else {
- compression = false;
- }
-
- tableEntries.push({tag: tag, offset: offset, compression: compression,
- compressedLength: compLength, length: origLength});
- p += 20;
- }
-
- return tableEntries;
-}
-
-/**
- * @typedef TableData
- * @type Object
- * @property {DataView} data - The DataView
- * @property {number} offset - The data offset.
- */
-
-/**
- * @param {DataView}
- * @param {Object}
- * @return {TableData}
- */
-function uncompressTable(data, tableEntry) {
- if (tableEntry.compression === 'WOFF') {
- var inBuffer = new Uint8Array(data.buffer, tableEntry.offset + 2, tableEntry.compressedLength - 2);
- var outBuffer = new Uint8Array(tableEntry.length);
- tinyInflate(inBuffer, outBuffer);
- if (outBuffer.byteLength !== tableEntry.length) {
- throw new Error('Decompression error: ' + tableEntry.tag + ' decompressed length doesn\'t match recorded length');
- }
-
- var view = new DataView(outBuffer.buffer, 0);
- return {data: view, offset: 0};
- } else {
- return {data: data, offset: tableEntry.offset};
- }
-}
-
-// Public API ///////////////////////////////////////////////////////////
-
-/**
- * Parse the OpenType file data (as an ArrayBuffer) and return a Font object.
- * Throws an error if the font could not be parsed.
- * @param {ArrayBuffer}
- * @param {Object} opt - options for parsing
- * @return {opentype.Font}
- */
-function parseBuffer(buffer, opt) {
- opt = (opt === undefined || opt === null) ? {} : opt;
-
- var indexToLocFormat;
- var ltagTable;
-
- // Since the constructor can also be called to create new fonts from scratch, we indicate this
- // should be an empty font that we'll fill with our own data.
- var font = new Font({empty: true});
-
- // OpenType fonts use big endian byte ordering.
- // We can't rely on typed array view types, because they operate with the endianness of the host computer.
- // Instead we use DataViews where we can specify endianness.
- var data = new DataView(buffer, 0);
- var numTables;
- var tableEntries = [];
- var signature = parse.getTag(data, 0);
- if (signature === String.fromCharCode(0, 1, 0, 0) || signature === 'true' || signature === 'typ1') {
- font.outlinesFormat = 'truetype';
- numTables = parse.getUShort(data, 4);
- tableEntries = parseOpenTypeTableEntries(data, numTables);
- } else if (signature === 'OTTO') {
- font.outlinesFormat = 'cff';
- numTables = parse.getUShort(data, 4);
- tableEntries = parseOpenTypeTableEntries(data, numTables);
- } else if (signature === 'wOFF') {
- var flavor = parse.getTag(data, 4);
- if (flavor === String.fromCharCode(0, 1, 0, 0)) {
- font.outlinesFormat = 'truetype';
- } else if (flavor === 'OTTO') {
- font.outlinesFormat = 'cff';
- } else {
- throw new Error('Unsupported OpenType flavor ' + signature);
- }
-
- numTables = parse.getUShort(data, 12);
- tableEntries = parseWOFFTableEntries(data, numTables);
- } else {
- throw new Error('Unsupported OpenType signature ' + signature);
- }
-
- var cffTableEntry;
- var fvarTableEntry;
- var glyfTableEntry;
- var gdefTableEntry;
- var gposTableEntry;
- var gsubTableEntry;
- var hmtxTableEntry;
- var kernTableEntry;
- var locaTableEntry;
- var nameTableEntry;
- var metaTableEntry;
- var p;
-
- for (var i = 0; i < numTables; i += 1) {
- var tableEntry = tableEntries[i];
- var table = (void 0);
- switch (tableEntry.tag) {
- case 'cmap':
- table = uncompressTable(data, tableEntry);
- font.tables.cmap = cmap.parse(table.data, table.offset);
- font.encoding = new CmapEncoding(font.tables.cmap);
- break;
- case 'cvt ' :
- table = uncompressTable(data, tableEntry);
- p = new parse.Parser(table.data, table.offset);
- font.tables.cvt = p.parseShortList(tableEntry.length / 2);
- break;
- case 'fvar':
- fvarTableEntry = tableEntry;
- break;
- case 'fpgm' :
- table = uncompressTable(data, tableEntry);
- p = new parse.Parser(table.data, table.offset);
- font.tables.fpgm = p.parseByteList(tableEntry.length);
- break;
- case 'head':
- table = uncompressTable(data, tableEntry);
- font.tables.head = head.parse(table.data, table.offset);
- font.unitsPerEm = font.tables.head.unitsPerEm;
- indexToLocFormat = font.tables.head.indexToLocFormat;
- break;
- case 'hhea':
- table = uncompressTable(data, tableEntry);
- font.tables.hhea = hhea.parse(table.data, table.offset);
- font.ascender = font.tables.hhea.ascender;
- font.descender = font.tables.hhea.descender;
- font.numberOfHMetrics = font.tables.hhea.numberOfHMetrics;
- break;
- case 'hmtx':
- hmtxTableEntry = tableEntry;
- break;
- case 'ltag':
- table = uncompressTable(data, tableEntry);
- ltagTable = ltag.parse(table.data, table.offset);
- break;
- case 'maxp':
- table = uncompressTable(data, tableEntry);
- font.tables.maxp = maxp.parse(table.data, table.offset);
- font.numGlyphs = font.tables.maxp.numGlyphs;
- break;
- case 'name':
- nameTableEntry = tableEntry;
- break;
- case 'OS/2':
- table = uncompressTable(data, tableEntry);
- font.tables.os2 = os2.parse(table.data, table.offset);
- break;
- case 'post':
- table = uncompressTable(data, tableEntry);
- font.tables.post = post.parse(table.data, table.offset);
- font.glyphNames = new GlyphNames(font.tables.post);
- break;
- case 'prep' :
- table = uncompressTable(data, tableEntry);
- p = new parse.Parser(table.data, table.offset);
- font.tables.prep = p.parseByteList(tableEntry.length);
- break;
- case 'glyf':
- glyfTableEntry = tableEntry;
- break;
- case 'loca':
- locaTableEntry = tableEntry;
- break;
- case 'CFF ':
- cffTableEntry = tableEntry;
- break;
- case 'kern':
- kernTableEntry = tableEntry;
- break;
- case 'GDEF':
- gdefTableEntry = tableEntry;
- break;
- case 'GPOS':
- gposTableEntry = tableEntry;
- break;
- case 'GSUB':
- gsubTableEntry = tableEntry;
- break;
- case 'meta':
- metaTableEntry = tableEntry;
- break;
- }
- }
-
- var nameTable = uncompressTable(data, nameTableEntry);
- font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable);
- font.names = font.tables.name;
-
- if (glyfTableEntry && locaTableEntry) {
- var shortVersion = indexToLocFormat === 0;
- var locaTable = uncompressTable(data, locaTableEntry);
- var locaOffsets = loca.parse(locaTable.data, locaTable.offset, font.numGlyphs, shortVersion);
- var glyfTable = uncompressTable(data, glyfTableEntry);
- font.glyphs = glyf.parse(glyfTable.data, glyfTable.offset, locaOffsets, font, opt);
- } else if (cffTableEntry) {
- var cffTable = uncompressTable(data, cffTableEntry);
- cff.parse(cffTable.data, cffTable.offset, font, opt);
- } else {
- throw new Error('Font doesn\'t contain TrueType or CFF outlines.');
- }
-
- var hmtxTable = uncompressTable(data, hmtxTableEntry);
- hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt);
- addGlyphNames(font, opt);
-
- if (kernTableEntry) {
- var kernTable = uncompressTable(data, kernTableEntry);
- font.kerningPairs = kern.parse(kernTable.data, kernTable.offset);
- } else {
- font.kerningPairs = {};
- }
-
- if (gdefTableEntry) {
- var gdefTable = uncompressTable(data, gdefTableEntry);
- font.tables.gdef = gdef.parse(gdefTable.data, gdefTable.offset);
- }
-
- if (gposTableEntry) {
- var gposTable = uncompressTable(data, gposTableEntry);
- font.tables.gpos = gpos.parse(gposTable.data, gposTable.offset);
- font.position.init();
- }
-
- if (gsubTableEntry) {
- var gsubTable = uncompressTable(data, gsubTableEntry);
- font.tables.gsub = gsub.parse(gsubTable.data, gsubTable.offset);
- }
-
- if (fvarTableEntry) {
- var fvarTable = uncompressTable(data, fvarTableEntry);
- font.tables.fvar = fvar.parse(fvarTable.data, fvarTable.offset, font.names);
- }
-
- if (metaTableEntry) {
- var metaTable = uncompressTable(data, metaTableEntry);
- font.tables.meta = meta.parse(metaTable.data, metaTable.offset);
- font.metas = font.tables.meta;
- }
-
- return font;
-}
-
-/**
- * Asynchronously load the font from a URL or a filesystem. When done, call the callback
- * with two arguments `(err, font)`. The `err` will be null on success,
- * the `font` is a Font object.
- * We use the node.js callback convention so that
- * opentype.js can integrate with frameworks like async.js.
- * @alias opentype.load
- * @param {string} url - The URL of the font to load.
- * @param {Function} callback - The callback.
- */
-function load(url, callback, opt) {
- opt = (opt === undefined || opt === null) ? {} : opt;
- var isNode = typeof window === 'undefined';
- var loadFn = isNode && !opt.isUrl ? loadFromFile : loadFromUrl;
-
- return new Promise(function (resolve, reject) {
- loadFn(url, function(err, arrayBuffer) {
- if (err) {
- if (callback) {
- return callback(err);
- } else {
- reject(err);
- }
- }
- var font;
- try {
- font = parseBuffer(arrayBuffer, opt);
- } catch (e) {
- if (callback) {
- return callback(e, null);
- } else {
- reject(e);
- }
- }
- if (callback) {
- return callback(null, font);
- } else {
- resolve(font);
- }
- });
- });
-}
-
-/**
- * Synchronously load the font from a URL or file.
- * When done, returns the font object or throws an error.
- * @alias opentype.loadSync
- * @param {string} url - The URL of the font to load.
- * @param {Object} opt - opt.lowMemory
- * @return {opentype.Font}
- */
-function loadSync(url, opt) {
- var fs = require('fs');
- var buffer = fs.readFileSync(url);
- return parseBuffer(nodeBufferToArrayBuffer(buffer), opt);
-}
-
-var opentype = /*#__PURE__*/Object.freeze({
- __proto__: null,
- Font: Font,
- Glyph: Glyph,
- Path: Path,
- BoundingBox: BoundingBox,
- _parse: parse,
- parse: parseBuffer,
- load: load,
- loadSync: loadSync
-});
-
-export default opentype;
-export { BoundingBox, Font, Glyph, Path, parse as _parse, load, loadSync, parseBuffer as parse };
-//# sourceMappingURL=opentype.module.js.map
diff --git a/bluebey-studio/vendor/lib/opentype.module.js.LICENSE b/bluebey-studio/vendor/lib/opentype.module.js.LICENSE
deleted file mode 100644
index 023e618..0000000
--- a/bluebey-studio/vendor/lib/opentype.module.js.LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2020 Frederik De Bleser
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/bluebey-studio/vendor/lib/opentype.module.js.VERSION b/bluebey-studio/vendor/lib/opentype.module.js.VERSION
deleted file mode 100644
index a0023af..0000000
--- a/bluebey-studio/vendor/lib/opentype.module.js.VERSION
+++ /dev/null
@@ -1 +0,0 @@
-opentype.js 1.3.4
diff --git a/bluebey-studio/vendor/three/LICENSE b/bluebey-studio/vendor/three/LICENSE
deleted file mode 100644
index 8ada2a5..0000000
--- a/bluebey-studio/vendor/three/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License
-
-Copyright © 2010-2026 three.js authors
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/bluebey-studio/vendor/three/VERSION b/bluebey-studio/vendor/three/VERSION
deleted file mode 100644
index 3ec66c8..0000000
--- a/bluebey-studio/vendor/three/VERSION
+++ /dev/null
@@ -1 +0,0 @@
-0.186.0
diff --git a/bluebey-studio/vendor/three/build/three.core.js b/bluebey-studio/vendor/three/build/three.core.js
deleted file mode 100644
index 0a1d6cb..0000000
--- a/bluebey-studio/vendor/three/build/three.core.js
+++ /dev/null
@@ -1,60586 +0,0 @@
-/**
- * @license
- * Copyright 2010-2026 Three.js Authors
- * SPDX-License-Identifier: MIT
- */
-const REVISION = '186';
-
-/**
- * Represents mouse buttons and interaction types in context of controls.
- *
- * @type {ConstantsMouse}
- * @constant
- */
-const MOUSE = { LEFT: 0, MIDDLE: 1, RIGHT: 2, ROTATE: 0, DOLLY: 1, PAN: 2 };
-
-/**
- * Represents touch interaction types in context of controls.
- *
- * @type {ConstantsTouch}
- * @constant
- */
-const TOUCH = { ROTATE: 0, PAN: 1, DOLLY_PAN: 2, DOLLY_ROTATE: 3 };
-
-/**
- * Disables face culling.
- *
- * @type {number}
- * @constant
- */
-const CullFaceNone = 0;
-
-/**
- * Culls back faces.
- *
- * @type {number}
- * @constant
- */
-const CullFaceBack = 1;
-
-/**
- * Culls front faces.
- *
- * @type {number}
- * @constant
- */
-const CullFaceFront = 2;
-
-/**
- * Culls both front and back faces.
- *
- * @type {number}
- * @constant
- */
-const CullFaceFrontBack = 3;
-
-/**
- * Gives unfiltered shadow maps - fastest, but lowest quality.
- *
- * @type {number}
- * @constant
- */
-const BasicShadowMap = 0;
-
-/**
- * Filters shadow maps using the Percentage-Closer Filtering (PCF) algorithm.
- *
- * @type {number}
- * @constant
- */
-const PCFShadowMap = 1;
-
-/**
- * Filters shadow maps using the Percentage-Closer Filtering (PCF) algorithm with
- * better soft shadows especially when using low-resolution shadow maps.
- *
- * @type {number}
- * @constant
- * @deprecated since r186. Use `PCFShadowMap` instead.
- */
-const PCFSoftShadowMap = 2;
-
-/**
- * Filters shadow maps using the Variance Shadow Map (VSM) algorithm.
- * When using VSMShadowMap all shadow receivers will also cast shadows.
- *
- * @type {number}
- * @constant
- */
-const VSMShadowMap = 3;
-
-/**
- * Only front faces are rendered.
- *
- * @type {number}
- * @constant
- */
-const FrontSide = 0;
-
-/**
- * Only back faces are rendered.
- *
- * @type {number}
- * @constant
- */
-const BackSide = 1;
-
-/**
- * Both front and back faces are rendered.
- *
- * @type {number}
- * @constant
- */
-const DoubleSide = 2;
-
-/**
- * No blending is performed which effectively disables
- * alpha transparency.
- *
- * @type {number}
- * @constant
- */
-const NoBlending = 0;
-
-/**
- * The default blending.
- *
- * @type {number}
- * @constant
- */
-const NormalBlending = 1;
-
-/**
- * Represents additive blending.
- *
- * @type {number}
- * @constant
- */
-const AdditiveBlending = 2;
-
-/**
- * Represents subtractive blending.
- *
- * @type {number}
- * @constant
- */
-const SubtractiveBlending = 3;
-
-/**
- * Represents multiply blending.
- *
- * @type {number}
- * @constant
- */
-const MultiplyBlending = 4;
-
-/**
- * Represents custom blending.
- *
- * @type {number}
- * @constant
- */
-const CustomBlending = 5;
-
-/**
- * Represents material blending.
- *
- * @type {number}
- * @constant
- */
-const MaterialBlending = 6;
-
-/**
- * A `source + destination` blending equation.
- *
- * @type {number}
- * @constant
- */
-const AddEquation = 100;
-
-/**
- * A `source - destination` blending equation.
- *
- * @type {number}
- * @constant
- */
-const SubtractEquation = 101;
-
-/**
- * A `destination - source` blending equation.
- *
- * @type {number}
- * @constant
- */
-const ReverseSubtractEquation = 102;
-
-/**
- * A blend equation that uses the minimum of source and destination.
- *
- * @type {number}
- * @constant
- */
-const MinEquation = 103;
-
-/**
- * A blend equation that uses the maximum of source and destination.
- *
- * @type {number}
- * @constant
- */
-const MaxEquation = 104;
-
-/**
- * Multiplies all colors by `0`.
- *
- * @type {number}
- * @constant
- */
-const ZeroFactor = 200;
-
-/**
- * Multiplies all colors by `1`.
- *
- * @type {number}
- * @constant
- */
-const OneFactor = 201;
-
-/**
- * Multiplies all colors by the source colors.
- *
- * @type {number}
- * @constant
- */
-const SrcColorFactor = 202;
-
-/**
- * Multiplies all colors by `1` minus each source color.
- *
- * @type {number}
- * @constant
- */
-const OneMinusSrcColorFactor = 203;
-
-/**
- * Multiplies all colors by the source alpha value.
- *
- * @type {number}
- * @constant
- */
-const SrcAlphaFactor = 204;
-
-/**
- * Multiplies all colors by 1 minus the source alpha value.
- *
- * @type {number}
- * @constant
- */
-const OneMinusSrcAlphaFactor = 205;
-
-/**
- * Multiplies all colors by the destination alpha value.
- *
- * @type {number}
- * @constant
- */
-const DstAlphaFactor = 206;
-
-/**
- * Multiplies all colors by `1` minus the destination alpha value.
- *
- * @type {number}
- * @constant
- */
-const OneMinusDstAlphaFactor = 207;
-
-/**
- * Multiplies all colors by the destination color.
- *
- * @type {number}
- * @constant
- */
-const DstColorFactor = 208;
-
-/**
- * Multiplies all colors by `1` minus each destination color.
- *
- * @type {number}
- * @constant
- */
-const OneMinusDstColorFactor = 209;
-
-/**
- * Multiplies the RGB colors by the smaller of either the source alpha
- * value or the value of `1` minus the destination alpha value. The alpha
- * value is multiplied by `1`.
- *
- * @type {number}
- * @constant
- */
-const SrcAlphaSaturateFactor = 210;
-
-/**
- * Multiplies all colors by a constant color.
- *
- * @type {number}
- * @constant
- */
-const ConstantColorFactor = 211;
-
-/**
- * Multiplies all colors by `1` minus a constant color.
- *
- * @type {number}
- * @constant
- */
-const OneMinusConstantColorFactor = 212;
-
-/**
- * Multiplies all colors by a constant alpha value.
- *
- * @type {number}
- * @constant
- */
-const ConstantAlphaFactor = 213;
-
-/**
- * Multiplies all colors by 1 minus a constant alpha value.
- *
- * @type {number}
- * @constant
- */
-const OneMinusConstantAlphaFactor = 214;
-
-/**
- * Never pass.
- *
- * @type {number}
- * @constant
- */
-const NeverDepth = 0;
-
-/**
- * Always pass.
- *
- * @type {number}
- * @constant
- */
-const AlwaysDepth = 1;
-
-/**
- * Pass if the incoming value is less than the depth buffer value.
- *
- * @type {number}
- * @constant
- */
-const LessDepth = 2;
-
-/**
- * Pass if the incoming value is less than or equal to the depth buffer value.
- *
- * @type {number}
- * @constant
- */
-const LessEqualDepth = 3;
-
-/**
- * Pass if the incoming value equals the depth buffer value.
- *
- * @type {number}
- * @constant
- */
-const EqualDepth = 4;
-
-/**
- * Pass if the incoming value is greater than or equal to the depth buffer value.
- *
- * @type {number}
- * @constant
- */
-const GreaterEqualDepth = 5;
-
-/**
- * Pass if the incoming value is greater than the depth buffer value.
- *
- * @type {number}
- * @constant
- */
-const GreaterDepth = 6;
-
-/**
- * Pass if the incoming value is not equal to the depth buffer value.
- *
- * @type {number}
- * @constant
- */
-const NotEqualDepth = 7;
-
-/**
- * Multiplies the environment map color with the surface color.
- *
- * @type {number}
- * @constant
- */
-const MultiplyOperation = 0;
-
-/**
- * Uses reflectivity to blend between the two colors.
- *
- * @type {number}
- * @constant
- */
-const MixOperation = 1;
-
-/**
- * Adds the two colors.
- *
- * @type {number}
- * @constant
- */
-const AddOperation = 2;
-
-/**
- * No tone mapping is applied.
- *
- * @type {number}
- * @constant
- */
-const NoToneMapping = 0;
-
-/**
- * Linear tone mapping.
- *
- * @type {number}
- * @constant
- */
-const LinearToneMapping = 1;
-
-/**
- * Reinhard tone mapping.
- *
- * @type {number}
- * @constant
- */
-const ReinhardToneMapping = 2;
-
-/**
- * Cineon tone mapping.
- *
- * @type {number}
- * @constant
- */
-const CineonToneMapping = 3;
-
-/**
- * ACES Filmic tone mapping.
- *
- * @type {number}
- * @constant
- */
-const ACESFilmicToneMapping = 4;
-
-/**
- * Custom tone mapping.
- *
- * Expects a custom implementation by modifying shader code of the material's fragment shader.
- *
- * @type {number}
- * @constant
- */
-const CustomToneMapping = 5;
-
-/**
- * AgX tone mapping.
- *
- * @type {number}
- * @constant
- */
-const AgXToneMapping = 6;
-
-/**
- * Neutral tone mapping.
- *
- * Implementation based on the Khronos 3D Commerce Group standard tone mapping.
- *
- * @type {number}
- * @constant
- */
-const NeutralToneMapping = 7;
-
-/**
- * The skinned mesh shares the same world space as the skeleton.
- *
- * @type {string}
- * @constant
- */
-const AttachedBindMode = 'attached';
-
-/**
- * The skinned mesh does not share the same world space as the skeleton.
- * This is useful when a skeleton is shared across multiple skinned meshes.
- *
- * @type {string}
- * @constant
- */
-const DetachedBindMode = 'detached';
-
-/**
- * Maps textures using the geometry's UV coordinates.
- *
- * @type {number}
- * @constant
- */
-const UVMapping = 300;
-
-/**
- * Reflection mapping for cube textures.
- *
- * @type {number}
- * @constant
- */
-const CubeReflectionMapping = 301;
-
-/**
- * Refraction mapping for cube textures.
- *
- * @type {number}
- * @constant
- */
-const CubeRefractionMapping = 302;
-
-/**
- * Reflection mapping for equirectangular textures.
- *
- * @type {number}
- * @constant
- */
-const EquirectangularReflectionMapping = 303;
-
-/**
- * Refraction mapping for equirectangular textures.
- *
- * @type {number}
- * @constant
- */
-const EquirectangularRefractionMapping = 304;
-
-/**
- * Reflection mapping for PMREM textures.
- *
- * @type {number}
- * @constant
- */
-const CubeUVReflectionMapping = 306;
-
-/**
- * The texture will simply repeat to infinity.
- *
- * @type {number}
- * @constant
- */
-const RepeatWrapping = 1000;
-
-/**
- * The last pixel of the texture stretches to the edge of the mesh.
- *
- * @type {number}
- * @constant
- */
-const ClampToEdgeWrapping = 1001;
-
-/**
- * The texture will repeats to infinity, mirroring on each repeat.
- *
- * @type {number}
- * @constant
- */
-const MirroredRepeatWrapping = 1002;
-
-/**
- * Returns the value of the texture element that is nearest (in Manhattan distance)
- * to the specified texture coordinates.
- *
- * @type {number}
- * @constant
- */
-const NearestFilter = 1003;
-
-/**
- * Chooses the mipmap that most closely matches the size of the pixel being textured
- * and uses the `NearestFilter` criterion (the texel nearest to the center of the pixel)
- * to produce a texture value.
- *
- * @type {number}
- * @constant
- */
-const NearestMipmapNearestFilter = 1004;
-const NearestMipMapNearestFilter = 1004; // legacy
-
-/**
- * Chooses the two mipmaps that most closely match the size of the pixel being textured and
- * uses the `NearestFilter` criterion to produce a texture value from each mipmap.
- * The final texture value is a weighted average of those two values.
- *
- * @type {number}
- * @constant
- */
-const NearestMipmapLinearFilter = 1005;
-const NearestMipMapLinearFilter = 1005; // legacy
-
-/**
- * Returns the weighted average of the four texture elements that are closest to the specified
- * texture coordinates, and can include items wrapped or repeated from other parts of a texture,
- * depending on the values of `wrapS` and `wrapT`, and on the exact mapping.
- *
- * @type {number}
- * @constant
- */
-const LinearFilter = 1006;
-
-/**
- * Chooses the mipmap that most closely matches the size of the pixel being textured and uses
- * the `LinearFilter` criterion (a weighted average of the four texels that are closest to the
- * center of the pixel) to produce a texture value.
- *
- * @type {number}
- * @constant
- */
-const LinearMipmapNearestFilter = 1007;
-const LinearMipMapNearestFilter = 1007; // legacy
-
-/**
- * Chooses the two mipmaps that most closely match the size of the pixel being textured and uses
- * the `LinearFilter` criterion to produce a texture value from each mipmap. The final texture value
- * is a weighted average of those two values.
- *
- * @type {number}
- * @constant
- */
-const LinearMipmapLinearFilter = 1008;
-const LinearMipMapLinearFilter = 1008; // legacy
-
-/**
- * An unsigned byte data type for textures.
- *
- * @type {number}
- * @constant
- */
-const UnsignedByteType = 1009;
-
-/**
- * A byte data type for textures.
- *
- * @type {number}
- * @constant
- */
-const ByteType = 1010;
-
-/**
- * A short data type for textures.
- *
- * @type {number}
- * @constant
- */
-const ShortType = 1011;
-
-/**
- * An unsigned short data type for textures.
- *
- * @type {number}
- * @constant
- */
-const UnsignedShortType = 1012;
-
-/**
- * An int data type for textures.
- *
- * @type {number}
- * @constant
- */
-const IntType = 1013;
-
-/**
- * An unsigned int data type for textures.
- *
- * @type {number}
- * @constant
- */
-const UnsignedIntType = 1014;
-
-/**
- * A float data type for textures.
- *
- * @type {number}
- * @constant
- */
-const FloatType = 1015;
-
-/**
- * A half float data type for textures.
- *
- * @type {number}
- * @constant
- */
-const HalfFloatType = 1016;
-
-/**
- * An unsigned short 4_4_4_4 (packed) data type for textures.
- *
- * @type {number}
- * @constant
- */
-const UnsignedShort4444Type = 1017;
-
-/**
- * An unsigned short 5_5_5_1 (packed) data type for textures.
- *
- * @type {number}
- * @constant
- */
-const UnsignedShort5551Type = 1018;
-
-/**
- * An unsigned int 24_8 data type for textures.
- *
- * @type {number}
- * @constant
- */
-const UnsignedInt248Type = 1020;
-
-/**
- * An unsigned int 5_9_9_9 (packed) data type for textures.
- *
- * @type {number}
- * @constant
- */
-const UnsignedInt5999Type = 35902;
-
-/**
- * An unsigned int 10_11_11 (packed) data type for textures.
- *
- * @type {number}
- * @constant
- */
-const UnsignedInt101111Type = 35899;
-
-/**
- * Discards the red, green and blue components and reads just the alpha component.
- *
- * @type {number}
- * @constant
- */
-const AlphaFormat = 1021;
-
-/**
- * Discards the alpha component and reads the red, green and blue component.
- *
- * @type {number}
- * @constant
- */
-const RGBFormat = 1022;
-
-/**
- * Reads the red, green, blue and alpha components.
- *
- * @type {number}
- * @constant
- */
-const RGBAFormat = 1023;
-
-/**
- * Reads each element as a single depth value, converts it to floating point, and clamps to the range `[0,1]`.
- *
- * @type {number}
- * @constant
- */
-const DepthFormat = 1026;
-
-/**
- * Reads each element is a pair of depth and stencil values. The depth component of the pair is interpreted as
- * in `DepthFormat`. The stencil component is interpreted based on the depth + stencil internal format.
- *
- * @type {number}
- * @constant
- */
-const DepthStencilFormat = 1027;
-
-/**
- * Discards the green, blue and alpha components and reads just the red component.
- *
- * @type {number}
- * @constant
- */
-const RedFormat = 1028;
-
-/**
- * Discards the green, blue and alpha components and reads just the red component. The texels are read as integers instead of floating point.
- *
- * @type {number}
- * @constant
- */
-const RedIntegerFormat = 1029;
-
-/**
- * Discards the alpha, and blue components and reads the red, and green components.
- *
- * @type {number}
- * @constant
- */
-const RGFormat = 1030;
-
-/**
- * Discards the alpha, and blue components and reads the red, and green components. The texels are read as integers instead of floating point.
- *
- * @type {number}
- * @constant
- */
-const RGIntegerFormat = 1031;
-
-/**
- * Discards the alpha component and reads the red, green and blue component. The texels are read as integers instead of floating point.
- *
- * @type {number}
- * @constant
- */
-const RGBIntegerFormat = 1032;
-
-/**
- * Reads the red, green, blue and alpha components. The texels are read as integers instead of floating point.
- *
- * @type {number}
- * @constant
- */
-const RGBAIntegerFormat = 1033;
-
-/**
- * A DXT1-compressed image in an RGB image format.
- *
- * @type {number}
- * @constant
- */
-const RGB_S3TC_DXT1_Format = 33776;
-
-/**
- * A DXT1-compressed image in an RGB image format with a simple on/off alpha value.
- *
- * @type {number}
- * @constant
- */
-const RGBA_S3TC_DXT1_Format = 33777;
-
-/**
- * A DXT3-compressed image in an RGBA image format. Compared to a 32-bit RGBA texture, it offers 4:1 compression.
- *
- * @type {number}
- * @constant
- */
-const RGBA_S3TC_DXT3_Format = 33778;
-
-/**
- * A DXT5-compressed image in an RGBA image format. It also provides a 4:1 compression, but differs to the DXT3
- * compression in how the alpha compression is done.
- *
- * @type {number}
- * @constant
- */
-const RGBA_S3TC_DXT5_Format = 33779;
-
-/**
- * PVRTC RGB compression in 4-bit mode. One block for each 4×4 pixels.
- *
- * @type {number}
- * @constant
- */
-const RGB_PVRTC_4BPPV1_Format = 35840;
-
-/**
- * PVRTC RGB compression in 2-bit mode. One block for each 8×4 pixels.
- *
- * @type {number}
- * @constant
- */
-const RGB_PVRTC_2BPPV1_Format = 35841;
-
-/**
- * PVRTC RGBA compression in 4-bit mode. One block for each 4×4 pixels.
- *
- * @type {number}
- * @constant
- */
-const RGBA_PVRTC_4BPPV1_Format = 35842;
-
-/**
- * PVRTC RGBA compression in 2-bit mode. One block for each 8×4 pixels.
- *
- * @type {number}
- * @constant
- */
-const RGBA_PVRTC_2BPPV1_Format = 35843;
-
-/**
- * ETC1 RGB format.
- *
- * @type {number}
- * @constant
- */
-const RGB_ETC1_Format = 36196;
-
-/**
- * ETC2 RGB format.
- *
- * @type {number}
- * @constant
- */
-const RGB_ETC2_Format = 37492;
-
-/**
- * ETC2 RGBA format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ETC2_EAC_Format = 37496;
-
-/**
- * EAC R11 UNORM format.
- *
- * @type {number}
- * @constant
- */
-const R11_EAC_Format = 37488; // 0x9270
-
-/**
- * EAC R11 SNORM format.
- *
- * @type {number}
- * @constant
- */
-const SIGNED_R11_EAC_Format = 37489; // 0x9271
-
-/**
- * EAC RG11 UNORM format.
- *
- * @type {number}
- * @constant
- */
-const RG11_EAC_Format = 37490; // 0x9272
-
-/**
- * EAC RG11 SNORM format.
- *
- * @type {number}
- * @constant
- */
-const SIGNED_RG11_EAC_Format = 37491; // 0x9273
-
-/**
- * ASTC RGBA 4x4 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_4x4_Format = 37808;
-
-/**
- * ASTC RGBA 5x4 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_5x4_Format = 37809;
-
-/**
- * ASTC RGBA 5x5 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_5x5_Format = 37810;
-
-/**
- * ASTC RGBA 6x5 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_6x5_Format = 37811;
-
-/**
- * ASTC RGBA 6x6 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_6x6_Format = 37812;
-
-/**
- * ASTC RGBA 8x5 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_8x5_Format = 37813;
-
-/**
- * ASTC RGBA 8x6 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_8x6_Format = 37814;
-
-/**
- * ASTC RGBA 8x8 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_8x8_Format = 37815;
-
-/**
- * ASTC RGBA 10x5 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_10x5_Format = 37816;
-
-/**
- * ASTC RGBA 10x6 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_10x6_Format = 37817;
-
-/**
- * ASTC RGBA 10x8 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_10x8_Format = 37818;
-
-/**
- * ASTC RGBA 10x10 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_10x10_Format = 37819;
-
-/**
- * ASTC RGBA 12x10 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_12x10_Format = 37820;
-
-/**
- * ASTC RGBA 12x12 format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_ASTC_12x12_Format = 37821;
-
-/**
- * BPTC RGBA format.
- *
- * @type {number}
- * @constant
- */
-const RGBA_BPTC_Format = 36492;
-
-/**
- * BPTC Signed RGB format.
- *
- * @type {number}
- * @constant
- */
-const RGB_BPTC_SIGNED_Format = 36494;
-
-/**
- * BPTC Unsigned RGB format.
- *
- * @type {number}
- * @constant
- */
-const RGB_BPTC_UNSIGNED_Format = 36495;
-
-/**
- * RGTC1 Red format.
- *
- * @type {number}
- * @constant
- */
-const RED_RGTC1_Format = 36283;
-
-/**
- * RGTC1 Signed Red format.
- *
- * @type {number}
- * @constant
- */
-const SIGNED_RED_RGTC1_Format = 36284;
-
-/**
- * RGTC2 Red Green format.
- *
- * @type {number}
- * @constant
- */
-const RED_GREEN_RGTC2_Format = 36285;
-
-/**
- * RGTC2 Signed Red Green format.
- *
- * @type {number}
- * @constant
- */
-const SIGNED_RED_GREEN_RGTC2_Format = 36286;
-
-/**
- * Animations are played once.
- *
- * @type {number}
- * @constant
- */
-const LoopOnce = 2200;
-
-/**
- * Animations are played with a chosen number of repetitions, each time jumping from
- * the end of the clip directly to its beginning.
- *
- * @type {number}
- * @constant
- */
-const LoopRepeat = 2201;
-
-/**
- * Animations are played with a chosen number of repetitions, alternately playing forward
- * and backward.
- *
- * @type {number}
- * @constant
- */
-const LoopPingPong = 2202;
-
-/**
- * Discrete interpolation mode for keyframe tracks.
- *
- * @type {number}
- * @constant
- */
-const InterpolateDiscrete = 2300;
-
-/**
- * Linear interpolation mode for keyframe tracks.
- *
- * @type {number}
- * @constant
- */
-const InterpolateLinear = 2301;
-
-/**
- * Smooth interpolation mode for keyframe tracks.
- *
- * @type {number}
- * @constant
- */
-const InterpolateSmooth = 2302;
-
-/**
- * Bezier interpolation mode for keyframe tracks.
- *
- * Uses cubic Bezier curves with explicit 2D control points.
- * Requires tangent data to be set on the track.
- *
- * @type {number}
- * @constant
- */
-const InterpolateBezier = 2303;
-
-/**
- * Zero curvature ending for animations.
- *
- * @type {number}
- * @constant
- */
-const ZeroCurvatureEnding = 2400;
-
-/**
- * Zero slope ending for animations.
- *
- * @type {number}
- * @constant
- */
-const ZeroSlopeEnding = 2401;
-
-/**
- * Wrap around ending for animations.
- *
- * @type {number}
- * @constant
- */
-const WrapAroundEnding = 2402;
-
-/**
- * Default animation blend mode.
- *
- * @type {number}
- * @constant
- */
-const NormalAnimationBlendMode = 2500;
-
-/**
- * Additive animation blend mode. Can be used to layer motions on top of
- * each other to build complex performances from smaller re-usable assets.
- *
- * @type {number}
- * @constant
- */
-const AdditiveAnimationBlendMode = 2501;
-
-/**
- * For every three vertices draw a single triangle.
- *
- * @type {number}
- * @constant
- */
-const TrianglesDrawMode = 0;
-
-/**
- * For each vertex draw a triangle from the last three vertices.
- *
- * @type {number}
- * @constant
- */
-const TriangleStripDrawMode = 1;
-
-/**
- * For each vertex draw a triangle from the first vertex and the last two vertices.
- *
- * @type {number}
- * @constant
- */
-const TriangleFanDrawMode = 2;
-
-/**
- * The depth value is inverted (1.0 - z) for visualization purposes.
- *
- * @type {number}
- * @constant
- */
-const BasicDepthPacking = 3200;
-
-/**
- * The depth value is packed into 32 bit RGBA.
- *
- * @type {number}
- * @constant
- */
-const RGBADepthPacking = 3201;
-
-/**
- * The depth value is packed into 24 bit RGB.
- *
- * @type {number}
- * @constant
- */
-const RGBDepthPacking = 3202;
-
-/**
- * The depth value is packed into 16 bit RG.
- *
- * @type {number}
- * @constant
- */
-const RGDepthPacking = 3203;
-
-/**
- * Normal information is relative to the underlying surface.
- *
- * @type {number}
- * @constant
- */
-const TangentSpaceNormalMap = 0;
-
-/**
- * Normal information is relative to the object orientation.
- *
- * @type {number}
- * @constant
- */
-const ObjectSpaceNormalMap = 1;
-
-// Color space string identifiers, matching CSS Color Module Level 4 and WebGPU names where available.
-
-/**
- * No color space.
- *
- * @type {string}
- * @constant
- */
-const NoColorSpace = '';
-
-/**
- * sRGB color space.
- *
- * @type {string}
- * @constant
- */
-const SRGBColorSpace = 'srgb';
-
-/**
- * sRGB-linear color space.
- *
- * @type {string}
- * @constant
- */
-const LinearSRGBColorSpace = 'srgb-linear';
-
-/**
- * Linear transfer function.
- *
- * @type {string}
- * @constant
- */
-const LinearTransfer = 'linear';
-
-/**
- * sRGB transfer function.
- *
- * @type {string}
- * @constant
- */
-const SRGBTransfer = 'srgb';
-
-/**
- * No normal map packing.
- *
- * @type {string}
- * @constant
- */
-const NoNormalPacking = '';
-
-/**
- * Normal RG packing.
- *
- * @type {string}
- * @constant
- */
-const NormalRGPacking = 'rg';
-
-/**
- * Normal GA packing.
- *
- * @type {string}
- * @constant
- */
-const NormalGAPacking = 'ga';
-
-/**
- * Sets the stencil buffer value to `0`.
- *
- * @type {number}
- * @constant
- */
-const ZeroStencilOp = 0;
-
-/**
- * Keeps the current value.
- *
- * @type {number}
- * @constant
- */
-const KeepStencilOp = 7680;
-
-/**
- * Sets the stencil buffer value to the specified reference value.
- *
- * @type {number}
- * @constant
- */
-const ReplaceStencilOp = 7681;
-
-/**
- * Increments the current stencil buffer value. Clamps to the maximum representable unsigned value.
- *
- * @type {number}
- * @constant
- */
-const IncrementStencilOp = 7682;
-
-/**
- * Decrements the current stencil buffer value. Clamps to `0`.
- *
- * @type {number}
- * @constant
- */
-const DecrementStencilOp = 7683;
-
-/**
- * Increments the current stencil buffer value. Wraps stencil buffer value to zero when incrementing
- * the maximum representable unsigned value.
- *
- * @type {number}
- * @constant
- */
-const IncrementWrapStencilOp = 34055;
-
-/**
- * Decrements the current stencil buffer value. Wraps stencil buffer value to the maximum representable
- * unsigned value when decrementing a stencil buffer value of `0`.
- *
- * @type {number}
- * @constant
- */
-const DecrementWrapStencilOp = 34056;
-
-/**
- * Inverts the current stencil buffer value bitwise.
- *
- * @type {number}
- * @constant
- */
-const InvertStencilOp = 5386;
-
-/**
- * Will never return true.
- *
- * @type {number}
- * @constant
- */
-const NeverStencilFunc = 512;
-
-/**
- * Will return true if the stencil reference value is less than the current stencil value.
- *
- * @type {number}
- * @constant
- */
-const LessStencilFunc = 513;
-
-/**
- * Will return true if the stencil reference value is equal to the current stencil value.
- *
- * @type {number}
- * @constant
- */
-const EqualStencilFunc = 514;
-
-/**
- * Will return true if the stencil reference value is less than or equal to the current stencil value.
- *
- * @type {number}
- * @constant
- */
-const LessEqualStencilFunc = 515;
-
-/**
- * Will return true if the stencil reference value is greater than the current stencil value.
- *
- * @type {number}
- * @constant
- */
-const GreaterStencilFunc = 516;
-
-/**
- * Will return true if the stencil reference value is not equal to the current stencil value.
- *
- * @type {number}
- * @constant
- */
-const NotEqualStencilFunc = 517;
-
-/**
- * Will return true if the stencil reference value is greater than or equal to the current stencil value.
- *
- * @type {number}
- * @constant
- */
-const GreaterEqualStencilFunc = 518;
-
-/**
- * Will always return true.
- *
- * @type {number}
- * @constant
- */
-const AlwaysStencilFunc = 519;
-
-/**
- * Never pass.
- *
- * @type {number}
- * @constant
- */
-const NeverCompare = 512;
-
-/**
- * Pass if the incoming value is less than the texture value.
- *
- * @type {number}
- * @constant
- */
-const LessCompare = 513;
-
-/**
- * Pass if the incoming value equals the texture value.
- *
- * @type {number}
- * @constant
- */
-const EqualCompare = 514;
-
-/**
- * Pass if the incoming value is less than or equal to the texture value.
- *
- * @type {number}
- * @constant
- */
-const LessEqualCompare = 515;
-
-/**
- * Pass if the incoming value is greater than the texture value.
- *
- * @type {number}
- * @constant
- */
-const GreaterCompare = 516;
-
-/**
- * Pass if the incoming value is not equal to the texture value.
- *
- * @type {number}
- * @constant
- */
-const NotEqualCompare = 517;
-
-/**
- * Pass if the incoming value is greater than or equal to the texture value.
- *
- * @type {number}
- * @constant
- */
-const GreaterEqualCompare = 518;
-
-/**
- * Always pass.
- *
- * @type {number}
- * @constant
- */
-const AlwaysCompare = 519;
-
-/**
- * The contents are intended to be specified once by the application, and used many
- * times as the source for drawing and image specification commands.
- *
- * @type {number}
- * @constant
- */
-const StaticDrawUsage = 35044;
-
-/**
- * The contents are intended to be respecified repeatedly by the application, and
- * used many times as the source for drawing and image specification commands.
- *
- * @type {number}
- * @constant
- */
-const DynamicDrawUsage = 35048;
-
-/**
- * The contents are intended to be specified once by the application, and used at most
- * a few times as the source for drawing and image specification commands.
- *
- * @type {number}
- * @constant
- */
-const StreamDrawUsage = 35040;
-
-/**
- * The contents are intended to be specified once by reading data from the 3D API, and queried
- * many times by the application.
- *
- * @type {number}
- * @constant
- */
-const StaticReadUsage = 35045;
-
-/**
- * The contents are intended to be respecified repeatedly by reading data from the 3D API, and queried
- * many times by the application.
- *
- * @type {number}
- * @constant
- */
-const DynamicReadUsage = 35049;
-
-/**
- * The contents are intended to be specified once by reading data from the 3D API, and queried at most
- * a few times by the application
- *
- * @type {number}
- * @constant
- */
-const StreamReadUsage = 35041;
-
-/**
- * The contents are intended to be specified once by reading data from the 3D API, and used many times as
- * the source for WebGL drawing and image specification commands.
- *
- * @type {number}
- * @constant
- */
-const StaticCopyUsage = 35046;
-
-/**
- * The contents are intended to be respecified repeatedly by reading data from the 3D API, and used many times
- * as the source for WebGL drawing and image specification commands.
- *
- * @type {number}
- * @constant
- */
-const DynamicCopyUsage = 35050;
-
-/**
- * The contents are intended to be specified once by reading data from the 3D API, and used at most a few times
- * as the source for WebGL drawing and image specification commands.
- *
- * @type {number}
- * @constant
- */
-const StreamCopyUsage = 35042;
-
-/**
- * GLSL 1 shader code.
- *
- * @type {string}
- * @constant
- */
-const GLSL1 = '100';
-
-/**
- * GLSL 3 shader code.
- *
- * @type {string}
- * @constant
- */
-const GLSL3 = '300 es';
-
-/**
- * WebGL coordinate system.
- *
- * @type {number}
- * @constant
- */
-const WebGLCoordinateSystem = 2000;
-
-/**
- * WebGPU coordinate system.
- *
- * @type {number}
- * @constant
- */
-const WebGPUCoordinateSystem = 2001;
-
-/**
- * Represents the different timestamp query types.
- *
- * @type {ConstantsTimestampQuery}
- * @constant
- */
-const TimestampQuery = {
- COMPUTE: 'compute',
- RENDER: 'render'
-};
-
-/**
- * Represents mouse buttons and interaction types in context of controls.
- *
- * @type {ConstantsInterpolationSamplingType}
- * @constant
- */
-const InterpolationSamplingType = {
- PERSPECTIVE: 'perspective',
- LINEAR: 'linear',
- FLAT: 'flat'
-};
-
-/**
- * Represents the different interpolation sampling modes.
- *
- * @type {ConstantsInterpolationSamplingMode}
- * @constant
- */
-const InterpolationSamplingMode = {
- NORMAL: 'normal',
- CENTROID: 'centroid',
- SAMPLE: 'sample',
- FIRST: 'first',
- EITHER: 'either'
-};
-
-/**
- * Compatibility flags for features that may not be supported across all platforms.
- *
- * @type {Object}
- * @constant
- */
-const Compatibility = {
- TEXTURE_COMPARE: 'depthTextureCompare'
-};
-
-/**
- * Represents the refresh types of render objects.
- *
- * @type {ConstantsRenderObjectRefreshType}
- * @constant
- */
-const RenderObjectRefreshType = {
- NONE: 0,
- SHARED: 1,
- FULL: 2
-};
-
-/**
- * This type represents mouse buttons and interaction types in context of controls.
- *
- * @typedef {Object} ConstantsMouse
- * @property {number} MIDDLE - The left mouse button.
- * @property {number} LEFT - The middle mouse button.
- * @property {number} RIGHT - The right mouse button.
- * @property {number} ROTATE - A rotate interaction.
- * @property {number} DOLLY - A dolly interaction.
- * @property {number} PAN - A pan interaction.
- **/
-
-/**
- * This type represents touch interaction types in context of controls.
- *
- * @typedef {Object} ConstantsTouch
- * @property {number} ROTATE - A rotate interaction.
- * @property {number} PAN - A pan interaction.
- * @property {number} DOLLY_PAN - The dolly-pan interaction.
- * @property {number} DOLLY_ROTATE - A dolly-rotate interaction.
- **/
-
-/**
- * This type represents the different timestamp query types.
- *
- * @typedef {Object} ConstantsTimestampQuery
- * @property {string} COMPUTE - A `compute` timestamp query.
- * @property {string} RENDER - A `render` timestamp query.
- **/
-
-/**
- * Represents the different interpolation sampling types.
- *
- * @typedef {Object} ConstantsInterpolationSamplingType
- * @property {string} PERSPECTIVE - Perspective-correct interpolation.
- * @property {string} LINEAR - Linear interpolation.
- * @property {string} FLAT - Flat interpolation.
- */
-
-/**
- * Represents the different interpolation sampling modes.
- *
- * @typedef {Object} ConstantsInterpolationSamplingMode
- * @property {string} NORMAL - Normal sampling mode.
- * @property {string} CENTROID - Centroid sampling mode.
- * @property {string} SAMPLE - Sample-specific sampling mode.
- * @property {string} FIRST - Flat interpolation using the first vertex.
- * @property {string} EITHER - Flat interpolation using either vertex.
- */
-
-/**
- * Represents the refresh types of render objects.
- *
- * @typedef {Object} ConstantsRenderObjectRefreshType
- * @property {number} NONE - No refresh required.
- * @property {number} SHARED - Only shared uniform buffers require an update.
- * @property {number} FULL - The render object requires a full refresh.
- */
-
-/**
- * Checks if an array contains values that require Uint32 representation.
- *
- * This function determines whether the array contains any values >= 65535,
- * which would require a Uint32Array rather than a Uint16Array for proper storage.
- * The function iterates from the end of the array, assuming larger values are
- * typically located at the end.
- *
- * @private
- * @param {Array} array - The array to check.
- * @return {boolean} True if the array contains values >= 65535, false otherwise.
- */
-function arrayNeedsUint32( array ) {
-
- // assumes larger values usually on last
-
- for ( let i = array.length - 1; i >= 0; -- i ) {
-
- if ( array[ i ] >= 65535 ) return true; // account for PRIMITIVE_RESTART_FIXED_INDEX, #24565
-
- }
-
- return false;
-
-}
-
-/**
- * Map of typed array constructor names to their constructors.
- * This mapping enables dynamic creation of typed arrays based on string type names.
- *
- * @private
- * @constant
- * @type {Object}
- */
-const TYPED_ARRAYS = {
- Int8Array: Int8Array,
- Uint8Array: Uint8Array,
- Uint8ClampedArray: Uint8ClampedArray,
- Int16Array: Int16Array,
- Uint16Array: Uint16Array,
- Int32Array: Int32Array,
- Uint32Array: Uint32Array,
- Float32Array: Float32Array,
- Float64Array: Float64Array
-};
-
-/**
- * Creates a typed array of the specified type from the given buffer.
- *
- * @private
- * @param {string} type - The name of the typed array type (e.g., 'Float32Array', 'Uint16Array').
- * @param {ArrayBuffer} buffer - The buffer to create the typed array from.
- * @return {TypedArray} A new typed array of the specified type.
- */
-function getTypedArray( type, buffer ) {
-
- return new TYPED_ARRAYS[ type ]( buffer );
-
-}
-
-/**
- * Returns `true` if the given object is a typed array.
- *
- * @param {any} array - The object to check.
- * @return {boolean} Whether the given object is a typed array.
- */
-function isTypedArray( array ) {
-
- return ArrayBuffer.isView( array ) && ! ( array instanceof DataView );
-
-}
-
-/**
- * Creates an XHTML element with the specified tag name.
- *
- * This function uses the XHTML namespace to create DOM elements,
- * ensuring proper element creation in XML-based contexts.
- *
- * @private
- * @param {string} name - The tag name of the element to create (e.g., 'canvas', 'div').
- * @return {HTMLElement} The created XHTML element.
- */
-function createElementNS( name ) {
-
- return document.createElementNS( 'http://www.w3.org/1999/xhtml', name );
-
-}
-
-/**
- * Creates a canvas element configured for block display.
- *
- * This is a convenience function that creates a canvas element with
- * display style set to 'block', which is commonly used in three.js
- * rendering contexts to avoid inline element spacing issues.
- *
- * @return {HTMLCanvasElement} A canvas element with display set to 'block'.
- */
-function createCanvasElement() {
-
- const canvas = createElementNS( 'canvas' );
- canvas.style.display = 'block';
- return canvas;
-
-}
-
-/**
- * Internal cache for tracking warning messages to prevent duplicate warnings.
- *
- * @private
- * @type {Object}
- */
-const _cache = {};
-
-/**
- * Custom console function handler for intercepting log, warn, and error calls.
- *
- * @private
- * @type {Function|null}
- */
-let _setConsoleFunction = null;
-
-/**
- * Sets a custom function to handle console output.
- *
- * This allows external code to intercept and handle console.log, console.warn,
- * and console.error calls made by three.js, which is useful for custom logging,
- * testing, or debugging workflows.
- *
- * @param {Function} fn - The function to handle console output. Should accept
- * (type, message, ...params) where type is 'log', 'warn', or 'error'.
- */
-function setConsoleFunction( fn ) {
-
- _setConsoleFunction = fn;
-
-}
-
-/**
- * Gets the currently set custom console function.
- *
- * @return {Function|null} The custom console function, or null if not set.
- */
-function getConsoleFunction() {
-
- return _setConsoleFunction;
-
-}
-
-/**
- * Logs an informational message with the 'THREE.' prefix.
- *
- * If a custom console function is set via setConsoleFunction(), it will be used
- * instead of the native console.log. The first parameter is treated as the
- * method name and is automatically prefixed with 'THREE.'.
- *
- * @param {...any} params - The message components. The first param is used as
- * the method name and prefixed with 'THREE.'.
- */
-function log( ...params ) {
-
- const message = 'THREE.' + params.shift();
-
- if ( _setConsoleFunction ) {
-
- _setConsoleFunction( 'log', message, ...params );
-
- } else {
-
- console.log( message, ...params );
-
- }
-
-}
-
-/**
- * Enhances log/warn/error messages related to TSL.
- *
- * @param {Array} params - The original message parameters.
- * @returns {Array} The filtered and enhanced message parameters.
- */
-function enhanceLogMessage( params ) {
-
- const message = params[ 0 ];
-
- if ( typeof message === 'string' && message.startsWith( 'TSL:' ) ) {
-
- const stackTrace = params[ 1 ];
-
- if ( stackTrace && stackTrace.isStackTrace ) {
-
- params[ 0 ] += ' ' + stackTrace.getLocation();
-
- } else {
-
- params[ 1 ] = 'Stack trace not available. Enable "THREE.Node.captureStackTrace" to capture stack traces.';
-
- }
-
- }
-
- return params;
-
-}
-
-/**
- * Logs a warning message with the 'THREE.' prefix.
- *
- * If a custom console function is set via setConsoleFunction(), it will be used
- * instead of the native console.warn. The first parameter is treated as the
- * method name and is automatically prefixed with 'THREE.'.
- *
- * @param {...any} params - The message components. The first param is used as
- * the method name and prefixed with 'THREE.'.
- */
-function warn( ...params ) {
-
- params = enhanceLogMessage( params );
-
- const message = 'THREE.' + params.shift();
-
- if ( _setConsoleFunction ) {
-
- _setConsoleFunction( 'warn', message, ...params );
-
- } else {
-
- const stackTrace = params[ 0 ];
-
- if ( stackTrace && stackTrace.isStackTrace ) {
-
- console.warn( stackTrace.getError( message ) );
-
- } else {
-
- console.warn( message, ...params );
-
- }
-
- }
-
-}
-
-/**
- * Logs an error message with the 'THREE.' prefix.
- *
- * If a custom console function is set via setConsoleFunction(), it will be used
- * instead of the native console.error. The first parameter is treated as the
- * method name and is automatically prefixed with 'THREE.'.
- *
- * @param {...any} params - The message components. The first param is used as
- * the method name and prefixed with 'THREE.'.
- */
-function error( ...params ) {
-
- params = enhanceLogMessage( params );
-
- const message = 'THREE.' + params.shift();
-
- if ( _setConsoleFunction ) {
-
- _setConsoleFunction( 'error', message, ...params );
-
- } else {
-
- const stackTrace = params[ 0 ];
-
- if ( stackTrace && stackTrace.isStackTrace ) {
-
- console.error( stackTrace.getError( message ) );
-
- } else {
-
- console.error( message, ...params );
-
- }
-
- }
-
-}
-
-/**
- * Logs a warning message only once, preventing duplicate warnings.
- *
- * This function maintains an internal cache of warning messages and will only
- * output each unique warning message once. Useful for warnings that may be
- * triggered repeatedly but should only be shown to the user once.
- *
- * @param {...any} params - The warning message components.
- */
-function warnOnce( ...params ) {
-
- const message = params.join( ' ' );
-
- if ( message in _cache ) return;
-
- _cache[ message ] = true;
-
- warn( ...params );
-
-}
-
-/**
- * Yields execution to the main thread to allow rendering and other tasks.
- * Uses scheduler.yield() when available (Chrome 115+), falls back to requestAnimationFrame.
- *
- * @return {Promise}
- */
-function yieldToMain() {
-
- if ( typeof self !== 'undefined' && typeof self.scheduler !== 'undefined' && typeof self.scheduler.yield !== 'undefined' ) {
-
- return self.scheduler.yield();
-
- }
-
- return new Promise( resolve => {
-
- requestAnimationFrame( resolve );
-
- } );
-
-}
-
-/**
- * Asynchronously probes for WebGL sync object completion.
- *
- * This function creates a promise that resolves when the WebGL sync object
- * signals completion or rejects if the sync operation fails. It uses polling
- * at the specified interval to check the sync status without blocking the
- * main thread. This is useful for GPU-CPU synchronization in WebGL contexts.
- *
- * @private
- * @param {WebGL2RenderingContext} gl - The WebGL rendering context.
- * @param {WebGLSync} sync - The WebGL sync object to wait for.
- * @param {number} interval - The polling interval in milliseconds.
- * @return {Promise} A promise that resolves when the sync completes or rejects if it fails.
- */
-function probeAsync( gl, sync, interval ) {
-
- return new Promise( function ( resolve, reject ) {
-
- function probe() {
-
- switch ( gl.clientWaitSync( sync, gl.SYNC_FLUSH_COMMANDS_BIT, 0 ) ) {
-
- case gl.WAIT_FAILED:
- reject();
- break;
-
- case gl.TIMEOUT_EXPIRED:
- setTimeout( probe, interval );
- break;
-
- default:
- resolve();
-
- }
-
- }
-
- setTimeout( probe, interval );
-
- } );
-
-}
-
-/**
- * Used to select the correct depth functions
- * when reversed depth buffer is used.
- *
- * @private
- * @type {Object}
- */
-const ReversedDepthFuncs = {
- [ NeverDepth ]: AlwaysDepth,
- [ LessDepth ]: GreaterDepth,
- [ EqualDepth ]: NotEqualDepth,
- [ LessEqualDepth ]: GreaterEqualDepth,
-
- [ AlwaysDepth ]: NeverDepth,
- [ GreaterDepth ]: LessDepth,
- [ NotEqualDepth ]: EqualDepth,
- [ GreaterEqualDepth ]: LessEqualDepth,
-};
-
-/**
- * This modules allows to dispatch event objects on custom JavaScript objects.
- *
- * Main repository: [eventdispatcher.js](https://github.com/mrdoob/eventdispatcher.js/)
- *
- * Code Example:
- * ```js
- * class Car extends EventDispatcher {
- * start() {
- * this.dispatchEvent( { type: 'start', message: 'vroom vroom!' } );
- * }
- *};
- *
- * // Using events with the custom object
- * const car = new Car();
- * car.addEventListener( 'start', function ( event ) {
- * alert( event.message );
- * } );
- *
- * car.start();
- * ```
- */
-class EventDispatcher {
-
- /**
- * Adds the given event listener to the given event type.
- *
- * @param {string} type - The type of event to listen to.
- * @param {Function} listener - The function that gets called when the event is fired.
- */
- addEventListener( type, listener ) {
-
- if ( this._listeners === undefined ) this._listeners = {};
-
- const listeners = this._listeners;
-
- if ( listeners[ type ] === undefined ) {
-
- listeners[ type ] = [];
-
- }
-
- if ( listeners[ type ].indexOf( listener ) === -1 ) {
-
- listeners[ type ].push( listener );
-
- }
-
- }
-
- /**
- * Returns `true` if the given event listener has been added to the given event type.
- *
- * @param {string} type - The type of event.
- * @param {Function} listener - The listener to check.
- * @return {boolean} Whether the given event listener has been added to the given event type.
- */
- hasEventListener( type, listener ) {
-
- const listeners = this._listeners;
-
- if ( listeners === undefined ) return false;
-
- return listeners[ type ] !== undefined && listeners[ type ].indexOf( listener ) !== -1;
-
- }
-
- /**
- * Removes the given event listener from the given event type.
- *
- * @param {string} type - The type of event.
- * @param {Function} listener - The listener to remove.
- */
- removeEventListener( type, listener ) {
-
- const listeners = this._listeners;
-
- if ( listeners === undefined ) return;
-
- const listenerArray = listeners[ type ];
-
- if ( listenerArray !== undefined ) {
-
- const index = listenerArray.indexOf( listener );
-
- if ( index !== -1 ) {
-
- listenerArray.splice( index, 1 );
-
- }
-
- }
-
- }
-
- /**
- * Dispatches an event object.
- *
- * @param {Object} event - The event that gets fired.
- */
- dispatchEvent( event ) {
-
- const listeners = this._listeners;
-
- if ( listeners === undefined ) return;
-
- const listenerArray = listeners[ event.type ];
-
- if ( listenerArray !== undefined ) {
-
- event.target = this;
-
- // Make a copy, in case listeners are removed while iterating.
- const array = listenerArray.slice( 0 );
-
- for ( let i = 0, l = array.length; i < l; i ++ ) {
-
- array[ i ].call( this, event );
-
- }
-
- event.target = null;
-
- }
-
- }
-
-}
-
-const _lut = [ '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '0a', '0b', '0c', '0d', '0e', '0f', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '1a', '1b', '1c', '1d', '1e', '1f', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '2a', '2b', '2c', '2d', '2e', '2f', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e', '3f', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '4a', '4b', '4c', '4d', '4e', '4f', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '5a', '5b', '5c', '5d', '5e', '5f', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '7a', '7b', '7c', '7d', '7e', '7f', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '8a', '8b', '8c', '8d', '8e', '8f', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '9a', '9b', '9c', '9d', '9e', '9f', 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'aa', 'ab', 'ac', 'ad', 'ae', 'af', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'ba', 'bb', 'bc', 'bd', 'be', 'bf', 'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8', 'c9', 'ca', 'cb', 'cc', 'cd', 'ce', 'cf', 'd0', 'd1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7', 'd8', 'd9', 'da', 'db', 'dc', 'dd', 'de', 'df', 'e0', 'e1', 'e2', 'e3', 'e4', 'e5', 'e6', 'e7', 'e8', 'e9', 'ea', 'eb', 'ec', 'ed', 'ee', 'ef', 'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff' ];
-
-let _seed = 1234567;
-
-
-const DEG2RAD = Math.PI / 180;
-const RAD2DEG = 180 / Math.PI;
-
-/**
- * Generate a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier)
- * (universally unique identifier).
- *
- * @return {string} The UUID.
- */
-function generateUUID() {
-
- // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136
-
- const d0 = Math.random() * 0xffffffff | 0;
- const d1 = Math.random() * 0xffffffff | 0;
- const d2 = Math.random() * 0xffffffff | 0;
- const d3 = Math.random() * 0xffffffff | 0;
- const uuid = _lut[ d0 & 0xff ] + _lut[ d0 >> 8 & 0xff ] + _lut[ d0 >> 16 & 0xff ] + _lut[ d0 >> 24 & 0xff ] + '-' +
- _lut[ d1 & 0xff ] + _lut[ d1 >> 8 & 0xff ] + '-' + _lut[ d1 >> 16 & 0x0f | 0x40 ] + _lut[ d1 >> 24 & 0xff ] + '-' +
- _lut[ d2 & 0x3f | 0x80 ] + _lut[ d2 >> 8 & 0xff ] + '-' + _lut[ d2 >> 16 & 0xff ] + _lut[ d2 >> 24 & 0xff ] +
- _lut[ d3 & 0xff ] + _lut[ d3 >> 8 & 0xff ] + _lut[ d3 >> 16 & 0xff ] + _lut[ d3 >> 24 & 0xff ];
-
- // .toLowerCase() here flattens concatenated strings to save heap memory space.
- return uuid.toLowerCase();
-
-}
-
-/**
- * Clamps the given value between min and max.
- *
- * @param {number} value - The value to clamp.
- * @param {number} min - The min value.
- * @param {number} max - The max value.
- * @return {number} The clamped value.
- */
-function clamp( value, min, max ) {
-
- return Math.max( min, Math.min( max, value ) );
-
-}
-
-/**
- * Computes the Euclidean modulo of the given parameters that
- * is `( ( n % m ) + m ) % m`.
- *
- * @param {number} n - The first parameter.
- * @param {number} m - The second parameter.
- * @return {number} The Euclidean modulo.
- */
-function euclideanModulo( n, m ) {
-
- // https://en.wikipedia.org/wiki/Modulo_operation
-
- return ( ( n % m ) + m ) % m;
-
-}
-
-/**
- * Performs a linear mapping from range `` to range ``
- * for the given value. `a2` must be greater than `a1`.
- *
- * @param {number} x - The value to be mapped.
- * @param {number} a1 - Minimum value for range A.
- * @param {number} a2 - Maximum value for range A.
- * @param {number} b1 - Minimum value for range B.
- * @param {number} b2 - Maximum value for range B.
- * @return {number} The mapped value.
- */
-function mapLinear( x, a1, a2, b1, b2 ) {
-
- return b1 + ( x - a1 ) * ( b2 - b1 ) / ( a2 - a1 );
-
-}
-
-/**
- * Returns the percentage in the closed interval `[0, 1]` of the given value
- * between the start and end point.
- *
- * @param {number} x - The start point
- * @param {number} y - The end point.
- * @param {number} value - A value between start and end.
- * @return {number} The interpolation factor.
- */
-function inverseLerp( x, y, value ) {
-
- // https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/inverse-lerp-a-super-useful-yet-often-overlooked-function-r5230/
-
- if ( x !== y ) {
-
- return ( value - x ) / ( y - x );
-
- } else {
-
- return 0;
-
- }
-
-}
-
-/**
- * Returns a value linearly interpolated from two known points based on the given interval -
- * `t = 0` will return `x` and `t = 1` will return `y`.
- *
- * @param {number} x - The start point
- * @param {number} y - The end point.
- * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
- * @return {number} The interpolated value.
- */
-function lerp( x, y, t ) {
-
- return ( 1 - t ) * x + t * y;
-
-}
-
-/**
- * Smoothly interpolate a number from `x` to `y` in a spring-like manner using a delta
- * time to maintain frame rate independent movement. For details, see
- * [Frame rate independent damping using lerp](http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/).
- *
- * @param {number} x - The current point.
- * @param {number} y - The target point.
- * @param {number} lambda - A higher lambda value will make the movement more sudden,
- * and a lower value will make the movement more gradual.
- * @param {number} dt - Delta time in seconds.
- * @return {number} The interpolated value.
- */
-function damp( x, y, lambda, dt ) {
-
- return lerp( x, y, 1 - Math.exp( - lambda * dt ) );
-
-}
-
-/**
- * Returns a value that alternates between `0` and the given `length` parameter.
- *
- * @param {number} x - The value to pingpong.
- * @param {number} [length=1] - The positive value the function will pingpong to.
- * @return {number} The alternated value.
- */
-function pingpong( x, length = 1 ) {
-
- // https://www.desmos.com/calculator/vcsjnyz7x4
-
- return length - Math.abs( euclideanModulo( x, length * 2 ) - length );
-
-}
-
-/**
- * Returns a value in the range `[0,1]` that represents the percentage that `x` has
- * moved between `min` and `max`, but smoothed or slowed down the closer `x` is to
- * the `min` and `max`.
- *
- * See [Smoothstep](http://en.wikipedia.org/wiki/Smoothstep) for more details.
- *
- * @param {number} x - The value to evaluate based on its position between `min` and `max`.
- * @param {number} min - The min value. Any `x` value below `min` will be `0`. `min` must be lower than `max`.
- * @param {number} max - The max value. Any `x` value above `max` will be `1`. `max` must be greater than `min`.
- * @return {number} The alternated value.
- */
-function smoothstep( x, min, max ) {
-
- if ( x <= min ) return 0;
- if ( x >= max ) return 1;
-
- x = ( x - min ) / ( max - min );
-
- return x * x * ( 3 - 2 * x );
-
-}
-
-/**
- * A [variation on smoothstep](https://en.wikipedia.org/wiki/Smoothstep#Variations)
- * that has zero 1st and 2nd order derivatives at `x=0` and `x=1`.
- *
- * @param {number} x - The value to evaluate based on its position between `min` and `max`.
- * @param {number} min - The min value. Any `x` value below `min` will be `0`. `min` must be lower than `max`.
- * @param {number} max - The max value. Any `x` value above `max` will be `1`. `max` must be greater than `min`.
- * @return {number} The alternated value.
- */
-function smootherstep( x, min, max ) {
-
- if ( x <= min ) return 0;
- if ( x >= max ) return 1;
-
- x = ( x - min ) / ( max - min );
-
- return x * x * x * ( x * ( x * 6 - 15 ) + 10 );
-
-}
-
-/**
- * Returns a random integer from `` interval.
- *
- * @param {number} low - The lower value boundary.
- * @param {number} high - The upper value boundary
- * @return {number} A random integer.
- */
-function randInt( low, high ) {
-
- return low + Math.floor( Math.random() * ( high - low + 1 ) );
-
-}
-
-/**
- * Returns a random float from `` interval.
- *
- * @param {number} low - The lower value boundary.
- * @param {number} high - The upper value boundary
- * @return {number} A random float.
- */
-function randFloat( low, high ) {
-
- return low + Math.random() * ( high - low );
-
-}
-
-/**
- * Returns a random integer from `<-range/2, range/2>` interval.
- *
- * @param {number} range - Defines the value range.
- * @return {number} A random float.
- */
-function randFloatSpread( range ) {
-
- return range * ( 0.5 - Math.random() );
-
-}
-
-/**
- * Returns a deterministic pseudo-random float in the interval `[0, 1]`.
- *
- * @param {number} [s] - The integer seed.
- * @return {number} A random float.
- */
-function seededRandom( s ) {
-
- if ( s !== undefined ) _seed = s;
-
- // Mulberry32 generator
-
- let t = _seed += 0x6D2B79F5;
-
- t = Math.imul( t ^ t >>> 15, t | 1 );
-
- t ^= t + Math.imul( t ^ t >>> 7, t | 61 );
-
- return ( ( t ^ t >>> 14 ) >>> 0 ) / 4294967296;
-
-}
-
-/**
- * Converts degrees to radians.
- *
- * @param {number} degrees - A value in degrees.
- * @return {number} The converted value in radians.
- */
-function degToRad( degrees ) {
-
- return degrees * DEG2RAD;
-
-}
-
-/**
- * Converts radians to degrees.
- *
- * @param {number} radians - A value in radians.
- * @return {number} The converted value in degrees.
- */
-function radToDeg( radians ) {
-
- return radians * RAD2DEG;
-
-}
-
-/**
- * Returns `true` if the given integer is a power of two.
- *
- * @param {number} value - The value to check.
- * @return {boolean} Whether the given integer is a power of two or not.
- */
-function isPowerOfTwo( value ) {
-
- return value > 0 && Number.isInteger( value ) && 2 ** Math.round( Math.log2( value ) ) === value;
-
-}
-
-/**
- * Returns the smallest power of two that is greater than or equal to the given number.
- *
- * @param {number} value - The value to find a POT for. Must be greater than `0`.
- * @return {number} The smallest power of two that is greater than or equal to the given number.
- */
-function ceilPowerOfTwo( value ) {
-
- return Math.pow( 2, Math.ceil( Math.log( value ) / Math.LN2 ) );
-
-}
-
-/**
- * Returns the largest power of two that is less than or equal to the given number.
- *
- * @param {number} value - The value to find a POT for. Must be greater than `0`.
- * @return {number} The largest power of two that is less than or equal to the given number.
- */
-function floorPowerOfTwo( value ) {
-
- return Math.pow( 2, Math.floor( Math.log( value ) / Math.LN2 ) );
-
-}
-
-/**
- * Sets the given quaternion from the [Intrinsic Proper Euler Angles](https://en.wikipedia.org/wiki/Euler_angles)
- * defined by the given angles and order.
- *
- * Rotations are applied to the axes in the order specified by order:
- * rotation by angle `a` is applied first, then by angle `b`, then by angle `c`.
- *
- * @param {Quaternion} q - The quaternion to set.
- * @param {number} a - The rotation applied to the first axis, in radians.
- * @param {number} b - The rotation applied to the second axis, in radians.
- * @param {number} c - The rotation applied to the third axis, in radians.
- * @param {('XYX'|'XZX'|'YXY'|'YZY'|'ZXZ'|'ZYZ')} order - A string specifying the axes order.
- */
-function setQuaternionFromProperEuler( q, a, b, c, order ) {
-
- const cos = Math.cos;
- const sin = Math.sin;
-
- const c2 = cos( b / 2 );
- const s2 = sin( b / 2 );
-
- const c13 = cos( ( a + c ) / 2 );
- const s13 = sin( ( a + c ) / 2 );
-
- const c1_3 = cos( ( a - c ) / 2 );
- const s1_3 = sin( ( a - c ) / 2 );
-
- const c3_1 = cos( ( c - a ) / 2 );
- const s3_1 = sin( ( c - a ) / 2 );
-
- switch ( order ) {
-
- case 'XYX':
- q.set( c2 * s13, s2 * c1_3, s2 * s1_3, c2 * c13 );
- break;
-
- case 'YZY':
- q.set( s2 * s1_3, c2 * s13, s2 * c1_3, c2 * c13 );
- break;
-
- case 'ZXZ':
- q.set( s2 * c1_3, s2 * s1_3, c2 * s13, c2 * c13 );
- break;
-
- case 'XZX':
- q.set( c2 * s13, s2 * s3_1, s2 * c3_1, c2 * c13 );
- break;
-
- case 'YXY':
- q.set( s2 * c3_1, c2 * s13, s2 * s3_1, c2 * c13 );
- break;
-
- case 'ZYZ':
- q.set( s2 * s3_1, s2 * c3_1, c2 * s13, c2 * c13 );
- break;
-
- default:
- warn( 'MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: ' + order );
-
- }
-
-}
-
-/**
- * Denormalizes the given value according to the given typed array.
- *
- * @param {number} value - The value to denormalize.
- * @param {TypedArray} array - The typed array that defines the data type of the value.
- * @return {number} The denormalize (float) value in the range `[0,1]`.
- */
-function denormalize( value, array ) {
-
- switch ( array.constructor ) {
-
- case Float32Array:
-
- return value;
-
- case Uint32Array:
-
- return value / 4294967295.0;
-
- case Uint16Array:
-
- return value / 65535.0;
-
- case Uint8Array:
- case Uint8ClampedArray:
-
- return value / 255.0;
-
- case Int32Array:
-
- return Math.max( value / 2147483647.0, -1 );
-
- case Int16Array:
-
- return Math.max( value / 32767.0, -1 );
-
- case Int8Array:
-
- return Math.max( value / 127.0, -1 );
-
- default:
-
- throw new Error( 'THREE.MathUtils: Invalid component type.' );
-
- }
-
-}
-
-/**
- * Normalizes the given value according to the given typed array.
- *
- * @param {number} value - The float value in the range `[0,1]` to normalize.
- * @param {TypedArray} array - The typed array that defines the data type of the value.
- * @return {number} The normalize value.
- */
-function normalize( value, array ) {
-
- switch ( array.constructor ) {
-
- case Float32Array:
-
- return value;
-
- case Uint32Array:
-
- return Math.round( value * 4294967295.0 );
-
- case Uint16Array:
-
- return Math.round( value * 65535.0 );
-
- case Uint8Array:
- case Uint8ClampedArray:
-
- return Math.round( value * 255.0 );
-
- case Int32Array:
-
- return Math.round( value * 2147483647.0 );
-
- case Int16Array:
-
- return Math.round( value * 32767.0 );
-
- case Int8Array:
-
- return Math.round( value * 127.0 );
-
- default:
-
- throw new Error( 'THREE.MathUtils: Invalid component type.' );
-
- }
-
-}
-
-/**
- * @class
- * @classdesc A collection of math utility functions.
- * @hideconstructor
- */
-const MathUtils = {
- DEG2RAD: DEG2RAD,
- RAD2DEG: RAD2DEG,
- /**
- * Generate a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier)
- * (universally unique identifier).
- *
- * @static
- * @method
- * @return {string} The UUID.
- */
- generateUUID: generateUUID,
- /**
- * Clamps the given value between min and max.
- *
- * @static
- * @method
- * @param {number} value - The value to clamp.
- * @param {number} min - The min value.
- * @param {number} max - The max value.
- * @return {number} The clamped value.
- */
- clamp: clamp,
- /**
- * Computes the Euclidean modulo of the given parameters that
- * is `( ( n % m ) + m ) % m`.
- *
- * @static
- * @method
- * @param {number} n - The first parameter.
- * @param {number} m - The second parameter.
- * @return {number} The Euclidean modulo.
- */
- euclideanModulo: euclideanModulo,
- /**
- * Performs a linear mapping from range `` to range ``
- * for the given value.
- *
- * @static
- * @method
- * @param {number} x - The value to be mapped.
- * @param {number} a1 - Minimum value for range A.
- * @param {number} a2 - Maximum value for range A.
- * @param {number} b1 - Minimum value for range B.
- * @param {number} b2 - Maximum value for range B.
- * @return {number} The mapped value.
- */
- mapLinear: mapLinear,
- /**
- * Returns the percentage in the closed interval `[0, 1]` of the given value
- * between the start and end point.
- *
- * @static
- * @method
- * @param {number} x - The start point
- * @param {number} y - The end point.
- * @param {number} value - A value between start and end.
- * @return {number} The interpolation factor.
- */
- inverseLerp: inverseLerp,
- /**
- * Returns a value linearly interpolated from two known points based on the given interval -
- * `t = 0` will return `x` and `t = 1` will return `y`.
- *
- * @static
- * @method
- * @param {number} x - The start point
- * @param {number} y - The end point.
- * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
- * @return {number} The interpolated value.
- */
- lerp: lerp,
- /**
- * Smoothly interpolate a number from `x` to `y` in a spring-like manner using a delta
- * time to maintain frame rate independent movement. For details, see
- * [Frame rate independent damping using lerp](http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/).
- *
- * @static
- * @method
- * @param {number} x - The current point.
- * @param {number} y - The target point.
- * @param {number} lambda - A higher lambda value will make the movement more sudden,
- * and a lower value will make the movement more gradual.
- * @param {number} dt - Delta time in seconds.
- * @return {number} The interpolated value.
- */
- damp: damp,
- /**
- * Returns a value that alternates between `0` and the given `length` parameter.
- *
- * @static
- * @method
- * @param {number} x - The value to pingpong.
- * @param {number} [length=1] - The positive value the function will pingpong to.
- * @return {number} The alternated value.
- */
- pingpong: pingpong,
- /**
- * Returns a value in the range `[0,1]` that represents the percentage that `x` has
- * moved between `min` and `max`, but smoothed or slowed down the closer `x` is to
- * the `min` and `max`.
- *
- * See [Smoothstep](http://en.wikipedia.org/wiki/Smoothstep) for more details.
- *
- * @static
- * @method
- * @param {number} x - The value to evaluate based on its position between min and max.
- * @param {number} min - The min value. Any x value below min will be `0`.
- * @param {number} max - The max value. Any x value above max will be `1`.
- * @return {number} The alternated value.
- */
- smoothstep: smoothstep,
- /**
- * A [variation on smoothstep](https://en.wikipedia.org/wiki/Smoothstep#Variations)
- * that has zero 1st and 2nd order derivatives at x=0 and x=1.
- *
- * @static
- * @method
- * @param {number} x - The value to evaluate based on its position between min and max.
- * @param {number} min - The min value. Any x value below min will be `0`.
- * @param {number} max - The max value. Any x value above max will be `1`.
- * @return {number} The alternated value.
- */
- smootherstep: smootherstep,
- /**
- * Returns a random integer from `` interval.
- *
- * @static
- * @method
- * @param {number} low - The lower value boundary.
- * @param {number} high - The upper value boundary
- * @return {number} A random integer.
- */
- randInt: randInt,
- /**
- * Returns a random float from `` interval.
- *
- * @static
- * @method
- * @param {number} low - The lower value boundary.
- * @param {number} high - The upper value boundary
- * @return {number} A random float.
- */
- randFloat: randFloat,
- /**
- * Returns a random integer from `<-range/2, range/2>` interval.
- *
- * @static
- * @method
- * @param {number} range - Defines the value range.
- * @return {number} A random float.
- */
- randFloatSpread: randFloatSpread,
- /**
- * Returns a deterministic pseudo-random float in the interval `[0, 1]`.
- *
- * @static
- * @method
- * @param {number} [s] - The integer seed.
- * @return {number} A random float.
- */
- seededRandom: seededRandom,
- /**
- * Converts degrees to radians.
- *
- * @static
- * @method
- * @param {number} degrees - A value in degrees.
- * @return {number} The converted value in radians.
- */
- degToRad: degToRad,
- /**
- * Converts radians to degrees.
- *
- * @static
- * @method
- * @param {number} radians - A value in radians.
- * @return {number} The converted value in degrees.
- */
- radToDeg: radToDeg,
- /**
- * Returns `true` if the given number is a power of two.
- *
- * @static
- * @method
- * @param {number} value - The value to check.
- * @return {boolean} Whether the given number is a power of two or not.
- */
- isPowerOfTwo: isPowerOfTwo,
- /**
- * Returns the smallest power of two that is greater than or equal to the given number.
- *
- * @static
- * @method
- * @param {number} value - The value to find a POT for.
- * @return {number} The smallest power of two that is greater than or equal to the given number.
- */
- ceilPowerOfTwo: ceilPowerOfTwo,
- /**
- * Returns the largest power of two that is less than or equal to the given number.
- *
- * @static
- * @method
- * @param {number} value - The value to find a POT for.
- * @return {number} The largest power of two that is less than or equal to the given number.
- */
- floorPowerOfTwo: floorPowerOfTwo,
- /**
- * Sets the given quaternion from the [Intrinsic Proper Euler Angles](https://en.wikipedia.org/wiki/Euler_angles)
- * defined by the given angles and order.
- *
- * Rotations are applied to the axes in the order specified by order:
- * rotation by angle `a` is applied first, then by angle `b`, then by angle `c`.
- *
- * @static
- * @method
- * @param {Quaternion} q - The quaternion to set.
- * @param {number} a - The rotation applied to the first axis, in radians.
- * @param {number} b - The rotation applied to the second axis, in radians.
- * @param {number} c - The rotation applied to the third axis, in radians.
- * @param {('XYX'|'XZX'|'YXY'|'YZY'|'ZXZ'|'ZYZ')} order - A string specifying the axes order.
- */
- setQuaternionFromProperEuler: setQuaternionFromProperEuler,
- /**
- * Normalizes the given value according to the given typed array.
- *
- * @static
- * @method
- * @param {number} value - The float value in the range `[0,1]` to normalize.
- * @param {TypedArray} array - The typed array that defines the data type of the value.
- * @return {number} The normalize value.
- */
- normalize: normalize,
- /**
- * Denormalizes the given value according to the given typed array.
- *
- * @static
- * @method
- * @param {number} value - The value to denormalize.
- * @param {TypedArray} array - The typed array that defines the data type of the value.
- * @return {number} The denormalize (float) value in the range `[0,1]`.
- */
- denormalize: denormalize
-};
-
-/**
- * Class representing a 2D vector. A 2D vector is an ordered pair of numbers
- * (labeled x and y), which can be used to represent a number of things, such as:
- *
- * - A point in 2D space (i.e. a position on a plane).
- * - A direction and length across a plane. In three.js the length will
- * always be the Euclidean distance(straight-line distance) from `(0, 0)` to `(x, y)`
- * and the direction is also measured from `(0, 0)` towards `(x, y)`.
- * - Any arbitrary ordered pair of numbers.
- *
- * There are other things a 2D vector can be used to represent, such as
- * momentum vectors, complex numbers and so on, however these are the most
- * common uses in three.js.
- *
- * Iterating through a vector instance will yield its components `(x, y)` in
- * the corresponding order.
- * ```js
- * const a = new THREE.Vector2( 0, 1 );
- *
- * //no arguments; will be initialised to (0, 0)
- * const b = new THREE.Vector2( );
- *
- * const d = a.distanceTo( b );
- * ```
- */
-class Vector2 {
-
- static {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- Vector2.prototype.isVector2 = true;
-
- }
-
- /**
- * Constructs a new 2D vector.
- *
- * @param {number} [x=0] - The x value of this vector.
- * @param {number} [y=0] - The y value of this vector.
- */
- constructor( x = 0, y = 0 ) {
-
- /**
- * The x value of this vector.
- *
- * @type {number}
- */
- this.x = x;
-
- /**
- * The y value of this vector.
- *
- * @type {number}
- */
- this.y = y;
-
- }
-
- /**
- * Alias for {@link Vector2#x}.
- *
- * @type {number}
- */
- get width() {
-
- return this.x;
-
- }
-
- set width( value ) {
-
- this.x = value;
-
- }
-
- /**
- * Alias for {@link Vector2#y}.
- *
- * @type {number}
- */
- get height() {
-
- return this.y;
-
- }
-
- set height( value ) {
-
- this.y = value;
-
- }
-
- /**
- * Sets the vector components.
- *
- * @param {number} x - The value of the x component.
- * @param {number} y - The value of the y component.
- * @return {Vector2} A reference to this vector.
- */
- set( x, y ) {
-
- this.x = x;
- this.y = y;
-
- return this;
-
- }
-
- /**
- * Sets the vector components to the same value.
- *
- * @param {number} scalar - The value to set for all vector components.
- * @return {Vector2} A reference to this vector.
- */
- setScalar( scalar ) {
-
- this.x = scalar;
- this.y = scalar;
-
- return this;
-
- }
-
- /**
- * Sets the vector's x component to the given value
- *
- * @param {number} x - The value to set.
- * @return {Vector2} A reference to this vector.
- */
- setX( x ) {
-
- this.x = x;
-
- return this;
-
- }
-
- /**
- * Sets the vector's y component to the given value
- *
- * @param {number} y - The value to set.
- * @return {Vector2} A reference to this vector.
- */
- setY( y ) {
-
- this.y = y;
-
- return this;
-
- }
-
- /**
- * Allows to set a vector component with an index.
- *
- * @param {number} index - The component index. `0` equals to x, `1` equals to y.
- * @param {number} value - The value to set.
- * @return {Vector2} A reference to this vector.
- */
- setComponent( index, value ) {
-
- switch ( index ) {
-
- case 0: this.x = value; break;
- case 1: this.y = value; break;
- default: throw new Error( 'THREE.Vector2: index is out of range: ' + index );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns the value of the vector component which matches the given index.
- *
- * @param {number} index - The component index. `0` equals to x, `1` equals to y.
- * @return {number} A vector component value.
- */
- getComponent( index ) {
-
- switch ( index ) {
-
- case 0: return this.x;
- case 1: return this.y;
- default: throw new Error( 'THREE.Vector2: index is out of range: ' + index );
-
- }
-
- }
-
- /**
- * Returns a new vector with copied values from this instance.
- *
- * @return {Vector2} A clone of this instance.
- */
- clone() {
-
- return new this.constructor( this.x, this.y );
-
- }
-
- /**
- * Copies the values of the given vector to this instance.
- *
- * @param {Vector2} v - The vector to copy.
- * @return {Vector2} A reference to this vector.
- */
- copy( v ) {
-
- this.x = v.x;
- this.y = v.y;
-
- return this;
-
- }
-
- /**
- * Adds the given vector to this instance.
- *
- * @param {Vector2} v - The vector to add.
- * @return {Vector2} A reference to this vector.
- */
- add( v ) {
-
- this.x += v.x;
- this.y += v.y;
-
- return this;
-
- }
-
- /**
- * Adds the given scalar value to all components of this instance.
- *
- * @param {number} s - The scalar to add.
- * @return {Vector2} A reference to this vector.
- */
- addScalar( s ) {
-
- this.x += s;
- this.y += s;
-
- return this;
-
- }
-
- /**
- * Adds the given vectors and stores the result in this instance.
- *
- * @param {Vector2} a - The first vector.
- * @param {Vector2} b - The second vector.
- * @return {Vector2} A reference to this vector.
- */
- addVectors( a, b ) {
-
- this.x = a.x + b.x;
- this.y = a.y + b.y;
-
- return this;
-
- }
-
- /**
- * Adds the given vector scaled by the given factor to this instance.
- *
- * @param {Vector2} v - The vector.
- * @param {number} s - The factor that scales `v`.
- * @return {Vector2} A reference to this vector.
- */
- addScaledVector( v, s ) {
-
- this.x += v.x * s;
- this.y += v.y * s;
-
- return this;
-
- }
-
- /**
- * Subtracts the given vector from this instance.
- *
- * @param {Vector2} v - The vector to subtract.
- * @return {Vector2} A reference to this vector.
- */
- sub( v ) {
-
- this.x -= v.x;
- this.y -= v.y;
-
- return this;
-
- }
-
- /**
- * Subtracts the given scalar value from all components of this instance.
- *
- * @param {number} s - The scalar to subtract.
- * @return {Vector2} A reference to this vector.
- */
- subScalar( s ) {
-
- this.x -= s;
- this.y -= s;
-
- return this;
-
- }
-
- /**
- * Subtracts the given vectors and stores the result in this instance.
- *
- * @param {Vector2} a - The first vector.
- * @param {Vector2} b - The second vector.
- * @return {Vector2} A reference to this vector.
- */
- subVectors( a, b ) {
-
- this.x = a.x - b.x;
- this.y = a.y - b.y;
-
- return this;
-
- }
-
- /**
- * Multiplies the given vector with this instance.
- *
- * @param {Vector2} v - The vector to multiply.
- * @return {Vector2} A reference to this vector.
- */
- multiply( v ) {
-
- this.x *= v.x;
- this.y *= v.y;
-
- return this;
-
- }
-
- /**
- * Multiplies the given scalar value with all components of this instance.
- *
- * @param {number} scalar - The scalar to multiply.
- * @return {Vector2} A reference to this vector.
- */
- multiplyScalar( scalar ) {
-
- this.x *= scalar;
- this.y *= scalar;
-
- return this;
-
- }
-
- /**
- * Divides this instance by the given vector.
- *
- * @param {Vector2} v - The vector to divide.
- * @return {Vector2} A reference to this vector.
- */
- divide( v ) {
-
- this.x /= v.x;
- this.y /= v.y;
-
- return this;
-
- }
-
- /**
- * Divides this vector by the given scalar.
- *
- * @param {number} scalar - The scalar to divide.
- * @return {Vector2} A reference to this vector.
- */
- divideScalar( scalar ) {
-
- return this.multiplyScalar( 1 / scalar );
-
- }
-
- /**
- * Multiplies this vector (with an implicit 1 as the 3rd component) by
- * the given 3x3 matrix.
- *
- * @param {Matrix3} m - The matrix to apply.
- * @return {Vector2} A reference to this vector.
- */
- applyMatrix3( m ) {
-
- const x = this.x, y = this.y;
- const e = m.elements;
-
- this.x = e[ 0 ] * x + e[ 3 ] * y + e[ 6 ];
- this.y = e[ 1 ] * x + e[ 4 ] * y + e[ 7 ];
-
- return this;
-
- }
-
- /**
- * If this vector's x or y value is greater than the given vector's x or y
- * value, replace that value with the corresponding min value.
- *
- * @param {Vector2} v - The vector.
- * @return {Vector2} A reference to this vector.
- */
- min( v ) {
-
- this.x = Math.min( this.x, v.x );
- this.y = Math.min( this.y, v.y );
-
- return this;
-
- }
-
- /**
- * If this vector's x or y value is less than the given vector's x or y
- * value, replace that value with the corresponding max value.
- *
- * @param {Vector2} v - The vector.
- * @return {Vector2} A reference to this vector.
- */
- max( v ) {
-
- this.x = Math.max( this.x, v.x );
- this.y = Math.max( this.y, v.y );
-
- return this;
-
- }
-
- /**
- * If this vector's x or y value is greater than the max vector's x or y
- * value, it is replaced by the corresponding value.
- * If this vector's x or y value is less than the min vector's x or y value,
- * it is replaced by the corresponding value.
- *
- * @param {Vector2} min - The minimum x and y values.
- * @param {Vector2} max - The maximum x and y values in the desired range.
- * @return {Vector2} A reference to this vector.
- */
- clamp( min, max ) {
-
- // assumes min < max, componentwise
-
- this.x = clamp( this.x, min.x, max.x );
- this.y = clamp( this.y, min.y, max.y );
-
- return this;
-
- }
-
- /**
- * If this vector's x or y values are greater than the max value, they are
- * replaced by the max value.
- * If this vector's x or y values are less than the min value, they are
- * replaced by the min value.
- *
- * @param {number} minVal - The minimum value the components will be clamped to.
- * @param {number} maxVal - The maximum value the components will be clamped to.
- * @return {Vector2} A reference to this vector.
- */
- clampScalar( minVal, maxVal ) {
-
- this.x = clamp( this.x, minVal, maxVal );
- this.y = clamp( this.y, minVal, maxVal );
-
- return this;
-
- }
-
- /**
- * If this vector's length is greater than the max value, it is replaced by
- * the max value.
- * If this vector's length is less than the min value, it is replaced by the
- * min value.
- *
- * @param {number} min - The minimum value the vector length will be clamped to.
- * @param {number} max - The maximum value the vector length will be clamped to.
- * @return {Vector2} A reference to this vector.
- */
- clampLength( min, max ) {
-
- const length = this.length();
-
- return this.divideScalar( length || 1 ).multiplyScalar( clamp( length, min, max ) );
-
- }
-
- /**
- * The components of this vector are rounded down to the nearest integer value.
- *
- * @return {Vector2} A reference to this vector.
- */
- floor() {
-
- this.x = Math.floor( this.x );
- this.y = Math.floor( this.y );
-
- return this;
-
- }
-
- /**
- * The components of this vector are rounded up to the nearest integer value.
- *
- * @return {Vector2} A reference to this vector.
- */
- ceil() {
-
- this.x = Math.ceil( this.x );
- this.y = Math.ceil( this.y );
-
- return this;
-
- }
-
- /**
- * The components of this vector are rounded to the nearest integer value
- *
- * @return {Vector2} A reference to this vector.
- */
- round() {
-
- this.x = Math.round( this.x );
- this.y = Math.round( this.y );
-
- return this;
-
- }
-
- /**
- * The components of this vector are rounded towards zero (up if negative,
- * down if positive) to an integer value.
- *
- * @return {Vector2} A reference to this vector.
- */
- roundToZero() {
-
- this.x = Math.trunc( this.x );
- this.y = Math.trunc( this.y );
-
- return this;
-
- }
-
- /**
- * Inverts this vector - i.e. sets x = -x and y = -y.
- *
- * @return {Vector2} A reference to this vector.
- */
- negate() {
-
- this.x = - this.x;
- this.y = - this.y;
-
- return this;
-
- }
-
- /**
- * Calculates the dot product of the given vector with this instance.
- *
- * @param {Vector2} v - The vector to compute the dot product with.
- * @return {number} The result of the dot product.
- */
- dot( v ) {
-
- return this.x * v.x + this.y * v.y;
-
- }
-
- /**
- * Calculates the cross product of the given vector with this instance.
- *
- * @param {Vector2} v - The vector to compute the cross product with.
- * @return {number} The result of the cross product.
- */
- cross( v ) {
-
- return this.x * v.y - this.y * v.x;
-
- }
-
- /**
- * Computes the square of the Euclidean length (straight-line length) from
- * (0, 0) to (x, y). If you are comparing the lengths of vectors, you should
- * compare the length squared instead as it is slightly more efficient to calculate.
- *
- * @return {number} The square length of this vector.
- */
- lengthSq() {
-
- return this.x * this.x + this.y * this.y;
-
- }
-
- /**
- * Computes the Euclidean length (straight-line length) from (0, 0) to (x, y).
- *
- * @return {number} The length of this vector.
- */
- length() {
-
- return Math.sqrt( this.x * this.x + this.y * this.y );
-
- }
-
- /**
- * Computes the Manhattan length of this vector.
- *
- * @return {number} The length of this vector.
- */
- manhattanLength() {
-
- return Math.abs( this.x ) + Math.abs( this.y );
-
- }
-
- /**
- * Converts this vector to a unit vector - that is, sets it equal to a vector
- * with the same direction as this one, but with a vector length of `1`.
- *
- * @return {Vector2} A reference to this vector.
- */
- normalize() {
-
- return this.divideScalar( this.length() || 1 );
-
- }
-
- /**
- * Computes the angle in radians of this vector with respect to the positive x-axis.
- *
- * @return {number} The angle in radians.
- */
- angle() {
-
- const angle = Math.atan2( - this.y, - this.x ) + Math.PI;
-
- return angle;
-
- }
-
- /**
- * Returns the angle between the given vector and this instance in radians.
- *
- * @param {Vector2} v - The vector to compute the angle with.
- * @return {number} The angle in radians.
- */
- angleTo( v ) {
-
- const denominator = Math.sqrt( this.lengthSq() * v.lengthSq() );
-
- if ( denominator === 0 ) return Math.PI / 2;
-
- const theta = this.dot( v ) / denominator;
-
- // clamp, to handle numerical problems
-
- return Math.acos( clamp( theta, -1, 1 ) );
-
- }
-
- /**
- * Computes the distance from the given vector to this instance.
- *
- * @param {Vector2} v - The vector to compute the distance to.
- * @return {number} The distance.
- */
- distanceTo( v ) {
-
- return Math.sqrt( this.distanceToSquared( v ) );
-
- }
-
- /**
- * Computes the squared distance from the given vector to this instance.
- * If you are just comparing the distance with another distance, you should compare
- * the distance squared instead as it is slightly more efficient to calculate.
- *
- * @param {Vector2} v - The vector to compute the squared distance to.
- * @return {number} The squared distance.
- */
- distanceToSquared( v ) {
-
- const dx = this.x - v.x, dy = this.y - v.y;
- return dx * dx + dy * dy;
-
- }
-
- /**
- * Computes the Manhattan distance from the given vector to this instance.
- *
- * @param {Vector2} v - The vector to compute the Manhattan distance to.
- * @return {number} The Manhattan distance.
- */
- manhattanDistanceTo( v ) {
-
- return Math.abs( this.x - v.x ) + Math.abs( this.y - v.y );
-
- }
-
- /**
- * Sets this vector to a vector with the same direction as this one, but
- * with the specified length.
- *
- * @param {number} length - The new length of this vector.
- * @return {Vector2} A reference to this vector.
- */
- setLength( length ) {
-
- return this.normalize().multiplyScalar( length );
-
- }
-
- /**
- * Linearly interpolates between the given vector and this instance, where
- * alpha is the percent distance along the line - alpha = 0 will be this
- * vector, and alpha = 1 will be the given one.
- *
- * @param {Vector2} v - The vector to interpolate towards.
- * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
- * @return {Vector2} A reference to this vector.
- */
- lerp( v, alpha ) {
-
- this.x += ( v.x - this.x ) * alpha;
- this.y += ( v.y - this.y ) * alpha;
-
- return this;
-
- }
-
- /**
- * Linearly interpolates between the given vectors, where alpha is the percent
- * distance along the line - alpha = 0 will be first vector, and alpha = 1 will
- * be the second one. The result is stored in this instance.
- *
- * @param {Vector2} v1 - The first vector.
- * @param {Vector2} v2 - The second vector.
- * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
- * @return {Vector2} A reference to this vector.
- */
- lerpVectors( v1, v2, alpha ) {
-
- this.x = v1.x + ( v2.x - v1.x ) * alpha;
- this.y = v1.y + ( v2.y - v1.y ) * alpha;
-
- return this;
-
- }
-
- /**
- * Returns `true` if this vector is equal with the given one.
- *
- * @param {Vector2} v - The vector to test for equality.
- * @return {boolean} Whether this vector is equal with the given one.
- */
- equals( v ) {
-
- return ( ( v.x === this.x ) && ( v.y === this.y ) );
-
- }
-
- /**
- * Sets this vector's x value to be `array[ offset ]` and y
- * value to be `array[ offset + 1 ]`.
- *
- * @param {Array} array - An array holding the vector component values.
- * @param {number} [offset=0] - The offset into the array.
- * @return {Vector2} A reference to this vector.
- */
- fromArray( array, offset = 0 ) {
-
- this.x = array[ offset ];
- this.y = array[ offset + 1 ];
-
- return this;
-
- }
-
- /**
- * Writes the components of this vector to the given array. If no array is provided,
- * the method returns a new instance.
- *
- * @param {Array} [array=[]] - The target array holding the vector components.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Array} The vector components.
- */
- toArray( array = [], offset = 0 ) {
-
- array[ offset ] = this.x;
- array[ offset + 1 ] = this.y;
-
- return array;
-
- }
-
- /**
- * Sets the components of this vector from the given buffer attribute.
- *
- * @param {BufferAttribute} attribute - The buffer attribute holding vector data.
- * @param {number} index - The index into the attribute.
- * @return {Vector2} A reference to this vector.
- */
- fromBufferAttribute( attribute, index ) {
-
- this.x = attribute.getX( index );
- this.y = attribute.getY( index );
-
- return this;
-
- }
-
- /**
- * Rotates this vector around the given center by the given angle.
- *
- * @param {Vector2} center - The point around which to rotate.
- * @param {number} angle - The angle to rotate, in radians.
- * @return {Vector2} A reference to this vector.
- */
- rotateAround( center, angle ) {
-
- const c = Math.cos( angle ), s = Math.sin( angle );
-
- const x = this.x - center.x;
- const y = this.y - center.y;
-
- this.x = x * c - y * s + center.x;
- this.y = x * s + y * c + center.y;
-
- return this;
-
- }
-
- /**
- * Sets each component of this vector to a pseudo-random value between `0` and
- * `1`, excluding `1`.
- *
- * @return {Vector2} A reference to this vector.
- */
- random() {
-
- this.x = Math.random();
- this.y = Math.random();
-
- return this;
-
- }
-
- *[ Symbol.iterator ]() {
-
- yield this.x;
- yield this.y;
-
- }
-
-}
-
-/**
- * Class for representing a Quaternion. Quaternions are used in three.js to represent rotations.
- *
- * Iterating through a vector instance will yield its components `(x, y, z, w)` in
- * the corresponding order.
- *
- * Note that three.js expects Quaternions to be normalized.
- * ```js
- * const quaternion = new THREE.Quaternion();
- * quaternion.setFromAxisAngle( new THREE.Vector3( 0, 1, 0 ), Math.PI / 2 );
- *
- * const vector = new THREE.Vector3( 1, 0, 0 );
- * vector.applyQuaternion( quaternion );
- * ```
- */
-class Quaternion {
-
- /**
- * Constructs a new quaternion.
- *
- * @param {number} [x=0] - The x value of this quaternion.
- * @param {number} [y=0] - The y value of this quaternion.
- * @param {number} [z=0] - The z value of this quaternion.
- * @param {number} [w=1] - The w value of this quaternion.
- */
- constructor( x = 0, y = 0, z = 0, w = 1 ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isQuaternion = true;
-
- this._x = x;
- this._y = y;
- this._z = z;
- this._w = w;
-
- }
-
- /**
- * Interpolates between two quaternions via SLERP. This implementation assumes the
- * quaternion data are managed in flat arrays.
- *
- * @param {Array} dst - The destination array.
- * @param {number} dstOffset - An offset into the destination array.
- * @param {Array} src0 - The source array of the first quaternion.
- * @param {number} srcOffset0 - An offset into the first source array.
- * @param {Array} src1 - The source array of the second quaternion.
- * @param {number} srcOffset1 - An offset into the second source array.
- * @param {number} t - The interpolation factor. A value in the range `[0,1]` will interpolate. A value outside the range `[0,1]` will extrapolate.
- * @see {@link Quaternion#slerp}
- */
- static slerpFlat( dst, dstOffset, src0, srcOffset0, src1, srcOffset1, t ) {
-
- let x0 = src0[ srcOffset0 + 0 ],
- y0 = src0[ srcOffset0 + 1 ],
- z0 = src0[ srcOffset0 + 2 ],
- w0 = src0[ srcOffset0 + 3 ];
-
- let x1 = src1[ srcOffset1 + 0 ],
- y1 = src1[ srcOffset1 + 1 ],
- z1 = src1[ srcOffset1 + 2 ],
- w1 = src1[ srcOffset1 + 3 ];
-
- if ( w0 !== w1 || x0 !== x1 || y0 !== y1 || z0 !== z1 ) {
-
- let dot = x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1;
-
- if ( dot < 0 ) {
-
- x1 = - x1;
- y1 = - y1;
- z1 = - z1;
- w1 = - w1;
-
- dot = - dot;
-
- }
-
- let s = 1 - t;
-
- if ( dot < 0.9995 ) {
-
- // slerp
-
- const theta = Math.acos( dot );
- const sin = Math.sin( theta );
-
- s = Math.sin( s * theta ) / sin;
- t = Math.sin( t * theta ) / sin;
-
- x0 = x0 * s + x1 * t;
- y0 = y0 * s + y1 * t;
- z0 = z0 * s + z1 * t;
- w0 = w0 * s + w1 * t;
-
- } else {
-
- // for small angles, lerp then normalize
-
- x0 = x0 * s + x1 * t;
- y0 = y0 * s + y1 * t;
- z0 = z0 * s + z1 * t;
- w0 = w0 * s + w1 * t;
-
- const f = 1 / Math.sqrt( x0 * x0 + y0 * y0 + z0 * z0 + w0 * w0 );
-
- x0 *= f;
- y0 *= f;
- z0 *= f;
- w0 *= f;
-
- }
-
- }
-
- dst[ dstOffset ] = x0;
- dst[ dstOffset + 1 ] = y0;
- dst[ dstOffset + 2 ] = z0;
- dst[ dstOffset + 3 ] = w0;
-
- }
-
- /**
- * Multiplies two quaternions. This implementation assumes the quaternion data are managed
- * in flat arrays.
- *
- * @param {Array} dst - The destination array.
- * @param {number} dstOffset - An offset into the destination array.
- * @param {Array} src0 - The source array of the first quaternion.
- * @param {number} srcOffset0 - An offset into the first source array.
- * @param {Array} src1 - The source array of the second quaternion.
- * @param {number} srcOffset1 - An offset into the second source array.
- * @return {Array} The destination array.
- * @see {@link Quaternion#multiplyQuaternions}.
- */
- static multiplyQuaternionsFlat( dst, dstOffset, src0, srcOffset0, src1, srcOffset1 ) {
-
- const x0 = src0[ srcOffset0 ];
- const y0 = src0[ srcOffset0 + 1 ];
- const z0 = src0[ srcOffset0 + 2 ];
- const w0 = src0[ srcOffset0 + 3 ];
-
- const x1 = src1[ srcOffset1 ];
- const y1 = src1[ srcOffset1 + 1 ];
- const z1 = src1[ srcOffset1 + 2 ];
- const w1 = src1[ srcOffset1 + 3 ];
-
- dst[ dstOffset ] = x0 * w1 + w0 * x1 + y0 * z1 - z0 * y1;
- dst[ dstOffset + 1 ] = y0 * w1 + w0 * y1 + z0 * x1 - x0 * z1;
- dst[ dstOffset + 2 ] = z0 * w1 + w0 * z1 + x0 * y1 - y0 * x1;
- dst[ dstOffset + 3 ] = w0 * w1 - x0 * x1 - y0 * y1 - z0 * z1;
-
- return dst;
-
- }
-
- /**
- * The x value of this quaternion.
- *
- * @type {number}
- * @default 0
- */
- get x() {
-
- return this._x;
-
- }
-
- set x( value ) {
-
- this._x = value;
- this._onChangeCallback();
-
- }
-
- /**
- * The y value of this quaternion.
- *
- * @type {number}
- * @default 0
- */
- get y() {
-
- return this._y;
-
- }
-
- set y( value ) {
-
- this._y = value;
- this._onChangeCallback();
-
- }
-
- /**
- * The z value of this quaternion.
- *
- * @type {number}
- * @default 0
- */
- get z() {
-
- return this._z;
-
- }
-
- set z( value ) {
-
- this._z = value;
- this._onChangeCallback();
-
- }
-
- /**
- * The w value of this quaternion.
- *
- * @type {number}
- * @default 1
- */
- get w() {
-
- return this._w;
-
- }
-
- set w( value ) {
-
- this._w = value;
- this._onChangeCallback();
-
- }
-
- /**
- * Sets the quaternion components.
- *
- * @param {number} x - The x value of this quaternion.
- * @param {number} y - The y value of this quaternion.
- * @param {number} z - The z value of this quaternion.
- * @param {number} w - The w value of this quaternion.
- * @return {Quaternion} A reference to this quaternion.
- */
- set( x, y, z, w ) {
-
- this._x = x;
- this._y = y;
- this._z = z;
- this._w = w;
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Returns a new quaternion with copied values from this instance.
- *
- * @return {Quaternion} A clone of this instance.
- */
- clone() {
-
- return new this.constructor( this._x, this._y, this._z, this._w );
-
- }
-
- /**
- * Copies the values of the given quaternion to this instance.
- *
- * @param {Quaternion} quaternion - The quaternion to copy.
- * @return {Quaternion} A reference to this quaternion.
- */
- copy( quaternion ) {
-
- this._x = quaternion.x;
- this._y = quaternion.y;
- this._z = quaternion.z;
- this._w = quaternion.w;
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Sets this quaternion from the rotation specified by the given
- * Euler angles.
- *
- * @param {Euler} euler - The Euler angles.
- * @param {boolean} [update=true] - Whether the internal `onChange` callback should be executed or not.
- * @return {Quaternion} A reference to this quaternion.
- */
- setFromEuler( euler, update = true ) {
-
- const x = euler._x, y = euler._y, z = euler._z, order = euler._order;
-
- const cos = Math.cos;
- const sin = Math.sin;
-
- const c1 = cos( x / 2 );
- const c2 = cos( y / 2 );
- const c3 = cos( z / 2 );
-
- const s1 = sin( x / 2 );
- const s2 = sin( y / 2 );
- const s3 = sin( z / 2 );
-
- switch ( order ) {
-
- case 'XYZ':
- this._x = s1 * c2 * c3 + c1 * s2 * s3;
- this._y = c1 * s2 * c3 - s1 * c2 * s3;
- this._z = c1 * c2 * s3 + s1 * s2 * c3;
- this._w = c1 * c2 * c3 - s1 * s2 * s3;
- break;
-
- case 'YXZ':
- this._x = s1 * c2 * c3 + c1 * s2 * s3;
- this._y = c1 * s2 * c3 - s1 * c2 * s3;
- this._z = c1 * c2 * s3 - s1 * s2 * c3;
- this._w = c1 * c2 * c3 + s1 * s2 * s3;
- break;
-
- case 'ZXY':
- this._x = s1 * c2 * c3 - c1 * s2 * s3;
- this._y = c1 * s2 * c3 + s1 * c2 * s3;
- this._z = c1 * c2 * s3 + s1 * s2 * c3;
- this._w = c1 * c2 * c3 - s1 * s2 * s3;
- break;
-
- case 'ZYX':
- this._x = s1 * c2 * c3 - c1 * s2 * s3;
- this._y = c1 * s2 * c3 + s1 * c2 * s3;
- this._z = c1 * c2 * s3 - s1 * s2 * c3;
- this._w = c1 * c2 * c3 + s1 * s2 * s3;
- break;
-
- case 'YZX':
- this._x = s1 * c2 * c3 + c1 * s2 * s3;
- this._y = c1 * s2 * c3 + s1 * c2 * s3;
- this._z = c1 * c2 * s3 - s1 * s2 * c3;
- this._w = c1 * c2 * c3 - s1 * s2 * s3;
- break;
-
- case 'XZY':
- this._x = s1 * c2 * c3 - c1 * s2 * s3;
- this._y = c1 * s2 * c3 - s1 * c2 * s3;
- this._z = c1 * c2 * s3 + s1 * s2 * c3;
- this._w = c1 * c2 * c3 + s1 * s2 * s3;
- break;
-
- default:
- warn( 'Quaternion: .setFromEuler() encountered an unknown order: ' + order );
-
- }
-
- if ( update === true ) this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Sets this quaternion from the given axis and angle.
- *
- * @param {Vector3} axis - The normalized axis.
- * @param {number} angle - The angle in radians.
- * @return {Quaternion} A reference to this quaternion.
- */
- setFromAxisAngle( axis, angle ) {
-
- const halfAngle = angle / 2, s = Math.sin( halfAngle );
-
- this._x = axis.x * s;
- this._y = axis.y * s;
- this._z = axis.z * s;
- this._w = Math.cos( halfAngle );
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Sets this quaternion from the given rotation matrix.
- *
- * @param {Matrix4} m - A 4x4 matrix of which the upper 3x3 of matrix is a pure rotation matrix (i.e. unscaled).
- * @return {Quaternion} A reference to this quaternion.
- */
- setFromRotationMatrix( m ) {
-
- // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
-
- const te = m.elements,
-
- m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ],
- m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ],
- m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ],
-
- trace = m11 + m22 + m33;
-
- if ( trace > 0 ) {
-
- const s = 0.5 / Math.sqrt( trace + 1.0 );
-
- this._w = 0.25 / s;
- this._x = ( m32 - m23 ) * s;
- this._y = ( m13 - m31 ) * s;
- this._z = ( m21 - m12 ) * s;
-
- } else if ( m11 > m22 && m11 > m33 ) {
-
- const s = 2.0 * Math.sqrt( 1.0 + m11 - m22 - m33 );
-
- this._w = ( m32 - m23 ) / s;
- this._x = 0.25 * s;
- this._y = ( m12 + m21 ) / s;
- this._z = ( m13 + m31 ) / s;
-
- } else if ( m22 > m33 ) {
-
- const s = 2.0 * Math.sqrt( 1.0 + m22 - m11 - m33 );
-
- this._w = ( m13 - m31 ) / s;
- this._x = ( m12 + m21 ) / s;
- this._y = 0.25 * s;
- this._z = ( m23 + m32 ) / s;
-
- } else {
-
- const s = 2.0 * Math.sqrt( 1.0 + m33 - m11 - m22 );
-
- this._w = ( m21 - m12 ) / s;
- this._x = ( m13 + m31 ) / s;
- this._y = ( m23 + m32 ) / s;
- this._z = 0.25 * s;
-
- }
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Sets this quaternion to the rotation required to rotate the direction vector
- * `vFrom` to the direction vector `vTo`.
- *
- * @param {Vector3} vFrom - The first (normalized) direction vector.
- * @param {Vector3} vTo - The second (normalized) direction vector.
- * @return {Quaternion} A reference to this quaternion.
- */
- setFromUnitVectors( vFrom, vTo ) {
-
- // assumes direction vectors vFrom and vTo are normalized
-
- let r = vFrom.dot( vTo ) + 1;
-
- if ( r < 1e-8 ) { // the epsilon value has been discussed in #31286
-
- // vFrom and vTo point in opposite directions
-
- r = 0;
-
- if ( Math.abs( vFrom.x ) > Math.abs( vFrom.z ) ) {
-
- this._x = - vFrom.y;
- this._y = vFrom.x;
- this._z = 0;
- this._w = r;
-
- } else {
-
- this._x = 0;
- this._y = - vFrom.z;
- this._z = vFrom.y;
- this._w = r;
-
- }
-
- } else {
-
- // crossVectors( vFrom, vTo ); // inlined to avoid cyclic dependency on Vector3
-
- this._x = vFrom.y * vTo.z - vFrom.z * vTo.y;
- this._y = vFrom.z * vTo.x - vFrom.x * vTo.z;
- this._z = vFrom.x * vTo.y - vFrom.y * vTo.x;
- this._w = r;
-
- }
-
- return this.normalize();
-
- }
-
- /**
- * Returns the angle between this quaternion and the given one in radians.
- *
- * @param {Quaternion} q - The quaternion to compute the angle with.
- * @return {number} The angle in radians.
- */
- angleTo( q ) {
-
- return 2 * Math.acos( Math.abs( clamp( this.dot( q ), -1, 1 ) ) );
-
- }
-
- /**
- * Rotates this quaternion by a given angular step to the given quaternion.
- * The method ensures that the final quaternion will not overshoot `q`.
- *
- * @param {Quaternion} q - The target quaternion.
- * @param {number} step - The angular step in radians.
- * @return {Quaternion} A reference to this quaternion.
- */
- rotateTowards( q, step ) {
-
- const angle = this.angleTo( q );
-
- if ( angle === 0 ) return this;
-
- const t = Math.min( 1, step / angle );
-
- this.slerp( q, t );
-
- return this;
-
- }
-
- /**
- * Sets this quaternion to the identity quaternion; that is, to the
- * quaternion that represents "no rotation".
- *
- * @return {Quaternion} A reference to this quaternion.
- */
- identity() {
-
- return this.set( 0, 0, 0, 1 );
-
- }
-
- /**
- * Inverts this quaternion via {@link Quaternion#conjugate}. The
- * quaternion is assumed to have unit length.
- *
- * @return {Quaternion} A reference to this quaternion.
- */
- invert() {
-
- return this.conjugate();
-
- }
-
- /**
- * Returns the rotational conjugate of this quaternion. The conjugate of a
- * quaternion represents the same rotation in the opposite direction about
- * the rotational axis.
- *
- * @return {Quaternion} A reference to this quaternion.
- */
- conjugate() {
-
- this._x *= -1;
- this._y *= -1;
- this._z *= -1;
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Calculates the dot product of this quaternion and the given one.
- *
- * @param {Quaternion} v - The quaternion to compute the dot product with.
- * @return {number} The result of the dot product.
- */
- dot( v ) {
-
- return this._x * v._x + this._y * v._y + this._z * v._z + this._w * v._w;
-
- }
-
- /**
- * Computes the squared Euclidean length (straight-line length) of this quaternion,
- * considered as a 4 dimensional vector. This can be useful if you are comparing the
- * lengths of two quaternions, as this is a slightly more efficient calculation than
- * {@link Quaternion#length}.
- *
- * @return {number} The squared Euclidean length.
- */
- lengthSq() {
-
- return this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w;
-
- }
-
- /**
- * Computes the Euclidean length (straight-line length) of this quaternion,
- * considered as a 4 dimensional vector.
- *
- * @return {number} The Euclidean length.
- */
- length() {
-
- return Math.sqrt( this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w );
-
- }
-
- /**
- * Normalizes this quaternion - that is, calculated the quaternion that performs
- * the same rotation as this one, but has a length equal to `1`.
- *
- * @return {Quaternion} A reference to this quaternion.
- */
- normalize() {
-
- let l = this.length();
-
- if ( l === 0 ) {
-
- this._x = 0;
- this._y = 0;
- this._z = 0;
- this._w = 1;
-
- } else {
-
- l = 1 / l;
-
- this._x = this._x * l;
- this._y = this._y * l;
- this._z = this._z * l;
- this._w = this._w * l;
-
- }
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Multiplies this quaternion by the given one.
- *
- * @param {Quaternion} q - The quaternion.
- * @return {Quaternion} A reference to this quaternion.
- */
- multiply( q ) {
-
- return this.multiplyQuaternions( this, q );
-
- }
-
- /**
- * Pre-multiplies this quaternion by the given one.
- *
- * @param {Quaternion} q - The quaternion.
- * @return {Quaternion} A reference to this quaternion.
- */
- premultiply( q ) {
-
- return this.multiplyQuaternions( q, this );
-
- }
-
- /**
- * Multiplies the given quaternions and stores the result in this instance.
- *
- * @param {Quaternion} a - The first quaternion.
- * @param {Quaternion} b - The second quaternion.
- * @return {Quaternion} A reference to this quaternion.
- */
- multiplyQuaternions( a, b ) {
-
- const qax = a._x, qay = a._y, qaz = a._z, qaw = a._w;
- const qbx = b._x, qby = b._y, qbz = b._z, qbw = b._w;
-
- this._x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
- this._y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
- this._z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
- this._w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Performs a spherical linear interpolation between this quaternion and the target quaternion.
- *
- * @param {Quaternion} qb - The target quaternion.
- * @param {number} t - The interpolation factor. A value in the range `[0,1]` will interpolate. A value outside the range `[0,1]` will extrapolate.
- * @return {Quaternion} A reference to this quaternion.
- */
- slerp( qb, t ) {
-
- let x = qb._x, y = qb._y, z = qb._z, w = qb._w;
-
- let dot = this.dot( qb );
-
- if ( dot < 0 ) {
-
- x = - x;
- y = - y;
- z = - z;
- w = - w;
-
- dot = - dot;
-
- }
-
- let s = 1 - t;
-
- if ( dot < 0.9995 ) {
-
- // slerp
-
- const theta = Math.acos( dot );
- const sin = Math.sin( theta );
-
- s = Math.sin( s * theta ) / sin;
- t = Math.sin( t * theta ) / sin;
-
- this._x = this._x * s + x * t;
- this._y = this._y * s + y * t;
- this._z = this._z * s + z * t;
- this._w = this._w * s + w * t;
-
- this._onChangeCallback();
-
- } else {
-
- // for small angles, lerp then normalize
-
- this._x = this._x * s + x * t;
- this._y = this._y * s + y * t;
- this._z = this._z * s + z * t;
- this._w = this._w * s + w * t;
-
- this.normalize(); // normalize calls _onChangeCallback()
-
- }
-
- return this;
-
- }
-
- /**
- * Performs a spherical linear interpolation between the given quaternions
- * and stores the result in this quaternion.
- *
- * @param {Quaternion} qa - The source quaternion.
- * @param {Quaternion} qb - The target quaternion.
- * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
- * @return {Quaternion} A reference to this quaternion.
- */
- slerpQuaternions( qa, qb, t ) {
-
- return this.copy( qa ).slerp( qb, t );
-
- }
-
- /**
- * Sets this quaternion to a uniformly random, normalized quaternion.
- *
- * @return {Quaternion} A reference to this quaternion.
- */
- random() {
-
- // Ken Shoemake
- // Uniform random rotations
- // D. Kirk, editor, Graphics Gems III, pages 124-132. Academic Press, New York, 1992.
-
- const theta1 = 2 * Math.PI * Math.random();
- const theta2 = 2 * Math.PI * Math.random();
-
- const x0 = Math.random();
- const r1 = Math.sqrt( 1 - x0 );
- const r2 = Math.sqrt( x0 );
-
- return this.set(
- r1 * Math.sin( theta1 ),
- r1 * Math.cos( theta1 ),
- r2 * Math.sin( theta2 ),
- r2 * Math.cos( theta2 ),
- );
-
- }
-
- /**
- * Returns `true` if this quaternion is equal with the given one.
- *
- * @param {Quaternion} quaternion - The quaternion to test for equality.
- * @return {boolean} Whether this quaternion is equal with the given one.
- */
- equals( quaternion ) {
-
- return ( quaternion._x === this._x ) && ( quaternion._y === this._y ) && ( quaternion._z === this._z ) && ( quaternion._w === this._w );
-
- }
-
- /**
- * Sets this quaternion's components from the given array.
- *
- * @param {Array} array - An array holding the quaternion component values.
- * @param {number} [offset=0] - The offset into the array.
- * @return {Quaternion} A reference to this quaternion.
- */
- fromArray( array, offset = 0 ) {
-
- this._x = array[ offset ];
- this._y = array[ offset + 1 ];
- this._z = array[ offset + 2 ];
- this._w = array[ offset + 3 ];
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Writes the components of this quaternion to the given array. If no array is provided,
- * the method returns a new instance.
- *
- * @param {Array} [array=[]] - The target array holding the quaternion components.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Array} The quaternion components.
- */
- toArray( array = [], offset = 0 ) {
-
- array[ offset ] = this._x;
- array[ offset + 1 ] = this._y;
- array[ offset + 2 ] = this._z;
- array[ offset + 3 ] = this._w;
-
- return array;
-
- }
-
- /**
- * Sets the components of this quaternion from the given buffer attribute.
- *
- * @param {BufferAttribute} attribute - The buffer attribute holding quaternion data.
- * @param {number} index - The index into the attribute.
- * @return {Quaternion} A reference to this quaternion.
- */
- fromBufferAttribute( attribute, index ) {
-
- this._x = attribute.getX( index );
- this._y = attribute.getY( index );
- this._z = attribute.getZ( index );
- this._w = attribute.getW( index );
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * This methods defines the serialization result of this class. Returns the
- * numerical elements of this quaternion in an array of format `[x, y, z, w]`.
- *
- * @return {Array} The serialized quaternion.
- */
- toJSON() {
-
- return this.toArray();
-
- }
-
- _onChange( callback ) {
-
- this._onChangeCallback = callback;
-
- return this;
-
- }
-
- _onChangeCallback() {}
-
- *[ Symbol.iterator ]() {
-
- yield this._x;
- yield this._y;
- yield this._z;
- yield this._w;
-
- }
-
-}
-
-/**
- * Class representing a 3D vector. A 3D vector is an ordered triplet of numbers
- * (labeled x, y and z), which can be used to represent a number of things, such as:
- *
- * - A point in 3D space.
- * - A direction and length in 3D space. In three.js the length will
- * always be the Euclidean distance(straight-line distance) from `(0, 0, 0)` to `(x, y, z)`
- * and the direction is also measured from `(0, 0, 0)` towards `(x, y, z)`.
- * - Any arbitrary ordered triplet of numbers.
- *
- * There are other things a 3D vector can be used to represent, such as
- * momentum vectors and so on, however these are the most
- * common uses in three.js.
- *
- * Iterating through a vector instance will yield its components `(x, y, z)` in
- * the corresponding order.
- * ```js
- * const a = new THREE.Vector3( 0, 1, 0 );
- *
- * //no arguments; will be initialised to (0, 0, 0)
- * const b = new THREE.Vector3( );
- *
- * const d = a.distanceTo( b );
- * ```
- */
-class Vector3 {
-
- static {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- Vector3.prototype.isVector3 = true;
-
- }
-
- /**
- * Constructs a new 3D vector.
- *
- * @param {number} [x=0] - The x value of this vector.
- * @param {number} [y=0] - The y value of this vector.
- * @param {number} [z=0] - The z value of this vector.
- */
- constructor( x = 0, y = 0, z = 0 ) {
-
- /**
- * The x value of this vector.
- *
- * @type {number}
- */
- this.x = x;
-
- /**
- * The y value of this vector.
- *
- * @type {number}
- */
- this.y = y;
-
- /**
- * The z value of this vector.
- *
- * @type {number}
- */
- this.z = z;
-
- }
-
- /**
- * Sets the vector components.
- *
- * @param {number} x - The value of the x component.
- * @param {number} y - The value of the y component.
- * @param {number} z - The value of the z component.
- * @return {Vector3} A reference to this vector.
- */
- set( x, y, z ) {
-
- if ( z === undefined ) z = this.z; // sprite.scale.set(x,y)
-
- this.x = x;
- this.y = y;
- this.z = z;
-
- return this;
-
- }
-
- /**
- * Sets the vector components to the same value.
- *
- * @param {number} scalar - The value to set for all vector components.
- * @return {Vector3} A reference to this vector.
- */
- setScalar( scalar ) {
-
- this.x = scalar;
- this.y = scalar;
- this.z = scalar;
-
- return this;
-
- }
-
- /**
- * Sets the vector's x component to the given value.
- *
- * @param {number} x - The value to set.
- * @return {Vector3} A reference to this vector.
- */
- setX( x ) {
-
- this.x = x;
-
- return this;
-
- }
-
- /**
- * Sets the vector's y component to the given value.
- *
- * @param {number} y - The value to set.
- * @return {Vector3} A reference to this vector.
- */
- setY( y ) {
-
- this.y = y;
-
- return this;
-
- }
-
- /**
- * Sets the vector's z component to the given value.
- *
- * @param {number} z - The value to set.
- * @return {Vector3} A reference to this vector.
- */
- setZ( z ) {
-
- this.z = z;
-
- return this;
-
- }
-
- /**
- * Allows to set a vector component with an index.
- *
- * @param {number} index - The component index. `0` equals to x, `1` equals to y, `2` equals to z.
- * @param {number} value - The value to set.
- * @return {Vector3} A reference to this vector.
- */
- setComponent( index, value ) {
-
- switch ( index ) {
-
- case 0: this.x = value; break;
- case 1: this.y = value; break;
- case 2: this.z = value; break;
- default: throw new Error( 'THREE.Vector3: index is out of range: ' + index );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns the value of the vector component which matches the given index.
- *
- * @param {number} index - The component index. `0` equals to x, `1` equals to y, `2` equals to z.
- * @return {number} A vector component value.
- */
- getComponent( index ) {
-
- switch ( index ) {
-
- case 0: return this.x;
- case 1: return this.y;
- case 2: return this.z;
- default: throw new Error( 'THREE.Vector3: index is out of range: ' + index );
-
- }
-
- }
-
- /**
- * Returns a new vector with copied values from this instance.
- *
- * @return {Vector3} A clone of this instance.
- */
- clone() {
-
- return new this.constructor( this.x, this.y, this.z );
-
- }
-
- /**
- * Copies the values of the given vector to this instance.
- *
- * @param {Vector3} v - The vector to copy.
- * @return {Vector3} A reference to this vector.
- */
- copy( v ) {
-
- this.x = v.x;
- this.y = v.y;
- this.z = v.z;
-
- return this;
-
- }
-
- /**
- * Adds the given vector to this instance.
- *
- * @param {Vector3} v - The vector to add.
- * @return {Vector3} A reference to this vector.
- */
- add( v ) {
-
- this.x += v.x;
- this.y += v.y;
- this.z += v.z;
-
- return this;
-
- }
-
- /**
- * Adds the given scalar value to all components of this instance.
- *
- * @param {number} s - The scalar to add.
- * @return {Vector3} A reference to this vector.
- */
- addScalar( s ) {
-
- this.x += s;
- this.y += s;
- this.z += s;
-
- return this;
-
- }
-
- /**
- * Adds the given vectors and stores the result in this instance.
- *
- * @param {Vector3} a - The first vector.
- * @param {Vector3} b - The second vector.
- * @return {Vector3} A reference to this vector.
- */
- addVectors( a, b ) {
-
- this.x = a.x + b.x;
- this.y = a.y + b.y;
- this.z = a.z + b.z;
-
- return this;
-
- }
-
- /**
- * Adds the given vector scaled by the given factor to this instance.
- *
- * @param {Vector3|Vector4} v - The vector.
- * @param {number} s - The factor that scales `v`.
- * @return {Vector3} A reference to this vector.
- */
- addScaledVector( v, s ) {
-
- this.x += v.x * s;
- this.y += v.y * s;
- this.z += v.z * s;
-
- return this;
-
- }
-
- /**
- * Subtracts the given vector from this instance.
- *
- * @param {Vector3} v - The vector to subtract.
- * @return {Vector3} A reference to this vector.
- */
- sub( v ) {
-
- this.x -= v.x;
- this.y -= v.y;
- this.z -= v.z;
-
- return this;
-
- }
-
- /**
- * Subtracts the given scalar value from all components of this instance.
- *
- * @param {number} s - The scalar to subtract.
- * @return {Vector3} A reference to this vector.
- */
- subScalar( s ) {
-
- this.x -= s;
- this.y -= s;
- this.z -= s;
-
- return this;
-
- }
-
- /**
- * Subtracts the given vectors and stores the result in this instance.
- *
- * @param {Vector3} a - The first vector.
- * @param {Vector3} b - The second vector.
- * @return {Vector3} A reference to this vector.
- */
- subVectors( a, b ) {
-
- this.x = a.x - b.x;
- this.y = a.y - b.y;
- this.z = a.z - b.z;
-
- return this;
-
- }
-
- /**
- * Multiplies the given vector with this instance.
- *
- * @param {Vector3} v - The vector to multiply.
- * @return {Vector3} A reference to this vector.
- */
- multiply( v ) {
-
- this.x *= v.x;
- this.y *= v.y;
- this.z *= v.z;
-
- return this;
-
- }
-
- /**
- * Multiplies the given scalar value with all components of this instance.
- *
- * @param {number} scalar - The scalar to multiply.
- * @return {Vector3} A reference to this vector.
- */
- multiplyScalar( scalar ) {
-
- this.x *= scalar;
- this.y *= scalar;
- this.z *= scalar;
-
- return this;
-
- }
-
- /**
- * Multiplies the given vectors and stores the result in this instance.
- *
- * @param {Vector3} a - The first vector.
- * @param {Vector3} b - The second vector.
- * @return {Vector3} A reference to this vector.
- */
- multiplyVectors( a, b ) {
-
- this.x = a.x * b.x;
- this.y = a.y * b.y;
- this.z = a.z * b.z;
-
- return this;
-
- }
-
- /**
- * Applies the given Euler rotation to this vector.
- *
- * @param {Euler} euler - The Euler angles.
- * @return {Vector3} A reference to this vector.
- */
- applyEuler( euler ) {
-
- return this.applyQuaternion( _quaternion$5.setFromEuler( euler ) );
-
- }
-
- /**
- * Applies a rotation specified by an axis and an angle to this vector.
- *
- * @param {Vector3} axis - A normalized vector representing the rotation axis.
- * @param {number} angle - The angle in radians.
- * @return {Vector3} A reference to this vector.
- */
- applyAxisAngle( axis, angle ) {
-
- return this.applyQuaternion( _quaternion$5.setFromAxisAngle( axis, angle ) );
-
- }
-
- /**
- * Multiplies this vector with the given 3x3 matrix.
- *
- * @param {Matrix3} m - The 3x3 matrix.
- * @return {Vector3} A reference to this vector.
- */
- applyMatrix3( m ) {
-
- const x = this.x, y = this.y, z = this.z;
- const e = m.elements;
-
- this.x = e[ 0 ] * x + e[ 3 ] * y + e[ 6 ] * z;
- this.y = e[ 1 ] * x + e[ 4 ] * y + e[ 7 ] * z;
- this.z = e[ 2 ] * x + e[ 5 ] * y + e[ 8 ] * z;
-
- return this;
-
- }
-
- /**
- * Multiplies this vector by the given normal matrix and normalizes
- * the result.
- *
- * @param {Matrix3} m - The normal matrix.
- * @return {Vector3} A reference to this vector.
- */
- applyNormalMatrix( m ) {
-
- return this.applyMatrix3( m ).normalize();
-
- }
-
- /**
- * Multiplies this vector (with an implicit 1 in the 4th dimension) by m, and
- * divides by perspective.
- *
- * @param {Matrix4} m - The matrix to apply.
- * @return {Vector3} A reference to this vector.
- */
- applyMatrix4( m ) {
-
- const x = this.x, y = this.y, z = this.z;
- const e = m.elements;
-
- const w = 1 / ( e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] );
-
- this.x = ( e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] ) * w;
- this.y = ( e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] ) * w;
- this.z = ( e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] ) * w;
-
- return this;
-
- }
-
- /**
- * Applies the given Quaternion to this vector.
- *
- * @param {Quaternion} q - The Quaternion.
- * @return {Vector3} A reference to this vector.
- */
- applyQuaternion( q ) {
-
- // quaternion q is assumed to have unit length
-
- const vx = this.x, vy = this.y, vz = this.z;
- const qx = q.x, qy = q.y, qz = q.z, qw = q.w;
-
- // t = 2 * cross( q.xyz, v );
- const tx = 2 * ( qy * vz - qz * vy );
- const ty = 2 * ( qz * vx - qx * vz );
- const tz = 2 * ( qx * vy - qy * vx );
-
- // v + q.w * t + cross( q.xyz, t );
- this.x = vx + qw * tx + qy * tz - qz * ty;
- this.y = vy + qw * ty + qz * tx - qx * tz;
- this.z = vz + qw * tz + qx * ty - qy * tx;
-
- return this;
-
- }
-
- /**
- * Projects this vector from world space into the camera's normalized
- * device coordinate (NDC) space.
- *
- * @param {Camera} camera - The camera.
- * @return {Vector3} A reference to this vector.
- */
- project( camera ) {
-
- return this.applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix );
-
- }
-
- /**
- * Unprojects this vector from the camera's normalized device coordinate (NDC)
- * space into world space.
- *
- * @param {Camera} camera - The camera.
- * @return {Vector3} A reference to this vector.
- */
- unproject( camera ) {
-
- return this.applyMatrix4( camera.projectionMatrixInverse ).applyMatrix4( camera.matrixWorld );
-
- }
-
- /**
- * Transforms this vector by the upper left 3x3 sub-matrix of the given 4x4 matrix,
- * and normalizes the result.
- *
- * @param {Matrix4} m - The matrix.
- * @return {Vector3} A reference to this vector.
- */
- transformDirection( m ) {
-
- // input: THREE.Matrix4 affine matrix
- // vector interpreted as a direction
-
- const x = this.x, y = this.y, z = this.z;
- const e = m.elements;
-
- this.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z;
- this.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z;
- this.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z;
-
- return this.normalize();
-
- }
-
- /**
- * Divides this instance by the given vector.
- *
- * @param {Vector3} v - The vector to divide.
- * @return {Vector3} A reference to this vector.
- */
- divide( v ) {
-
- this.x /= v.x;
- this.y /= v.y;
- this.z /= v.z;
-
- return this;
-
- }
-
- /**
- * Divides this vector by the given scalar.
- *
- * @param {number} scalar - The scalar to divide.
- * @return {Vector3} A reference to this vector.
- */
- divideScalar( scalar ) {
-
- return this.multiplyScalar( 1 / scalar );
-
- }
-
- /**
- * If this vector's x, y or z value is greater than the given vector's x, y or z
- * value, replace that value with the corresponding min value.
- *
- * @param {Vector3} v - The vector.
- * @return {Vector3} A reference to this vector.
- */
- min( v ) {
-
- this.x = Math.min( this.x, v.x );
- this.y = Math.min( this.y, v.y );
- this.z = Math.min( this.z, v.z );
-
- return this;
-
- }
-
- /**
- * If this vector's x, y or z value is less than the given vector's x, y or z
- * value, replace that value with the corresponding max value.
- *
- * @param {Vector3} v - The vector.
- * @return {Vector3} A reference to this vector.
- */
- max( v ) {
-
- this.x = Math.max( this.x, v.x );
- this.y = Math.max( this.y, v.y );
- this.z = Math.max( this.z, v.z );
-
- return this;
-
- }
-
- /**
- * If this vector's x, y or z value is greater than the max vector's x, y or z
- * value, it is replaced by the corresponding value.
- * If this vector's x, y or z value is less than the min vector's x, y or z value,
- * it is replaced by the corresponding value.
- *
- * @param {Vector3} min - The minimum x, y and z values.
- * @param {Vector3} max - The maximum x, y and z values in the desired range.
- * @return {Vector3} A reference to this vector.
- */
- clamp( min, max ) {
-
- // assumes min < max, componentwise
-
- this.x = clamp( this.x, min.x, max.x );
- this.y = clamp( this.y, min.y, max.y );
- this.z = clamp( this.z, min.z, max.z );
-
- return this;
-
- }
-
- /**
- * If this vector's x, y or z values are greater than the max value, they are
- * replaced by the max value.
- * If this vector's x, y or z values are less than the min value, they are
- * replaced by the min value.
- *
- * @param {number} minVal - The minimum value the components will be clamped to.
- * @param {number} maxVal - The maximum value the components will be clamped to.
- * @return {Vector3} A reference to this vector.
- */
- clampScalar( minVal, maxVal ) {
-
- this.x = clamp( this.x, minVal, maxVal );
- this.y = clamp( this.y, minVal, maxVal );
- this.z = clamp( this.z, minVal, maxVal );
-
- return this;
-
- }
-
- /**
- * If this vector's length is greater than the max value, it is replaced by
- * the max value.
- * If this vector's length is less than the min value, it is replaced by the
- * min value.
- *
- * @param {number} min - The minimum value the vector length will be clamped to.
- * @param {number} max - The maximum value the vector length will be clamped to.
- * @return {Vector3} A reference to this vector.
- */
- clampLength( min, max ) {
-
- const length = this.length();
-
- return this.divideScalar( length || 1 ).multiplyScalar( clamp( length, min, max ) );
-
- }
-
- /**
- * The components of this vector are rounded down to the nearest integer value.
- *
- * @return {Vector3} A reference to this vector.
- */
- floor() {
-
- this.x = Math.floor( this.x );
- this.y = Math.floor( this.y );
- this.z = Math.floor( this.z );
-
- return this;
-
- }
-
- /**
- * The components of this vector are rounded up to the nearest integer value.
- *
- * @return {Vector3} A reference to this vector.
- */
- ceil() {
-
- this.x = Math.ceil( this.x );
- this.y = Math.ceil( this.y );
- this.z = Math.ceil( this.z );
-
- return this;
-
- }
-
- /**
- * The components of this vector are rounded to the nearest integer value
- *
- * @return {Vector3} A reference to this vector.
- */
- round() {
-
- this.x = Math.round( this.x );
- this.y = Math.round( this.y );
- this.z = Math.round( this.z );
-
- return this;
-
- }
-
- /**
- * The components of this vector are rounded towards zero (up if negative,
- * down if positive) to an integer value.
- *
- * @return {Vector3} A reference to this vector.
- */
- roundToZero() {
-
- this.x = Math.trunc( this.x );
- this.y = Math.trunc( this.y );
- this.z = Math.trunc( this.z );
-
- return this;
-
- }
-
- /**
- * Inverts this vector - i.e. sets x = -x, y = -y and z = -z.
- *
- * @return {Vector3} A reference to this vector.
- */
- negate() {
-
- this.x = - this.x;
- this.y = - this.y;
- this.z = - this.z;
-
- return this;
-
- }
-
- /**
- * Calculates the dot product of the given vector with this instance.
- *
- * @param {Vector3} v - The vector to compute the dot product with.
- * @return {number} The result of the dot product.
- */
- dot( v ) {
-
- return this.x * v.x + this.y * v.y + this.z * v.z;
-
- }
-
- /**
- * Computes the square of the Euclidean length (straight-line length) from
- * (0, 0, 0) to (x, y, z). If you are comparing the lengths of vectors, you should
- * compare the length squared instead as it is slightly more efficient to calculate.
- *
- * @return {number} The square length of this vector.
- */
- lengthSq() {
-
- return this.x * this.x + this.y * this.y + this.z * this.z;
-
- }
-
- /**
- * Computes the Euclidean length (straight-line length) from (0, 0, 0) to (x, y, z).
- *
- * @return {number} The length of this vector.
- */
- length() {
-
- return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z );
-
- }
-
- /**
- * Computes the Manhattan length of this vector.
- *
- * @return {number} The length of this vector.
- */
- manhattanLength() {
-
- return Math.abs( this.x ) + Math.abs( this.y ) + Math.abs( this.z );
-
- }
-
- /**
- * Converts this vector to a unit vector - that is, sets it equal to a vector
- * with the same direction as this one, but with a vector length of `1`.
- *
- * @return {Vector3} A reference to this vector.
- */
- normalize() {
-
- return this.divideScalar( this.length() || 1 );
-
- }
-
- /**
- * Sets this vector to a vector with the same direction as this one, but
- * with the specified length.
- *
- * @param {number} length - The new length of this vector.
- * @return {Vector3} A reference to this vector.
- */
- setLength( length ) {
-
- return this.normalize().multiplyScalar( length );
-
- }
-
- /**
- * Linearly interpolates between the given vector and this instance, where
- * alpha is the percent distance along the line - alpha = 0 will be this
- * vector, and alpha = 1 will be the given one.
- *
- * @param {Vector3} v - The vector to interpolate towards.
- * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
- * @return {Vector3} A reference to this vector.
- */
- lerp( v, alpha ) {
-
- this.x += ( v.x - this.x ) * alpha;
- this.y += ( v.y - this.y ) * alpha;
- this.z += ( v.z - this.z ) * alpha;
-
- return this;
-
- }
-
- /**
- * Linearly interpolates between the given vectors, where alpha is the percent
- * distance along the line - alpha = 0 will be first vector, and alpha = 1 will
- * be the second one. The result is stored in this instance.
- *
- * @param {Vector3} v1 - The first vector.
- * @param {Vector3} v2 - The second vector.
- * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
- * @return {Vector3} A reference to this vector.
- */
- lerpVectors( v1, v2, alpha ) {
-
- this.x = v1.x + ( v2.x - v1.x ) * alpha;
- this.y = v1.y + ( v2.y - v1.y ) * alpha;
- this.z = v1.z + ( v2.z - v1.z ) * alpha;
-
- return this;
-
- }
-
- /**
- * Calculates the cross product of the given vector with this instance.
- *
- * @param {Vector3} v - The vector to compute the cross product with.
- * @return {Vector3} The result of the cross product.
- */
- cross( v ) {
-
- return this.crossVectors( this, v );
-
- }
-
- /**
- * Calculates the cross product of the given vectors and stores the result
- * in this instance.
- *
- * @param {Vector3} a - The first vector.
- * @param {Vector3} b - The second vector.
- * @return {Vector3} A reference to this vector.
- */
- crossVectors( a, b ) {
-
- const ax = a.x, ay = a.y, az = a.z;
- const bx = b.x, by = b.y, bz = b.z;
-
- this.x = ay * bz - az * by;
- this.y = az * bx - ax * bz;
- this.z = ax * by - ay * bx;
-
- return this;
-
- }
-
- /**
- * Projects this vector onto the given one.
- *
- * @param {Vector3} v - The vector to project to.
- * @return {Vector3} A reference to this vector.
- */
- projectOnVector( v ) {
-
- const denominator = v.lengthSq();
-
- if ( denominator === 0 ) return this.set( 0, 0, 0 );
-
- const scalar = v.dot( this ) / denominator;
-
- return this.copy( v ).multiplyScalar( scalar );
-
- }
-
- /**
- * Projects this vector onto a plane by subtracting this
- * vector projected onto the plane's normal from this vector.
- *
- * @param {Vector3} planeNormal - The plane normal.
- * @return {Vector3} A reference to this vector.
- */
- projectOnPlane( planeNormal ) {
-
- _vector$c.copy( this ).projectOnVector( planeNormal );
-
- return this.sub( _vector$c );
-
- }
-
- /**
- * Reflects this vector off a plane orthogonal to the given normal vector.
- *
- * @param {Vector3} normal - The (normalized) normal vector.
- * @return {Vector3} A reference to this vector.
- */
- reflect( normal ) {
-
- return this.sub( _vector$c.copy( normal ).multiplyScalar( 2 * this.dot( normal ) ) );
-
- }
- /**
- * Returns the angle between the given vector and this instance in radians.
- *
- * @param {Vector3} v - The vector to compute the angle with.
- * @return {number} The angle in radians.
- */
- angleTo( v ) {
-
- const denominator = Math.sqrt( this.lengthSq() * v.lengthSq() );
-
- if ( denominator === 0 ) return Math.PI / 2;
-
- const theta = this.dot( v ) / denominator;
-
- // clamp, to handle numerical problems
-
- return Math.acos( clamp( theta, -1, 1 ) );
-
- }
-
- /**
- * Computes the distance from the given vector to this instance.
- *
- * @param {Vector3} v - The vector to compute the distance to.
- * @return {number} The distance.
- */
- distanceTo( v ) {
-
- return Math.sqrt( this.distanceToSquared( v ) );
-
- }
-
- /**
- * Computes the squared distance from the given vector to this instance.
- * If you are just comparing the distance with another distance, you should compare
- * the distance squared instead as it is slightly more efficient to calculate.
- *
- * @param {Vector3} v - The vector to compute the squared distance to.
- * @return {number} The squared distance.
- */
- distanceToSquared( v ) {
-
- const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z;
-
- return dx * dx + dy * dy + dz * dz;
-
- }
-
- /**
- * Computes the Manhattan distance from the given vector to this instance.
- *
- * @param {Vector3} v - The vector to compute the Manhattan distance to.
- * @return {number} The Manhattan distance.
- */
- manhattanDistanceTo( v ) {
-
- return Math.abs( this.x - v.x ) + Math.abs( this.y - v.y ) + Math.abs( this.z - v.z );
-
- }
-
- /**
- * Sets the vector components from the given spherical coordinates.
- *
- * @param {Spherical} s - The spherical coordinates.
- * @return {Vector3} A reference to this vector.
- */
- setFromSpherical( s ) {
-
- return this.setFromSphericalCoords( s.radius, s.phi, s.theta );
-
- }
-
- /**
- * Sets the vector components from the given spherical coordinates.
- *
- * @param {number} radius - The radius.
- * @param {number} phi - The phi angle in radians.
- * @param {number} theta - The theta angle in radians.
- * @return {Vector3} A reference to this vector.
- */
- setFromSphericalCoords( radius, phi, theta ) {
-
- const sinPhiRadius = Math.sin( phi ) * radius;
-
- this.x = sinPhiRadius * Math.sin( theta );
- this.y = Math.cos( phi ) * radius;
- this.z = sinPhiRadius * Math.cos( theta );
-
- return this;
-
- }
-
- /**
- * Sets the vector components from the given cylindrical coordinates.
- *
- * @param {Cylindrical} c - The cylindrical coordinates.
- * @return {Vector3} A reference to this vector.
- */
- setFromCylindrical( c ) {
-
- return this.setFromCylindricalCoords( c.radius, c.theta, c.y );
-
- }
-
- /**
- * Sets the vector components from the given cylindrical coordinates.
- *
- * @param {number} radius - The radius.
- * @param {number} theta - The theta angle in radians.
- * @param {number} y - The y value.
- * @return {Vector3} A reference to this vector.
- */
- setFromCylindricalCoords( radius, theta, y ) {
-
- this.x = radius * Math.sin( theta );
- this.y = y;
- this.z = radius * Math.cos( theta );
-
- return this;
-
- }
-
- /**
- * Sets the vector components to the position elements of the
- * given transformation matrix.
- *
- * @param {Matrix4} m - The 4x4 matrix.
- * @return {Vector3} A reference to this vector.
- */
- setFromMatrixPosition( m ) {
-
- const e = m.elements;
-
- this.x = e[ 12 ];
- this.y = e[ 13 ];
- this.z = e[ 14 ];
-
- return this;
-
- }
-
- /**
- * Sets the vector components to the scale elements of the
- * given transformation matrix.
- *
- * @param {Matrix4} m - The 4x4 matrix.
- * @return {Vector3} A reference to this vector.
- */
- setFromMatrixScale( m ) {
-
- const sx = this.setFromMatrixColumn( m, 0 ).length();
- const sy = this.setFromMatrixColumn( m, 1 ).length();
- const sz = this.setFromMatrixColumn( m, 2 ).length();
-
- this.x = sx;
- this.y = sy;
- this.z = sz;
-
- return this;
-
- }
-
- /**
- * Sets the vector components from the specified matrix column.
- *
- * @param {Matrix4} m - The 4x4 matrix.
- * @param {number} index - The column index.
- * @return {Vector3} A reference to this vector.
- */
- setFromMatrixColumn( m, index ) {
-
- return this.fromArray( m.elements, index * 4 );
-
- }
-
- /**
- * Sets the vector components from the specified matrix column.
- *
- * @param {Matrix3} m - The 3x3 matrix.
- * @param {number} index - The column index.
- * @return {Vector3} A reference to this vector.
- */
- setFromMatrix3Column( m, index ) {
-
- return this.fromArray( m.elements, index * 3 );
-
- }
-
- /**
- * Sets the vector components from the given Euler angles.
- *
- * @param {Euler} e - The Euler angles to set.
- * @return {Vector3} A reference to this vector.
- */
- setFromEuler( e ) {
-
- this.x = e._x;
- this.y = e._y;
- this.z = e._z;
-
- return this;
-
- }
-
- /**
- * Sets the vector components from the RGB components of the
- * given color.
- *
- * @param {Color} c - The color to set.
- * @return {Vector3} A reference to this vector.
- */
- setFromColor( c ) {
-
- this.x = c.r;
- this.y = c.g;
- this.z = c.b;
-
- return this;
-
- }
-
- /**
- * Returns `true` if this vector is equal with the given one.
- *
- * @param {Vector3} v - The vector to test for equality.
- * @return {boolean} Whether this vector is equal with the given one.
- */
- equals( v ) {
-
- return ( ( v.x === this.x ) && ( v.y === this.y ) && ( v.z === this.z ) );
-
- }
-
- /**
- * Sets this vector's x value to be `array[ offset ]`, y value to be `array[ offset + 1 ]`
- * and z value to be `array[ offset + 2 ]`.
- *
- * @param {Array} array - An array holding the vector component values.
- * @param {number} [offset=0] - The offset into the array.
- * @return {Vector3} A reference to this vector.
- */
- fromArray( array, offset = 0 ) {
-
- this.x = array[ offset ];
- this.y = array[ offset + 1 ];
- this.z = array[ offset + 2 ];
-
- return this;
-
- }
-
- /**
- * Writes the components of this vector to the given array. If no array is provided,
- * the method returns a new instance.
- *
- * @param {Array} [array=[]] - The target array holding the vector components.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Array} The vector components.
- */
- toArray( array = [], offset = 0 ) {
-
- array[ offset ] = this.x;
- array[ offset + 1 ] = this.y;
- array[ offset + 2 ] = this.z;
-
- return array;
-
- }
-
- /**
- * Sets the components of this vector from the given buffer attribute.
- *
- * @param {BufferAttribute} attribute - The buffer attribute holding vector data.
- * @param {number} index - The index into the attribute.
- * @return {Vector3} A reference to this vector.
- */
- fromBufferAttribute( attribute, index ) {
-
- this.x = attribute.getX( index );
- this.y = attribute.getY( index );
- this.z = attribute.getZ( index );
-
- return this;
-
- }
-
- /**
- * Sets each component of this vector to a pseudo-random value between `0` and
- * `1`, excluding `1`.
- *
- * @return {Vector3} A reference to this vector.
- */
- random() {
-
- this.x = Math.random();
- this.y = Math.random();
- this.z = Math.random();
-
- return this;
-
- }
-
- /**
- * Sets this vector to a uniformly random point on a unit sphere.
- *
- * @return {Vector3} A reference to this vector.
- */
- randomDirection() {
-
- // https://mathworld.wolfram.com/SpherePointPicking.html
-
- const theta = Math.random() * Math.PI * 2;
- const u = Math.random() * 2 - 1;
- const c = Math.sqrt( 1 - u * u );
-
- this.x = c * Math.cos( theta );
- this.y = u;
- this.z = c * Math.sin( theta );
-
- return this;
-
- }
-
- *[ Symbol.iterator ]() {
-
- yield this.x;
- yield this.y;
- yield this.z;
-
- }
-
-}
-
-const _vector$c = /*@__PURE__*/ new Vector3();
-const _quaternion$5 = /*@__PURE__*/ new Quaternion();
-
-/**
- * Represents a 3x3 matrix.
- *
- * A Note on Row-Major and Column-Major Ordering:
- *
- * The constructor and {@link Matrix3#set} method take arguments in
- * [row-major](https://en.wikipedia.org/wiki/Row-_and_column-major_order#Column-major_order)
- * order, while internally they are stored in the {@link Matrix3#elements} array in column-major order.
- * This means that calling:
- * ```js
- * const m = new THREE.Matrix();
- * m.set( 11, 12, 13,
- * 21, 22, 23,
- * 31, 32, 33 );
- * ```
- * will result in the elements array containing:
- * ```js
- * m.elements = [ 11, 21, 31,
- * 12, 22, 32,
- * 13, 23, 33 ];
- * ```
- * and internally all calculations are performed using column-major ordering.
- * However, as the actual ordering makes no difference mathematically and
- * most people are used to thinking about matrices in row-major order, the
- * three.js documentation shows matrices in row-major order. Just bear in
- * mind that if you are reading the source code, you'll have to take the
- * transpose of any matrices outlined here to make sense of the calculations.
- */
-class Matrix3 {
-
- static {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- Matrix3.prototype.isMatrix3 = true;
-
- }
-
- /**
- * Constructs a new 3x3 matrix. The arguments are supposed to be
- * in row-major order. If no arguments are provided, the constructor
- * initializes the matrix as an identity matrix.
- *
- * @param {number} [n11] - 1-1 matrix element.
- * @param {number} [n12] - 1-2 matrix element.
- * @param {number} [n13] - 1-3 matrix element.
- * @param {number} [n21] - 2-1 matrix element.
- * @param {number} [n22] - 2-2 matrix element.
- * @param {number} [n23] - 2-3 matrix element.
- * @param {number} [n31] - 3-1 matrix element.
- * @param {number} [n32] - 3-2 matrix element.
- * @param {number} [n33] - 3-3 matrix element.
- */
- constructor( n11, n12, n13, n21, n22, n23, n31, n32, n33 ) {
-
- /**
- * A column-major list of matrix values.
- *
- * @type {Array}
- */
- this.elements = [
-
- 1, 0, 0,
- 0, 1, 0,
- 0, 0, 1
-
- ];
-
- if ( n11 !== undefined ) {
-
- this.set( n11, n12, n13, n21, n22, n23, n31, n32, n33 );
-
- }
-
- }
-
- /**
- * Sets the elements of the matrix.The arguments are supposed to be
- * in row-major order.
- *
- * @param {number} [n11] - 1-1 matrix element.
- * @param {number} [n12] - 1-2 matrix element.
- * @param {number} [n13] - 1-3 matrix element.
- * @param {number} [n21] - 2-1 matrix element.
- * @param {number} [n22] - 2-2 matrix element.
- * @param {number} [n23] - 2-3 matrix element.
- * @param {number} [n31] - 3-1 matrix element.
- * @param {number} [n32] - 3-2 matrix element.
- * @param {number} [n33] - 3-3 matrix element.
- * @return {Matrix3} A reference to this matrix.
- */
- set( n11, n12, n13, n21, n22, n23, n31, n32, n33 ) {
-
- const te = this.elements;
-
- te[ 0 ] = n11; te[ 1 ] = n21; te[ 2 ] = n31;
- te[ 3 ] = n12; te[ 4 ] = n22; te[ 5 ] = n32;
- te[ 6 ] = n13; te[ 7 ] = n23; te[ 8 ] = n33;
-
- return this;
-
- }
-
- /**
- * Sets this matrix to the 3x3 identity matrix.
- *
- * @return {Matrix3} A reference to this matrix.
- */
- identity() {
-
- this.set(
-
- 1, 0, 0,
- 0, 1, 0,
- 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Copies the values of the given matrix to this instance.
- *
- * @param {Matrix3} m - The matrix to copy.
- * @return {Matrix3} A reference to this matrix.
- */
- copy( m ) {
-
- const te = this.elements;
- const me = m.elements;
-
- te[ 0 ] = me[ 0 ]; te[ 1 ] = me[ 1 ]; te[ 2 ] = me[ 2 ];
- te[ 3 ] = me[ 3 ]; te[ 4 ] = me[ 4 ]; te[ 5 ] = me[ 5 ];
- te[ 6 ] = me[ 6 ]; te[ 7 ] = me[ 7 ]; te[ 8 ] = me[ 8 ];
-
- return this;
-
- }
-
- /**
- * Extracts the basis of this matrix into the three axis vectors provided.
- *
- * @param {Vector3} xAxis - The basis's x axis.
- * @param {Vector3} yAxis - The basis's y axis.
- * @param {Vector3} zAxis - The basis's z axis.
- * @return {Matrix3} A reference to this matrix.
- */
- extractBasis( xAxis, yAxis, zAxis ) {
-
- xAxis.setFromMatrix3Column( this, 0 );
- yAxis.setFromMatrix3Column( this, 1 );
- zAxis.setFromMatrix3Column( this, 2 );
-
- return this;
-
- }
-
- /**
- * Set this matrix to the upper 3x3 matrix of the given 4x4 matrix.
- *
- * @param {Matrix4} m - The 4x4 matrix.
- * @return {Matrix3} A reference to this matrix.
- */
- setFromMatrix4( m ) {
-
- const me = m.elements;
-
- this.set(
-
- me[ 0 ], me[ 4 ], me[ 8 ],
- me[ 1 ], me[ 5 ], me[ 9 ],
- me[ 2 ], me[ 6 ], me[ 10 ]
-
- );
-
- return this;
-
- }
-
- /**
- * Post-multiplies this matrix by the given 3x3 matrix.
- *
- * @param {Matrix3} m - The matrix to multiply with.
- * @return {Matrix3} A reference to this matrix.
- */
- multiply( m ) {
-
- return this.multiplyMatrices( this, m );
-
- }
-
- /**
- * Pre-multiplies this matrix by the given 3x3 matrix.
- *
- * @param {Matrix3} m - The matrix to multiply with.
- * @return {Matrix3} A reference to this matrix.
- */
- premultiply( m ) {
-
- return this.multiplyMatrices( m, this );
-
- }
-
- /**
- * Multiples the given 3x3 matrices and stores the result
- * in this matrix.
- *
- * @param {Matrix3} a - The first matrix.
- * @param {Matrix3} b - The second matrix.
- * @return {Matrix3} A reference to this matrix.
- */
- multiplyMatrices( a, b ) {
-
- const ae = a.elements;
- const be = b.elements;
- const te = this.elements;
-
- const a11 = ae[ 0 ], a12 = ae[ 3 ], a13 = ae[ 6 ];
- const a21 = ae[ 1 ], a22 = ae[ 4 ], a23 = ae[ 7 ];
- const a31 = ae[ 2 ], a32 = ae[ 5 ], a33 = ae[ 8 ];
-
- const b11 = be[ 0 ], b12 = be[ 3 ], b13 = be[ 6 ];
- const b21 = be[ 1 ], b22 = be[ 4 ], b23 = be[ 7 ];
- const b31 = be[ 2 ], b32 = be[ 5 ], b33 = be[ 8 ];
-
- te[ 0 ] = a11 * b11 + a12 * b21 + a13 * b31;
- te[ 3 ] = a11 * b12 + a12 * b22 + a13 * b32;
- te[ 6 ] = a11 * b13 + a12 * b23 + a13 * b33;
-
- te[ 1 ] = a21 * b11 + a22 * b21 + a23 * b31;
- te[ 4 ] = a21 * b12 + a22 * b22 + a23 * b32;
- te[ 7 ] = a21 * b13 + a22 * b23 + a23 * b33;
-
- te[ 2 ] = a31 * b11 + a32 * b21 + a33 * b31;
- te[ 5 ] = a31 * b12 + a32 * b22 + a33 * b32;
- te[ 8 ] = a31 * b13 + a32 * b23 + a33 * b33;
-
- return this;
-
- }
-
- /**
- * Multiplies every component of the matrix by the given scalar.
- *
- * @param {number} s - The scalar.
- * @return {Matrix3} A reference to this matrix.
- */
- multiplyScalar( s ) {
-
- const te = this.elements;
-
- te[ 0 ] *= s; te[ 3 ] *= s; te[ 6 ] *= s;
- te[ 1 ] *= s; te[ 4 ] *= s; te[ 7 ] *= s;
- te[ 2 ] *= s; te[ 5 ] *= s; te[ 8 ] *= s;
-
- return this;
-
- }
-
- /**
- * Computes and returns the determinant of this matrix.
- *
- * @return {number} The determinant.
- */
- determinant() {
-
- const te = this.elements;
-
- const a = te[ 0 ], b = te[ 1 ], c = te[ 2 ],
- d = te[ 3 ], e = te[ 4 ], f = te[ 5 ],
- g = te[ 6 ], h = te[ 7 ], i = te[ 8 ];
-
- return a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g;
-
- }
-
- /**
- * Inverts this matrix, using the [analytic method](https://en.wikipedia.org/wiki/Invertible_matrix#Analytic_solution).
- * You can not invert with a determinant of zero. If you attempt this, the method produces
- * a zero matrix instead.
- *
- * @return {Matrix3} A reference to this matrix.
- */
- invert() {
-
- const te = this.elements,
-
- n11 = te[ 0 ], n21 = te[ 1 ], n31 = te[ 2 ],
- n12 = te[ 3 ], n22 = te[ 4 ], n32 = te[ 5 ],
- n13 = te[ 6 ], n23 = te[ 7 ], n33 = te[ 8 ],
-
- t11 = n33 * n22 - n32 * n23,
- t12 = n32 * n13 - n33 * n12,
- t13 = n23 * n12 - n22 * n13,
-
- det = n11 * t11 + n21 * t12 + n31 * t13;
-
- if ( det === 0 ) return this.set( 0, 0, 0, 0, 0, 0, 0, 0, 0 );
-
- const detInv = 1 / det;
-
- te[ 0 ] = t11 * detInv;
- te[ 1 ] = ( n31 * n23 - n33 * n21 ) * detInv;
- te[ 2 ] = ( n32 * n21 - n31 * n22 ) * detInv;
-
- te[ 3 ] = t12 * detInv;
- te[ 4 ] = ( n33 * n11 - n31 * n13 ) * detInv;
- te[ 5 ] = ( n31 * n12 - n32 * n11 ) * detInv;
-
- te[ 6 ] = t13 * detInv;
- te[ 7 ] = ( n21 * n13 - n23 * n11 ) * detInv;
- te[ 8 ] = ( n22 * n11 - n21 * n12 ) * detInv;
-
- return this;
-
- }
-
- /**
- * Transposes this matrix in place.
- *
- * @return {Matrix3} A reference to this matrix.
- */
- transpose() {
-
- let tmp;
- const m = this.elements;
-
- tmp = m[ 1 ]; m[ 1 ] = m[ 3 ]; m[ 3 ] = tmp;
- tmp = m[ 2 ]; m[ 2 ] = m[ 6 ]; m[ 6 ] = tmp;
- tmp = m[ 5 ]; m[ 5 ] = m[ 7 ]; m[ 7 ] = tmp;
-
- return this;
-
- }
-
- /**
- * Computes the normal matrix which is the inverse transpose of the upper
- * left 3x3 portion of the given 4x4 matrix.
- *
- * @param {Matrix4} matrix4 - The 4x4 matrix.
- * @return {Matrix3} A reference to this matrix.
- */
- getNormalMatrix( matrix4 ) {
-
- return this.setFromMatrix4( matrix4 ).invert().transpose();
-
- }
-
- /**
- * Transposes this matrix into the supplied array, and returns itself unchanged.
- *
- * @param {Array} r - An array to store the transposed matrix elements.
- * @return {Matrix3} A reference to this matrix.
- */
- transposeIntoArray( r ) {
-
- const m = this.elements;
-
- r[ 0 ] = m[ 0 ];
- r[ 1 ] = m[ 3 ];
- r[ 2 ] = m[ 6 ];
- r[ 3 ] = m[ 1 ];
- r[ 4 ] = m[ 4 ];
- r[ 5 ] = m[ 7 ];
- r[ 6 ] = m[ 2 ];
- r[ 7 ] = m[ 5 ];
- r[ 8 ] = m[ 8 ];
-
- return this;
-
- }
-
- /**
- * Sets the UV transform matrix from offset, repeat, rotation, and center.
- *
- * @param {number} tx - Offset x.
- * @param {number} ty - Offset y.
- * @param {number} sx - Repeat x.
- * @param {number} sy - Repeat y.
- * @param {number} rotation - Rotation, in radians. Positive values rotate counterclockwise.
- * @param {number} cx - Center x of rotation.
- * @param {number} cy - Center y of rotation
- * @return {Matrix3} A reference to this matrix.
- */
- setUvTransform( tx, ty, sx, sy, rotation, cx, cy ) {
-
- const c = Math.cos( rotation );
- const s = Math.sin( rotation );
-
- this.set(
- sx * c, sx * s, - sx * ( c * cx + s * cy ) + cx + tx,
- - sy * s, sy * c, - sy * ( - s * cx + c * cy ) + cy + ty,
- 0, 0, 1
- );
-
- return this;
-
- }
-
- /**
- * Scales this matrix with the given scalar values.
- *
- * @deprecated
- * @param {number} sx - The amount to scale in the X axis.
- * @param {number} sy - The amount to scale in the Y axis.
- * @return {Matrix3} A reference to this matrix.
- */
- scale( sx, sy ) {
-
- warnOnce( 'Matrix3: .scale() is deprecated. Use .makeScale() instead.' ); // @deprecated r185
-
- this.premultiply( _m3.makeScale( sx, sy ) );
-
- return this;
-
- }
-
- /**
- * Rotates this matrix by the given angle.
- *
- * @deprecated
- * @param {number} theta - The rotation in radians.
- * @return {Matrix3} A reference to this matrix.
- */
- rotate( theta ) {
-
- warnOnce( 'Matrix3: .rotate() is deprecated. Use .makeRotation() instead.' ); // @deprecated r185
-
- this.premultiply( _m3.makeRotation( - theta ) );
-
- return this;
-
- }
-
- /**
- * Translates this matrix by the given scalar values.
- *
- * @deprecated
- * @param {number} tx - The amount to translate in the X axis.
- * @param {number} ty - The amount to translate in the Y axis.
- * @return {Matrix3} A reference to this matrix.
- */
- translate( tx, ty ) {
-
- warnOnce( 'Matrix3: .translate() is deprecated. Use .makeTranslation() instead.' ); // @deprecated r185
-
- this.premultiply( _m3.makeTranslation( tx, ty ) );
-
- return this;
-
- }
-
- // for 2D Transforms
-
- /**
- * Sets this matrix as a 2D translation transform.
- *
- * @param {number|Vector2} x - The amount to translate in the X axis or alternatively a translation vector.
- * @param {number} y - The amount to translate in the Y axis.
- * @return {Matrix3} A reference to this matrix.
- */
- makeTranslation( x, y ) {
-
- if ( x.isVector2 ) {
-
- this.set(
-
- 1, 0, x.x,
- 0, 1, x.y,
- 0, 0, 1
-
- );
-
- } else {
-
- this.set(
-
- 1, 0, x,
- 0, 1, y,
- 0, 0, 1
-
- );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets this matrix as a 2D rotational transformation.
- *
- * @param {number} theta - The rotation in radians.
- * @return {Matrix3} A reference to this matrix.
- */
- makeRotation( theta ) {
-
- // counterclockwise
-
- const c = Math.cos( theta );
- const s = Math.sin( theta );
-
- this.set(
-
- c, - s, 0,
- s, c, 0,
- 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Sets this matrix as a 2D scale transform.
- *
- * @param {number} x - The amount to scale in the X axis.
- * @param {number} y - The amount to scale in the Y axis.
- * @return {Matrix3} A reference to this matrix.
- */
- makeScale( x, y ) {
-
- this.set(
-
- x, 0, 0,
- 0, y, 0,
- 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Returns `true` if this matrix is equal with the given one.
- *
- * @param {Matrix3} matrix - The matrix to test for equality.
- * @return {boolean} Whether this matrix is equal with the given one.
- */
- equals( matrix ) {
-
- const te = this.elements;
- const me = matrix.elements;
-
- for ( let i = 0; i < 9; i ++ ) {
-
- if ( te[ i ] !== me[ i ] ) return false;
-
- }
-
- return true;
-
- }
-
- /**
- * Sets the elements of the matrix from the given array.
- *
- * @param {Array} array - The matrix elements in column-major order.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Matrix3} A reference to this matrix.
- */
- fromArray( array, offset = 0 ) {
-
- for ( let i = 0; i < 9; i ++ ) {
-
- this.elements[ i ] = array[ i + offset ];
-
- }
-
- return this;
-
- }
-
- /**
- * Writes the elements of this matrix to the given array. If no array is provided,
- * the method returns a new instance.
- *
- * @param {Array} [array=[]] - The target array holding the matrix elements in column-major order.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Array} The matrix elements in column-major order.
- */
- toArray( array = [], offset = 0 ) {
-
- const te = this.elements;
-
- array[ offset ] = te[ 0 ];
- array[ offset + 1 ] = te[ 1 ];
- array[ offset + 2 ] = te[ 2 ];
-
- array[ offset + 3 ] = te[ 3 ];
- array[ offset + 4 ] = te[ 4 ];
- array[ offset + 5 ] = te[ 5 ];
-
- array[ offset + 6 ] = te[ 6 ];
- array[ offset + 7 ] = te[ 7 ];
- array[ offset + 8 ] = te[ 8 ];
-
- return array;
-
- }
-
- /**
- * Returns a matrix with copied values from this instance.
- *
- * @return {Matrix3} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().fromArray( this.elements );
-
- }
-
-}
-
-const _m3 = /*@__PURE__*/ new Matrix3();
-
-const LINEAR_REC709_TO_XYZ = /*@__PURE__*/ new Matrix3().set(
- 0.4123908, 0.3575843, 0.1804808,
- 0.2126390, 0.7151687, 0.0721923,
- 0.0193308, 0.1191948, 0.9505322
-);
-
-const XYZ_TO_LINEAR_REC709 = /*@__PURE__*/ new Matrix3().set(
- 3.2409699, -1.5373832, -0.4986108,
- -0.9692436, 1.8759675, 0.0415551,
- 0.0556301, -0.203977, 1.0569715
-);
-
-function createColorManagement() {
-
- const ColorManagement = {
-
- enabled: true,
-
- workingColorSpace: LinearSRGBColorSpace,
-
- /**
- * Implementations of supported color spaces.
- *
- * Required:
- * - primaries: chromaticity coordinates [ rx ry gx gy bx by ]
- * - whitePoint: reference white [ x y ]
- * - transfer: transfer function (pre-defined)
- * - toXYZ: Matrix3 RGB to XYZ transform
- * - fromXYZ: Matrix3 XYZ to RGB transform
- * - luminanceCoefficients: RGB luminance coefficients
- *
- * Optional:
- * - outputColorSpaceConfig: { drawingBufferColorSpace: ColorSpace, toneMappingMode: 'extended' | 'standard' }
- * - workingColorSpaceConfig: { unpackColorSpace: ColorSpace }
- *
- * Reference:
- * - https://www.russellcottrell.com/photo/matrixCalculator.htm
- */
- spaces: {},
-
- convert: function ( color, sourceColorSpace, targetColorSpace ) {
-
- if ( this.enabled === false || sourceColorSpace === targetColorSpace || ! sourceColorSpace || ! targetColorSpace ) {
-
- return color;
-
- }
-
- if ( this.spaces[ sourceColorSpace ].transfer === SRGBTransfer ) {
-
- color.r = SRGBToLinear( color.r );
- color.g = SRGBToLinear( color.g );
- color.b = SRGBToLinear( color.b );
-
- }
-
- if ( this.spaces[ sourceColorSpace ].primaries !== this.spaces[ targetColorSpace ].primaries ) {
-
- color.applyMatrix3( this.spaces[ sourceColorSpace ].toXYZ );
- color.applyMatrix3( this.spaces[ targetColorSpace ].fromXYZ );
-
- }
-
- if ( this.spaces[ targetColorSpace ].transfer === SRGBTransfer ) {
-
- color.r = LinearToSRGB( color.r );
- color.g = LinearToSRGB( color.g );
- color.b = LinearToSRGB( color.b );
-
- }
-
- return color;
-
- },
-
- workingToColorSpace: function ( color, targetColorSpace ) {
-
- return this.convert( color, this.workingColorSpace, targetColorSpace );
-
- },
-
- colorSpaceToWorking: function ( color, sourceColorSpace ) {
-
- return this.convert( color, sourceColorSpace, this.workingColorSpace );
-
- },
-
- getPrimaries: function ( colorSpace ) {
-
- return this.spaces[ colorSpace ].primaries;
-
- },
-
- getTransfer: function ( colorSpace ) {
-
- if ( colorSpace === NoColorSpace ) return LinearTransfer;
-
- return this.spaces[ colorSpace ].transfer;
-
- },
-
- getToneMappingMode: function ( colorSpace ) {
-
- return this.spaces[ colorSpace ].outputColorSpaceConfig.toneMappingMode || 'standard';
-
- },
-
- getLuminanceCoefficients: function ( target, colorSpace = this.workingColorSpace ) {
-
- return target.fromArray( this.spaces[ colorSpace ].luminanceCoefficients );
-
- },
-
- define: function ( colorSpaces ) {
-
- Object.assign( this.spaces, colorSpaces );
-
- },
-
- // Internal APIs
-
- _getMatrix: function ( targetMatrix, sourceColorSpace, targetColorSpace ) {
-
- return targetMatrix
- .copy( this.spaces[ sourceColorSpace ].toXYZ )
- .multiply( this.spaces[ targetColorSpace ].fromXYZ );
-
- },
-
- _getDrawingBufferColorSpace: function ( colorSpace ) {
-
- return this.spaces[ colorSpace ].outputColorSpaceConfig.drawingBufferColorSpace;
-
- },
-
- _getUnpackColorSpace: function ( colorSpace = this.workingColorSpace ) {
-
- return this.spaces[ colorSpace ].workingColorSpaceConfig.unpackColorSpace;
-
- },
-
- // Deprecated
-
- fromWorkingColorSpace: function ( color, targetColorSpace ) {
-
- warnOnce( 'ColorManagement: .fromWorkingColorSpace() has been renamed to .workingToColorSpace().' ); // @deprecated, r177
-
- return ColorManagement.workingToColorSpace( color, targetColorSpace );
-
- },
-
- toWorkingColorSpace: function ( color, sourceColorSpace ) {
-
- warnOnce( 'ColorManagement: .toWorkingColorSpace() has been renamed to .colorSpaceToWorking().' ); // @deprecated, r177
-
- return ColorManagement.colorSpaceToWorking( color, sourceColorSpace );
-
- },
-
- };
-
- /******************************************************************************
- * sRGB definitions
- */
-
- const REC709_PRIMARIES = [ 0.640, 0.330, 0.300, 0.600, 0.150, 0.060 ];
- const REC709_LUMINANCE_COEFFICIENTS = [ 0.2126, 0.7152, 0.0722 ];
- const D65 = [ 0.3127, 0.3290 ];
-
- ColorManagement.define( {
-
- [ LinearSRGBColorSpace ]: {
- primaries: REC709_PRIMARIES,
- whitePoint: D65,
- transfer: LinearTransfer,
- toXYZ: LINEAR_REC709_TO_XYZ,
- fromXYZ: XYZ_TO_LINEAR_REC709,
- luminanceCoefficients: REC709_LUMINANCE_COEFFICIENTS,
- workingColorSpaceConfig: { unpackColorSpace: SRGBColorSpace },
- outputColorSpaceConfig: { drawingBufferColorSpace: SRGBColorSpace }
- },
-
- [ SRGBColorSpace ]: {
- primaries: REC709_PRIMARIES,
- whitePoint: D65,
- transfer: SRGBTransfer,
- toXYZ: LINEAR_REC709_TO_XYZ,
- fromXYZ: XYZ_TO_LINEAR_REC709,
- luminanceCoefficients: REC709_LUMINANCE_COEFFICIENTS,
- outputColorSpaceConfig: { drawingBufferColorSpace: SRGBColorSpace }
- },
-
- } );
-
- return ColorManagement;
-
-}
-
-const ColorManagement = /*@__PURE__*/ createColorManagement();
-
-function SRGBToLinear( c ) {
-
- return ( c < 0.04045 ) ? c * 0.0773993808 : Math.pow( c * 0.9478672986 + 0.0521327014, 2.4 );
-
-}
-
-function LinearToSRGB( c ) {
-
- return ( c < 0.0031308 ) ? c * 12.92 : 1.055 * ( Math.pow( c, 0.41666 ) ) - 0.055;
-
-}
-
-let _canvas;
-
-/**
- * A class containing utility functions for images.
- *
- * @hideconstructor
- */
-class ImageUtils {
-
- /**
- * Returns a data URI containing a representation of the given image.
- *
- * @param {(HTMLImageElement|HTMLCanvasElement)} image - The image object.
- * @param {string} [type='image/png'] - Indicates the image format.
- * @return {string} The data URI.
- */
- static getDataURL( image, type = 'image/png' ) {
-
- if ( /^data:/i.test( image.src ) ) {
-
- return image.src;
-
- }
-
- if ( typeof HTMLCanvasElement === 'undefined' ) {
-
- return image.src;
-
- }
-
- let canvas;
-
- if ( image instanceof HTMLCanvasElement ) {
-
- canvas = image;
-
- } else {
-
- if ( _canvas === undefined ) _canvas = createElementNS( 'canvas' );
-
- _canvas.width = image.width;
- _canvas.height = image.height;
-
- const context = _canvas.getContext( '2d' );
-
- if ( image instanceof ImageData ) {
-
- context.putImageData( image, 0, 0 );
-
- } else {
-
- context.drawImage( image, 0, 0, image.width, image.height );
-
- }
-
- canvas = _canvas;
-
- }
-
- return canvas.toDataURL( type );
-
- }
-
- /**
- * Converts the given sRGB image data to linear color space.
- *
- * @param {(HTMLImageElement|HTMLCanvasElement|ImageBitmap|Object)} image - The image object.
- * @return {HTMLCanvasElement|Object} The converted image.
- */
- static sRGBToLinear( image ) {
-
- if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
- ( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
- ( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {
-
- const canvas = createElementNS( 'canvas' );
-
- canvas.width = image.width;
- canvas.height = image.height;
-
- const context = canvas.getContext( '2d' );
- context.drawImage( image, 0, 0, image.width, image.height );
-
- const imageData = context.getImageData( 0, 0, image.width, image.height );
- const data = imageData.data;
-
- for ( let i = 0; i < data.length; i ++ ) {
-
- data[ i ] = SRGBToLinear( data[ i ] / 255 ) * 255;
-
- }
-
- context.putImageData( imageData, 0, 0 );
-
- return canvas;
-
- } else if ( image.data ) {
-
- const data = image.data.slice( 0 );
-
- for ( let i = 0; i < data.length; i ++ ) {
-
- if ( data instanceof Uint8Array || data instanceof Uint8ClampedArray ) {
-
- data[ i ] = Math.floor( SRGBToLinear( data[ i ] / 255 ) * 255 );
-
- } else {
-
- // assuming float
-
- data[ i ] = SRGBToLinear( data[ i ] );
-
- }
-
- }
-
- return {
- data: data,
- width: image.width,
- height: image.height
- };
-
- } else {
-
- warn( 'ImageUtils.sRGBToLinear(): Unsupported image type. No color space conversion applied.' );
- return image;
-
- }
-
- }
-
-}
-
-let _sourceId = 0;
-
-/**
- * Represents the data source of a texture.
- *
- * The main purpose of this class is to decouple the data definition from the texture
- * definition so the same data can be used with multiple texture instances.
- */
-class TextureSource {
-
- /**
- * Constructs a new texture source.
- *
- * @param {any} [data=null] - The data definition of a texture.
- */
- constructor( data = null ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isTextureSource = true;
-
- /**
- * The ID of the source.
- *
- * @name TextureSource#id
- * @type {number}
- * @readonly
- */
- Object.defineProperty( this, 'id', { value: _sourceId ++ } );
-
- /**
- * The UUID of the source.
- *
- * @type {string}
- * @readonly
- */
- this.uuid = generateUUID();
-
- /**
- * The data definition of a texture.
- *
- * @type {any}
- */
- this.data = data;
-
- /**
- * This property is only relevant when {@link TextureSource#needsUpdate} is set to `true` and
- * provides more control on how texture data should be processed. When `dataReady` is set
- * to `false`, the engine performs the memory allocation (if necessary) but does not transfer
- * the data into the GPU memory.
- *
- * @type {boolean}
- * @default true
- */
- this.dataReady = true;
-
- /**
- * This starts at `0` and counts how many times {@link TextureSource#needsUpdate} is set to `true`.
- *
- * @type {number}
- * @readonly
- * @default 0
- */
- this.version = 0;
-
- }
-
- /**
- * Returns the dimensions of the source into the given target vector.
- *
- * @param {(Vector2|Vector3)} target - The target object the result is written into.
- * @return {(Vector2|Vector3)} The dimensions of the source.
- */
- getSize( target ) {
-
- const data = this.data;
-
- if ( ( typeof HTMLVideoElement !== 'undefined' ) && ( data instanceof HTMLVideoElement ) ) {
-
- target.set( data.videoWidth, data.videoHeight, 0 );
-
- } else if ( ( typeof VideoFrame !== 'undefined' ) && ( data instanceof VideoFrame ) ) {
-
- target.set( data.displayWidth, data.displayHeight, 0 );
-
- } else if ( data !== null ) {
-
- target.set( data.width, data.height, data.depth || 0 );
-
- } else {
-
- target.set( 0, 0, 0 );
-
- }
-
- return target;
-
- }
-
- /**
- * When the property is set to `true`, the engine allocates the memory
- * for the texture (if necessary) and triggers the actual texture upload
- * to the GPU next time the source is used.
- *
- * @type {boolean}
- * @default false
- * @param {boolean} value
- */
- set needsUpdate( value ) {
-
- if ( value === true ) this.version ++;
-
- }
-
- /**
- * Serializes the source into JSON.
- *
- * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
- * @return {Object} A JSON object representing the serialized source.
- * @see {@link ObjectLoader#parse}
- */
- toJSON( meta ) {
-
- const isRootObject = ( meta === undefined || typeof meta === 'string' );
-
- if ( ! isRootObject && meta.images[ this.uuid ] !== undefined ) {
-
- return meta.images[ this.uuid ];
-
- }
-
- const output = {
- uuid: this.uuid,
- url: ''
- };
-
- const data = this.data;
-
- if ( data !== null ) {
-
- let url;
-
- if ( Array.isArray( data ) ) {
-
- // cube texture
-
- url = [];
-
- for ( let i = 0, l = data.length; i < l; i ++ ) {
-
- if ( data[ i ].isDataTexture ) {
-
- url.push( serializeImage( data[ i ].image ) );
-
- } else {
-
- url.push( serializeImage( data[ i ] ) );
-
- }
-
- }
-
- } else {
-
- // texture
-
- url = serializeImage( data );
-
- }
-
- output.url = url;
-
- }
-
- if ( ! isRootObject ) {
-
- meta.images[ this.uuid ] = output;
-
- }
-
- return output;
-
- }
-
-}
-
-function serializeImage( image ) {
-
- if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
- ( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
- ( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {
-
- // default images
-
- return ImageUtils.getDataURL( image );
-
- } else {
-
- if ( image.data ) {
-
- // images of DataTexture
-
- return {
- data: Array.from( image.data ),
- width: image.width,
- height: image.height,
- type: image.data.constructor.name
- };
-
- } else {
-
- warn( 'Texture: Unable to serialize Texture.' );
- return {};
-
- }
-
- }
-
-}
-
-/**
- * @deprecated since r186. Use {@link TextureSource} instead. `Source` has been renamed to `TextureSource`.
- */
-class Source extends TextureSource {
-
- /**
- * Constructs a new texture source.
- *
- * @param {any} [data=null] - The data definition of a texture.
- * @deprecated since r186. Use {@link TextureSource} instead.
- */
- constructor( data = null ) {
-
- warnOnce( 'Source: "Source" has been renamed to "TextureSource". Please update your code to use "THREE.TextureSource" instead.' ); // @deprecated, r186
-
- super( data );
-
- /**
- * This flag can be used for type testing.
- *
- * @deprecated since r186. Use {@link TextureSource#isTextureSource} instead.
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isSource = true;
-
- }
-
-}
-
-let _textureId = 0;
-
-const _tempVec3 = /*@__PURE__*/ new Vector3();
-
-/**
- * Base class for all textures.
- *
- * Note: After the initial use of a texture, its dimensions, format, and type
- * cannot be changed. Instead, call {@link Texture#dispose} on the texture and instantiate a new one.
- *
- * @augments EventDispatcher
- */
-class Texture extends EventDispatcher {
-
- /**
- * Constructs a new texture.
- *
- * @param {?Object} [image=Texture.DEFAULT_IMAGE] - The image holding the texture data.
- * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
- * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
- * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
- * @param {number} [magFilter=LinearFilter] - The mag filter value.
- * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.
- * @param {number} [format=RGBAFormat] - The texture format.
- * @param {number} [type=UnsignedByteType] - The texture type.
- * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
- * @param {string} [colorSpace=NoColorSpace] - The color space.
- */
- constructor( image = Texture.DEFAULT_IMAGE, mapping = Texture.DEFAULT_MAPPING, wrapS = ClampToEdgeWrapping, wrapT = ClampToEdgeWrapping, magFilter = LinearFilter, minFilter = LinearMipmapLinearFilter, format = RGBAFormat, type = UnsignedByteType, anisotropy = Texture.DEFAULT_ANISOTROPY, colorSpace = NoColorSpace ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isTexture = true;
-
- /**
- * The ID of the texture.
- *
- * @name Texture#id
- * @type {number}
- * @readonly
- */
- Object.defineProperty( this, 'id', { value: _textureId ++ } );
-
- /**
- * The UUID of the texture.
- *
- * @type {string}
- * @readonly
- */
- this.uuid = generateUUID();
-
- /**
- * The name of the texture.
- *
- * @type {string}
- */
- this.name = '';
-
- /**
- * The data definition of a texture. A reference to the data source can be
- * shared across textures. This is often useful in context of spritesheets
- * where multiple textures render the same data but with different texture
- * transformations.
- *
- * @type {TextureSource}
- */
- this.source = new TextureSource( image );
-
- /**
- * An array holding user-defined mipmaps.
- *
- * @type {Array}
- */
- this.mipmaps = [];
-
- /**
- * How the texture is applied to the object. The value `UVMapping`
- * is the default, where texture or uv coordinates are used to apply the map.
- *
- * @type {(UVMapping|CubeReflectionMapping|CubeRefractionMapping|EquirectangularReflectionMapping|EquirectangularRefractionMapping|CubeUVReflectionMapping)}
- * @default UVMapping
- */
- this.mapping = mapping;
-
- /**
- * Lets you select the uv attribute to map the texture to. `0` for `uv`,
- * `1` for `uv1`, `2` for `uv2` and `3` for `uv3`.
- *
- * @type {number}
- * @default 0
- */
- this.channel = 0;
-
- /**
- * This defines how the texture is wrapped horizontally and corresponds to
- * *U* in UV mapping.
- *
- * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}
- * @default ClampToEdgeWrapping
- */
- this.wrapS = wrapS;
-
- /**
- * This defines how the texture is wrapped horizontally and corresponds to
- * *V* in UV mapping.
- *
- * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}
- * @default ClampToEdgeWrapping
- */
- this.wrapT = wrapT;
-
- /**
- * How the texture is sampled when a texel covers more than one pixel.
- *
- * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
- * @default LinearFilter
- */
- this.magFilter = magFilter;
-
- /**
- * How the texture is sampled when a texel covers less than one pixel.
- *
- * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
- * @default LinearMipmapLinearFilter
- */
- this.minFilter = minFilter;
-
- /**
- * The number of samples taken along the axis through the pixel that has the
- * highest density of texels. By default, this value is `1`. A higher value
- * gives a less blurry result than a basic mipmap, at the cost of more
- * texture samples being used.
- *
- * @type {number}
- * @default Texture.DEFAULT_ANISOTROPY
- */
- this.anisotropy = anisotropy;
-
- /**
- * The format of the texture.
- *
- * @type {number}
- * @default RGBAFormat
- */
- this.format = format;
-
- /**
- * The default internal format is derived from {@link Texture#format} and {@link Texture#type} and
- * defines how the texture data is going to be stored on the GPU.
- *
- * This property allows to overwrite the default format.
- *
- * @type {?string}
- * @default null
- */
- this.internalFormat = null;
-
- /**
- * The data type of the texture.
- *
- * @type {number}
- * @default UnsignedByteType
- */
- this.type = type;
-
- /**
- * How much a single repetition of the texture is offset from the beginning,
- * in each direction U and V. Typical range is `0.0` to `1.0`.
- *
- * @type {Vector2}
- * @default (0,0)
- */
- this.offset = new Vector2( 0, 0 );
-
- /**
- * How many times the texture is repeated across the surface, in each
- * direction U and V. If repeat is set greater than `1` in either direction,
- * the corresponding wrap parameter should also be set to `RepeatWrapping`
- * or `MirroredRepeatWrapping` to achieve the desired tiling effect.
- *
- * @type {Vector2}
- * @default (1,1)
- */
- this.repeat = new Vector2( 1, 1 );
-
- /**
- * The point around which rotation occurs. A value of `(0.5, 0.5)` corresponds
- * to the center of the texture. Default is `(0, 0)`, the lower left.
- *
- * @type {Vector2}
- * @default (0,0)
- */
- this.center = new Vector2( 0, 0 );
-
- /**
- * How much the texture is rotated around the center point, in radians.
- * Positive values are counter-clockwise.
- *
- * @type {number}
- * @default 0
- */
- this.rotation = 0;
-
- /**
- * Whether to update the texture's uv-transformation {@link Texture#matrix}
- * from the properties {@link Texture#offset}, {@link Texture#repeat},
- * {@link Texture#rotation}, and {@link Texture#center}.
- *
- * Set this to `false` if you are specifying the uv-transform matrix directly.
- *
- * @type {boolean}
- * @default true
- */
- this.matrixAutoUpdate = true;
-
- /**
- * The uv-transformation matrix of the texture.
- *
- * @type {Matrix3}
- */
- this.matrix = new Matrix3();
-
- /**
- * Whether to generate mipmaps (if possible) for a texture.
- *
- * Set this to `false` if you are creating mipmaps manually.
- *
- * @type {boolean}
- * @default true
- */
- this.generateMipmaps = true;
-
- /**
- * If set to `true`, the alpha channel, if present, is multiplied into the
- * color channels when the texture is uploaded to the GPU.
- *
- * Note that this property has no effect when using `ImageBitmap`. You need to
- * configure premultiply alpha on bitmap creation instead.
- *
- * @type {boolean}
- * @default false
- */
- this.premultiplyAlpha = false;
-
- /**
- * If set to `true`, the texture is flipped along the vertical axis when
- * uploaded to the GPU.
- *
- * Note that this property has no effect when using `ImageBitmap`. You need to
- * configure the flip on bitmap creation instead.
- *
- * @type {boolean}
- * @default true
- */
- this.flipY = true;
-
- /**
- * Specifies the alignment requirements for the start of each pixel row in memory.
- * The allowable values are `1` (byte-alignment), `2` (rows aligned to even-numbered bytes),
- * `4` (word-alignment), and `8` (rows start on double-word boundaries).
- *
- * @type {number}
- * @default 4
- */
- this.unpackAlignment = 4; // valid values: 1, 2, 4, 8 (see http://www.khronos.org/opengles/sdk/docs/man/xhtml/glPixelStorei.xml)
-
- /**
- * Textures containing color data should be annotated with `SRGBColorSpace` or `LinearSRGBColorSpace`.
- *
- * @type {string}
- * @default NoColorSpace
- */
- this.colorSpace = colorSpace;
-
- /**
- * An object that can be used to store custom data about the texture. It
- * should not hold references to functions as these will not be cloned.
- *
- * @type {Object}
- */
- this.userData = {};
-
- /**
- * This can be used to only update a subregion or specific rows of the texture (for example, just the
- * first 3 rows). Use the `addUpdateRange()` function to add ranges to this array.
- *
- * @type {Array}
- */
- this.updateRanges = [];
-
- /**
- * This starts at `0` and counts how many times {@link Texture#needsUpdate} is set to `true`.
- *
- * @type {number}
- * @readonly
- * @default 0
- */
- this.version = 0;
-
- /**
- * A callback function, called when the texture is updated (e.g., when
- * {@link Texture#needsUpdate} has been set to true and then the texture is used).
- *
- * @type {?Function}
- * @default null
- */
- this.onUpdate = null;
-
- /**
- * An optional back reference to the textures render target.
- *
- * @type {?(RenderTarget|WebGLRenderTarget)}
- * @default null
- */
- this.renderTarget = null;
-
- /**
- * Indicates whether a texture belongs to a render target or not.
- *
- * @type {boolean}
- * @readonly
- * @default false
- */
- this.isRenderTargetTexture = false;
-
- /**
- * Indicates if a texture should be handled like a texture array.
- *
- * @type {boolean}
- * @readonly
- * @default false
- */
- this.isArrayTexture = image && image.depth && image.depth > 1 ? true : false;
-
- /**
- * Indicates whether this texture should be processed by `PMREMGenerator` or not
- * (only relevant for render target textures).
- *
- * @type {number}
- * @readonly
- * @default 0
- */
- this.pmremVersion = 0;
-
- /**
- * Whether the texture should use one of the 16 bit integer formats which are normalized
- * to [0, 1] or [-1, 1] (depending on signed/unsigned) when sampled.
- *
- * @type {boolean}
- * @default false
- */
- this.normalized = false;
-
- }
-
- /**
- * The width of the texture in pixels.
- */
- get width() {
-
- return this.source.getSize( _tempVec3 ).x;
-
- }
-
- /**
- * The height of the texture in pixels.
- */
- get height() {
-
- return this.source.getSize( _tempVec3 ).y;
-
- }
-
- /**
- * The depth of the texture in pixels.
- */
- get depth() {
-
- return this.source.getSize( _tempVec3 ).z;
-
- }
-
- /**
- * The image object holding the texture data.
- *
- * @type {?Object}
- */
- get image() {
-
- return this.source.data;
-
- }
-
- set image( value ) {
-
- this.source.data = value;
-
- }
-
- /**
- * Updates the texture transformation matrix from the properties {@link Texture#offset},
- * {@link Texture#repeat}, {@link Texture#rotation}, and {@link Texture#center}.
- */
- updateMatrix() {
-
- this.matrix.setUvTransform( this.offset.x, this.offset.y, this.repeat.x, this.repeat.y, this.rotation, this.center.x, this.center.y );
-
- }
-
- /**
- * Adds a range of data in the data texture to be updated on the GPU.
- *
- * @param {number} start - Position at which to start update.
- * @param {number} count - The number of components to update.
- */
- addUpdateRange( start, count ) {
-
- this.updateRanges.push( { start, count } );
-
- }
-
- /**
- * Clears the update ranges.
- */
- clearUpdateRanges() {
-
- this.updateRanges.length = 0;
-
- }
-
- /**
- * Returns a new texture with copied values from this instance.
- *
- * @return {Texture} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Copies the values of the given texture to this instance.
- *
- * @param {Texture} source - The texture to copy.
- * @return {Texture} A reference to this instance.
- */
- copy( source ) {
-
- this.name = source.name;
-
- this.source = source.source;
- this.mipmaps = source.mipmaps.slice( 0 );
-
- this.mapping = source.mapping;
- this.channel = source.channel;
-
- this.wrapS = source.wrapS;
- this.wrapT = source.wrapT;
-
- this.magFilter = source.magFilter;
- this.minFilter = source.minFilter;
-
- this.anisotropy = source.anisotropy;
-
- this.format = source.format;
- this.internalFormat = source.internalFormat;
- this.type = source.type;
- this.normalized = source.normalized;
-
- this.offset.copy( source.offset );
- this.repeat.copy( source.repeat );
- this.center.copy( source.center );
- this.rotation = source.rotation;
-
- this.matrixAutoUpdate = source.matrixAutoUpdate;
- this.matrix.copy( source.matrix );
-
- this.generateMipmaps = source.generateMipmaps;
- this.premultiplyAlpha = source.premultiplyAlpha;
- this.flipY = source.flipY;
- this.unpackAlignment = source.unpackAlignment;
- this.colorSpace = source.colorSpace;
-
- this.renderTarget = source.renderTarget;
- this.isRenderTargetTexture = source.isRenderTargetTexture;
- this.isArrayTexture = source.isArrayTexture;
-
- this.userData = JSON.parse( JSON.stringify( source.userData ) );
-
- this.needsUpdate = true;
-
- return this;
-
- }
-
- /**
- * Sets this texture's properties based on `values`.
- * @param {Object} values - A container with texture parameters.
- */
- setValues( values ) {
-
- for ( const key in values ) {
-
- const newValue = values[ key ];
-
- if ( newValue === undefined ) {
-
- warn( `Texture.setValues(): parameter '${ key }' has value of undefined.` );
- continue;
-
- }
-
- const currentValue = this[ key ];
-
- if ( currentValue === undefined ) {
-
- warn( `Texture.setValues(): property '${ key }' does not exist.` );
- continue;
-
- }
-
- if ( ( currentValue && newValue ) && ( currentValue.isVector2 && newValue.isVector2 ) ) {
-
- currentValue.copy( newValue );
-
- } else if ( ( currentValue && newValue ) && ( currentValue.isVector3 && newValue.isVector3 ) ) {
-
- currentValue.copy( newValue );
-
- } else if ( ( currentValue && newValue ) && ( currentValue.isMatrix3 && newValue.isMatrix3 ) ) {
-
- currentValue.copy( newValue );
-
- } else {
-
- this[ key ] = newValue;
-
- }
-
- }
-
- }
-
- /**
- * Serializes the texture into JSON.
- *
- * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
- * @return {Object} A JSON object representing the serialized texture.
- * @see {@link ObjectLoader#parse}
- */
- toJSON( meta ) {
-
- const isRootObject = ( meta === undefined || typeof meta === 'string' );
-
- if ( ! isRootObject && meta.textures[ this.uuid ] !== undefined ) {
-
- return meta.textures[ this.uuid ];
-
- }
-
- const output = {
-
- metadata: {
- version: 4.7,
- type: 'Texture',
- generator: 'Texture.toJSON'
- },
-
- uuid: this.uuid,
- name: this.name,
-
- image: this.source.toJSON( meta ).uuid,
-
- mapping: this.mapping,
- channel: this.channel,
-
- repeat: [ this.repeat.x, this.repeat.y ],
- offset: [ this.offset.x, this.offset.y ],
- center: [ this.center.x, this.center.y ],
- rotation: this.rotation,
-
- wrap: [ this.wrapS, this.wrapT ],
-
- format: this.format,
- internalFormat: this.internalFormat,
- type: this.type,
- normalized: this.normalized,
- colorSpace: this.colorSpace,
-
- minFilter: this.minFilter,
- magFilter: this.magFilter,
- anisotropy: this.anisotropy,
-
- flipY: this.flipY,
-
- generateMipmaps: this.generateMipmaps,
- premultiplyAlpha: this.premultiplyAlpha,
- unpackAlignment: this.unpackAlignment
-
- };
-
- if ( Object.keys( this.userData ).length > 0 ) output.userData = this.userData;
-
- if ( ! isRootObject ) {
-
- meta.textures[ this.uuid ] = output;
-
- }
-
- return output;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- *
- * @fires Texture#dispose
- */
- dispose() {
-
- /**
- * Fires when the texture has been disposed of.
- *
- * @event Texture#dispose
- * @type {Object}
- */
- this.dispatchEvent( { type: 'dispose' } );
-
- }
-
- /**
- * Transforms the given uv vector with the textures uv transformation matrix.
- *
- * @param {Vector2} uv - The uv vector.
- * @return {Vector2} The transformed uv vector.
- */
- transformUv( uv ) {
-
- if ( this.mapping !== UVMapping ) return uv;
-
- uv.applyMatrix3( this.matrix );
-
- if ( uv.x < 0 || uv.x > 1 ) {
-
- switch ( this.wrapS ) {
-
- case RepeatWrapping:
-
- uv.x = uv.x - Math.floor( uv.x );
- break;
-
- case ClampToEdgeWrapping:
-
- uv.x = uv.x < 0 ? 0 : 1;
- break;
-
- case MirroredRepeatWrapping:
-
- if ( Math.abs( Math.floor( uv.x ) % 2 ) === 1 ) {
-
- uv.x = Math.ceil( uv.x ) - uv.x;
-
- } else {
-
- uv.x = uv.x - Math.floor( uv.x );
-
- }
-
- break;
-
- }
-
- }
-
- if ( uv.y < 0 || uv.y > 1 ) {
-
- switch ( this.wrapT ) {
-
- case RepeatWrapping:
-
- uv.y = uv.y - Math.floor( uv.y );
- break;
-
- case ClampToEdgeWrapping:
-
- uv.y = uv.y < 0 ? 0 : 1;
- break;
-
- case MirroredRepeatWrapping:
-
- if ( Math.abs( Math.floor( uv.y ) % 2 ) === 1 ) {
-
- uv.y = Math.ceil( uv.y ) - uv.y;
-
- } else {
-
- uv.y = uv.y - Math.floor( uv.y );
-
- }
-
- break;
-
- }
-
- }
-
- if ( this.flipY ) {
-
- uv.y = 1 - uv.y;
-
- }
-
- return uv;
-
- }
-
- /**
- * Setting this property to `true` indicates the engine the texture
- * must be updated in the next render. This triggers a texture upload
- * to the GPU and ensures correct texture parameter configuration.
- *
- * @type {boolean}
- * @default false
- * @param {boolean} value
- */
- set needsUpdate( value ) {
-
- if ( value === true ) {
-
- this.version ++;
- this.source.needsUpdate = true;
-
- }
-
- }
-
- /**
- * Setting this property to `true` indicates the engine the PMREM
- * must be regenerated.
- *
- * @type {boolean}
- * @default false
- * @param {boolean} value
- */
- set needsPMREMUpdate( value ) {
-
- if ( value === true ) {
-
- this.pmremVersion ++;
-
- }
-
- }
-
-}
-
-/**
- * The default image for all textures.
- *
- * @static
- * @type {?Image}
- * @default null
- */
-Texture.DEFAULT_IMAGE = null;
-
-/**
- * The default mapping for all textures.
- *
- * @static
- * @type {number}
- * @default UVMapping
- */
-Texture.DEFAULT_MAPPING = UVMapping;
-
-/**
- * The default anisotropy value for all textures.
- *
- * @static
- * @type {number}
- * @default 1
- */
-Texture.DEFAULT_ANISOTROPY = 1;
-
-/**
- * Class representing a 4D vector. A 4D vector is an ordered quadruplet of numbers
- * (labeled x, y, z and w), which can be used to represent a number of things, such as:
- *
- * - A point in 4D space.
- * - A direction and length in 4D space. In three.js the length will
- * always be the Euclidean distance(straight-line distance) from `(0, 0, 0, 0)` to `(x, y, z, w)`
- * and the direction is also measured from `(0, 0, 0, 0)` towards `(x, y, z, w)`.
- * - Any arbitrary ordered quadruplet of numbers.
- *
- * There are other things a 4D vector can be used to represent, however these
- * are the most common uses in *three.js*.
- *
- * Iterating through a vector instance will yield its components `(x, y, z, w)` in
- * the corresponding order.
- * ```js
- * const a = new THREE.Vector4( 0, 1, 0, 0 );
- *
- * //no arguments; will be initialised to (0, 0, 0, 1)
- * const b = new THREE.Vector4( );
- *
- * const d = a.dot( b );
- * ```
- */
-class Vector4 {
-
- static {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- Vector4.prototype.isVector4 = true;
-
- }
-
- /**
- * Constructs a new 4D vector.
- *
- * @param {number} [x=0] - The x value of this vector.
- * @param {number} [y=0] - The y value of this vector.
- * @param {number} [z=0] - The z value of this vector.
- * @param {number} [w=1] - The w value of this vector.
- */
- constructor( x = 0, y = 0, z = 0, w = 1 ) {
-
- /**
- * The x value of this vector.
- *
- * @type {number}
- */
- this.x = x;
-
- /**
- * The y value of this vector.
- *
- * @type {number}
- */
- this.y = y;
-
- /**
- * The z value of this vector.
- *
- * @type {number}
- */
- this.z = z;
-
- /**
- * The w value of this vector.
- *
- * @type {number}
- */
- this.w = w;
-
- }
-
- /**
- * Alias for {@link Vector4#z}.
- *
- * @type {number}
- */
- get width() {
-
- return this.z;
-
- }
-
- set width( value ) {
-
- this.z = value;
-
- }
-
- /**
- * Alias for {@link Vector4#w}.
- *
- * @type {number}
- */
- get height() {
-
- return this.w;
-
- }
-
- set height( value ) {
-
- this.w = value;
-
- }
-
- /**
- * Sets the vector components.
- *
- * @param {number} x - The value of the x component.
- * @param {number} y - The value of the y component.
- * @param {number} z - The value of the z component.
- * @param {number} w - The value of the w component.
- * @return {Vector4} A reference to this vector.
- */
- set( x, y, z, w ) {
-
- this.x = x;
- this.y = y;
- this.z = z;
- this.w = w;
-
- return this;
-
- }
-
- /**
- * Sets the vector components to the same value.
- *
- * @param {number} scalar - The value to set for all vector components.
- * @return {Vector4} A reference to this vector.
- */
- setScalar( scalar ) {
-
- this.x = scalar;
- this.y = scalar;
- this.z = scalar;
- this.w = scalar;
-
- return this;
-
- }
-
- /**
- * Sets the vector's x component to the given value
- *
- * @param {number} x - The value to set.
- * @return {Vector4} A reference to this vector.
- */
- setX( x ) {
-
- this.x = x;
-
- return this;
-
- }
-
- /**
- * Sets the vector's y component to the given value
- *
- * @param {number} y - The value to set.
- * @return {Vector4} A reference to this vector.
- */
- setY( y ) {
-
- this.y = y;
-
- return this;
-
- }
-
- /**
- * Sets the vector's z component to the given value
- *
- * @param {number} z - The value to set.
- * @return {Vector4} A reference to this vector.
- */
- setZ( z ) {
-
- this.z = z;
-
- return this;
-
- }
-
- /**
- * Sets the vector's w component to the given value
- *
- * @param {number} w - The value to set.
- * @return {Vector4} A reference to this vector.
- */
- setW( w ) {
-
- this.w = w;
-
- return this;
-
- }
-
- /**
- * Allows to set a vector component with an index.
- *
- * @param {number} index - The component index. `0` equals to x, `1` equals to y,
- * `2` equals to z, `3` equals to w.
- * @param {number} value - The value to set.
- * @return {Vector4} A reference to this vector.
- */
- setComponent( index, value ) {
-
- switch ( index ) {
-
- case 0: this.x = value; break;
- case 1: this.y = value; break;
- case 2: this.z = value; break;
- case 3: this.w = value; break;
- default: throw new Error( 'THREE.Vector4: index is out of range: ' + index );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns the value of the vector component which matches the given index.
- *
- * @param {number} index - The component index. `0` equals to x, `1` equals to y,
- * `2` equals to z, `3` equals to w.
- * @return {number} A vector component value.
- */
- getComponent( index ) {
-
- switch ( index ) {
-
- case 0: return this.x;
- case 1: return this.y;
- case 2: return this.z;
- case 3: return this.w;
- default: throw new Error( 'THREE.Vector4: index is out of range: ' + index );
-
- }
-
- }
-
- /**
- * Returns a new vector with copied values from this instance.
- *
- * @return {Vector4} A clone of this instance.
- */
- clone() {
-
- return new this.constructor( this.x, this.y, this.z, this.w );
-
- }
-
- /**
- * Copies the values of the given vector to this instance.
- *
- * @param {Vector3|Vector4} v - The vector to copy.
- * @return {Vector4} A reference to this vector.
- */
- copy( v ) {
-
- this.x = v.x;
- this.y = v.y;
- this.z = v.z;
- this.w = ( v.w !== undefined ) ? v.w : 1;
-
- return this;
-
- }
-
- /**
- * Adds the given vector to this instance.
- *
- * @param {Vector4} v - The vector to add.
- * @return {Vector4} A reference to this vector.
- */
- add( v ) {
-
- this.x += v.x;
- this.y += v.y;
- this.z += v.z;
- this.w += v.w;
-
- return this;
-
- }
-
- /**
- * Adds the given scalar value to all components of this instance.
- *
- * @param {number} s - The scalar to add.
- * @return {Vector4} A reference to this vector.
- */
- addScalar( s ) {
-
- this.x += s;
- this.y += s;
- this.z += s;
- this.w += s;
-
- return this;
-
- }
-
- /**
- * Adds the given vectors and stores the result in this instance.
- *
- * @param {Vector4} a - The first vector.
- * @param {Vector4} b - The second vector.
- * @return {Vector4} A reference to this vector.
- */
- addVectors( a, b ) {
-
- this.x = a.x + b.x;
- this.y = a.y + b.y;
- this.z = a.z + b.z;
- this.w = a.w + b.w;
-
- return this;
-
- }
-
- /**
- * Adds the given vector scaled by the given factor to this instance.
- *
- * @param {Vector4} v - The vector.
- * @param {number} s - The factor that scales `v`.
- * @return {Vector4} A reference to this vector.
- */
- addScaledVector( v, s ) {
-
- this.x += v.x * s;
- this.y += v.y * s;
- this.z += v.z * s;
- this.w += v.w * s;
-
- return this;
-
- }
-
- /**
- * Subtracts the given vector from this instance.
- *
- * @param {Vector4} v - The vector to subtract.
- * @return {Vector4} A reference to this vector.
- */
- sub( v ) {
-
- this.x -= v.x;
- this.y -= v.y;
- this.z -= v.z;
- this.w -= v.w;
-
- return this;
-
- }
-
- /**
- * Subtracts the given scalar value from all components of this instance.
- *
- * @param {number} s - The scalar to subtract.
- * @return {Vector4} A reference to this vector.
- */
- subScalar( s ) {
-
- this.x -= s;
- this.y -= s;
- this.z -= s;
- this.w -= s;
-
- return this;
-
- }
-
- /**
- * Subtracts the given vectors and stores the result in this instance.
- *
- * @param {Vector4} a - The first vector.
- * @param {Vector4} b - The second vector.
- * @return {Vector4} A reference to this vector.
- */
- subVectors( a, b ) {
-
- this.x = a.x - b.x;
- this.y = a.y - b.y;
- this.z = a.z - b.z;
- this.w = a.w - b.w;
-
- return this;
-
- }
-
- /**
- * Multiplies the given vector with this instance.
- *
- * @param {Vector4} v - The vector to multiply.
- * @return {Vector4} A reference to this vector.
- */
- multiply( v ) {
-
- this.x *= v.x;
- this.y *= v.y;
- this.z *= v.z;
- this.w *= v.w;
-
- return this;
-
- }
-
- /**
- * Multiplies the given scalar value with all components of this instance.
- *
- * @param {number} scalar - The scalar to multiply.
- * @return {Vector4} A reference to this vector.
- */
- multiplyScalar( scalar ) {
-
- this.x *= scalar;
- this.y *= scalar;
- this.z *= scalar;
- this.w *= scalar;
-
- return this;
-
- }
-
- /**
- * Multiplies this vector with the given 4x4 matrix.
- *
- * @param {Matrix4} m - The 4x4 matrix.
- * @return {Vector4} A reference to this vector.
- */
- applyMatrix4( m ) {
-
- const x = this.x, y = this.y, z = this.z, w = this.w;
- const e = m.elements;
-
- this.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] * w;
- this.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] * w;
- this.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] * w;
- this.w = e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] * w;
-
- return this;
-
- }
-
- /**
- * Divides this instance by the given vector.
- *
- * @param {Vector4} v - The vector to divide.
- * @return {Vector4} A reference to this vector.
- */
- divide( v ) {
-
- this.x /= v.x;
- this.y /= v.y;
- this.z /= v.z;
- this.w /= v.w;
-
- return this;
-
- }
-
- /**
- * Divides this vector by the given scalar.
- *
- * @param {number} scalar - The scalar to divide.
- * @return {Vector4} A reference to this vector.
- */
- divideScalar( scalar ) {
-
- return this.multiplyScalar( 1 / scalar );
-
- }
-
- /**
- * Sets the x, y and z components of this
- * vector to the quaternion's axis and w to the angle.
- *
- * @param {Quaternion} q - The Quaternion to set.
- * @return {Vector4} A reference to this vector.
- */
- setAxisAngleFromQuaternion( q ) {
-
- // q is assumed to be normalized
-
- this.w = 2 * Math.acos( q.w );
-
- const s = Math.sqrt( 1 - q.w * q.w );
-
- if ( s < 0.0001 ) {
-
- this.x = 1;
- this.y = 0;
- this.z = 0;
-
- } else {
-
- this.x = q.x / s;
- this.y = q.y / s;
- this.z = q.z / s;
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the x, y and z components of this
- * vector to the axis of rotation and w to the angle.
- *
- * @param {Matrix4} m - A 4x4 matrix of which the upper left 3x3 matrix is a pure rotation matrix.
- * @return {Vector4} A reference to this vector.
- */
- setAxisAngleFromRotationMatrix( m ) {
-
- // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
-
- let angle, x, y, z; // variables for result
- const epsilon = 0.01, // margin to allow for rounding errors
- epsilon2 = 0.1, // margin to distinguish between 0 and 180 degrees
-
- te = m.elements,
-
- m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ],
- m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ],
- m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ];
-
- if ( ( Math.abs( m12 - m21 ) < epsilon ) &&
- ( Math.abs( m13 - m31 ) < epsilon ) &&
- ( Math.abs( m23 - m32 ) < epsilon ) ) {
-
- // singularity found
- // first check for identity matrix which must have +1 for all terms
- // in leading diagonal and zero in other terms
-
- if ( ( Math.abs( m12 + m21 ) < epsilon2 ) &&
- ( Math.abs( m13 + m31 ) < epsilon2 ) &&
- ( Math.abs( m23 + m32 ) < epsilon2 ) &&
- ( Math.abs( m11 + m22 + m33 - 3 ) < epsilon2 ) ) {
-
- // this singularity is identity matrix so angle = 0
-
- this.set( 1, 0, 0, 0 );
-
- return this; // zero angle, arbitrary axis
-
- }
-
- // otherwise this singularity is angle = 180
-
- angle = Math.PI;
-
- const xx = ( m11 + 1 ) / 2;
- const yy = ( m22 + 1 ) / 2;
- const zz = ( m33 + 1 ) / 2;
- const xy = ( m12 + m21 ) / 4;
- const xz = ( m13 + m31 ) / 4;
- const yz = ( m23 + m32 ) / 4;
-
- if ( ( xx > yy ) && ( xx > zz ) ) {
-
- // m11 is the largest diagonal term
-
- if ( xx < epsilon ) {
-
- x = 0;
- y = 0.707106781;
- z = 0.707106781;
-
- } else {
-
- x = Math.sqrt( xx );
- y = xy / x;
- z = xz / x;
-
- }
-
- } else if ( yy > zz ) {
-
- // m22 is the largest diagonal term
-
- if ( yy < epsilon ) {
-
- x = 0.707106781;
- y = 0;
- z = 0.707106781;
-
- } else {
-
- y = Math.sqrt( yy );
- x = xy / y;
- z = yz / y;
-
- }
-
- } else {
-
- // m33 is the largest diagonal term so base result on this
-
- if ( zz < epsilon ) {
-
- x = 0.707106781;
- y = 0.707106781;
- z = 0;
-
- } else {
-
- z = Math.sqrt( zz );
- x = xz / z;
- y = yz / z;
-
- }
-
- }
-
- this.set( x, y, z, angle );
-
- return this; // return 180 deg rotation
-
- }
-
- // as we have reached here there are no singularities so we can handle normally
-
- let s = Math.sqrt( ( m32 - m23 ) * ( m32 - m23 ) +
- ( m13 - m31 ) * ( m13 - m31 ) +
- ( m21 - m12 ) * ( m21 - m12 ) ); // used to normalize
-
- if ( Math.abs( s ) < 0.001 ) s = 1;
-
- // prevent divide by zero, should not happen if matrix is orthogonal and should be
- // caught by singularity test above, but I've left it in just in case
-
- this.x = ( m32 - m23 ) / s;
- this.y = ( m13 - m31 ) / s;
- this.z = ( m21 - m12 ) / s;
- this.w = Math.acos( ( m11 + m22 + m33 - 1 ) / 2 );
-
- return this;
-
- }
-
- /**
- * Sets the vector components to the position elements of the
- * given transformation matrix.
- *
- * @param {Matrix4} m - The 4x4 matrix.
- * @return {Vector4} A reference to this vector.
- */
- setFromMatrixPosition( m ) {
-
- const e = m.elements;
-
- this.x = e[ 12 ];
- this.y = e[ 13 ];
- this.z = e[ 14 ];
- this.w = e[ 15 ];
-
- return this;
-
- }
-
- /**
- * If this vector's x, y, z or w value is greater than the given vector's x, y, z or w
- * value, replace that value with the corresponding min value.
- *
- * @param {Vector4} v - The vector.
- * @return {Vector4} A reference to this vector.
- */
- min( v ) {
-
- this.x = Math.min( this.x, v.x );
- this.y = Math.min( this.y, v.y );
- this.z = Math.min( this.z, v.z );
- this.w = Math.min( this.w, v.w );
-
- return this;
-
- }
-
- /**
- * If this vector's x, y, z or w value is less than the given vector's x, y, z or w
- * value, replace that value with the corresponding max value.
- *
- * @param {Vector4} v - The vector.
- * @return {Vector4} A reference to this vector.
- */
- max( v ) {
-
- this.x = Math.max( this.x, v.x );
- this.y = Math.max( this.y, v.y );
- this.z = Math.max( this.z, v.z );
- this.w = Math.max( this.w, v.w );
-
- return this;
-
- }
-
- /**
- * If this vector's x, y, z or w value is greater than the max vector's x, y, z or w
- * value, it is replaced by the corresponding value.
- * If this vector's x, y, z or w value is less than the min vector's x, y, z or w value,
- * it is replaced by the corresponding value.
- *
- * @param {Vector4} min - The minimum x, y and z values.
- * @param {Vector4} max - The maximum x, y and z values in the desired range.
- * @return {Vector4} A reference to this vector.
- */
- clamp( min, max ) {
-
- // assumes min < max, componentwise
-
- this.x = clamp( this.x, min.x, max.x );
- this.y = clamp( this.y, min.y, max.y );
- this.z = clamp( this.z, min.z, max.z );
- this.w = clamp( this.w, min.w, max.w );
-
- return this;
-
- }
-
- /**
- * If this vector's x, y, z or w values are greater than the max value, they are
- * replaced by the max value.
- * If this vector's x, y, z or w values are less than the min value, they are
- * replaced by the min value.
- *
- * @param {number} minVal - The minimum value the components will be clamped to.
- * @param {number} maxVal - The maximum value the components will be clamped to.
- * @return {Vector4} A reference to this vector.
- */
- clampScalar( minVal, maxVal ) {
-
- this.x = clamp( this.x, minVal, maxVal );
- this.y = clamp( this.y, minVal, maxVal );
- this.z = clamp( this.z, minVal, maxVal );
- this.w = clamp( this.w, minVal, maxVal );
-
- return this;
-
- }
-
- /**
- * If this vector's length is greater than the max value, it is replaced by
- * the max value.
- * If this vector's length is less than the min value, it is replaced by the
- * min value.
- *
- * @param {number} min - The minimum value the vector length will be clamped to.
- * @param {number} max - The maximum value the vector length will be clamped to.
- * @return {Vector4} A reference to this vector.
- */
- clampLength( min, max ) {
-
- const length = this.length();
-
- return this.divideScalar( length || 1 ).multiplyScalar( clamp( length, min, max ) );
-
- }
-
- /**
- * The components of this vector are rounded down to the nearest integer value.
- *
- * @return {Vector4} A reference to this vector.
- */
- floor() {
-
- this.x = Math.floor( this.x );
- this.y = Math.floor( this.y );
- this.z = Math.floor( this.z );
- this.w = Math.floor( this.w );
-
- return this;
-
- }
-
- /**
- * The components of this vector are rounded up to the nearest integer value.
- *
- * @return {Vector4} A reference to this vector.
- */
- ceil() {
-
- this.x = Math.ceil( this.x );
- this.y = Math.ceil( this.y );
- this.z = Math.ceil( this.z );
- this.w = Math.ceil( this.w );
-
- return this;
-
- }
-
- /**
- * The components of this vector are rounded to the nearest integer value
- *
- * @return {Vector4} A reference to this vector.
- */
- round() {
-
- this.x = Math.round( this.x );
- this.y = Math.round( this.y );
- this.z = Math.round( this.z );
- this.w = Math.round( this.w );
-
- return this;
-
- }
-
- /**
- * The components of this vector are rounded towards zero (up if negative,
- * down if positive) to an integer value.
- *
- * @return {Vector4} A reference to this vector.
- */
- roundToZero() {
-
- this.x = Math.trunc( this.x );
- this.y = Math.trunc( this.y );
- this.z = Math.trunc( this.z );
- this.w = Math.trunc( this.w );
-
- return this;
-
- }
-
- /**
- * Inverts this vector - i.e. sets x = -x, y = -y, z = -z, w = -w.
- *
- * @return {Vector4} A reference to this vector.
- */
- negate() {
-
- this.x = - this.x;
- this.y = - this.y;
- this.z = - this.z;
- this.w = - this.w;
-
- return this;
-
- }
-
- /**
- * Calculates the dot product of the given vector with this instance.
- *
- * @param {Vector4} v - The vector to compute the dot product with.
- * @return {number} The result of the dot product.
- */
- dot( v ) {
-
- return this.x * v.x + this.y * v.y + this.z * v.z + this.w * v.w;
-
- }
-
- /**
- * Computes the square of the Euclidean length (straight-line length) from
- * (0, 0, 0, 0) to (x, y, z, w). If you are comparing the lengths of vectors, you should
- * compare the length squared instead as it is slightly more efficient to calculate.
- *
- * @return {number} The square length of this vector.
- */
- lengthSq() {
-
- return this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w;
-
- }
-
- /**
- * Computes the Euclidean length (straight-line length) from (0, 0, 0, 0) to (x, y, z, w).
- *
- * @return {number} The length of this vector.
- */
- length() {
-
- return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w );
-
- }
-
- /**
- * Computes the Manhattan length of this vector.
- *
- * @return {number} The length of this vector.
- */
- manhattanLength() {
-
- return Math.abs( this.x ) + Math.abs( this.y ) + Math.abs( this.z ) + Math.abs( this.w );
-
- }
-
- /**
- * Converts this vector to a unit vector - that is, sets it equal to a vector
- * with the same direction as this one, but with a vector length of `1`.
- *
- * @return {Vector4} A reference to this vector.
- */
- normalize() {
-
- return this.divideScalar( this.length() || 1 );
-
- }
-
- /**
- * Sets this vector to a vector with the same direction as this one, but
- * with the specified length.
- *
- * @param {number} length - The new length of this vector.
- * @return {Vector4} A reference to this vector.
- */
- setLength( length ) {
-
- return this.normalize().multiplyScalar( length );
-
- }
-
- /**
- * Linearly interpolates between the given vector and this instance, where
- * alpha is the percent distance along the line - alpha = 0 will be this
- * vector, and alpha = 1 will be the given one.
- *
- * @param {Vector4} v - The vector to interpolate towards.
- * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
- * @return {Vector4} A reference to this vector.
- */
- lerp( v, alpha ) {
-
- this.x += ( v.x - this.x ) * alpha;
- this.y += ( v.y - this.y ) * alpha;
- this.z += ( v.z - this.z ) * alpha;
- this.w += ( v.w - this.w ) * alpha;
-
- return this;
-
- }
-
- /**
- * Linearly interpolates between the given vectors, where alpha is the percent
- * distance along the line - alpha = 0 will be first vector, and alpha = 1 will
- * be the second one. The result is stored in this instance.
- *
- * @param {Vector4} v1 - The first vector.
- * @param {Vector4} v2 - The second vector.
- * @param {number} alpha - The interpolation factor, typically in the closed interval `[0, 1]`.
- * @return {Vector4} A reference to this vector.
- */
- lerpVectors( v1, v2, alpha ) {
-
- this.x = v1.x + ( v2.x - v1.x ) * alpha;
- this.y = v1.y + ( v2.y - v1.y ) * alpha;
- this.z = v1.z + ( v2.z - v1.z ) * alpha;
- this.w = v1.w + ( v2.w - v1.w ) * alpha;
-
- return this;
-
- }
-
- /**
- * Returns `true` if this vector is equal with the given one.
- *
- * @param {Vector4} v - The vector to test for equality.
- * @return {boolean} Whether this vector is equal with the given one.
- */
- equals( v ) {
-
- return ( ( v.x === this.x ) && ( v.y === this.y ) && ( v.z === this.z ) && ( v.w === this.w ) );
-
- }
-
- /**
- * Sets this vector's x value to be `array[ offset ]`, y value to be `array[ offset + 1 ]`,
- * z value to be `array[ offset + 2 ]`, w value to be `array[ offset + 3 ]`.
- *
- * @param {Array} array - An array holding the vector component values.
- * @param {number} [offset=0] - The offset into the array.
- * @return {Vector4} A reference to this vector.
- */
- fromArray( array, offset = 0 ) {
-
- this.x = array[ offset ];
- this.y = array[ offset + 1 ];
- this.z = array[ offset + 2 ];
- this.w = array[ offset + 3 ];
-
- return this;
-
- }
-
- /**
- * Writes the components of this vector to the given array. If no array is provided,
- * the method returns a new instance.
- *
- * @param {Array} [array=[]] - The target array holding the vector components.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Array} The vector components.
- */
- toArray( array = [], offset = 0 ) {
-
- array[ offset ] = this.x;
- array[ offset + 1 ] = this.y;
- array[ offset + 2 ] = this.z;
- array[ offset + 3 ] = this.w;
-
- return array;
-
- }
-
- /**
- * Sets the components of this vector from the given buffer attribute.
- *
- * @param {BufferAttribute} attribute - The buffer attribute holding vector data.
- * @param {number} index - The index into the attribute.
- * @return {Vector4} A reference to this vector.
- */
- fromBufferAttribute( attribute, index ) {
-
- this.x = attribute.getX( index );
- this.y = attribute.getY( index );
- this.z = attribute.getZ( index );
- this.w = attribute.getW( index );
-
- return this;
-
- }
-
- /**
- * Sets each component of this vector to a pseudo-random value between `0` and
- * `1`, excluding `1`.
- *
- * @return {Vector4} A reference to this vector.
- */
- random() {
-
- this.x = Math.random();
- this.y = Math.random();
- this.z = Math.random();
- this.w = Math.random();
-
- return this;
-
- }
-
- *[ Symbol.iterator ]() {
-
- yield this.x;
- yield this.y;
- yield this.z;
- yield this.w;
-
- }
-
-}
-
-/**
- * A render target is a buffer where the video card draws pixels for a scene
- * that is being rendered in the background. It is used in different effects,
- * such as applying postprocessing to a rendered image before displaying it
- * on the screen.
- *
- * @augments EventDispatcher
- */
-class RenderTarget extends EventDispatcher {
-
- /**
- * Render target options.
- *
- * @typedef {Object} RenderTarget~Options
- * @property {boolean} [generateMipmaps=false] - Whether to generate mipmaps or not.
- * @property {number} [magFilter=LinearFilter] - The mag filter.
- * @property {number} [minFilter=LinearFilter] - The min filter.
- * @property {number} [format=RGBAFormat] - The texture format.
- * @property {number} [type=UnsignedByteType] - The texture type.
- * @property {?string} [internalFormat=null] - The texture's internal format.
- * @property {number} [wrapS=ClampToEdgeWrapping] - The texture's uv wrapping mode.
- * @property {number} [wrapT=ClampToEdgeWrapping] - The texture's uv wrapping mode.
- * @property {number} [anisotropy=1] - The texture's anisotropy value.
- * @property {string} [colorSpace=NoColorSpace] - The texture's color space.
- * @property {boolean} [depthBuffer=true] - Whether to allocate a depth buffer or not.
- * @property {boolean} [stencilBuffer=false] - Whether to allocate a stencil buffer or not.
- * @property {boolean} [resolveColorBuffer=true] - Whether to resolve the color buffer or not. Only relevant for multisampled render targets.
- * @property {boolean} [resolveDepthBuffer=true] - Whether to resolve the depth buffer or not. Only relevant for multisampled render targets.
- * @property {boolean} [resolveStencilBuffer=true] - Whether to resolve the stencil buffer or not. Only relevant for multisampled render targets.
- * @property {boolean} [storeMultisampledColorBuffer=true] - Whether to store the multisampled color buffer or not. Setting to `false` saves memory bandwidth when the multisampled data are not needed after a render pass.
- * @property {boolean} [storeMultisampledDepthBuffer=true] - Whether to store the multisampled depth buffer or not. Setting to `false` saves memory bandwidth when the multisampled data are not needed after a render pass.
- * @property {boolean} [storeMultisampledStencilBuffer=true] - Whether to store the multisampled stencil buffer or not. Setting to `false` saves memory bandwidth when the multisampled data are not needed after a render pass.
- * @property {?Texture} [depthTexture=null] - Reference to a depth texture.
- * @property {number} [samples=0] - The MSAA samples count.
- * @property {number} [count=1] - Defines the number of color attachments . Must be at least `1`.
- * @property {number} [depth=1] - The texture depth.
- * @property {boolean} [multiview=false] - Whether this target is used for multiview rendering (WebGL OVR_multiview2 extension).
- * @property {boolean} [useArrayDepthTexture=false] - Whether to create the depth texture as an array texture for per-layer depth testing. This is separate from multiview so layered render targets can use array depth without the multiview extension.
- */
-
- /**
- * Constructs a new render target.
- *
- * @param {number} [width=1] - The width of the render target.
- * @param {number} [height=1] - The height of the render target.
- * @param {RenderTarget~Options} [options] - The configuration object.
- */
- constructor( width = 1, height = 1, options = {} ) {
-
- super();
-
- options = Object.assign( {
- generateMipmaps: false,
- internalFormat: null,
- minFilter: LinearFilter,
- depthBuffer: true,
- stencilBuffer: false,
- resolveColorBuffer: true,
- resolveDepthBuffer: true,
- resolveStencilBuffer: true,
- storeMultisampledColorBuffer: true,
- storeMultisampledDepthBuffer: true,
- storeMultisampledStencilBuffer: true,
- depthTexture: null,
- samples: 0,
- count: 1,
- depth: 1,
- multiview: false,
- useArrayDepthTexture: false
- }, options );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isRenderTarget = true;
-
- /**
- * The width of the render target.
- *
- * @type {number}
- * @default 1
- */
- this.width = width;
-
- /**
- * The height of the render target.
- *
- * @type {number}
- * @default 1
- */
- this.height = height;
-
- /**
- * The depth of the render target.
- *
- * @type {number}
- * @default 1
- */
- this.depth = options.depth;
-
- /**
- * A rectangular area inside the render target's viewport. Fragments that are
- * outside the area will be discarded.
- *
- * @type {Vector4}
- * @default (0,0,width,height)
- */
- this.scissor = new Vector4( 0, 0, width, height );
-
- /**
- * Indicates whether the scissor test should be enabled when rendering into
- * this render target or not.
- *
- * @type {boolean}
- * @default false
- */
- this.scissorTest = false;
-
- /**
- * A rectangular area representing the render target's viewport.
- *
- * @type {Vector4}
- * @default (0,0,width,height)
- */
- this.viewport = new Vector4( 0, 0, width, height );
-
- /**
- * An array of textures. Each color attachment is represented as a separate texture.
- * Has at least a single entry for the default color attachment.
- *
- * @type {Array}
- */
- this.textures = [];
-
- const image = { width: width, height: height, depth: options.depth };
- const texture = new Texture( image );
-
- const count = options.count;
- for ( let i = 0; i < count; i ++ ) {
-
- this.textures[ i ] = texture.clone();
- this.textures[ i ].isRenderTargetTexture = true;
- this.textures[ i ].renderTarget = this;
-
- }
-
- this._setTextureOptions( options );
-
- /**
- * Whether to allocate a depth buffer or not.
- *
- * @type {boolean}
- * @default true
- */
- this.depthBuffer = options.depthBuffer;
-
- /**
- * Whether to allocate a stencil buffer or not.
- *
- * @type {boolean}
- * @default false
- */
- this.stencilBuffer = options.stencilBuffer;
-
- /**
- * Whether to resolve the color buffer or not. When set to `false`, the color
- * attachments do not receive the resolved (single-sampled) output of a render
- * pass and the render target's textures are left untouched. The rendered
- * content is then only accessible within the render pass itself.
- *
- * Only relevant for multisampled render targets.
- *
- * @type {boolean}
- * @default true
- */
- this.resolveColorBuffer = options.resolveColorBuffer;
-
- /**
- * Whether to resolve the depth buffer or not. When set to `false`, the depth
- * texture does not receive the resolved depth output of a render pass which
- * saves memory bandwidth. Use this setting when the depth data of a render
- * pass are not required afterwards.
- *
- * Only relevant for multisampled render targets in WebGL. WebGPU does not
- * support depth resolves; sampling the depth texture of a multisampled render
- * target accesses the multisampled data directly, see
- * {@link RenderTarget#storeMultisampledDepthBuffer}.
- *
- * @type {boolean}
- * @default true
- */
- this.resolveDepthBuffer = options.resolveDepthBuffer;
-
- /**
- * Whether to resolve the stencil buffer or not. Analogous to
- * {@link RenderTarget#resolveDepthBuffer} but for the stencil aspect.
- *
- * @type {boolean}
- * @default true
- */
- this.resolveStencilBuffer = options.resolveStencilBuffer;
-
- /**
- * Whether to store the multisampled color buffer or not. When set to `false`,
- * the multisampled data are discarded at the end of a render pass, right after
- * they have been resolved. This saves memory bandwidth, especially on tile-based
- * GPUs, and is the recommended setting for render targets that are fully redrawn
- * each frame and whose output is only accessed via the resolved textures (e.g.
- * scene passes in post-processing chains).
- *
- * Must be kept `true` when the multisampled data are needed after the render
- * pass ends, e.g. when rendering into the target without clearing or when the
- * scene contains transmissive objects which require a mid-pass framebuffer copy.
- *
- * @type {boolean}
- * @default true
- */
- this.storeMultisampledColorBuffer = options.storeMultisampledColorBuffer;
-
- /**
- * Whether to store the multisampled depth buffer or not. When set to `false`,
- * the multisampled depth data are discarded at the end of a render pass which
- * saves memory bandwidth.
- *
- * Must be kept `true` in WebGPU when the depth texture of a multisampled render
- * target is sampled (e.g. by depth-based post-processing effects) since depth
- * is read directly from the multisampled data.
- *
- * @type {boolean}
- * @default true
- */
- this.storeMultisampledDepthBuffer = options.storeMultisampledDepthBuffer;
-
- /**
- * Whether to store the multisampled stencil buffer or not. Analogous to
- * {@link RenderTarget#storeMultisampledDepthBuffer} but for the stencil aspect.
- *
- * @type {boolean}
- * @default true
- */
- this.storeMultisampledStencilBuffer = options.storeMultisampledStencilBuffer;
-
- this._depthTexture = null;
- this.depthTexture = options.depthTexture;
-
- /**
- * The number of MSAA samples.
- *
- * A value of `0` disables MSAA.
- *
- * @type {number}
- * @default 0
- */
- this.samples = options.samples;
-
- /**
- * Whether to this target is used in multiview rendering.
- *
- * @type {boolean}
- * @default false
- */
- this.multiview = options.multiview;
-
- /**
- * Whether to create the depth texture as an array texture for per-layer depth testing.
- * This is separate from multiview so layered render targets can use array depth without
- * the multiview extension.
- *
- * @type {boolean}
- * @default false
- */
- this.useArrayDepthTexture = options.useArrayDepthTexture;
-
- }
-
- _setTextureOptions( options = {} ) {
-
- const values = {
- minFilter: LinearFilter,
- generateMipmaps: false,
- flipY: false,
- internalFormat: null
- };
-
- if ( options.mapping !== undefined ) values.mapping = options.mapping;
- if ( options.wrapS !== undefined ) values.wrapS = options.wrapS;
- if ( options.wrapT !== undefined ) values.wrapT = options.wrapT;
- if ( options.wrapR !== undefined ) values.wrapR = options.wrapR;
- if ( options.magFilter !== undefined ) values.magFilter = options.magFilter;
- if ( options.minFilter !== undefined ) values.minFilter = options.minFilter;
- if ( options.format !== undefined ) values.format = options.format;
- if ( options.type !== undefined ) values.type = options.type;
- if ( options.anisotropy !== undefined ) values.anisotropy = options.anisotropy;
- if ( options.colorSpace !== undefined ) values.colorSpace = options.colorSpace;
- if ( options.flipY !== undefined ) values.flipY = options.flipY;
- if ( options.generateMipmaps !== undefined ) values.generateMipmaps = options.generateMipmaps;
- if ( options.internalFormat !== undefined ) values.internalFormat = options.internalFormat;
-
- for ( let i = 0; i < this.textures.length; i ++ ) {
-
- const texture = this.textures[ i ];
- texture.setValues( values );
-
- }
-
- }
-
- /**
- * The texture representing the default color attachment.
- *
- * @type {Texture}
- */
- get texture() {
-
- return this.textures[ 0 ];
-
- }
-
- set texture( value ) {
-
- this.textures[ 0 ] = value;
-
- }
-
- set depthTexture( current ) {
-
- if ( this._depthTexture !== null && this._depthTexture.renderTarget === this ) this._depthTexture.renderTarget = null;
- if ( current !== null && current.renderTarget === null ) current.renderTarget = this;
-
- this._depthTexture = current;
-
- }
-
- /**
- * Instead of saving the depth in a renderbuffer, a texture
- * can be used instead which is useful for further processing
- * e.g. in context of post-processing.
- *
- * @type {?DepthTexture}
- * @default null
- */
- get depthTexture() {
-
- return this._depthTexture;
-
- }
-
- /**
- * Sets the size of this render target.
- *
- * @param {number} width - The width.
- * @param {number} height - The height.
- * @param {number} [depth=1] - The depth.
- */
- setSize( width, height, depth = 1 ) {
-
- if ( this.width !== width || this.height !== height || this.depth !== depth ) {
-
- this.width = width;
- this.height = height;
- this.depth = depth;
-
- for ( let i = 0, il = this.textures.length; i < il; i ++ ) {
-
- this.textures[ i ].image.width = width;
- this.textures[ i ].image.height = height;
- this.textures[ i ].image.depth = depth;
-
- if ( this.textures[ i ].isData3DTexture !== true ) { // Fix for #31693
-
- // TODO: Reconsider setting isArrayTexture flag here and in the ctor of Texture.
- // Maybe a method `isArrayTexture()` or just a getter could replace a flag since
- // both are evaluated on each call?
-
- this.textures[ i ].isArrayTexture = this.textures[ i ].image.depth > 1;
-
- }
-
- }
-
- this.dispose();
-
- }
-
- this.viewport.set( 0, 0, width, height );
- this.scissor.set( 0, 0, width, height );
-
- }
-
- /**
- * Returns a new render target with copied values from this instance.
- *
- * @return {RenderTarget} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Copies the settings of the given render target. This is a structural copy so
- * no resources are shared between render targets after the copy. That includes
- * all MRT textures and the depth texture.
- *
- * @param {RenderTarget} source - The render target to copy.
- * @return {RenderTarget} A reference to this instance.
- */
- copy( source ) {
-
- this.width = source.width;
- this.height = source.height;
- this.depth = source.depth;
-
- this.scissor.copy( source.scissor );
- this.scissorTest = source.scissorTest;
-
- this.viewport.copy( source.viewport );
-
- this.textures.length = 0;
-
- for ( let i = 0, il = source.textures.length; i < il; i ++ ) {
-
- this.textures[ i ] = source.textures[ i ].clone();
- this.textures[ i ].isRenderTargetTexture = true;
- this.textures[ i ].renderTarget = this;
-
- // ensure image object is not shared, see #20328
-
- const image = Object.assign( {}, source.textures[ i ].image );
- this.textures[ i ].source = new TextureSource( image );
-
- }
-
- this.depthBuffer = source.depthBuffer;
- this.stencilBuffer = source.stencilBuffer;
-
- this.resolveColorBuffer = source.resolveColorBuffer;
- this.resolveDepthBuffer = source.resolveDepthBuffer;
- this.resolveStencilBuffer = source.resolveStencilBuffer;
-
- this.storeMultisampledColorBuffer = source.storeMultisampledColorBuffer;
- this.storeMultisampledDepthBuffer = source.storeMultisampledDepthBuffer;
- this.storeMultisampledStencilBuffer = source.storeMultisampledStencilBuffer;
-
- if ( source.depthTexture !== null ) {
-
- if ( source.depthTexture.renderTarget === source ) {
-
- const depthTexture = source.depthTexture.clone();
- depthTexture.renderTarget = null;
-
- this.depthTexture = depthTexture;
-
- } else {
-
- this.depthTexture = source.depthTexture;
-
- }
-
- }
-
- this.samples = source.samples;
- this.multiview = source.multiview;
- this.useArrayDepthTexture = source.useArrayDepthTexture;
-
- return this;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- *
- * @fires RenderTarget#dispose
- */
- dispose() {
-
- this.dispatchEvent( { type: 'dispose' } );
-
- }
-
-}
-
-/**
- * A render target used in context of {@link WebGLRenderer}.
- *
- * @augments RenderTarget
- */
-class WebGLRenderTarget extends RenderTarget {
-
- /**
- * Constructs a new 3D render target.
- *
- * @param {number} [width=1] - The width of the render target.
- * @param {number} [height=1] - The height of the render target.
- * @param {RenderTarget~Options} [options] - The configuration object.
- */
- constructor( width = 1, height = 1, options = {} ) {
-
- super( width, height, options );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isWebGLRenderTarget = true;
-
- }
-
-}
-
-/**
- * Creates an array of textures directly from raw buffer data.
- *
- * @augments Texture
- */
-class DataArrayTexture extends Texture {
-
- /**
- * Constructs a new data array texture.
- *
- * @param {?TypedArray} [data=null] - The buffer data.
- * @param {number} [width=1] - The width of the texture.
- * @param {number} [height=1] - The height of the texture.
- * @param {number} [depth=1] - The depth of the texture.
- */
- constructor( data = null, width = 1, height = 1, depth = 1 ) {
-
- super( null );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isDataArrayTexture = true;
-
- /**
- * The image definition of a data texture.
- *
- * @type {{data:TypedArray,width:number,height:number,depth:number}}
- */
- this.image = { data, width, height, depth };
-
- /**
- * How the texture is sampled when a texel covers more than one pixel.
- *
- * Overwritten and set to `NearestFilter` by default.
- *
- * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
- * @default NearestFilter
- */
- this.magFilter = NearestFilter;
-
- /**
- * How the texture is sampled when a texel covers less than one pixel.
- *
- * Overwritten and set to `NearestFilter` by default.
- *
- * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
- * @default NearestFilter
- */
- this.minFilter = NearestFilter;
-
- /**
- * This defines how the texture is wrapped in the depth and corresponds to
- * *W* in UVW mapping.
- *
- * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}
- * @default ClampToEdgeWrapping
- */
- this.wrapR = ClampToEdgeWrapping;
-
- /**
- * Whether to generate mipmaps (if possible) for a texture.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.generateMipmaps = false;
-
- /**
- * If set to `true`, the texture is flipped along the vertical axis when
- * uploaded to the GPU.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.flipY = false;
-
- /**
- * Specifies the alignment requirements for the start of each pixel row in memory.
- *
- * Overwritten and set to `1` by default.
- *
- * @type {boolean}
- * @default 1
- */
- this.unpackAlignment = 1;
-
- /**
- * A set of all layers which need to be updated in the texture.
- *
- * @type {Set}
- */
- this.layerUpdates = new Set();
-
- }
-
- /**
- * Copies the values of the given texture to this instance.
- *
- * @param {DataArrayTexture} source - The texture to copy.
- * @return {DataArrayTexture} A reference to this instance.
- */
- copy( source ) {
-
- super.copy( source );
-
- this.wrapR = source.wrapR;
-
- return this;
-
- }
-
- /**
- * Describes that a specific layer of the texture needs to be updated.
- * Normally when {@link Texture#needsUpdate} is set to `true`, the
- * entire data texture array is sent to the GPU. Marking specific
- * layers will only transmit subsets of all mipmaps associated with a
- * specific depth in the array which is often much more performant.
- *
- * @param {number} layerIndex - The layer index that should be updated.
- */
- addLayerUpdate( layerIndex ) {
-
- this.layerUpdates.add( layerIndex );
-
- }
-
- /**
- * Resets the layer updates registry.
- */
- clearLayerUpdates() {
-
- this.layerUpdates.clear();
-
- }
-
-}
-
-/**
- * An array render target used in context of {@link WebGLRenderer}.
- *
- * @augments WebGLRenderTarget
- */
-class WebGLArrayRenderTarget extends WebGLRenderTarget {
-
- /**
- * Constructs a new array render target.
- *
- * @param {number} [width=1] - The width of the render target.
- * @param {number} [height=1] - The height of the render target.
- * @param {number} [depth=1] - The height of the render target.
- * @param {RenderTarget~Options} [options] - The configuration object.
- */
- constructor( width = 1, height = 1, depth = 1, options = {} ) {
-
- super( width, height, options );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isWebGLArrayRenderTarget = true;
-
- this.depth = depth;
-
- /**
- * Overwritten with a different texture type.
- *
- * @type {DataArrayTexture}
- */
- this.texture = new DataArrayTexture( null, width, height, depth );
- this._setTextureOptions( options );
-
- this.texture.isRenderTargetTexture = true;
-
- }
-
-}
-
-/**
- * Creates a three-dimensional texture from raw data, with parameters to
- * divide it into width, height, and depth.
- *
- * @augments Texture
- */
-class Data3DTexture extends Texture {
-
- /**
- * Constructs a new data array texture.
- *
- * @param {?TypedArray} [data=null] - The buffer data.
- * @param {number} [width=1] - The width of the texture.
- * @param {number} [height=1] - The height of the texture.
- * @param {number} [depth=1] - The depth of the texture.
- */
- constructor( data = null, width = 1, height = 1, depth = 1 ) {
-
- // We're going to add .setXXX() methods for setting properties later.
- // Users can still set in Data3DTexture directly.
- //
- // const texture = new THREE.Data3DTexture( data, width, height, depth );
- // texture.anisotropy = 16;
- //
- // See #14839
-
- super( null );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isData3DTexture = true;
-
- /**
- * The image definition of a data texture.
- *
- * @type {{data:TypedArray,width:number,height:number,depth:number}}
- */
- this.image = { data, width, height, depth };
-
- /**
- * How the texture is sampled when a texel covers more than one pixel.
- *
- * Overwritten and set to `NearestFilter` by default.
- *
- * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
- * @default NearestFilter
- */
- this.magFilter = NearestFilter;
-
- /**
- * How the texture is sampled when a texel covers less than one pixel.
- *
- * Overwritten and set to `NearestFilter` by default.
- *
- * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
- * @default NearestFilter
- */
- this.minFilter = NearestFilter;
-
- /**
- * This defines how the texture is wrapped in the depth and corresponds to
- * *W* in UVW mapping.
- *
- * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}
- * @default ClampToEdgeWrapping
- */
- this.wrapR = ClampToEdgeWrapping;
-
- /**
- * Whether to generate mipmaps (if possible) for a texture.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.generateMipmaps = false;
-
- /**
- * If set to `true`, the texture is flipped along the vertical axis when
- * uploaded to the GPU.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.flipY = false;
-
- /**
- * Specifies the alignment requirements for the start of each pixel row in memory.
- *
- * Overwritten and set to `1` by default.
- *
- * @type {boolean}
- * @default 1
- */
- this.unpackAlignment = 1;
-
- }
-
- /**
- * Copies the values of the given texture to this instance.
- *
- * @param {Data3DTexture} source - The texture to copy.
- * @return {Data3DTexture} A reference to this instance.
- */
- copy( source ) {
-
- super.copy( source );
-
- this.wrapR = source.wrapR;
-
- return this;
-
- }
-
-}
-
-/**
- * A 3D render target used in context of {@link WebGLRenderer}.
- *
- * @augments WebGLRenderTarget
- */
-class WebGL3DRenderTarget extends WebGLRenderTarget {
-
- /**
- * Constructs a new 3D render target.
- *
- * @param {number} [width=1] - The width of the render target.
- * @param {number} [height=1] - The height of the render target.
- * @param {number} [depth=1] - The height of the render target.
- * @param {RenderTarget~Options} [options] - The configuration object.
- */
- constructor( width = 1, height = 1, depth = 1, options = {} ) {
-
- super( width, height, options );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isWebGL3DRenderTarget = true;
-
- this.depth = depth;
-
- /**
- * Overwritten with a different texture type.
- *
- * @type {Data3DTexture}
- */
- this.texture = new Data3DTexture( null, width, height, depth );
- this._setTextureOptions( options );
-
- this.texture.isRenderTargetTexture = true;
-
- }
-
-}
-
-/**
- * Represents a 4x4 matrix.
- *
- * The most common use of a 4x4 matrix in 3D computer graphics is as a transformation matrix.
- * For an introduction to transformation matrices as used in WebGL, check out [this tutorial](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices)
- *
- * This allows a 3D vector representing a point in 3D space to undergo
- * transformations such as translation, rotation, shear, scale, reflection,
- * orthogonal or perspective projection and so on, by being multiplied by the
- * matrix. This is known as `applying` the matrix to the vector.
- *
- * A Note on Row-Major and Column-Major Ordering:
- *
- * The constructor and {@link Matrix3#set} method take arguments in
- * [row-major](https://en.wikipedia.org/wiki/Row-_and_column-major_order#Column-major_order)
- * order, while internally they are stored in the {@link Matrix3#elements} array in column-major order.
- * This means that calling:
- * ```js
- * const m = new THREE.Matrix4();
- * m.set( 11, 12, 13, 14,
- * 21, 22, 23, 24,
- * 31, 32, 33, 34,
- * 41, 42, 43, 44 );
- * ```
- * will result in the elements array containing:
- * ```js
- * m.elements = [ 11, 21, 31, 41,
- * 12, 22, 32, 42,
- * 13, 23, 33, 43,
- * 14, 24, 34, 44 ];
- * ```
- * and internally all calculations are performed using column-major ordering.
- * However, as the actual ordering makes no difference mathematically and
- * most people are used to thinking about matrices in row-major order, the
- * three.js documentation shows matrices in row-major order. Just bear in
- * mind that if you are reading the source code, you'll have to take the
- * transpose of any matrices outlined here to make sense of the calculations.
- */
-class Matrix4 {
-
- static {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- Matrix4.prototype.isMatrix4 = true;
-
- }
-
- /**
- * Constructs a new 4x4 matrix. The arguments are supposed to be
- * in row-major order. If no arguments are provided, the constructor
- * initializes the matrix as an identity matrix.
- *
- * @param {number} [n11] - 1-1 matrix element.
- * @param {number} [n12] - 1-2 matrix element.
- * @param {number} [n13] - 1-3 matrix element.
- * @param {number} [n14] - 1-4 matrix element.
- * @param {number} [n21] - 2-1 matrix element.
- * @param {number} [n22] - 2-2 matrix element.
- * @param {number} [n23] - 2-3 matrix element.
- * @param {number} [n24] - 2-4 matrix element.
- * @param {number} [n31] - 3-1 matrix element.
- * @param {number} [n32] - 3-2 matrix element.
- * @param {number} [n33] - 3-3 matrix element.
- * @param {number} [n34] - 3-4 matrix element.
- * @param {number} [n41] - 4-1 matrix element.
- * @param {number} [n42] - 4-2 matrix element.
- * @param {number} [n43] - 4-3 matrix element.
- * @param {number} [n44] - 4-4 matrix element.
- */
- constructor( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) {
-
- /**
- * A column-major list of matrix values.
- *
- * @type {Array}
- */
- this.elements = [
-
- 1, 0, 0, 0,
- 0, 1, 0, 0,
- 0, 0, 1, 0,
- 0, 0, 0, 1
-
- ];
-
- if ( n11 !== undefined ) {
-
- this.set( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 );
-
- }
-
- }
-
- /**
- * Sets the elements of the matrix.The arguments are supposed to be
- * in row-major order.
- *
- * @param {number} [n11] - 1-1 matrix element.
- * @param {number} [n12] - 1-2 matrix element.
- * @param {number} [n13] - 1-3 matrix element.
- * @param {number} [n14] - 1-4 matrix element.
- * @param {number} [n21] - 2-1 matrix element.
- * @param {number} [n22] - 2-2 matrix element.
- * @param {number} [n23] - 2-3 matrix element.
- * @param {number} [n24] - 2-4 matrix element.
- * @param {number} [n31] - 3-1 matrix element.
- * @param {number} [n32] - 3-2 matrix element.
- * @param {number} [n33] - 3-3 matrix element.
- * @param {number} [n34] - 3-4 matrix element.
- * @param {number} [n41] - 4-1 matrix element.
- * @param {number} [n42] - 4-2 matrix element.
- * @param {number} [n43] - 4-3 matrix element.
- * @param {number} [n44] - 4-4 matrix element.
- * @return {Matrix4} A reference to this matrix.
- */
- set( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) {
-
- const te = this.elements;
-
- te[ 0 ] = n11; te[ 4 ] = n12; te[ 8 ] = n13; te[ 12 ] = n14;
- te[ 1 ] = n21; te[ 5 ] = n22; te[ 9 ] = n23; te[ 13 ] = n24;
- te[ 2 ] = n31; te[ 6 ] = n32; te[ 10 ] = n33; te[ 14 ] = n34;
- te[ 3 ] = n41; te[ 7 ] = n42; te[ 11 ] = n43; te[ 15 ] = n44;
-
- return this;
-
- }
-
- /**
- * Sets this matrix to the 4x4 identity matrix.
- *
- * @return {Matrix4} A reference to this matrix.
- */
- identity() {
-
- this.set(
-
- 1, 0, 0, 0,
- 0, 1, 0, 0,
- 0, 0, 1, 0,
- 0, 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Returns a matrix with copied values from this instance.
- *
- * @return {Matrix4} A clone of this instance.
- */
- clone() {
-
- return new Matrix4().fromArray( this.elements );
-
- }
-
- /**
- * Copies the values of the given matrix to this instance.
- *
- * @param {Matrix4} m - The matrix to copy.
- * @return {Matrix4} A reference to this matrix.
- */
- copy( m ) {
-
- const te = this.elements;
- const me = m.elements;
-
- te[ 0 ] = me[ 0 ]; te[ 1 ] = me[ 1 ]; te[ 2 ] = me[ 2 ]; te[ 3 ] = me[ 3 ];
- te[ 4 ] = me[ 4 ]; te[ 5 ] = me[ 5 ]; te[ 6 ] = me[ 6 ]; te[ 7 ] = me[ 7 ];
- te[ 8 ] = me[ 8 ]; te[ 9 ] = me[ 9 ]; te[ 10 ] = me[ 10 ]; te[ 11 ] = me[ 11 ];
- te[ 12 ] = me[ 12 ]; te[ 13 ] = me[ 13 ]; te[ 14 ] = me[ 14 ]; te[ 15 ] = me[ 15 ];
-
- return this;
-
- }
-
- /**
- * Copies the translation component of the given matrix
- * into this matrix's translation component.
- *
- * @param {Matrix4} m - The matrix to copy the translation component.
- * @return {Matrix4} A reference to this matrix.
- */
- copyPosition( m ) {
-
- const te = this.elements, me = m.elements;
-
- te[ 12 ] = me[ 12 ];
- te[ 13 ] = me[ 13 ];
- te[ 14 ] = me[ 14 ];
-
- return this;
-
- }
-
- /**
- * Set the upper 3x3 elements of this matrix to the values of given 3x3 matrix.
- *
- * @param {Matrix3} m - The 3x3 matrix.
- * @return {Matrix4} A reference to this matrix.
- */
- setFromMatrix3( m ) {
-
- const me = m.elements;
-
- this.set(
-
- me[ 0 ], me[ 3 ], me[ 6 ], 0,
- me[ 1 ], me[ 4 ], me[ 7 ], 0,
- me[ 2 ], me[ 5 ], me[ 8 ], 0,
- 0, 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Extracts the basis vectors of this matrix into the three vectors provided.
- *
- * @param {Vector3} xAxis - The basis's x axis.
- * @param {Vector3} yAxis - The basis's y axis.
- * @param {Vector3} zAxis - The basis's z axis.
- * @return {Matrix4} A reference to this matrix.
- */
- extractBasis( xAxis, yAxis, zAxis ) {
-
- if ( this.determinantAffine() === 0 ) {
-
- xAxis.set( 1, 0, 0 );
- yAxis.set( 0, 1, 0 );
- zAxis.set( 0, 0, 1 );
-
- return this;
-
- }
-
- xAxis.setFromMatrixColumn( this, 0 );
- yAxis.setFromMatrixColumn( this, 1 );
- zAxis.setFromMatrixColumn( this, 2 );
-
- return this;
-
- }
-
- /**
- * Sets the given basis vectors to this matrix.
- *
- * @param {Vector3} xAxis - The basis's x axis.
- * @param {Vector3} yAxis - The basis's y axis.
- * @param {Vector3} zAxis - The basis's z axis.
- * @return {Matrix4} A reference to this matrix.
- */
- makeBasis( xAxis, yAxis, zAxis ) {
-
- this.set(
- xAxis.x, yAxis.x, zAxis.x, 0,
- xAxis.y, yAxis.y, zAxis.y, 0,
- xAxis.z, yAxis.z, zAxis.z, 0,
- 0, 0, 0, 1
- );
-
- return this;
-
- }
-
- /**
- * Extracts the rotation component of the given matrix
- * into this matrix's rotation component.
- *
- * Note: This method does not support reflection matrices.
- *
- * @param {Matrix4} m - The matrix.
- * @return {Matrix4} A reference to this matrix.
- */
- extractRotation( m ) {
-
- if ( m.determinantAffine() === 0 ) {
-
- return this.identity();
-
- }
-
- const te = this.elements;
- const me = m.elements;
-
- const scaleX = 1 / _v1$7.setFromMatrixColumn( m, 0 ).length();
- const scaleY = 1 / _v1$7.setFromMatrixColumn( m, 1 ).length();
- const scaleZ = 1 / _v1$7.setFromMatrixColumn( m, 2 ).length();
-
- te[ 0 ] = me[ 0 ] * scaleX;
- te[ 1 ] = me[ 1 ] * scaleX;
- te[ 2 ] = me[ 2 ] * scaleX;
- te[ 3 ] = 0;
-
- te[ 4 ] = me[ 4 ] * scaleY;
- te[ 5 ] = me[ 5 ] * scaleY;
- te[ 6 ] = me[ 6 ] * scaleY;
- te[ 7 ] = 0;
-
- te[ 8 ] = me[ 8 ] * scaleZ;
- te[ 9 ] = me[ 9 ] * scaleZ;
- te[ 10 ] = me[ 10 ] * scaleZ;
- te[ 11 ] = 0;
-
- te[ 12 ] = 0;
- te[ 13 ] = 0;
- te[ 14 ] = 0;
- te[ 15 ] = 1;
-
- return this;
-
- }
-
- /**
- * Sets the rotation component (the upper left 3x3 matrix) of this matrix to
- * the rotation specified by the given Euler angles. The rest of
- * the matrix is set to the identity. Depending on the {@link Euler#order},
- * there are six possible outcomes. See [this page](https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix)
- * for a complete list.
- *
- * @param {Euler} euler - The Euler angles.
- * @return {Matrix4} A reference to this matrix.
- */
- makeRotationFromEuler( euler ) {
-
- const te = this.elements;
-
- const x = euler.x, y = euler.y, z = euler.z;
- const a = Math.cos( x ), b = Math.sin( x );
- const c = Math.cos( y ), d = Math.sin( y );
- const e = Math.cos( z ), f = Math.sin( z );
-
- if ( euler.order === 'XYZ' ) {
-
- const ae = a * e, af = a * f, be = b * e, bf = b * f;
-
- te[ 0 ] = c * e;
- te[ 4 ] = - c * f;
- te[ 8 ] = d;
-
- te[ 1 ] = af + be * d;
- te[ 5 ] = ae - bf * d;
- te[ 9 ] = - b * c;
-
- te[ 2 ] = bf - ae * d;
- te[ 6 ] = be + af * d;
- te[ 10 ] = a * c;
-
- } else if ( euler.order === 'YXZ' ) {
-
- const ce = c * e, cf = c * f, de = d * e, df = d * f;
-
- te[ 0 ] = ce + df * b;
- te[ 4 ] = de * b - cf;
- te[ 8 ] = a * d;
-
- te[ 1 ] = a * f;
- te[ 5 ] = a * e;
- te[ 9 ] = - b;
-
- te[ 2 ] = cf * b - de;
- te[ 6 ] = df + ce * b;
- te[ 10 ] = a * c;
-
- } else if ( euler.order === 'ZXY' ) {
-
- const ce = c * e, cf = c * f, de = d * e, df = d * f;
-
- te[ 0 ] = ce - df * b;
- te[ 4 ] = - a * f;
- te[ 8 ] = de + cf * b;
-
- te[ 1 ] = cf + de * b;
- te[ 5 ] = a * e;
- te[ 9 ] = df - ce * b;
-
- te[ 2 ] = - a * d;
- te[ 6 ] = b;
- te[ 10 ] = a * c;
-
- } else if ( euler.order === 'ZYX' ) {
-
- const ae = a * e, af = a * f, be = b * e, bf = b * f;
-
- te[ 0 ] = c * e;
- te[ 4 ] = be * d - af;
- te[ 8 ] = ae * d + bf;
-
- te[ 1 ] = c * f;
- te[ 5 ] = bf * d + ae;
- te[ 9 ] = af * d - be;
-
- te[ 2 ] = - d;
- te[ 6 ] = b * c;
- te[ 10 ] = a * c;
-
- } else if ( euler.order === 'YZX' ) {
-
- const ac = a * c, ad = a * d, bc = b * c, bd = b * d;
-
- te[ 0 ] = c * e;
- te[ 4 ] = bd - ac * f;
- te[ 8 ] = bc * f + ad;
-
- te[ 1 ] = f;
- te[ 5 ] = a * e;
- te[ 9 ] = - b * e;
-
- te[ 2 ] = - d * e;
- te[ 6 ] = ad * f + bc;
- te[ 10 ] = ac - bd * f;
-
- } else if ( euler.order === 'XZY' ) {
-
- const ac = a * c, ad = a * d, bc = b * c, bd = b * d;
-
- te[ 0 ] = c * e;
- te[ 4 ] = - f;
- te[ 8 ] = d * e;
-
- te[ 1 ] = ac * f + bd;
- te[ 5 ] = a * e;
- te[ 9 ] = ad * f - bc;
-
- te[ 2 ] = bc * f - ad;
- te[ 6 ] = b * e;
- te[ 10 ] = bd * f + ac;
-
- }
-
- // bottom row
- te[ 3 ] = 0;
- te[ 7 ] = 0;
- te[ 11 ] = 0;
-
- // last column
- te[ 12 ] = 0;
- te[ 13 ] = 0;
- te[ 14 ] = 0;
- te[ 15 ] = 1;
-
- return this;
-
- }
-
- /**
- * Sets the rotation component of this matrix to the rotation specified by
- * the given Quaternion as outlined [here](https://en.wikipedia.org/wiki/Rotation_matrix#Quaternion)
- * The rest of the matrix is set to the identity.
- *
- * @param {Quaternion} q - The Quaternion.
- * @return {Matrix4} A reference to this matrix.
- */
- makeRotationFromQuaternion( q ) {
-
- return this.compose( _zero, q, _one );
-
- }
-
- /**
- * Sets the rotation component of the transformation matrix, looking from `eye` towards
- * `target`, and oriented by the up-direction.
- *
- * @param {Vector3} eye - The eye vector.
- * @param {Vector3} target - The target vector.
- * @param {Vector3} up - The up vector.
- * @return {Matrix4} A reference to this matrix.
- */
- lookAt( eye, target, up ) {
-
- const te = this.elements;
-
- _z.subVectors( eye, target );
-
- if ( _z.lengthSq() === 0 ) {
-
- // eye and target are in the same position
-
- _z.z = 1;
-
- }
-
- _z.normalize();
- _x.crossVectors( up, _z );
-
- if ( _x.lengthSq() === 0 ) {
-
- // up and z are parallel
-
- if ( Math.abs( up.z ) === 1 ) {
-
- _z.x += 0.0001;
-
- } else {
-
- _z.z += 0.0001;
-
- }
-
- _z.normalize();
- _x.crossVectors( up, _z );
-
- }
-
- _x.normalize();
- _y.crossVectors( _z, _x );
-
- te[ 0 ] = _x.x; te[ 4 ] = _y.x; te[ 8 ] = _z.x;
- te[ 1 ] = _x.y; te[ 5 ] = _y.y; te[ 9 ] = _z.y;
- te[ 2 ] = _x.z; te[ 6 ] = _y.z; te[ 10 ] = _z.z;
-
- return this;
-
- }
-
- /**
- * Post-multiplies this matrix by the given 4x4 matrix.
- *
- * @param {Matrix4} m - The matrix to multiply with.
- * @return {Matrix4} A reference to this matrix.
- */
- multiply( m ) {
-
- return this.multiplyMatrices( this, m );
-
- }
-
- /**
- * Pre-multiplies this matrix by the given 4x4 matrix.
- *
- * @param {Matrix4} m - The matrix to multiply with.
- * @return {Matrix4} A reference to this matrix.
- */
- premultiply( m ) {
-
- return this.multiplyMatrices( m, this );
-
- }
-
- /**
- * Multiples the given 4x4 matrices and stores the result
- * in this matrix.
- *
- * @param {Matrix4} a - The first matrix.
- * @param {Matrix4} b - The second matrix.
- * @return {Matrix4} A reference to this matrix.
- */
- multiplyMatrices( a, b ) {
-
- const ae = a.elements;
- const be = b.elements;
- const te = this.elements;
-
- const a11 = ae[ 0 ], a12 = ae[ 4 ], a13 = ae[ 8 ], a14 = ae[ 12 ];
- const a21 = ae[ 1 ], a22 = ae[ 5 ], a23 = ae[ 9 ], a24 = ae[ 13 ];
- const a31 = ae[ 2 ], a32 = ae[ 6 ], a33 = ae[ 10 ], a34 = ae[ 14 ];
- const a41 = ae[ 3 ], a42 = ae[ 7 ], a43 = ae[ 11 ], a44 = ae[ 15 ];
-
- const b11 = be[ 0 ], b12 = be[ 4 ], b13 = be[ 8 ], b14 = be[ 12 ];
- const b21 = be[ 1 ], b22 = be[ 5 ], b23 = be[ 9 ], b24 = be[ 13 ];
- const b31 = be[ 2 ], b32 = be[ 6 ], b33 = be[ 10 ], b34 = be[ 14 ];
- const b41 = be[ 3 ], b42 = be[ 7 ], b43 = be[ 11 ], b44 = be[ 15 ];
-
- te[ 0 ] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;
- te[ 4 ] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;
- te[ 8 ] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;
- te[ 12 ] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;
-
- te[ 1 ] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;
- te[ 5 ] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;
- te[ 9 ] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;
- te[ 13 ] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;
-
- te[ 2 ] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;
- te[ 6 ] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;
- te[ 10 ] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;
- te[ 14 ] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;
-
- te[ 3 ] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;
- te[ 7 ] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;
- te[ 11 ] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;
- te[ 15 ] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;
-
- return this;
-
- }
-
- /**
- * Multiplies every component of the matrix by the given scalar.
- *
- * @param {number} s - The scalar.
- * @return {Matrix4} A reference to this matrix.
- */
- multiplyScalar( s ) {
-
- const te = this.elements;
-
- te[ 0 ] *= s; te[ 4 ] *= s; te[ 8 ] *= s; te[ 12 ] *= s;
- te[ 1 ] *= s; te[ 5 ] *= s; te[ 9 ] *= s; te[ 13 ] *= s;
- te[ 2 ] *= s; te[ 6 ] *= s; te[ 10 ] *= s; te[ 14 ] *= s;
- te[ 3 ] *= s; te[ 7 ] *= s; te[ 11 ] *= s; te[ 15 ] *= s;
-
- return this;
-
- }
-
- /**
- * Computes and returns the determinant of this matrix.
- *
- * @return {number} The determinant.
- */
- determinant() {
-
- const te = this.elements;
-
- const n11 = te[ 0 ], n12 = te[ 4 ], n13 = te[ 8 ], n14 = te[ 12 ];
- const n21 = te[ 1 ], n22 = te[ 5 ], n23 = te[ 9 ], n24 = te[ 13 ];
- const n31 = te[ 2 ], n32 = te[ 6 ], n33 = te[ 10 ], n34 = te[ 14 ];
- const n41 = te[ 3 ], n42 = te[ 7 ], n43 = te[ 11 ], n44 = te[ 15 ];
-
- const t11 = n23 * n34 - n24 * n33;
- const t12 = n22 * n34 - n24 * n32;
- const t13 = n22 * n33 - n23 * n32;
-
- const t21 = n21 * n34 - n24 * n31;
- const t22 = n21 * n33 - n23 * n31;
- const t23 = n21 * n32 - n22 * n31;
-
- return n11 * ( n42 * t11 - n43 * t12 + n44 * t13 ) -
- n12 * ( n41 * t11 - n43 * t21 + n44 * t22 ) +
- n13 * ( n41 * t12 - n42 * t21 + n44 * t23 ) -
- n14 * ( n41 * t13 - n42 * t22 + n43 * t23 );
-
- }
-
- /**
- * Computes and returns the determinant of the 4x4 matrix, but assumes the
- * matrix is affine, saving some computations.
- *
- * For affine matrices (like an object's world matrix), this value equals the
- * full 4x4 {@link Matrix4#determinant} but is cheaper to compute.
- *
- * Assumes the bottom row is [0, 0, 0, 1].
- *
- * @return {number} The determinant of the matrix.
- */
- determinantAffine() {
-
- const te = this.elements;
-
- const n11 = te[ 0 ], n12 = te[ 4 ], n13 = te[ 8 ];
- const n21 = te[ 1 ], n22 = te[ 5 ], n23 = te[ 9 ];
- const n31 = te[ 2 ], n32 = te[ 6 ], n33 = te[ 10 ];
-
- return n11 * ( n22 * n33 - n23 * n32 ) -
- n12 * ( n21 * n33 - n23 * n31 ) +
- n13 * ( n21 * n32 - n22 * n31 );
-
- }
-
- /**
- * Transposes this matrix in place.
- *
- * @return {Matrix4} A reference to this matrix.
- */
- transpose() {
-
- const te = this.elements;
- let tmp;
-
- tmp = te[ 1 ]; te[ 1 ] = te[ 4 ]; te[ 4 ] = tmp;
- tmp = te[ 2 ]; te[ 2 ] = te[ 8 ]; te[ 8 ] = tmp;
- tmp = te[ 6 ]; te[ 6 ] = te[ 9 ]; te[ 9 ] = tmp;
-
- tmp = te[ 3 ]; te[ 3 ] = te[ 12 ]; te[ 12 ] = tmp;
- tmp = te[ 7 ]; te[ 7 ] = te[ 13 ]; te[ 13 ] = tmp;
- tmp = te[ 11 ]; te[ 11 ] = te[ 14 ]; te[ 14 ] = tmp;
-
- return this;
-
- }
-
- /**
- * Sets the position component for this matrix from the given vector,
- * without affecting the rest of the matrix.
- *
- * @param {number|Vector3} x - The x component of the vector or alternatively the vector object.
- * @param {number} y - The y component of the vector.
- * @param {number} z - The z component of the vector.
- * @return {Matrix4} A reference to this matrix.
- */
- setPosition( x, y, z ) {
-
- const te = this.elements;
-
- if ( x.isVector3 ) {
-
- te[ 12 ] = x.x;
- te[ 13 ] = x.y;
- te[ 14 ] = x.z;
-
- } else {
-
- te[ 12 ] = x;
- te[ 13 ] = y;
- te[ 14 ] = z;
-
- }
-
- return this;
-
- }
-
- /**
- * Inverts this matrix, using the [analytic method](https://en.wikipedia.org/wiki/Invertible_matrix#Analytic_solution).
- * You can not invert with a determinant of zero. If you attempt this, the method produces
- * a zero matrix instead.
- *
- * @return {Matrix4} A reference to this matrix.
- */
- invert() {
-
- // based on https://github.com/toji/gl-matrix
- const te = this.elements,
-
- n11 = te[ 0 ], n21 = te[ 1 ], n31 = te[ 2 ], n41 = te[ 3 ],
- n12 = te[ 4 ], n22 = te[ 5 ], n32 = te[ 6 ], n42 = te[ 7 ],
- n13 = te[ 8 ], n23 = te[ 9 ], n33 = te[ 10 ], n43 = te[ 11 ],
- n14 = te[ 12 ], n24 = te[ 13 ], n34 = te[ 14 ], n44 = te[ 15 ],
-
- t1 = n11 * n22 - n21 * n12,
- t2 = n11 * n32 - n31 * n12,
- t3 = n11 * n42 - n41 * n12,
- t4 = n21 * n32 - n31 * n22,
- t5 = n21 * n42 - n41 * n22,
- t6 = n31 * n42 - n41 * n32,
- t7 = n13 * n24 - n23 * n14,
- t8 = n13 * n34 - n33 * n14,
- t9 = n13 * n44 - n43 * n14,
- t10 = n23 * n34 - n33 * n24,
- t11 = n23 * n44 - n43 * n24,
- t12 = n33 * n44 - n43 * n34;
-
- const det = t1 * t12 - t2 * t11 + t3 * t10 + t4 * t9 - t5 * t8 + t6 * t7;
-
- if ( det === 0 ) return this.set( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 );
-
- const detInv = 1 / det;
-
- te[ 0 ] = ( n22 * t12 - n32 * t11 + n42 * t10 ) * detInv;
- te[ 1 ] = ( n31 * t11 - n21 * t12 - n41 * t10 ) * detInv;
- te[ 2 ] = ( n24 * t6 - n34 * t5 + n44 * t4 ) * detInv;
- te[ 3 ] = ( n33 * t5 - n23 * t6 - n43 * t4 ) * detInv;
-
- te[ 4 ] = ( n32 * t9 - n12 * t12 - n42 * t8 ) * detInv;
- te[ 5 ] = ( n11 * t12 - n31 * t9 + n41 * t8 ) * detInv;
- te[ 6 ] = ( n34 * t3 - n14 * t6 - n44 * t2 ) * detInv;
- te[ 7 ] = ( n13 * t6 - n33 * t3 + n43 * t2 ) * detInv;
-
- te[ 8 ] = ( n12 * t11 - n22 * t9 + n42 * t7 ) * detInv;
- te[ 9 ] = ( n21 * t9 - n11 * t11 - n41 * t7 ) * detInv;
- te[ 10 ] = ( n14 * t5 - n24 * t3 + n44 * t1 ) * detInv;
- te[ 11 ] = ( n23 * t3 - n13 * t5 - n43 * t1 ) * detInv;
-
- te[ 12 ] = ( n22 * t8 - n12 * t10 - n32 * t7 ) * detInv;
- te[ 13 ] = ( n11 * t10 - n21 * t8 + n31 * t7 ) * detInv;
- te[ 14 ] = ( n24 * t2 - n14 * t4 - n34 * t1 ) * detInv;
- te[ 15 ] = ( n13 * t4 - n23 * t2 + n33 * t1 ) * detInv;
-
- return this;
-
- }
-
- /**
- * Scales each of the first three columns of this matrix by the corresponding component of the given vector.
- *
- * @param {Vector3} v - The scale vector.
- * @return {Matrix4} A reference to this matrix.
- */
- scale( v ) {
-
- const te = this.elements;
- const x = v.x, y = v.y, z = v.z;
-
- te[ 0 ] *= x; te[ 4 ] *= y; te[ 8 ] *= z;
- te[ 1 ] *= x; te[ 5 ] *= y; te[ 9 ] *= z;
- te[ 2 ] *= x; te[ 6 ] *= y; te[ 10 ] *= z;
- te[ 3 ] *= x; te[ 7 ] *= y; te[ 11 ] *= z;
-
- return this;
-
- }
-
- /**
- * Gets the maximum scale value of the three axes.
- *
- * @return {number} The maximum scale.
- */
- getMaxScaleOnAxis() {
-
- const te = this.elements;
-
- const scaleXSq = te[ 0 ] * te[ 0 ] + te[ 1 ] * te[ 1 ] + te[ 2 ] * te[ 2 ];
- const scaleYSq = te[ 4 ] * te[ 4 ] + te[ 5 ] * te[ 5 ] + te[ 6 ] * te[ 6 ];
- const scaleZSq = te[ 8 ] * te[ 8 ] + te[ 9 ] * te[ 9 ] + te[ 10 ] * te[ 10 ];
-
- return Math.sqrt( Math.max( scaleXSq, scaleYSq, scaleZSq ) );
-
- }
-
- /**
- * Sets this matrix as a translation transform from the given vector.
- *
- * @param {number|Vector3} x - The amount to translate in the X axis or alternatively a translation vector.
- * @param {number} y - The amount to translate in the Y axis.
- * @param {number} z - The amount to translate in the z axis.
- * @return {Matrix4} A reference to this matrix.
- */
- makeTranslation( x, y, z ) {
-
- if ( x.isVector3 ) {
-
- this.set(
-
- 1, 0, 0, x.x,
- 0, 1, 0, x.y,
- 0, 0, 1, x.z,
- 0, 0, 0, 1
-
- );
-
- } else {
-
- this.set(
-
- 1, 0, 0, x,
- 0, 1, 0, y,
- 0, 0, 1, z,
- 0, 0, 0, 1
-
- );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets this matrix as a rotational transformation around the X axis by
- * the given angle.
- *
- * @param {number} theta - The rotation in radians.
- * @return {Matrix4} A reference to this matrix.
- */
- makeRotationX( theta ) {
-
- const c = Math.cos( theta ), s = Math.sin( theta );
-
- this.set(
-
- 1, 0, 0, 0,
- 0, c, - s, 0,
- 0, s, c, 0,
- 0, 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Sets this matrix as a rotational transformation around the Y axis by
- * the given angle.
- *
- * @param {number} theta - The rotation in radians.
- * @return {Matrix4} A reference to this matrix.
- */
- makeRotationY( theta ) {
-
- const c = Math.cos( theta ), s = Math.sin( theta );
-
- this.set(
-
- c, 0, s, 0,
- 0, 1, 0, 0,
- - s, 0, c, 0,
- 0, 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Sets this matrix as a rotational transformation around the Z axis by
- * the given angle.
- *
- * @param {number} theta - The rotation in radians.
- * @return {Matrix4} A reference to this matrix.
- */
- makeRotationZ( theta ) {
-
- const c = Math.cos( theta ), s = Math.sin( theta );
-
- this.set(
-
- c, - s, 0, 0,
- s, c, 0, 0,
- 0, 0, 1, 0,
- 0, 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Sets this matrix as a rotational transformation around the given axis by
- * the given angle.
- *
- * @param {Vector3} axis - The normalized rotation axis.
- * @param {number} angle - The rotation in radians.
- * @return {Matrix4} A reference to this matrix.
- */
- makeRotationAxis( axis, angle ) {
-
- const c = Math.cos( angle );
- const s = Math.sin( angle );
- const t = 1 - c;
- const x = axis.x, y = axis.y, z = axis.z;
- const tx = t * x, ty = t * y;
-
- this.set(
-
- tx * x + c, tx * y - s * z, tx * z + s * y, 0,
- tx * y + s * z, ty * y + c, ty * z - s * x, 0,
- tx * z - s * y, ty * z + s * x, t * z * z + c, 0,
- 0, 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Sets this matrix as a scale transformation.
- *
- * @param {number} x - The amount to scale in the X axis.
- * @param {number} y - The amount to scale in the Y axis.
- * @param {number} z - The amount to scale in the Z axis.
- * @return {Matrix4} A reference to this matrix.
- */
- makeScale( x, y, z ) {
-
- this.set(
-
- x, 0, 0, 0,
- 0, y, 0, 0,
- 0, 0, z, 0,
- 0, 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Sets this matrix as a shear transformation.
- *
- * @param {number} xy - The amount to shear X by Y.
- * @param {number} xz - The amount to shear X by Z.
- * @param {number} yx - The amount to shear Y by X.
- * @param {number} yz - The amount to shear Y by Z.
- * @param {number} zx - The amount to shear Z by X.
- * @param {number} zy - The amount to shear Z by Y.
- * @return {Matrix4} A reference to this matrix.
- */
- makeShear( xy, xz, yx, yz, zx, zy ) {
-
- this.set(
-
- 1, yx, zx, 0,
- xy, 1, zy, 0,
- xz, yz, 1, 0,
- 0, 0, 0, 1
-
- );
-
- return this;
-
- }
-
- /**
- * Sets this matrix to the transformation composed of the given position,
- * rotation (Quaternion) and scale.
- *
- * @param {Vector3} position - The position vector.
- * @param {Quaternion} quaternion - The rotation as a Quaternion.
- * @param {Vector3} scale - The scale vector.
- * @return {Matrix4} A reference to this matrix.
- */
- compose( position, quaternion, scale ) {
-
- const te = this.elements;
-
- const x = quaternion._x, y = quaternion._y, z = quaternion._z, w = quaternion._w;
- const x2 = x + x, y2 = y + y, z2 = z + z;
- const xx = x * x2, xy = x * y2, xz = x * z2;
- const yy = y * y2, yz = y * z2, zz = z * z2;
- const wx = w * x2, wy = w * y2, wz = w * z2;
-
- const sx = scale.x, sy = scale.y, sz = scale.z;
-
- te[ 0 ] = ( 1 - ( yy + zz ) ) * sx;
- te[ 1 ] = ( xy + wz ) * sx;
- te[ 2 ] = ( xz - wy ) * sx;
- te[ 3 ] = 0;
-
- te[ 4 ] = ( xy - wz ) * sy;
- te[ 5 ] = ( 1 - ( xx + zz ) ) * sy;
- te[ 6 ] = ( yz + wx ) * sy;
- te[ 7 ] = 0;
-
- te[ 8 ] = ( xz + wy ) * sz;
- te[ 9 ] = ( yz - wx ) * sz;
- te[ 10 ] = ( 1 - ( xx + yy ) ) * sz;
- te[ 11 ] = 0;
-
- te[ 12 ] = position.x;
- te[ 13 ] = position.y;
- te[ 14 ] = position.z;
- te[ 15 ] = 1;
-
- return this;
-
- }
-
- /**
- * Decomposes this matrix into its position, rotation and scale components
- * and provides the result in the given objects.
- *
- * Note: Not all matrices are decomposable in this way. For example, if an
- * object has a non-uniformly scaled parent, then the object's world matrix
- * may not be decomposable, and this method may not be appropriate.
- *
- * @param {Vector3} position - The position vector.
- * @param {Quaternion} quaternion - The rotation as a Quaternion.
- * @param {Vector3} scale - The scale vector.
- * @return {Matrix4} A reference to this matrix.
- */
- decompose( position, quaternion, scale ) {
-
- const te = this.elements;
-
- position.x = te[ 12 ];
- position.y = te[ 13 ];
- position.z = te[ 14 ];
-
- const det = this.determinantAffine();
-
- if ( det === 0 ) {
-
- scale.set( 1, 1, 1 );
- quaternion.identity();
-
- return this;
-
- }
-
- let sx = _v1$7.set( te[ 0 ], te[ 1 ], te[ 2 ] ).length();
- const sy = _v1$7.set( te[ 4 ], te[ 5 ], te[ 6 ] ).length();
- const sz = _v1$7.set( te[ 8 ], te[ 9 ], te[ 10 ] ).length();
-
- // if determinant is negative, we need to invert one scale
- if ( det < 0 ) sx = - sx;
-
- // scale the rotation part
- _m1$2.copy( this );
-
- const invSX = 1 / sx;
- const invSY = 1 / sy;
- const invSZ = 1 / sz;
-
- _m1$2.elements[ 0 ] *= invSX;
- _m1$2.elements[ 1 ] *= invSX;
- _m1$2.elements[ 2 ] *= invSX;
-
- _m1$2.elements[ 4 ] *= invSY;
- _m1$2.elements[ 5 ] *= invSY;
- _m1$2.elements[ 6 ] *= invSY;
-
- _m1$2.elements[ 8 ] *= invSZ;
- _m1$2.elements[ 9 ] *= invSZ;
- _m1$2.elements[ 10 ] *= invSZ;
-
- quaternion.setFromRotationMatrix( _m1$2 );
-
- scale.x = sx;
- scale.y = sy;
- scale.z = sz;
-
- return this;
-
- }
-
- /**
- * Creates a perspective projection matrix. This is used internally by
- * {@link PerspectiveCamera#updateProjectionMatrix}.
-
- * @param {number} left - Left boundary of the viewing frustum at the near plane.
- * @param {number} right - Right boundary of the viewing frustum at the near plane.
- * @param {number} top - Top boundary of the viewing frustum at the near plane.
- * @param {number} bottom - Bottom boundary of the viewing frustum at the near plane.
- * @param {number} near - The distance from the camera to the near plane.
- * @param {number} far - The distance from the camera to the far plane.
- * @param {(WebGLCoordinateSystem|WebGPUCoordinateSystem)} [coordinateSystem=WebGLCoordinateSystem] - The coordinate system.
- * @param {boolean} [reversedDepth=false] - Whether to use a reversed depth.
- * @return {Matrix4} A reference to this matrix.
- */
- makePerspective( left, right, top, bottom, near, far, coordinateSystem = WebGLCoordinateSystem, reversedDepth = false ) {
-
- const te = this.elements;
-
- const x = 2 * near / ( right - left );
- const y = 2 * near / ( top - bottom );
-
- const a = ( right + left ) / ( right - left );
- const b = ( top + bottom ) / ( top - bottom );
-
- let c, d;
-
- if ( reversedDepth ) {
-
- c = near / ( far - near );
- d = ( far * near ) / ( far - near );
-
- } else {
-
- if ( coordinateSystem === WebGLCoordinateSystem ) {
-
- c = - ( far + near ) / ( far - near );
- d = ( -2 * far * near ) / ( far - near );
-
- } else if ( coordinateSystem === WebGPUCoordinateSystem ) {
-
- c = - far / ( far - near );
- d = ( - far * near ) / ( far - near );
-
- } else {
-
- throw new Error( 'THREE.Matrix4.makePerspective(): Invalid coordinate system: ' + coordinateSystem );
-
- }
-
- }
-
- te[ 0 ] = x; te[ 4 ] = 0; te[ 8 ] = a; te[ 12 ] = 0;
- te[ 1 ] = 0; te[ 5 ] = y; te[ 9 ] = b; te[ 13 ] = 0;
- te[ 2 ] = 0; te[ 6 ] = 0; te[ 10 ] = c; te[ 14 ] = d;
- te[ 3 ] = 0; te[ 7 ] = 0; te[ 11 ] = -1; te[ 15 ] = 0;
-
- return this;
-
- }
-
- /**
- * Creates a orthographic projection matrix. This is used internally by
- * {@link OrthographicCamera#updateProjectionMatrix}.
-
- * @param {number} left - Left boundary of the viewing frustum at the near plane.
- * @param {number} right - Right boundary of the viewing frustum at the near plane.
- * @param {number} top - Top boundary of the viewing frustum at the near plane.
- * @param {number} bottom - Bottom boundary of the viewing frustum at the near plane.
- * @param {number} near - The distance from the camera to the near plane.
- * @param {number} far - The distance from the camera to the far plane.
- * @param {(WebGLCoordinateSystem|WebGPUCoordinateSystem)} [coordinateSystem=WebGLCoordinateSystem] - The coordinate system.
- * @param {boolean} [reversedDepth=false] - Whether to use a reversed depth.
- * @return {Matrix4} A reference to this matrix.
- */
- makeOrthographic( left, right, top, bottom, near, far, coordinateSystem = WebGLCoordinateSystem, reversedDepth = false ) {
-
- const te = this.elements;
-
- const x = 2 / ( right - left );
- const y = 2 / ( top - bottom );
-
- const a = - ( right + left ) / ( right - left );
- const b = - ( top + bottom ) / ( top - bottom );
-
- let c, d;
-
- if ( reversedDepth ) {
-
- c = 1 / ( far - near );
- d = far / ( far - near );
-
- } else {
-
- if ( coordinateSystem === WebGLCoordinateSystem ) {
-
- c = -2 / ( far - near );
- d = - ( far + near ) / ( far - near );
-
- } else if ( coordinateSystem === WebGPUCoordinateSystem ) {
-
- c = -1 / ( far - near );
- d = - near / ( far - near );
-
- } else {
-
- throw new Error( 'THREE.Matrix4.makeOrthographic(): Invalid coordinate system: ' + coordinateSystem );
-
- }
-
- }
-
- te[ 0 ] = x; te[ 4 ] = 0; te[ 8 ] = 0; te[ 12 ] = a;
- te[ 1 ] = 0; te[ 5 ] = y; te[ 9 ] = 0; te[ 13 ] = b;
- te[ 2 ] = 0; te[ 6 ] = 0; te[ 10 ] = c; te[ 14 ] = d;
- te[ 3 ] = 0; te[ 7 ] = 0; te[ 11 ] = 0; te[ 15 ] = 1;
-
- return this;
-
- }
-
- /**
- * Returns `true` if this matrix is equal with the given one.
- *
- * @param {Matrix4} matrix - The matrix to test for equality.
- * @return {boolean} Whether this matrix is equal with the given one.
- */
- equals( matrix ) {
-
- const te = this.elements;
- const me = matrix.elements;
-
- for ( let i = 0; i < 16; i ++ ) {
-
- if ( te[ i ] !== me[ i ] ) return false;
-
- }
-
- return true;
-
- }
-
- /**
- * Sets the elements of the matrix from the given array.
- *
- * @param {Array} array - The matrix elements in column-major order.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Matrix4} A reference to this matrix.
- */
- fromArray( array, offset = 0 ) {
-
- for ( let i = 0; i < 16; i ++ ) {
-
- this.elements[ i ] = array[ i + offset ];
-
- }
-
- return this;
-
- }
-
- /**
- * Writes the elements of this matrix to the given array. If no array is provided,
- * the method returns a new instance.
- *
- * @param {Array} [array=[]] - The target array holding the matrix elements in column-major order.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Array} The matrix elements in column-major order.
- */
- toArray( array = [], offset = 0 ) {
-
- const te = this.elements;
-
- array[ offset ] = te[ 0 ];
- array[ offset + 1 ] = te[ 1 ];
- array[ offset + 2 ] = te[ 2 ];
- array[ offset + 3 ] = te[ 3 ];
-
- array[ offset + 4 ] = te[ 4 ];
- array[ offset + 5 ] = te[ 5 ];
- array[ offset + 6 ] = te[ 6 ];
- array[ offset + 7 ] = te[ 7 ];
-
- array[ offset + 8 ] = te[ 8 ];
- array[ offset + 9 ] = te[ 9 ];
- array[ offset + 10 ] = te[ 10 ];
- array[ offset + 11 ] = te[ 11 ];
-
- array[ offset + 12 ] = te[ 12 ];
- array[ offset + 13 ] = te[ 13 ];
- array[ offset + 14 ] = te[ 14 ];
- array[ offset + 15 ] = te[ 15 ];
-
- return array;
-
- }
-
-}
-
-const _v1$7 = /*@__PURE__*/ new Vector3();
-const _m1$2 = /*@__PURE__*/ new Matrix4();
-const _zero = /*@__PURE__*/ new Vector3( 0, 0, 0 );
-const _one = /*@__PURE__*/ new Vector3( 1, 1, 1 );
-const _x = /*@__PURE__*/ new Vector3();
-const _y = /*@__PURE__*/ new Vector3();
-const _z = /*@__PURE__*/ new Vector3();
-
-const _matrix$2 = /*@__PURE__*/ new Matrix4();
-const _quaternion$4 = /*@__PURE__*/ new Quaternion();
-
-/**
- * A class representing Euler angles.
- *
- * Euler angles describe a rotational transformation by rotating an object on
- * its various axes in specified amounts per axis, and a specified axis
- * order.
- *
- * Iterating through an instance will yield its components (x, y, z,
- * order) in the corresponding order.
- *
- * ```js
- * const a = new THREE.Euler( 0, 1, 1.57, 'XYZ' );
- * const b = new THREE.Vector3( 1, 0, 1 );
- * b.applyEuler(a);
- * ```
- */
-class Euler {
-
- /**
- * Constructs a new euler instance.
- *
- * @param {number} [x=0] - The angle of the x axis in radians.
- * @param {number} [y=0] - The angle of the y axis in radians.
- * @param {number} [z=0] - The angle of the z axis in radians.
- * @param {string} [order=Euler.DEFAULT_ORDER] - A string representing the order that the rotations are applied.
- */
- constructor( x = 0, y = 0, z = 0, order = Euler.DEFAULT_ORDER ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isEuler = true;
-
- this._x = x;
- this._y = y;
- this._z = z;
- this._order = order;
-
- }
-
- /**
- * The angle of the x axis in radians.
- *
- * @type {number}
- * @default 0
- */
- get x() {
-
- return this._x;
-
- }
-
- set x( value ) {
-
- this._x = value;
- this._onChangeCallback();
-
- }
-
- /**
- * The angle of the y axis in radians.
- *
- * @type {number}
- * @default 0
- */
- get y() {
-
- return this._y;
-
- }
-
- set y( value ) {
-
- this._y = value;
- this._onChangeCallback();
-
- }
-
- /**
- * The angle of the z axis in radians.
- *
- * @type {number}
- * @default 0
- */
- get z() {
-
- return this._z;
-
- }
-
- set z( value ) {
-
- this._z = value;
- this._onChangeCallback();
-
- }
-
- /**
- * A string representing the order that the rotations are applied.
- *
- * @type {string}
- * @default 'XYZ'
- */
- get order() {
-
- return this._order;
-
- }
-
- set order( value ) {
-
- this._order = value;
- this._onChangeCallback();
-
- }
-
- /**
- * Sets the Euler components.
- *
- * @param {number} x - The angle of the x axis in radians.
- * @param {number} y - The angle of the y axis in radians.
- * @param {number} z - The angle of the z axis in radians.
- * @param {string} [order] - A string representing the order that the rotations are applied.
- * @return {Euler} A reference to this Euler instance.
- */
- set( x, y, z, order = this._order ) {
-
- this._x = x;
- this._y = y;
- this._z = z;
- this._order = order;
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Returns a new Euler instance with copied values from this instance.
- *
- * @return {Euler} A clone of this instance.
- */
- clone() {
-
- return new this.constructor( this._x, this._y, this._z, this._order );
-
- }
-
- /**
- * Copies the values of the given Euler instance to this instance.
- *
- * @param {Euler} euler - The Euler instance to copy.
- * @return {Euler} A reference to this Euler instance.
- */
- copy( euler ) {
-
- this._x = euler._x;
- this._y = euler._y;
- this._z = euler._z;
- this._order = euler._order;
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Sets the angles of this Euler instance from a pure rotation matrix.
- *
- * @param {Matrix4} m - A 4x4 matrix of which the upper 3x3 of matrix is a pure rotation matrix (i.e. unscaled).
- * @param {string} [order] - A string representing the order that the rotations are applied.
- * @param {boolean} [update=true] - Whether the internal `onChange` callback should be executed or not.
- * @return {Euler} A reference to this Euler instance.
- */
- setFromRotationMatrix( m, order = this._order, update = true ) {
-
- const te = m.elements;
- const m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ];
- const m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ];
- const m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ];
-
- switch ( order ) {
-
- case 'XYZ':
-
- this._y = Math.asin( clamp( m13, -1, 1 ) );
-
- if ( Math.abs( m13 ) < 0.9999999 ) {
-
- this._x = Math.atan2( - m23, m33 );
- this._z = Math.atan2( - m12, m11 );
-
- } else {
-
- this._x = Math.atan2( m32, m22 );
- this._z = 0;
-
- }
-
- break;
-
- case 'YXZ':
-
- this._x = Math.asin( - clamp( m23, -1, 1 ) );
-
- if ( Math.abs( m23 ) < 0.9999999 ) {
-
- this._y = Math.atan2( m13, m33 );
- this._z = Math.atan2( m21, m22 );
-
- } else {
-
- this._y = Math.atan2( - m31, m11 );
- this._z = 0;
-
- }
-
- break;
-
- case 'ZXY':
-
- this._x = Math.asin( clamp( m32, -1, 1 ) );
-
- if ( Math.abs( m32 ) < 0.9999999 ) {
-
- this._y = Math.atan2( - m31, m33 );
- this._z = Math.atan2( - m12, m22 );
-
- } else {
-
- this._y = 0;
- this._z = Math.atan2( m21, m11 );
-
- }
-
- break;
-
- case 'ZYX':
-
- this._y = Math.asin( - clamp( m31, -1, 1 ) );
-
- if ( Math.abs( m31 ) < 0.9999999 ) {
-
- this._x = Math.atan2( m32, m33 );
- this._z = Math.atan2( m21, m11 );
-
- } else {
-
- this._x = 0;
- this._z = Math.atan2( - m12, m22 );
-
- }
-
- break;
-
- case 'YZX':
-
- this._z = Math.asin( clamp( m21, -1, 1 ) );
-
- if ( Math.abs( m21 ) < 0.9999999 ) {
-
- this._x = Math.atan2( - m23, m22 );
- this._y = Math.atan2( - m31, m11 );
-
- } else {
-
- this._x = 0;
- this._y = Math.atan2( m13, m33 );
-
- }
-
- break;
-
- case 'XZY':
-
- this._z = Math.asin( - clamp( m12, -1, 1 ) );
-
- if ( Math.abs( m12 ) < 0.9999999 ) {
-
- this._x = Math.atan2( m32, m22 );
- this._y = Math.atan2( m13, m11 );
-
- } else {
-
- this._x = Math.atan2( - m23, m33 );
- this._y = 0;
-
- }
-
- break;
-
- default:
-
- warn( 'Euler: .setFromRotationMatrix() encountered an unknown order: ' + order );
-
- }
-
- this._order = order;
-
- if ( update === true ) this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Sets the angles of this Euler instance from a normalized quaternion.
- *
- * @param {Quaternion} q - A normalized Quaternion.
- * @param {string} [order] - A string representing the order that the rotations are applied.
- * @param {boolean} [update=true] - Whether the internal `onChange` callback should be executed or not.
- * @return {Euler} A reference to this Euler instance.
- */
- setFromQuaternion( q, order, update ) {
-
- _matrix$2.makeRotationFromQuaternion( q );
-
- return this.setFromRotationMatrix( _matrix$2, order, update );
-
- }
-
- /**
- * Sets the angles of this Euler instance from the given vector.
- *
- * @param {Vector3} v - The vector.
- * @param {string} [order] - A string representing the order that the rotations are applied.
- * @return {Euler} A reference to this Euler instance.
- */
- setFromVector3( v, order = this._order ) {
-
- return this.set( v.x, v.y, v.z, order );
-
- }
-
- /**
- * Resets the euler angle with a new order by creating a quaternion from this
- * euler angle and then setting this euler angle with the quaternion and the
- * new order.
- *
- * Warning: This discards revolution information.
- *
- * @param {string} [newOrder] - A string representing the new order that the rotations are applied.
- * @return {Euler} A reference to this Euler instance.
- */
- reorder( newOrder ) {
-
- _quaternion$4.setFromEuler( this );
-
- return this.setFromQuaternion( _quaternion$4, newOrder );
-
- }
-
- /**
- * Returns `true` if this Euler instance is equal with the given one.
- *
- * @param {Euler} euler - The Euler instance to test for equality.
- * @return {boolean} Whether this Euler instance is equal with the given one.
- */
- equals( euler ) {
-
- return ( euler._x === this._x ) && ( euler._y === this._y ) && ( euler._z === this._z ) && ( euler._order === this._order );
-
- }
-
- /**
- * Sets this Euler instance's components to values from the given array. The first three
- * entries of the array are assign to the x,y and z components. An optional fourth entry
- * defines the Euler order.
- *
- * @param {Array} array - An array holding the Euler component values.
- * @return {Euler} A reference to this Euler instance.
- */
- fromArray( array ) {
-
- this._x = array[ 0 ];
- this._y = array[ 1 ];
- this._z = array[ 2 ];
- if ( array[ 3 ] !== undefined ) this._order = array[ 3 ];
-
- this._onChangeCallback();
-
- return this;
-
- }
-
- /**
- * Writes the components of this Euler instance to the given array. If no array is provided,
- * the method returns a new instance.
- *
- * @param {Array} [array=[]] - The target array holding the Euler components.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Array} The Euler components.
- */
- toArray( array = [], offset = 0 ) {
-
- array[ offset ] = this._x;
- array[ offset + 1 ] = this._y;
- array[ offset + 2 ] = this._z;
- array[ offset + 3 ] = this._order;
-
- return array;
-
- }
-
- _onChange( callback ) {
-
- this._onChangeCallback = callback;
-
- return this;
-
- }
-
- _onChangeCallback() {}
-
- *[ Symbol.iterator ]() {
-
- yield this._x;
- yield this._y;
- yield this._z;
- yield this._order;
-
- }
-
-}
-
-/**
- * The default Euler angle order.
- *
- * @static
- * @type {string}
- * @default 'XYZ'
- */
-Euler.DEFAULT_ORDER = 'XYZ';
-
-/**
- * A layers object assigns an 3D object to 1 or more of 32
- * layers numbered `0` to `31` - internally the layers are stored as a
- * bit mask], and by default all 3D objects are a member of layer `0`.
- *
- * This can be used to control visibility - an object must share a layer with
- * a camera to be visible when that camera's view is
- * rendered.
- *
- * All classes that inherit from {@link Object3D} have an `layers` property which
- * is an instance of this class.
- */
-class Layers {
-
- /**
- * Constructs a new layers instance, with membership
- * initially set to layer `0`.
- */
- constructor() {
-
- /**
- * A bit mask storing which of the 32 layers this layers object is currently
- * a member of.
- *
- * @type {number}
- */
- this.mask = 1 | 0;
-
- }
-
- /**
- * Sets membership to the given layer, and remove membership all other layers.
- *
- * @param {number} layer - The layer to set.
- */
- set( layer ) {
-
- this.mask = ( 1 << layer | 0 ) >>> 0;
-
- }
-
- /**
- * Adds membership of the given layer.
- *
- * @param {number} layer - The layer to enable.
- */
- enable( layer ) {
-
- this.mask |= 1 << layer | 0;
-
- }
-
- /**
- * Adds membership to all layers.
- */
- enableAll() {
-
- this.mask = 0xffffffff | 0;
-
- }
-
- /**
- * Toggles the membership of the given layer.
- *
- * @param {number} layer - The layer to toggle.
- */
- toggle( layer ) {
-
- this.mask ^= 1 << layer | 0;
-
- }
-
- /**
- * Removes membership of the given layer.
- *
- * @param {number} layer - The layer to enable.
- */
- disable( layer ) {
-
- this.mask &= ~ ( 1 << layer | 0 );
-
- }
-
- /**
- * Removes the membership from all layers.
- */
- disableAll() {
-
- this.mask = 0;
-
- }
-
- /**
- * Returns `true` if this and the given layers object have at least one
- * layer in common.
- *
- * @param {Layers} layers - The layers to test.
- * @return {boolean } Whether this and the given layers object have at least one layer in common or not.
- */
- test( layers ) {
-
- return ( this.mask & layers.mask ) !== 0;
-
- }
-
- /**
- * Returns `true` if the given layer is enabled.
- *
- * @param {number} layer - The layer to test.
- * @return {boolean } Whether the given layer is enabled or not.
- */
- isEnabled( layer ) {
-
- return ( this.mask & ( 1 << layer | 0 ) ) !== 0;
-
- }
-
-}
-
-let _object3DId = 0;
-
-const _v1$6 = /*@__PURE__*/ new Vector3();
-const _q1 = /*@__PURE__*/ new Quaternion();
-const _m1$1 = /*@__PURE__*/ new Matrix4();
-const _target = /*@__PURE__*/ new Vector3();
-
-const _position$4 = /*@__PURE__*/ new Vector3();
-const _scale$3 = /*@__PURE__*/ new Vector3();
-const _quaternion$3 = /*@__PURE__*/ new Quaternion();
-
-const _xAxis = /*@__PURE__*/ new Vector3( 1, 0, 0 );
-const _yAxis = /*@__PURE__*/ new Vector3( 0, 1, 0 );
-const _zAxis = /*@__PURE__*/ new Vector3( 0, 0, 1 );
-
-/**
- * Fires when the object has been added to its parent object.
- *
- * @event Object3D#added
- * @type {Object}
- */
-const _addedEvent = { type: 'added' };
-
-/**
- * Fires when the object has been removed from its parent object.
- *
- * @event Object3D#removed
- * @type {Object}
- */
-const _removedEvent = { type: 'removed' };
-
-/**
- * Fires when a new child object has been added.
- *
- * @event Object3D#childadded
- * @type {Object}
- */
-const _childaddedEvent = { type: 'childadded', child: null };
-
-/**
- * Fires when a child object has been removed.
- *
- * @event Object3D#childremoved
- * @type {Object}
- */
-const _childremovedEvent = { type: 'childremoved', child: null };
-
-/**
- * This is the base class for most objects in three.js and provides a set of
- * properties and methods for manipulating objects in 3D space.
- *
- * @augments EventDispatcher
- */
-class Object3D extends EventDispatcher {
-
- /**
- * Constructs a new 3D object.
- */
- constructor() {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isObject3D = true;
-
- /**
- * The ID of the 3D object.
- *
- * @name Object3D#id
- * @type {number}
- * @readonly
- */
- Object.defineProperty( this, 'id', { value: _object3DId ++ } );
-
- /**
- * The UUID of the 3D object.
- *
- * @type {string}
- * @readonly
- */
- this.uuid = generateUUID();
-
- /**
- * The name of the 3D object.
- *
- * @type {string}
- */
- this.name = '';
-
- /**
- * The type property is used for detecting the object type
- * in context of serialization/deserialization.
- *
- * @type {string}
- * @readonly
- */
- this.type = 'Object3D';
-
- /**
- * A reference to the parent object.
- *
- * @type {?Object3D}
- * @default null
- */
- this.parent = null;
-
- /**
- * An array holding the child 3D objects of this instance.
- *
- * @type {Array}
- */
- this.children = [];
-
- /**
- * Defines the `up` direction of the 3D object which influences
- * the orientation via methods like {@link Object3D#lookAt}.
- *
- * The default values for all 3D objects is defined by `Object3D.DEFAULT_UP`.
- *
- * @type {Vector3}
- */
- this.up = Object3D.DEFAULT_UP.clone();
-
- const position = new Vector3();
- const rotation = new Euler();
- const quaternion = new Quaternion();
- const scale = new Vector3( 1, 1, 1 );
-
- function onRotationChange() {
-
- quaternion.setFromEuler( rotation, false );
-
- }
-
- function onQuaternionChange() {
-
- rotation.setFromQuaternion( quaternion, undefined, false );
-
- }
-
- rotation._onChange( onRotationChange );
- quaternion._onChange( onQuaternionChange );
-
- Object.defineProperties( this, {
- /**
- * Represents the object's local position.
- *
- * @name Object3D#position
- * @type {Vector3}
- * @default (0,0,0)
- */
- position: {
- configurable: true,
- enumerable: true,
- value: position
- },
- /**
- * Represents the object's local rotation as Euler angles, in radians.
- *
- * @name Object3D#rotation
- * @type {Euler}
- * @default (0,0,0)
- */
- rotation: {
- configurable: true,
- enumerable: true,
- value: rotation
- },
- /**
- * Represents the object's local rotation as Quaternions.
- *
- * @name Object3D#quaternion
- * @type {Quaternion}
- */
- quaternion: {
- configurable: true,
- enumerable: true,
- value: quaternion
- },
- /**
- * Represents the object's local scale.
- *
- * @name Object3D#scale
- * @type {Vector3}
- * @default (1,1,1)
- */
- scale: {
- configurable: true,
- enumerable: true,
- value: scale
- },
- /**
- * Represents the object's model-view matrix.
- *
- * @name Object3D#modelViewMatrix
- * @type {Matrix4}
- */
- modelViewMatrix: {
- value: new Matrix4()
- },
- /**
- * Represents the object's normal matrix.
- *
- * @name Object3D#normalMatrix
- * @type {Matrix3}
- */
- normalMatrix: {
- value: new Matrix3()
- }
- } );
-
- /**
- * Represents the object's transformation matrix in local space.
- *
- * @type {Matrix4}
- */
- this.matrix = new Matrix4();
-
- /**
- * Represents the object's transformation matrix in world space.
- * If the 3D object has no parent, then it's identical to the local transformation matrix
- *
- * @type {Matrix4}
- */
- this.matrixWorld = new Matrix4();
-
- /**
- * When set to `true`, the engine automatically computes the local matrix from position,
- * rotation and scale every frame. If set to `false`, the app is responsible for recomputing
- * the local matrix by calling `updateMatrix()`.
- *
- * The default values for all 3D objects is defined by `Object3D.DEFAULT_MATRIX_AUTO_UPDATE`.
- *
- * @type {boolean}
- * @default true
- */
- this.matrixAutoUpdate = Object3D.DEFAULT_MATRIX_AUTO_UPDATE;
-
- /**
- * When set to `true`, the engine automatically computes the world matrix from the current local
- * matrix and the object's transformation hierarchy. If set to `false`, the app is responsible for
- * recomputing the world matrix by directly updating the `matrixWorld` property.
- *
- * The default values for all 3D objects is defined by `Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE`.
- *
- * @type {boolean}
- * @default true
- */
- this.matrixWorldAutoUpdate = Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE; // checked by the renderer
-
- /**
- * When set to `true`, it calculates the world matrix in that frame and resets this property
- * to `false`.
- *
- * @type {boolean}
- * @default false
- */
- this.matrixWorldNeedsUpdate = false;
-
- /**
- * The layer membership of the 3D object. The 3D object is only visible if it has
- * at least one layer in common with the camera in use. This property can also be
- * used to filter out unwanted objects in ray-intersection tests when using {@link Raycaster}.
- *
- * @type {Layers}
- */
- this.layers = new Layers();
-
- /**
- * When set to `true`, the 3D object gets rendered.
- *
- * @type {boolean}
- * @default true
- */
- this.visible = true;
-
- /**
- * When set to `true`, the 3D object gets rendered into shadow maps.
- *
- * @type {boolean}
- * @default false
- */
- this.castShadow = false;
-
- /**
- * When set to `true`, the 3D object is affected by shadows in the scene.
- *
- * @type {boolean}
- * @default false
- */
- this.receiveShadow = false;
-
- /**
- * When set to `true`, the 3D object is honored by view frustum culling.
- *
- * @type {boolean}
- * @default true
- */
- this.frustumCulled = true;
-
- /**
- * This value allows the default rendering order of scene graph objects to be
- * overridden although opaque and transparent objects remain sorted independently.
- * When this property is set for an instance of {@link Group},all descendants
- * objects will be sorted and rendered together. Sorting is from lowest to highest
- * render order.
- *
- * @type {number}
- * @default 0
- */
- this.renderOrder = 0;
-
- /**
- * An array holding the animation clips of the 3D object.
- *
- * @type {Array}
- */
- this.animations = [];
-
- /**
- * Custom depth material to be used when rendering to the depth map. Can only be used
- * in context of meshes. When shadow-casting with a {@link DirectionalLight} or {@link SpotLight},
- * if you are modifying vertex positions in the vertex shader you must specify a custom depth
- * material for proper shadows.
- *
- * Only relevant in context of {@link WebGLRenderer}.
- *
- * @type {(Material|undefined)}
- * @default undefined
- */
- this.customDepthMaterial = undefined;
-
- /**
- * Same as {@link Object3D#customDepthMaterial}, but used with {@link PointLight}.
- *
- * Only relevant in context of {@link WebGLRenderer}.
- *
- * @type {(Material|undefined)}
- * @default undefined
- */
- this.customDistanceMaterial = undefined;
-
- /**
- * Whether the 3D object is supposed to be static or not. If set to `true`, it means
- * the 3D object is not going to be changed after the initial renderer. This includes
- * geometry and material settings. A static 3D object can be processed by the renderer
- * slightly faster since certain state checks can be bypassed.
- *
- * Only relevant in context of {@link WebGPURenderer}.
- *
- * @type {boolean}
- * @default false
- */
- this.static = false;
-
- /**
- * An object that can be used to store custom data about the 3D object. It
- * should not hold references to functions as these will not be cloned.
- *
- * @type {Object}
- */
- this.userData = {};
-
- /**
- * The pivot point for rotation and scale transformations.
- * When set, rotation and scale are applied around this point
- * instead of the object's origin.
- *
- * @type {?Vector3}
- * @default null
- */
- this.pivot = null;
-
- }
-
- /**
- * A callback that is executed immediately before a 3D object is rendered to a shadow map.
- *
- * @param {Renderer|WebGLRenderer} renderer - The renderer.
- * @param {Object3D} object - The 3D object.
- * @param {Camera} camera - The camera that is used to render the scene.
- * @param {Camera} shadowCamera - The shadow camera.
- * @param {BufferGeometry} geometry - The 3D object's geometry.
- * @param {Material} depthMaterial - The depth material.
- * @param {Object} group - The geometry group data.
- */
- onBeforeShadow( /* renderer, object, camera, shadowCamera, geometry, depthMaterial, group */ ) {}
-
- /**
- * A callback that is executed immediately after a 3D object is rendered to a shadow map.
- *
- * @param {Renderer|WebGLRenderer} renderer - The renderer.
- * @param {Object3D} object - The 3D object.
- * @param {Camera} camera - The camera that is used to render the scene.
- * @param {Camera} shadowCamera - The shadow camera.
- * @param {BufferGeometry} geometry - The 3D object's geometry.
- * @param {Material} depthMaterial - The depth material.
- * @param {Object} group - The geometry group data.
- */
- onAfterShadow( /* renderer, object, camera, shadowCamera, geometry, depthMaterial, group */ ) {}
-
- /**
- * A callback that is executed immediately before a 3D object is rendered.
- *
- * @param {Renderer|WebGLRenderer} renderer - The renderer.
- * @param {Object3D} object - The 3D object.
- * @param {Camera} camera - The camera that is used to render the scene.
- * @param {BufferGeometry} geometry - The 3D object's geometry.
- * @param {Material} material - The 3D object's material.
- * @param {Object} group - The geometry group data.
- */
- onBeforeRender( /* renderer, scene, camera, geometry, material, group */ ) {}
-
- /**
- * A callback that is executed immediately after a 3D object is rendered.
- *
- * @param {Renderer|WebGLRenderer} renderer - The renderer.
- * @param {Object3D} object - The 3D object.
- * @param {Camera} camera - The camera that is used to render the scene.
- * @param {BufferGeometry} geometry - The 3D object's geometry.
- * @param {Material} material - The 3D object's material.
- * @param {Object} group - The geometry group data.
- */
- onAfterRender( /* renderer, scene, camera, geometry, material, group */ ) {}
-
- /**
- * Applies the given transformation matrix to the object and updates the object's position,
- * rotation and scale.
- *
- * @param {Matrix4} matrix - The transformation matrix.
- */
- applyMatrix4( matrix ) {
-
- if ( this.matrixAutoUpdate ) this.updateMatrix();
-
- this.matrix.premultiply( matrix );
-
- this.matrix.decompose( this.position, this.quaternion, this.scale );
-
- }
-
- /**
- * Applies a rotation represented by given the quaternion to the 3D object.
- *
- * @param {Quaternion} q - The quaternion.
- * @return {Object3D} A reference to this instance.
- */
- applyQuaternion( q ) {
-
- this.quaternion.premultiply( q );
-
- return this;
-
- }
-
- /**
- * Sets the given rotation represented as an axis/angle couple to the 3D object.
- *
- * @param {Vector3} axis - The (normalized) axis vector.
- * @param {number} angle - The angle in radians.
- */
- setRotationFromAxisAngle( axis, angle ) {
-
- // assumes axis is normalized
-
- this.quaternion.setFromAxisAngle( axis, angle );
-
- }
-
- /**
- * Sets the given rotation represented as Euler angles to the 3D object.
- *
- * @param {Euler} euler - The Euler angles.
- */
- setRotationFromEuler( euler ) {
-
- this.quaternion.setFromEuler( euler, true );
-
- }
-
- /**
- * Sets the given rotation represented as rotation matrix to the 3D object.
- *
- * @param {Matrix4} m - Although a 4x4 matrix is expected, the upper 3x3 portion must be
- * a pure rotation matrix (i.e, unscaled).
- */
- setRotationFromMatrix( m ) {
-
- // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)
-
- this.quaternion.setFromRotationMatrix( m );
-
- }
-
- /**
- * Sets the given rotation represented as a Quaternion to the 3D object.
- *
- * @param {Quaternion} q - The Quaternion
- */
- setRotationFromQuaternion( q ) {
-
- // assumes q is normalized
-
- this.quaternion.copy( q );
-
- }
-
- /**
- * Rotates the 3D object along an axis in local space.
- *
- * @param {Vector3} axis - The (normalized) axis vector.
- * @param {number} angle - The angle in radians.
- * @return {Object3D} A reference to this instance.
- */
- rotateOnAxis( axis, angle ) {
-
- // rotate object on axis in object space
- // axis is assumed to be normalized
-
- _q1.setFromAxisAngle( axis, angle );
-
- this.quaternion.multiply( _q1 );
-
- return this;
-
- }
-
- /**
- * Rotates the 3D object along an axis in world space.
- *
- * @param {Vector3} axis - The (normalized) axis vector.
- * @param {number} angle - The angle in radians.
- * @return {Object3D} A reference to this instance.
- */
- rotateOnWorldAxis( axis, angle ) {
-
- // rotate object on axis in world space
- // axis is assumed to be normalized
- // method assumes no rotated parent
-
- _q1.setFromAxisAngle( axis, angle );
-
- this.quaternion.premultiply( _q1 );
-
- return this;
-
- }
-
- /**
- * Rotates the 3D object around its X axis in local space.
- *
- * @param {number} angle - The angle in radians.
- * @return {Object3D} A reference to this instance.
- */
- rotateX( angle ) {
-
- return this.rotateOnAxis( _xAxis, angle );
-
- }
-
- /**
- * Rotates the 3D object around its Y axis in local space.
- *
- * @param {number} angle - The angle in radians.
- * @return {Object3D} A reference to this instance.
- */
- rotateY( angle ) {
-
- return this.rotateOnAxis( _yAxis, angle );
-
- }
-
- /**
- * Rotates the 3D object around its Z axis in local space.
- *
- * @param {number} angle - The angle in radians.
- * @return {Object3D} A reference to this instance.
- */
- rotateZ( angle ) {
-
- return this.rotateOnAxis( _zAxis, angle );
-
- }
-
- /**
- * Translate the 3D object by a distance along the given axis in local space.
- *
- * @param {Vector3} axis - The (normalized) axis vector.
- * @param {number} distance - The distance in world units.
- * @return {Object3D} A reference to this instance.
- */
- translateOnAxis( axis, distance ) {
-
- // translate object by distance along axis in object space
- // axis is assumed to be normalized
-
- _v1$6.copy( axis ).applyQuaternion( this.quaternion );
-
- this.position.add( _v1$6.multiplyScalar( distance ) );
-
- return this;
-
- }
-
- /**
- * Translate the 3D object by a distance along its X-axis in local space.
- *
- * @param {number} distance - The distance in world units.
- * @return {Object3D} A reference to this instance.
- */
- translateX( distance ) {
-
- return this.translateOnAxis( _xAxis, distance );
-
- }
-
- /**
- * Translate the 3D object by a distance along its Y-axis in local space.
- *
- * @param {number} distance - The distance in world units.
- * @return {Object3D} A reference to this instance.
- */
- translateY( distance ) {
-
- return this.translateOnAxis( _yAxis, distance );
-
- }
-
- /**
- * Translate the 3D object by a distance along its Z-axis in local space.
- *
- * @param {number} distance - The distance in world units.
- * @return {Object3D} A reference to this instance.
- */
- translateZ( distance ) {
-
- return this.translateOnAxis( _zAxis, distance );
-
- }
-
- /**
- * Converts the given vector from this 3D object's local space to world space.
- *
- * @param {Vector3} vector - The vector to convert.
- * @return {Vector3} The converted vector.
- */
- localToWorld( vector ) {
-
- this.updateWorldMatrix( true, false );
-
- return vector.applyMatrix4( this.matrixWorld );
-
- }
-
- /**
- * Converts the given vector from this 3D object's world space to local space.
- *
- * @param {Vector3} vector - The vector to convert.
- * @return {Vector3} The converted vector.
- */
- worldToLocal( vector ) {
-
- this.updateWorldMatrix( true, false );
-
- return vector.applyMatrix4( _m1$1.copy( this.matrixWorld ).invert() );
-
- }
-
- /**
- * Rotates the object to face a point in world space.
- *
- * This method does not support objects having non-uniformly-scaled parent(s).
- *
- * @param {number|Vector3} x - The x coordinate in world space. Alternatively, a vector representing a position in world space
- * @param {number} [y] - The y coordinate in world space.
- * @param {number} [z] - The z coordinate in world space.
- */
- lookAt( x, y, z ) {
-
- // This method does not support objects having non-uniformly-scaled parent(s)
-
- if ( x.isVector3 ) {
-
- _target.copy( x );
-
- } else {
-
- _target.set( x, y, z );
-
- }
-
- const parent = this.parent;
-
- this.updateWorldMatrix( true, false );
-
- _position$4.setFromMatrixPosition( this.matrixWorld );
-
- if ( this.isCamera || this.isLight ) {
-
- _m1$1.lookAt( _position$4, _target, this.up );
-
- } else {
-
- _m1$1.lookAt( _target, _position$4, this.up );
-
- }
-
- this.quaternion.setFromRotationMatrix( _m1$1 );
-
- if ( parent ) {
-
- _m1$1.extractRotation( parent.matrixWorld );
- _q1.setFromRotationMatrix( _m1$1 );
- this.quaternion.premultiply( _q1.invert() );
-
- }
-
- }
-
- /**
- * Adds the given 3D object as a child to this 3D object. An arbitrary number of
- * objects may be added. Any current parent on an object passed in here will be
- * removed, since an object can have at most one parent.
- *
- * @fires Object3D#added
- * @fires Object3D#childadded
- * @param {Object3D} object - The 3D object to add.
- * @return {Object3D} A reference to this instance.
- */
- add( object ) {
-
- if ( arguments.length > 1 ) {
-
- for ( let i = 0; i < arguments.length; i ++ ) {
-
- this.add( arguments[ i ] );
-
- }
-
- return this;
-
- }
-
- if ( object === this ) {
-
- error( 'Object3D.add: object can\'t be added as a child of itself.', object );
- return this;
-
- }
-
- if ( object && object.isObject3D ) {
-
- object.removeFromParent();
- object.parent = this;
- this.children.push( object );
-
- object.dispatchEvent( _addedEvent );
-
- _childaddedEvent.child = object;
- this.dispatchEvent( _childaddedEvent );
- _childaddedEvent.child = null;
-
- } else {
-
- error( 'Object3D.add: object not an instance of THREE.Object3D.', object );
-
- }
-
- return this;
-
- }
-
- /**
- * Removes the given 3D object as child from this 3D object.
- * An arbitrary number of objects may be removed.
- *
- * @fires Object3D#removed
- * @fires Object3D#childremoved
- * @param {Object3D} object - The 3D object to remove.
- * @return {Object3D} A reference to this instance.
- */
- remove( object ) {
-
- if ( arguments.length > 1 ) {
-
- for ( let i = 0; i < arguments.length; i ++ ) {
-
- this.remove( arguments[ i ] );
-
- }
-
- return this;
-
- }
-
- const index = this.children.indexOf( object );
-
- if ( index !== -1 ) {
-
- object.parent = null;
- this.children.splice( index, 1 );
-
- object.dispatchEvent( _removedEvent );
-
- _childremovedEvent.child = object;
- this.dispatchEvent( _childremovedEvent );
- _childremovedEvent.child = null;
-
- }
-
- return this;
-
- }
-
- /**
- * Removes this 3D object from its current parent.
- *
- * @fires Object3D#removed
- * @fires Object3D#childremoved
- * @return {Object3D} A reference to this instance.
- */
- removeFromParent() {
-
- const parent = this.parent;
-
- if ( parent !== null ) {
-
- parent.remove( this );
-
- }
-
- return this;
-
- }
-
- /**
- * Removes all child objects.
- *
- * @fires Object3D#removed
- * @fires Object3D#childremoved
- * @return {Object3D} A reference to this instance.
- */
- clear() {
-
- return this.remove( ... this.children );
-
- }
-
- /**
- * Adds the given 3D object as a child of this 3D object, while maintaining the object's world
- * transform. This method does not support scene graphs having non-uniformly-scaled nodes(s).
- *
- * @fires Object3D#added
- * @fires Object3D#childadded
- * @param {Object3D} object - The 3D object to attach.
- * @return {Object3D} A reference to this instance.
- */
- attach( object ) {
-
- // adds object as a child of this, while maintaining the object's world transform
-
- // Note: This method does not support scene graphs having non-uniformly-scaled nodes(s)
-
- this.updateWorldMatrix( true, false );
-
- _m1$1.copy( this.matrixWorld ).invert();
-
- if ( object.parent !== null ) {
-
- object.parent.updateWorldMatrix( true, false );
-
- _m1$1.multiply( object.parent.matrixWorld );
-
- }
-
- object.applyMatrix4( _m1$1 );
-
- object.removeFromParent();
- object.parent = this;
- this.children.push( object );
-
- object.updateWorldMatrix( false, true );
-
- object.dispatchEvent( _addedEvent );
-
- _childaddedEvent.child = object;
- this.dispatchEvent( _childaddedEvent );
- _childaddedEvent.child = null;
-
- return this;
-
- }
-
- /**
- * Searches through the 3D object and its children, starting with the 3D object
- * itself, and returns the first with a matching ID.
- *
- * @param {number} id - The id.
- * @return {Object3D|undefined} The found 3D object. Returns `undefined` if no 3D object has been found.
- */
- getObjectById( id ) {
-
- return this.getObjectByProperty( 'id', id );
-
- }
-
- /**
- * Searches through the 3D object and its children, starting with the 3D object
- * itself, and returns the first with a matching name.
- *
- * @param {string} name - The name.
- * @return {Object3D|undefined} The found 3D object. Returns `undefined` if no 3D object has been found.
- */
- getObjectByName( name ) {
-
- return this.getObjectByProperty( 'name', name );
-
- }
-
- /**
- * Searches through the 3D object and its children, starting with the 3D object
- * itself, and returns the first with a matching property value.
- *
- * @param {string} name - The name of the property.
- * @param {any} value - The value.
- * @return {Object3D|undefined} The found 3D object. Returns `undefined` if no 3D object has been found.
- */
- getObjectByProperty( name, value ) {
-
- if ( this[ name ] === value ) return this;
-
- for ( let i = 0, l = this.children.length; i < l; i ++ ) {
-
- const child = this.children[ i ];
- const object = child.getObjectByProperty( name, value );
-
- if ( object !== undefined ) {
-
- return object;
-
- }
-
- }
-
- return undefined;
-
- }
-
- /**
- * Searches through the 3D object and its children, starting with the 3D object
- * itself, and returns all 3D objects with a matching property value.
- *
- * @param {string} name - The name of the property.
- * @param {any} value - The value.
- * @param {Array} result - The method stores the result in this array.
- * @return {Array} The found 3D objects.
- */
- getObjectsByProperty( name, value, result = [] ) {
-
- if ( this[ name ] === value ) result.push( this );
-
- const children = this.children;
-
- for ( let i = 0, l = children.length; i < l; i ++ ) {
-
- children[ i ].getObjectsByProperty( name, value, result );
-
- }
-
- return result;
-
- }
-
- /**
- * Returns a vector representing the position of the 3D object in world space.
- *
- * @param {Vector3} target - The target vector the result is stored to.
- * @return {Vector3} The 3D object's position in world space.
- */
- getWorldPosition( target ) {
-
- this.updateWorldMatrix( true, false );
-
- return target.setFromMatrixPosition( this.matrixWorld );
-
- }
-
- /**
- * Returns a Quaternion representing the position of the 3D object in world space.
- *
- * @param {Quaternion} target - The target Quaternion the result is stored to.
- * @return {Quaternion} The 3D object's rotation in world space.
- */
- getWorldQuaternion( target ) {
-
- this.updateWorldMatrix( true, false );
-
- this.matrixWorld.decompose( _position$4, target, _scale$3 );
-
- return target;
-
- }
-
- /**
- * Returns a vector representing the scale of the 3D object in world space.
- *
- * @param {Vector3} target - The target vector the result is stored to.
- * @return {Vector3} The 3D object's scale in world space.
- */
- getWorldScale( target ) {
-
- this.updateWorldMatrix( true, false );
-
- this.matrixWorld.decompose( _position$4, _quaternion$3, target );
-
- return target;
-
- }
-
- /**
- * Returns a vector representing the ("look") direction of the 3D object in world space.
- *
- * @param {Vector3} target - The target vector the result is stored to.
- * @return {Vector3} The 3D object's direction in world space.
- */
- getWorldDirection( target ) {
-
- this.updateWorldMatrix( true, false );
-
- const e = this.matrixWorld.elements;
-
- return target.set( e[ 8 ], e[ 9 ], e[ 10 ] ).normalize();
-
- }
-
- /**
- * Abstract method to get intersections between a casted ray and this
- * 3D object. Renderable 3D objects such as {@link Mesh}, {@link Line} or {@link Points}
- * implement this method in order to use raycasting.
- *
- * @abstract
- * @param {Raycaster} raycaster - The raycaster.
- * @param {Array} intersects - An array holding the result of the method.
- */
- raycast( /* raycaster, intersects */ ) {}
-
- /**
- * Abstract method to test whether this 3D object intersects the given frustum.
- * Renderable 3D objects such as {@link Mesh}, {@link Line} or {@link Points}
- * implement this method in order to use frustum culling.
- *
- * @abstract
- * @param {Frustum|FrustumArray} frustum - The frustum to test.
- * @return {boolean|undefined} Whether this 3D object intersects the given frustum or not.
- */
- intersectsFrustum( /* frustum */ ) {}
-
- /**
- * Executes the callback on this 3D object and all descendants.
- *
- * Note: Modifying the scene graph inside the callback is discouraged.
- *
- * @param {Function} callback - A callback function that allows to process the current 3D object.
- */
- traverse( callback ) {
-
- callback( this );
-
- const children = this.children;
-
- for ( let i = 0, l = children.length; i < l; i ++ ) {
-
- children[ i ].traverse( callback );
-
- }
-
- }
-
- /**
- * Like {@link Object3D#traverse}, but the callback will only be executed for visible 3D objects.
- * Descendants of invisible 3D objects are not traversed.
- *
- * Note: Modifying the scene graph inside the callback is discouraged.
- *
- * @param {Function} callback - A callback function that allows to process the current 3D object.
- */
- traverseVisible( callback ) {
-
- if ( this.visible === false ) return;
-
- callback( this );
-
- const children = this.children;
-
- for ( let i = 0, l = children.length; i < l; i ++ ) {
-
- children[ i ].traverseVisible( callback );
-
- }
-
- }
-
- /**
- * Like {@link Object3D#traverse}, but the callback will only be executed for all ancestors.
- *
- * Note: Modifying the scene graph inside the callback is discouraged.
- *
- * @param {Function} callback - A callback function that allows to process the current 3D object.
- */
- traverseAncestors( callback ) {
-
- const parent = this.parent;
-
- if ( parent !== null ) {
-
- callback( parent );
-
- parent.traverseAncestors( callback );
-
- }
-
- }
-
- /**
- * Updates the transformation matrix in local space by computing it from the current
- * position, rotation and scale values.
- */
- updateMatrix() {
-
- this.matrix.compose( this.position, this.quaternion, this.scale );
-
- const pivot = this.pivot;
-
- if ( pivot !== null ) {
-
- const px = pivot.x, py = pivot.y, pz = pivot.z;
- const te = this.matrix.elements;
-
- te[ 12 ] += px - te[ 0 ] * px - te[ 4 ] * py - te[ 8 ] * pz;
- te[ 13 ] += py - te[ 1 ] * px - te[ 5 ] * py - te[ 9 ] * pz;
- te[ 14 ] += pz - te[ 2 ] * px - te[ 6 ] * py - te[ 10 ] * pz;
-
- }
-
- this.matrixWorldNeedsUpdate = true;
-
- }
-
- /**
- * Updates the transformation matrix in world space of this 3D objects and its descendants.
- *
- * To ensure correct results, this method also recomputes the 3D object's transformation matrix in
- * local space. The computation of the local and world matrix can be controlled with the
- * {@link Object3D#matrixAutoUpdate} and {@link Object3D#matrixWorldAutoUpdate} flags which are both
- * `true` by default. Set these flags to `false` if you need more control over the update matrix process.
- *
- * @param {boolean} [force=false] - When set to `true`, a recomputation of world matrices is forced even
- * when {@link Object3D#matrixWorldNeedsUpdate} is `false`.
- */
- updateMatrixWorld( force ) {
-
- if ( this.matrixAutoUpdate ) this.updateMatrix();
-
- if ( this.matrixWorldNeedsUpdate || force ) {
-
- if ( this.matrixWorldAutoUpdate === true ) {
-
- if ( this.parent === null ) {
-
- this.matrixWorld.copy( this.matrix );
-
- } else {
-
- this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
-
- }
-
- }
-
- this.matrixWorldNeedsUpdate = false;
-
- force = true;
-
- }
-
- // make sure descendants are updated if required
-
- const children = this.children;
-
- for ( let i = 0, l = children.length; i < l; i ++ ) {
-
- const child = children[ i ];
-
- child.updateMatrixWorld( force );
-
- }
-
- }
-
- /**
- * An alternative version of {@link Object3D#updateMatrixWorld} with more control over the
- * update of ancestor and descendant nodes.
- *
- * @param {boolean} [updateParents=false] Whether ancestor nodes should be updated or not.
- * @param {boolean} [updateChildren=false] Whether descendant nodes should be updated or not.
- * @param {boolean} [force=false] - When set to `true`, a recomputation of world matrices is forced even
- * when {@link Object3D#matrixWorldNeedsUpdate} is `false`.
- */
- updateWorldMatrix( updateParents, updateChildren, force = false ) {
-
- const parent = this.parent;
-
- if ( updateParents === true && parent !== null ) {
-
- parent.updateWorldMatrix( true, false );
-
- }
-
- if ( this.matrixAutoUpdate ) this.updateMatrix();
-
- if ( this.matrixWorldNeedsUpdate || force ) {
-
- if ( this.matrixWorldAutoUpdate === true ) {
-
- if ( this.parent === null ) {
-
- this.matrixWorld.copy( this.matrix );
-
- } else {
-
- this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
-
- }
-
- }
-
- this.matrixWorldNeedsUpdate = false;
-
- force = true;
-
- }
-
- // make sure descendants are updated
-
- if ( updateChildren === true ) {
-
- const children = this.children;
-
- for ( let i = 0, l = children.length; i < l; i ++ ) {
-
- const child = children[ i ];
-
- child.updateWorldMatrix( false, true, force );
-
- }
-
- }
-
- }
-
- /**
- * Serializes the 3D object into JSON.
- *
- * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
- * @return {Object} A JSON object representing the serialized 3D object.
- * @see {@link ObjectLoader#parse}
- */
- toJSON( meta ) {
-
- // meta is a string when called from JSON.stringify
- const isRootObject = ( meta === undefined || typeof meta === 'string' );
-
- const output = {};
-
- // meta is a hash used to collect geometries, materials.
- // not providing it implies that this is the root object
- // being serialized.
- if ( isRootObject ) {
-
- // initialize meta obj
- meta = {
- geometries: {},
- materials: {},
- textures: {},
- images: {},
- shapes: {},
- skeletons: {},
- animations: {},
- nodes: {}
- };
-
- output.metadata = {
- version: 4.7,
- type: 'Object',
- generator: 'Object3D.toJSON'
- };
-
- }
-
- // standard Object3D serialization
-
- const object = {};
-
- object.uuid = this.uuid;
- object.type = this.type;
-
- object.name = this.name;
- object.castShadow = this.castShadow;
- object.receiveShadow = this.receiveShadow;
- object.visible = this.visible;
- object.frustumCulled = this.frustumCulled;
- object.renderOrder = this.renderOrder;
- object.static = this.static;
- object.matrixAutoUpdate = this.matrixAutoUpdate;
-
- if ( Object.keys( this.userData ).length > 0 ) object.userData = this.userData;
-
- object.layers = this.layers.mask;
- object.matrix = this.matrix.toArray();
- object.up = this.up.toArray();
-
- if ( this.pivot !== null ) object.pivot = this.pivot.toArray();
-
- if ( this.morphTargetDictionary !== undefined ) object.morphTargetDictionary = Object.assign( {}, this.morphTargetDictionary );
- if ( this.morphTargetInfluences !== undefined ) object.morphTargetInfluences = this.morphTargetInfluences.slice();
-
- // object specific properties
-
- if ( this.isInstancedMesh ) {
-
- object.type = 'InstancedMesh';
- object.count = this.count;
- object.instanceMatrix = this.instanceMatrix.toJSON();
- if ( this.instanceColor !== null ) object.instanceColor = this.instanceColor.toJSON();
-
- }
-
- if ( this.isBatchedMesh ) {
-
- object.type = 'BatchedMesh';
- object.perObjectFrustumCulled = this.perObjectFrustumCulled;
- object.sortObjects = this.sortObjects;
-
- object.drawRanges = this._drawRanges;
- object.reservedRanges = this._reservedRanges;
-
- object.geometryInfo = this._geometryInfo.map( info => ( {
- ...info,
- boundingBox: info.boundingBox ? info.boundingBox.toJSON() : undefined,
- boundingSphere: info.boundingSphere ? info.boundingSphere.toJSON() : undefined
- } ) );
- object.instanceInfo = this._instanceInfo.map( info => ( { ...info } ) );
-
- object.availableInstanceIds = this._availableInstanceIds.slice();
- object.availableGeometryIds = this._availableGeometryIds.slice();
-
- object.nextIndexStart = this._nextIndexStart;
- object.nextVertexStart = this._nextVertexStart;
- object.geometryCount = this._geometryCount;
-
- object.maxInstanceCount = this._maxInstanceCount;
- object.maxVertexCount = this._maxVertexCount;
- object.maxIndexCount = this._maxIndexCount;
-
- object.geometryInitialized = this._geometryInitialized;
-
- object.matricesTexture = this._matricesTexture.toJSON( meta );
-
- object.indirectTexture = this._indirectTexture.toJSON( meta );
-
- if ( this._colorsTexture !== null ) {
-
- object.colorsTexture = this._colorsTexture.toJSON( meta );
-
- }
-
- if ( this.boundingSphere !== null ) {
-
- object.boundingSphere = this.boundingSphere.toJSON();
-
- }
-
- if ( this.boundingBox !== null ) {
-
- object.boundingBox = this.boundingBox.toJSON();
-
- }
-
- }
-
- //
-
- function serialize( library, element ) {
-
- if ( library[ element.uuid ] === undefined ) {
-
- library[ element.uuid ] = element.toJSON( meta );
-
- }
-
- return element.uuid;
-
- }
-
- if ( this.isScene ) {
-
- if ( this.background ) {
-
- if ( this.background.isColor ) {
-
- object.background = this.background.toJSON();
-
- } else if ( this.background.isTexture ) {
-
- object.background = this.background.toJSON( meta ).uuid;
-
- }
-
- }
-
- if ( this.environment && this.environment.isTexture && this.environment.isRenderTargetTexture !== true ) {
-
- object.environment = this.environment.toJSON( meta ).uuid;
-
- }
-
- } else if ( this.isMesh || this.isLine || this.isPoints ) {
-
- object.geometry = serialize( meta.geometries, this.geometry );
-
- const parameters = this.geometry.parameters;
-
- if ( parameters !== undefined && parameters.shapes !== undefined ) {
-
- const shapes = parameters.shapes;
-
- if ( Array.isArray( shapes ) ) {
-
- for ( let i = 0, l = shapes.length; i < l; i ++ ) {
-
- const shape = shapes[ i ];
-
- serialize( meta.shapes, shape );
-
- }
-
- } else {
-
- serialize( meta.shapes, shapes );
-
- }
-
- }
-
- }
-
- if ( this.isSkinnedMesh ) {
-
- object.bindMode = this.bindMode;
- object.bindMatrix = this.bindMatrix.toArray();
-
- if ( this.skeleton !== undefined ) {
-
- serialize( meta.skeletons, this.skeleton );
-
- object.skeleton = this.skeleton.uuid;
-
- }
-
- }
-
- if ( this.material !== undefined ) {
-
- if ( Array.isArray( this.material ) ) {
-
- const uuids = [];
-
- for ( let i = 0, l = this.material.length; i < l; i ++ ) {
-
- uuids.push( serialize( meta.materials, this.material[ i ] ) );
-
- }
-
- object.material = uuids;
-
- } else {
-
- object.material = serialize( meta.materials, this.material );
-
- }
-
- }
-
- //
-
- if ( this.children.length > 0 ) {
-
- object.children = [];
-
- for ( let i = 0; i < this.children.length; i ++ ) {
-
- object.children.push( this.children[ i ].toJSON( meta ).object );
-
- }
-
- }
-
- //
-
- if ( this.animations.length > 0 ) {
-
- object.animations = [];
-
- for ( let i = 0; i < this.animations.length; i ++ ) {
-
- const animation = this.animations[ i ];
-
- object.animations.push( serialize( meta.animations, animation ) );
-
- }
-
- }
-
- if ( isRootObject ) {
-
- const geometries = extractFromCache( meta.geometries );
- const materials = extractFromCache( meta.materials );
- const textures = extractFromCache( meta.textures );
- const images = extractFromCache( meta.images );
- const shapes = extractFromCache( meta.shapes );
- const skeletons = extractFromCache( meta.skeletons );
- const animations = extractFromCache( meta.animations );
- const nodes = extractFromCache( meta.nodes );
-
- if ( geometries.length > 0 ) output.geometries = geometries;
- if ( materials.length > 0 ) output.materials = materials;
- if ( textures.length > 0 ) output.textures = textures;
- if ( images.length > 0 ) output.images = images;
- if ( shapes.length > 0 ) output.shapes = shapes;
- if ( skeletons.length > 0 ) output.skeletons = skeletons;
- if ( animations.length > 0 ) output.animations = animations;
- if ( nodes.length > 0 ) output.nodes = nodes;
-
- }
-
- output.object = object;
-
- return output;
-
- // extract data from the cache hash
- // remove metadata on each item
- // and return as array
- function extractFromCache( cache ) {
-
- const values = [];
- for ( const key in cache ) {
-
- const data = cache[ key ];
- delete data.metadata;
- values.push( data );
-
- }
-
- return values;
-
- }
-
- }
-
- /**
- * Returns a new 3D object with copied values from this instance.
- *
- * @param {boolean} [recursive=true] - When set to `true`, descendants of the 3D object are also cloned.
- * @return {Object3D} A clone of this instance.
- */
- clone( recursive ) {
-
- return new this.constructor().copy( this, recursive );
-
- }
-
- /**
- * Copies the values of the given 3D object to this instance.
- *
- * @param {Object3D} source - The 3D object to copy.
- * @param {boolean} [recursive=true] - When set to `true`, descendants of the 3D object are cloned.
- * @return {Object3D} A reference to this instance.
- */
- copy( source, recursive = true ) {
-
- this.name = source.name;
-
- this.up.copy( source.up );
-
- this.position.copy( source.position );
- this.rotation.order = source.rotation.order;
- this.quaternion.copy( source.quaternion );
- this.scale.copy( source.scale );
-
- this.pivot = ( source.pivot !== null ) ? source.pivot.clone() : null;
-
- this.matrix.copy( source.matrix );
- this.matrixWorld.copy( source.matrixWorld );
-
- this.matrixAutoUpdate = source.matrixAutoUpdate;
-
- this.matrixWorldAutoUpdate = source.matrixWorldAutoUpdate;
- this.matrixWorldNeedsUpdate = source.matrixWorldNeedsUpdate;
-
- this.layers.mask = source.layers.mask;
- this.visible = source.visible;
-
- this.castShadow = source.castShadow;
- this.receiveShadow = source.receiveShadow;
-
- this.frustumCulled = source.frustumCulled;
- this.renderOrder = source.renderOrder;
-
- this.static = source.static;
-
- this.animations = source.animations.slice();
-
- this.userData = JSON.parse( JSON.stringify( source.userData ) );
-
- if ( recursive === true ) {
-
- for ( let i = 0; i < source.children.length; i ++ ) {
-
- const child = source.children[ i ];
- this.add( child.clone() );
-
- }
-
- }
-
- return this;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- *
- * Geometries, materials and textures are potentially shared with other
- * 3D objects and must be disposed of separately.
- *
- * @fires Object3D#dispose
- */
- dispose() {
-
- /**
- * Fires when the 3D object has been disposed of.
- *
- * @event Object3D#dispose
- * @type {Object}
- */
- this.dispatchEvent( { type: 'dispose' } );
-
- }
-
-}
-
-/**
- * The default up direction for objects, also used as the default
- * position for {@link DirectionalLight} and {@link HemisphereLight}.
- *
- * @static
- * @type {Vector3}
- * @default (0,1,0)
- */
-Object3D.DEFAULT_UP = /*@__PURE__*/ new Vector3( 0, 1, 0 );
-
-/**
- * The default setting for {@link Object3D#matrixAutoUpdate} for
- * newly created 3D objects.
- *
- * @static
- * @type {boolean}
- * @default true
- */
-Object3D.DEFAULT_MATRIX_AUTO_UPDATE = true;
-
-/**
- * The default setting for {@link Object3D#matrixWorldAutoUpdate} for
- * newly created 3D objects.
- *
- * @static
- * @type {boolean}
- * @default true
- */
-Object3D.DEFAULT_MATRIX_WORLD_AUTO_UPDATE = true;
-
-/**
- * This is almost identical to an {@link Object3D}. Its purpose is to
- * make working with groups of objects syntactically clearer.
- *
- * ```js
- * // Create a group and add the two cubes.
- * // These cubes can now be rotated / scaled etc as a group.
- * const group = new THREE.Group();
- *
- * group.add( meshA );
- * group.add( meshB );
- *
- * scene.add( group );
- * ```
- *
- * @augments Object3D
- */
-class Group extends Object3D {
-
- constructor() {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isGroup = true;
-
- this.type = 'Group';
-
- }
-
-}
-
-const _moveEvent = { type: 'move' };
-
-/**
- * Class for representing a XR controller with its
- * different coordinate systems.
- *
- * @private
- */
-class WebXRController {
-
- /**
- * Constructs a new XR controller.
- */
- constructor() {
-
- /**
- * A group representing the target ray space
- * of the XR controller.
- *
- * @private
- * @type {?Group}
- * @default null
- */
- this._targetRay = null;
-
- /**
- * A group representing the grip space
- * of the XR controller.
- *
- * @private
- * @type {?Group}
- * @default null
- */
- this._grip = null;
-
- /**
- * A group representing the hand space
- * of the XR controller.
- *
- * @private
- * @type {?Group}
- * @default null
- */
- this._hand = null;
-
- }
-
- /**
- * Returns a group representing the hand space of the XR controller.
- *
- * @return {Group} A group representing the hand space of the XR controller.
- */
- getHandSpace() {
-
- if ( this._hand === null ) {
-
- this._hand = new Group();
- this._hand.matrixAutoUpdate = false;
- this._hand.visible = false;
-
- this._hand.joints = {};
- this._hand.inputState = { pinching: false };
-
- }
-
- return this._hand;
-
- }
-
- /**
- * Returns a group representing the target ray space of the XR controller.
- *
- * @return {Group} A group representing the target ray space of the XR controller.
- */
- getTargetRaySpace() {
-
- if ( this._targetRay === null ) {
-
- this._targetRay = new Group();
- this._targetRay.matrixAutoUpdate = false;
- this._targetRay.visible = false;
- this._targetRay.hasLinearVelocity = false;
- this._targetRay.linearVelocity = new Vector3();
- this._targetRay.hasAngularVelocity = false;
- this._targetRay.angularVelocity = new Vector3();
-
- }
-
- return this._targetRay;
-
- }
-
- /**
- * Returns a group representing the grip space of the XR controller.
- *
- * @return {Group} A group representing the grip space of the XR controller.
- */
- getGripSpace() {
-
- if ( this._grip === null ) {
-
- this._grip = new Group();
- this._grip.matrixAutoUpdate = false;
- this._grip.visible = false;
- this._grip.hasLinearVelocity = false;
- this._grip.linearVelocity = new Vector3();
- this._grip.hasAngularVelocity = false;
- this._grip.angularVelocity = new Vector3();
- this._grip.eventsEnabled = false;
-
- }
-
- return this._grip;
-
- }
-
- /**
- * Dispatches the given event to the groups representing
- * the different coordinate spaces of the XR controller.
- *
- * @param {Object} event - The event to dispatch.
- * @return {WebXRController} A reference to this instance.
- */
- dispatchEvent( event ) {
-
- if ( this._targetRay !== null ) {
-
- this._targetRay.dispatchEvent( event );
-
- }
-
- if ( this._grip !== null ) {
-
- this._grip.dispatchEvent( event );
-
- }
-
- if ( this._hand !== null ) {
-
- this._hand.dispatchEvent( event );
-
- }
-
- return this;
-
- }
-
- /**
- * Connects the controller with the given XR input source.
- *
- * @param {XRInputSource} inputSource - The input source.
- * @return {WebXRController} A reference to this instance.
- */
- connect( inputSource ) {
-
- if ( inputSource && inputSource.hand ) {
-
- const hand = this._hand;
-
- if ( hand ) {
-
- for ( const inputjoint of inputSource.hand.values() ) {
-
- // Initialize hand with joints when connected
- this._getHandJoint( hand, inputjoint );
-
- }
-
- }
-
- }
-
- this.dispatchEvent( { type: 'connected', data: inputSource } );
-
- return this;
-
- }
-
- /**
- * Disconnects the controller from the given XR input source.
- *
- * @param {XRInputSource} inputSource - The input source.
- * @return {WebXRController} A reference to this instance.
- */
- disconnect( inputSource ) {
-
- this.dispatchEvent( { type: 'disconnected', data: inputSource } );
-
- if ( this._targetRay !== null ) {
-
- this._targetRay.visible = false;
-
- }
-
- if ( this._grip !== null ) {
-
- this._grip.visible = false;
-
- }
-
- if ( this._hand !== null ) {
-
- this._hand.visible = false;
-
- }
-
- return this;
-
- }
-
- /**
- * Updates the controller with the given input source, XR frame and reference space.
- * This updates the transformations of the groups that represent the different
- * coordinate systems of the controller.
- *
- * @param {XRInputSource} inputSource - The input source.
- * @param {XRFrame} frame - The XR frame.
- * @param {XRReferenceSpace} referenceSpace - The reference space.
- * @return {WebXRController} A reference to this instance.
- */
- update( inputSource, frame, referenceSpace ) {
-
- let inputPose = null;
- let gripPose = null;
- let handPose = null;
-
- const targetRay = this._targetRay;
- const grip = this._grip;
- const hand = this._hand;
-
- if ( inputSource && frame.session.visibilityState !== 'visible-blurred' ) {
-
- if ( hand && inputSource.hand ) {
-
- handPose = true;
-
- for ( const inputjoint of inputSource.hand.values() ) {
-
- // Update the joints groups with the XRJoint poses
- const jointPose = frame.getJointPose( inputjoint, referenceSpace );
-
- // The transform of this joint will be updated with the joint pose on each frame
- const joint = this._getHandJoint( hand, inputjoint );
-
- if ( jointPose !== null ) {
-
- joint.matrix.fromArray( jointPose.transform.matrix );
- joint.matrix.decompose( joint.position, joint.rotation, joint.scale );
- joint.matrixWorldNeedsUpdate = true;
- joint.jointRadius = jointPose.radius;
-
- }
-
- joint.visible = jointPose !== null;
-
- }
-
- // Custom events
-
- // Check pinchz
- const indexTip = hand.joints[ 'index-finger-tip' ];
- const thumbTip = hand.joints[ 'thumb-tip' ];
- const distance = indexTip.position.distanceTo( thumbTip.position );
-
- const distanceToPinch = 0.02;
- const threshold = 0.005;
-
- if ( hand.inputState.pinching && distance > distanceToPinch + threshold ) {
-
- hand.inputState.pinching = false;
- this.dispatchEvent( {
- type: 'pinchend',
- handedness: inputSource.handedness,
- target: this
- } );
-
- } else if ( ! hand.inputState.pinching && distance <= distanceToPinch - threshold ) {
-
- hand.inputState.pinching = true;
- this.dispatchEvent( {
- type: 'pinchstart',
- handedness: inputSource.handedness,
- target: this
- } );
-
- }
-
- } else {
-
- if ( grip !== null && inputSource.gripSpace ) {
-
- gripPose = frame.getPose( inputSource.gripSpace, referenceSpace );
-
- if ( gripPose !== null ) {
-
- grip.matrix.fromArray( gripPose.transform.matrix );
- grip.matrix.decompose( grip.position, grip.rotation, grip.scale );
- grip.matrixWorldNeedsUpdate = true;
-
- if ( gripPose.linearVelocity ) {
-
- grip.hasLinearVelocity = true;
- grip.linearVelocity.copy( gripPose.linearVelocity );
-
- } else {
-
- grip.hasLinearVelocity = false;
-
- }
-
- if ( gripPose.angularVelocity ) {
-
- grip.hasAngularVelocity = true;
- grip.angularVelocity.copy( gripPose.angularVelocity );
-
- } else {
-
- grip.hasAngularVelocity = false;
-
- }
-
- // grip update event if enabled
- if ( grip.eventsEnabled ) {
-
- grip.dispatchEvent( {
- type: 'gripUpdated',
- data: inputSource,
- target: this
- } );
-
- }
-
- }
-
- }
-
- }
-
- if ( targetRay !== null ) {
-
- inputPose = frame.getPose( inputSource.targetRaySpace, referenceSpace );
-
- // Some runtimes (namely Vive Cosmos with Vive OpenXR Runtime) have only grip space and ray space is equal to it
- if ( inputPose === null && gripPose !== null ) {
-
- inputPose = gripPose;
-
- }
-
- if ( inputPose !== null ) {
-
- targetRay.matrix.fromArray( inputPose.transform.matrix );
- targetRay.matrix.decompose( targetRay.position, targetRay.rotation, targetRay.scale );
- targetRay.matrixWorldNeedsUpdate = true;
-
- if ( inputPose.linearVelocity ) {
-
- targetRay.hasLinearVelocity = true;
- targetRay.linearVelocity.copy( inputPose.linearVelocity );
-
- } else {
-
- targetRay.hasLinearVelocity = false;
-
- }
-
- if ( inputPose.angularVelocity ) {
-
- targetRay.hasAngularVelocity = true;
- targetRay.angularVelocity.copy( inputPose.angularVelocity );
-
- } else {
-
- targetRay.hasAngularVelocity = false;
-
- }
-
- this.dispatchEvent( _moveEvent );
-
- }
-
- }
-
-
- }
-
- if ( targetRay !== null ) {
-
- targetRay.visible = ( inputPose !== null );
-
- }
-
- if ( grip !== null ) {
-
- grip.visible = ( gripPose !== null );
-
- }
-
- if ( hand !== null ) {
-
- hand.visible = ( handPose !== null );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns a group representing the hand joint for the given input joint.
- *
- * @private
- * @param {Group} hand - The group representing the hand space.
- * @param {XRJointSpace} inputjoint - The hand joint data.
- * @return {Group} A group representing the hand joint for the given input joint.
- */
- _getHandJoint( hand, inputjoint ) {
-
- if ( hand.joints[ inputjoint.jointName ] === undefined ) {
-
- const joint = new Group();
- joint.matrixAutoUpdate = false;
- joint.visible = false;
- hand.joints[ inputjoint.jointName ] = joint;
-
- hand.add( joint );
-
- }
-
- return hand.joints[ inputjoint.jointName ];
-
- }
-
-}
-
-const _colorKeywords = { 'aliceblue': 0xF0F8FF, 'antiquewhite': 0xFAEBD7, 'aqua': 0x00FFFF, 'aquamarine': 0x7FFFD4, 'azure': 0xF0FFFF,
- 'beige': 0xF5F5DC, 'bisque': 0xFFE4C4, 'black': 0x000000, 'blanchedalmond': 0xFFEBCD, 'blue': 0x0000FF, 'blueviolet': 0x8A2BE2,
- 'brown': 0xA52A2A, 'burlywood': 0xDEB887, 'cadetblue': 0x5F9EA0, 'chartreuse': 0x7FFF00, 'chocolate': 0xD2691E, 'coral': 0xFF7F50,
- 'cornflowerblue': 0x6495ED, 'cornsilk': 0xFFF8DC, 'crimson': 0xDC143C, 'cyan': 0x00FFFF, 'darkblue': 0x00008B, 'darkcyan': 0x008B8B,
- 'darkgoldenrod': 0xB8860B, 'darkgray': 0xA9A9A9, 'darkgreen': 0x006400, 'darkgrey': 0xA9A9A9, 'darkkhaki': 0xBDB76B, 'darkmagenta': 0x8B008B,
- 'darkolivegreen': 0x556B2F, 'darkorange': 0xFF8C00, 'darkorchid': 0x9932CC, 'darkred': 0x8B0000, 'darksalmon': 0xE9967A, 'darkseagreen': 0x8FBC8F,
- 'darkslateblue': 0x483D8B, 'darkslategray': 0x2F4F4F, 'darkslategrey': 0x2F4F4F, 'darkturquoise': 0x00CED1, 'darkviolet': 0x9400D3,
- 'deeppink': 0xFF1493, 'deepskyblue': 0x00BFFF, 'dimgray': 0x696969, 'dimgrey': 0x696969, 'dodgerblue': 0x1E90FF, 'firebrick': 0xB22222,
- 'floralwhite': 0xFFFAF0, 'forestgreen': 0x228B22, 'fuchsia': 0xFF00FF, 'gainsboro': 0xDCDCDC, 'ghostwhite': 0xF8F8FF, 'gold': 0xFFD700,
- 'goldenrod': 0xDAA520, 'gray': 0x808080, 'green': 0x008000, 'greenyellow': 0xADFF2F, 'grey': 0x808080, 'honeydew': 0xF0FFF0, 'hotpink': 0xFF69B4,
- 'indianred': 0xCD5C5C, 'indigo': 0x4B0082, 'ivory': 0xFFFFF0, 'khaki': 0xF0E68C, 'lavender': 0xE6E6FA, 'lavenderblush': 0xFFF0F5, 'lawngreen': 0x7CFC00,
- 'lemonchiffon': 0xFFFACD, 'lightblue': 0xADD8E6, 'lightcoral': 0xF08080, 'lightcyan': 0xE0FFFF, 'lightgoldenrodyellow': 0xFAFAD2, 'lightgray': 0xD3D3D3,
- 'lightgreen': 0x90EE90, 'lightgrey': 0xD3D3D3, 'lightpink': 0xFFB6C1, 'lightsalmon': 0xFFA07A, 'lightseagreen': 0x20B2AA, 'lightskyblue': 0x87CEFA,
- 'lightslategray': 0x778899, 'lightslategrey': 0x778899, 'lightsteelblue': 0xB0C4DE, 'lightyellow': 0xFFFFE0, 'lime': 0x00FF00, 'limegreen': 0x32CD32,
- 'linen': 0xFAF0E6, 'magenta': 0xFF00FF, 'maroon': 0x800000, 'mediumaquamarine': 0x66CDAA, 'mediumblue': 0x0000CD, 'mediumorchid': 0xBA55D3,
- 'mediumpurple': 0x9370DB, 'mediumseagreen': 0x3CB371, 'mediumslateblue': 0x7B68EE, 'mediumspringgreen': 0x00FA9A, 'mediumturquoise': 0x48D1CC,
- 'mediumvioletred': 0xC71585, 'midnightblue': 0x191970, 'mintcream': 0xF5FFFA, 'mistyrose': 0xFFE4E1, 'moccasin': 0xFFE4B5, 'navajowhite': 0xFFDEAD,
- 'navy': 0x000080, 'oldlace': 0xFDF5E6, 'olive': 0x808000, 'olivedrab': 0x6B8E23, 'orange': 0xFFA500, 'orangered': 0xFF4500, 'orchid': 0xDA70D6,
- 'palegoldenrod': 0xEEE8AA, 'palegreen': 0x98FB98, 'paleturquoise': 0xAFEEEE, 'palevioletred': 0xDB7093, 'papayawhip': 0xFFEFD5, 'peachpuff': 0xFFDAB9,
- 'peru': 0xCD853F, 'pink': 0xFFC0CB, 'plum': 0xDDA0DD, 'powderblue': 0xB0E0E6, 'purple': 0x800080, 'rebeccapurple': 0x663399, 'red': 0xFF0000, 'rosybrown': 0xBC8F8F,
- 'royalblue': 0x4169E1, 'saddlebrown': 0x8B4513, 'salmon': 0xFA8072, 'sandybrown': 0xF4A460, 'seagreen': 0x2E8B57, 'seashell': 0xFFF5EE,
- 'sienna': 0xA0522D, 'silver': 0xC0C0C0, 'skyblue': 0x87CEEB, 'slateblue': 0x6A5ACD, 'slategray': 0x708090, 'slategrey': 0x708090, 'snow': 0xFFFAFA,
- 'springgreen': 0x00FF7F, 'steelblue': 0x4682B4, 'tan': 0xD2B48C, 'teal': 0x008080, 'thistle': 0xD8BFD8, 'tomato': 0xFF6347, 'turquoise': 0x40E0D0,
- 'violet': 0xEE82EE, 'wheat': 0xF5DEB3, 'white': 0xFFFFFF, 'whitesmoke': 0xF5F5F5, 'yellow': 0xFFFF00, 'yellowgreen': 0x9ACD32 };
-
-const _hslA = { h: 0, s: 0, l: 0 };
-const _hslB = { h: 0, s: 0, l: 0 };
-
-function hue2rgb( p, q, t ) {
-
- if ( t < 0 ) t += 1;
- if ( t > 1 ) t -= 1;
- if ( t < 1 / 6 ) return p + ( q - p ) * 6 * t;
- if ( t < 1 / 2 ) return q;
- if ( t < 2 / 3 ) return p + ( q - p ) * 6 * ( 2 / 3 - t );
- return p;
-
-}
-
-/**
- * A Color instance is represented by RGB components in the linear working
- * color space , which defaults to `LinearSRGBColorSpace`. Inputs
- * conventionally using `SRGBColorSpace` (such as hexadecimals and CSS
- * strings) are converted to the working color space automatically.
- *
- * ```js
- * // converted automatically from SRGBColorSpace to LinearSRGBColorSpace
- * const color = new THREE.Color().setHex( 0x112233 );
- * ```
- * Source color spaces may be specified explicitly, to ensure correct conversions.
- * ```js
- * // assumed already LinearSRGBColorSpace; no conversion
- * const color = new THREE.Color().setRGB( 0.5, 0.5, 0.5 );
- *
- * // converted explicitly from SRGBColorSpace to LinearSRGBColorSpace
- * const color = new THREE.Color().setRGB( 0.5, 0.5, 0.5, SRGBColorSpace );
- * ```
- * If THREE.ColorManagement is disabled, no conversions occur. For details,
- * see Color management . Iterating through a Color instance will yield
- * its components (r, g, b) in the corresponding order. A Color can be initialised
- * in any of the following ways:
- * ```js
- * //empty constructor - will default white
- * const color1 = new THREE.Color();
- *
- * //Hexadecimal color (recommended)
- * const color2 = new THREE.Color( 0xff0000 );
- *
- * //RGB string
- * const color3 = new THREE.Color("rgb(255, 0, 0)");
- * const color4 = new THREE.Color("rgb(100%, 0%, 0%)");
- *
- * //X11 color name - all 140 color names are supported.
- * //Note the lack of CamelCase in the name
- * const color5 = new THREE.Color( 'skyblue' );
- * //HSL string
- * const color6 = new THREE.Color("hsl(0, 100%, 50%)");
- *
- * //Separate RGB values between 0 and 1
- * const color7 = new THREE.Color( 1, 0, 0 );
- * ```
- */
-class Color {
-
- /**
- * Constructs a new color.
- *
- * Note that standard method of specifying color in three.js is with a hexadecimal triplet,
- * and that method is used throughout the rest of the documentation.
- *
- * @param {(number|string|Color)} [r] - The red component of the color. If `g` and `b` are
- * not provided, it can be hexadecimal triplet, a CSS-style string or another `Color` instance.
- * @param {number} [g] - The green component.
- * @param {number} [b] - The blue component.
- */
- constructor( r, g, b ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isColor = true;
-
- /**
- * The red component.
- *
- * @type {number}
- * @default 1
- */
- this.r = 1;
-
- /**
- * The green component.
- *
- * @type {number}
- * @default 1
- */
- this.g = 1;
-
- /**
- * The blue component.
- *
- * @type {number}
- * @default 1
- */
- this.b = 1;
-
- return this.set( r, g, b );
-
- }
-
- /**
- * Sets the colors's components from the given values.
- *
- * @param {(number|string|Color)} [r] - The red component of the color. If `g` and `b` are
- * not provided, it can be hexadecimal triplet, a CSS-style string or another `Color` instance.
- * @param {number} [g] - The green component.
- * @param {number} [b] - The blue component.
- * @return {Color} A reference to this color.
- */
- set( r, g, b ) {
-
- if ( g === undefined && b === undefined ) {
-
- // r is THREE.Color, hex or string
-
- const value = r;
-
- if ( value && value.isColor ) {
-
- this.copy( value );
-
- } else if ( typeof value === 'number' ) {
-
- this.setHex( value );
-
- } else if ( typeof value === 'string' ) {
-
- this.setStyle( value );
-
- }
-
- } else {
-
- this.setRGB( r, g, b );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the colors's components to the given scalar value.
- *
- * @param {number} scalar - The scalar value.
- * @return {Color} A reference to this color.
- */
- setScalar( scalar ) {
-
- this.r = scalar;
- this.g = scalar;
- this.b = scalar;
-
- return this;
-
- }
-
- /**
- * Sets this color from a hexadecimal value.
- *
- * @param {number} hex - The hexadecimal value.
- * @param {string} [colorSpace=SRGBColorSpace] - The color space.
- * @return {Color} A reference to this color.
- */
- setHex( hex, colorSpace = SRGBColorSpace ) {
-
- hex = Math.floor( hex );
-
- this.r = ( hex >> 16 & 255 ) / 255;
- this.g = ( hex >> 8 & 255 ) / 255;
- this.b = ( hex & 255 ) / 255;
-
- ColorManagement.colorSpaceToWorking( this, colorSpace );
-
- return this;
-
- }
-
- /**
- * Sets this color from RGB values.
- *
- * @param {number} r - Red channel value between `0.0` and `1.0`.
- * @param {number} g - Green channel value between `0.0` and `1.0`.
- * @param {number} b - Blue channel value between `0.0` and `1.0`.
- * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.
- * @return {Color} A reference to this color.
- */
- setRGB( r, g, b, colorSpace = ColorManagement.workingColorSpace ) {
-
- this.r = r;
- this.g = g;
- this.b = b;
-
- ColorManagement.colorSpaceToWorking( this, colorSpace );
-
- return this;
-
- }
-
- /**
- * Sets this color from RGB values.
- *
- * @param {number} h - Hue value between `0.0` and `1.0`.
- * @param {number} s - Saturation value between `0.0` and `1.0`.
- * @param {number} l - Lightness value between `0.0` and `1.0`.
- * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.
- * @return {Color} A reference to this color.
- */
- setHSL( h, s, l, colorSpace = ColorManagement.workingColorSpace ) {
-
- // h,s,l ranges are in 0.0 - 1.0
- h = euclideanModulo( h, 1 );
- s = clamp( s, 0, 1 );
- l = clamp( l, 0, 1 );
-
- if ( s === 0 ) {
-
- this.r = this.g = this.b = l;
-
- } else {
-
- const p = l <= 0.5 ? l * ( 1 + s ) : l + s - ( l * s );
- const q = ( 2 * l ) - p;
-
- this.r = hue2rgb( q, p, h + 1 / 3 );
- this.g = hue2rgb( q, p, h );
- this.b = hue2rgb( q, p, h - 1 / 3 );
-
- }
-
- ColorManagement.colorSpaceToWorking( this, colorSpace );
-
- return this;
-
- }
-
- /**
- * Sets this color from a CSS-style string. For example, `rgb(250, 0,0)`,
- * `rgb(100%, 0%, 0%)`, `hsl(0, 100%, 50%)`, `#ff0000`, `#f00`, or `red` ( or
- * any [X11 color name](https://en.wikipedia.org/wiki/X11_color_names#Color_name_chart) -
- * all 140 color names are supported).
- *
- * @param {string} style - Color as a CSS-style string.
- * @param {string} [colorSpace=SRGBColorSpace] - The color space.
- * @return {Color} A reference to this color.
- */
- setStyle( style, colorSpace = SRGBColorSpace ) {
-
- function handleAlpha( string ) {
-
- if ( string === undefined ) return;
-
- if ( parseFloat( string ) < 1 ) {
-
- warn( 'Color: Alpha component of ' + style + ' will be ignored.' );
-
- }
-
- }
-
-
- let m;
-
- if ( m = /^(\w+)\(([^\)]*)\)/.exec( style ) ) {
-
- // rgb / hsl
-
- let color;
- const name = m[ 1 ];
- const components = m[ 2 ];
-
- switch ( name ) {
-
- case 'rgb':
- case 'rgba':
-
- if ( color = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) {
-
- // rgb(255,0,0) rgba(255,0,0,0.5)
-
- handleAlpha( color[ 4 ] );
-
- return this.setRGB(
- Math.min( 255, parseInt( color[ 1 ], 10 ) ) / 255,
- Math.min( 255, parseInt( color[ 2 ], 10 ) ) / 255,
- Math.min( 255, parseInt( color[ 3 ], 10 ) ) / 255,
- colorSpace
- );
-
- }
-
- if ( color = /^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) {
-
- // rgb(100%,0%,0%) rgba(100%,0%,0%,0.5)
-
- handleAlpha( color[ 4 ] );
-
- return this.setRGB(
- Math.min( 100, parseInt( color[ 1 ], 10 ) ) / 100,
- Math.min( 100, parseInt( color[ 2 ], 10 ) ) / 100,
- Math.min( 100, parseInt( color[ 3 ], 10 ) ) / 100,
- colorSpace
- );
-
- }
-
- break;
-
- case 'hsl':
- case 'hsla':
-
- if ( color = /^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) {
-
- // hsl(120,50%,50%) hsla(120,50%,50%,0.5)
-
- handleAlpha( color[ 4 ] );
-
- return this.setHSL(
- parseFloat( color[ 1 ] ) / 360,
- parseFloat( color[ 2 ] ) / 100,
- parseFloat( color[ 3 ] ) / 100,
- colorSpace
- );
-
- }
-
- break;
-
- default:
-
- warn( 'Color: Unknown color model ' + style );
-
- }
-
- } else if ( m = /^\#([A-Fa-f\d]+)$/.exec( style ) ) {
-
- // hex color
-
- const hex = m[ 1 ];
- const size = hex.length;
-
- if ( size === 3 ) {
-
- // #ff0
- return this.setRGB(
- parseInt( hex.charAt( 0 ), 16 ) / 15,
- parseInt( hex.charAt( 1 ), 16 ) / 15,
- parseInt( hex.charAt( 2 ), 16 ) / 15,
- colorSpace
- );
-
- } else if ( size === 6 ) {
-
- // #ff0000
- return this.setHex( parseInt( hex, 16 ), colorSpace );
-
- } else {
-
- warn( 'Color: Invalid hex color ' + style );
-
- }
-
- } else if ( style && style.length > 0 ) {
-
- return this.setColorName( style, colorSpace );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets this color from a color name. Faster than {@link Color#setStyle} if
- * you don't need the other CSS-style formats.
- *
- * For convenience, the list of names is exposed in `Color.NAMES` as a hash.
- * ```js
- * Color.NAMES.aliceblue // returns 0xF0F8FF
- * ```
- *
- * @param {string} style - The color name.
- * @param {string} [colorSpace=SRGBColorSpace] - The color space.
- * @return {Color} A reference to this color.
- */
- setColorName( style, colorSpace = SRGBColorSpace ) {
-
- // color keywords
- const hex = _colorKeywords[ style.toLowerCase() ];
-
- if ( hex !== undefined ) {
-
- // red
- this.setHex( hex, colorSpace );
-
- } else {
-
- // unknown color
- warn( 'Color: Unknown color ' + style );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns a new color with copied values from this instance.
- *
- * @return {Color} A clone of this instance.
- */
- clone() {
-
- return new this.constructor( this.r, this.g, this.b );
-
- }
-
- /**
- * Copies the values of the given color to this instance.
- *
- * @param {Color} color - The color to copy.
- * @return {Color} A reference to this color.
- */
- copy( color ) {
-
- this.r = color.r;
- this.g = color.g;
- this.b = color.b;
-
- return this;
-
- }
-
- /**
- * Copies the given color into this color, and then converts this color from
- * `SRGBColorSpace` to `LinearSRGBColorSpace`.
- *
- * @param {Color} color - The color to copy/convert.
- * @return {Color} A reference to this color.
- */
- copySRGBToLinear( color ) {
-
- this.r = SRGBToLinear( color.r );
- this.g = SRGBToLinear( color.g );
- this.b = SRGBToLinear( color.b );
-
- return this;
-
- }
-
- /**
- * Copies the given color into this color, and then converts this color from
- * `LinearSRGBColorSpace` to `SRGBColorSpace`.
- *
- * @param {Color} color - The color to copy/convert.
- * @return {Color} A reference to this color.
- */
- copyLinearToSRGB( color ) {
-
- this.r = LinearToSRGB( color.r );
- this.g = LinearToSRGB( color.g );
- this.b = LinearToSRGB( color.b );
-
- return this;
-
- }
-
- /**
- * Converts this color from `SRGBColorSpace` to `LinearSRGBColorSpace`.
- *
- * @return {Color} A reference to this color.
- */
- convertSRGBToLinear() {
-
- this.copySRGBToLinear( this );
-
- return this;
-
- }
-
- /**
- * Converts this color from `LinearSRGBColorSpace` to `SRGBColorSpace`.
- *
- * @return {Color} A reference to this color.
- */
- convertLinearToSRGB() {
-
- this.copyLinearToSRGB( this );
-
- return this;
-
- }
-
- /**
- * Returns the hexadecimal value of this color.
- *
- * @param {string} [colorSpace=SRGBColorSpace] - The color space.
- * @return {number} The hexadecimal value.
- */
- getHex( colorSpace = SRGBColorSpace ) {
-
- ColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );
-
- return Math.round( clamp( _color.r * 255, 0, 255 ) ) * 65536 + Math.round( clamp( _color.g * 255, 0, 255 ) ) * 256 + Math.round( clamp( _color.b * 255, 0, 255 ) );
-
- }
-
- /**
- * Returns the hexadecimal value of this color as a string (for example, 'FFFFFF').
- *
- * @param {string} [colorSpace=SRGBColorSpace] - The color space.
- * @return {string} The hexadecimal value as a string.
- */
- getHexString( colorSpace = SRGBColorSpace ) {
-
- return ( '000000' + this.getHex( colorSpace ).toString( 16 ) ).slice( -6 );
-
- }
-
- /**
- * Converts the colors RGB values into the HSL format and stores them into the
- * given target object.
- *
- * @param {{h:number,s:number,l:number}} target - The target object that is used to store the method's result.
- * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.
- * @return {{h:number,s:number,l:number}} The HSL representation of this color.
- */
- getHSL( target, colorSpace = ColorManagement.workingColorSpace ) {
-
- // h,s,l ranges are in 0.0 - 1.0
-
- ColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );
-
- const r = _color.r, g = _color.g, b = _color.b;
-
- const max = Math.max( r, g, b );
- const min = Math.min( r, g, b );
-
- let hue, saturation;
- const lightness = ( min + max ) / 2.0;
-
- if ( min === max ) {
-
- hue = 0;
- saturation = 0;
-
- } else {
-
- const delta = max - min;
-
- saturation = lightness <= 0.5 ? delta / ( max + min ) : delta / ( 2 - max - min );
-
- switch ( max ) {
-
- case r: hue = ( g - b ) / delta + ( g < b ? 6 : 0 ); break;
- case g: hue = ( b - r ) / delta + 2; break;
- case b: hue = ( r - g ) / delta + 4; break;
-
- }
-
- hue /= 6;
-
- }
-
- target.h = hue;
- target.s = saturation;
- target.l = lightness;
-
- return target;
-
- }
-
- /**
- * Returns the RGB values of this color and stores them into the given target object.
- *
- * @param {Color} target - The target color that is used to store the method's result.
- * @param {string} [colorSpace=ColorManagement.workingColorSpace] - The color space.
- * @return {Color} The RGB representation of this color.
- */
- getRGB( target, colorSpace = ColorManagement.workingColorSpace ) {
-
- ColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );
-
- target.r = _color.r;
- target.g = _color.g;
- target.b = _color.b;
-
- return target;
-
- }
-
- /**
- * Returns the value of this color as a CSS style string. Example: `rgb(255,0,0)`.
- *
- * @param {string} [colorSpace=SRGBColorSpace] - The color space.
- * @return {string} The CSS representation of this color.
- */
- getStyle( colorSpace = SRGBColorSpace ) {
-
- ColorManagement.workingToColorSpace( _color.copy( this ), colorSpace );
-
- const r = _color.r, g = _color.g, b = _color.b;
-
- if ( colorSpace !== SRGBColorSpace ) {
-
- // Requires CSS Color Module Level 4 (https://www.w3.org/TR/css-color-4/).
- return `color(${ colorSpace } ${ r.toFixed( 3 ) } ${ g.toFixed( 3 ) } ${ b.toFixed( 3 ) })`;
-
- }
-
- return `rgb(${ Math.round( r * 255 ) },${ Math.round( g * 255 ) },${ Math.round( b * 255 ) })`;
-
- }
-
- /**
- * Adds the given HSL values to this color's values.
- * Internally, this converts the color's RGB values to HSL, adds HSL
- * and then converts the color back to RGB.
- *
- * @param {number} h - Hue value between `0.0` and `1.0`.
- * @param {number} s - Saturation value between `0.0` and `1.0`.
- * @param {number} l - Lightness value between `0.0` and `1.0`.
- * @return {Color} A reference to this color.
- */
- offsetHSL( h, s, l ) {
-
- this.getHSL( _hslA );
-
- return this.setHSL( _hslA.h + h, _hslA.s + s, _hslA.l + l );
-
- }
-
- /**
- * Adds the RGB values of the given color to the RGB values of this color.
- *
- * @param {Color} color - The color to add.
- * @return {Color} A reference to this color.
- */
- add( color ) {
-
- this.r += color.r;
- this.g += color.g;
- this.b += color.b;
-
- return this;
-
- }
-
- /**
- * Adds the RGB values of the given colors and stores the result in this instance.
- *
- * @param {Color} color1 - The first color.
- * @param {Color} color2 - The second color.
- * @return {Color} A reference to this color.
- */
- addColors( color1, color2 ) {
-
- this.r = color1.r + color2.r;
- this.g = color1.g + color2.g;
- this.b = color1.b + color2.b;
-
- return this;
-
- }
-
- /**
- * Adds the given scalar value to the RGB values of this color.
- *
- * @param {number} s - The scalar to add.
- * @return {Color} A reference to this color.
- */
- addScalar( s ) {
-
- this.r += s;
- this.g += s;
- this.b += s;
-
- return this;
-
- }
-
- /**
- * Subtracts the RGB values of the given color from the RGB values of this color.
- *
- * @param {Color} color - The color to subtract.
- * @return {Color} A reference to this color.
- */
- sub( color ) {
-
- this.r = Math.max( 0, this.r - color.r );
- this.g = Math.max( 0, this.g - color.g );
- this.b = Math.max( 0, this.b - color.b );
-
- return this;
-
- }
-
- /**
- * Multiplies the RGB values of the given color with the RGB values of this color.
- *
- * @param {Color} color - The color to multiply.
- * @return {Color} A reference to this color.
- */
- multiply( color ) {
-
- this.r *= color.r;
- this.g *= color.g;
- this.b *= color.b;
-
- return this;
-
- }
-
- /**
- * Multiplies the given scalar value with the RGB values of this color.
- *
- * @param {number} s - The scalar to multiply.
- * @return {Color} A reference to this color.
- */
- multiplyScalar( s ) {
-
- this.r *= s;
- this.g *= s;
- this.b *= s;
-
- return this;
-
- }
-
- /**
- * Linearly interpolates this color's RGB values toward the RGB values of the
- * given color. The alpha argument can be thought of as the ratio between
- * the two colors, where `0.0` is this color and `1.0` is the first argument.
- *
- * @param {Color} color - The color to converge on.
- * @param {number} alpha - The interpolation factor in the closed interval `[0,1]`.
- * @return {Color} A reference to this color.
- */
- lerp( color, alpha ) {
-
- this.r += ( color.r - this.r ) * alpha;
- this.g += ( color.g - this.g ) * alpha;
- this.b += ( color.b - this.b ) * alpha;
-
- return this;
-
- }
-
- /**
- * Linearly interpolates between the given colors and stores the result in this instance.
- * The alpha argument can be thought of as the ratio between the two colors, where `0.0`
- * is the first and `1.0` is the second color.
- *
- * @param {Color} color1 - The first color.
- * @param {Color} color2 - The second color.
- * @param {number} alpha - The interpolation factor in the closed interval `[0,1]`.
- * @return {Color} A reference to this color.
- */
- lerpColors( color1, color2, alpha ) {
-
- this.r = color1.r + ( color2.r - color1.r ) * alpha;
- this.g = color1.g + ( color2.g - color1.g ) * alpha;
- this.b = color1.b + ( color2.b - color1.b ) * alpha;
-
- return this;
-
- }
-
- /**
- * Linearly interpolates this color's HSL values toward the HSL values of the
- * given color. It differs from {@link Color#lerp} by not interpolating straight
- * from one color to the other, but instead going through all the hues in between
- * those two colors. The alpha argument can be thought of as the ratio between
- * the two colors, where 0.0 is this color and 1.0 is the first argument.
- *
- * @param {Color} color - The color to converge on.
- * @param {number} alpha - The interpolation factor in the closed interval `[0,1]`.
- * @return {Color} A reference to this color.
- */
- lerpHSL( color, alpha ) {
-
- this.getHSL( _hslA );
- color.getHSL( _hslB );
-
- const h = lerp( _hslA.h, _hslB.h, alpha );
- const s = lerp( _hslA.s, _hslB.s, alpha );
- const l = lerp( _hslA.l, _hslB.l, alpha );
-
- this.setHSL( h, s, l );
-
- return this;
-
- }
-
- /**
- * Sets the color's RGB components from the given 3D vector.
- *
- * @param {Vector3} v - The vector to set.
- * @return {Color} A reference to this color.
- */
- setFromVector3( v ) {
-
- this.r = v.x;
- this.g = v.y;
- this.b = v.z;
-
- return this;
-
- }
-
- /**
- * Transforms this color with the given 3x3 matrix.
- *
- * @param {Matrix3} m - The matrix.
- * @return {Color} A reference to this color.
- */
- applyMatrix3( m ) {
-
- const r = this.r, g = this.g, b = this.b;
- const e = m.elements;
-
- this.r = e[ 0 ] * r + e[ 3 ] * g + e[ 6 ] * b;
- this.g = e[ 1 ] * r + e[ 4 ] * g + e[ 7 ] * b;
- this.b = e[ 2 ] * r + e[ 5 ] * g + e[ 8 ] * b;
-
- return this;
-
- }
-
- /**
- * Returns `true` if this color is equal with the given one.
- *
- * @param {Color} c - The color to test for equality.
- * @return {boolean} Whether this bounding color is equal with the given one.
- */
- equals( c ) {
-
- return ( c.r === this.r ) && ( c.g === this.g ) && ( c.b === this.b );
-
- }
-
- /**
- * Sets this color's RGB components from the given array.
- *
- * @param {Array} array - An array holding the RGB values.
- * @param {number} [offset=0] - The offset into the array.
- * @return {Color} A reference to this color.
- */
- fromArray( array, offset = 0 ) {
-
- this.r = array[ offset ];
- this.g = array[ offset + 1 ];
- this.b = array[ offset + 2 ];
-
- return this;
-
- }
-
- /**
- * Writes the RGB components of this color to the given array. If no array is provided,
- * the method returns a new instance.
- *
- * @param {Array} [array=[]] - The target array holding the color components.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Array} The color components.
- */
- toArray( array = [], offset = 0 ) {
-
- array[ offset ] = this.r;
- array[ offset + 1 ] = this.g;
- array[ offset + 2 ] = this.b;
-
- return array;
-
- }
-
- /**
- * Sets the components of this color from the given buffer attribute.
- *
- * @param {BufferAttribute} attribute - The buffer attribute holding color data.
- * @param {number} index - The index into the attribute.
- * @return {Color} A reference to this color.
- */
- fromBufferAttribute( attribute, index ) {
-
- this.r = attribute.getX( index );
- this.g = attribute.getY( index );
- this.b = attribute.getZ( index );
-
- return this;
-
- }
-
- /**
- * This methods defines the serialization result of this class. Returns the color
- * as a hexadecimal value.
- *
- * @return {number} The hexadecimal value.
- */
- toJSON() {
-
- return this.getHex();
-
- }
-
- *[ Symbol.iterator ]() {
-
- yield this.r;
- yield this.g;
- yield this.b;
-
- }
-
-}
-
-const _color = /*@__PURE__*/ new Color();
-
-/**
- * A dictionary with X11 color names.
- *
- * Note that multiple words such as Dark Orange become the string 'darkorange'.
- *
- * @static
- * @type {Object}
- */
-Color.NAMES = _colorKeywords;
-
-/**
- * This class can be used to define an exponential squared fog,
- * which gives a clear view near the camera and a faster than exponentially
- * densening fog farther from the camera.
- *
- * ```js
- * const scene = new THREE.Scene();
- * scene.fog = new THREE.FogExp2( 0xcccccc, 0.002 );
- * ```
- */
-class FogExp2 {
-
- /**
- * Constructs a new fog.
- *
- * @param {number|Color} color - The fog's color.
- * @param {number} [density=0.00025] - Defines how fast the fog will grow dense.
- */
- constructor( color, density = 0.00025 ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isFogExp2 = true;
-
- /**
- * The name of the fog.
- *
- * @type {string}
- */
- this.name = '';
-
- /**
- * The fog's color.
- *
- * @type {Color}
- */
- this.color = new Color( color );
-
- /**
- * Defines how fast the fog will grow dense.
- *
- * @type {number}
- * @default 0.00025
- */
- this.density = density;
-
- }
-
- /**
- * Returns a new fog with copied values from this instance.
- *
- * @return {FogExp2} A clone of this instance.
- */
- clone() {
-
- return new FogExp2( this.color, this.density );
-
- }
-
- /**
- * Serializes the fog into JSON.
- *
- * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
- * @return {Object} A JSON object representing the serialized fog
- */
- toJSON( /* meta */ ) {
-
- return {
- type: 'FogExp2',
- name: this.name,
- color: this.color.getHex(),
- density: this.density
- };
-
- }
-
-}
-
-/**
- * This class can be used to define a linear fog that grows linearly denser
- * with the distance.
- *
- * ```js
- * const scene = new THREE.Scene();
- * scene.fog = new THREE.Fog( 0xcccccc, 10, 15 );
- * ```
- */
-class Fog {
-
- /**
- * Constructs a new fog.
- *
- * @param {number|Color} color - The fog's color.
- * @param {number} [near=1] - The minimum distance to start applying fog.
- * @param {number} [far=1000] - The maximum distance at which fog stops being calculated and applied.
- */
- constructor( color, near = 1, far = 1000 ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isFog = true;
-
- /**
- * The name of the fog.
- *
- * @type {string}
- */
- this.name = '';
-
- /**
- * The fog's color.
- *
- * @type {Color}
- */
- this.color = new Color( color );
-
- /**
- * The minimum distance to start applying fog. Objects that are less than
- * `near` units from the active camera won't be affected by fog.
- *
- * @type {number}
- * @default 1
- */
- this.near = near;
-
- /**
- * The maximum distance at which fog stops being calculated and applied.
- * Objects that are more than `far` units away from the active camera won't
- * be affected by fog.
- *
- * @type {number}
- * @default 1000
- */
- this.far = far;
-
- }
-
- /**
- * Returns a new fog with copied values from this instance.
- *
- * @return {Fog} A clone of this instance.
- */
- clone() {
-
- return new Fog( this.color, this.near, this.far );
-
- }
-
- /**
- * Serializes the fog into JSON.
- *
- * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
- * @return {Object} A JSON object representing the serialized fog
- */
- toJSON( /* meta */ ) {
-
- return {
- type: 'Fog',
- name: this.name,
- color: this.color.getHex(),
- near: this.near,
- far: this.far
- };
-
- }
-
-}
-
-/**
- * Scenes allow you to set up what is to be rendered and where by three.js.
- * This is where you place 3D objects like meshes, lines or lights.
- *
- * @augments Object3D
- */
-class Scene extends Object3D {
-
- /**
- * Constructs a new scene.
- */
- constructor() {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isScene = true;
-
- this.type = 'Scene';
-
- /**
- * Defines the background of the scene. Valid inputs are:
- *
- * - A color for defining a uniform colored background.
- * - A texture for defining a (flat) textured background.
- * - Cube textures or equirectangular textures for defining a skybox.
- *
- * @type {?(Color|Texture)}
- * @default null
- */
- this.background = null;
-
- /**
- * Sets the environment map for all physical materials in the scene. However,
- * it's not possible to overwrite an existing texture assigned to the `envMap`
- * material property.
- *
- * @type {?Texture}
- * @default null
- */
- this.environment = null;
-
- /**
- * A fog instance defining the type of fog that affects everything
- * rendered in the scene.
- *
- * @type {?(Fog|FogExp2)}
- * @default null
- */
- this.fog = null;
-
- /**
- * Sets the blurriness of the background. Only influences environment maps
- * assigned to {@link Scene#background}. Valid input is a float between `0`
- * and `1`.
- *
- * @type {number}
- * @default 0
- */
- this.backgroundBlurriness = 0;
-
- /**
- * Attenuates the color of the background. Only applies to background textures.
- *
- * @type {number}
- * @default 1
- */
- this.backgroundIntensity = 1;
-
- /**
- * The rotation of the background in radians. Only influences environment maps
- * assigned to {@link Scene#background}.
- *
- * @type {Euler}
- * @default (0,0,0)
- */
- this.backgroundRotation = new Euler();
-
- /**
- * Attenuates the color of the environment. Only influences environment maps
- * assigned to {@link Scene#environment}.
- *
- * @type {number}
- * @default 1
- */
- this.environmentIntensity = 1;
-
- /**
- * The rotation of the environment map in radians. Only influences physical materials
- * in the scene when {@link Scene#environment} is used.
- *
- * @type {Euler}
- * @default (0,0,0)
- */
- this.environmentRotation = new Euler();
-
- /**
- * Forces everything in the scene to be rendered with the defined material. It is possible
- * to exclude materials from override by setting {@link Material#allowOverride} to `false`.
- *
- * @type {?Material}
- * @default null
- */
- this.overrideMaterial = null;
-
- if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {
-
- __THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) );
-
- }
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- if ( source.background !== null ) this.background = source.background.clone();
- if ( source.environment !== null ) this.environment = source.environment.clone();
- if ( source.fog !== null ) this.fog = source.fog.clone();
-
- this.backgroundBlurriness = source.backgroundBlurriness;
- this.backgroundIntensity = source.backgroundIntensity;
- this.backgroundRotation.copy( source.backgroundRotation );
-
- this.environmentIntensity = source.environmentIntensity;
- this.environmentRotation.copy( source.environmentRotation );
-
- if ( source.overrideMaterial !== null ) this.overrideMaterial = source.overrideMaterial.clone();
-
- this.matrixAutoUpdate = source.matrixAutoUpdate;
-
- return this;
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- if ( this.fog !== null ) data.object.fog = this.fog.toJSON();
-
- data.object.backgroundBlurriness = this.backgroundBlurriness;
- data.object.backgroundIntensity = this.backgroundIntensity;
- data.object.backgroundRotation = this.backgroundRotation.toArray();
-
- data.object.environmentIntensity = this.environmentIntensity;
- data.object.environmentRotation = this.environmentRotation.toArray();
-
- return data;
-
- }
-
-}
-
-const _v0$2 = /*@__PURE__*/ new Vector3();
-const _v1$5 = /*@__PURE__*/ new Vector3();
-const _v2$4 = /*@__PURE__*/ new Vector3();
-const _v3$2 = /*@__PURE__*/ new Vector3();
-
-const _vab = /*@__PURE__*/ new Vector3();
-const _vac = /*@__PURE__*/ new Vector3();
-const _vbc = /*@__PURE__*/ new Vector3();
-const _vap = /*@__PURE__*/ new Vector3();
-const _vbp = /*@__PURE__*/ new Vector3();
-const _vcp = /*@__PURE__*/ new Vector3();
-
-const _v40 = /*@__PURE__*/ new Vector4();
-const _v41 = /*@__PURE__*/ new Vector4();
-const _v42 = /*@__PURE__*/ new Vector4();
-
-/**
- * A geometric triangle as defined by three vectors representing its three corners.
- */
-class Triangle {
-
- /**
- * Constructs a new triangle.
- *
- * @param {Vector3} [a=(0,0,0)] - The first corner of the triangle.
- * @param {Vector3} [b=(0,0,0)] - The second corner of the triangle.
- * @param {Vector3} [c=(0,0,0)] - The third corner of the triangle.
- */
- constructor( a = new Vector3(), b = new Vector3(), c = new Vector3() ) {
-
- /**
- * The first corner of the triangle.
- *
- * @type {Vector3}
- */
- this.a = a;
-
- /**
- * The second corner of the triangle.
- *
- * @type {Vector3}
- */
- this.b = b;
-
- /**
- * The third corner of the triangle.
- *
- * @type {Vector3}
- */
- this.c = c;
-
- }
-
- /**
- * Computes the normal vector of a triangle.
- *
- * @param {Vector3} a - The first corner of the triangle.
- * @param {Vector3} b - The second corner of the triangle.
- * @param {Vector3} c - The third corner of the triangle.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The triangle's normal.
- */
- static getNormal( a, b, c, target ) {
-
- target.subVectors( c, b );
- _v0$2.subVectors( a, b );
- target.cross( _v0$2 );
-
- const targetLengthSq = target.lengthSq();
- if ( targetLengthSq > 0 ) {
-
- return target.multiplyScalar( 1 / Math.sqrt( targetLengthSq ) );
-
- }
-
- return target.set( 0, 0, 0 );
-
- }
-
- /**
- * Computes a barycentric coordinates from the given vector.
- * Returns `null` if the triangle is degenerate.
- *
- * @param {Vector3} point - A point in 3D space.
- * @param {Vector3} a - The first corner of the triangle.
- * @param {Vector3} b - The second corner of the triangle.
- * @param {Vector3} c - The third corner of the triangle.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {?Vector3} The barycentric coordinates for the given point
- */
- static getBarycoord( point, a, b, c, target ) {
-
- _v0$2.subVectors( c, a );
- _v1$5.subVectors( b, a );
- _v2$4.subVectors( point, a );
-
- const dot00 = _v0$2.dot( _v0$2 );
- const dot01 = _v0$2.dot( _v1$5 );
- const dot02 = _v0$2.dot( _v2$4 );
- const dot11 = _v1$5.dot( _v1$5 );
- const dot12 = _v1$5.dot( _v2$4 );
-
- const denom = ( dot00 * dot11 - dot01 * dot01 );
-
- // collinear or singular triangle
- if ( denom === 0 ) {
-
- target.set( 0, 0, 0 );
- return null;
-
- }
-
- const invDenom = 1 / denom;
- const u = ( dot11 * dot02 - dot01 * dot12 ) * invDenom;
- const v = ( dot00 * dot12 - dot01 * dot02 ) * invDenom;
-
- // barycentric coordinates must always sum to 1
- return target.set( 1 - u - v, v, u );
-
- }
-
- /**
- * Returns `true` if the given point, when projected onto the plane of the
- * triangle, lies within the triangle.
- *
- * @param {Vector3} point - The point in 3D space to test.
- * @param {Vector3} a - The first corner of the triangle.
- * @param {Vector3} b - The second corner of the triangle.
- * @param {Vector3} c - The third corner of the triangle.
- * @return {boolean} Whether the given point, when projected onto the plane of the
- * triangle, lies within the triangle or not.
- */
- static containsPoint( point, a, b, c ) {
-
- // if the triangle is degenerate then we can't contain a point
- if ( this.getBarycoord( point, a, b, c, _v3$2 ) === null ) {
-
- return false;
-
- }
-
- return ( _v3$2.x >= 0 ) && ( _v3$2.y >= 0 ) && ( ( _v3$2.x + _v3$2.y ) <= 1 );
-
- }
-
- /**
- * Computes the value barycentrically interpolated for the given point on the
- * triangle. Returns `null` if the triangle is degenerate.
- *
- * @param {Vector3} point - Position of interpolated point.
- * @param {Vector3} p1 - The first corner of the triangle.
- * @param {Vector3} p2 - The second corner of the triangle.
- * @param {Vector3} p3 - The third corner of the triangle.
- * @param {Vector3} v1 - Value to interpolate of first vertex.
- * @param {Vector3} v2 - Value to interpolate of second vertex.
- * @param {Vector3} v3 - Value to interpolate of third vertex.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {?Vector3} The interpolated value.
- */
- static getInterpolation( point, p1, p2, p3, v1, v2, v3, target ) {
-
- if ( this.getBarycoord( point, p1, p2, p3, _v3$2 ) === null ) {
-
- target.x = 0;
- target.y = 0;
- if ( 'z' in target ) target.z = 0;
- if ( 'w' in target ) target.w = 0;
- return null;
-
- }
-
- target.setScalar( 0 );
- target.addScaledVector( v1, _v3$2.x );
- target.addScaledVector( v2, _v3$2.y );
- target.addScaledVector( v3, _v3$2.z );
-
- return target;
-
- }
-
- /**
- * Computes the value barycentrically interpolated for the given attribute and indices.
- *
- * @param {BufferAttribute} attr - The attribute to interpolate.
- * @param {number} i1 - Index of first vertex.
- * @param {number} i2 - Index of second vertex.
- * @param {number} i3 - Index of third vertex.
- * @param {Vector3} barycoord - The barycoordinate value to use to interpolate.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The interpolated attribute value.
- */
- static getInterpolatedAttribute( attr, i1, i2, i3, barycoord, target ) {
-
- _v40.setScalar( 0 );
- _v41.setScalar( 0 );
- _v42.setScalar( 0 );
-
- _v40.fromBufferAttribute( attr, i1 );
- _v41.fromBufferAttribute( attr, i2 );
- _v42.fromBufferAttribute( attr, i3 );
-
- target.setScalar( 0 );
- target.addScaledVector( _v40, barycoord.x );
- target.addScaledVector( _v41, barycoord.y );
- target.addScaledVector( _v42, barycoord.z );
-
- return target;
-
- }
-
- /**
- * Returns `true` if the triangle is oriented towards the given direction.
- *
- * @param {Vector3} a - The first corner of the triangle.
- * @param {Vector3} b - The second corner of the triangle.
- * @param {Vector3} c - The third corner of the triangle.
- * @param {Vector3} direction - The (normalized) direction vector.
- * @return {boolean} Whether the triangle is oriented towards the given direction or not.
- */
- static isFrontFacing( a, b, c, direction ) {
-
- _v0$2.subVectors( c, b );
- _v1$5.subVectors( a, b );
-
- // strictly front facing
- return _v0$2.cross( _v1$5 ).dot( direction ) < 0;
-
- }
-
- /**
- * Sets the triangle's vertices by copying the given values.
- *
- * @param {Vector3} a - The first corner of the triangle.
- * @param {Vector3} b - The second corner of the triangle.
- * @param {Vector3} c - The third corner of the triangle.
- * @return {Triangle} A reference to this triangle.
- */
- set( a, b, c ) {
-
- this.a.copy( a );
- this.b.copy( b );
- this.c.copy( c );
-
- return this;
-
- }
-
- /**
- * Sets the triangle's vertices by copying the given array values.
- *
- * @param {Array} points - An array with 3D points.
- * @param {number} i0 - The array index representing the first corner of the triangle.
- * @param {number} i1 - The array index representing the second corner of the triangle.
- * @param {number} i2 - The array index representing the third corner of the triangle.
- * @return {Triangle} A reference to this triangle.
- */
- setFromPointsAndIndices( points, i0, i1, i2 ) {
-
- this.a.copy( points[ i0 ] );
- this.b.copy( points[ i1 ] );
- this.c.copy( points[ i2 ] );
-
- return this;
-
- }
-
- /**
- * Sets the triangle's vertices by copying the given attribute values.
- *
- * @param {BufferAttribute} attribute - A buffer attribute with 3D points data.
- * @param {number} i0 - The attribute index representing the first corner of the triangle.
- * @param {number} i1 - The attribute index representing the second corner of the triangle.
- * @param {number} i2 - The attribute index representing the third corner of the triangle.
- * @return {Triangle} A reference to this triangle.
- */
- setFromAttributeAndIndices( attribute, i0, i1, i2 ) {
-
- this.a.fromBufferAttribute( attribute, i0 );
- this.b.fromBufferAttribute( attribute, i1 );
- this.c.fromBufferAttribute( attribute, i2 );
-
- return this;
-
- }
-
- /**
- * Returns a new triangle with copied values from this instance.
- *
- * @return {Triangle} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Copies the values of the given triangle to this instance.
- *
- * @param {Triangle} triangle - The triangle to copy.
- * @return {Triangle} A reference to this triangle.
- */
- copy( triangle ) {
-
- this.a.copy( triangle.a );
- this.b.copy( triangle.b );
- this.c.copy( triangle.c );
-
- return this;
-
- }
-
- /**
- * Computes the area of the triangle.
- *
- * @return {number} The triangle's area.
- */
- getArea() {
-
- _v0$2.subVectors( this.c, this.b );
- _v1$5.subVectors( this.a, this.b );
-
- return _v0$2.cross( _v1$5 ).length() * 0.5;
-
- }
-
- /**
- * Computes the midpoint of the triangle.
- *
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The triangle's midpoint.
- */
- getMidpoint( target ) {
-
- return target.addVectors( this.a, this.b ).add( this.c ).multiplyScalar( 1 / 3 );
-
- }
-
- /**
- * Computes the normal of the triangle.
- *
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The triangle's normal.
- */
- getNormal( target ) {
-
- return Triangle.getNormal( this.a, this.b, this.c, target );
-
- }
-
- /**
- * Computes a plane the triangle lies within.
- *
- * @param {Plane} target - The target vector that is used to store the method's result.
- * @return {Plane} The plane the triangle lies within.
- */
- getPlane( target ) {
-
- return target.setFromCoplanarPoints( this.a, this.b, this.c );
-
- }
-
- /**
- * Computes a barycentric coordinates from the given vector.
- * Returns `null` if the triangle is degenerate.
- *
- * @param {Vector3} point - A point in 3D space.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {?Vector3} The barycentric coordinates for the given point
- */
- getBarycoord( point, target ) {
-
- return Triangle.getBarycoord( point, this.a, this.b, this.c, target );
-
- }
-
- /**
- * Computes the value barycentrically interpolated for the given point on the
- * triangle. Returns `null` if the triangle is degenerate.
- *
- * @param {Vector3} point - Position of interpolated point.
- * @param {Vector3} v1 - Value to interpolate of first vertex.
- * @param {Vector3} v2 - Value to interpolate of second vertex.
- * @param {Vector3} v3 - Value to interpolate of third vertex.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {?Vector3} The interpolated value.
- */
- getInterpolation( point, v1, v2, v3, target ) {
-
- return Triangle.getInterpolation( point, this.a, this.b, this.c, v1, v2, v3, target );
-
- }
-
- /**
- * Returns `true` if the given point, when projected onto the plane of the
- * triangle, lies within the triangle.
- *
- * @param {Vector3} point - The point in 3D space to test.
- * @return {boolean} Whether the given point, when projected onto the plane of the
- * triangle, lies within the triangle or not.
- */
- containsPoint( point ) {
-
- return Triangle.containsPoint( point, this.a, this.b, this.c );
-
- }
-
- /**
- * Returns `true` if the triangle is oriented towards the given direction.
- *
- * @param {Vector3} direction - The (normalized) direction vector.
- * @return {boolean} Whether the triangle is oriented towards the given direction or not.
- */
- isFrontFacing( direction ) {
-
- return Triangle.isFrontFacing( this.a, this.b, this.c, direction );
-
- }
-
- /**
- * Returns `true` if this triangle intersects with the given box.
- *
- * @param {Box3} box - The box to intersect.
- * @return {boolean} Whether this triangle intersects with the given box or not.
- */
- intersectsBox( box ) {
-
- return box.intersectsTriangle( this );
-
- }
-
- /**
- * Returns the closest point on the triangle to the given point.
- *
- * @param {Vector3} p - The point to compute the closest point for.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The closest point on the triangle.
- */
- closestPointToPoint( p, target ) {
-
- const a = this.a, b = this.b, c = this.c;
- let v, w;
-
- // algorithm thanks to Real-Time Collision Detection by Christer Ericson,
- // published by Morgan Kaufmann Publishers, (c) 2005 Elsevier Inc.,
- // under the accompanying license; see chapter 5.1.5 for detailed explanation.
- // basically, we're distinguishing which of the voronoi regions of the triangle
- // the point lies in with the minimum amount of redundant computation.
-
- _vab.subVectors( b, a );
- _vac.subVectors( c, a );
- _vap.subVectors( p, a );
- const d1 = _vab.dot( _vap );
- const d2 = _vac.dot( _vap );
- if ( d1 <= 0 && d2 <= 0 ) {
-
- // vertex region of A; barycentric coords (1, 0, 0)
- return target.copy( a );
-
- }
-
- _vbp.subVectors( p, b );
- const d3 = _vab.dot( _vbp );
- const d4 = _vac.dot( _vbp );
- if ( d3 >= 0 && d4 <= d3 ) {
-
- // vertex region of B; barycentric coords (0, 1, 0)
- return target.copy( b );
-
- }
-
- const vc = d1 * d4 - d3 * d2;
- if ( vc <= 0 && d1 >= 0 && d3 <= 0 ) {
-
- v = d1 / ( d1 - d3 );
- // edge region of AB; barycentric coords (1-v, v, 0)
- return target.copy( a ).addScaledVector( _vab, v );
-
- }
-
- _vcp.subVectors( p, c );
- const d5 = _vab.dot( _vcp );
- const d6 = _vac.dot( _vcp );
- if ( d6 >= 0 && d5 <= d6 ) {
-
- // vertex region of C; barycentric coords (0, 0, 1)
- return target.copy( c );
-
- }
-
- const vb = d5 * d2 - d1 * d6;
- if ( vb <= 0 && d2 >= 0 && d6 <= 0 ) {
-
- w = d2 / ( d2 - d6 );
- // edge region of AC; barycentric coords (1-w, 0, w)
- return target.copy( a ).addScaledVector( _vac, w );
-
- }
-
- const va = d3 * d6 - d5 * d4;
- if ( va <= 0 && ( d4 - d3 ) >= 0 && ( d5 - d6 ) >= 0 ) {
-
- _vbc.subVectors( c, b );
- w = ( d4 - d3 ) / ( ( d4 - d3 ) + ( d5 - d6 ) );
- // edge region of BC; barycentric coords (0, 1-w, w)
- return target.copy( b ).addScaledVector( _vbc, w ); // edge region of BC
-
- }
-
- // face region
- const denom = 1 / ( va + vb + vc );
- // u = va * denom
- v = vb * denom;
- w = vc * denom;
-
- return target.copy( a ).addScaledVector( _vab, v ).addScaledVector( _vac, w );
-
- }
-
- /**
- * Returns `true` if this triangle is equal with the given one.
- *
- * @param {Triangle} triangle - The triangle to test for equality.
- * @return {boolean} Whether this triangle is equal with the given one.
- */
- equals( triangle ) {
-
- return triangle.a.equals( this.a ) && triangle.b.equals( this.b ) && triangle.c.equals( this.c );
-
- }
-
-}
-
-/**
- * Represents an axis-aligned bounding box (AABB) in 3D space.
- */
-class Box3 {
-
- /**
- * Constructs a new bounding box.
- *
- * @param {Vector3} [min=(Infinity,Infinity,Infinity)] - A vector representing the lower boundary of the box.
- * @param {Vector3} [max=(-Infinity,-Infinity,-Infinity)] - A vector representing the upper boundary of the box.
- */
- constructor( min = new Vector3( + Infinity, + Infinity, + Infinity ), max = new Vector3( - Infinity, - Infinity, - Infinity ) ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isBox3 = true;
-
- /**
- * The lower boundary of the box.
- *
- * @type {Vector3}
- */
- this.min = min;
-
- /**
- * The upper boundary of the box.
- *
- * @type {Vector3}
- */
- this.max = max;
-
- }
-
- /**
- * Sets the lower and upper boundaries of this box.
- * Please note that this method only copies the values from the given objects.
- *
- * @param {Vector3} min - The lower boundary of the box.
- * @param {Vector3} max - The upper boundary of the box.
- * @return {Box3} A reference to this bounding box.
- */
- set( min, max ) {
-
- this.min.copy( min );
- this.max.copy( max );
-
- return this;
-
- }
-
- /**
- * Sets the upper and lower bounds of this box so it encloses the position data
- * in the given array.
- *
- * @param {Array} array - An array holding 3D position data.
- * @return {Box3} A reference to this bounding box.
- */
- setFromArray( array ) {
-
- this.makeEmpty();
-
- for ( let i = 0, il = array.length; i < il; i += 3 ) {
-
- this.expandByPoint( _vector$b.fromArray( array, i ) );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the upper and lower bounds of this box so it encloses the position data
- * in the given buffer attribute.
- *
- * @param {BufferAttribute} attribute - A buffer attribute holding 3D position data.
- * @return {Box3} A reference to this bounding box.
- */
- setFromBufferAttribute( attribute ) {
-
- this.makeEmpty();
-
- for ( let i = 0, il = attribute.count; i < il; i ++ ) {
-
- this.expandByPoint( _vector$b.fromBufferAttribute( attribute, i ) );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the upper and lower bounds of this box so it encloses the position data
- * in the given array.
- *
- * @param {Array} points - An array holding 3D position data as instances of {@link Vector3}.
- * @return {Box3} A reference to this bounding box.
- */
- setFromPoints( points ) {
-
- this.makeEmpty();
-
- for ( let i = 0, il = points.length; i < il; i ++ ) {
-
- this.expandByPoint( points[ i ] );
-
- }
-
- return this;
-
- }
-
- /**
- * Centers this box on the given center vector and sets this box's width, height and
- * depth to the given size values.
- *
- * @param {Vector3} center - The center of the box.
- * @param {Vector3} size - The x, y and z dimensions of the box.
- * @return {Box3} A reference to this bounding box.
- */
- setFromCenterAndSize( center, size ) {
-
- const halfSize = _vector$b.copy( size ).multiplyScalar( 0.5 );
-
- this.min.copy( center ).sub( halfSize );
- this.max.copy( center ).add( halfSize );
-
- return this;
-
- }
-
- /**
- * Computes the world-axis-aligned bounding box for the given 3D object
- * (including its children), accounting for the object's, and children's,
- * world transforms. The function may result in a larger box than strictly necessary.
- *
- * Note: To compute the correct bounding box, make sure the given 3D object
- * has an up-to-date world matrix that reflects the current transformation of its
- * ancestor nodes. Call `object.updateWorldMatrix( true, false )` beforehand if
- * you're unsure.
- *
- * @param {Object3D} object - The 3D object to compute the bounding box for.
- * @param {boolean} [precise=false] - If set to `true`, the method computes the smallest
- * world-axis-aligned bounding box at the expense of more computation.
- * @return {Box3} A reference to this bounding box.
- */
- setFromObject( object, precise = false ) {
-
- this.makeEmpty();
-
- return this.expandByObject( object, precise );
-
- }
-
- /**
- * Returns a new box with copied values from this instance.
- *
- * @return {Box3} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Copies the values of the given box to this instance.
- *
- * @param {Box3} box - The box to copy.
- * @return {Box3} A reference to this bounding box.
- */
- copy( box ) {
-
- this.min.copy( box.min );
- this.max.copy( box.max );
-
- return this;
-
- }
-
- /**
- * Makes this box empty which means in encloses a zero space in 3D.
- *
- * @return {Box3} A reference to this bounding box.
- */
- makeEmpty() {
-
- this.min.x = this.min.y = this.min.z = + Infinity;
- this.max.x = this.max.y = this.max.z = - Infinity;
-
- return this;
-
- }
-
- /**
- * Returns true if this box includes zero points within its bounds.
- * Note that a box with equal lower and upper bounds still includes one
- * point, the one both bounds share.
- *
- * @return {boolean} Whether this box is empty or not.
- */
- isEmpty() {
-
- // this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes
-
- return ( this.max.x < this.min.x ) || ( this.max.y < this.min.y ) || ( this.max.z < this.min.z );
-
- }
-
- /**
- * Returns the center point of this box.
- *
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The center point.
- */
- getCenter( target ) {
-
- return this.isEmpty() ? target.set( 0, 0, 0 ) : target.addVectors( this.min, this.max ).multiplyScalar( 0.5 );
-
- }
-
- /**
- * Returns the dimensions of this box.
- *
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The size.
- */
- getSize( target ) {
-
- return this.isEmpty() ? target.set( 0, 0, 0 ) : target.subVectors( this.max, this.min );
-
- }
-
- /**
- * Expands the boundaries of this box to include the given point.
- *
- * @param {Vector3} point - The point that should be included by the bounding box.
- * @return {Box3} A reference to this bounding box.
- */
- expandByPoint( point ) {
-
- this.min.min( point );
- this.max.max( point );
-
- return this;
-
- }
-
- /**
- * Expands this box equilaterally by the given vector. The width of this
- * box will be expanded by the x component of the vector in both
- * directions. The height of this box will be expanded by the y component of
- * the vector in both directions. The depth of this box will be
- * expanded by the z component of the vector in both directions.
- *
- * @param {Vector3} vector - The vector that should expand the bounding box.
- * @return {Box3} A reference to this bounding box.
- */
- expandByVector( vector ) {
-
- this.min.sub( vector );
- this.max.add( vector );
-
- return this;
-
- }
-
- /**
- * Expands each dimension of the box by the given scalar. If negative, the
- * dimensions of the box will be contracted.
- *
- * @param {number} scalar - The scalar value that should expand the bounding box.
- * @return {Box3} A reference to this bounding box.
- */
- expandByScalar( scalar ) {
-
- this.min.addScalar( - scalar );
- this.max.addScalar( scalar );
-
- return this;
-
- }
-
- /**
- * Expands the boundaries of this box to include the given 3D object and
- * its children, accounting for the object's, and children's, world
- * transforms. The function may result in a larger box than strictly
- * necessary (unless the precise parameter is set to true).
- *
- * @param {Object3D} object - The 3D object that should expand the bounding box.
- * @param {boolean} precise - If set to `true`, the method expands the bounding box
- * as little as necessary at the expense of more computation.
- * @return {Box3} A reference to this bounding box.
- */
- expandByObject( object, precise = false ) {
-
- // Computes the world-axis-aligned bounding box of an object (including its children),
- // accounting for both the object's, and children's, world transforms
-
- object.updateWorldMatrix( false, false );
-
- const geometry = object.geometry;
-
- if ( geometry !== undefined ) {
-
- const positionAttribute = geometry.getAttribute( 'position' );
-
- // precise AABB computation based on vertex data requires at least a position attribute.
- // instancing isn't supported so far and uses the normal (conservative) code path.
-
- if ( precise === true && positionAttribute !== undefined && object.isInstancedMesh !== true ) {
-
- for ( let i = 0, l = positionAttribute.count; i < l; i ++ ) {
-
- if ( object.isMesh === true ) {
-
- object.getVertexPosition( i, _vector$b );
-
- } else {
-
- _vector$b.fromBufferAttribute( positionAttribute, i );
-
- }
-
- _vector$b.applyMatrix4( object.matrixWorld );
- this.expandByPoint( _vector$b );
-
- }
-
- } else {
-
- if ( object.boundingBox !== undefined ) {
-
- // object-level bounding box
-
- if ( object.boundingBox === null ) {
-
- object.computeBoundingBox();
-
- }
-
- _box$4.copy( object.boundingBox );
-
-
- } else {
-
- // geometry-level bounding box
-
- if ( geometry.boundingBox === null ) {
-
- geometry.computeBoundingBox();
-
- }
-
- _box$4.copy( geometry.boundingBox );
-
- }
-
- _box$4.applyMatrix4( object.matrixWorld );
-
- this.union( _box$4 );
-
- }
-
- }
-
- const children = object.children;
-
- for ( let i = 0, l = children.length; i < l; i ++ ) {
-
- this.expandByObject( children[ i ], precise );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns `true` if the given point lies within or on the boundaries of this box.
- *
- * @param {Vector3} point - The point to test.
- * @return {boolean} Whether the bounding box contains the given point or not.
- */
- containsPoint( point ) {
-
- return point.x >= this.min.x && point.x <= this.max.x &&
- point.y >= this.min.y && point.y <= this.max.y &&
- point.z >= this.min.z && point.z <= this.max.z;
-
- }
-
- /**
- * Returns `true` if this bounding box includes the entirety of the given bounding box.
- * If this box and the given one are identical, this function also returns `true`.
- *
- * @param {Box3} box - The bounding box to test.
- * @return {boolean} Whether the bounding box contains the given bounding box or not.
- */
- containsBox( box ) {
-
- return this.min.x <= box.min.x && box.max.x <= this.max.x &&
- this.min.y <= box.min.y && box.max.y <= this.max.y &&
- this.min.z <= box.min.z && box.max.z <= this.max.z;
-
- }
-
- /**
- * Returns a point as a proportion of this box's width, height and depth.
- *
- * @param {Vector3} point - A point in 3D space.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} A point as a proportion of this box's width, height and depth.
- */
- getParameter( point, target ) {
-
- // This can potentially have a divide by zero if the box
- // has a size dimension of 0.
-
- return target.set(
- ( point.x - this.min.x ) / ( this.max.x - this.min.x ),
- ( point.y - this.min.y ) / ( this.max.y - this.min.y ),
- ( point.z - this.min.z ) / ( this.max.z - this.min.z )
- );
-
- }
-
- /**
- * Returns `true` if the given bounding box intersects with this bounding box.
- *
- * @param {Box3} box - The bounding box to test.
- * @return {boolean} Whether the given bounding box intersects with this bounding box.
- */
- intersectsBox( box ) {
-
- // using 6 splitting planes to rule out intersections.
- return box.max.x >= this.min.x && box.min.x <= this.max.x &&
- box.max.y >= this.min.y && box.min.y <= this.max.y &&
- box.max.z >= this.min.z && box.min.z <= this.max.z;
-
- }
-
- /**
- * Returns `true` if the given bounding sphere intersects with this bounding box.
- *
- * @param {Sphere} sphere - The bounding sphere to test.
- * @return {boolean} Whether the given bounding sphere intersects with this bounding box.
- */
- intersectsSphere( sphere ) {
-
- // Find the point on the AABB closest to the sphere center.
- this.clampPoint( sphere.center, _vector$b );
-
- // If that point is inside the sphere, the AABB and sphere intersect.
- return _vector$b.distanceToSquared( sphere.center ) <= ( sphere.radius * sphere.radius );
-
- }
-
- /**
- * Returns `true` if the given plane intersects with this bounding box.
- *
- * @param {Plane} plane - The plane to test.
- * @return {boolean} Whether the given plane intersects with this bounding box.
- */
- intersectsPlane( plane ) {
-
- // We compute the minimum and maximum dot product values. If those values
- // are on the same side (back or front) of the plane, then there is no intersection.
-
- let min, max;
-
- if ( plane.normal.x > 0 ) {
-
- min = plane.normal.x * this.min.x;
- max = plane.normal.x * this.max.x;
-
- } else {
-
- min = plane.normal.x * this.max.x;
- max = plane.normal.x * this.min.x;
-
- }
-
- if ( plane.normal.y > 0 ) {
-
- min += plane.normal.y * this.min.y;
- max += plane.normal.y * this.max.y;
-
- } else {
-
- min += plane.normal.y * this.max.y;
- max += plane.normal.y * this.min.y;
-
- }
-
- if ( plane.normal.z > 0 ) {
-
- min += plane.normal.z * this.min.z;
- max += plane.normal.z * this.max.z;
-
- } else {
-
- min += plane.normal.z * this.max.z;
- max += plane.normal.z * this.min.z;
-
- }
-
- return ( min <= - plane.constant && max >= - plane.constant );
-
- }
-
- /**
- * Returns `true` if the given triangle intersects with this bounding box.
- *
- * @param {Triangle} triangle - The triangle to test.
- * @return {boolean} Whether the given triangle intersects with this bounding box.
- */
- intersectsTriangle( triangle ) {
-
- if ( this.isEmpty() ) {
-
- return false;
-
- }
-
- // compute box center and extents
- this.getCenter( _center );
- _extents.subVectors( this.max, _center );
-
- // translate triangle to aabb origin
- _v0$1.subVectors( triangle.a, _center );
- _v1$4.subVectors( triangle.b, _center );
- _v2$3.subVectors( triangle.c, _center );
-
- // compute edge vectors for triangle
- _f0.subVectors( _v1$4, _v0$1 );
- _f1.subVectors( _v2$3, _v1$4 );
- _f2.subVectors( _v0$1, _v2$3 );
-
- // test against axes that are given by cross product combinations of the edges of the triangle and the edges of the aabb
- // make an axis testing of each of the 3 sides of the aabb against each of the 3 sides of the triangle = 9 axis of separation
- // axis_ij = u_i x f_j (u0, u1, u2 = face normals of aabb = x,y,z axes vectors since aabb is axis aligned)
- let axes = [
- 0, - _f0.z, _f0.y, 0, - _f1.z, _f1.y, 0, - _f2.z, _f2.y,
- _f0.z, 0, - _f0.x, _f1.z, 0, - _f1.x, _f2.z, 0, - _f2.x,
- - _f0.y, _f0.x, 0, - _f1.y, _f1.x, 0, - _f2.y, _f2.x, 0
- ];
- if ( ! satForAxes( axes, _v0$1, _v1$4, _v2$3, _extents ) ) {
-
- return false;
-
- }
-
- // test 3 face normals from the aabb
- axes = [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];
- if ( ! satForAxes( axes, _v0$1, _v1$4, _v2$3, _extents ) ) {
-
- return false;
-
- }
-
- // finally testing the face normal of the triangle
- // use already existing triangle edge vectors here
- _triangleNormal.crossVectors( _f0, _f1 );
- axes = [ _triangleNormal.x, _triangleNormal.y, _triangleNormal.z ];
-
- return satForAxes( axes, _v0$1, _v1$4, _v2$3, _extents );
-
- }
-
- /**
- * Clamps the given point within the bounds of this box.
- *
- * @param {Vector3} point - The point to clamp.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The clamped point.
- */
- clampPoint( point, target ) {
-
- return target.copy( point ).clamp( this.min, this.max );
-
- }
-
- /**
- * Returns the euclidean distance from any edge of this box to the specified point. If
- * the given point lies inside of this box, the distance will be `0`.
- *
- * @param {Vector3} point - The point to compute the distance to.
- * @return {number} The euclidean distance.
- */
- distanceToPoint( point ) {
-
- return this.clampPoint( point, _vector$b ).distanceTo( point );
-
- }
-
- /**
- * Returns a bounding sphere that encloses this bounding box.
- *
- * @param {Sphere} target - The target sphere that is used to store the method's result.
- * @return {Sphere} The bounding sphere that encloses this bounding box.
- */
- getBoundingSphere( target ) {
-
- if ( this.isEmpty() ) {
-
- target.makeEmpty();
-
- } else {
-
- this.getCenter( target.center );
-
- target.radius = this.getSize( _vector$b ).length() * 0.5;
-
- }
-
- return target;
-
- }
-
- /**
- * Computes the intersection of this bounding box and the given one, setting the upper
- * bound of this box to the lesser of the two boxes' upper bounds and the
- * lower bound of this box to the greater of the two boxes' lower bounds. If
- * there's no overlap, makes this box empty.
- *
- * @param {Box3} box - The bounding box to intersect with.
- * @return {Box3} A reference to this bounding box.
- */
- intersect( box ) {
-
- this.min.max( box.min );
- this.max.min( box.max );
-
- // ensure that if there is no overlap, the result is fully empty, not slightly empty with non-inf/+inf values that will cause subsequence intersects to erroneously return valid values.
- if ( this.isEmpty() ) this.makeEmpty();
-
- return this;
-
- }
-
- /**
- * Computes the union of this box and another and the given one, setting the upper
- * bound of this box to the greater of the two boxes' upper bounds and the
- * lower bound of this box to the lesser of the two boxes' lower bounds.
- *
- * @param {Box3} box - The bounding box that will be unioned with this instance.
- * @return {Box3} A reference to this bounding box.
- */
- union( box ) {
-
- this.min.min( box.min );
- this.max.max( box.max );
-
- return this;
-
- }
-
- /**
- * Transforms this bounding box by the given 4x4 transformation matrix.
- *
- * @param {Matrix4} matrix - The transformation matrix.
- * @return {Box3} A reference to this bounding box.
- */
- applyMatrix4( matrix ) {
-
- // transform of empty box is an empty box.
- if ( this.isEmpty() ) return this;
-
- // NOTE: I am using a binary pattern to specify all 2^3 combinations below
- _points[ 0 ].set( this.min.x, this.min.y, this.min.z ).applyMatrix4( matrix ); // 000
- _points[ 1 ].set( this.min.x, this.min.y, this.max.z ).applyMatrix4( matrix ); // 001
- _points[ 2 ].set( this.min.x, this.max.y, this.min.z ).applyMatrix4( matrix ); // 010
- _points[ 3 ].set( this.min.x, this.max.y, this.max.z ).applyMatrix4( matrix ); // 011
- _points[ 4 ].set( this.max.x, this.min.y, this.min.z ).applyMatrix4( matrix ); // 100
- _points[ 5 ].set( this.max.x, this.min.y, this.max.z ).applyMatrix4( matrix ); // 101
- _points[ 6 ].set( this.max.x, this.max.y, this.min.z ).applyMatrix4( matrix ); // 110
- _points[ 7 ].set( this.max.x, this.max.y, this.max.z ).applyMatrix4( matrix ); // 111
-
- this.setFromPoints( _points );
-
- return this;
-
- }
-
- /**
- * Adds the given offset to both the upper and lower bounds of this bounding box,
- * effectively moving it in 3D space.
- *
- * @param {Vector3} offset - The offset that should be used to translate the bounding box.
- * @return {Box3} A reference to this bounding box.
- */
- translate( offset ) {
-
- this.min.add( offset );
- this.max.add( offset );
-
- return this;
-
- }
-
- /**
- * Returns `true` if this bounding box is equal with the given one.
- *
- * @param {Box3} box - The box to test for equality.
- * @return {boolean} Whether this bounding box is equal with the given one.
- */
- equals( box ) {
-
- return box.min.equals( this.min ) && box.max.equals( this.max );
-
- }
-
- /**
- * Returns a serialized structure of the bounding box.
- *
- * @return {Object} Serialized structure with fields representing the object state.
- */
- toJSON() {
-
- return {
- min: this.min.toArray(),
- max: this.max.toArray()
- };
-
- }
-
- /**
- * Returns a serialized structure of the bounding box.
- *
- * @param {Object} json - The serialized json to set the box from.
- * @return {Box3} A reference to this bounding box.
- */
- fromJSON( json ) {
-
- this.min.fromArray( json.min );
- this.max.fromArray( json.max );
- return this;
-
- }
-
-}
-
-const _points = [
- /*@__PURE__*/ new Vector3(),
- /*@__PURE__*/ new Vector3(),
- /*@__PURE__*/ new Vector3(),
- /*@__PURE__*/ new Vector3(),
- /*@__PURE__*/ new Vector3(),
- /*@__PURE__*/ new Vector3(),
- /*@__PURE__*/ new Vector3(),
- /*@__PURE__*/ new Vector3()
-];
-
-const _vector$b = /*@__PURE__*/ new Vector3();
-
-const _box$4 = /*@__PURE__*/ new Box3();
-
-// triangle centered vertices
-
-const _v0$1 = /*@__PURE__*/ new Vector3();
-const _v1$4 = /*@__PURE__*/ new Vector3();
-const _v2$3 = /*@__PURE__*/ new Vector3();
-
-// triangle edge vectors
-
-const _f0 = /*@__PURE__*/ new Vector3();
-const _f1 = /*@__PURE__*/ new Vector3();
-const _f2 = /*@__PURE__*/ new Vector3();
-
-const _center = /*@__PURE__*/ new Vector3();
-const _extents = /*@__PURE__*/ new Vector3();
-const _triangleNormal = /*@__PURE__*/ new Vector3();
-const _testAxis = /*@__PURE__*/ new Vector3();
-
-function satForAxes( axes, v0, v1, v2, extents ) {
-
- for ( let i = 0, j = axes.length - 3; i <= j; i += 3 ) {
-
- _testAxis.fromArray( axes, i );
- // project the aabb onto the separating axis
- const r = extents.x * Math.abs( _testAxis.x ) + extents.y * Math.abs( _testAxis.y ) + extents.z * Math.abs( _testAxis.z );
- // project all 3 vertices of the triangle onto the separating axis
- const p0 = v0.dot( _testAxis );
- const p1 = v1.dot( _testAxis );
- const p2 = v2.dot( _testAxis );
- // actual test, basically see if either of the most extreme of the triangle points intersects r
- if ( Math.max( - Math.max( p0, p1, p2 ), Math.min( p0, p1, p2 ) ) > r ) {
-
- // points of the projected triangle are outside the projected half-length of the aabb
- // the axis is separating and we can exit
- return false;
-
- }
-
- }
-
- return true;
-
-}
-
-// Fast Half Float Conversions, http://www.fox-toolkit.org/ftp/fasthalffloatconversion.pdf
-
-const _tables = /*@__PURE__*/ _generateTables();
-
-function _generateTables() {
-
- // float32 to float16 helpers
-
- const buffer = new ArrayBuffer( 4 );
- const floatView = new Float32Array( buffer );
- const uint32View = new Uint32Array( buffer );
-
- const baseTable = new Uint32Array( 512 );
- const shiftTable = new Uint32Array( 512 );
-
- for ( let i = 0; i < 256; ++ i ) {
-
- const e = i - 127;
-
- // very small number (0, -0)
-
- if ( e < -27 ) {
-
- baseTable[ i ] = 0x0000;
- baseTable[ i | 0x100 ] = 0x8000;
- shiftTable[ i ] = 24;
- shiftTable[ i | 0x100 ] = 24;
-
- // small number (denorm)
-
- } else if ( e < -14 ) {
-
- baseTable[ i ] = 0x0400 >> ( - e - 14 );
- baseTable[ i | 0x100 ] = ( 0x0400 >> ( - e - 14 ) ) | 0x8000;
- shiftTable[ i ] = - e - 1;
- shiftTable[ i | 0x100 ] = - e - 1;
-
- // normal number
-
- } else if ( e <= 15 ) {
-
- baseTable[ i ] = ( e + 15 ) << 10;
- baseTable[ i | 0x100 ] = ( ( e + 15 ) << 10 ) | 0x8000;
- shiftTable[ i ] = 13;
- shiftTable[ i | 0x100 ] = 13;
-
- // large number (Infinity, -Infinity)
-
- } else if ( e < 128 ) {
-
- baseTable[ i ] = 0x7c00;
- baseTable[ i | 0x100 ] = 0xfc00;
- shiftTable[ i ] = 24;
- shiftTable[ i | 0x100 ] = 24;
-
- // stay (NaN, Infinity, -Infinity)
-
- } else {
-
- baseTable[ i ] = 0x7c00;
- baseTable[ i | 0x100 ] = 0xfc00;
- shiftTable[ i ] = 13;
- shiftTable[ i | 0x100 ] = 13;
-
- }
-
- }
-
- // float16 to float32 helpers
-
- const mantissaTable = new Uint32Array( 2048 );
- const exponentTable = new Uint32Array( 64 );
- const offsetTable = new Uint32Array( 64 );
-
- for ( let i = 1; i < 1024; ++ i ) {
-
- let m = i << 13; // zero pad mantissa bits
- let e = 0; // zero exponent
-
- // normalized
- while ( ( m & 0x00800000 ) === 0 ) {
-
- m <<= 1;
- e -= 0x00800000; // decrement exponent
-
- }
-
- m &= -8388609; // clear leading 1 bit
- e += 0x38800000; // adjust bias
-
- mantissaTable[ i ] = m | e;
-
- }
-
- for ( let i = 1024; i < 2048; ++ i ) {
-
- mantissaTable[ i ] = 0x38000000 + ( ( i - 1024 ) << 13 );
-
- }
-
- for ( let i = 1; i < 31; ++ i ) {
-
- exponentTable[ i ] = i << 23;
-
- }
-
- exponentTable[ 31 ] = 0x47800000;
- exponentTable[ 32 ] = 0x80000000;
-
- for ( let i = 33; i < 63; ++ i ) {
-
- exponentTable[ i ] = 0x80000000 + ( ( i - 32 ) << 23 );
-
- }
-
- exponentTable[ 63 ] = 0xc7800000;
-
- for ( let i = 1; i < 64; ++ i ) {
-
- if ( i !== 32 ) {
-
- offsetTable[ i ] = 1024;
-
- }
-
- }
-
- return {
- floatView: floatView,
- uint32View: uint32View,
- baseTable: baseTable,
- shiftTable: shiftTable,
- mantissaTable: mantissaTable,
- exponentTable: exponentTable,
- offsetTable: offsetTable
- };
-
-}
-
-/**
- * Returns a half precision floating point value (FP16) from the given single
- * precision floating point value (FP32).
- *
- * @param {number} val - A single precision floating point value.
- * @return {number} The FP16 value.
- */
-function toHalfFloat( val ) {
-
- if ( Math.abs( val ) > 65504 ) warn( 'DataUtils.toHalfFloat(): Value out of range.' );
-
- val = clamp( val, -65504, 65504 );
-
- _tables.floatView[ 0 ] = val;
- const f = _tables.uint32View[ 0 ];
- const e = ( f >> 23 ) & 0x1ff;
- return _tables.baseTable[ e ] + ( ( f & 0x007fffff ) >> _tables.shiftTable[ e ] );
-
-}
-
-/**
- * Returns a single precision floating point value (FP32) from the given half
- * precision floating point value (FP16).
- *
- * @param {number} val - A half precision floating point value.
- * @return {number} The FP32 value.
- */
-function fromHalfFloat( val ) {
-
- const m = val >> 10;
- _tables.uint32View[ 0 ] = _tables.mantissaTable[ _tables.offsetTable[ m ] + ( val & 0x3ff ) ] + _tables.exponentTable[ m ];
- return _tables.floatView[ 0 ];
-
-}
-
-/**
- * A class containing utility functions for data.
- *
- * @hideconstructor
- */
-class DataUtils {
-
- /**
- * Returns a half precision floating point value (FP16) from the given single
- * precision floating point value (FP32).
- *
- * @param {number} val - A single precision floating point value.
- * @return {number} The FP16 value.
- */
- static toHalfFloat( val ) {
-
- return toHalfFloat( val );
-
- }
-
- /**
- * Returns a single precision floating point value (FP32) from the given half
- * precision floating point value (FP16).
- *
- * @param {number} val - A half precision floating point value.
- * @return {number} The FP32 value.
- */
- static fromHalfFloat( val ) {
-
- return fromHalfFloat( val );
-
- }
-
-}
-
-const _vector$a = /*@__PURE__*/ new Vector3();
-const _vector2$1 = /*@__PURE__*/ new Vector2();
-
-let _id$2 = 0;
-
-/**
- * This class stores data for an attribute (such as vertex positions, face
- * indices, normals, colors, UVs, and any custom attributes ) associated with
- * a geometry, which allows for more efficient passing of data to the GPU.
- *
- * When working with vector-like data, the `fromBufferAttribute( attribute, index )`
- * helper methods on vector and color class might be helpful. E.g. {@link Vector3#fromBufferAttribute}.
- */
-class BufferAttribute extends EventDispatcher {
-
- /**
- * Constructs a new buffer attribute.
- *
- * @param {TypedArray} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( array, itemSize, normalized = false ) {
-
- super();
-
- if ( Array.isArray( array ) ) {
-
- throw new TypeError( 'THREE.BufferAttribute: array should be a Typed Array.' );
-
- }
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isBufferAttribute = true;
-
- /**
- * The ID of the buffer attribute.
- *
- * @name BufferAttribute#id
- * @type {number}
- * @readonly
- */
- Object.defineProperty( this, 'id', { value: _id$2 ++ } );
-
- /**
- * The name of the buffer attribute.
- *
- * @type {string}
- */
- this.name = '';
-
- /**
- * The array holding the attribute data. It should have `itemSize * numVertices`
- * elements, where `numVertices` is the number of vertices in the associated geometry.
- *
- * @type {TypedArray}
- */
- this.array = array;
-
- /**
- * The number of values of the array that should be associated with a particular vertex.
- * For instance, if this attribute is storing a 3-component vector (such as a position,
- * normal, or color), then the value should be `3`.
- *
- * @type {number}
- */
- this.itemSize = itemSize;
-
- /**
- * Represents the number of items this buffer attribute stores. It is internally computed
- * by dividing the `array` length by the `itemSize`.
- *
- * @type {number}
- * @readonly
- */
- this.count = array !== undefined ? array.length / itemSize : 0;
-
- /**
- * Applies to integer data only. Indicates how the underlying data in the buffer maps to
- * the values in the GLSL code. For instance, if `array` is an instance of `UInt16Array`,
- * and `normalized` is `true`, the values `0 - +65535` in the array data will be mapped to
- * `0.0f - +1.0f` in the GLSL attribute. If `normalized` is `false`, the values will be converted
- * to floats unmodified, i.e. `65535` becomes `65535.0f`.
- *
- * @type {boolean}
- */
- this.normalized = normalized;
-
- /**
- * Defines the intended usage pattern of the data store for optimization purposes.
- *
- * Note: After the initial use of a buffer, its usage cannot be changed. Instead,
- * instantiate a new one and set the desired usage before the next render.
- *
- * @type {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)}
- * @default StaticDrawUsage
- */
- this.usage = StaticDrawUsage;
-
- /**
- * This can be used to only update some components of stored vectors (for example, just the
- * component related to color). Use the `addUpdateRange()` function to add ranges to this array.
- *
- * @type {Array}
- */
- this.updateRanges = [];
-
- /**
- * Configures the bound GPU type for use in shaders.
- *
- * Note: this only has an effect for integer arrays and is not configurable for float arrays.
- * For lower precision float types, use `Float16BufferAttribute`.
- *
- * @type {(FloatType|IntType)}
- * @default FloatType
- */
- this.gpuType = FloatType;
-
- /**
- * A version number, incremented every time the `needsUpdate` is set to `true`.
- *
- * @type {number}
- */
- this.version = 0;
-
- }
-
- /**
- * A callback function that is executed after the renderer has transferred the attribute
- * array data to the GPU.
- */
- onUploadCallback() {}
-
- /**
- * Flag to indicate that this attribute has changed and should be re-sent to
- * the GPU. Set this to `true` when you modify the value of the array.
- *
- * @type {number}
- * @default false
- * @param {boolean} value
- */
- set needsUpdate( value ) {
-
- if ( value === true ) this.version ++;
-
- }
-
- /**
- * Sets the usage of this buffer attribute.
- *
- * @param {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)} value - The usage to set.
- * @return {BufferAttribute} A reference to this buffer attribute.
- */
- setUsage( value ) {
-
- this.usage = value;
-
- return this;
-
- }
-
- /**
- * Adds a range of data in the data array to be updated on the GPU.
- *
- * @param {number} start - Position at which to start update.
- * @param {number} count - The number of components to update.
- */
- addUpdateRange( start, count ) {
-
- this.updateRanges.push( { start, count } );
-
- }
-
- /**
- * Clears the update ranges.
- */
- clearUpdateRanges() {
-
- this.updateRanges.length = 0;
-
- }
-
- /**
- * Copies the values of the given buffer attribute to this instance.
- *
- * @param {BufferAttribute} source - The buffer attribute to copy.
- * @return {BufferAttribute} A reference to this instance.
- */
- copy( source ) {
-
- this.name = source.name;
- this.array = new source.array.constructor( source.array );
- this.itemSize = source.itemSize;
- this.count = source.count;
- this.normalized = source.normalized;
-
- this.usage = source.usage;
- this.gpuType = source.gpuType;
-
- return this;
-
- }
-
- /**
- * Copies a vector from the given buffer attribute to this one. The start
- * and destination position in the attribute buffers are represented by the
- * given indices.
- *
- * @param {number} index1 - The destination index into this buffer attribute.
- * @param {BufferAttribute} attribute - The buffer attribute to copy from.
- * @param {number} index2 - The source index into the given buffer attribute.
- * @return {BufferAttribute} A reference to this instance.
- */
- copyAt( index1, attribute, index2 ) {
-
- index1 *= this.itemSize;
- index2 *= attribute.itemSize;
-
- for ( let i = 0, l = this.itemSize; i < l; i ++ ) {
-
- this.array[ index1 + i ] = attribute.array[ index2 + i ];
-
- }
-
- return this;
-
- }
-
- /**
- * Copies the given array data into this buffer attribute.
- *
- * @param {(TypedArray|Array)} array - The array to copy.
- * @return {BufferAttribute} A reference to this instance.
- */
- copyArray( array ) {
-
- this.array.set( array );
-
- return this;
-
- }
-
- /**
- * Applies the given 3x3 matrix to the given attribute. Works with
- * item size `2` and `3`.
- *
- * @param {Matrix3} m - The matrix to apply.
- * @return {BufferAttribute} A reference to this instance.
- */
- applyMatrix3( m ) {
-
- if ( this.itemSize === 2 ) {
-
- for ( let i = 0, l = this.count; i < l; i ++ ) {
-
- _vector2$1.fromBufferAttribute( this, i );
- _vector2$1.applyMatrix3( m );
-
- this.setXY( i, _vector2$1.x, _vector2$1.y );
-
- }
-
- } else if ( this.itemSize === 3 ) {
-
- for ( let i = 0, l = this.count; i < l; i ++ ) {
-
- _vector$a.fromBufferAttribute( this, i );
- _vector$a.applyMatrix3( m );
-
- this.setXYZ( i, _vector$a.x, _vector$a.y, _vector$a.z );
-
- }
-
- }
-
- return this;
-
- }
-
- /**
- * Applies the given 4x4 matrix to the given attribute. Only works with
- * item size `3`.
- *
- * @param {Matrix4} m - The matrix to apply.
- * @return {BufferAttribute} A reference to this instance.
- */
- applyMatrix4( m ) {
-
- for ( let i = 0, l = this.count; i < l; i ++ ) {
-
- _vector$a.fromBufferAttribute( this, i );
-
- _vector$a.applyMatrix4( m );
-
- this.setXYZ( i, _vector$a.x, _vector$a.y, _vector$a.z );
-
- }
-
- return this;
-
- }
-
- /**
- * Applies the given 3x3 normal matrix to the given attribute. Only works with
- * item size `3`.
- *
- * @param {Matrix3} m - The normal matrix to apply.
- * @return {BufferAttribute} A reference to this instance.
- */
- applyNormalMatrix( m ) {
-
- for ( let i = 0, l = this.count; i < l; i ++ ) {
-
- _vector$a.fromBufferAttribute( this, i );
-
- _vector$a.applyNormalMatrix( m );
-
- this.setXYZ( i, _vector$a.x, _vector$a.y, _vector$a.z );
-
- }
-
- return this;
-
- }
-
- /**
- * Applies the given 4x4 matrix to the given attribute. Only works with
- * item size `3` and with direction vectors.
- *
- * @param {Matrix4} m - The matrix to apply.
- * @return {BufferAttribute} A reference to this instance.
- */
- transformDirection( m ) {
-
- for ( let i = 0, l = this.count; i < l; i ++ ) {
-
- _vector$a.fromBufferAttribute( this, i );
-
- _vector$a.transformDirection( m );
-
- this.setXYZ( i, _vector$a.x, _vector$a.y, _vector$a.z );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the given array data in the buffer attribute.
- *
- * @param {(TypedArray|Array)} value - The array data to set.
- * @param {number} [offset=0] - The offset in this buffer attribute's array.
- * @return {BufferAttribute} A reference to this instance.
- */
- set( value, offset = 0 ) {
-
- // Matching BufferAttribute constructor, do not normalize the array.
- this.array.set( value, offset );
-
- return this;
-
- }
-
- /**
- * Returns the given component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} component - The component index.
- * @return {number} The returned value.
- */
- getComponent( index, component ) {
-
- let value = this.array[ index * this.itemSize + component ];
-
- if ( this.normalized ) value = denormalize( value, this.array );
-
- return value;
-
- }
-
- /**
- * Sets the given value to the given component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} component - The component index.
- * @param {number} value - The value to set.
- * @return {BufferAttribute} A reference to this instance.
- */
- setComponent( index, component, value ) {
-
- if ( this.normalized ) value = normalize( value, this.array );
-
- this.array[ index * this.itemSize + component ] = value;
-
- return this;
-
- }
-
- /**
- * Returns the x component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @return {number} The x component.
- */
- getX( index ) {
-
- let x = this.array[ index * this.itemSize ];
-
- if ( this.normalized ) x = denormalize( x, this.array );
-
- return x;
-
- }
-
- /**
- * Sets the x component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} x - The value to set.
- * @return {BufferAttribute} A reference to this instance.
- */
- setX( index, x ) {
-
- if ( this.normalized ) x = normalize( x, this.array );
-
- this.array[ index * this.itemSize ] = x;
-
- return this;
-
- }
-
- /**
- * Returns the y component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @return {number} The y component.
- */
- getY( index ) {
-
- let y = this.array[ index * this.itemSize + 1 ];
-
- if ( this.normalized ) y = denormalize( y, this.array );
-
- return y;
-
- }
-
- /**
- * Sets the y component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} y - The value to set.
- * @return {BufferAttribute} A reference to this instance.
- */
- setY( index, y ) {
-
- if ( this.normalized ) y = normalize( y, this.array );
-
- this.array[ index * this.itemSize + 1 ] = y;
-
- return this;
-
- }
-
- /**
- * Returns the z component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @return {number} The z component.
- */
- getZ( index ) {
-
- let z = this.array[ index * this.itemSize + 2 ];
-
- if ( this.normalized ) z = denormalize( z, this.array );
-
- return z;
-
- }
-
- /**
- * Sets the z component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} z - The value to set.
- * @return {BufferAttribute} A reference to this instance.
- */
- setZ( index, z ) {
-
- if ( this.normalized ) z = normalize( z, this.array );
-
- this.array[ index * this.itemSize + 2 ] = z;
-
- return this;
-
- }
-
- /**
- * Returns the w component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @return {number} The w component.
- */
- getW( index ) {
-
- let w = this.array[ index * this.itemSize + 3 ];
-
- if ( this.normalized ) w = denormalize( w, this.array );
-
- return w;
-
- }
-
- /**
- * Sets the w component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} w - The value to set.
- * @return {BufferAttribute} A reference to this instance.
- */
- setW( index, w ) {
-
- if ( this.normalized ) w = normalize( w, this.array );
-
- this.array[ index * this.itemSize + 3 ] = w;
-
- return this;
-
- }
-
- /**
- * Sets the x and y component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} x - The value for the x component to set.
- * @param {number} y - The value for the y component to set.
- * @return {BufferAttribute} A reference to this instance.
- */
- setXY( index, x, y ) {
-
- index *= this.itemSize;
-
- if ( this.normalized ) {
-
- x = normalize( x, this.array );
- y = normalize( y, this.array );
-
- }
-
- this.array[ index + 0 ] = x;
- this.array[ index + 1 ] = y;
-
- return this;
-
- }
-
- /**
- * Sets the x, y and z component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} x - The value for the x component to set.
- * @param {number} y - The value for the y component to set.
- * @param {number} z - The value for the z component to set.
- * @return {BufferAttribute} A reference to this instance.
- */
- setXYZ( index, x, y, z ) {
-
- index *= this.itemSize;
-
- if ( this.normalized ) {
-
- x = normalize( x, this.array );
- y = normalize( y, this.array );
- z = normalize( z, this.array );
-
- }
-
- this.array[ index + 0 ] = x;
- this.array[ index + 1 ] = y;
- this.array[ index + 2 ] = z;
-
- return this;
-
- }
-
- /**
- * Sets the x, y, z and w component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} x - The value for the x component to set.
- * @param {number} y - The value for the y component to set.
- * @param {number} z - The value for the z component to set.
- * @param {number} w - The value for the w component to set.
- * @return {BufferAttribute} A reference to this instance.
- */
- setXYZW( index, x, y, z, w ) {
-
- index *= this.itemSize;
-
- if ( this.normalized ) {
-
- x = normalize( x, this.array );
- y = normalize( y, this.array );
- z = normalize( z, this.array );
- w = normalize( w, this.array );
-
- }
-
- this.array[ index + 0 ] = x;
- this.array[ index + 1 ] = y;
- this.array[ index + 2 ] = z;
- this.array[ index + 3 ] = w;
-
- return this;
-
- }
-
- /**
- * Sets the given callback function that is executed after the Renderer has transferred
- * the attribute array data to the GPU. Can be used to perform clean-up operations after
- * the upload when attribute data are not needed anymore on the CPU side.
- *
- * @param {Function} callback - The `onUpload()` callback.
- * @return {BufferAttribute} A reference to this instance.
- */
- onUpload( callback ) {
-
- this.onUploadCallback = callback;
-
- return this;
-
- }
-
- /**
- * Returns a new buffer attribute with copied values from this instance.
- *
- * @return {BufferAttribute} A clone of this instance.
- */
- clone() {
-
- return new this.constructor( this.array, this.itemSize ).copy( this );
-
- }
-
- /**
- * Serializes the buffer attribute into JSON.
- *
- * @return {Object} A JSON object representing the serialized buffer attribute.
- */
- toJSON() {
-
- const data = {
- itemSize: this.itemSize,
- type: this.array.constructor.name,
- array: Array.from( this.array ),
- normalized: this.normalized
- };
-
- data.name = this.name;
- data.usage = this.usage;
- data.gpuType = this.gpuType;
-
- return data;
-
- }
-
- /**
- * Disposes of the buffer attribute. Available only in {@link WebGPURenderer}.
- */
- dispose() {
-
- this.dispatchEvent( { type: 'dispose' } );
-
- }
-
-}
-
-/**
- * Convenient class that can be used when creating a `Int8` buffer attribute with
- * a plain `Array` instance.
- *
- * @augments BufferAttribute
- */
-class Int8BufferAttribute extends BufferAttribute {
-
- /**
- * Constructs a new buffer attribute.
- *
- * @param {(Array|Int8Array)} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( array, itemSize, normalized ) {
-
- super( new Int8Array( array ), itemSize, normalized );
-
- }
-
-}
-
-/**
- * Convenient class that can be used when creating a `UInt8` buffer attribute with
- * a plain `Array` instance.
- *
- * @augments BufferAttribute
- */
-class Uint8BufferAttribute extends BufferAttribute {
-
- /**
- * Constructs a new buffer attribute.
- *
- * @param {(Array|Uint8Array)} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( array, itemSize, normalized ) {
-
- super( new Uint8Array( array ), itemSize, normalized );
-
- }
-
-}
-
-/**
- * Convenient class that can be used when creating a `UInt8Clamped` buffer attribute with
- * a plain `Array` instance.
- *
- * @augments BufferAttribute
- */
-class Uint8ClampedBufferAttribute extends BufferAttribute {
-
- /**
- * Constructs a new buffer attribute.
- *
- * @param {(Array|Uint8ClampedArray)} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( array, itemSize, normalized ) {
-
- super( new Uint8ClampedArray( array ), itemSize, normalized );
-
- }
-
-}
-
-/**
- * Convenient class that can be used when creating a `Int16` buffer attribute with
- * a plain `Array` instance.
- *
- * @augments BufferAttribute
- */
-class Int16BufferAttribute extends BufferAttribute {
-
- /**
- * Constructs a new buffer attribute.
- *
- * @param {(Array|Int16Array)} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( array, itemSize, normalized ) {
-
- super( new Int16Array( array ), itemSize, normalized );
-
- }
-
-}
-
-/**
- * Convenient class that can be used when creating a `UInt16` buffer attribute with
- * a plain `Array` instance.
- *
- * @augments BufferAttribute
- */
-class Uint16BufferAttribute extends BufferAttribute {
-
- /**
- * Constructs a new buffer attribute.
- *
- * @param {(Array|Uint16Array)} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( array, itemSize, normalized ) {
-
- super( new Uint16Array( array ), itemSize, normalized );
-
- }
-
-}
-
-/**
- * Convenient class that can be used when creating a `Int32` buffer attribute with
- * a plain `Array` instance.
- *
- * @augments BufferAttribute
- */
-class Int32BufferAttribute extends BufferAttribute {
-
- /**
- * Constructs a new buffer attribute.
- *
- * @param {(Array|Int32Array)} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( array, itemSize, normalized ) {
-
- super( new Int32Array( array ), itemSize, normalized );
-
- }
-
-}
-
-/**
- * Convenient class that can be used when creating a `UInt32` buffer attribute with
- * a plain `Array` instance.
- *
- * @augments BufferAttribute
- */
-class Uint32BufferAttribute extends BufferAttribute {
-
- /**
- * Constructs a new buffer attribute.
- *
- * @param {(Array|Uint32Array)} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( array, itemSize, normalized ) {
-
- super( new Uint32Array( array ), itemSize, normalized );
-
- }
-
-}
-
-/**
- * Convenient class that can be used when creating a `Float16` buffer attribute with
- * a plain `Array` instance.
- *
- * This class automatically converts to and from FP16 via `Uint16Array` since `Float16Array`
- * browser support is still problematic.
- *
- * @augments BufferAttribute
- */
-class Float16BufferAttribute extends BufferAttribute {
-
- /**
- * Constructs a new buffer attribute.
- *
- * @param {(Array|Uint16Array)} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( array, itemSize, normalized ) {
-
- super( new Uint16Array( array ), itemSize, normalized );
-
- this.isFloat16BufferAttribute = true;
-
- }
-
- getX( index ) {
-
- let x = fromHalfFloat( this.array[ index * this.itemSize ] );
-
- if ( this.normalized ) x = denormalize( x, this.array );
-
- return x;
-
- }
-
- setX( index, x ) {
-
- if ( this.normalized ) x = normalize( x, this.array );
-
- this.array[ index * this.itemSize ] = toHalfFloat( x );
-
- return this;
-
- }
-
- getY( index ) {
-
- let y = fromHalfFloat( this.array[ index * this.itemSize + 1 ] );
-
- if ( this.normalized ) y = denormalize( y, this.array );
-
- return y;
-
- }
-
- setY( index, y ) {
-
- if ( this.normalized ) y = normalize( y, this.array );
-
- this.array[ index * this.itemSize + 1 ] = toHalfFloat( y );
-
- return this;
-
- }
-
- getZ( index ) {
-
- let z = fromHalfFloat( this.array[ index * this.itemSize + 2 ] );
-
- if ( this.normalized ) z = denormalize( z, this.array );
-
- return z;
-
- }
-
- setZ( index, z ) {
-
- if ( this.normalized ) z = normalize( z, this.array );
-
- this.array[ index * this.itemSize + 2 ] = toHalfFloat( z );
-
- return this;
-
- }
-
- getW( index ) {
-
- let w = fromHalfFloat( this.array[ index * this.itemSize + 3 ] );
-
- if ( this.normalized ) w = denormalize( w, this.array );
-
- return w;
-
- }
-
- setW( index, w ) {
-
- if ( this.normalized ) w = normalize( w, this.array );
-
- this.array[ index * this.itemSize + 3 ] = toHalfFloat( w );
-
- return this;
-
- }
-
- setXY( index, x, y ) {
-
- index *= this.itemSize;
-
- if ( this.normalized ) {
-
- x = normalize( x, this.array );
- y = normalize( y, this.array );
-
- }
-
- this.array[ index + 0 ] = toHalfFloat( x );
- this.array[ index + 1 ] = toHalfFloat( y );
-
- return this;
-
- }
-
- setXYZ( index, x, y, z ) {
-
- index *= this.itemSize;
-
- if ( this.normalized ) {
-
- x = normalize( x, this.array );
- y = normalize( y, this.array );
- z = normalize( z, this.array );
-
- }
-
- this.array[ index + 0 ] = toHalfFloat( x );
- this.array[ index + 1 ] = toHalfFloat( y );
- this.array[ index + 2 ] = toHalfFloat( z );
-
- return this;
-
- }
-
- setXYZW( index, x, y, z, w ) {
-
- index *= this.itemSize;
-
- if ( this.normalized ) {
-
- x = normalize( x, this.array );
- y = normalize( y, this.array );
- z = normalize( z, this.array );
- w = normalize( w, this.array );
-
- }
-
- this.array[ index + 0 ] = toHalfFloat( x );
- this.array[ index + 1 ] = toHalfFloat( y );
- this.array[ index + 2 ] = toHalfFloat( z );
- this.array[ index + 3 ] = toHalfFloat( w );
-
- return this;
-
- }
-
-}
-
-/**
- * Convenient class that can be used when creating a `Float32` buffer attribute with
- * a plain `Array` instance.
- *
- * @augments BufferAttribute
- */
-class Float32BufferAttribute extends BufferAttribute {
-
- /**
- * Constructs a new buffer attribute.
- *
- * @param {(Array|Float32Array)} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( array, itemSize, normalized ) {
-
- super( new Float32Array( array ), itemSize, normalized );
-
- }
-
-}
-
-const _box$3 = /*@__PURE__*/ new Box3();
-const _v1$3 = /*@__PURE__*/ new Vector3();
-const _v2$2 = /*@__PURE__*/ new Vector3();
-
-/**
- * An analytical 3D sphere defined by a center and radius. This class is mainly
- * used as a Bounding Sphere for 3D objects.
- */
-class Sphere {
-
- /**
- * Constructs a new sphere.
- *
- * @param {Vector3} [center=(0,0,0)] - The center of the sphere
- * @param {number} [radius=-1] - The radius of the sphere.
- */
- constructor( center = new Vector3(), radius = -1 ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isSphere = true;
-
- /**
- * The center of the sphere
- *
- * @type {Vector3}
- */
- this.center = center;
-
- /**
- * The radius of the sphere.
- *
- * @type {number}
- */
- this.radius = radius;
-
- }
-
- /**
- * Sets the sphere's components by copying the given values.
- *
- * @param {Vector3} center - The center.
- * @param {number} radius - The radius.
- * @return {Sphere} A reference to this sphere.
- */
- set( center, radius ) {
-
- this.center.copy( center );
- this.radius = radius;
-
- return this;
-
- }
-
- /**
- * Computes the minimum bounding sphere for list of points.
- * If the optional center point is given, it is used as the sphere's
- * center. Otherwise, the center of the axis-aligned bounding box
- * encompassing the points is calculated.
- *
- * @param {Array} points - A list of points in 3D space.
- * @param {Vector3} [optionalCenter] - The center of the sphere.
- * @return {Sphere} A reference to this sphere.
- */
- setFromPoints( points, optionalCenter ) {
-
- const center = this.center;
-
- if ( optionalCenter !== undefined ) {
-
- center.copy( optionalCenter );
-
- } else {
-
- _box$3.setFromPoints( points ).getCenter( center );
-
- }
-
- let maxRadiusSq = 0;
-
- for ( let i = 0, il = points.length; i < il; i ++ ) {
-
- maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( points[ i ] ) );
-
- }
-
- this.radius = Math.sqrt( maxRadiusSq );
-
- return this;
-
- }
-
- /**
- * Copies the values of the given sphere to this instance.
- *
- * @param {Sphere} sphere - The sphere to copy.
- * @return {Sphere} A reference to this sphere.
- */
- copy( sphere ) {
-
- this.center.copy( sphere.center );
- this.radius = sphere.radius;
-
- return this;
-
- }
-
- /**
- * Returns `true` if the sphere is empty (the radius set to a negative number).
- *
- * Spheres with a radius of `0` contain only their center point and are not
- * considered to be empty.
- *
- * @return {boolean} Whether this sphere is empty or not.
- */
- isEmpty() {
-
- return ( this.radius < 0 );
-
- }
-
- /**
- * Makes this sphere empty which means in encloses a zero space in 3D.
- *
- * @return {Sphere} A reference to this sphere.
- */
- makeEmpty() {
-
- this.center.set( 0, 0, 0 );
- this.radius = -1;
-
- return this;
-
- }
-
- /**
- * Returns `true` if this sphere contains the given point inclusive of
- * the surface of the sphere.
- *
- * @param {Vector3} point - The point to check.
- * @return {boolean} Whether this sphere contains the given point or not.
- */
- containsPoint( point ) {
-
- return ( point.distanceToSquared( this.center ) <= ( this.radius * this.radius ) );
-
- }
-
- /**
- * Returns the closest distance from the boundary of the sphere to the
- * given point. If the sphere contains the point, the distance will
- * be negative.
- *
- * @param {Vector3} point - The point to compute the distance to.
- * @return {number} The distance to the point.
- */
- distanceToPoint( point ) {
-
- return ( point.distanceTo( this.center ) - this.radius );
-
- }
-
- /**
- * Returns `true` if this sphere intersects with the given one.
- *
- * @param {Sphere} sphere - The sphere to test.
- * @return {boolean} Whether this sphere intersects with the given one or not.
- */
- intersectsSphere( sphere ) {
-
- const radiusSum = this.radius + sphere.radius;
-
- return sphere.center.distanceToSquared( this.center ) <= ( radiusSum * radiusSum );
-
- }
-
- /**
- * Returns `true` if this sphere intersects with the given box.
- *
- * @param {Box3} box - The box to test.
- * @return {boolean} Whether this sphere intersects with the given box or not.
- */
- intersectsBox( box ) {
-
- return box.intersectsSphere( this );
-
- }
-
- /**
- * Returns `true` if this sphere intersects with the given plane.
- *
- * @param {Plane} plane - The plane to test.
- * @return {boolean} Whether this sphere intersects with the given plane or not.
- */
- intersectsPlane( plane ) {
-
- return Math.abs( plane.distanceToPoint( this.center ) ) <= this.radius;
-
- }
-
- /**
- * Clamps a point within the sphere. If the point is outside the sphere, it
- * will clamp it to the closest point on the edge of the sphere. Points
- * already inside the sphere will not be affected.
- *
- * @param {Vector3} point - The plane to clamp.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The clamped point.
- */
- clampPoint( point, target ) {
-
- const deltaLengthSq = this.center.distanceToSquared( point );
-
- target.copy( point );
-
- if ( deltaLengthSq > ( this.radius * this.radius ) ) {
-
- target.sub( this.center ).normalize();
- target.multiplyScalar( this.radius ).add( this.center );
-
- }
-
- return target;
-
- }
-
- /**
- * Returns a bounding box that encloses this sphere.
- *
- * @param {Box3} target - The target box that is used to store the method's result.
- * @return {Box3} The bounding box that encloses this sphere.
- */
- getBoundingBox( target ) {
-
- if ( this.isEmpty() ) {
-
- // Empty sphere produces empty bounding box
- target.makeEmpty();
- return target;
-
- }
-
- target.set( this.center, this.center );
- target.expandByScalar( this.radius );
-
- return target;
-
- }
-
- /**
- * Transforms this sphere with the given 4x4 transformation matrix.
- *
- * @param {Matrix4} matrix - The transformation matrix.
- * @return {Sphere} A reference to this sphere.
- */
- applyMatrix4( matrix ) {
-
- this.center.applyMatrix4( matrix );
- this.radius = this.radius * matrix.getMaxScaleOnAxis();
-
- return this;
-
- }
-
- /**
- * Translates the sphere's center by the given offset.
- *
- * @param {Vector3} offset - The offset.
- * @return {Sphere} A reference to this sphere.
- */
- translate( offset ) {
-
- this.center.add( offset );
-
- return this;
-
- }
-
- /**
- * Expands the boundaries of this sphere to include the given point.
- *
- * @param {Vector3} point - The point to include.
- * @return {Sphere} A reference to this sphere.
- */
- expandByPoint( point ) {
-
- if ( this.isEmpty() ) {
-
- this.center.copy( point );
-
- this.radius = 0;
-
- return this;
-
- }
-
- _v1$3.subVectors( point, this.center );
-
- const lengthSq = _v1$3.lengthSq();
-
- if ( lengthSq > ( this.radius * this.radius ) ) {
-
- // calculate the minimal sphere
-
- const length = Math.sqrt( lengthSq );
-
- const delta = ( length - this.radius ) * 0.5;
-
- this.center.addScaledVector( _v1$3, delta / length );
-
- this.radius += delta;
-
- }
-
- return this;
-
- }
-
- /**
- * Expands this sphere to enclose both the original sphere and the given sphere.
- *
- * @param {Sphere} sphere - The sphere to include.
- * @return {Sphere} A reference to this sphere.
- */
- union( sphere ) {
-
- if ( sphere.isEmpty() ) {
-
- return this;
-
- }
-
- if ( this.isEmpty() ) {
-
- this.copy( sphere );
-
- return this;
-
- }
-
- if ( this.center.equals( sphere.center ) === true ) {
-
- this.radius = Math.max( this.radius, sphere.radius );
-
- } else {
-
- _v2$2.subVectors( sphere.center, this.center ).setLength( sphere.radius );
-
- this.expandByPoint( _v1$3.copy( sphere.center ).add( _v2$2 ) );
-
- this.expandByPoint( _v1$3.copy( sphere.center ).sub( _v2$2 ) );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns `true` if this sphere is equal with the given one.
- *
- * @param {Sphere} sphere - The sphere to test for equality.
- * @return {boolean} Whether this bounding sphere is equal with the given one.
- */
- equals( sphere ) {
-
- return sphere.center.equals( this.center ) && ( sphere.radius === this.radius );
-
- }
-
- /**
- * Returns a new sphere with copied values from this instance.
- *
- * @return {Sphere} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Returns a serialized structure of the bounding sphere.
- *
- * @return {Object} Serialized structure with fields representing the object state.
- */
- toJSON() {
-
- return {
- radius: this.radius,
- center: this.center.toArray()
- };
-
- }
-
- /**
- * Returns a serialized structure of the bounding sphere.
- *
- * @param {Object} json - The serialized json to set the sphere from.
- * @return {Sphere} A reference to this bounding sphere.
- */
- fromJSON( json ) {
-
- this.radius = json.radius;
- this.center.fromArray( json.center );
- return this;
-
- }
-
-}
-
-let _id$1 = 0;
-
-const _m1 = /*@__PURE__*/ new Matrix4();
-const _obj = /*@__PURE__*/ new Object3D();
-const _offset = /*@__PURE__*/ new Vector3();
-const _box$2 = /*@__PURE__*/ new Box3();
-const _boxMorphTargets = /*@__PURE__*/ new Box3();
-const _vector$9 = /*@__PURE__*/ new Vector3();
-
-/**
- * A representation of mesh, line, or point geometry. Includes vertex
- * positions, face indices, normals, colors, UVs, and custom attributes
- * within buffers, reducing the cost of passing all this data to the GPU.
- *
- * ```js
- * const geometry = new THREE.BufferGeometry();
- * // create a simple square shape. We duplicate the top left and bottom right
- * // vertices because each vertex needs to appear once per triangle.
- * const vertices = new Float32Array( [
- * -1.0, -1.0, 1.0, // v0
- * 1.0, -1.0, 1.0, // v1
- * 1.0, 1.0, 1.0, // v2
- *
- * 1.0, 1.0, 1.0, // v3
- * -1.0, 1.0, 1.0, // v4
- * -1.0, -1.0, 1.0 // v5
- * ] );
- * // itemSize = 3 because there are 3 values (components) per vertex
- * geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
- * const material = new THREE.MeshBasicMaterial( { color: 0xff0000 } );
- * const mesh = new THREE.Mesh( geometry, material );
- * ```
- *
- * @augments EventDispatcher
- */
-class BufferGeometry extends EventDispatcher {
-
- /**
- * Constructs a new geometry.
- */
- constructor() {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isBufferGeometry = true;
-
- /**
- * The ID of the geometry.
- *
- * @name BufferGeometry#id
- * @type {number}
- * @readonly
- */
- Object.defineProperty( this, 'id', { value: _id$1 ++ } );
-
- /**
- * The UUID of the geometry.
- *
- * @type {string}
- * @readonly
- */
- this.uuid = generateUUID();
-
- /**
- * The name of the geometry.
- *
- * @type {string}
- */
- this.name = '';
- this.type = 'BufferGeometry';
-
- /**
- * Allows for vertices to be re-used across multiple triangles; this is
- * called using "indexed triangles". Each triangle is associated with the
- * indices of three vertices. This attribute therefore stores the index of
- * each vertex for each triangular face. If this attribute is not set, the
- * renderer assumes that each three contiguous positions represent a single triangle.
- *
- * @type {?BufferAttribute}
- * @default null
- */
- this.index = null;
-
- /**
- * A (storage) buffer attribute which was generated with a compute shader and
- * now defines indirect draw calls.
- *
- * Can only be used with {@link WebGPURenderer} and a WebGPU backend.
- *
- * @type {?BufferAttribute}
- * @default null
- */
- this.indirect = null;
-
- /**
- * The offset, in bytes, into the indirect drawing buffer where the value data begins. If an array is provided, multiple indirect draw calls will be made for each offset.
- *
- * Can only be used with {@link WebGPURenderer} and a WebGPU backend.
- *
- * @type {number|Array}
- * @default 0
- */
- this.indirectOffset = 0;
-
- /**
- * This dictionary has as id the name of the attribute to be set and as value
- * the buffer attribute to set it to. Rather than accessing this property directly,
- * use `setAttribute()` and `getAttribute()` to access attributes of this geometry.
- *
- * @type {Object}
- */
- this.attributes = {};
-
- /**
- * This dictionary holds the morph targets of the geometry.
- *
- * Note: Once the geometry has been rendered, the morph attribute data cannot
- * be changed. You will have to call `dispose()`, and create a new geometry instance.
- *
- * @type {Object}
- */
- this.morphAttributes = {};
-
- /**
- * Used to control the morph target behavior; when set to `true`, the morph
- * target data is treated as relative offsets, rather than as absolute
- * positions/normals.
- *
- * @type {boolean}
- * @default false
- */
- this.morphTargetsRelative = false;
-
- /**
- * Split the geometry into groups, each of which will be rendered in a
- * separate draw call. This allows an array of materials to be used with the geometry.
- *
- * Use `addGroup()` and `clearGroups()` to edit groups, rather than modifying this array directly.
- *
- * Every vertex and index must belong to exactly one group — groups must not share vertices or
- * indices, and must not leave vertices or indices unused.
- *
- * @type {Array}
- */
- this.groups = [];
-
- /**
- * Bounding box for the geometry which can be calculated with `computeBoundingBox()`.
- *
- * @type {?Box3}
- * @default null
- */
- this.boundingBox = null;
-
- /**
- * Bounding sphere for the geometry which can be calculated with `computeBoundingSphere()`.
- *
- * @type {?Sphere}
- * @default null
- */
- this.boundingSphere = null;
-
- /**
- * Determines the part of the geometry to render. This should not be set directly,
- * instead use `setDrawRange()`.
- *
- * @type {{start:number,count:number}}
- */
- this.drawRange = { start: 0, count: Infinity };
-
- /**
- * An object that can be used to store custom data about the geometry.
- * It should not hold references to functions as these will not be cloned.
- *
- * @type {Object}
- */
- this.userData = {};
-
- /**
- * `true` when the geometry has been transformed since construction
- * (e.g. via {@link BufferGeometry#applyMatrix4}). Only relevant for
- * geometry generators (subclasses that populate `parameters`): when set,
- * {@link BufferGeometry#toJSON} omits `parameters` since they no longer
- * describe the geometry.
- *
- * @private
- * @type {boolean}
- * @default false
- */
- this._transformed = false;
-
- }
-
- /**
- * Returns the index of this geometry.
- *
- * @return {?BufferAttribute} The index. Returns `null` if no index is defined.
- */
- getIndex() {
-
- return this.index;
-
- }
-
- /**
- * Sets the given index to this geometry.
- *
- * @param {Array|BufferAttribute} index - The index to set.
- * @return {BufferGeometry} A reference to this instance.
- */
- setIndex( index ) {
-
- if ( Array.isArray( index ) ) {
-
- this.index = new ( arrayNeedsUint32( index ) ? Uint32BufferAttribute : Uint16BufferAttribute )( index, 1 );
-
- } else {
-
- this.index = index;
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the given indirect attribute to this geometry.
- *
- * @param {BufferAttribute} indirect - The attribute holding indirect draw calls.
- * @param {number|Array} [indirectOffset=0] - The offset, in bytes, into the indirect drawing buffer where the value data begins. If an array is provided, multiple indirect draw calls will be made for each offset.
- * @return {BufferGeometry} A reference to this instance.
- */
- setIndirect( indirect, indirectOffset = 0 ) {
-
- this.indirect = indirect;
- this.indirectOffset = indirectOffset;
-
- return this;
-
- }
-
- /**
- * Returns the indirect attribute of this geometry.
- *
- * @return {?BufferAttribute} The indirect attribute. Returns `null` if no indirect attribute is defined.
- */
- getIndirect() {
-
- return this.indirect;
-
- }
-
- /**
- * Returns the buffer attribute for the given name.
- *
- * @param {string} name - The attribute name.
- * @return {BufferAttribute|InterleavedBufferAttribute|undefined} The buffer attribute.
- * Returns `undefined` if not attribute has been found.
- */
- getAttribute( name ) {
-
- return this.attributes[ name ];
-
- }
-
- /**
- * Sets the given attribute for the given name.
- *
- * @param {string} name - The attribute name.
- * @param {BufferAttribute|InterleavedBufferAttribute} attribute - The attribute to set.
- * @return {BufferGeometry} A reference to this instance.
- */
- setAttribute( name, attribute ) {
-
- this.attributes[ name ] = attribute;
-
- return this;
-
- }
-
- /**
- * Deletes the attribute for the given name.
- *
- * @param {string} name - The attribute name to delete.
- * @return {BufferGeometry} A reference to this instance.
- */
- deleteAttribute( name ) {
-
- delete this.attributes[ name ];
-
- return this;
-
- }
-
- /**
- * Returns `true` if this geometry has an attribute for the given name.
- *
- * @param {string} name - The attribute name.
- * @return {boolean} Whether this geometry has an attribute for the given name or not.
- */
- hasAttribute( name ) {
-
- return this.attributes[ name ] !== undefined;
-
- }
-
- /**
- * Adds a group to this geometry.
- *
- * @param {number} start - The first element in this draw call. That is the first
- * vertex for non-indexed geometry, otherwise the first triangle index.
- * @param {number} count - Specifies how many vertices (or indices) are part of this group.
- * @param {number} [materialIndex=0] - The material array index to use.
- */
- addGroup( start, count, materialIndex = 0 ) {
-
- this.groups.push( {
-
- start: start,
- count: count,
- materialIndex: materialIndex
-
- } );
-
- }
-
- /**
- * Clears all groups.
- */
- clearGroups() {
-
- this.groups = [];
-
- }
-
- /**
- * Sets the draw range for this geometry.
- *
- * @param {number} start - The first vertex for non-indexed geometry, otherwise the first triangle index.
- * @param {number} count - For non-indexed BufferGeometry, `count` is the number of vertices to render.
- * For indexed BufferGeometry, `count` is the number of indices to render.
- */
- setDrawRange( start, count ) {
-
- this.drawRange.start = start;
- this.drawRange.count = count;
-
- }
-
- /**
- * Applies the given 4x4 transformation matrix to the geometry.
- *
- * @param {Matrix4} matrix - The matrix to apply.
- * @return {BufferGeometry} A reference to this instance.
- */
- applyMatrix4( matrix ) {
-
- const position = this.attributes.position;
-
- if ( position !== undefined ) {
-
- position.applyMatrix4( matrix );
-
- position.needsUpdate = true;
-
- }
-
- const normal = this.attributes.normal;
-
- if ( normal !== undefined ) {
-
- const normalMatrix = new Matrix3().getNormalMatrix( matrix );
-
- normal.applyNormalMatrix( normalMatrix );
-
- normal.needsUpdate = true;
-
- }
-
- const tangent = this.attributes.tangent;
-
- if ( tangent !== undefined ) {
-
- tangent.transformDirection( matrix );
-
- tangent.needsUpdate = true;
-
- }
-
- if ( this.boundingBox !== null ) {
-
- this.computeBoundingBox();
-
- }
-
- if ( this.boundingSphere !== null ) {
-
- this.computeBoundingSphere();
-
- }
-
- this._transformed = true;
-
- return this;
-
- }
-
- /**
- * Applies the rotation represented by the Quaternion to the geometry.
- *
- * @param {Quaternion} q - The Quaternion to apply.
- * @return {BufferGeometry} A reference to this instance.
- */
- applyQuaternion( q ) {
-
- _m1.makeRotationFromQuaternion( q );
-
- this.applyMatrix4( _m1 );
-
- return this;
-
- }
-
- /**
- * Rotates the geometry about the X axis. This is typically done as a one time
- * operation, and not during a loop. Use {@link Object3D#rotation} for typical
- * real-time mesh rotation.
- *
- * @param {number} angle - The angle in radians.
- * @return {BufferGeometry} A reference to this instance.
- */
- rotateX( angle ) {
-
- // rotate geometry around world x-axis
-
- _m1.makeRotationX( angle );
-
- this.applyMatrix4( _m1 );
-
- return this;
-
- }
-
- /**
- * Rotates the geometry about the Y axis. This is typically done as a one time
- * operation, and not during a loop. Use {@link Object3D#rotation} for typical
- * real-time mesh rotation.
- *
- * @param {number} angle - The angle in radians.
- * @return {BufferGeometry} A reference to this instance.
- */
- rotateY( angle ) {
-
- // rotate geometry around world y-axis
-
- _m1.makeRotationY( angle );
-
- this.applyMatrix4( _m1 );
-
- return this;
-
- }
-
- /**
- * Rotates the geometry about the Z axis. This is typically done as a one time
- * operation, and not during a loop. Use {@link Object3D#rotation} for typical
- * real-time mesh rotation.
- *
- * @param {number} angle - The angle in radians.
- * @return {BufferGeometry} A reference to this instance.
- */
- rotateZ( angle ) {
-
- // rotate geometry around world z-axis
-
- _m1.makeRotationZ( angle );
-
- this.applyMatrix4( _m1 );
-
- return this;
-
- }
-
- /**
- * Translates the geometry. This is typically done as a one time
- * operation, and not during a loop. Use {@link Object3D#position} for typical
- * real-time mesh rotation.
- *
- * @param {number} x - The x offset.
- * @param {number} y - The y offset.
- * @param {number} z - The z offset.
- * @return {BufferGeometry} A reference to this instance.
- */
- translate( x, y, z ) {
-
- // translate geometry
-
- _m1.makeTranslation( x, y, z );
-
- this.applyMatrix4( _m1 );
-
- return this;
-
- }
-
- /**
- * Scales the geometry. This is typically done as a one time
- * operation, and not during a loop. Use {@link Object3D#scale} for typical
- * real-time mesh rotation.
- *
- * @param {number} x - The x scale.
- * @param {number} y - The y scale.
- * @param {number} z - The z scale.
- * @return {BufferGeometry} A reference to this instance.
- */
- scale( x, y, z ) {
-
- // scale geometry
-
- _m1.makeScale( x, y, z );
-
- this.applyMatrix4( _m1 );
-
- return this;
-
- }
-
- /**
- * Rotates the geometry to face a point in 3D space. This is typically done as a one time
- * operation, and not during a loop. Use {@link Object3D#lookAt} for typical
- * real-time mesh rotation.
- *
- * @param {Vector3} vector - The target point.
- * @return {BufferGeometry} A reference to this instance.
- */
- lookAt( vector ) {
-
- _obj.lookAt( vector );
-
- _obj.updateMatrix();
-
- this.applyMatrix4( _obj.matrix );
-
- return this;
-
- }
-
- /**
- * Center the geometry based on its bounding box.
- *
- * @return {BufferGeometry} A reference to this instance.
- */
- center() {
-
- this.computeBoundingBox();
-
- this.boundingBox.getCenter( _offset ).negate();
-
- this.translate( _offset.x, _offset.y, _offset.z );
-
- return this;
-
- }
-
- /**
- * Defines a geometry by creating a `position` attribute based on the given array of points. The array
- * can hold 2D or 3D vectors. When using two-dimensional data, the `z` coordinate for all vertices is
- * set to `0`.
- *
- * If the method is used with an existing `position` attribute, the vertex data are overwritten with the
- * data from the array. The length of the array must match the vertex count.
- *
- * @param {Array|Array} points - The points.
- * @return {BufferGeometry} A reference to this instance.
- */
- setFromPoints( points ) {
-
- const positionAttribute = this.getAttribute( 'position' );
-
- if ( positionAttribute === undefined ) {
-
- const position = [];
-
- for ( let i = 0, l = points.length; i < l; i ++ ) {
-
- const point = points[ i ];
- position.push( point.x, point.y, point.z || 0 );
-
- }
-
- this.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );
-
- } else {
-
- const l = Math.min( points.length, positionAttribute.count ); // make sure data do not exceed buffer size
-
- for ( let i = 0; i < l; i ++ ) {
-
- const point = points[ i ];
- positionAttribute.setXYZ( i, point.x, point.y, point.z || 0 );
-
- }
-
- if ( points.length > positionAttribute.count ) {
-
- warn( 'BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry.' );
-
- }
-
- positionAttribute.needsUpdate = true;
-
- }
-
- return this;
-
- }
-
- /**
- * Computes the bounding box of the geometry, and updates the `boundingBox` member.
- * The bounding box is not computed by the engine; it must be computed by your app.
- * You may need to recompute the bounding box if the geometry vertices are modified.
- */
- computeBoundingBox() {
-
- if ( this.boundingBox === null ) {
-
- this.boundingBox = new Box3();
-
- }
-
- const position = this.attributes.position;
- const morphAttributesPosition = this.morphAttributes.position;
-
- if ( position && position.isGLBufferAttribute ) {
-
- error( 'BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.', this );
-
- this.boundingBox.set(
- new Vector3( - Infinity, - Infinity, - Infinity ),
- new Vector3( + Infinity, + Infinity, + Infinity )
- );
-
- return;
-
- }
-
- if ( position !== undefined ) {
-
- this.boundingBox.setFromBufferAttribute( position );
-
- // process morph attributes if present
-
- if ( morphAttributesPosition ) {
-
- for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {
-
- const morphAttribute = morphAttributesPosition[ i ];
- _box$2.setFromBufferAttribute( morphAttribute );
-
- if ( this.morphTargetsRelative ) {
-
- _vector$9.addVectors( this.boundingBox.min, _box$2.min );
- this.boundingBox.expandByPoint( _vector$9 );
-
- _vector$9.addVectors( this.boundingBox.max, _box$2.max );
- this.boundingBox.expandByPoint( _vector$9 );
-
- } else {
-
- this.boundingBox.expandByPoint( _box$2.min );
- this.boundingBox.expandByPoint( _box$2.max );
-
- }
-
- }
-
- }
-
- } else {
-
- this.boundingBox.makeEmpty();
-
- }
-
- if ( isNaN( this.boundingBox.min.x ) || isNaN( this.boundingBox.min.y ) || isNaN( this.boundingBox.min.z ) ) {
-
- error( 'BufferGeometry.computeBoundingBox(): Computed min/max have NaN values. The "position" attribute is likely to have NaN values.', this );
-
- }
-
- }
-
- /**
- * Computes the bounding sphere of the geometry, and updates the `boundingSphere` member.
- * The engine automatically computes the bounding sphere when it is needed, e.g., for ray casting or view frustum culling.
- * You may need to recompute the bounding sphere if the geometry vertices are modified.
- */
- computeBoundingSphere() {
-
- if ( this.boundingSphere === null ) {
-
- this.boundingSphere = new Sphere();
-
- }
-
- const position = this.attributes.position;
- const morphAttributesPosition = this.morphAttributes.position;
-
- if ( position && position.isGLBufferAttribute ) {
-
- error( 'BufferGeometry.computeBoundingSphere(): GLBufferAttribute requires a manual bounding sphere.', this );
-
- this.boundingSphere.set( new Vector3(), Infinity );
-
- return;
-
- }
-
- if ( position ) {
-
- // first, find the center of the bounding sphere
-
- const center = this.boundingSphere.center;
-
- _box$2.setFromBufferAttribute( position );
-
- // process morph attributes if present
-
- if ( morphAttributesPosition ) {
-
- for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {
-
- const morphAttribute = morphAttributesPosition[ i ];
- _boxMorphTargets.setFromBufferAttribute( morphAttribute );
-
- if ( this.morphTargetsRelative ) {
-
- _vector$9.addVectors( _box$2.min, _boxMorphTargets.min );
- _box$2.expandByPoint( _vector$9 );
-
- _vector$9.addVectors( _box$2.max, _boxMorphTargets.max );
- _box$2.expandByPoint( _vector$9 );
-
- } else {
-
- _box$2.expandByPoint( _boxMorphTargets.min );
- _box$2.expandByPoint( _boxMorphTargets.max );
-
- }
-
- }
-
- }
-
- _box$2.getCenter( center );
-
- // second, try to find a boundingSphere with a radius smaller than the
- // boundingSphere of the boundingBox: sqrt(3) smaller in the best case
-
- let maxRadiusSq = 0;
-
- for ( let i = 0, il = position.count; i < il; i ++ ) {
-
- _vector$9.fromBufferAttribute( position, i );
-
- maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector$9 ) );
-
- }
-
- // process morph attributes if present
-
- if ( morphAttributesPosition ) {
-
- for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {
-
- const morphAttribute = morphAttributesPosition[ i ];
- const morphTargetsRelative = this.morphTargetsRelative;
-
- for ( let j = 0, jl = morphAttribute.count; j < jl; j ++ ) {
-
- _vector$9.fromBufferAttribute( morphAttribute, j );
-
- if ( morphTargetsRelative ) {
-
- _offset.fromBufferAttribute( position, j );
- _vector$9.add( _offset );
-
- }
-
- maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector$9 ) );
-
- }
-
- }
-
- }
-
- this.boundingSphere.radius = Math.sqrt( maxRadiusSq );
-
- if ( isNaN( this.boundingSphere.radius ) ) {
-
- error( 'BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values.', this );
-
- }
-
- }
-
- }
-
- /**
- * Calculates and adds a tangent attribute to this geometry.
- *
- * The computation is only supported for indexed geometries and if position, normal, and uv attributes
- * are defined. When using a tangent space normal map, prefer the MikkTSpace algorithm provided by
- * {@link BufferGeometryUtils#computeMikkTSpaceTangents} instead.
- */
- computeTangents() {
-
- const index = this.index;
- const attributes = this.attributes;
-
- // based on http://www.terathon.com/code/tangent.html
- // (per vertex tangents)
-
- if ( index === null ||
- attributes.position === undefined ||
- attributes.normal === undefined ||
- attributes.uv === undefined ) {
-
- error( 'BufferGeometry: .computeTangents() failed. Missing required attributes (index, position, normal or uv)' );
- return;
-
- }
-
- const positionAttribute = attributes.position;
- const normalAttribute = attributes.normal;
- const uvAttribute = attributes.uv;
-
- let tangentAttribute = this.getAttribute( 'tangent' );
-
- if ( tangentAttribute === undefined || tangentAttribute.count !== positionAttribute.count ) {
-
- tangentAttribute = new BufferAttribute( new Float32Array( 4 * positionAttribute.count ), 4 );
- this.setAttribute( 'tangent', tangentAttribute );
-
- }
-
- const tan1 = [], tan2 = [];
-
- for ( let i = 0; i < positionAttribute.count; i ++ ) {
-
- tan1[ i ] = new Vector3();
- tan2[ i ] = new Vector3();
-
- }
-
- const vA = new Vector3(),
- vB = new Vector3(),
- vC = new Vector3(),
-
- uvA = new Vector2(),
- uvB = new Vector2(),
- uvC = new Vector2(),
-
- sdir = new Vector3(),
- tdir = new Vector3();
-
- function handleTriangle( a, b, c ) {
-
- vA.fromBufferAttribute( positionAttribute, a );
- vB.fromBufferAttribute( positionAttribute, b );
- vC.fromBufferAttribute( positionAttribute, c );
-
- uvA.fromBufferAttribute( uvAttribute, a );
- uvB.fromBufferAttribute( uvAttribute, b );
- uvC.fromBufferAttribute( uvAttribute, c );
-
- vB.sub( vA );
- vC.sub( vA );
-
- uvB.sub( uvA );
- uvC.sub( uvA );
-
- const r = 1.0 / ( uvB.x * uvC.y - uvC.x * uvB.y );
-
- // silently ignore degenerate uv triangles having coincident or colinear vertices
-
- if ( ! isFinite( r ) ) return;
-
- sdir.copy( vB ).multiplyScalar( uvC.y ).addScaledVector( vC, - uvB.y ).multiplyScalar( r );
- tdir.copy( vC ).multiplyScalar( uvB.x ).addScaledVector( vB, - uvC.x ).multiplyScalar( r );
-
- tan1[ a ].add( sdir );
- tan1[ b ].add( sdir );
- tan1[ c ].add( sdir );
-
- tan2[ a ].add( tdir );
- tan2[ b ].add( tdir );
- tan2[ c ].add( tdir );
-
- }
-
- let groups = this.groups;
-
- if ( groups.length === 0 ) {
-
- groups = [ {
- start: 0,
- count: index.count
- } ];
-
- }
-
- for ( let i = 0, il = groups.length; i < il; ++ i ) {
-
- const group = groups[ i ];
-
- const start = group.start;
- const count = group.count;
-
- for ( let j = start, jl = start + count; j < jl; j += 3 ) {
-
- handleTriangle(
- index.getX( j + 0 ),
- index.getX( j + 1 ),
- index.getX( j + 2 )
- );
-
- }
-
- }
-
- const tmp = new Vector3(), tmp2 = new Vector3();
- const n = new Vector3(), n2 = new Vector3();
-
- function handleVertex( v ) {
-
- n.fromBufferAttribute( normalAttribute, v );
- n2.copy( n );
-
- const t = tan1[ v ];
-
- // Gram-Schmidt orthogonalize
-
- tmp.copy( t );
- tmp.sub( n.multiplyScalar( n.dot( t ) ) ).normalize();
-
- // Calculate handedness
-
- tmp2.crossVectors( n2, t );
- const test = tmp2.dot( tan2[ v ] );
- const w = ( test < 0.0 ) ? -1 : 1.0;
-
- tangentAttribute.setXYZW( v, tmp.x, tmp.y, tmp.z, w );
-
- }
-
- for ( let i = 0, il = groups.length; i < il; ++ i ) {
-
- const group = groups[ i ];
-
- const start = group.start;
- const count = group.count;
-
- for ( let j = start, jl = start + count; j < jl; j += 3 ) {
-
- handleVertex( index.getX( j + 0 ) );
- handleVertex( index.getX( j + 1 ) );
- handleVertex( index.getX( j + 2 ) );
-
- }
-
- }
-
- this._transformed = true;
-
- }
-
- /**
- * Computes vertex normals for the given vertex data. For indexed geometries, the method sets
- * each vertex normal to be the average of the face normals of the faces that share that vertex.
- * For non-indexed geometries, vertices are not shared, and the method sets each vertex normal
- * to be the same as the face normal.
- */
- computeVertexNormals() {
-
- const index = this.index;
- const positionAttribute = this.getAttribute( 'position' );
-
- if ( positionAttribute !== undefined ) {
-
- let normalAttribute = this.getAttribute( 'normal' );
-
- if ( normalAttribute === undefined || normalAttribute.count !== positionAttribute.count ) {
-
- normalAttribute = new BufferAttribute( new Float32Array( positionAttribute.count * 3 ), 3 );
- this.setAttribute( 'normal', normalAttribute );
-
- } else {
-
- // reset existing normals to zero
-
- for ( let i = 0, il = normalAttribute.count; i < il; i ++ ) {
-
- normalAttribute.setXYZ( i, 0, 0, 0 );
-
- }
-
- }
-
- const pA = new Vector3(), pB = new Vector3(), pC = new Vector3();
- const nA = new Vector3(), nB = new Vector3(), nC = new Vector3();
- const cb = new Vector3(), ab = new Vector3();
-
- // indexed elements
-
- if ( index ) {
-
- for ( let i = 0, il = index.count; i < il; i += 3 ) {
-
- const vA = index.getX( i + 0 );
- const vB = index.getX( i + 1 );
- const vC = index.getX( i + 2 );
-
- pA.fromBufferAttribute( positionAttribute, vA );
- pB.fromBufferAttribute( positionAttribute, vB );
- pC.fromBufferAttribute( positionAttribute, vC );
-
- cb.subVectors( pC, pB );
- ab.subVectors( pA, pB );
- cb.cross( ab );
-
- nA.fromBufferAttribute( normalAttribute, vA );
- nB.fromBufferAttribute( normalAttribute, vB );
- nC.fromBufferAttribute( normalAttribute, vC );
-
- nA.add( cb );
- nB.add( cb );
- nC.add( cb );
-
- normalAttribute.setXYZ( vA, nA.x, nA.y, nA.z );
- normalAttribute.setXYZ( vB, nB.x, nB.y, nB.z );
- normalAttribute.setXYZ( vC, nC.x, nC.y, nC.z );
-
- }
-
- } else {
-
- // non-indexed elements (unconnected triangle soup)
-
- for ( let i = 0, il = positionAttribute.count; i < il; i += 3 ) {
-
- pA.fromBufferAttribute( positionAttribute, i + 0 );
- pB.fromBufferAttribute( positionAttribute, i + 1 );
- pC.fromBufferAttribute( positionAttribute, i + 2 );
-
- cb.subVectors( pC, pB );
- ab.subVectors( pA, pB );
- cb.cross( ab );
-
- normalAttribute.setXYZ( i + 0, cb.x, cb.y, cb.z );
- normalAttribute.setXYZ( i + 1, cb.x, cb.y, cb.z );
- normalAttribute.setXYZ( i + 2, cb.x, cb.y, cb.z );
-
- }
-
- }
-
- this.normalizeNormals();
-
- normalAttribute.needsUpdate = true;
-
- }
-
- }
-
- /**
- * Ensures every normal vector in a geometry will have a magnitude of `1`. This will
- * correct lighting on the geometry surfaces.
- */
- normalizeNormals() {
-
- const normals = this.attributes.normal;
-
- for ( let i = 0, il = normals.count; i < il; i ++ ) {
-
- _vector$9.fromBufferAttribute( normals, i );
-
- _vector$9.normalize();
-
- normals.setXYZ( i, _vector$9.x, _vector$9.y, _vector$9.z );
-
- }
-
- }
-
- /**
- * Return a new non-index version of this indexed geometry. If the geometry
- * is already non-indexed, the method is a NOOP.
- *
- * @return {BufferGeometry} The non-indexed version of this indexed geometry.
- */
- toNonIndexed() {
-
- function convertBufferAttribute( attribute, indices ) {
-
- const array = attribute.array;
- const itemSize = attribute.itemSize;
- const normalized = attribute.normalized;
-
- const array2 = new array.constructor( indices.length * itemSize );
-
- let index = 0, index2 = 0;
-
- for ( let i = 0, l = indices.length; i < l; i ++ ) {
-
- if ( attribute.isInterleavedBufferAttribute ) {
-
- index = indices[ i ] * attribute.data.stride + attribute.offset;
-
- } else {
-
- index = indices[ i ] * itemSize;
-
- }
-
- for ( let j = 0; j < itemSize; j ++ ) {
-
- array2[ index2 ++ ] = array[ index ++ ];
-
- }
-
- }
-
- return new BufferAttribute( array2, itemSize, normalized );
-
- }
-
- //
-
- if ( this.index === null ) {
-
- warn( 'BufferGeometry.toNonIndexed(): BufferGeometry is already non-indexed.' );
- return this;
-
- }
-
- const geometry2 = new BufferGeometry();
-
- const indices = this.index.array;
- const attributes = this.attributes;
-
- // attributes
-
- for ( const name in attributes ) {
-
- const attribute = attributes[ name ];
-
- const newAttribute = convertBufferAttribute( attribute, indices );
-
- geometry2.setAttribute( name, newAttribute );
-
- }
-
- // morph attributes
-
- const morphAttributes = this.morphAttributes;
-
- for ( const name in morphAttributes ) {
-
- const morphArray = [];
- const morphAttribute = morphAttributes[ name ]; // morphAttribute: array of Float32BufferAttributes
-
- for ( let i = 0, il = morphAttribute.length; i < il; i ++ ) {
-
- const attribute = morphAttribute[ i ];
-
- const newAttribute = convertBufferAttribute( attribute, indices );
-
- morphArray.push( newAttribute );
-
- }
-
- geometry2.morphAttributes[ name ] = morphArray;
-
- }
-
- geometry2.morphTargetsRelative = this.morphTargetsRelative;
-
- // groups
-
- const groups = this.groups;
-
- for ( let i = 0, l = groups.length; i < l; i ++ ) {
-
- const group = groups[ i ];
- geometry2.addGroup( group.start, group.count, group.materialIndex );
-
- }
-
- return geometry2;
-
- }
-
- /**
- * Serializes the geometry into JSON.
- *
- * @return {Object} A JSON object representing the serialized geometry.
- */
- toJSON() {
-
- const data = {
- metadata: {
- version: 4.7,
- type: 'BufferGeometry',
- generator: 'BufferGeometry.toJSON'
- }
- };
-
- // standard BufferGeometry serialization
-
- data.uuid = this.uuid;
- data.type = ( this.parameters !== undefined && this._transformed === true ) ? 'BufferGeometry' : this.type;
- data.name = this.name;
- if ( Object.keys( this.userData ).length > 0 ) data.userData = this.userData;
-
- if ( this.parameters !== undefined && this._transformed !== true ) {
-
- const parameters = this.parameters;
-
- for ( const key in parameters ) {
-
- if ( parameters[ key ] !== undefined ) data[ key ] = parameters[ key ];
-
- }
-
- return data;
-
- }
-
- // for simplicity the code assumes attributes are not shared across geometries, see #15811
-
- data.data = { attributes: {} };
-
- const index = this.index;
-
- if ( index !== null ) {
-
- data.data.index = {
- type: index.array.constructor.name,
- array: Array.prototype.slice.call( index.array )
- };
-
- }
-
- const attributes = this.attributes;
-
- for ( const key in attributes ) {
-
- const attribute = attributes[ key ];
-
- data.data.attributes[ key ] = attribute.toJSON( data.data );
-
- }
-
- const morphAttributes = {};
- let hasMorphAttributes = false;
-
- for ( const key in this.morphAttributes ) {
-
- const attributeArray = this.morphAttributes[ key ];
-
- const array = [];
-
- for ( let i = 0, il = attributeArray.length; i < il; i ++ ) {
-
- const attribute = attributeArray[ i ];
-
- array.push( attribute.toJSON( data.data ) );
-
- }
-
- if ( array.length > 0 ) {
-
- morphAttributes[ key ] = array;
-
- hasMorphAttributes = true;
-
- }
-
- }
-
- if ( hasMorphAttributes ) {
-
- data.data.morphAttributes = morphAttributes;
- data.data.morphTargetsRelative = this.morphTargetsRelative;
-
- }
-
- const groups = this.groups;
-
- if ( groups.length > 0 ) {
-
- data.data.groups = JSON.parse( JSON.stringify( groups ) );
-
- }
-
- const boundingSphere = this.boundingSphere;
-
- if ( boundingSphere !== null ) {
-
- data.data.boundingSphere = boundingSphere.toJSON();
-
- }
-
- return data;
-
- }
-
- /**
- * Returns a new geometry with copied values from this instance.
- *
- * @return {BufferGeometry} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Copies the values of the given geometry to this instance.
- *
- * @param {BufferGeometry} source - The geometry to copy.
- * @return {BufferGeometry} A reference to this instance.
- */
- copy( source ) {
-
- // reset
-
- this.index = null;
- this.attributes = {};
- this.morphAttributes = {};
- this.groups = [];
- this.boundingBox = null;
- this.boundingSphere = null;
-
- // used for storing cloned, shared data
-
- const data = {};
-
- // name
-
- this.name = source.name;
-
- // index
-
- const index = source.index;
-
- if ( index !== null ) {
-
- this.setIndex( index.clone() );
-
- }
-
- // attributes
-
- const attributes = source.attributes;
-
- for ( const name in attributes ) {
-
- const attribute = attributes[ name ];
- this.setAttribute( name, attribute.clone( data ) );
-
- }
-
- // morph attributes
-
- const morphAttributes = source.morphAttributes;
-
- for ( const name in morphAttributes ) {
-
- const array = [];
- const morphAttribute = morphAttributes[ name ]; // morphAttribute: array of Float32BufferAttributes
-
- for ( let i = 0, l = morphAttribute.length; i < l; i ++ ) {
-
- array.push( morphAttribute[ i ].clone( data ) );
-
- }
-
- this.morphAttributes[ name ] = array;
-
- }
-
- this.morphTargetsRelative = source.morphTargetsRelative;
-
- // groups
-
- const groups = source.groups;
-
- for ( let i = 0, l = groups.length; i < l; i ++ ) {
-
- const group = groups[ i ];
- this.addGroup( group.start, group.count, group.materialIndex );
-
- }
-
- // bounding box
-
- const boundingBox = source.boundingBox;
-
- if ( boundingBox !== null ) {
-
- this.boundingBox = boundingBox.clone();
-
- }
-
- // bounding sphere
-
- const boundingSphere = source.boundingSphere;
-
- if ( boundingSphere !== null ) {
-
- this.boundingSphere = boundingSphere.clone();
-
- }
-
- // draw range
-
- this.drawRange.start = source.drawRange.start;
- this.drawRange.count = source.drawRange.count;
-
- // user data
-
- this.userData = source.userData;
-
- // transformed flag
-
- this._transformed = source._transformed;
-
- return this;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- *
- * @fires BufferGeometry#dispose
- */
- dispose() {
-
- this.dispatchEvent( { type: 'dispose' } );
-
- }
-
-}
-
-/**
- * "Interleaved" means that multiple attributes, possibly of different types,
- * (e.g., position, normal, uv, color) are packed into a single array buffer.
- *
- * An introduction into interleaved arrays can be found here: [Interleaved array basics](https://blog.tojicode.com/2011/05/interleaved-array-basics.html)
- */
-class InterleavedBuffer {
-
- /**
- * Constructs a new interleaved buffer.
- *
- * @param {TypedArray} array - A typed array with a shared buffer storing attribute data.
- * @param {number} stride - The number of typed-array elements per vertex.
- */
- constructor( array, stride ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isInterleavedBuffer = true;
-
- /**
- * A typed array with a shared buffer storing attribute data.
- *
- * @type {TypedArray}
- */
- this.array = array;
-
- /**
- * The number of typed-array elements per vertex.
- *
- * @type {number}
- */
- this.stride = stride;
-
- /**
- * The total number of elements in the array
- *
- * @type {number}
- * @readonly
- */
- this.count = array !== undefined ? array.length / stride : 0;
-
- /**
- * Defines the intended usage pattern of the data store for optimization purposes.
- *
- * Note: After the initial use of a buffer, its usage cannot be changed. Instead,
- * instantiate a new one and set the desired usage before the next render.
- *
- * @type {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)}
- * @default StaticDrawUsage
- */
- this.usage = StaticDrawUsage;
-
- /**
- * This can be used to only update some components of stored vectors (for example, just the
- * component related to color). Use the `addUpdateRange()` function to add ranges to this array.
- *
- * @type {Array}
- */
- this.updateRanges = [];
-
- /**
- * A version number, incremented every time the `needsUpdate` is set to `true`.
- *
- * @type {number}
- */
- this.version = 0;
-
- /**
- * The UUID of the interleaved buffer.
- *
- * @type {string}
- * @readonly
- */
- this.uuid = generateUUID();
-
- }
-
- /**
- * A callback function that is executed after the renderer has transferred the attribute array
- * data to the GPU.
- */
- onUploadCallback() {}
-
- /**
- * Flag to indicate that this attribute has changed and should be re-sent to
- * the GPU. Set this to `true` when you modify the value of the array.
- *
- * @type {number}
- * @default false
- * @param {boolean} value
- */
- set needsUpdate( value ) {
-
- if ( value === true ) this.version ++;
-
- }
-
- /**
- * Sets the usage of this interleaved buffer.
- *
- * @param {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)} value - The usage to set.
- * @return {InterleavedBuffer} A reference to this interleaved buffer.
- */
- setUsage( value ) {
-
- this.usage = value;
-
- return this;
-
- }
-
- /**
- * Adds a range of data in the data array to be updated on the GPU.
- *
- * @param {number} start - Position at which to start update.
- * @param {number} count - The number of components to update.
- */
- addUpdateRange( start, count ) {
-
- this.updateRanges.push( { start, count } );
-
- }
-
- /**
- * Clears the update ranges.
- */
- clearUpdateRanges() {
-
- this.updateRanges.length = 0;
-
- }
-
- /**
- * Copies the values of the given interleaved buffer to this instance.
- *
- * @param {InterleavedBuffer} source - The interleaved buffer to copy.
- * @return {InterleavedBuffer} A reference to this instance.
- */
- copy( source ) {
-
- this.array = new source.array.constructor( source.array );
- this.count = source.count;
- this.stride = source.stride;
- this.usage = source.usage;
-
- return this;
-
- }
-
- /**
- * Copies a vector from the given interleaved buffer to this one. The start
- * and destination position in the attribute buffers are represented by the
- * given indices.
- *
- * @param {number} index1 - The destination index into this interleaved buffer.
- * @param {InterleavedBuffer} interleavedBuffer - The interleaved buffer to copy from.
- * @param {number} index2 - The source index into the given interleaved buffer.
- * @return {InterleavedBuffer} A reference to this instance.
- */
- copyAt( index1, interleavedBuffer, index2 ) {
-
- index1 *= this.stride;
- index2 *= interleavedBuffer.stride;
-
- for ( let i = 0, l = this.stride; i < l; i ++ ) {
-
- this.array[ index1 + i ] = interleavedBuffer.array[ index2 + i ];
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the given array data in the interleaved buffer.
- *
- * @param {(TypedArray|Array)} value - The array data to set.
- * @param {number} [offset=0] - The offset in this interleaved buffer's array.
- * @return {InterleavedBuffer} A reference to this instance.
- */
- set( value, offset = 0 ) {
-
- this.array.set( value, offset );
-
- return this;
-
- }
-
- /**
- * Returns a new interleaved buffer with copied values from this instance.
- *
- * @param {Object} [data] - An object with shared array buffers that allows to retain shared structures.
- * @return {InterleavedBuffer} A clone of this instance.
- */
- clone( data ) {
-
- if ( data.arrayBuffers === undefined ) {
-
- data.arrayBuffers = {};
-
- }
-
- if ( this.array.buffer._uuid === undefined ) {
-
- this.array.buffer._uuid = generateUUID();
-
- }
-
- if ( data.arrayBuffers[ this.array.buffer._uuid ] === undefined ) {
-
- data.arrayBuffers[ this.array.buffer._uuid ] = this.array.slice( 0 ).buffer;
-
- }
-
- const array = new this.array.constructor( data.arrayBuffers[ this.array.buffer._uuid ] );
-
- const ib = new this.constructor( array, this.stride );
- ib.setUsage( this.usage );
-
- return ib;
-
- }
-
- /**
- * Sets the given callback function that is executed after the Renderer has transferred
- * the array data to the GPU. Can be used to perform clean-up operations after
- * the upload when data are not needed anymore on the CPU side.
- *
- * @param {Function} callback - The `onUpload()` callback.
- * @return {InterleavedBuffer} A reference to this instance.
- */
- onUpload( callback ) {
-
- this.onUploadCallback = callback;
-
- return this;
-
- }
-
- /**
- * Serializes the interleaved buffer into JSON.
- *
- * @param {Object} [data] - An optional value holding meta information about the serialization.
- * @return {Object} A JSON object representing the serialized interleaved buffer.
- */
- toJSON( data ) {
-
- if ( data.arrayBuffers === undefined ) {
-
- data.arrayBuffers = {};
-
- }
-
- // generate UUID for array buffer if necessary
-
- if ( this.array.buffer._uuid === undefined ) {
-
- this.array.buffer._uuid = generateUUID();
-
- }
-
- if ( data.arrayBuffers[ this.array.buffer._uuid ] === undefined ) {
-
- data.arrayBuffers[ this.array.buffer._uuid ] = Array.from( new Uint32Array( this.array.buffer ) );
-
- }
-
- //
-
- const json = {
- uuid: this.uuid,
- buffer: this.array.buffer._uuid,
- type: this.array.constructor.name,
- stride: this.stride
- };
-
- json.usage = this.usage;
-
- return json;
-
- }
-
-}
-
-const _vector$8 = /*@__PURE__*/ new Vector3();
-
-/**
- * An alternative version of a buffer attribute with interleaved data. Interleaved
- * attributes share a common interleaved data storage ({@link InterleavedBuffer}) and refer with
- * different offsets into the buffer.
- */
-class InterleavedBufferAttribute {
-
- /**
- * Constructs a new interleaved buffer attribute.
- *
- * @param {InterleavedBuffer} interleavedBuffer - The buffer holding the interleaved data.
- * @param {number} itemSize - The item size.
- * @param {number} offset - The attribute offset into the buffer.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( interleavedBuffer, itemSize, offset, normalized = false ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isInterleavedBufferAttribute = true;
-
- /**
- * The name of the buffer attribute.
- *
- * @type {string}
- */
- this.name = '';
-
- /**
- * The buffer holding the interleaved data.
- *
- * @type {InterleavedBuffer}
- */
- this.data = interleavedBuffer;
-
- /**
- * The item size, see {@link BufferAttribute#itemSize}.
- *
- * @type {number}
- */
- this.itemSize = itemSize;
-
- /**
- * The attribute offset into the buffer.
- *
- * @type {number}
- */
- this.offset = offset;
-
- /**
- * Whether the data are normalized or not, see {@link BufferAttribute#normalized}
- *
- * @type {InterleavedBuffer}
- */
- this.normalized = normalized;
-
- }
-
- /**
- * The item count of this buffer attribute.
- *
- * @type {number}
- * @readonly
- */
- get count() {
-
- return this.data.count;
-
- }
-
- /**
- * The array holding the interleaved buffer attribute data.
- *
- * @type {TypedArray}
- */
- get array() {
-
- return this.data.array;
-
- }
-
- /**
- * Flag to indicate that this attribute has changed and should be re-sent to
- * the GPU. Set this to `true` when you modify the value of the array.
- *
- * @type {number}
- * @default false
- * @param {boolean} value
- */
- set needsUpdate( value ) {
-
- this.data.needsUpdate = value;
-
- }
-
- /**
- * Applies the given 4x4 matrix to the given attribute. Only works with
- * item size `3`.
- *
- * @param {Matrix4} m - The matrix to apply.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- applyMatrix4( m ) {
-
- for ( let i = 0, l = this.data.count; i < l; i ++ ) {
-
- _vector$8.fromBufferAttribute( this, i );
-
- _vector$8.applyMatrix4( m );
-
- this.setXYZ( i, _vector$8.x, _vector$8.y, _vector$8.z );
-
- }
-
- return this;
-
- }
-
- /**
- * Applies the given 3x3 normal matrix to the given attribute. Only works with
- * item size `3`.
- *
- * @param {Matrix3} m - The normal matrix to apply.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- applyNormalMatrix( m ) {
-
- for ( let i = 0, l = this.count; i < l; i ++ ) {
-
- _vector$8.fromBufferAttribute( this, i );
-
- _vector$8.applyNormalMatrix( m );
-
- this.setXYZ( i, _vector$8.x, _vector$8.y, _vector$8.z );
-
- }
-
- return this;
-
- }
-
- /**
- * Applies the given 4x4 matrix to the given attribute. Only works with
- * item size `3` and with direction vectors.
- *
- * @param {Matrix4} m - The matrix to apply.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- transformDirection( m ) {
-
- for ( let i = 0, l = this.count; i < l; i ++ ) {
-
- _vector$8.fromBufferAttribute( this, i );
-
- _vector$8.transformDirection( m );
-
- this.setXYZ( i, _vector$8.x, _vector$8.y, _vector$8.z );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns the given component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} component - The component index.
- * @return {number} The returned value.
- */
- getComponent( index, component ) {
-
- let value = this.array[ index * this.data.stride + this.offset + component ];
-
- if ( this.normalized ) value = denormalize( value, this.array );
-
- return value;
-
- }
-
- /**
- * Sets the given value to the given component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} component - The component index.
- * @param {number} value - The value to set.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- setComponent( index, component, value ) {
-
- if ( this.normalized ) value = normalize( value, this.array );
-
- this.data.array[ index * this.data.stride + this.offset + component ] = value;
-
- return this;
-
- }
-
- /**
- * Sets the x component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} x - The value to set.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- setX( index, x ) {
-
- if ( this.normalized ) x = normalize( x, this.array );
-
- this.data.array[ index * this.data.stride + this.offset ] = x;
-
- return this;
-
- }
-
- /**
- * Sets the y component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} y - The value to set.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- setY( index, y ) {
-
- if ( this.normalized ) y = normalize( y, this.array );
-
- this.data.array[ index * this.data.stride + this.offset + 1 ] = y;
-
- return this;
-
- }
-
- /**
- * Sets the z component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} z - The value to set.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- setZ( index, z ) {
-
- if ( this.normalized ) z = normalize( z, this.array );
-
- this.data.array[ index * this.data.stride + this.offset + 2 ] = z;
-
- return this;
-
- }
-
- /**
- * Sets the w component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} w - The value to set.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- setW( index, w ) {
-
- if ( this.normalized ) w = normalize( w, this.array );
-
- this.data.array[ index * this.data.stride + this.offset + 3 ] = w;
-
- return this;
-
- }
-
- /**
- * Returns the x component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @return {number} The x component.
- */
- getX( index ) {
-
- let x = this.data.array[ index * this.data.stride + this.offset ];
-
- if ( this.normalized ) x = denormalize( x, this.array );
-
- return x;
-
- }
-
- /**
- * Returns the y component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @return {number} The y component.
- */
- getY( index ) {
-
- let y = this.data.array[ index * this.data.stride + this.offset + 1 ];
-
- if ( this.normalized ) y = denormalize( y, this.array );
-
- return y;
-
- }
-
- /**
- * Returns the z component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @return {number} The z component.
- */
- getZ( index ) {
-
- let z = this.data.array[ index * this.data.stride + this.offset + 2 ];
-
- if ( this.normalized ) z = denormalize( z, this.array );
-
- return z;
-
- }
-
- /**
- * Returns the w component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @return {number} The w component.
- */
- getW( index ) {
-
- let w = this.data.array[ index * this.data.stride + this.offset + 3 ];
-
- if ( this.normalized ) w = denormalize( w, this.array );
-
- return w;
-
- }
-
- /**
- * Sets the x and y component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} x - The value for the x component to set.
- * @param {number} y - The value for the y component to set.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- setXY( index, x, y ) {
-
- index = index * this.data.stride + this.offset;
-
- if ( this.normalized ) {
-
- x = normalize( x, this.array );
- y = normalize( y, this.array );
-
- }
-
- this.data.array[ index + 0 ] = x;
- this.data.array[ index + 1 ] = y;
-
- return this;
-
- }
-
- /**
- * Sets the x, y and z component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} x - The value for the x component to set.
- * @param {number} y - The value for the y component to set.
- * @param {number} z - The value for the z component to set.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- setXYZ( index, x, y, z ) {
-
- index = index * this.data.stride + this.offset;
-
- if ( this.normalized ) {
-
- x = normalize( x, this.array );
- y = normalize( y, this.array );
- z = normalize( z, this.array );
-
- }
-
- this.data.array[ index + 0 ] = x;
- this.data.array[ index + 1 ] = y;
- this.data.array[ index + 2 ] = z;
-
- return this;
-
- }
-
- /**
- * Sets the x, y, z and w component of the vector at the given index.
- *
- * @param {number} index - The index into the buffer attribute.
- * @param {number} x - The value for the x component to set.
- * @param {number} y - The value for the y component to set.
- * @param {number} z - The value for the z component to set.
- * @param {number} w - The value for the w component to set.
- * @return {InterleavedBufferAttribute} A reference to this instance.
- */
- setXYZW( index, x, y, z, w ) {
-
- index = index * this.data.stride + this.offset;
-
- if ( this.normalized ) {
-
- x = normalize( x, this.array );
- y = normalize( y, this.array );
- z = normalize( z, this.array );
- w = normalize( w, this.array );
-
- }
-
- this.data.array[ index + 0 ] = x;
- this.data.array[ index + 1 ] = y;
- this.data.array[ index + 2 ] = z;
- this.data.array[ index + 3 ] = w;
-
- return this;
-
- }
-
- /**
- * Returns a new buffer attribute with copied values from this instance.
- *
- * If no parameter is provided, cloning an interleaved buffer attribute will de-interleave buffer data.
- *
- * @param {Object} [data] - An object with interleaved buffers that allows to retain the interleaved property.
- * @return {BufferAttribute|InterleavedBufferAttribute} A clone of this instance.
- */
- clone( data ) {
-
- if ( data === undefined ) {
-
- log( 'InterleavedBufferAttribute.clone(): Cloning an interleaved buffer attribute will de-interleave buffer data.' );
-
- const array = [];
-
- for ( let i = 0; i < this.count; i ++ ) {
-
- const index = i * this.data.stride + this.offset;
-
- for ( let j = 0; j < this.itemSize; j ++ ) {
-
- array.push( this.data.array[ index + j ] );
-
- }
-
- }
-
- return new BufferAttribute( new this.array.constructor( array ), this.itemSize, this.normalized );
-
- } else {
-
- if ( data.interleavedBuffers === undefined ) {
-
- data.interleavedBuffers = {};
-
- }
-
- if ( data.interleavedBuffers[ this.data.uuid ] === undefined ) {
-
- data.interleavedBuffers[ this.data.uuid ] = this.data.clone( data );
-
- }
-
- return new InterleavedBufferAttribute( data.interleavedBuffers[ this.data.uuid ], this.itemSize, this.offset, this.normalized );
-
- }
-
- }
-
- /**
- * Serializes the buffer attribute into JSON.
- *
- * If no parameter is provided, cloning an interleaved buffer attribute will de-interleave buffer data.
- *
- * @param {Object} [data] - An optional value holding meta information about the serialization.
- * @return {Object} A JSON object representing the serialized buffer attribute.
- */
- toJSON( data ) {
-
- if ( data === undefined ) {
-
- log( 'InterleavedBufferAttribute.toJSON(): Serializing an interleaved buffer attribute will de-interleave buffer data.' );
-
- const array = [];
-
- for ( let i = 0; i < this.count; i ++ ) {
-
- const index = i * this.data.stride + this.offset;
-
- for ( let j = 0; j < this.itemSize; j ++ ) {
-
- array.push( this.data.array[ index + j ] );
-
- }
-
- }
-
- // de-interleave data and save it as an ordinary buffer attribute for now
-
- return {
- itemSize: this.itemSize,
- type: this.array.constructor.name,
- array: array,
- normalized: this.normalized
- };
-
- } else {
-
- // save as true interleaved attribute
-
- if ( data.interleavedBuffers === undefined ) {
-
- data.interleavedBuffers = {};
-
- }
-
- if ( data.interleavedBuffers[ this.data.uuid ] === undefined ) {
-
- data.interleavedBuffers[ this.data.uuid ] = this.data.toJSON( data );
-
- }
-
- return {
- isInterleavedBufferAttribute: true,
- itemSize: this.itemSize,
- data: this.data.uuid,
- offset: this.offset,
- normalized: this.normalized
- };
-
- }
-
- }
-
-}
-
-const _vector1 = /*@__PURE__*/ new Vector3();
-const _vector2 = /*@__PURE__*/ new Vector3();
-const _normalMatrix = /*@__PURE__*/ new Matrix3();
-
-/**
- * A two dimensional surface that extends infinitely in 3D space, represented
- * in [Hessian normal form](https://mathworld.wolfram.com/HessianNormalForm.html)
- * by a unit length normal vector and a constant.
- */
-class Plane {
-
- /**
- * Constructs a new plane.
- *
- * @param {Vector3} [normal=(1,0,0)] - A unit length vector defining the normal of the plane.
- * @param {number} [constant=0] - The signed distance from the origin to the plane.
- */
- constructor( normal = new Vector3( 1, 0, 0 ), constant = 0 ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isPlane = true;
-
- /**
- * A unit length vector defining the normal of the plane.
- *
- * @type {Vector3}
- */
- this.normal = normal;
-
- /**
- * The signed distance from the origin to the plane.
- *
- * @type {number}
- * @default 0
- */
- this.constant = constant;
-
- }
-
- /**
- * Sets the plane components by copying the given values.
- *
- * @param {Vector3} normal - The normal.
- * @param {number} constant - The constant.
- * @return {Plane} A reference to this plane.
- */
- set( normal, constant ) {
-
- this.normal.copy( normal );
- this.constant = constant;
-
- return this;
-
- }
-
- /**
- * Sets the plane components by defining `x`, `y`, `z` as the
- * plane normal and `w` as the constant.
- *
- * @param {number} x - The value for the normal's x component.
- * @param {number} y - The value for the normal's y component.
- * @param {number} z - The value for the normal's z component.
- * @param {number} w - The constant value.
- * @return {Plane} A reference to this plane.
- */
- setComponents( x, y, z, w ) {
-
- this.normal.set( x, y, z );
- this.constant = w;
-
- return this;
-
- }
-
- /**
- * Sets the plane from the given normal and coplanar point (that is a point
- * that lies onto the plane).
- *
- * @param {Vector3} normal - The normal.
- * @param {Vector3} point - A coplanar point.
- * @return {Plane} A reference to this plane.
- */
- setFromNormalAndCoplanarPoint( normal, point ) {
-
- this.normal.copy( normal );
- this.constant = - point.dot( this.normal );
-
- return this;
-
- }
-
- /**
- * Sets the plane from three coplanar points. The winding order is
- * assumed to be counter-clockwise, and determines the direction of
- * the plane normal.
- *
- * @param {Vector3} a - The first coplanar point.
- * @param {Vector3} b - The second coplanar point.
- * @param {Vector3} c - The third coplanar point.
- * @return {Plane} A reference to this plane.
- */
- setFromCoplanarPoints( a, b, c ) {
-
- const normal = _vector1.subVectors( c, b ).cross( _vector2.subVectors( a, b ) ).normalize();
-
- // Q: should an error be thrown if normal is zero (e.g. degenerate plane)?
-
- this.setFromNormalAndCoplanarPoint( normal, a );
-
- return this;
-
- }
-
- /**
- * Copies the values of the given plane to this instance.
- *
- * @param {Plane} plane - The plane to copy.
- * @return {Plane} A reference to this plane.
- */
- copy( plane ) {
-
- this.normal.copy( plane.normal );
- this.constant = plane.constant;
-
- return this;
-
- }
-
- /**
- * Normalizes the plane normal and adjusts the constant accordingly.
- *
- * @return {Plane} A reference to this plane.
- */
- normalize() {
-
- // Note: will lead to a divide by zero if the plane is invalid.
-
- const inverseNormalLength = 1.0 / this.normal.length();
- this.normal.multiplyScalar( inverseNormalLength );
- this.constant *= inverseNormalLength;
-
- return this;
-
- }
-
- /**
- * Negates both the plane normal and the constant.
- *
- * @return {Plane} A reference to this plane.
- */
- negate() {
-
- this.constant *= -1;
- this.normal.negate();
-
- return this;
-
- }
-
- /**
- * Returns the signed distance from the given point to this plane.
- *
- * @param {Vector3} point - The point to compute the distance for.
- * @return {number} The signed distance.
- */
- distanceToPoint( point ) {
-
- return this.normal.dot( point ) + this.constant;
-
- }
-
- /**
- * Returns the signed distance from the given sphere to this plane.
- *
- * @param {Sphere} sphere - The sphere to compute the distance for.
- * @return {number} The signed distance.
- */
- distanceToSphere( sphere ) {
-
- return this.distanceToPoint( sphere.center ) - sphere.radius;
-
- }
-
- /**
- * Projects a the given point onto the plane.
- *
- * @param {Vector3} point - The point to project.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The projected point on the plane.
- */
- projectPoint( point, target ) {
-
- return target.copy( point ).addScaledVector( this.normal, - this.distanceToPoint( point ) );
-
- }
-
- /**
- * Returns the intersection point of the passed line and the plane. Returns
- * `null` if the line does not intersect. Returns the line's starting point if
- * the line is coplanar with the plane.
- *
- * @param {Line3} line - The line to compute the intersection for.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @param {boolean} [clampToLine=true] - Whether to clamp the intersection to the line segment.
- * @return {?Vector3} The intersection point. Returns `null` if no intersection is detected.
- */
- intersectLine( line, target, clampToLine = true ) {
-
- const direction = line.delta( _vector1 );
-
- const denominator = this.normal.dot( direction );
-
- if ( denominator === 0 ) {
-
- // line is coplanar, return origin
- if ( this.distanceToPoint( line.start ) === 0 ) {
-
- return target.copy( line.start );
-
- }
-
- // Unsure if this is the correct method to handle this case.
- return null;
-
- }
-
- const t = - ( line.start.dot( this.normal ) + this.constant ) / denominator;
-
- if ( ( clampToLine === true ) && ( t < 0 || t > 1 ) ) {
-
- return null;
-
- }
-
- return target.copy( line.start ).addScaledVector( direction, t );
-
- }
-
- /**
- * Returns `true` if the given line segment intersects with (passes through) the plane.
- *
- * @param {Line3} line - The line to test.
- * @return {boolean} Whether the given line segment intersects with the plane or not.
- */
- intersectsLine( line ) {
-
- // Note: this tests if a line intersects the plane, not whether it (or its end-points) are coplanar with it.
-
- const startSign = this.distanceToPoint( line.start );
- const endSign = this.distanceToPoint( line.end );
-
- return ( startSign < 0 && endSign > 0 ) || ( endSign < 0 && startSign > 0 );
-
- }
-
- /**
- * Returns `true` if the given bounding box intersects with the plane.
- *
- * @param {Box3} box - The bounding box to test.
- * @return {boolean} Whether the given bounding box intersects with the plane or not.
- */
- intersectsBox( box ) {
-
- return box.intersectsPlane( this );
-
- }
-
- /**
- * Returns `true` if the given bounding sphere intersects with the plane.
- *
- * @param {Sphere} sphere - The bounding sphere to test.
- * @return {boolean} Whether the given bounding sphere intersects with the plane or not.
- */
- intersectsSphere( sphere ) {
-
- return sphere.intersectsPlane( this );
-
- }
-
- /**
- * Returns a coplanar vector to the plane, by calculating the
- * projection of the normal at the origin onto the plane.
- *
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The coplanar point.
- */
- coplanarPoint( target ) {
-
- return target.copy( this.normal ).multiplyScalar( - this.constant );
-
- }
-
- /**
- * Apply a 4x4 matrix to the plane. The matrix must be an affine, homogeneous transform.
- *
- * The optional normal matrix can be pre-computed like so:
- * ```js
- * const optionalNormalMatrix = new THREE.Matrix3().getNormalMatrix( matrix );
- * ```
- *
- * @param {Matrix4} matrix - The transformation matrix.
- * @param {Matrix4} [optionalNormalMatrix] - A pre-computed normal matrix.
- * @return {Plane} A reference to this plane.
- */
- applyMatrix4( matrix, optionalNormalMatrix ) {
-
- const normalMatrix = optionalNormalMatrix || _normalMatrix.getNormalMatrix( matrix );
-
- const referencePoint = this.coplanarPoint( _vector1 ).applyMatrix4( matrix );
-
- const normal = this.normal.applyMatrix3( normalMatrix ).normalize();
-
- this.constant = - referencePoint.dot( normal );
-
- return this;
-
- }
-
- /**
- * Translates the plane by the distance defined by the given offset vector.
- * Note that this only affects the plane constant and will not affect the normal vector.
- *
- * @param {Vector3} offset - The offset vector.
- * @return {Plane} A reference to this plane.
- */
- translate( offset ) {
-
- this.constant -= offset.dot( this.normal );
-
- return this;
-
- }
-
- /**
- * Returns `true` if this plane is equal with the given one.
- *
- * @param {Plane} plane - The plane to test for equality.
- * @return {boolean} Whether this plane is equal with the given one.
- */
- equals( plane ) {
-
- return plane.normal.equals( this.normal ) && ( plane.constant === this.constant );
-
- }
-
- /**
- * Returns a new plane with copied values from this instance.
- *
- * @return {Plane} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Returns a serialized structure of the plane.
- *
- * @return {Object} Serialized structure with fields representing the object state.
- */
- toJSON() {
-
- return {
- normal: this.normal.toArray(),
- constant: this.constant
- };
-
- }
-
- /**
- * Sets the plane properties from the given JSON.
- *
- * @param {Object} json - The serialized json to set the plane from.
- * @return {Plane} A reference to this plane.
- */
- fromJSON( json ) {
-
- this.normal.fromArray( json.normal );
- this.constant = json.constant;
-
- return this;
-
- }
-
-}
-
-let _materialId = 0;
-
-/**
- * Abstract base class for materials.
- *
- * Materials define the appearance of renderable 3D objects.
- *
- * @abstract
- * @augments EventDispatcher
- */
-class Material extends EventDispatcher {
-
- /**
- * Constructs a new material.
- */
- constructor() {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMaterial = true;
-
- /**
- * The ID of the material.
- *
- * @name Material#id
- * @type {number}
- * @readonly
- */
- Object.defineProperty( this, 'id', { value: _materialId ++ } );
-
- /**
- * The UUID of the material.
- *
- * @type {string}
- * @readonly
- */
- this.uuid = generateUUID();
-
- /**
- * The name of the material.
- *
- * @type {string}
- */
- this.name = '';
-
- /**
- * The type property is used for detecting the object type
- * in context of serialization/deserialization.
- *
- * @type {string}
- * @readonly
- */
- this.type = 'Material';
-
- /**
- * Defines the blending type of the material.
- *
- * It must be set to `CustomBlending` if custom blending properties like
- * {@link Material#blendSrc}, {@link Material#blendDst} or {@link Material#blendEquation}
- * should have any effect.
- *
- * @type {(NoBlending|NormalBlending|AdditiveBlending|SubtractiveBlending|MultiplyBlending|CustomBlending)}
- * @default NormalBlending
- */
- this.blending = NormalBlending;
-
- /**
- * Defines which side of faces will be rendered - front, back or both.
- *
- * @type {(FrontSide|BackSide|DoubleSide)}
- * @default FrontSide
- */
- this.side = FrontSide;
-
- /**
- * If set to `true`, vertex colors should be used.
- *
- * The engine supports RGB and RGBA vertex colors depending on whether a three (RGB) or
- * four (RGBA) component color buffer attribute is used.
- *
- * @type {boolean}
- * @default false
- */
- this.vertexColors = false;
-
- /**
- * Defines how transparent the material is.
- * A value of `0.0` indicates fully transparent, `1.0` is fully opaque.
- *
- * If the {@link Material#transparent} is not set to `true`,
- * the material will remain fully opaque and this value will only affect its color.
- *
- * @type {number}
- * @default 1
- */
- this.opacity = 1;
-
- /**
- * Defines whether this material is transparent. This has an effect on
- * rendering as transparent objects need special treatment and are rendered
- * after non-transparent objects.
- *
- * When set to true, the extent to which the material is transparent is
- * controlled by {@link Material#opacity}.
- *
- * @type {boolean}
- * @default false
- */
- this.transparent = false;
-
- /**
- * Enables alpha hashed transparency, an alternative to {@link Material#transparent} or
- * {@link Material#alphaTest}. The material will not be rendered if opacity is lower than
- * a random threshold. Randomization introduces some grain or noise, but approximates alpha
- * blending without the associated problems of sorting. Using TAA can reduce the resulting noise.
- *
- * @type {boolean}
- * @default false
- */
- this.alphaHash = false;
-
- /**
- * Defines the blending source factor.
- *
- * @type {(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
- * @default SrcAlphaFactor
- */
- this.blendSrc = SrcAlphaFactor;
-
- /**
- * Defines the blending destination factor.
- *
- * @type {(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
- * @default OneMinusSrcAlphaFactor
- */
- this.blendDst = OneMinusSrcAlphaFactor;
-
- /**
- * Defines the blending equation.
- *
- * @type {(AddEquation|SubtractEquation|ReverseSubtractEquation|MinEquation|MaxEquation)}
- * @default AddEquation
- */
- this.blendEquation = AddEquation;
-
- /**
- * Defines the blending source alpha factor.
- *
- * @type {?(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
- * @default null
- */
- this.blendSrcAlpha = null;
-
- /**
- * Defines the blending destination alpha factor.
- *
- * @type {?(ZeroFactor|OneFactor|SrcColorFactor|OneMinusSrcColorFactor|SrcAlphaFactor|OneMinusSrcAlphaFactor|DstAlphaFactor|OneMinusDstAlphaFactor|DstColorFactor|OneMinusDstColorFactor|SrcAlphaSaturateFactor|ConstantColorFactor|OneMinusConstantColorFactor|ConstantAlphaFactor|OneMinusConstantAlphaFactor)}
- * @default null
- */
- this.blendDstAlpha = null;
-
- /**
- * Defines the blending equation of the alpha channel.
- *
- * @type {?(AddEquation|SubtractEquation|ReverseSubtractEquation|MinEquation|MaxEquation)}
- * @default null
- */
- this.blendEquationAlpha = null;
-
- /**
- * Represents the RGB values of the constant blend color.
- *
- * This property has only an effect when using custom blending with `ConstantColor` or `OneMinusConstantColor`.
- *
- * @type {Color}
- * @default (0,0,0)
- */
- this.blendColor = new Color( 0, 0, 0 );
-
- /**
- * Represents the alpha value of the constant blend color.
- *
- * This property has only an effect when using custom blending with `ConstantAlpha` or `OneMinusConstantAlpha`.
- *
- * @type {number}
- * @default 0
- */
- this.blendAlpha = 0;
-
- /**
- * Defines the depth function.
- *
- * @type {(NeverDepth|AlwaysDepth|LessDepth|LessEqualDepth|EqualDepth|GreaterEqualDepth|GreaterDepth|NotEqualDepth)}
- * @default LessEqualDepth
- */
- this.depthFunc = LessEqualDepth;
-
- /**
- * Whether to have depth test enabled when rendering this material.
- * When the depth test is disabled, the depth write will also be implicitly disabled.
- *
- * @type {boolean}
- * @default true
- */
- this.depthTest = true;
-
- /**
- * Whether rendering this material has any effect on the depth buffer.
- *
- * When drawing 2D overlays it can be useful to disable the depth writing in
- * order to layer several things together without creating z-index artifacts.
- *
- * @type {boolean}
- * @default true
- */
- this.depthWrite = true;
-
- /**
- * The bit mask to use when writing to the stencil buffer.
- *
- * @type {number}
- * @default 0xff
- */
- this.stencilWriteMask = 0xff;
-
- /**
- * The stencil comparison function to use.
- *
- * @type {NeverStencilFunc|LessStencilFunc|EqualStencilFunc|LessEqualStencilFunc|GreaterStencilFunc|NotEqualStencilFunc|GreaterEqualStencilFunc|AlwaysStencilFunc}
- * @default AlwaysStencilFunc
- */
- this.stencilFunc = AlwaysStencilFunc;
-
- /**
- * The value to use when performing stencil comparisons or stencil operations.
- *
- * @type {number}
- * @default 0
- */
- this.stencilRef = 0;
-
- /**
- * The bit mask to use when comparing against the stencil buffer.
- *
- * @type {number}
- * @default 0xff
- */
- this.stencilFuncMask = 0xff;
-
- /**
- * Which stencil operation to perform when the comparison function returns `false`.
- *
- * @type {ZeroStencilOp|KeepStencilOp|ReplaceStencilOp|IncrementStencilOp|DecrementStencilOp|IncrementWrapStencilOp|DecrementWrapStencilOp|InvertStencilOp}
- * @default KeepStencilOp
- */
- this.stencilFail = KeepStencilOp;
-
- /**
- * Which stencil operation to perform when the comparison function returns
- * `true` but the depth test fails.
- *
- * @type {ZeroStencilOp|KeepStencilOp|ReplaceStencilOp|IncrementStencilOp|DecrementStencilOp|IncrementWrapStencilOp|DecrementWrapStencilOp|InvertStencilOp}
- * @default KeepStencilOp
- */
- this.stencilZFail = KeepStencilOp;
-
- /**
- * Which stencil operation to perform when the comparison function returns
- * `true` and the depth test passes.
- *
- * @type {ZeroStencilOp|KeepStencilOp|ReplaceStencilOp|IncrementStencilOp|DecrementStencilOp|IncrementWrapStencilOp|DecrementWrapStencilOp|InvertStencilOp}
- * @default KeepStencilOp
- */
- this.stencilZPass = KeepStencilOp;
-
- /**
- * Whether stencil operations are performed against the stencil buffer. In
- * order to perform writes or comparisons against the stencil buffer this
- * value must be `true`.
- *
- * @type {boolean}
- * @default false
- */
- this.stencilWrite = false;
-
- /**
- * User-defined clipping planes specified as THREE.Plane objects in world
- * space. These planes apply to the objects this material is attached to.
- * Points in space whose signed distance to the plane is negative are clipped
- * (not rendered). This requires {@link WebGLRenderer#localClippingEnabled} to
- * be `true`.
- *
- * @type {?Array}
- * @default null
- */
- this.clippingPlanes = null;
-
- /**
- * Changes the behavior of clipping planes so that only their intersection is
- * clipped, rather than their union.
- *
- * @type {boolean}
- * @default false
- */
- this.clipIntersection = false;
-
- /**
- * Defines whether to clip shadows according to the clipping planes specified
- * on this material.
- *
- * @type {boolean}
- * @default false
- */
- this.clipShadows = false;
-
- /**
- * Defines which side of faces cast shadows. If `null`, the side casting shadows
- * is determined as follows:
- *
- * - When {@link Material#side} is set to `FrontSide`, the back side cast shadows.
- * - When {@link Material#side} is set to `BackSide`, the front side cast shadows.
- * - When {@link Material#side} is set to `DoubleSide`, both sides cast shadows.
- *
- * @type {?(FrontSide|BackSide|DoubleSide)}
- * @default null
- */
- this.shadowSide = null;
-
- /**
- * Whether to render the material's color.
- *
- * This can be used in conjunction with {@link Object3D#renderOder} to create invisible
- * objects that occlude other objects.
- *
- * @type {boolean}
- * @default true
- */
- this.colorWrite = true;
-
- /**
- * Override the renderer's default precision for this material.
- *
- * @type {?('highp'|'mediump'|'lowp')}
- * @default null
- */
- this.precision = null;
-
- /**
- * Whether to use polygon offset or not. When enabled, each fragment's depth value will
- * be offset after it is interpolated from the depth values of the appropriate vertices.
- * The offset is added before the depth test is performed and before the value is written
- * into the depth buffer.
- *
- * Can be useful for rendering hidden-line images, for applying decals to surfaces, and for
- * rendering solids with highlighted edges.
- *
- * @type {boolean}
- * @default false
- */
- this.polygonOffset = false;
-
- /**
- * Specifies a scale factor that is used to create a variable depth offset for each polygon.
- *
- * @type {number}
- * @default 0
- */
- this.polygonOffsetFactor = 0;
-
- /**
- * Is multiplied by an implementation-specific value to create a constant depth offset.
- *
- * @type {number}
- * @default 0
- */
- this.polygonOffsetUnits = 0;
-
- /**
- * Whether to apply dithering to the color to remove the appearance of banding.
- *
- * @type {boolean}
- * @default false
- */
- this.dithering = false;
-
- /**
- * Whether alpha to coverage should be enabled or not. Can only be used with MSAA-enabled contexts
- * (meaning when the renderer was created with *antialias* parameter set to `true`). Enabling this
- * will smooth aliasing on clip plane edges and alphaTest-clipped edges.
- *
- * @type {boolean}
- * @default false
- */
- this.alphaToCoverage = false;
-
- /**
- * Whether to premultiply the alpha (transparency) value.
- *
- * @type {boolean}
- * @default false
- */
- this.premultipliedAlpha = false;
-
- /**
- * Whether double-sided, transparent objects should be rendered with a single pass or not.
- *
- * The engine renders double-sided, transparent objects with two draw calls (back faces first,
- * then front faces) to mitigate transparency artifacts. There are scenarios however where this
- * approach produces no quality gains but still doubles draw calls e.g. when rendering flat
- * vegetation like grass sprites. In these cases, set the `forceSinglePass` flag to `true` to
- * disable the two pass rendering to avoid performance issues.
- *
- * @type {boolean}
- * @default false
- */
- this.forceSinglePass = false;
-
- /**
- * Whether it's possible to override the material with {@link Scene#overrideMaterial} or not.
- *
- * @type {boolean}
- * @default true
- */
- this.allowOverride = true;
-
- /**
- * Defines whether 3D objects using this material are visible.
- *
- * @type {boolean}
- * @default true
- */
- this.visible = true;
-
- /**
- * Defines whether this material is tone mapped according to the renderer's tone mapping setting.
- *
- * It is ignored when rendering to a render target or using post processing or when using
- * `WebGPURenderer`. In all these cases, all materials are honored by tone mapping.
- *
- * @type {boolean}
- * @default true
- */
- this.toneMapped = true;
-
- /**
- * An object that can be used to store custom data about the Material. It
- * should not hold references to functions as these will not be cloned.
- *
- * @type {Object}
- */
- this.userData = {};
-
- /**
- * This starts at `0` and counts how many times {@link Material#needsUpdate} is set to `true`.
- *
- * @type {number}
- * @readonly
- * @default 0
- */
- this.version = 0;
-
- this._alphaTest = 0;
-
- }
-
- /**
- * Sets the alpha value to be used when running an alpha test. The material
- * will not be rendered if the opacity is lower than this value.
- *
- * @type {number}
- * @readonly
- * @default 0
- */
- get alphaTest() {
-
- return this._alphaTest;
-
- }
-
- set alphaTest( value ) {
-
- if ( this._alphaTest > 0 !== value > 0 ) {
-
- this.version ++;
-
- }
-
- this._alphaTest = value;
-
- }
-
- /**
- * An optional callback that is executed immediately before the material is used to render a 3D object.
- *
- * This method can only be used when rendering with {@link WebGLRenderer}.
- *
- * @param {WebGLRenderer} renderer - The renderer.
- * @param {Scene} scene - The scene.
- * @param {Camera} camera - The camera that is used to render the scene.
- * @param {BufferGeometry} geometry - The 3D object's geometry.
- * @param {Object3D} object - The 3D object.
- * @param {Object} group - The geometry group data.
- */
- onBeforeRender( /* renderer, scene, camera, geometry, object, group */ ) {}
-
- /**
- * An optional callback that is executed immediately before the shader
- * program is compiled. This function is called with the shader source code
- * as a parameter. Useful for the modification of built-in materials.
- *
- * This method can only be used when rendering with {@link WebGLRenderer}. The
- * recommended approach when customizing materials is to use `WebGPURenderer` with the new
- * Node Material system and [TSL](https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language).
- *
- * @param {{vertexShader:string,fragmentShader:string,uniforms:Object}} shaderobject - The object holds the uniforms and the vertex and fragment shader source.
- * @param {WebGLRenderer} renderer - A reference to the renderer.
- */
- onBeforeCompile( /* shaderobject, renderer */ ) {}
-
- /**
- * In case {@link Material#onBeforeCompile} is used, this callback can be used to identify
- * values of settings used in `onBeforeCompile()`, so three.js can reuse a cached
- * shader or recompile the shader for this material as needed.
- *
- * This method can only be used when rendering with {@link WebGLRenderer}.
- *
- * @return {string} The custom program cache key.
- */
- customProgramCacheKey() {
-
- return this.onBeforeCompile.toString();
-
- }
-
- /**
- * This method can be used to set default values from parameter objects.
- * It is a generic implementation so it can be used with different types
- * of materials.
- *
- * @param {Object} [values] - The material values to set.
- */
- setValues( values ) {
-
- if ( values === undefined ) return;
-
- for ( const key in values ) {
-
- const newValue = values[ key ];
-
- if ( newValue === undefined ) {
-
- warn( `Material: parameter '${ key }' has value of undefined.` );
- continue;
-
- }
-
- const currentValue = this[ key ];
-
- if ( currentValue === undefined ) {
-
- warn( `Material: '${ key }' is not a property of THREE.${ this.type }.` );
- continue;
-
- }
-
- if ( currentValue && currentValue.isColor ) {
-
- currentValue.set( newValue );
-
- } else if (
- ( ( currentValue && currentValue.isVector2 ) && ( newValue && newValue.isVector2 ) ) ||
- ( ( currentValue && currentValue.isEuler ) && ( newValue && newValue.isEuler ) ) ||
- ( ( currentValue && currentValue.isVector3 ) && ( newValue && newValue.isVector3 ) )
- ) {
-
- currentValue.copy( newValue );
-
- } else {
-
- this[ key ] = newValue;
-
- }
-
- }
-
- }
-
- /**
- * Serializes the material into JSON.
- *
- * @param {?(Object|string)} meta - An optional value holding meta information about the serialization.
- * @return {Object} A JSON object representing the serialized material.
- * @see {@link ObjectLoader#parse}
- */
- toJSON( meta ) {
-
- const isRootObject = ( meta === undefined || typeof meta === 'string' );
-
- if ( isRootObject ) {
-
- meta = {
- textures: {},
- images: {}
- };
-
- }
-
- const data = {
- metadata: {
- version: 4.7,
- type: 'Material',
- generator: 'Material.toJSON'
- }
- };
-
- // standard Material serialization
-
- data.uuid = this.uuid;
- data.type = this.type;
-
- data.blending = this.blending;
- data.side = this.side;
- data.shadowSide = this.shadowSide;
- data.vertexColors = this.vertexColors;
-
- data.opacity = this.opacity;
- data.transparent = this.transparent;
-
- data.blendSrc = this.blendSrc;
- data.blendDst = this.blendDst;
- data.blendEquation = this.blendEquation;
- data.blendSrcAlpha = this.blendSrcAlpha;
- data.blendDstAlpha = this.blendDstAlpha;
- data.blendEquationAlpha = this.blendEquationAlpha;
- data.blendColor = this.blendColor.getHex();
- data.blendAlpha = this.blendAlpha;
-
- data.depthFunc = this.depthFunc;
- data.depthTest = this.depthTest;
- data.depthWrite = this.depthWrite;
- data.colorWrite = this.colorWrite;
-
- data.clipIntersection = this.clipIntersection;
- data.clipShadows = this.clipShadows;
-
- data.stencilWriteMask = this.stencilWriteMask;
- data.stencilFunc = this.stencilFunc;
- data.stencilRef = this.stencilRef;
- data.stencilFuncMask = this.stencilFuncMask;
- data.stencilFail = this.stencilFail;
- data.stencilZFail = this.stencilZFail;
- data.stencilZPass = this.stencilZPass;
- data.stencilWrite = this.stencilWrite;
-
- data.polygonOffset = this.polygonOffset;
- data.polygonOffsetFactor = this.polygonOffsetFactor;
- data.polygonOffsetUnits = this.polygonOffsetUnits;
-
- data.dithering = this.dithering;
-
- data.alphaTest = this.alphaTest;
- data.alphaHash = this.alphaHash;
- data.alphaToCoverage = this.alphaToCoverage;
- data.premultipliedAlpha = this.premultipliedAlpha;
- data.forceSinglePass = this.forceSinglePass;
- data.allowOverride = this.allowOverride;
-
- data.visible = this.visible;
- data.toneMapped = this.toneMapped;
-
- data.name = this.name;
-
- if ( this.color && this.color.isColor ) data.color = this.color.getHex();
-
- if ( this.roughness !== undefined ) data.roughness = this.roughness;
- if ( this.metalness !== undefined ) data.metalness = this.metalness;
-
- if ( this.sheen !== undefined ) data.sheen = this.sheen;
- if ( this.sheenColor && this.sheenColor.isColor ) data.sheenColor = this.sheenColor.getHex();
- if ( this.sheenRoughness !== undefined ) data.sheenRoughness = this.sheenRoughness;
- if ( this.emissive && this.emissive.isColor ) data.emissive = this.emissive.getHex();
- if ( this.emissiveIntensity !== undefined ) data.emissiveIntensity = this.emissiveIntensity;
-
- if ( this.specular && this.specular.isColor ) data.specular = this.specular.getHex();
- if ( this.specularIntensity !== undefined ) data.specularIntensity = this.specularIntensity;
- if ( this.specularColor && this.specularColor.isColor ) data.specularColor = this.specularColor.getHex();
- if ( this.shininess !== undefined ) data.shininess = this.shininess;
- if ( this.clearcoat !== undefined ) data.clearcoat = this.clearcoat;
- if ( this.clearcoatRoughness !== undefined ) data.clearcoatRoughness = this.clearcoatRoughness;
-
- if ( this.clearcoatMap && this.clearcoatMap.isTexture ) {
-
- data.clearcoatMap = this.clearcoatMap.toJSON( meta ).uuid;
-
- }
-
- if ( this.clearcoatRoughnessMap && this.clearcoatRoughnessMap.isTexture ) {
-
- data.clearcoatRoughnessMap = this.clearcoatRoughnessMap.toJSON( meta ).uuid;
-
- }
-
- if ( this.clearcoatNormalMap && this.clearcoatNormalMap.isTexture ) {
-
- data.clearcoatNormalMap = this.clearcoatNormalMap.toJSON( meta ).uuid;
- data.clearcoatNormalScale = this.clearcoatNormalScale.toArray();
-
- }
-
- if ( this.sheenColorMap && this.sheenColorMap.isTexture ) {
-
- data.sheenColorMap = this.sheenColorMap.toJSON( meta ).uuid;
-
- }
-
- if ( this.sheenRoughnessMap && this.sheenRoughnessMap.isTexture ) {
-
- data.sheenRoughnessMap = this.sheenRoughnessMap.toJSON( meta ).uuid;
-
- }
-
- if ( this.dispersion !== undefined ) data.dispersion = this.dispersion;
- if ( this.retroreflectivity !== undefined ) data.retroreflectivity = this.retroreflectivity;
-
- if ( this.iridescence !== undefined ) data.iridescence = this.iridescence;
- if ( this.iridescenceIOR !== undefined ) data.iridescenceIOR = this.iridescenceIOR;
- if ( this.iridescenceThicknessRange !== undefined ) data.iridescenceThicknessRange = this.iridescenceThicknessRange;
-
- if ( this.iridescenceMap && this.iridescenceMap.isTexture ) {
-
- data.iridescenceMap = this.iridescenceMap.toJSON( meta ).uuid;
-
- }
-
- if ( this.iridescenceThicknessMap && this.iridescenceThicknessMap.isTexture ) {
-
- data.iridescenceThicknessMap = this.iridescenceThicknessMap.toJSON( meta ).uuid;
-
- }
-
- if ( this.anisotropy !== undefined ) data.anisotropy = this.anisotropy;
- if ( this.anisotropyRotation !== undefined ) data.anisotropyRotation = this.anisotropyRotation;
-
- if ( this.anisotropyMap && this.anisotropyMap.isTexture ) {
-
- data.anisotropyMap = this.anisotropyMap.toJSON( meta ).uuid;
-
- }
-
- if ( this.map && this.map.isTexture ) data.map = this.map.toJSON( meta ).uuid;
- if ( this.matcap && this.matcap.isTexture ) data.matcap = this.matcap.toJSON( meta ).uuid;
- if ( this.alphaMap && this.alphaMap.isTexture ) data.alphaMap = this.alphaMap.toJSON( meta ).uuid;
-
- if ( this.lightMap && this.lightMap.isTexture ) {
-
- data.lightMap = this.lightMap.toJSON( meta ).uuid;
- data.lightMapIntensity = this.lightMapIntensity;
-
- }
-
- if ( this.aoMap && this.aoMap.isTexture ) {
-
- data.aoMap = this.aoMap.toJSON( meta ).uuid;
- data.aoMapIntensity = this.aoMapIntensity;
-
- }
-
- if ( this.bumpMap && this.bumpMap.isTexture ) {
-
- data.bumpMap = this.bumpMap.toJSON( meta ).uuid;
- data.bumpScale = this.bumpScale;
-
- }
-
- if ( this.normalMap && this.normalMap.isTexture ) {
-
- data.normalMap = this.normalMap.toJSON( meta ).uuid;
- data.normalMapType = this.normalMapType;
- data.normalScale = this.normalScale.toArray();
-
- }
-
- if ( this.displacementMap && this.displacementMap.isTexture ) {
-
- data.displacementMap = this.displacementMap.toJSON( meta ).uuid;
- data.displacementScale = this.displacementScale;
- data.displacementBias = this.displacementBias;
-
- }
-
- if ( this.roughnessMap && this.roughnessMap.isTexture ) data.roughnessMap = this.roughnessMap.toJSON( meta ).uuid;
- if ( this.metalnessMap && this.metalnessMap.isTexture ) data.metalnessMap = this.metalnessMap.toJSON( meta ).uuid;
-
- if ( this.emissiveMap && this.emissiveMap.isTexture ) data.emissiveMap = this.emissiveMap.toJSON( meta ).uuid;
- if ( this.specularMap && this.specularMap.isTexture ) data.specularMap = this.specularMap.toJSON( meta ).uuid;
- if ( this.specularIntensityMap && this.specularIntensityMap.isTexture ) data.specularIntensityMap = this.specularIntensityMap.toJSON( meta ).uuid;
- if ( this.specularColorMap && this.specularColorMap.isTexture ) data.specularColorMap = this.specularColorMap.toJSON( meta ).uuid;
-
- if ( this.envMap && this.envMap.isTexture ) {
-
- data.envMap = this.envMap.toJSON( meta ).uuid;
-
- if ( this.combine !== undefined ) data.combine = this.combine;
-
- }
-
- if ( this.envMapRotation !== undefined ) data.envMapRotation = this.envMapRotation.toArray();
- if ( this.envMapIntensity !== undefined ) data.envMapIntensity = this.envMapIntensity;
- if ( this.reflectivity !== undefined ) data.reflectivity = this.reflectivity;
- if ( this.refractionRatio !== undefined ) data.refractionRatio = this.refractionRatio;
-
- if ( this.gradientMap && this.gradientMap.isTexture ) {
-
- data.gradientMap = this.gradientMap.toJSON( meta ).uuid;
-
- }
-
- if ( this.transmission !== undefined ) data.transmission = this.transmission;
- if ( this.transmissionMap && this.transmissionMap.isTexture ) data.transmissionMap = this.transmissionMap.toJSON( meta ).uuid;
- if ( this.thickness !== undefined ) data.thickness = this.thickness;
- if ( this.thicknessMap && this.thicknessMap.isTexture ) data.thicknessMap = this.thicknessMap.toJSON( meta ).uuid;
- if ( this.attenuationDistance !== undefined ) data.attenuationDistance = this.attenuationDistance;
- if ( this.attenuationColor !== undefined ) data.attenuationColor = this.attenuationColor.getHex();
-
- if ( this.size !== undefined ) data.size = this.size;
- if ( this.sizeAttenuation !== undefined ) data.sizeAttenuation = this.sizeAttenuation;
-
- if ( Array.isArray( this.clippingPlanes ) && this.clippingPlanes.length > 0 ) {
-
- data.clippingPlanes = this.clippingPlanes.map( plane => plane.toJSON() );
-
- }
-
- // rotation (SpriteMaterial)
- if ( this.rotation !== undefined ) data.rotation = this.rotation;
-
- // depthPacking (MeshDepthMaterial)
- if ( this.depthPacking !== undefined ) data.depthPacking = this.depthPacking;
-
- if ( this.linewidth !== undefined ) data.linewidth = this.linewidth;
- if ( this.linecap !== undefined ) data.linecap = this.linecap;
- if ( this.linejoin !== undefined ) data.linejoin = this.linejoin;
- if ( this.dashSize !== undefined ) data.dashSize = this.dashSize;
- if ( this.gapSize !== undefined ) data.gapSize = this.gapSize;
- if ( this.scale !== undefined ) data.scale = this.scale;
-
- if ( this.wireframe !== undefined ) data.wireframe = this.wireframe;
- if ( this.wireframeLinewidth !== undefined ) data.wireframeLinewidth = this.wireframeLinewidth;
- if ( this.wireframeLinecap !== undefined ) data.wireframeLinecap = this.wireframeLinecap;
- if ( this.wireframeLinejoin !== undefined ) data.wireframeLinejoin = this.wireframeLinejoin;
-
- if ( this.flatShading !== undefined ) data.flatShading = this.flatShading;
-
- if ( this.fog !== undefined ) data.fog = this.fog;
-
- if ( Object.keys( this.userData ).length > 0 ) data.userData = this.userData;
-
- // TODO: Copied from Object3D.toJSON
-
- function extractFromCache( cache ) {
-
- const values = [];
-
- for ( const key in cache ) {
-
- const data = cache[ key ];
- delete data.metadata;
- values.push( data );
-
- }
-
- return values;
-
- }
-
- if ( isRootObject ) {
-
- const textures = extractFromCache( meta.textures );
- const images = extractFromCache( meta.images );
-
- if ( textures.length > 0 ) data.textures = textures;
- if ( images.length > 0 ) data.images = images;
-
- }
-
- return data;
-
- }
-
- /**
- * Deserializes the material from the given JSON.
- *
- * @param {Object} json - The JSON holding the serialized material.
- * @param {Object} textures - A dictionary holding textures referenced by the material.
- * @return {Material} A reference to this material.
- */
- fromJSON( json, textures ) {
-
- if ( json.uuid !== undefined ) this.uuid = json.uuid;
- if ( json.name !== undefined ) this.name = json.name;
- if ( json.color !== undefined && this.color !== undefined ) this.color.setHex( json.color );
- if ( json.roughness !== undefined ) this.roughness = json.roughness;
- if ( json.metalness !== undefined ) this.metalness = json.metalness;
- if ( json.sheen !== undefined ) this.sheen = json.sheen;
- if ( json.sheenColor !== undefined ) this.sheenColor = new Color().setHex( json.sheenColor );
- if ( json.sheenRoughness !== undefined ) this.sheenRoughness = json.sheenRoughness;
- if ( json.emissive !== undefined && this.emissive !== undefined ) this.emissive.setHex( json.emissive );
- if ( json.specular !== undefined && this.specular !== undefined ) this.specular.setHex( json.specular );
- if ( json.specularIntensity !== undefined ) this.specularIntensity = json.specularIntensity;
- if ( json.specularColor !== undefined && this.specularColor !== undefined ) this.specularColor.setHex( json.specularColor );
- if ( json.shininess !== undefined ) this.shininess = json.shininess;
- if ( json.clearcoat !== undefined ) this.clearcoat = json.clearcoat;
- if ( json.clearcoatRoughness !== undefined ) this.clearcoatRoughness = json.clearcoatRoughness;
- if ( json.dispersion !== undefined ) this.dispersion = json.dispersion;
- if ( json.retroreflectivity !== undefined ) this.retroreflectivity = json.retroreflectivity;
- if ( json.iridescence !== undefined ) this.iridescence = json.iridescence;
- if ( json.iridescenceIOR !== undefined ) this.iridescenceIOR = json.iridescenceIOR;
- if ( json.iridescenceThicknessRange !== undefined ) this.iridescenceThicknessRange = json.iridescenceThicknessRange;
- if ( json.transmission !== undefined ) this.transmission = json.transmission;
- if ( json.thickness !== undefined ) this.thickness = json.thickness;
- if ( json.attenuationDistance !== undefined ) this.attenuationDistance = json.attenuationDistance;
- if ( json.attenuationColor !== undefined && this.attenuationColor !== undefined ) this.attenuationColor.setHex( json.attenuationColor );
- if ( json.anisotropy !== undefined ) this.anisotropy = json.anisotropy;
- if ( json.anisotropyRotation !== undefined ) this.anisotropyRotation = json.anisotropyRotation;
- if ( json.fog !== undefined ) this.fog = json.fog;
- if ( json.flatShading !== undefined ) this.flatShading = json.flatShading;
- if ( json.blending !== undefined ) this.blending = json.blending;
- if ( json.combine !== undefined ) this.combine = json.combine;
- if ( json.side !== undefined ) this.side = json.side;
- if ( json.shadowSide !== undefined ) this.shadowSide = json.shadowSide;
- if ( json.opacity !== undefined ) this.opacity = json.opacity;
- if ( json.transparent !== undefined ) this.transparent = json.transparent;
- if ( json.alphaTest !== undefined ) this.alphaTest = json.alphaTest;
- if ( json.alphaHash !== undefined ) this.alphaHash = json.alphaHash;
- if ( json.depthFunc !== undefined ) this.depthFunc = json.depthFunc;
- if ( json.depthTest !== undefined ) this.depthTest = json.depthTest;
- if ( json.depthWrite !== undefined ) this.depthWrite = json.depthWrite;
- if ( json.colorWrite !== undefined ) this.colorWrite = json.colorWrite;
- if ( json.clippingPlanes !== undefined ) this.clippingPlanes = json.clippingPlanes.map( plane => new Plane().fromJSON( plane ) );
- if ( json.clipIntersection !== undefined ) this.clipIntersection = json.clipIntersection;
- if ( json.clipShadows !== undefined ) this.clipShadows = json.clipShadows;
- if ( json.depthPacking !== undefined ) this.depthPacking = json.depthPacking;
- if ( json.blendSrc !== undefined ) this.blendSrc = json.blendSrc;
- if ( json.blendDst !== undefined ) this.blendDst = json.blendDst;
- if ( json.blendEquation !== undefined ) this.blendEquation = json.blendEquation;
- if ( json.blendSrcAlpha !== undefined ) this.blendSrcAlpha = json.blendSrcAlpha;
- if ( json.blendDstAlpha !== undefined ) this.blendDstAlpha = json.blendDstAlpha;
- if ( json.blendEquationAlpha !== undefined ) this.blendEquationAlpha = json.blendEquationAlpha;
- if ( json.blendColor !== undefined && this.blendColor !== undefined ) this.blendColor.setHex( json.blendColor );
- if ( json.blendAlpha !== undefined ) this.blendAlpha = json.blendAlpha;
- if ( json.stencilWriteMask !== undefined ) this.stencilWriteMask = json.stencilWriteMask;
- if ( json.stencilFunc !== undefined ) this.stencilFunc = json.stencilFunc;
- if ( json.stencilRef !== undefined ) this.stencilRef = json.stencilRef;
- if ( json.stencilFuncMask !== undefined ) this.stencilFuncMask = json.stencilFuncMask;
- if ( json.stencilFail !== undefined ) this.stencilFail = json.stencilFail;
- if ( json.stencilZFail !== undefined ) this.stencilZFail = json.stencilZFail;
- if ( json.stencilZPass !== undefined ) this.stencilZPass = json.stencilZPass;
- if ( json.stencilWrite !== undefined ) this.stencilWrite = json.stencilWrite;
-
- if ( json.wireframe !== undefined ) this.wireframe = json.wireframe;
- if ( json.wireframeLinewidth !== undefined ) this.wireframeLinewidth = json.wireframeLinewidth;
- if ( json.wireframeLinecap !== undefined ) this.wireframeLinecap = json.wireframeLinecap;
- if ( json.wireframeLinejoin !== undefined ) this.wireframeLinejoin = json.wireframeLinejoin;
-
- if ( json.rotation !== undefined ) this.rotation = json.rotation;
-
- if ( json.linewidth !== undefined ) this.linewidth = json.linewidth;
- if ( json.linecap !== undefined ) this.linecap = json.linecap;
- if ( json.linejoin !== undefined ) this.linejoin = json.linejoin;
- if ( json.dashSize !== undefined ) this.dashSize = json.dashSize;
- if ( json.gapSize !== undefined ) this.gapSize = json.gapSize;
- if ( json.scale !== undefined ) this.scale = json.scale;
-
- if ( json.polygonOffset !== undefined ) this.polygonOffset = json.polygonOffset;
- if ( json.polygonOffsetFactor !== undefined ) this.polygonOffsetFactor = json.polygonOffsetFactor;
- if ( json.polygonOffsetUnits !== undefined ) this.polygonOffsetUnits = json.polygonOffsetUnits;
-
- if ( json.dithering !== undefined ) this.dithering = json.dithering;
-
- if ( json.alphaToCoverage !== undefined ) this.alphaToCoverage = json.alphaToCoverage;
- if ( json.premultipliedAlpha !== undefined ) this.premultipliedAlpha = json.premultipliedAlpha;
- if ( json.forceSinglePass !== undefined ) this.forceSinglePass = json.forceSinglePass;
- if ( json.allowOverride !== undefined ) this.allowOverride = json.allowOverride;
-
- if ( json.visible !== undefined ) this.visible = json.visible;
-
- if ( json.toneMapped !== undefined ) this.toneMapped = json.toneMapped;
-
- if ( json.userData !== undefined ) this.userData = json.userData;
-
- if ( json.vertexColors !== undefined ) {
-
- if ( typeof json.vertexColors === 'number' ) {
-
- this.vertexColors = json.vertexColors > 0;
-
- } else {
-
- this.vertexColors = json.vertexColors;
-
- }
-
- }
-
- // for PointsMaterial
-
- if ( json.size !== undefined ) this.size = json.size;
- if ( json.sizeAttenuation !== undefined ) this.sizeAttenuation = json.sizeAttenuation;
-
- // maps
-
- if ( json.map !== undefined ) this.map = textures[ json.map ] || null;
- if ( json.matcap !== undefined ) this.matcap = textures[ json.matcap ] || null;
-
- if ( json.alphaMap !== undefined ) this.alphaMap = textures[ json.alphaMap ] || null;
-
- if ( json.bumpMap !== undefined ) this.bumpMap = textures[ json.bumpMap ] || null;
- if ( json.bumpScale !== undefined ) this.bumpScale = json.bumpScale;
-
- if ( json.normalMap !== undefined ) this.normalMap = textures[ json.normalMap ] || null;
- if ( json.normalMapType !== undefined ) this.normalMapType = json.normalMapType;
- if ( json.normalScale !== undefined ) {
-
- let normalScale = json.normalScale;
-
- if ( Array.isArray( normalScale ) === false ) {
-
- // Blender exporter used to export a scalar. See #7459
-
- normalScale = [ normalScale, normalScale ];
-
- }
-
- this.normalScale = new Vector2().fromArray( normalScale );
-
- }
-
- if ( json.displacementMap !== undefined ) this.displacementMap = textures[ json.displacementMap ] || null;
- if ( json.displacementScale !== undefined ) this.displacementScale = json.displacementScale;
- if ( json.displacementBias !== undefined ) this.displacementBias = json.displacementBias;
-
- if ( json.roughnessMap !== undefined ) this.roughnessMap = textures[ json.roughnessMap ] || null;
- if ( json.metalnessMap !== undefined ) this.metalnessMap = textures[ json.metalnessMap ] || null;
-
- if ( json.emissiveMap !== undefined ) this.emissiveMap = textures[ json.emissiveMap ] || null;
- if ( json.emissiveIntensity !== undefined ) this.emissiveIntensity = json.emissiveIntensity;
-
- if ( json.specularMap !== undefined ) this.specularMap = textures[ json.specularMap ] || null;
- if ( json.specularIntensityMap !== undefined ) this.specularIntensityMap = textures[ json.specularIntensityMap ] || null;
- if ( json.specularColorMap !== undefined ) this.specularColorMap = textures[ json.specularColorMap ] || null;
-
- if ( json.envMap !== undefined ) this.envMap = textures[ json.envMap ] || null;
- if ( json.envMapRotation !== undefined ) this.envMapRotation.fromArray( json.envMapRotation );
- if ( json.envMapIntensity !== undefined ) this.envMapIntensity = json.envMapIntensity;
-
- if ( json.reflectivity !== undefined ) this.reflectivity = json.reflectivity;
- if ( json.refractionRatio !== undefined ) this.refractionRatio = json.refractionRatio;
-
- if ( json.lightMap !== undefined ) this.lightMap = textures[ json.lightMap ] || null;
- if ( json.lightMapIntensity !== undefined ) this.lightMapIntensity = json.lightMapIntensity;
-
- if ( json.aoMap !== undefined ) this.aoMap = textures[ json.aoMap ] || null;
- if ( json.aoMapIntensity !== undefined ) this.aoMapIntensity = json.aoMapIntensity;
-
- if ( json.gradientMap !== undefined ) this.gradientMap = textures[ json.gradientMap ] || null;
-
- if ( json.clearcoatMap !== undefined ) this.clearcoatMap = textures[ json.clearcoatMap ] || null;
- if ( json.clearcoatRoughnessMap !== undefined ) this.clearcoatRoughnessMap = textures[ json.clearcoatRoughnessMap ] || null;
- if ( json.clearcoatNormalMap !== undefined ) this.clearcoatNormalMap = textures[ json.clearcoatNormalMap ] || null;
- if ( json.clearcoatNormalScale !== undefined ) this.clearcoatNormalScale = new Vector2().fromArray( json.clearcoatNormalScale );
-
- if ( json.iridescenceMap !== undefined ) this.iridescenceMap = textures[ json.iridescenceMap ] || null;
- if ( json.iridescenceThicknessMap !== undefined ) this.iridescenceThicknessMap = textures[ json.iridescenceThicknessMap ] || null;
-
- if ( json.transmissionMap !== undefined ) this.transmissionMap = textures[ json.transmissionMap ] || null;
- if ( json.thicknessMap !== undefined ) this.thicknessMap = textures[ json.thicknessMap ] || null;
-
- if ( json.anisotropyMap !== undefined ) this.anisotropyMap = textures[ json.anisotropyMap ] || null;
-
- if ( json.sheenColorMap !== undefined ) this.sheenColorMap = textures[ json.sheenColorMap ] || null;
- if ( json.sheenRoughnessMap !== undefined ) this.sheenRoughnessMap = textures[ json.sheenRoughnessMap ] || null;
-
- return this;
-
- }
-
- /**
- * Returns a new material with copied values from this instance.
- *
- * @return {Material} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Copies the values of the given material to this instance.
- *
- * @param {Material} source - The material to copy.
- * @return {Material} A reference to this instance.
- */
- copy( source ) {
-
- this.name = source.name;
-
- this.blending = source.blending;
- this.side = source.side;
- this.vertexColors = source.vertexColors;
-
- this.opacity = source.opacity;
- this.transparent = source.transparent;
-
- this.blendSrc = source.blendSrc;
- this.blendDst = source.blendDst;
- this.blendEquation = source.blendEquation;
- this.blendSrcAlpha = source.blendSrcAlpha;
- this.blendDstAlpha = source.blendDstAlpha;
- this.blendEquationAlpha = source.blendEquationAlpha;
- this.blendColor.copy( source.blendColor );
- this.blendAlpha = source.blendAlpha;
-
- this.depthFunc = source.depthFunc;
- this.depthTest = source.depthTest;
- this.depthWrite = source.depthWrite;
-
- this.stencilWriteMask = source.stencilWriteMask;
- this.stencilFunc = source.stencilFunc;
- this.stencilRef = source.stencilRef;
- this.stencilFuncMask = source.stencilFuncMask;
- this.stencilFail = source.stencilFail;
- this.stencilZFail = source.stencilZFail;
- this.stencilZPass = source.stencilZPass;
- this.stencilWrite = source.stencilWrite;
-
- const srcPlanes = source.clippingPlanes;
- let dstPlanes = null;
-
- if ( srcPlanes !== null ) {
-
- const n = srcPlanes.length;
- dstPlanes = new Array( n );
-
- for ( let i = 0; i !== n; ++ i ) {
-
- dstPlanes[ i ] = srcPlanes[ i ].clone();
-
- }
-
- }
-
- this.clippingPlanes = dstPlanes;
- this.clipIntersection = source.clipIntersection;
- this.clipShadows = source.clipShadows;
-
- this.shadowSide = source.shadowSide;
-
- this.colorWrite = source.colorWrite;
-
- this.precision = source.precision;
-
- this.polygonOffset = source.polygonOffset;
- this.polygonOffsetFactor = source.polygonOffsetFactor;
- this.polygonOffsetUnits = source.polygonOffsetUnits;
-
- this.dithering = source.dithering;
-
- this.alphaTest = source.alphaTest;
- this.alphaHash = source.alphaHash;
- this.alphaToCoverage = source.alphaToCoverage;
- this.premultipliedAlpha = source.premultipliedAlpha;
- this.forceSinglePass = source.forceSinglePass;
- this.allowOverride = source.allowOverride;
-
- this.visible = source.visible;
-
- this.toneMapped = source.toneMapped;
-
- this.userData = JSON.parse( JSON.stringify( source.userData ) );
-
- return this;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- *
- * @fires Material#dispose
- */
- dispose() {
-
- /**
- * Fires when the material has been disposed of.
- *
- * @event Material#dispose
- * @type {Object}
- */
- this.dispatchEvent( { type: 'dispose' } );
-
- }
-
- /**
- * Setting this property to `true` indicates the engine the material
- * needs to be recompiled.
- *
- * @type {boolean}
- * @default false
- * @param {boolean} value
- */
- set needsUpdate( value ) {
-
- if ( value === true ) this.version ++;
-
- }
-
-}
-
-/**
- * A material for rendering instances of {@link Sprite}.
- *
- * ```js
- * const map = new THREE.TextureLoader().load( 'textures/sprite.png' );
- * const material = new THREE.SpriteMaterial( { map: map, color: 0xffffff } );
- *
- * const sprite = new THREE.Sprite( material );
- * sprite.scale.set(200, 200, 1)
- * scene.add( sprite );
- * ```
- *
- * @augments Material
- */
-class SpriteMaterial extends Material {
-
- /**
- * Constructs a new sprite material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isSpriteMaterial = true;
-
- this.type = 'SpriteMaterial';
-
- /**
- * Color of the material.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.color = new Color( 0xffffff );
-
- /**
- * The color map. May optionally include an alpha channel, typically combined
- * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
- * color is modulated by the diffuse `color`.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * The alpha map is a grayscale texture that controls the opacity across the
- * surface (black: fully transparent; white: fully opaque).
- *
- * Only the color of the texture is used, ignoring the alpha channel if one
- * exists. For RGB and RGBA textures, the renderer will use the green channel
- * when sampling this texture due to the extra bit of precision provided for
- * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
- * luminance/alpha textures will also still work as expected.
- *
- * `alphaMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.alphaMap = null;
-
- /**
- * The rotation of the sprite in radians.
- *
- * @type {number}
- * @default 0
- */
- this.rotation = 0;
-
- /**
- * Specifies whether size of the sprite is attenuated by the camera depth (perspective camera only).
- *
- * @type {boolean}
- * @default true
- */
- this.sizeAttenuation = true;
-
- /**
- * Overwritten since sprite materials are transparent
- * by default.
- *
- * @type {boolean}
- * @default true
- */
- this.transparent = true;
-
- /**
- * Whether the material is affected by fog or not.
- *
- * @type {boolean}
- * @default true
- */
- this.fog = true;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.color.copy( source.color );
-
- this.map = source.map;
-
- this.alphaMap = source.alphaMap;
-
- this.rotation = source.rotation;
-
- this.sizeAttenuation = source.sizeAttenuation;
-
- this.fog = source.fog;
-
- return this;
-
- }
-
-}
-
-let _geometry;
-
-const _intersectPoint = /*@__PURE__*/ new Vector3();
-const _worldScale = /*@__PURE__*/ new Vector3();
-const _mvPosition = /*@__PURE__*/ new Vector3();
-
-const _alignedPosition = /*@__PURE__*/ new Vector2();
-const _rotatedPosition = /*@__PURE__*/ new Vector2();
-const _viewWorldMatrix = /*@__PURE__*/ new Matrix4();
-
-const _vA$1 = /*@__PURE__*/ new Vector3();
-const _vB$1 = /*@__PURE__*/ new Vector3();
-const _vC$1 = /*@__PURE__*/ new Vector3();
-
-const _uvA = /*@__PURE__*/ new Vector2();
-const _uvB = /*@__PURE__*/ new Vector2();
-const _uvC = /*@__PURE__*/ new Vector2();
-
-/**
- * A sprite is a plane that always faces towards the camera, generally with a
- * partially transparent texture applied.
- *
- * Sprites do not cast shadows, setting {@link Object3D#castShadow} to `true` will
- * have no effect.
- *
- * ```js
- * const map = new THREE.TextureLoader().load( 'sprite.png' );
- * const material = new THREE.SpriteMaterial( { map: map } );
- *
- * const sprite = new THREE.Sprite( material );
- * scene.add( sprite );
- * ```
- *
- * @augments Object3D
- */
-class Sprite extends Object3D {
-
- /**
- * Constructs a new sprite.
- *
- * @param {(SpriteMaterial|SpriteNodeMaterial)} [material] - The sprite material.
- */
- constructor( material = new SpriteMaterial() ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isSprite = true;
-
- this.type = 'Sprite';
-
- if ( _geometry === undefined ) {
-
- _geometry = new BufferGeometry();
-
- const float32Array = new Float32Array( [
- -0.5, -0.5, 0, 0, 0,
- 0.5, -0.5, 0, 1, 0,
- 0.5, 0.5, 0, 1, 1,
- -0.5, 0.5, 0, 0, 1
- ] );
-
- const interleavedBuffer = new InterleavedBuffer( float32Array, 5 );
-
- _geometry.setIndex( [ 0, 1, 2, 0, 2, 3 ] );
- _geometry.setAttribute( 'position', new InterleavedBufferAttribute( interleavedBuffer, 3, 0, false ) );
- _geometry.setAttribute( 'uv', new InterleavedBufferAttribute( interleavedBuffer, 2, 3, false ) );
-
- }
-
- /**
- * The sprite geometry.
- *
- * @type {BufferGeometry}
- */
- this.geometry = _geometry;
-
- /**
- * The sprite material.
- *
- * @type {(SpriteMaterial|SpriteNodeMaterial)}
- */
- this.material = material;
-
- /**
- * The sprite's anchor point, and the point around which the sprite rotates.
- * A value of `(0.5, 0.5)` corresponds to the midpoint of the sprite. A value
- * of `(0, 0)` corresponds to the lower left corner of the sprite.
- *
- * @type {Vector2}
- * @default (0.5,0.5)
- */
- this.center = new Vector2( 0.5, 0.5 );
-
- /**
- * The number of instances of this sprite.
- * Can only be used with {@link WebGPURenderer}.
- *
- * @type {number}
- * @default 1
- */
- this.count = 1;
-
- }
-
- /**
- * Returns `true` if this sprite intersects the given frustum.
- *
- * @param {Frustum|FrustumArray} frustum - The frustum to test.
- * @return {boolean} Whether this sprite intersects the given frustum or not.
- */
- intersectsFrustum( frustum ) {
-
- return frustum.intersectsSprite( this );
-
- }
-
- /**
- * Computes intersection points between a casted ray and this sprite.
- *
- * @param {Raycaster} raycaster - The raycaster.
- * @param {Array} intersects - The target array that holds the intersection points.
- */
- raycast( raycaster, intersects ) {
-
- if ( raycaster.camera === null ) {
-
- error( 'Sprite: "Raycaster.camera" needs to be set in order to raycast against sprites.' );
-
- }
-
- _worldScale.setFromMatrixScale( this.matrixWorld );
-
- _viewWorldMatrix.copy( raycaster.camera.matrixWorld );
- this.modelViewMatrix.multiplyMatrices( raycaster.camera.matrixWorldInverse, this.matrixWorld );
-
- _mvPosition.setFromMatrixPosition( this.modelViewMatrix );
-
- if ( raycaster.camera.isPerspectiveCamera && this.material.sizeAttenuation === false ) {
-
- _worldScale.multiplyScalar( - _mvPosition.z );
-
- }
-
- const rotation = this.material.rotation;
- let sin, cos;
-
- if ( rotation !== 0 ) {
-
- cos = Math.cos( rotation );
- sin = Math.sin( rotation );
-
- }
-
- const center = this.center;
-
- transformVertex( _vA$1.set( -0.5, -0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );
- transformVertex( _vB$1.set( 0.5, -0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );
- transformVertex( _vC$1.set( 0.5, 0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );
-
- _uvA.set( 0, 0 );
- _uvB.set( 1, 0 );
- _uvC.set( 1, 1 );
-
- // check first triangle
- let intersect = raycaster.ray.intersectTriangle( _vA$1, _vB$1, _vC$1, false, _intersectPoint );
-
- if ( intersect === null ) {
-
- // check second triangle
- transformVertex( _vB$1.set( -0.5, 0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );
- _uvB.set( 0, 1 );
-
- intersect = raycaster.ray.intersectTriangle( _vA$1, _vC$1, _vB$1, false, _intersectPoint );
- if ( intersect === null ) {
-
- return;
-
- }
-
- }
-
- const distance = raycaster.ray.origin.distanceTo( _intersectPoint );
-
- if ( distance < raycaster.near || distance > raycaster.far ) return;
-
- intersects.push( {
-
- distance: distance,
- point: _intersectPoint.clone(),
- uv: Triangle.getInterpolation( _intersectPoint, _vA$1, _vB$1, _vC$1, _uvA, _uvB, _uvC, new Vector2() ),
- face: null,
- object: this
-
- } );
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- if ( source.center !== undefined ) this.center.copy( source.center );
-
- this.material = source.material;
-
- return this;
-
- }
-
-}
-
-function transformVertex( vertexPosition, mvPosition, center, scale, sin, cos ) {
-
- // compute position in camera space
- _alignedPosition.subVectors( vertexPosition, center ).addScalar( 0.5 ).multiply( scale );
-
- // to check if rotation is not zero
- if ( sin !== undefined ) {
-
- _rotatedPosition.x = ( cos * _alignedPosition.x ) - ( sin * _alignedPosition.y );
- _rotatedPosition.y = ( sin * _alignedPosition.x ) + ( cos * _alignedPosition.y );
-
- } else {
-
- _rotatedPosition.copy( _alignedPosition );
-
- }
-
-
- vertexPosition.copy( mvPosition );
- vertexPosition.x += _rotatedPosition.x;
- vertexPosition.y += _rotatedPosition.y;
-
- // transform to world space
- vertexPosition.applyMatrix4( _viewWorldMatrix );
-
-}
-
-const _v1$2 = /*@__PURE__*/ new Vector3();
-const _v2$1 = /*@__PURE__*/ new Vector3();
-
-/**
- * A component for providing a basic Level of Detail (LOD) mechanism.
- *
- * Every LOD level is associated with an object, and rendering can be switched
- * between them at the distances specified. Typically you would create, say,
- * three meshes, one for far away (low detail), one for mid range (medium
- * detail) and one for close up (high detail).
- *
- * ```js
- * const lod = new THREE.LOD();
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- *
- * //Create spheres with 3 levels of detail and create new LOD levels for them
- * for( let i = 0; i < 3; i++ ) {
- *
- * const geometry = new THREE.IcosahedronGeometry( 10, 3 - i );
- * const mesh = new THREE.Mesh( geometry, material );
- * lod.addLevel( mesh, i * 75 );
- *
- * }
- *
- * scene.add( lod );
- * ```
- *
- * @augments Object3D
- */
-class LOD extends Object3D {
-
- /**
- * Constructs a new LOD.
- */
- constructor() {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isLOD = true;
-
- /**
- * The current LOD index.
- *
- * @private
- * @type {number}
- * @default 0
- */
- this._currentLevel = 0;
-
- this.type = 'LOD';
-
- Object.defineProperties( this, {
- /**
- * This array holds the LOD levels.
- *
- * @name LOD#levels
- * @type {Array<{object:Object3D,distance:number,hysteresis:number}>}
- */
- levels: {
- enumerable: true,
- value: []
- }
- } );
-
- /**
- * Whether the LOD object is updated automatically by the renderer per frame
- * or not. If set to `false`, you have to call {@link LOD#update} in the
- * render loop by yourself.
- *
- * @type {boolean}
- * @default true
- */
- this.autoUpdate = true;
-
- }
-
- copy( source ) {
-
- super.copy( source, false );
-
- const levels = source.levels;
-
- for ( let i = 0, l = levels.length; i < l; i ++ ) {
-
- const level = levels[ i ];
-
- this.addLevel( level.object.clone(), level.distance, level.hysteresis );
-
- }
-
- this.autoUpdate = source.autoUpdate;
-
- return this;
-
- }
-
- /**
- * Adds a mesh that will display at a certain distance and greater. Typically
- * the further away the distance, the lower the detail on the mesh.
- *
- * @param {Object3D} object - The 3D object to display at this level.
- * @param {number} [distance=0] - The distance at which to display this level of detail.
- * @param {number} [hysteresis=0] - Threshold used to avoid flickering at LOD boundaries, as a fraction of distance.
- * @return {LOD} A reference to this instance.
- */
- addLevel( object, distance = 0, hysteresis = 0 ) {
-
- distance = Math.abs( distance );
-
- const levels = this.levels;
-
- let l;
-
- for ( l = 0; l < levels.length; l ++ ) {
-
- if ( distance < levels[ l ].distance ) {
-
- break;
-
- }
-
- }
-
- levels.splice( l, 0, { distance: distance, hysteresis: hysteresis, object: object } );
-
- this.add( object );
-
- return this;
-
- }
-
- /**
- * Removes an existing level, based on the distance from the camera.
- * Returns `true` when the level has been removed. Otherwise `false`.
- *
- * @param {number} distance - Distance of the level to remove.
- * @return {boolean} Whether the level has been removed or not.
- */
- removeLevel( distance ) {
-
- const levels = this.levels;
-
- for ( let i = 0; i < levels.length; i ++ ) {
-
- if ( levels[ i ].distance === distance ) {
-
- const removedElements = levels.splice( i, 1 );
- this.remove( removedElements[ 0 ].object );
-
- return true;
-
- }
-
- }
-
- return false;
-
- }
-
- /**
- * Returns the currently active LOD level index.
- *
- * @return {number} The current active LOD level index.
- */
- getCurrentLevel() {
-
- return this._currentLevel;
-
- }
-
- /**
- * Returns a reference to the first 3D object that is greater than
- * the given distance.
- *
- * @param {number} distance - The LOD distance.
- * @return {?Object3D} The found 3D object. `null` if no 3D object has been found.
- */
- getObjectForDistance( distance ) {
-
- const levels = this.levels;
-
- if ( levels.length > 0 ) {
-
- let i, l;
-
- for ( i = 1, l = levels.length; i < l; i ++ ) {
-
- let levelDistance = levels[ i ].distance;
-
- if ( levels[ i ].object.visible ) {
-
- levelDistance -= levelDistance * levels[ i ].hysteresis;
-
- }
-
- if ( distance < levelDistance ) {
-
- break;
-
- }
-
- }
-
- return levels[ i - 1 ].object;
-
- }
-
- return null;
-
- }
-
- /**
- * Computes intersection points between a casted ray and this LOD.
- *
- * @param {Raycaster} raycaster - The raycaster.
- * @param {Array} intersects - The target array that holds the intersection points.
- */
- raycast( raycaster, intersects ) {
-
- const levels = this.levels;
-
- if ( levels.length > 0 ) {
-
- _v1$2.setFromMatrixPosition( this.matrixWorld );
-
- const distance = raycaster.ray.origin.distanceTo( _v1$2 );
-
- this.getObjectForDistance( distance ).raycast( raycaster, intersects );
-
- }
-
- }
-
- /**
- * Updates the LOD by computing which LOD level should be visible according
- * to the current distance of the given camera.
- *
- * @param {Camera} camera - The camera the scene is rendered with.
- */
- update( camera ) {
-
- const levels = this.levels;
-
- if ( levels.length > 1 ) {
-
- _v1$2.setFromMatrixPosition( camera.matrixWorld );
- _v2$1.setFromMatrixPosition( this.matrixWorld );
-
- const distance = _v1$2.distanceTo( _v2$1 ) / camera.zoom;
-
- levels[ 0 ].object.visible = true;
-
- let i, l;
-
- for ( i = 1, l = levels.length; i < l; i ++ ) {
-
- let levelDistance = levels[ i ].distance;
-
- if ( levels[ i ].object.visible ) {
-
- levelDistance -= levelDistance * levels[ i ].hysteresis;
-
- }
-
- if ( distance >= levelDistance ) {
-
- levels[ i - 1 ].object.visible = false;
- levels[ i ].object.visible = true;
-
- } else {
-
- break;
-
- }
-
- }
-
- this._currentLevel = i - 1;
-
- for ( ; i < l; i ++ ) {
-
- levels[ i ].object.visible = false;
-
- }
-
- }
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.object.autoUpdate = this.autoUpdate;
-
- data.object.levels = [];
-
- const levels = this.levels;
-
- for ( let i = 0, l = levels.length; i < l; i ++ ) {
-
- const level = levels[ i ];
-
- data.object.levels.push( {
- object: level.object.uuid,
- distance: level.distance,
- hysteresis: level.hysteresis
- } );
-
- }
-
- return data;
-
- }
-
-}
-
-const _vector$7 = /*@__PURE__*/ new Vector3();
-const _segCenter = /*@__PURE__*/ new Vector3();
-const _segDir = /*@__PURE__*/ new Vector3();
-const _diff = /*@__PURE__*/ new Vector3();
-
-/**
- * A ray that emits from an origin in a certain direction. The class is used by
- * {@link Raycaster} to assist with raycasting. Raycasting is used for
- * mouse picking (working out what objects in the 3D space the mouse is over)
- * amongst other things.
- */
-class Ray {
-
- /**
- * Constructs a new ray.
- *
- * @param {Vector3} [origin=(0,0,0)] - The origin of the ray.
- * @param {Vector3} [direction=(0,0,-1)] - The (normalized) direction of the ray.
- */
- constructor( origin = new Vector3(), direction = new Vector3( 0, 0, -1 ) ) {
-
- /**
- * The origin of the ray.
- *
- * @type {Vector3}
- */
- this.origin = origin;
-
- /**
- * The (normalized) direction of the ray.
- *
- * @type {Vector3}
- */
- this.direction = direction;
-
- }
-
- /**
- * Sets the ray's components by copying the given values.
- *
- * @param {Vector3} origin - The origin.
- * @param {Vector3} direction - The direction.
- * @return {Ray} A reference to this ray.
- */
- set( origin, direction ) {
-
- this.origin.copy( origin );
- this.direction.copy( direction );
-
- return this;
-
- }
-
- /**
- * Copies the values of the given ray to this instance.
- *
- * @param {Ray} ray - The ray to copy.
- * @return {Ray} A reference to this ray.
- */
- copy( ray ) {
-
- this.origin.copy( ray.origin );
- this.direction.copy( ray.direction );
-
- return this;
-
- }
-
- /**
- * Returns a vector that is located at a given distance along this ray.
- *
- * @param {number} t - The distance along the ray to retrieve a position for.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} A position on the ray.
- */
- at( t, target ) {
-
- return target.copy( this.origin ).addScaledVector( this.direction, t );
-
- }
-
- /**
- * Adjusts the direction of the ray to point at the given vector in world space.
- *
- * @param {Vector3} v - The target position.
- * @return {Ray} A reference to this ray.
- */
- lookAt( v ) {
-
- this.direction.copy( v ).sub( this.origin ).normalize();
-
- return this;
-
- }
-
- /**
- * Shift the origin of this ray along its direction by the given distance.
- *
- * @param {number} t - The distance along the ray to interpolate.
- * @return {Ray} A reference to this ray.
- */
- recast( t ) {
-
- this.origin.copy( this.at( t, _vector$7 ) );
-
- return this;
-
- }
-
- /**
- * Returns the point along this ray that is closest to the given point.
- *
- * @param {Vector3} point - A point in 3D space to get the closet location on the ray for.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The closest point on this ray.
- */
- closestPointToPoint( point, target ) {
-
- target.subVectors( point, this.origin );
-
- const directionDistance = target.dot( this.direction );
-
- if ( directionDistance < 0 ) {
-
- return target.copy( this.origin );
-
- }
-
- return target.copy( this.origin ).addScaledVector( this.direction, directionDistance );
-
- }
-
- /**
- * Returns the distance of the closest approach between this ray and the given point.
- *
- * @param {Vector3} point - A point in 3D space to compute the distance to.
- * @return {number} The distance.
- */
- distanceToPoint( point ) {
-
- return Math.sqrt( this.distanceSqToPoint( point ) );
-
- }
-
- /**
- * Returns the squared distance of the closest approach between this ray and the given point.
- *
- * @param {Vector3} point - A point in 3D space to compute the distance to.
- * @return {number} The squared distance.
- */
- distanceSqToPoint( point ) {
-
- const directionDistance = _vector$7.subVectors( point, this.origin ).dot( this.direction );
-
- // point behind the ray
-
- if ( directionDistance < 0 ) {
-
- return this.origin.distanceToSquared( point );
-
- }
-
- _vector$7.copy( this.origin ).addScaledVector( this.direction, directionDistance );
-
- return _vector$7.distanceToSquared( point );
-
- }
-
- /**
- * Returns the squared distance between this ray and the given line segment.
- *
- * @param {Vector3} v0 - The start point of the line segment.
- * @param {Vector3} v1 - The end point of the line segment.
- * @param {Vector3} [optionalPointOnRay] - When provided, it receives the point on this ray that is closest to the segment.
- * @param {Vector3} [optionalPointOnSegment] - When provided, it receives the point on the line segment that is closest to this ray.
- * @return {number} The squared distance.
- */
- distanceSqToSegment( v0, v1, optionalPointOnRay, optionalPointOnSegment ) {
-
- // from https://github.com/pmjoniak/GeometricTools/blob/master/GTEngine/Include/Mathematics/GteDistRaySegment.h
- // It returns the min distance between the ray and the segment
- // defined by v0 and v1
- // It can also set two optional targets :
- // - The closest point on the ray
- // - The closest point on the segment
-
- _segCenter.copy( v0 ).add( v1 ).multiplyScalar( 0.5 );
- _segDir.copy( v1 ).sub( v0 ).normalize();
- _diff.copy( this.origin ).sub( _segCenter );
-
- const segExtent = v0.distanceTo( v1 ) * 0.5;
- const a01 = - this.direction.dot( _segDir );
- const b0 = _diff.dot( this.direction );
- const b1 = - _diff.dot( _segDir );
- const c = _diff.lengthSq();
- const det = Math.abs( 1 - a01 * a01 );
- let s0, s1, sqrDist, extDet;
-
- if ( det > 0 ) {
-
- // The ray and segment are not parallel.
-
- s0 = a01 * b1 - b0;
- s1 = a01 * b0 - b1;
- extDet = segExtent * det;
-
- if ( s0 >= 0 ) {
-
- if ( s1 >= - extDet ) {
-
- if ( s1 <= extDet ) {
-
- // region 0
- // Minimum at interior points of ray and segment.
-
- const invDet = 1 / det;
- s0 *= invDet;
- s1 *= invDet;
- sqrDist = s0 * ( s0 + a01 * s1 + 2 * b0 ) + s1 * ( a01 * s0 + s1 + 2 * b1 ) + c;
-
- } else {
-
- // region 1
-
- s1 = segExtent;
- s0 = Math.max( 0, - ( a01 * s1 + b0 ) );
- sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;
-
- }
-
- } else {
-
- // region 5
-
- s1 = - segExtent;
- s0 = Math.max( 0, - ( a01 * s1 + b0 ) );
- sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;
-
- }
-
- } else {
-
- if ( s1 <= - extDet ) {
-
- // region 4
-
- s0 = Math.max( 0, - ( - a01 * segExtent + b0 ) );
- s1 = ( s0 > 0 ) ? - segExtent : Math.min( Math.max( - segExtent, - b1 ), segExtent );
- sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;
-
- } else if ( s1 <= extDet ) {
-
- // region 3
-
- s0 = 0;
- s1 = Math.min( Math.max( - segExtent, - b1 ), segExtent );
- sqrDist = s1 * ( s1 + 2 * b1 ) + c;
-
- } else {
-
- // region 2
-
- s0 = Math.max( 0, - ( a01 * segExtent + b0 ) );
- s1 = ( s0 > 0 ) ? segExtent : Math.min( Math.max( - segExtent, - b1 ), segExtent );
- sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;
-
- }
-
- }
-
- } else {
-
- // Ray and segment are parallel.
-
- s1 = ( a01 > 0 ) ? - segExtent : segExtent;
- s0 = Math.max( 0, - ( a01 * s1 + b0 ) );
- sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;
-
- }
-
- if ( optionalPointOnRay ) {
-
- optionalPointOnRay.copy( this.origin ).addScaledVector( this.direction, s0 );
-
- }
-
- if ( optionalPointOnSegment ) {
-
- optionalPointOnSegment.copy( _segCenter ).addScaledVector( _segDir, s1 );
-
- }
-
- return sqrDist;
-
- }
-
- /**
- * Intersects this ray with the given sphere, returning the intersection
- * point or `null` if there is no intersection.
- *
- * @param {Sphere} sphere - The sphere to intersect.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {?Vector3} The intersection point.
- */
- intersectSphere( sphere, target ) {
-
- if ( sphere.radius < 0 ) return null; // handle empty spheres, see #31187
-
- _vector$7.subVectors( sphere.center, this.origin );
- const tca = _vector$7.dot( this.direction );
- const d2 = _vector$7.dot( _vector$7 ) - tca * tca;
- const radius2 = sphere.radius * sphere.radius;
-
- if ( d2 > radius2 ) return null;
-
- const thc = Math.sqrt( radius2 - d2 );
-
- // t0 = first intersect point - entrance on front of sphere
- const t0 = tca - thc;
-
- // t1 = second intersect point - exit point on back of sphere
- const t1 = tca + thc;
-
- // test to see if t1 is behind the ray - if so, return null
- if ( t1 < 0 ) return null;
-
- // test to see if t0 is behind the ray:
- // if it is, the ray is inside the sphere, so return the second exit point scaled by t1,
- // in order to always return an intersect point that is in front of the ray.
- if ( t0 < 0 ) return this.at( t1, target );
-
- // else t0 is in front of the ray, so return the first collision point scaled by t0
- return this.at( t0, target );
-
- }
-
- /**
- * Returns `true` if this ray intersects with the given sphere.
- *
- * @param {Sphere} sphere - The sphere to intersect.
- * @return {boolean} Whether this ray intersects with the given sphere or not.
- */
- intersectsSphere( sphere ) {
-
- if ( sphere.radius < 0 ) return false; // handle empty spheres, see #31187
-
- return this.distanceSqToPoint( sphere.center ) <= ( sphere.radius * sphere.radius );
-
- }
-
- /**
- * Computes the distance from the ray's origin to the given plane. Returns `null` if the ray
- * does not intersect with the plane.
- *
- * @param {Plane} plane - The plane to compute the distance to.
- * @return {?number} Whether this ray intersects with the given sphere or not.
- */
- distanceToPlane( plane ) {
-
- const denominator = plane.normal.dot( this.direction );
-
- if ( denominator === 0 ) {
-
- // line is coplanar, return origin
- if ( plane.distanceToPoint( this.origin ) === 0 ) {
-
- return 0;
-
- }
-
- // Null is preferable to undefined since undefined means.... it is undefined
-
- return null;
-
- }
-
- const t = - ( this.origin.dot( plane.normal ) + plane.constant ) / denominator;
-
- // Return if the ray never intersects the plane
-
- return t >= 0 ? t : null;
-
- }
-
- /**
- * Intersects this ray with the given plane, returning the intersection
- * point or `null` if there is no intersection.
- *
- * @param {Plane} plane - The plane to intersect.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {?Vector3} The intersection point.
- */
- intersectPlane( plane, target ) {
-
- const t = this.distanceToPlane( plane );
-
- if ( t === null ) {
-
- return null;
-
- }
-
- return this.at( t, target );
-
- }
-
- /**
- * Returns `true` if this ray intersects with the given plane.
- *
- * @param {Plane} plane - The plane to intersect.
- * @return {boolean} Whether this ray intersects with the given plane or not.
- */
- intersectsPlane( plane ) {
-
- // check if the ray lies on the plane first
-
- const distToPoint = plane.distanceToPoint( this.origin );
-
- if ( distToPoint === 0 ) {
-
- return true;
-
- }
-
- const denominator = plane.normal.dot( this.direction );
-
- if ( denominator * distToPoint < 0 ) {
-
- return true;
-
- }
-
- // ray origin is behind the plane (and is pointing behind it)
-
- return false;
-
- }
-
- /**
- * Intersects this ray with the given bounding box, returning the intersection
- * point or `null` if there is no intersection.
- *
- * @param {Box3} box - The box to intersect.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {?Vector3} The intersection point.
- */
- intersectBox( box, target ) {
-
- let tmin, tmax, tymin, tymax, tzmin, tzmax;
-
- const invdirx = 1 / this.direction.x,
- invdiry = 1 / this.direction.y,
- invdirz = 1 / this.direction.z;
-
- const origin = this.origin;
-
- if ( invdirx >= 0 ) {
-
- tmin = ( box.min.x - origin.x ) * invdirx;
- tmax = ( box.max.x - origin.x ) * invdirx;
-
- } else {
-
- tmin = ( box.max.x - origin.x ) * invdirx;
- tmax = ( box.min.x - origin.x ) * invdirx;
-
- }
-
- if ( invdiry >= 0 ) {
-
- tymin = ( box.min.y - origin.y ) * invdiry;
- tymax = ( box.max.y - origin.y ) * invdiry;
-
- } else {
-
- tymin = ( box.max.y - origin.y ) * invdiry;
- tymax = ( box.min.y - origin.y ) * invdiry;
-
- }
-
- if ( ( tmin > tymax ) || ( tymin > tmax ) ) return null;
-
- if ( tymin > tmin || isNaN( tmin ) ) tmin = tymin;
-
- if ( tymax < tmax || isNaN( tmax ) ) tmax = tymax;
-
- if ( invdirz >= 0 ) {
-
- tzmin = ( box.min.z - origin.z ) * invdirz;
- tzmax = ( box.max.z - origin.z ) * invdirz;
-
- } else {
-
- tzmin = ( box.max.z - origin.z ) * invdirz;
- tzmax = ( box.min.z - origin.z ) * invdirz;
-
- }
-
- if ( ( tmin > tzmax ) || ( tzmin > tmax ) ) return null;
-
- if ( tzmin > tmin || tmin !== tmin ) tmin = tzmin;
-
- if ( tzmax < tmax || tmax !== tmax ) tmax = tzmax;
-
- //return point closest to the ray (positive side)
-
- if ( tmax < 0 ) return null;
-
- return this.at( tmin >= 0 ? tmin : tmax, target );
-
- }
-
- /**
- * Returns `true` if this ray intersects with the given box.
- *
- * @param {Box3} box - The box to intersect.
- * @return {boolean} Whether this ray intersects with the given box or not.
- */
- intersectsBox( box ) {
-
- return this.intersectBox( box, _vector$7 ) !== null;
-
- }
-
- /**
- * Intersects this ray with the given triangle, returning the intersection
- * point or `null` if there is no intersection.
- *
- * @param {Vector3} a - The first vertex of the triangle.
- * @param {Vector3} b - The second vertex of the triangle.
- * @param {Vector3} c - The third vertex of the triangle.
- * @param {boolean} backfaceCulling - Whether to use backface culling or not.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {?Vector3} The intersection point.
- */
- intersectTriangle( a, b, c, backfaceCulling, target ) {
-
- // Watertight ray/triangle intersection. Reference: Woop, Benthin, Wald,
- // "Watertight Ray/Triangle Intersection", JCGT vol. 2 no. 1 (2013), Appendix A.
- // https://jcgt.org/published/0002/01/05/
-
- const origin = this.origin;
- const direction = this.direction;
-
- const dx = direction.x;
- const dy = direction.y;
- const dz = direction.z;
-
- // triangle vertices relative to the ray origin
-
- const aox = a.x - origin.x, aoy = a.y - origin.y, aoz = a.z - origin.z;
- const box = b.x - origin.x, boy = b.y - origin.y, boz = b.z - origin.z;
- const cox = c.x - origin.x, coy = c.y - origin.y, coz = c.z - origin.z;
-
- // Use the dimension where the ray direction is maximal as the projection
- // axis (kz) and read every component already permuted into (kx, ky, kz).
- // kx and ky are swapped when the direction's kz component is negative, to
- // preserve the winding order of triangles.
-
- const adx = Math.abs( dx ), ady = Math.abs( dy ), adz = Math.abs( dz );
-
- let dkx, dky, dkz;
- let akx, aky, akz, bkx, bky, bkz, ckx, cky, ckz;
-
- if ( adx >= ady && adx >= adz ) {
-
- dkz = dx; akz = aox; bkz = box; ckz = cox;
-
- if ( dx >= 0 ) {
-
- dkx = dy; dky = dz;
- akx = aoy; aky = aoz; bkx = boy; bky = boz; ckx = coy; cky = coz;
-
- } else {
-
- dkx = dz; dky = dy;
- akx = aoz; aky = aoy; bkx = boz; bky = boy; ckx = coz; cky = coy;
-
- }
-
- } else if ( ady >= adz ) {
-
- dkz = dy; akz = aoy; bkz = boy; ckz = coy;
-
- if ( dy >= 0 ) {
-
- dkx = dz; dky = dx;
- akx = aoz; aky = aox; bkx = boz; bky = box; ckx = coz; cky = cox;
-
- } else {
-
- dkx = dx; dky = dz;
- akx = aox; aky = aoz; bkx = box; bky = boz; ckx = cox; cky = coz;
-
- }
-
- } else {
-
- dkz = dz; akz = aoz; bkz = boz; ckz = coz;
-
- if ( dz >= 0 ) {
-
- dkx = dx; dky = dy;
- akx = aox; aky = aoy; bkx = box; bky = boy; ckx = cox; cky = coy;
-
- } else {
-
- dkx = dy; dky = dx;
- akx = aoy; aky = aox; bkx = boy; bky = box; ckx = coy; cky = cox;
-
- }
-
- }
-
- // a zero direction has no maximal axis and cannot intersect
-
- if ( dkz === 0 ) return null;
-
- // shear constants that align the ray with the +kz axis
-
- const sx = dkx / dkz, sy = dky / dkz, sz = 1 / dkz;
-
- // sheared and scaled vertices
-
- const ax = akx - sx * akz, ay = aky - sy * akz;
- const bx = bkx - sx * bkz, by = bky - sy * bkz;
- const cx = ckx - sx * ckz, cy = cky - sy * ckz;
-
- // scaled barycentric coordinates (signed edge functions); the shear makes a
- // shared edge evaluate identically for both adjacent triangles, so the ray
- // can never fall between them
-
- const u = cx * by - cy * bx;
- const v = ax * cy - ay * cx;
- const w = bx * ay - by * ax;
-
- if ( backfaceCulling ) {
-
- if ( u < 0 || v < 0 || w < 0 ) return null;
-
- } else {
-
- if ( ( u < 0 || v < 0 || w < 0 ) && ( u > 0 || v > 0 || w > 0 ) ) return null;
-
- }
-
- const det = u + v + w;
-
- // ray is co-planar with the triangle
-
- if ( det === 0 ) return null;
-
- // scaled hit distance; t = tScaled / det must lie in front of the origin
-
- const tScaled = sz * ( u * akz + v * bkz + w * ckz );
-
- if ( det > 0 ? tScaled < 0 : tScaled > 0 ) return null;
-
- return this.at( tScaled / det, target );
-
- }
-
- /**
- * Transforms this ray with the given 4x4 transformation matrix.
- *
- * @param {Matrix4} matrix4 - The transformation matrix.
- * @return {Ray} A reference to this ray.
- */
- applyMatrix4( matrix4 ) {
-
- this.origin.applyMatrix4( matrix4 );
- this.direction.transformDirection( matrix4 );
-
- return this;
-
- }
-
- /**
- * Returns `true` if this ray is equal with the given one.
- *
- * @param {Ray} ray - The ray to test for equality.
- * @return {boolean} Whether this ray is equal with the given one.
- */
- equals( ray ) {
-
- return ray.origin.equals( this.origin ) && ray.direction.equals( this.direction );
-
- }
-
- /**
- * Returns a new ray with copied values from this instance.
- *
- * @return {Ray} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
-}
-
-/**
- * A material for drawing geometries in a simple shaded (flat or wireframe) way.
- *
- * This material is not affected by lights.
- *
- * @augments Material
- * @demo scenes/material-browser.html#MeshBasicMaterial
- */
-class MeshBasicMaterial extends Material {
-
- /**
- * Constructs a new mesh basic material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMeshBasicMaterial = true;
-
- this.type = 'MeshBasicMaterial';
-
- /**
- * Color of the material.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.color = new Color( 0xffffff ); // diffuse
-
- /**
- * The color map. May optionally include an alpha channel, typically combined
- * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
- * color is modulated by the diffuse `color`.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * The light map. Requires a second set of UVs.
- *
- * `lightMap` represents pre-baked illuminance data, and the texture must be assigned
- * a {@link Texture#colorSpace}. Most `lightMap` textures set
- * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats
- * such as `.exr` or `.hdr`.
- *
- * @type {?Texture}
- * @default null
- */
- this.lightMap = null;
-
- /**
- * Intensity of the baked light.
- *
- * @type {number}
- * @default 1
- */
- this.lightMapIntensity = 1.0;
-
- /**
- * The red channel of this texture is used as the ambient occlusion map.
- * Requires a second set of UVs.
- *
- * `aoMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.aoMap = null;
-
- /**
- * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`
- * disables ambient occlusion. Where intensity is `1` and the AO map's
- * red channel is also `1`, ambient light is fully occluded on a surface.
- *
- * @type {number}
- * @default 1
- */
- this.aoMapIntensity = 1.0;
-
- /**
- * Specular map used by the material.
- *
- * `specularMap` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `specularMap` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.specularMap = null;
-
- /**
- * The alpha map is a grayscale texture that controls the opacity across the
- * surface (black: fully transparent; white: fully opaque).
- *
- * Only the color of the texture is used, ignoring the alpha channel if one
- * exists. For RGB and RGBA textures, the renderer will use the green channel
- * when sampling this texture due to the extra bit of precision provided for
- * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
- * luminance/alpha textures will also still work as expected.
- *
- * `alphaMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.alphaMap = null;
-
- /**
- * The environment map.
- *
- * `envMap` represents luminance data, and the texture must be assigned
- * a {@link Texture#colorSpace}. Most `envMap` textures set
- * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats
- * such as `.exr` or `.hdr`.
- *
- * @type {?Texture}
- * @default null
- */
- this.envMap = null;
-
- /**
- * The rotation of the environment map in radians.
- *
- * @type {Euler}
- * @default (0,0,0)
- */
- this.envMapRotation = new Euler();
-
- /**
- * How to combine the result of the surface's color with the environment map, if any.
- *
- * When set to `MixOperation`, the {@link MeshBasicMaterial#reflectivity} is used to
- * blend between the two colors.
- *
- * @type {(MultiplyOperation|MixOperation|AddOperation)}
- * @default MultiplyOperation
- */
- this.combine = MultiplyOperation;
-
- /**
- * How much the environment map affects the surface.
- * The valid range is between `0` (no reflections) and `1` (full reflections).
- *
- * @type {number}
- * @default 1
- */
- this.reflectivity = 1;
-
- /**
- * The index of refraction (IOR) of air (approximately 1) divided by the
- * index of refraction of the material. It is used with environment mapping
- * modes {@link CubeRefractionMapping} and {@link EquirectangularRefractionMapping}.
- * The refraction ratio should not exceed `1`.
- *
- * @type {number}
- * @default 0.98
- */
- this.refractionRatio = 0.98;
-
- /**
- * Renders the geometry as a wireframe.
- *
- * @type {boolean}
- * @default false
- */
- this.wireframe = false;
-
- /**
- * Controls the thickness of the wireframe.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {number}
- * @default 1
- */
- this.wireframeLinewidth = 1;
-
- /**
- * Defines appearance of wireframe ends.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.wireframeLinecap = 'round';
-
- /**
- * Defines appearance of wireframe joints.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.wireframeLinejoin = 'round';
-
- /**
- * Whether the material is affected by fog or not.
- *
- * @type {boolean}
- * @default true
- */
- this.fog = true;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.color.copy( source.color );
-
- this.map = source.map;
-
- this.lightMap = source.lightMap;
- this.lightMapIntensity = source.lightMapIntensity;
-
- this.aoMap = source.aoMap;
- this.aoMapIntensity = source.aoMapIntensity;
-
- this.specularMap = source.specularMap;
-
- this.alphaMap = source.alphaMap;
-
- this.envMap = source.envMap;
- this.envMapRotation.copy( source.envMapRotation );
- this.combine = source.combine;
- this.reflectivity = source.reflectivity;
- this.refractionRatio = source.refractionRatio;
-
- this.wireframe = source.wireframe;
- this.wireframeLinewidth = source.wireframeLinewidth;
- this.wireframeLinecap = source.wireframeLinecap;
- this.wireframeLinejoin = source.wireframeLinejoin;
-
- this.fog = source.fog;
-
- return this;
-
- }
-
-}
-
-const _inverseMatrix$3 = /*@__PURE__*/ new Matrix4();
-const _ray$3 = /*@__PURE__*/ new Ray();
-const _sphere$6 = /*@__PURE__*/ new Sphere();
-const _sphereHitAt = /*@__PURE__*/ new Vector3();
-
-const _vA = /*@__PURE__*/ new Vector3();
-const _vB = /*@__PURE__*/ new Vector3();
-const _vC = /*@__PURE__*/ new Vector3();
-
-const _tempA = /*@__PURE__*/ new Vector3();
-const _morphA = /*@__PURE__*/ new Vector3();
-
-const _intersectionPoint = /*@__PURE__*/ new Vector3();
-const _intersectionPointWorld = /*@__PURE__*/ new Vector3();
-
-/**
- * Class representing triangular polygon mesh based objects.
- *
- * ```js
- * const geometry = new THREE.BoxGeometry( 1, 1, 1 );
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const mesh = new THREE.Mesh( geometry, material );
- * scene.add( mesh );
- * ```
- *
- * @augments Object3D
- */
-class Mesh extends Object3D {
-
- /**
- * Constructs a new mesh.
- *
- * @param {BufferGeometry} [geometry] - The mesh geometry.
- * @param {Material|Array} [material] - The mesh material.
- */
- constructor( geometry = new BufferGeometry(), material = new MeshBasicMaterial() ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMesh = true;
-
- this.type = 'Mesh';
-
- /**
- * The mesh geometry.
- *
- * @type {BufferGeometry}
- */
- this.geometry = geometry;
-
- /**
- * The mesh material.
- *
- * @type {Material|Array}
- * @default MeshBasicMaterial
- */
- this.material = material;
-
- /**
- * A dictionary representing the morph targets in the geometry. The key is the
- * morph targets name, the value its attribute index. This member is `undefined`
- * by default and only set when morph targets are detected in the geometry.
- *
- * @type {Object|undefined}
- * @default undefined
- */
- this.morphTargetDictionary = undefined;
-
- /**
- * An array of weights typically in the range `[0,1]` that specify how much of the morph
- * is applied. This member is `undefined` by default and only set when morph targets are
- * detected in the geometry.
- *
- * @type {Array|undefined}
- * @default undefined
- */
- this.morphTargetInfluences = undefined;
-
- /**
- * The number of instances of this mesh.
- * Can only be used with {@link WebGPURenderer}.
- *
- * @type {number}
- * @default 1
- */
- this.count = 1;
-
- this.updateMorphTargets();
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- if ( source.morphTargetInfluences !== undefined ) {
-
- this.morphTargetInfluences = source.morphTargetInfluences.slice();
-
- }
-
- if ( source.morphTargetDictionary !== undefined ) {
-
- this.morphTargetDictionary = Object.assign( {}, source.morphTargetDictionary );
-
- }
-
- this.material = Array.isArray( source.material ) ? source.material.slice() : source.material;
- this.geometry = source.geometry;
-
- return this;
-
- }
-
- /**
- * Sets the values of {@link Mesh#morphTargetDictionary} and {@link Mesh#morphTargetInfluences}
- * to make sure existing morph targets can influence this 3D object.
- */
- updateMorphTargets() {
-
- const geometry = this.geometry;
-
- const morphAttributes = geometry.morphAttributes;
- const keys = Object.keys( morphAttributes );
-
- if ( keys.length > 0 ) {
-
- const morphAttribute = morphAttributes[ keys[ 0 ] ];
-
- if ( morphAttribute !== undefined ) {
-
- this.morphTargetInfluences = [];
- this.morphTargetDictionary = {};
-
- for ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {
-
- const name = morphAttribute[ m ].name || String( m );
-
- this.morphTargetInfluences.push( 0 );
- this.morphTargetDictionary[ name ] = m;
-
- }
-
- }
-
- }
-
- }
-
- /**
- * Returns the local-space position of the vertex at the given index, taking into
- * account the current animation state of both morph targets and skinning.
- *
- * @param {number} index - The vertex index.
- * @param {Vector3} target - The target object that is used to store the method's result.
- * @return {Vector3} The vertex position in local space.
- */
- getVertexPosition( index, target ) {
-
- const geometry = this.geometry;
- const position = geometry.attributes.position;
- const morphPosition = geometry.morphAttributes.position;
- const morphTargetsRelative = geometry.morphTargetsRelative;
-
- target.fromBufferAttribute( position, index );
-
- const morphInfluences = this.morphTargetInfluences;
-
- if ( morphPosition && morphInfluences ) {
-
- _morphA.set( 0, 0, 0 );
-
- for ( let i = 0, il = morphPosition.length; i < il; i ++ ) {
-
- const influence = morphInfluences[ i ];
- const morphAttribute = morphPosition[ i ];
-
- if ( influence === 0 ) continue;
-
- _tempA.fromBufferAttribute( morphAttribute, index );
-
- if ( morphTargetsRelative ) {
-
- _morphA.addScaledVector( _tempA, influence );
-
- } else {
-
- _morphA.addScaledVector( _tempA.sub( target ), influence );
-
- }
-
- }
-
- target.add( _morphA );
-
- }
-
- return target;
-
- }
-
- /**
- * Returns `true` if this mesh intersects the given frustum.
- *
- * @param {Frustum|FrustumArray} frustum - The frustum to test.
- * @return {boolean} Whether this mesh intersects the given frustum or not.
- */
- intersectsFrustum( frustum ) {
-
- return frustum.intersectsObject( this );
-
- }
-
- /**
- * Computes intersection points between a casted ray and this line.
- *
- * @param {Raycaster} raycaster - The raycaster.
- * @param {Array} intersects - The target array that holds the intersection points.
- */
- raycast( raycaster, intersects ) {
-
- const geometry = this.geometry;
- const material = this.material;
- const matrixWorld = this.matrixWorld;
-
- if ( material === undefined ) return;
-
- // test with bounding sphere in world space
-
- if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
-
- _sphere$6.copy( geometry.boundingSphere );
- _sphere$6.applyMatrix4( matrixWorld );
-
- // check distance from ray origin to bounding sphere
-
- _ray$3.copy( raycaster.ray ).recast( raycaster.near );
-
- if ( _sphere$6.containsPoint( _ray$3.origin ) === false ) {
-
- if ( _ray$3.intersectSphere( _sphere$6, _sphereHitAt ) === null ) return;
-
- if ( _ray$3.origin.distanceToSquared( _sphereHitAt ) > ( raycaster.far - raycaster.near ) ** 2 ) return;
-
- }
-
- // convert ray to local space of mesh
-
- _inverseMatrix$3.copy( matrixWorld ).invert();
- _ray$3.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$3 );
-
- // test with bounding box in local space
-
- if ( geometry.boundingBox !== null ) {
-
- if ( _ray$3.intersectsBox( geometry.boundingBox ) === false ) return;
-
- }
-
- // test for intersections with geometry
-
- this._computeIntersections( raycaster, intersects, _ray$3 );
-
- }
-
- _computeIntersections( raycaster, intersects, rayLocalSpace ) {
-
- let intersection;
-
- const geometry = this.geometry;
- const material = this.material;
-
- const index = geometry.index;
- const position = geometry.attributes.position;
- const uv = geometry.attributes.uv;
- const uv1 = geometry.attributes.uv1;
- const normal = geometry.attributes.normal;
- const groups = geometry.groups;
- const drawRange = geometry.drawRange;
-
- if ( index !== null ) {
-
- // indexed buffer geometry
-
- if ( Array.isArray( material ) ) {
-
- for ( let i = 0, il = groups.length; i < il; i ++ ) {
-
- const group = groups[ i ];
- const groupMaterial = material[ group.materialIndex ];
-
- const start = Math.max( group.start, drawRange.start );
- const end = Math.min( index.count, Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) ) );
-
- for ( let j = start, jl = end; j < jl; j += 3 ) {
-
- const a = index.getX( j );
- const b = index.getX( j + 1 );
- const c = index.getX( j + 2 );
-
- intersection = checkGeometryIntersection( this, groupMaterial, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );
-
- if ( intersection ) {
-
- intersection.faceIndex = Math.floor( j / 3 ); // triangle number in indexed buffer semantics
- intersection.face.materialIndex = group.materialIndex;
- intersects.push( intersection );
-
- }
-
- }
-
- }
-
- } else {
-
- const start = Math.max( 0, drawRange.start );
- const end = Math.min( index.count, ( drawRange.start + drawRange.count ) );
-
- for ( let i = start, il = end; i < il; i += 3 ) {
-
- const a = index.getX( i );
- const b = index.getX( i + 1 );
- const c = index.getX( i + 2 );
-
- intersection = checkGeometryIntersection( this, material, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );
-
- if ( intersection ) {
-
- intersection.faceIndex = Math.floor( i / 3 ); // triangle number in indexed buffer semantics
- intersects.push( intersection );
-
- }
-
- }
-
- }
-
- } else if ( position !== undefined ) {
-
- // non-indexed buffer geometry
-
- if ( Array.isArray( material ) ) {
-
- for ( let i = 0, il = groups.length; i < il; i ++ ) {
-
- const group = groups[ i ];
- const groupMaterial = material[ group.materialIndex ];
-
- const start = Math.max( group.start, drawRange.start );
- const end = Math.min( position.count, Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) ) );
-
- for ( let j = start, jl = end; j < jl; j += 3 ) {
-
- const a = j;
- const b = j + 1;
- const c = j + 2;
-
- intersection = checkGeometryIntersection( this, groupMaterial, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );
-
- if ( intersection ) {
-
- intersection.faceIndex = Math.floor( j / 3 ); // triangle number in non-indexed buffer semantics
- intersection.face.materialIndex = group.materialIndex;
- intersects.push( intersection );
-
- }
-
- }
-
- }
-
- } else {
-
- const start = Math.max( 0, drawRange.start );
- const end = Math.min( position.count, ( drawRange.start + drawRange.count ) );
-
- for ( let i = start, il = end; i < il; i += 3 ) {
-
- const a = i;
- const b = i + 1;
- const c = i + 2;
-
- intersection = checkGeometryIntersection( this, material, raycaster, rayLocalSpace, uv, uv1, normal, a, b, c );
-
- if ( intersection ) {
-
- intersection.faceIndex = Math.floor( i / 3 ); // triangle number in non-indexed buffer semantics
- intersects.push( intersection );
-
- }
-
- }
-
- }
-
- }
-
- }
-
-}
-
-function checkIntersection$1( object, material, raycaster, ray, pA, pB, pC, point ) {
-
- let intersect;
-
- if ( material.side === BackSide ) {
-
- intersect = ray.intersectTriangle( pC, pB, pA, true, point );
-
- } else {
-
- intersect = ray.intersectTriangle( pA, pB, pC, ( material.side === FrontSide ), point );
-
- }
-
- if ( intersect === null ) return null;
-
- _intersectionPointWorld.copy( point );
- _intersectionPointWorld.applyMatrix4( object.matrixWorld );
-
- const distance = raycaster.ray.origin.distanceTo( _intersectionPointWorld );
-
- if ( distance < raycaster.near || distance > raycaster.far ) return null;
-
- return {
- distance: distance,
- point: _intersectionPointWorld.clone(),
- object: object
- };
-
-}
-
-function checkGeometryIntersection( object, material, raycaster, ray, uv, uv1, normal, a, b, c ) {
-
- object.getVertexPosition( a, _vA );
- object.getVertexPosition( b, _vB );
- object.getVertexPosition( c, _vC );
-
- const intersection = checkIntersection$1( object, material, raycaster, ray, _vA, _vB, _vC, _intersectionPoint );
-
- if ( intersection ) {
-
- const barycoord = new Vector3();
- Triangle.getBarycoord( _intersectionPoint, _vA, _vB, _vC, barycoord );
-
- if ( uv ) {
-
- intersection.uv = Triangle.getInterpolatedAttribute( uv, a, b, c, barycoord, new Vector2() );
-
- }
-
- if ( uv1 ) {
-
- intersection.uv1 = Triangle.getInterpolatedAttribute( uv1, a, b, c, barycoord, new Vector2() );
-
- }
-
- if ( normal ) {
-
- intersection.normal = Triangle.getInterpolatedAttribute( normal, a, b, c, barycoord, new Vector3() );
-
- if ( intersection.normal.dot( ray.direction ) > 0 ) {
-
- intersection.normal.multiplyScalar( -1 );
-
- }
-
- }
-
- const face = {
- a: a,
- b: b,
- c: c,
- normal: new Vector3(),
- materialIndex: 0
- };
-
- Triangle.getNormal( _vA, _vB, _vC, face.normal );
-
- intersection.face = face;
- intersection.barycoord = barycoord;
-
- }
-
- return intersection;
-
-}
-
-const _baseVector = /*@__PURE__*/ new Vector4();
-
-const _skinIndex = /*@__PURE__*/ new Vector4();
-const _skinWeight = /*@__PURE__*/ new Vector4();
-
-const _vector4 = /*@__PURE__*/ new Vector4();
-const _matrix4 = /*@__PURE__*/ new Matrix4();
-const _vertex = /*@__PURE__*/ new Vector3();
-
-const _sphere$5 = /*@__PURE__*/ new Sphere();
-const _inverseMatrix$2 = /*@__PURE__*/ new Matrix4();
-const _ray$2 = /*@__PURE__*/ new Ray();
-
-/**
- * A mesh that has a {@link Skeleton} that can then be used to animate the
- * vertices of the geometry with skinning/skeleton animation.
- *
- * Next to a valid skeleton, the skinned mesh requires skin indices and weights
- * as buffer attributes in its geometry. These attribute define which bones affect a single
- * vertex to a certain extend.
- *
- * Typically skinned meshes are not created manually but loaders like {@link GLTFLoader}
- * or {@link FBXLoader } import respective models.
- *
- * @augments Mesh
- * @demo scenes/bones-browser.html
- */
-class SkinnedMesh extends Mesh {
-
- /**
- * Constructs a new skinned mesh.
- *
- * @param {BufferGeometry} [geometry] - The mesh geometry.
- * @param {Material|Array} [material] - The mesh material.
- */
- constructor( geometry, material ) {
-
- super( geometry, material );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isSkinnedMesh = true;
-
- this.type = 'SkinnedMesh';
-
- /**
- * `AttachedBindMode` means the skinned mesh shares the same world space as the skeleton.
- * This is not true when using `DetachedBindMode` which is useful when sharing a skeleton
- * across multiple skinned meshes.
- *
- * @type {(AttachedBindMode|DetachedBindMode)}
- * @default AttachedBindMode
- */
- this.bindMode = AttachedBindMode;
-
- /**
- * The base matrix that is used for the bound bone transforms.
- *
- * @type {Matrix4}
- */
- this.bindMatrix = new Matrix4();
-
- /**
- * The base matrix that is used for resetting the bound bone transforms.
- *
- * @type {Matrix4}
- */
- this.bindMatrixInverse = new Matrix4();
-
- /**
- * The bounding box of the skinned mesh. Can be computed via {@link SkinnedMesh#computeBoundingBox}.
- *
- * @type {?Box3}
- * @default null
- */
- this.boundingBox = null;
-
- /**
- * The bounding sphere of the skinned mesh. Can be computed via {@link SkinnedMesh#computeBoundingSphere}.
- *
- * @type {?Sphere}
- * @default null
- */
- this.boundingSphere = null;
-
- }
-
- /**
- * Computes the bounding box of the skinned mesh, and updates {@link SkinnedMesh#boundingBox}.
- * The bounding box is not automatically computed by the engine; this method must be called by your app.
- * If the skinned mesh is animated, the bounding box should be recomputed per frame in order to reflect
- * the current animation state.
- */
- computeBoundingBox() {
-
- const geometry = this.geometry;
-
- if ( this.boundingBox === null ) {
-
- this.boundingBox = new Box3();
-
- }
-
- this.boundingBox.makeEmpty();
-
- const positionAttribute = geometry.getAttribute( 'position' );
-
- for ( let i = 0; i < positionAttribute.count; i ++ ) {
-
- this.getVertexPosition( i, _vertex );
- this.boundingBox.expandByPoint( _vertex );
-
- }
-
- }
-
- /**
- * Computes the bounding sphere of the skinned mesh, and updates {@link SkinnedMesh#boundingSphere}.
- * The bounding sphere is automatically computed by the engine once when it is needed, e.g., for ray casting
- * and view frustum culling. If the skinned mesh is animated, the bounding sphere should be recomputed
- * per frame in order to reflect the current animation state.
- */
- computeBoundingSphere() {
-
- const geometry = this.geometry;
-
- if ( this.boundingSphere === null ) {
-
- this.boundingSphere = new Sphere();
-
- }
-
- this.boundingSphere.makeEmpty();
-
- const positionAttribute = geometry.getAttribute( 'position' );
-
- for ( let i = 0; i < positionAttribute.count; i ++ ) {
-
- this.getVertexPosition( i, _vertex );
- this.boundingSphere.expandByPoint( _vertex );
-
- }
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.bindMode = source.bindMode;
- this.bindMatrix.copy( source.bindMatrix );
- this.bindMatrixInverse.copy( source.bindMatrixInverse );
-
- this.skeleton = source.skeleton;
-
- if ( source.boundingBox !== null ) this.boundingBox = source.boundingBox.clone();
- if ( source.boundingSphere !== null ) this.boundingSphere = source.boundingSphere.clone();
-
- return this;
-
- }
-
- raycast( raycaster, intersects ) {
-
- const material = this.material;
- const matrixWorld = this.matrixWorld;
-
- if ( material === undefined ) return;
-
- // test with bounding sphere in world space
-
- if ( this.boundingSphere === null ) this.computeBoundingSphere();
-
- _sphere$5.copy( this.boundingSphere );
- _sphere$5.applyMatrix4( matrixWorld );
-
- if ( raycaster.ray.intersectsSphere( _sphere$5 ) === false ) return;
-
- // convert ray to local space of skinned mesh
-
- _inverseMatrix$2.copy( matrixWorld ).invert();
- _ray$2.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$2 );
-
- // test with bounding box in local space
-
- if ( this.boundingBox !== null ) {
-
- if ( _ray$2.intersectsBox( this.boundingBox ) === false ) return;
-
- }
-
- // test for intersections with geometry
-
- this._computeIntersections( raycaster, intersects, _ray$2 );
-
- }
-
- getVertexPosition( index, target ) {
-
- super.getVertexPosition( index, target );
-
- this.applyBoneTransform( index, target );
-
- return target;
-
- }
-
- /**
- * Binds the given skeleton to the skinned mesh.
- *
- * @param {Skeleton} skeleton - The skeleton to bind.
- * @param {Matrix4} [bindMatrix] - The bind matrix. If no bind matrix is provided,
- * the skinned mesh's world matrix will be used instead.
- */
- bind( skeleton, bindMatrix ) {
-
- this.skeleton = skeleton;
-
- if ( bindMatrix === undefined ) {
-
- this.updateMatrixWorld( true );
-
- this.skeleton.calculateInverses();
-
- bindMatrix = this.matrixWorld;
-
- }
-
- this.bindMatrix.copy( bindMatrix );
- this.bindMatrixInverse.copy( bindMatrix ).invert();
-
- }
-
- /**
- * This method sets the skinned mesh in the rest pose).
- */
- pose() {
-
- this.skeleton.pose();
-
- }
-
- /**
- * Normalizes the skin weights which are defined as a buffer attribute
- * in the skinned mesh's geometry.
- */
- normalizeSkinWeights() {
-
- const vector = new Vector4();
-
- const skinWeight = this.geometry.attributes.skinWeight;
-
- for ( let i = 0, l = skinWeight.count; i < l; i ++ ) {
-
- vector.fromBufferAttribute( skinWeight, i );
-
- const scale = 1.0 / vector.manhattanLength();
-
- if ( scale !== Infinity ) {
-
- vector.multiplyScalar( scale );
-
- } else {
-
- vector.set( 1, 0, 0, 0 ); // do something reasonable
-
- }
-
- skinWeight.setXYZW( i, vector.x, vector.y, vector.z, vector.w );
-
- }
-
- }
-
- updateMatrixWorld( force ) {
-
- super.updateMatrixWorld( force );
-
- if ( this.bindMode === AttachedBindMode ) {
-
- this.bindMatrixInverse.copy( this.matrixWorld ).invert();
-
- } else if ( this.bindMode === DetachedBindMode ) {
-
- this.bindMatrixInverse.copy( this.bindMatrix ).invert();
-
- } else {
-
- warn( 'SkinnedMesh: Unrecognized bindMode: ' + this.bindMode );
-
- }
-
- }
-
- /**
- * Applies the bone transform associated with the given index to the given
- * vector. Can be used to transform positions or direction vectors by providing
- * a Vector4 with 1 or 0 in the w component respectively. Returns the updated vector.
- *
- * @param {number} index - The vertex index.
- * @param {Vector3|Vector4} target - The target object that is used to store the method's result.
- * @return {Vector3|Vector4} The updated vertex attribute data.
- */
- applyBoneTransform( index, target ) {
-
- const skeleton = this.skeleton;
- const geometry = this.geometry;
-
- _skinIndex.fromBufferAttribute( geometry.attributes.skinIndex, index );
- _skinWeight.fromBufferAttribute( geometry.attributes.skinWeight, index );
-
- if ( target.isVector4 ) {
-
- _baseVector.copy( target );
- target.set( 0, 0, 0, 0 );
-
- } else {
-
- _baseVector.set( ...target, 1 );
- target.set( 0, 0, 0 );
-
- }
-
- _baseVector.applyMatrix4( this.bindMatrix );
-
- for ( let i = 0; i < 4; i ++ ) {
-
- const weight = _skinWeight.getComponent( i );
-
- if ( weight !== 0 ) {
-
- const boneIndex = _skinIndex.getComponent( i );
-
- _matrix4.multiplyMatrices( skeleton.bones[ boneIndex ].matrixWorld, skeleton.boneInverses[ boneIndex ] );
-
- target.addScaledVector( _vector4.copy( _baseVector ).applyMatrix4( _matrix4 ), weight );
-
- }
-
- }
-
- if ( target.isVector4 ) {
-
- // ensure the homogenous coordinate remains unchanged after vector operations
- target.w = _baseVector.w;
-
- }
-
- return target.applyMatrix4( this.bindMatrixInverse );
-
- }
-
-}
-
-/**
- * A bone which is part of a {@link Skeleton}. The skeleton in turn is used by
- * the {@link SkinnedMesh}.
- *
- * ```js
- * const root = new THREE.Bone();
- * const child = new THREE.Bone();
- *
- * root.add( child );
- * child.position.y = 5;
- * ```
- *
- * @augments Object3D
- */
-class Bone extends Object3D {
-
- /**
- * Constructs a new bone.
- */
- constructor() {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isBone = true;
-
- this.type = 'Bone';
-
- }
-
-}
-
-/**
- * Creates a texture directly from raw buffer data.
- *
- * The interpretation of the data depends on type and format: If the type is
- * `UnsignedByteType`, a `Uint8Array` will be useful for addressing the
- * texel data. If the format is `RGBAFormat`, data needs four values for
- * one texel; Red, Green, Blue and Alpha (typically the opacity).
- *
- * @augments Texture
- */
-class DataTexture extends Texture {
-
- /**
- * Constructs a new data texture.
- *
- * @param {?TypedArray} [data=null] - The buffer data.
- * @param {number} [width=1] - The width of the texture.
- * @param {number} [height=1] - The height of the texture.
- * @param {number} [format=RGBAFormat] - The texture format.
- * @param {number} [type=UnsignedByteType] - The texture type.
- * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
- * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
- * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
- * @param {number} [magFilter=NearestFilter] - The mag filter value.
- * @param {number} [minFilter=NearestFilter] - The min filter value.
- * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
- * @param {string} [colorSpace=NoColorSpace] - The color space.
- */
- constructor( data = null, width = 1, height = 1, format, type, mapping, wrapS, wrapT, magFilter = NearestFilter, minFilter = NearestFilter, anisotropy, colorSpace ) {
-
- super( null, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isDataTexture = true;
-
- /**
- * The image definition of a data texture.
- *
- * @type {{data:TypedArray,width:number,height:number}}
- */
- this.image = { data: data, width: width, height: height };
-
- /**
- * Whether to generate mipmaps (if possible) for a texture.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.generateMipmaps = false;
-
- /**
- * If set to `true`, the texture is flipped along the vertical axis when
- * uploaded to the GPU.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.flipY = false;
-
- /**
- * Specifies the alignment requirements for the start of each pixel row in memory.
- *
- * Overwritten and set to `1` by default.
- *
- * @type {boolean}
- * @default 1
- */
- this.unpackAlignment = 1;
-
- }
-
-}
-
-const _offsetMatrix = /*@__PURE__*/ new Matrix4();
-const _identityMatrix = /*@__PURE__*/ new Matrix4();
-
-/**
- * Class for representing the armatures in `three.js`. The skeleton
- * is defined by a hierarchy of bones.
- *
- * ```js
- * const bones = [];
- *
- * const shoulder = new THREE.Bone();
- * const elbow = new THREE.Bone();
- * const hand = new THREE.Bone();
- *
- * shoulder.add( elbow );
- * elbow.add( hand );
- *
- * bones.push( shoulder , elbow, hand);
- *
- * shoulder.position.y = -5;
- * elbow.position.y = 0;
- * hand.position.y = 5;
- *
- * const armSkeleton = new THREE.Skeleton( bones );
- * ```
- */
-class Skeleton {
-
- /**
- * Constructs a new skeleton.
- *
- * @param {Array} [bones] - An array of bones.
- * @param {Array} [boneInverses] - An array of bone inverse matrices.
- * If not provided, these matrices will be computed automatically via {@link Skeleton#calculateInverses}.
- */
- constructor( bones = [], boneInverses = [] ) {
-
- this.uuid = generateUUID();
-
- /**
- * An array of bones defining the skeleton.
- *
- * @type {Array}
- */
- this.bones = bones.slice( 0 );
-
- /**
- * An array of bone inverse matrices.
- *
- * @type {Array}
- */
- this.boneInverses = boneInverses;
-
- /**
- * An array buffer holding the bone data.
- * Input data for {@link Skeleton#boneTexture}.
- *
- * @type {?Float32Array}
- * @default null
- */
- this.boneMatrices = null;
-
- /**
- * A texture holding the bone data for use
- * in the vertex shader.
- *
- * @type {?DataTexture}
- * @default null
- */
- this.boneTexture = null;
-
- this.init();
-
- }
-
- /**
- * Initializes the skeleton. This method gets automatically called by the constructor
- * but depending on how the skeleton is created it might be necessary to call this method
- * manually.
- */
- init() {
-
- const bones = this.bones;
- const boneInverses = this.boneInverses;
-
- this.boneMatrices = new Float32Array( bones.length * 16 );
-
- // calculate inverse bone matrices if necessary
-
- if ( boneInverses.length === 0 ) {
-
- this.calculateInverses();
-
- } else {
-
- // handle special case
-
- if ( bones.length !== boneInverses.length ) {
-
- warn( 'Skeleton: Number of inverse bone matrices does not match amount of bones.' );
-
- this.boneInverses = [];
-
- for ( let i = 0, il = this.bones.length; i < il; i ++ ) {
-
- this.boneInverses.push( new Matrix4() );
-
- }
-
- }
-
- }
-
- }
-
- /**
- * Computes the bone inverse matrices. This method resets {@link Skeleton#boneInverses}
- * and fills it with new matrices.
- */
- calculateInverses() {
-
- this.boneInverses.length = 0;
-
- for ( let i = 0, il = this.bones.length; i < il; i ++ ) {
-
- const inverse = new Matrix4();
-
- if ( this.bones[ i ] ) {
-
- inverse.copy( this.bones[ i ].matrixWorld ).invert();
-
- }
-
- this.boneInverses.push( inverse );
-
- }
-
- }
-
- /**
- * Resets the skeleton to the base pose.
- */
- pose() {
-
- // recover the bind-time world matrices
-
- for ( let i = 0, il = this.bones.length; i < il; i ++ ) {
-
- const bone = this.bones[ i ];
-
- if ( bone ) {
-
- bone.matrixWorld.copy( this.boneInverses[ i ] ).invert();
-
- }
-
- }
-
- // compute the local matrices, positions, rotations and scales
-
- for ( let i = 0, il = this.bones.length; i < il; i ++ ) {
-
- const bone = this.bones[ i ];
-
- if ( bone ) {
-
- if ( bone.parent && bone.parent.isBone ) {
-
- bone.matrix.copy( bone.parent.matrixWorld ).invert();
- bone.matrix.multiply( bone.matrixWorld );
-
- } else {
-
- bone.matrix.copy( bone.matrixWorld );
-
- }
-
- bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
-
- }
-
- }
-
- }
-
- /**
- * Resets the skeleton to the base pose.
- */
- update() {
-
- const bones = this.bones;
- const boneInverses = this.boneInverses;
- const boneMatrices = this.boneMatrices;
- const boneTexture = this.boneTexture;
-
- // flatten bone matrices to array
-
- for ( let i = 0, il = bones.length; i < il; i ++ ) {
-
- // compute the offset between the current and the original transform
-
- const matrix = bones[ i ] ? bones[ i ].matrixWorld : _identityMatrix;
-
- _offsetMatrix.multiplyMatrices( matrix, boneInverses[ i ] );
- _offsetMatrix.toArray( boneMatrices, i * 16 );
-
- }
-
- if ( boneTexture !== null ) {
-
- boneTexture.needsUpdate = true;
-
- }
-
- }
-
- /**
- * Returns a new skeleton with copied values from this instance.
- *
- * @return {Skeleton} A clone of this instance.
- */
- clone() {
-
- return new Skeleton( this.bones, this.boneInverses );
-
- }
-
- /**
- * Computes a data texture for passing bone data to the vertex shader.
- *
- * @return {Skeleton} A reference of this instance.
- */
- computeBoneTexture() {
-
- // layout (1 matrix = 4 pixels)
- // RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
- // with 8x8 pixel texture max 16 bones * 4 pixels = (8 * 8)
- // 16x16 pixel texture max 64 bones * 4 pixels = (16 * 16)
- // 32x32 pixel texture max 256 bones * 4 pixels = (32 * 32)
- // 64x64 pixel texture max 1024 bones * 4 pixels = (64 * 64)
-
- let size = Math.sqrt( this.bones.length * 4 ); // 4 pixels needed for 1 matrix
- size = Math.ceil( size / 4 ) * 4;
- size = Math.max( size, 4 );
-
- const boneMatrices = new Float32Array( size * size * 4 ); // 4 floats per RGBA pixel
- boneMatrices.set( this.boneMatrices ); // copy current values
-
- const boneTexture = new DataTexture( boneMatrices, size, size, RGBAFormat, FloatType );
- boneTexture.needsUpdate = true;
-
- this.boneMatrices = boneMatrices;
- this.boneTexture = boneTexture;
-
- return this;
-
- }
-
- /**
- * Searches through the skeleton's bone array and returns the first with a
- * matching name.
- *
- * @param {string} name - The name of the bone.
- * @return {Bone|undefined} The found bone. `undefined` if no bone has been found.
- */
- getBoneByName( name ) {
-
- for ( let i = 0, il = this.bones.length; i < il; i ++ ) {
-
- const bone = this.bones[ i ];
-
- if ( bone.name === name ) {
-
- return bone;
-
- }
-
- }
-
- return undefined;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose( ) {
-
- if ( this.boneTexture !== null ) {
-
- this.boneTexture.dispose();
-
- this.boneTexture = null;
-
- }
-
- }
-
- /**
- * Setups the skeleton by the given JSON and bones.
- *
- * @param {Object} json - The skeleton as serialized JSON.
- * @param {Object} bones - An array of bones.
- * @return {Skeleton} A reference of this instance.
- */
- fromJSON( json, bones ) {
-
- this.uuid = json.uuid;
-
- for ( let i = 0, l = json.bones.length; i < l; i ++ ) {
-
- const uuid = json.bones[ i ];
- let bone = bones[ uuid ];
-
- if ( bone === undefined ) {
-
- warn( 'Skeleton: No bone found with UUID:', uuid );
- bone = new Bone();
-
- }
-
- this.bones.push( bone );
- this.boneInverses.push( new Matrix4().fromArray( json.boneInverses[ i ] ) );
-
- }
-
- this.init();
-
- return this;
-
- }
-
- /**
- * Serializes the skeleton into JSON.
- *
- * @return {Object} A JSON object representing the serialized skeleton.
- * @see {@link ObjectLoader#parse}
- */
- toJSON() {
-
- const data = {
- metadata: {
- version: 4.7,
- type: 'Skeleton',
- generator: 'Skeleton.toJSON'
- },
- bones: [],
- boneInverses: []
- };
-
- data.uuid = this.uuid;
-
- const bones = this.bones;
- const boneInverses = this.boneInverses;
-
- for ( let i = 0, l = bones.length; i < l; i ++ ) {
-
- const bone = bones[ i ];
- data.bones.push( bone.uuid );
-
- const boneInverse = boneInverses[ i ];
- data.boneInverses.push( boneInverse.toArray() );
-
- }
-
- return data;
-
- }
-
-}
-
-/**
- * An instanced version of a buffer attribute.
- *
- * @augments BufferAttribute
- */
-class InstancedBufferAttribute extends BufferAttribute {
-
- /**
- * Constructs a new instanced buffer attribute.
- *
- * @param {TypedArray} array - The array holding the attribute data.
- * @param {number} itemSize - The item size.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- * @param {number} [meshPerAttribute=1] - How often a value of this buffer attribute should be repeated.
- */
- constructor( array, itemSize, normalized, meshPerAttribute = 1 ) {
-
- super( array, itemSize, normalized );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isInstancedBufferAttribute = true;
-
- /**
- * Defines how often a value of this buffer attribute should be repeated. A
- * value of one means that each value of the instanced attribute is used for
- * a single instance. A value of two means that each value is used for two
- * consecutive instances (and so on).
- *
- * @type {number}
- * @default 1
- */
- this.meshPerAttribute = meshPerAttribute;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.meshPerAttribute = source.meshPerAttribute;
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.meshPerAttribute = this.meshPerAttribute;
-
- data.isInstancedBufferAttribute = true;
-
- return data;
-
- }
-
-}
-
-const _instanceLocalMatrix = /*@__PURE__*/ new Matrix4();
-const _instanceWorldMatrix = /*@__PURE__*/ new Matrix4();
-
-const _instanceIntersects = [];
-
-const _box3 = /*@__PURE__*/ new Box3();
-const _identity = /*@__PURE__*/ new Matrix4();
-const _mesh$1 = /*@__PURE__*/ new Mesh();
-const _sphere$4 = /*@__PURE__*/ new Sphere();
-
-/**
- * A special version of a mesh with instanced rendering support. Use
- * this class if you have to render a large number of objects with the same
- * geometry and material(s) but with different world transformations. The usage
- * of 'InstancedMesh' will help you to reduce the number of draw calls and thus
- * improve the overall rendering performance in your application.
- *
- * @augments Mesh
- */
-class InstancedMesh extends Mesh {
-
- /**
- * Constructs a new instanced mesh.
- *
- * @param {BufferGeometry} [geometry] - The mesh geometry.
- * @param {Material|Array} [material] - The mesh material.
- * @param {number} count - The number of instances.
- */
- constructor( geometry, material, count ) {
-
- super( geometry, material );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isInstancedMesh = true;
-
- /**
- * Represents the local transformation of all instances. You have to set its
- * {@link BufferAttribute#needsUpdate} flag to true if you modify instanced data
- * via {@link InstancedMesh#setMatrixAt}.
- *
- * @type {InstancedBufferAttribute}
- */
- this.instanceMatrix = new InstancedBufferAttribute( new Float32Array( count * 16 ), 16 );
-
- /**
- * Represents the color of all instances. You have to set its
- * {@link BufferAttribute#needsUpdate} flag to true if you modify instanced data
- * via {@link InstancedMesh#setColorAt}.
- *
- * @type {?InstancedBufferAttribute}
- * @default null
- */
- this.instanceColor = null;
-
- /**
- * Represents the morph target weights of all instances. You have to set its
- * {@link Texture#needsUpdate} flag to true if you modify instanced data
- * via {@link InstancedMesh#setMorphAt}.
- *
- * @type {?DataTexture}
- * @default null
- */
- this.morphTexture = null;
-
- /**
- * The number of instances.
- *
- * @type {number}
- */
- this.count = count;
-
- /**
- * The bounding box of the instanced mesh. Can be computed via {@link InstancedMesh#computeBoundingBox}.
- *
- * @type {?Box3}
- * @default null
- */
- this.boundingBox = null;
-
- /**
- * The bounding sphere of the instanced mesh. Can be computed via {@link InstancedMesh#computeBoundingSphere}.
- *
- * @type {?Sphere}
- * @default null
- */
- this.boundingSphere = null;
-
- for ( let i = 0; i < count; i ++ ) {
-
- this.setMatrixAt( i, _identity );
-
- }
-
- }
-
- /**
- * Computes the bounding box of the instanced mesh, and updates {@link InstancedMesh#boundingBox}.
- * The bounding box is not automatically computed by the engine; this method must be called by your app.
- * You may need to recompute the bounding box if an instance is transformed via {@link InstancedMesh#setMatrixAt}.
- */
- computeBoundingBox() {
-
- const geometry = this.geometry;
- const count = this.count;
-
- if ( this.boundingBox === null ) {
-
- this.boundingBox = new Box3();
-
- }
-
- if ( geometry.boundingBox === null ) {
-
- geometry.computeBoundingBox();
-
- }
-
- this.boundingBox.makeEmpty();
-
- for ( let i = 0; i < count; i ++ ) {
-
- this.getMatrixAt( i, _instanceLocalMatrix );
-
- _box3.copy( geometry.boundingBox ).applyMatrix4( _instanceLocalMatrix );
-
- this.boundingBox.union( _box3 );
-
- }
-
- }
-
- /**
- * Computes the bounding sphere of the instanced mesh, and updates {@link InstancedMesh#boundingSphere}
- * The engine automatically computes the bounding sphere when it is needed, e.g., for ray casting or view frustum culling.
- * You may need to recompute the bounding sphere if an instance is transformed via {@link InstancedMesh#setMatrixAt}.
- */
- computeBoundingSphere() {
-
- const geometry = this.geometry;
- const count = this.count;
-
- if ( this.boundingSphere === null ) {
-
- this.boundingSphere = new Sphere();
-
- }
-
- if ( geometry.boundingSphere === null ) {
-
- geometry.computeBoundingSphere();
-
- }
-
- this.boundingSphere.makeEmpty();
-
- for ( let i = 0; i < count; i ++ ) {
-
- this.getMatrixAt( i, _instanceLocalMatrix );
-
- _sphere$4.copy( geometry.boundingSphere ).applyMatrix4( _instanceLocalMatrix );
-
- this.boundingSphere.union( _sphere$4 );
-
- }
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.instanceMatrix.copy( source.instanceMatrix );
-
- if ( source.morphTexture !== null ) this.morphTexture = source.morphTexture.clone();
- if ( source.instanceColor !== null ) this.instanceColor = source.instanceColor.clone();
-
- this.count = source.count;
-
- if ( source.boundingBox !== null ) this.boundingBox = source.boundingBox.clone();
- if ( source.boundingSphere !== null ) this.boundingSphere = source.boundingSphere.clone();
-
- return this;
-
- }
-
- /**
- * Gets the color of the defined instance.
- *
- * @param {number} index - The instance index.
- * @param {Color} color - The target object that is used to store the method's result.
- * @return {Color} A reference to the target color.
- */
- getColorAt( index, color ) {
-
- if ( this.instanceColor === null ) {
-
- return color.setRGB( 1, 1, 1 );
-
- } else {
-
- return color.fromArray( this.instanceColor.array, index * 3 );
-
- }
-
- }
-
- /**
- * Gets the local transformation matrix of the defined instance.
- *
- * @param {number} index - The instance index.
- * @param {Matrix4} matrix - The target object that is used to store the method's result.
- * @return {Matrix4} A reference to the target matrix.
- */
- getMatrixAt( index, matrix ) {
-
- return matrix.fromArray( this.instanceMatrix.array, index * 16 );
-
- }
-
- /**
- * Gets the morph target weights of the defined instance.
- *
- * @param {number} index - The instance index.
- * @param {Mesh} object - The target object that is used to store the method's result.
- */
- getMorphAt( index, object ) {
-
- const objectInfluences = object.morphTargetInfluences;
-
- const array = this.morphTexture.source.data.data;
-
- const len = objectInfluences.length + 1; // All influences + the baseInfluenceSum
-
- const dataIndex = index * len + 1; // Skip the baseInfluenceSum at the beginning
-
- for ( let i = 0; i < objectInfluences.length; i ++ ) {
-
- objectInfluences[ i ] = array[ dataIndex + i ];
-
- }
-
- }
-
- raycast( raycaster, intersects ) {
-
- const matrixWorld = this.matrixWorld;
- const raycastTimes = this.count;
-
- _mesh$1.geometry = this.geometry;
- _mesh$1.material = this.material;
-
- if ( _mesh$1.material === undefined ) return;
-
- // test with bounding sphere first
-
- if ( this.boundingSphere === null ) this.computeBoundingSphere();
-
- _sphere$4.copy( this.boundingSphere );
- _sphere$4.applyMatrix4( matrixWorld );
-
- if ( raycaster.ray.intersectsSphere( _sphere$4 ) === false ) return;
-
- // now test each instance
-
- for ( let instanceId = 0; instanceId < raycastTimes; instanceId ++ ) {
-
- // calculate the world matrix for each instance
-
- this.getMatrixAt( instanceId, _instanceLocalMatrix );
-
- _instanceWorldMatrix.multiplyMatrices( matrixWorld, _instanceLocalMatrix );
-
- // the mesh represents this single instance
-
- _mesh$1.matrixWorld = _instanceWorldMatrix;
-
- _mesh$1.raycast( raycaster, _instanceIntersects );
-
- // process the result of raycast
-
- for ( let i = 0, l = _instanceIntersects.length; i < l; i ++ ) {
-
- const intersect = _instanceIntersects[ i ];
- intersect.instanceId = instanceId;
- intersect.object = this;
- intersects.push( intersect );
-
- }
-
- _instanceIntersects.length = 0;
-
- }
-
- }
-
- /**
- * Sets the given color to the defined instance. Make sure you set the `needsUpdate` flag of
- * {@link InstancedMesh#instanceColor} to `true` after updating all the colors.
- *
- * @param {number} index - The instance index.
- * @param {Color} color - The instance color.
- * @return {InstancedMesh} A reference to this instanced mesh.
- */
- setColorAt( index, color ) {
-
- if ( this.instanceColor === null ) {
-
- this.instanceColor = new InstancedBufferAttribute( new Float32Array( this.instanceMatrix.count * 3 ).fill( 1 ), 3 );
-
- }
-
- color.toArray( this.instanceColor.array, index * 3 );
- return this;
-
- }
-
- /**
- * Sets the given local transformation matrix to the defined instance. Make sure you set the `needsUpdate` flag of
- * {@link InstancedMesh#instanceMatrix} to `true` after updating all the matrices.
- *
- * @param {number} index - The instance index.
- * @param {Matrix4} matrix - The local transformation.
- * @return {InstancedMesh} A reference to this instanced mesh.
- */
- setMatrixAt( index, matrix ) {
-
- matrix.toArray( this.instanceMatrix.array, index * 16 );
- return this;
-
- }
-
- /**
- * Sets the morph target weights to the defined instance. Make sure you set the `needsUpdate` flag of
- * {@link InstancedMesh#morphTexture} to `true` after updating all the influences.
- *
- * @param {number} index - The instance index.
- * @param {Mesh} object - A mesh which `morphTargetInfluences` property containing the morph target weights
- * of a single instance.
- * @return {InstancedMesh} A reference to this instanced mesh.
- */
- setMorphAt( index, object ) {
-
- const objectInfluences = object.morphTargetInfluences;
-
- const len = objectInfluences.length + 1; // morphBaseInfluence + all influences
-
- if ( this.morphTexture === null ) {
-
- this.morphTexture = new DataTexture( new Float32Array( len * this.count ), len, this.count, RedFormat, FloatType );
-
- }
-
- const array = this.morphTexture.source.data.data;
-
- let morphInfluencesSum = 0;
-
- for ( let i = 0; i < objectInfluences.length; i ++ ) {
-
- morphInfluencesSum += objectInfluences[ i ];
-
- }
-
- const morphBaseInfluence = this.geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;
-
- const dataIndex = len * index;
-
- array[ dataIndex ] = morphBaseInfluence;
-
- array.set( objectInfluences, dataIndex + 1 );
- return this;
-
- }
-
- updateMorphTargets() {
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- if ( this.morphTexture !== null ) {
-
- this.morphTexture.dispose();
- this.morphTexture = null;
-
- }
-
- }
-
-}
-
-const _sphere$3 = /*@__PURE__*/ new Sphere();
-const _defaultSpriteCenter = /*@__PURE__*/ new Vector2( 0.5, 0.5 );
-const _vector$6 = /*@__PURE__*/ new Vector3();
-
-/**
- * Frustums are used to determine what is inside the camera's field of view.
- * They help speed up the rendering process - objects which lie outside a camera's
- * frustum can safely be excluded from rendering.
- *
- * This class is mainly intended for use internally by a renderer.
- */
-class Frustum {
-
- /**
- * Constructs a new frustum.
- *
- * @param {Plane} [p0] - The first plane that encloses the frustum.
- * @param {Plane} [p1] - The second plane that encloses the frustum.
- * @param {Plane} [p2] - The third plane that encloses the frustum.
- * @param {Plane} [p3] - The fourth plane that encloses the frustum.
- * @param {Plane} [p4] - The fifth plane that encloses the frustum.
- * @param {Plane} [p5] - The sixth plane that encloses the frustum.
- */
- constructor( p0 = new Plane(), p1 = new Plane(), p2 = new Plane(), p3 = new Plane(), p4 = new Plane(), p5 = new Plane() ) {
-
- /**
- * This array holds the planes that enclose the frustum.
- *
- * @type {Array}
- */
- this.planes = [ p0, p1, p2, p3, p4, p5 ];
-
- }
-
- /**
- * Sets the frustum planes by copying the given planes.
- *
- * @param {Plane} [p0] - The first plane that encloses the frustum.
- * @param {Plane} [p1] - The second plane that encloses the frustum.
- * @param {Plane} [p2] - The third plane that encloses the frustum.
- * @param {Plane} [p3] - The fourth plane that encloses the frustum.
- * @param {Plane} [p4] - The fifth plane that encloses the frustum.
- * @param {Plane} [p5] - The sixth plane that encloses the frustum.
- * @return {Frustum} A reference to this frustum.
- */
- set( p0, p1, p2, p3, p4, p5 ) {
-
- const planes = this.planes;
-
- planes[ 0 ].copy( p0 );
- planes[ 1 ].copy( p1 );
- planes[ 2 ].copy( p2 );
- planes[ 3 ].copy( p3 );
- planes[ 4 ].copy( p4 );
- planes[ 5 ].copy( p5 );
-
- return this;
-
- }
-
- /**
- * Copies the values of the given frustum to this instance.
- *
- * @param {Frustum} frustum - The frustum to copy.
- * @return {Frustum} A reference to this frustum.
- */
- copy( frustum ) {
-
- const planes = this.planes;
-
- for ( let i = 0; i < 6; i ++ ) {
-
- planes[ i ].copy( frustum.planes[ i ] );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the frustum planes from the given projection matrix.
- *
- * @param {Matrix4} m - The projection matrix.
- * @param {(WebGLCoordinateSystem|WebGPUCoordinateSystem)} coordinateSystem - The coordinate system.
- * @param {boolean} [reversedDepth=false] - Whether to use a reversed depth.
- * @return {Frustum} A reference to this frustum.
- */
- setFromProjectionMatrix( m, coordinateSystem = WebGLCoordinateSystem, reversedDepth = false ) {
-
- const planes = this.planes;
- const me = m.elements;
- const me0 = me[ 0 ], me1 = me[ 1 ], me2 = me[ 2 ], me3 = me[ 3 ];
- const me4 = me[ 4 ], me5 = me[ 5 ], me6 = me[ 6 ], me7 = me[ 7 ];
- const me8 = me[ 8 ], me9 = me[ 9 ], me10 = me[ 10 ], me11 = me[ 11 ];
- const me12 = me[ 12 ], me13 = me[ 13 ], me14 = me[ 14 ], me15 = me[ 15 ];
-
- planes[ 0 ].setComponents( me3 - me0, me7 - me4, me11 - me8, me15 - me12 ).normalize();
- planes[ 1 ].setComponents( me3 + me0, me7 + me4, me11 + me8, me15 + me12 ).normalize();
- planes[ 2 ].setComponents( me3 + me1, me7 + me5, me11 + me9, me15 + me13 ).normalize();
- planes[ 3 ].setComponents( me3 - me1, me7 - me5, me11 - me9, me15 - me13 ).normalize();
-
- if ( reversedDepth ) {
-
- planes[ 4 ].setComponents( me2, me6, me10, me14 ).normalize(); // far
- planes[ 5 ].setComponents( me3 - me2, me7 - me6, me11 - me10, me15 - me14 ).normalize(); // near
-
- } else {
-
- planes[ 4 ].setComponents( me3 - me2, me7 - me6, me11 - me10, me15 - me14 ).normalize(); // far
-
- if ( coordinateSystem === WebGLCoordinateSystem ) {
-
- planes[ 5 ].setComponents( me3 + me2, me7 + me6, me11 + me10, me15 + me14 ).normalize(); // near
-
- } else if ( coordinateSystem === WebGPUCoordinateSystem ) {
-
- planes[ 5 ].setComponents( me2, me6, me10, me14 ).normalize(); // near
-
- } else {
-
- throw new Error( 'THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: ' + coordinateSystem );
-
- }
-
- }
-
- return this;
-
- }
-
- /**
- * Returns `true` if the 3D object's bounding sphere is intersecting this frustum.
- *
- * Note that the 3D object must have a geometry so that the bounding sphere can be calculated.
- *
- * @param {Object3D} object - The 3D object to test.
- * @return {boolean} Whether the 3D object's bounding sphere is intersecting this frustum or not.
- */
- intersectsObject( object ) {
-
- if ( object.boundingSphere !== undefined ) {
-
- if ( object.boundingSphere === null ) object.computeBoundingSphere();
-
- _sphere$3.copy( object.boundingSphere ).applyMatrix4( object.matrixWorld );
-
- } else {
-
- const geometry = object.geometry;
-
- if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
-
- _sphere$3.copy( geometry.boundingSphere ).applyMatrix4( object.matrixWorld );
-
- }
-
- return this.intersectsSphere( _sphere$3 );
-
- }
-
- /**
- * Returns `true` if the given sprite is intersecting this frustum.
- *
- * @param {Sprite} sprite - The sprite to test.
- * @return {boolean} Whether the sprite is intersecting this frustum or not.
- */
- intersectsSprite( sprite ) {
-
- _sphere$3.center.set( 0, 0, 0 );
-
- const offset = _defaultSpriteCenter.distanceTo( sprite.center );
-
- _sphere$3.radius = 0.7071067811865476 + offset;
- _sphere$3.applyMatrix4( sprite.matrixWorld );
-
- return this.intersectsSphere( _sphere$3 );
-
- }
-
- /**
- * Returns `true` if the given bounding sphere is intersecting this frustum.
- *
- * This is a fast, conservative test that favors performance over precision. It can
- * report false positives for spheres that lie outside the frustum but are not separated
- * by a single frustum plane. It never reports false negatives, so it is safe for culling.
- *
- * @param {Sphere} sphere - The bounding sphere to test.
- * @return {boolean} Whether the bounding sphere is intersecting this frustum or not.
- */
- intersectsSphere( sphere ) {
-
- const planes = this.planes;
- const center = sphere.center;
- const negRadius = - sphere.radius;
-
- for ( let i = 0; i < 6; i ++ ) {
-
- const distance = planes[ i ].distanceToPoint( center );
-
- if ( distance < negRadius ) {
-
- return false;
-
- }
-
- }
-
- return true;
-
- }
-
- /**
- * Returns `true` if the given bounding box is intersecting this frustum.
- *
- * This is a fast, conservative test that favors performance over precision. It can
- * report false positives for large boxes that lie outside the frustum but are not
- * separated by a single frustum plane. It never reports false negatives, so it is
- * safe for culling.
- *
- * @param {Box3} box - The bounding box to test.
- * @return {boolean} Whether the bounding box is intersecting this frustum or not.
- */
- intersectsBox( box ) {
-
- const planes = this.planes;
-
- for ( let i = 0; i < 6; i ++ ) {
-
- const plane = planes[ i ];
-
- // corner at max distance
-
- _vector$6.x = plane.normal.x > 0 ? box.max.x : box.min.x;
- _vector$6.y = plane.normal.y > 0 ? box.max.y : box.min.y;
- _vector$6.z = plane.normal.z > 0 ? box.max.z : box.min.z;
-
- if ( plane.distanceToPoint( _vector$6 ) < 0 ) {
-
- return false;
-
- }
-
- }
-
- return true;
-
- }
-
- /**
- * Returns `true` if the given point lies within the frustum.
- *
- * @param {Vector3} point - The point to test.
- * @return {boolean} Whether the point lies within this frustum or not.
- */
- containsPoint( point ) {
-
- const planes = this.planes;
-
- for ( let i = 0; i < 6; i ++ ) {
-
- if ( planes[ i ].distanceToPoint( point ) < 0 ) {
-
- return false;
-
- }
-
- }
-
- return true;
-
- }
-
- /**
- * Returns a new frustum with copied values from this instance.
- *
- * @return {Frustum} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
-}
-
-const _projScreenMatrix$1 = /*@__PURE__*/ new Matrix4();
-
-/**
- * FrustumArray is used to determine if an object is visible in at least one camera
- * from an array of cameras. This is particularly useful for multi-view renderers.
-*/
-class FrustumArray {
-
- /**
- * Constructs a new frustum array.
- *
- */
- constructor() {
-
- /**
- * The coordinate system to use.
- *
- * @type {WebGLCoordinateSystem|WebGPUCoordinateSystem}
- * @default WebGLCoordinateSystem
- */
- this.coordinateSystem = WebGLCoordinateSystem;
-
- /**
- * A pool of frustum instances. It may hold more entries than are
- * currently in use; surplus instances are kept for reuse to avoid
- * reallocating when array cameras of different lengths are rendered.
- *
- * @private
- * @type {Array}
- */
- this._frustums = [];
-
- /**
- * The number of frustums in {@link FrustumArray#_frustums} that are currently
- * in use.
- *
- * @private
- * @type {number}
- * @default 0
- */
- this._count = 0;
-
- }
-
- /**
- * Computes and caches a frustum for each camera of the given array camera.
- *
- * @param {ArrayCamera} cameraArray - The array camera whose sub-cameras define the frustums.
- * @return {FrustumArray} A reference to this frustum array.
- */
- setFromArrayCamera( cameraArray ) {
-
- const cameras = cameraArray.cameras;
- const frustums = this._frustums;
-
- for ( let i = 0; i < cameras.length; i ++ ) {
-
- const camera = cameras[ i ];
-
- _projScreenMatrix$1.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
-
- if ( frustums[ i ] === undefined ) frustums[ i ] = new Frustum();
-
- frustums[ i ].setFromProjectionMatrix( _projScreenMatrix$1, camera.coordinateSystem, camera.reversedDepth );
-
- }
-
- this._count = cameras.length;
-
- return this;
-
- }
-
- /**
- * Returns `true` if the 3D object's bounding sphere is intersecting any cached frustum.
- *
- * {@link FrustumArray#setFromArrayCamera} must be called once per render before this method.
- *
- * @param {Object3D} object - The 3D object to test.
- * @return {boolean} Whether the 3D object is visible in any camera.
- */
- intersectsObject( object ) {
-
- const frustums = this._frustums;
-
- for ( let i = 0; i < this._count; i ++ ) {
-
- if ( frustums[ i ].intersectsObject( object ) ) return true;
-
- }
-
- return false;
-
- }
-
- /**
- * Returns `true` if the given sprite is intersecting any cached frustum.
- *
- * {@link FrustumArray#setFromArrayCamera} must be called once per render before this method.
- *
- * @param {Sprite} sprite - The sprite to test.
- * @return {boolean} Whether the sprite is visible in any camera.
- */
- intersectsSprite( sprite ) {
-
- const frustums = this._frustums;
-
- for ( let i = 0; i < this._count; i ++ ) {
-
- if ( frustums[ i ].intersectsSprite( sprite ) ) return true;
-
- }
-
- return false;
-
- }
-
- /**
- * Returns `true` if the given bounding sphere is intersecting any cached frustum.
- *
- * {@link FrustumArray#setFromArrayCamera} must be called once per render before this method.
- *
- * @param {Sphere} sphere - The bounding sphere to test.
- * @return {boolean} Whether the sphere is visible in any camera.
- */
- intersectsSphere( sphere ) {
-
- const frustums = this._frustums;
-
- for ( let i = 0; i < this._count; i ++ ) {
-
- if ( frustums[ i ].intersectsSphere( sphere ) ) return true;
-
- }
-
- return false;
-
- }
-
- /**
- * Returns `true` if the given bounding box is intersecting any cached frustum.
- *
- * {@link FrustumArray#setFromArrayCamera} must be called once per render before this method.
- *
- * @param {Box3} box - The bounding box to test.
- * @return {boolean} Whether the box is visible in any camera.
- */
- intersectsBox( box ) {
-
- const frustums = this._frustums;
-
- for ( let i = 0; i < this._count; i ++ ) {
-
- if ( frustums[ i ].intersectsBox( box ) ) return true;
-
- }
-
- return false;
-
- }
-
- /**
- * Returns `true` if the given point lies within any cached frustum.
- *
- * {@link FrustumArray#setFromArrayCamera} must be called once per render before this method.
- *
- * @param {Vector3} point - The point to test.
- * @return {boolean} Whether the point is visible in any camera.
- */
- containsPoint( point ) {
-
- const frustums = this._frustums;
-
- for ( let i = 0; i < this._count; i ++ ) {
-
- if ( frustums[ i ].containsPoint( point ) ) return true;
-
- }
-
- return false;
-
- }
-
- /**
- * Copies the values of the given frustum array to this instance.
- *
- * @param {FrustumArray} source - The frustum array to copy.
- * @return {FrustumArray} A reference to this frustum array.
- */
- copy( source ) {
-
- this.coordinateSystem = source.coordinateSystem;
-
- const frustums = this._frustums;
- const sourceFrustums = source._frustums;
-
- for ( let i = 0; i < source._count; i ++ ) {
-
- if ( frustums[ i ] === undefined ) frustums[ i ] = new Frustum();
-
- frustums[ i ].copy( sourceFrustums[ i ] );
-
- }
-
- this._count = source._count;
-
- return this;
-
- }
-
- /**
- * Returns a new frustum array with copied values from this instance.
- *
- * @return {FrustumArray} A clone of this instance.
- */
- clone() {
-
- return new FrustumArray().copy( this );
-
- }
-
-}
-
-function ascIdSort( a, b ) {
-
- return a - b;
-
-}
-
-function sortOpaque( a, b ) {
-
- return a.z - b.z;
-
-}
-
-function sortTransparent( a, b ) {
-
- return b.z - a.z;
-
-}
-
-class MultiDrawRenderList {
-
- constructor() {
-
- this.index = 0;
- this.pool = [];
- this.list = [];
-
- }
-
- push( start, count, z, index ) {
-
- const pool = this.pool;
- const list = this.list;
- if ( this.index >= pool.length ) {
-
- pool.push( {
-
- start: -1,
- count: -1,
- z: -1,
- index: -1,
-
- } );
-
- }
-
- const item = pool[ this.index ];
- list.push( item );
- this.index ++;
-
- item.start = start;
- item.count = count;
- item.z = z;
- item.index = index;
-
- }
-
- reset() {
-
- this.list.length = 0;
- this.index = 0;
-
- }
-
-}
-
-const _matrix$1 = /*@__PURE__*/ new Matrix4();
-const _whiteColor = /*@__PURE__*/ new Color( 1, 1, 1 );
-const _frustum = /*@__PURE__*/ new Frustum();
-const _frustumArray = /*@__PURE__*/ new FrustumArray();
-const _box$1 = /*@__PURE__*/ new Box3();
-const _sphere$2 = /*@__PURE__*/ new Sphere();
-const _vector$5 = /*@__PURE__*/ new Vector3();
-const _forward$1 = /*@__PURE__*/ new Vector3();
-const _temp = /*@__PURE__*/ new Vector3();
-const _renderList = /*@__PURE__*/ new MultiDrawRenderList();
-const _mesh = /*@__PURE__*/ new Mesh();
-const _batchIntersects = [];
-
-// copies data from attribute "src" into "target" starting at "targetOffset"
-function copyAttributeData( src, target, targetOffset = 0 ) {
-
- const itemSize = target.itemSize;
- if ( src.isInterleavedBufferAttribute || src.array.constructor !== target.array.constructor ) {
-
- // use the component getters and setters if the array data cannot
- // be copied directly
- const vertexCount = src.count;
- for ( let i = 0; i < vertexCount; i ++ ) {
-
- for ( let c = 0; c < itemSize; c ++ ) {
-
- target.setComponent( i + targetOffset, c, src.getComponent( i, c ) );
-
- }
-
- }
-
- } else {
-
- // faster copy approach using typed array set function
- target.array.set( src.array, targetOffset * itemSize );
-
- }
-
- target.needsUpdate = true;
-
-}
-
-// safely copies array contents to a potentially smaller array
-function copyArrayContents( src, target ) {
-
- if ( src.constructor !== target.constructor ) {
-
- // if arrays are of a different type (eg due to index size increasing) then data must be per-element copied
- const len = Math.min( src.length, target.length );
- for ( let i = 0; i < len; i ++ ) {
-
- target[ i ] = src[ i ];
-
- }
-
- } else {
-
- // if the arrays use the same data layout we can use a fast block copy
- const len = Math.min( src.length, target.length );
- target.set( new src.constructor( src.buffer, 0, len ) );
-
- }
-
-}
-
-/**
- * A special version of a mesh with multi draw batch rendering support. Use
- * this class if you have to render a large number of objects with the same
- * material but with different geometries or world transformations. The usage of
- * `BatchedMesh` will help you to reduce the number of draw calls and thus improve the overall
- * rendering performance in your application.
- *
- * ```js
- * const box = new THREE.BoxGeometry( 1, 1, 1 );
- * const sphere = new THREE.SphereGeometry( 1, 12, 12 );
- * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
- *
- * // initialize and add geometries into the batched mesh
- * const batchedMesh = new BatchedMesh( 10, 5000, 10000, material );
- * const boxGeometryId = batchedMesh.addGeometry( box );
- * const sphereGeometryId = batchedMesh.addGeometry( sphere );
- *
- * // create instances of those geometries
- * const boxInstancedId1 = batchedMesh.addInstance( boxGeometryId );
- * const boxInstancedId2 = batchedMesh.addInstance( boxGeometryId );
- *
- * const sphereInstancedId1 = batchedMesh.addInstance( sphereGeometryId );
- * const sphereInstancedId2 = batchedMesh.addInstance( sphereGeometryId );
- *
- * // position the geometries
- * batchedMesh.setMatrixAt( boxInstancedId1, boxMatrix1 );
- * batchedMesh.setMatrixAt( boxInstancedId2, boxMatrix2 );
- *
- * batchedMesh.setMatrixAt( sphereInstancedId1, sphereMatrix1 );
- * batchedMesh.setMatrixAt( sphereInstancedId2, sphereMatrix2 );
- *
- * scene.add( batchedMesh );
- * ```
- *
- * @augments Mesh
- */
-class BatchedMesh extends Mesh {
-
- /**
- * Constructs a new batched mesh.
- *
- * @param {number} maxInstanceCount - The maximum number of individual instances planned to be added and rendered.
- * @param {number} maxVertexCount - The maximum number of vertices to be used by all unique geometries.
- * @param {number} [maxIndexCount=maxVertexCount*2] - The maximum number of indices to be used by all unique geometries
- * @param {Material|Array} [material] - The mesh material.
- */
- constructor( maxInstanceCount, maxVertexCount, maxIndexCount = maxVertexCount * 2, material ) {
-
- super( new BufferGeometry(), material );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isBatchedMesh = true;
-
- /**
- * When set ot `true`, the individual objects of a batch are frustum culled.
- *
- * @type {boolean}
- * @default true
- */
- this.perObjectFrustumCulled = true;
-
- /**
- * When set to `true`, the individual objects of a batch are sorted to improve overdraw-related artifacts.
- * If the material is marked as "transparent" objects are rendered back to front and if not then they are
- * rendered front to back.
- *
- * @type {boolean}
- * @default true
- */
- this.sortObjects = true;
-
- /**
- * The bounding box of the batched mesh. Can be computed via {@link BatchedMesh#computeBoundingBox}.
- *
- * @type {?Box3}
- * @default null
- */
- this.boundingBox = null;
-
- /**
- * The bounding sphere of the batched mesh. Can be computed via {@link BatchedMesh#computeBoundingSphere}.
- *
- * @type {?Sphere}
- * @default null
- */
- this.boundingSphere = null;
-
- /**
- * Takes a sort a function that is run before render. The function takes a list of instances to
- * sort and a camera. The objects in the list include a "z" field to perform a depth-ordered
- * sort with.
- *
- * @type {?Function}
- * @default null
- */
- this.customSort = null;
-
- // stores visible, active, and geometry id per instance and reserved buffer ranges for geometries
- this._instanceInfo = [];
- this._geometryInfo = [];
-
- // instance, geometry ids that have been set as inactive, and are available to be overwritten
- this._availableInstanceIds = [];
- this._availableGeometryIds = [];
-
- // used to track where the next point is that geometry should be inserted
- this._nextIndexStart = 0;
- this._nextVertexStart = 0;
- this._geometryCount = 0;
-
- // flags
- this._visibilityChanged = true;
- this._geometryInitialized = false;
-
- // cached user options
- this._maxInstanceCount = maxInstanceCount;
- this._maxVertexCount = maxVertexCount;
- this._maxIndexCount = maxIndexCount;
-
- // buffers for multi draw
- this._multiDrawCounts = new Int32Array( maxInstanceCount );
- this._multiDrawStarts = new Int32Array( maxInstanceCount );
- this._multiDrawCount = 0;
- this._multiDrawBytesPerElement = 1;
-
- // Local matrix per geometry by using data texture
- this._matricesTexture = null;
- this._indirectTexture = null;
- this._colorsTexture = null;
-
- this._initMatricesTexture();
- this._initIndirectTexture();
-
- }
-
- /**
- * The maximum number of individual instances that can be stored in the batch.
- *
- * @type {number}
- * @readonly
- */
- get maxInstanceCount() {
-
- return this._maxInstanceCount;
-
- }
-
- /**
- * The instance count.
- *
- * @type {number}
- * @readonly
- */
- get instanceCount() {
-
- return this._instanceInfo.length - this._availableInstanceIds.length;
-
- }
-
- /**
- * The number of unused vertices.
- *
- * @type {number}
- * @readonly
- */
- get unusedVertexCount() {
-
- return this._maxVertexCount - this._nextVertexStart;
-
- }
-
- /**
- * The number of unused indices.
- *
- * @type {number}
- * @readonly
- */
- get unusedIndexCount() {
-
- return this._maxIndexCount - this._nextIndexStart;
-
- }
-
- _initMatricesTexture() {
-
- // layout (1 matrix = 4 pixels)
- // RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
- // with 8x8 pixel texture max 16 matrices * 4 pixels = (8 * 8)
- // 16x16 pixel texture max 64 matrices * 4 pixels = (16 * 16)
- // 32x32 pixel texture max 256 matrices * 4 pixels = (32 * 32)
- // 64x64 pixel texture max 1024 matrices * 4 pixels = (64 * 64)
-
- let size = Math.sqrt( this._maxInstanceCount * 4 ); // 4 pixels needed for 1 matrix
- size = Math.ceil( size / 4 ) * 4;
- size = Math.max( size, 4 );
-
- const matricesArray = new Float32Array( size * size * 4 ); // 4 floats per RGBA pixel
- const matricesTexture = new DataTexture( matricesArray, size, size, RGBAFormat, FloatType );
-
- this._matricesTexture = matricesTexture;
-
- }
-
- _initIndirectTexture() {
-
- let size = Math.sqrt( this._maxInstanceCount );
- size = Math.ceil( size );
-
- const indirectArray = new Uint32Array( size * size );
- const indirectTexture = new DataTexture( indirectArray, size, size, RedIntegerFormat, UnsignedIntType );
-
- this._indirectTexture = indirectTexture;
-
- }
-
- _initColorsTexture() {
-
- let size = Math.sqrt( this._maxInstanceCount );
- size = Math.ceil( size );
-
- // 4 floats per RGBA pixel initialized to white
- const colorsArray = new Float32Array( size * size * 4 ).fill( 1 );
- const colorsTexture = new DataTexture( colorsArray, size, size, RGBAFormat, FloatType );
- colorsTexture.colorSpace = ColorManagement.workingColorSpace;
-
- this._colorsTexture = colorsTexture;
-
- }
-
- _initializeGeometry( reference ) {
-
- const geometry = this.geometry;
- const maxVertexCount = this._maxVertexCount;
- const maxIndexCount = this._maxIndexCount;
- if ( this._geometryInitialized === false ) {
-
- for ( const attributeName in reference.attributes ) {
-
- const srcAttribute = reference.getAttribute( attributeName );
- const { array, itemSize, normalized } = srcAttribute;
-
- const dstArray = new array.constructor( maxVertexCount * itemSize );
- const dstAttribute = new BufferAttribute( dstArray, itemSize, normalized );
-
- geometry.setAttribute( attributeName, dstAttribute );
-
- }
-
- if ( reference.getIndex() !== null ) {
-
- // Reserve last u16 index for primitive restart.
- const indexArray = maxVertexCount > 65535
- ? new Uint32Array( maxIndexCount )
- : new Uint16Array( maxIndexCount );
-
- geometry.setIndex( new BufferAttribute( indexArray, 1 ) );
-
- }
-
- this._geometryInitialized = true;
-
- }
-
- }
-
- // Make sure the geometry is compatible with the existing combined geometry attributes
- _validateGeometry( geometry ) {
-
- // check to ensure the geometries are using consistent attributes and indices
- const batchGeometry = this.geometry;
- if ( Boolean( geometry.getIndex() ) !== Boolean( batchGeometry.getIndex() ) ) {
-
- throw new Error( 'THREE.BatchedMesh: All geometries must consistently have "index".' );
-
- }
-
- for ( const attributeName in batchGeometry.attributes ) {
-
- if ( ! geometry.hasAttribute( attributeName ) ) {
-
- throw new Error( `THREE.BatchedMesh: Added geometry missing "${ attributeName }". All geometries must have consistent attributes.` );
-
- }
-
- const srcAttribute = geometry.getAttribute( attributeName );
- const dstAttribute = batchGeometry.getAttribute( attributeName );
- if ( srcAttribute.itemSize !== dstAttribute.itemSize || srcAttribute.normalized !== dstAttribute.normalized ) {
-
- throw new Error( 'THREE.BatchedMesh: All attributes must have a consistent itemSize and normalized value.' );
-
- }
-
- }
-
- }
-
- /**
- * Validates the instance defined by the given ID.
- *
- * @param {number} instanceId - The instance to validate.
- */
- validateInstanceId( instanceId ) {
-
- const instanceInfo = this._instanceInfo;
- if ( instanceId < 0 || instanceId >= instanceInfo.length || instanceInfo[ instanceId ].active === false ) {
-
- throw new Error( `THREE.BatchedMesh: Invalid instanceId ${instanceId}. Instance is either out of range or has been deleted.` );
-
- }
-
- }
-
- /**
- * Validates the geometry defined by the given ID.
- *
- * @param {number} geometryId - The geometry to validate.
- */
- validateGeometryId( geometryId ) {
-
- const geometryInfoList = this._geometryInfo;
- if ( geometryId < 0 || geometryId >= geometryInfoList.length || geometryInfoList[ geometryId ].active === false ) {
-
- throw new Error( `THREE.BatchedMesh: Invalid geometryId ${geometryId}. Geometry is either out of range or has been deleted.` );
-
- }
-
- }
-
- /**
- * Takes a sort a function that is run before render. The function takes a list of instances to
- * sort and a camera. The objects in the list include a "z" field to perform a depth-ordered sort with.
- *
- * @param {Function} func - The custom sort function.
- * @return {BatchedMesh} A reference to this batched mesh.
- */
- setCustomSort( func ) {
-
- this.customSort = func;
- return this;
-
- }
-
- /**
- * Computes the bounding box, updating {@link BatchedMesh#boundingBox}.
- * Bounding boxes aren't computed by default. They need to be explicitly computed,
- * otherwise they are `null`.
- */
- computeBoundingBox() {
-
- if ( this.boundingBox === null ) {
-
- this.boundingBox = new Box3();
-
- }
-
- const boundingBox = this.boundingBox;
- const instanceInfo = this._instanceInfo;
-
- boundingBox.makeEmpty();
- for ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {
-
- if ( instanceInfo[ i ].active === false ) continue;
-
- const geometryId = instanceInfo[ i ].geometryIndex;
- this.getMatrixAt( i, _matrix$1 );
- this.getBoundingBoxAt( geometryId, _box$1 ).applyMatrix4( _matrix$1 );
- boundingBox.union( _box$1 );
-
- }
-
- }
-
- /**
- * Computes the bounding sphere, updating {@link BatchedMesh#boundingSphere}.
- * Bounding spheres aren't computed by default. They need to be explicitly computed,
- * otherwise they are `null`.
- */
- computeBoundingSphere() {
-
- if ( this.boundingSphere === null ) {
-
- this.boundingSphere = new Sphere();
-
- }
-
- const boundingSphere = this.boundingSphere;
- const instanceInfo = this._instanceInfo;
-
- boundingSphere.makeEmpty();
- for ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {
-
- if ( instanceInfo[ i ].active === false ) continue;
-
- const geometryId = instanceInfo[ i ].geometryIndex;
- this.getMatrixAt( i, _matrix$1 );
- this.getBoundingSphereAt( geometryId, _sphere$2 ).applyMatrix4( _matrix$1 );
- boundingSphere.union( _sphere$2 );
-
- }
-
- }
-
- /**
- * Adds a new instance to the batch using the geometry of the given ID and returns
- * a new id referring to the new instance to be used by other functions.
- *
- * @param {number} geometryId - The ID of a previously added geometry via {@link BatchedMesh#addGeometry}.
- * @return {number} The instance ID.
- */
- addInstance( geometryId ) {
-
- const atCapacity = this._instanceInfo.length >= this.maxInstanceCount;
-
- // ensure we're not over geometry
- if ( atCapacity && this._availableInstanceIds.length === 0 ) {
-
- throw new Error( 'THREE.BatchedMesh: Maximum item count reached.' );
-
- }
-
- const instanceInfo = {
- visible: true,
- active: true,
- geometryIndex: geometryId,
- };
-
- let drawId = null;
-
- // Prioritize using previously freed instance ids
- if ( this._availableInstanceIds.length > 0 ) {
-
- this._availableInstanceIds.sort( ascIdSort );
-
- drawId = this._availableInstanceIds.shift();
- this._instanceInfo[ drawId ] = instanceInfo;
-
- } else {
-
- drawId = this._instanceInfo.length;
- this._instanceInfo.push( instanceInfo );
-
- }
-
- const matricesTexture = this._matricesTexture;
- _matrix$1.identity().toArray( matricesTexture.image.data, drawId * 16 );
- matricesTexture.needsUpdate = true;
-
- const colorsTexture = this._colorsTexture;
- if ( colorsTexture ) {
-
- _whiteColor.toArray( colorsTexture.image.data, drawId * 4 );
- colorsTexture.needsUpdate = true;
-
- }
-
- this._visibilityChanged = true;
- return drawId;
-
- }
-
- /**
- * Adds the given geometry to the batch and returns the associated
- * geometry id referring to it to be used in other functions.
- *
- * @param {BufferGeometry} geometry - The geometry to add.
- * @param {number} [reservedVertexCount=-1] - Optional parameter specifying the amount of
- * vertex buffer space to reserve for the added geometry. This is necessary if it is planned
- * to set a new geometry at this index at a later time that is larger than the original geometry.
- * Defaults to the length of the given geometry vertex buffer.
- * @param {number} [reservedIndexCount=-1] - Optional parameter specifying the amount of index
- * buffer space to reserve for the added geometry. This is necessary if it is planned to set a
- * new geometry at this index at a later time that is larger than the original geometry. Defaults to
- * the length of the given geometry index buffer.
- * @return {number} The geometry ID.
- */
- addGeometry( geometry, reservedVertexCount = -1, reservedIndexCount = -1 ) {
-
- this._initializeGeometry( geometry );
-
- this._validateGeometry( geometry );
-
- const geometryInfo = {
- // geometry information
- vertexStart: -1,
- vertexCount: -1,
- reservedVertexCount: -1,
-
- indexStart: -1,
- indexCount: -1,
- reservedIndexCount: -1,
-
- // draw range information
- start: -1,
- count: -1,
-
- // state
- boundingBox: null,
- boundingSphere: null,
- active: true,
- };
-
- const geometryInfoList = this._geometryInfo;
- geometryInfo.vertexStart = this._nextVertexStart;
- geometryInfo.reservedVertexCount = reservedVertexCount === -1 ? geometry.getAttribute( 'position' ).count : reservedVertexCount;
-
- const index = geometry.getIndex();
- const hasIndex = index !== null;
- if ( hasIndex ) {
-
- geometryInfo.indexStart = this._nextIndexStart;
- geometryInfo.reservedIndexCount = reservedIndexCount === -1 ? index.count : reservedIndexCount;
-
- }
-
- if (
- geometryInfo.indexStart !== -1 &&
- geometryInfo.indexStart + geometryInfo.reservedIndexCount > this._maxIndexCount ||
- geometryInfo.vertexStart + geometryInfo.reservedVertexCount > this._maxVertexCount
- ) {
-
- throw new Error( 'THREE.BatchedMesh: Reserved space request exceeds the maximum buffer size.' );
-
- }
-
- // update id
- let geometryId;
- if ( this._availableGeometryIds.length > 0 ) {
-
- this._availableGeometryIds.sort( ascIdSort );
-
- geometryId = this._availableGeometryIds.shift();
- geometryInfoList[ geometryId ] = geometryInfo;
-
-
- } else {
-
- geometryId = this._geometryCount;
- this._geometryCount ++;
- geometryInfoList.push( geometryInfo );
-
- }
-
- // update the geometry
- this.setGeometryAt( geometryId, geometry );
-
- // increment the next geometry position
- this._nextIndexStart = geometryInfo.indexStart + geometryInfo.reservedIndexCount;
- this._nextVertexStart = geometryInfo.vertexStart + geometryInfo.reservedVertexCount;
-
- return geometryId;
-
- }
-
- /**
- * Replaces the geometry at the given ID with the provided geometry. Throws an error if there
- * is not enough space reserved for geometry. Calling this will change all instances that are
- * rendering that geometry.
- *
- * @param {number} geometryId - The ID of the geometry that should be replaced with the given geometry.
- * @param {BufferGeometry} geometry - The new geometry.
- * @return {number} The geometry ID.
- */
- setGeometryAt( geometryId, geometry ) {
-
- if ( geometryId >= this._geometryCount ) {
-
- throw new Error( 'THREE.BatchedMesh: Maximum geometry count reached.' );
-
- }
-
- this._validateGeometry( geometry );
-
- const batchGeometry = this.geometry;
- const hasIndex = batchGeometry.getIndex() !== null;
- const dstIndex = batchGeometry.getIndex();
- const srcIndex = geometry.getIndex();
- const geometryInfo = this._geometryInfo[ geometryId ];
- if (
- hasIndex &&
- srcIndex.count > geometryInfo.reservedIndexCount ||
- geometry.attributes.position.count > geometryInfo.reservedVertexCount
- ) {
-
- throw new Error( 'THREE.BatchedMesh: Reserved space not large enough for provided geometry.' );
-
- }
-
- // copy geometry buffer data over
- const vertexStart = geometryInfo.vertexStart;
- const reservedVertexCount = geometryInfo.reservedVertexCount;
- geometryInfo.vertexCount = geometry.getAttribute( 'position' ).count;
-
- for ( const attributeName in batchGeometry.attributes ) {
-
- // copy attribute data
- const srcAttribute = geometry.getAttribute( attributeName );
- const dstAttribute = batchGeometry.getAttribute( attributeName );
- copyAttributeData( srcAttribute, dstAttribute, vertexStart );
-
- // fill the rest in with zeroes
- const itemSize = srcAttribute.itemSize;
- for ( let i = srcAttribute.count, l = reservedVertexCount; i < l; i ++ ) {
-
- const index = vertexStart + i;
- for ( let c = 0; c < itemSize; c ++ ) {
-
- dstAttribute.setComponent( index, c, 0 );
-
- }
-
- }
-
- dstAttribute.needsUpdate = true;
- dstAttribute.addUpdateRange( vertexStart * itemSize, reservedVertexCount * itemSize );
-
- }
-
- // copy index
- if ( hasIndex ) {
-
- const indexStart = geometryInfo.indexStart;
- const reservedIndexCount = geometryInfo.reservedIndexCount;
- geometryInfo.indexCount = geometry.getIndex().count;
-
- // copy index data over
- for ( let i = 0; i < srcIndex.count; i ++ ) {
-
- dstIndex.setX( indexStart + i, vertexStart + srcIndex.getX( i ) );
-
- }
-
- // fill the rest in with zeroes
- for ( let i = srcIndex.count, l = reservedIndexCount; i < l; i ++ ) {
-
- dstIndex.setX( indexStart + i, vertexStart );
-
- }
-
- dstIndex.needsUpdate = true;
- dstIndex.addUpdateRange( indexStart, geometryInfo.reservedIndexCount );
-
- }
-
- // update the draw range
- geometryInfo.start = hasIndex ? geometryInfo.indexStart : geometryInfo.vertexStart;
- geometryInfo.count = hasIndex ? geometryInfo.indexCount : geometryInfo.vertexCount;
-
- // store the bounding boxes
- geometryInfo.boundingBox = null;
- if ( geometry.boundingBox !== null ) {
-
- geometryInfo.boundingBox = geometry.boundingBox.clone();
-
- }
-
- geometryInfo.boundingSphere = null;
- if ( geometry.boundingSphere !== null ) {
-
- geometryInfo.boundingSphere = geometry.boundingSphere.clone();
-
- }
-
- this._visibilityChanged = true;
- return geometryId;
-
- }
-
- /**
- * Deletes the geometry defined by the given ID from this batch. Any instances referencing
- * this geometry will also be removed as a side effect.
- *
- * @param {number} geometryId - The ID of the geometry to remove from the batch.
- * @return {BatchedMesh} A reference to this batched mesh.
- */
- deleteGeometry( geometryId ) {
-
- const geometryInfoList = this._geometryInfo;
- if ( geometryId >= geometryInfoList.length || geometryInfoList[ geometryId ].active === false ) {
-
- return this;
-
- }
-
- // delete any instances associated with this geometry
- const instanceInfo = this._instanceInfo;
- for ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {
-
- if ( instanceInfo[ i ].active && instanceInfo[ i ].geometryIndex === geometryId ) {
-
- this.deleteInstance( i );
-
- }
-
- }
-
- geometryInfoList[ geometryId ].active = false;
- this._availableGeometryIds.push( geometryId );
- this._visibilityChanged = true;
-
- return this;
-
- }
-
- /**
- * Deletes an existing instance from the batch using the given ID.
- *
- * @param {number} instanceId - The ID of the instance to remove from the batch.
- * @return {BatchedMesh} A reference to this batched mesh.
- */
- deleteInstance( instanceId ) {
-
- this.validateInstanceId( instanceId );
-
- this._instanceInfo[ instanceId ].active = false;
- this._availableInstanceIds.push( instanceId );
- this._visibilityChanged = true;
-
- return this;
-
- }
-
- /**
- * Repacks the sub geometries in BatchedMesh to remove any unused space remaining from
- * previously deleted geometry, freeing up space to add new geometry.
- *
- * @return {BatchedMesh} A reference to this batched mesh.
- */
- optimize() {
-
- // track the next indices to copy data to
- let nextVertexStart = 0;
- let nextIndexStart = 0;
-
- // Iterate over all geometry ranges in order sorted from earliest in the geometry buffer to latest
- // in the geometry buffer. Because draw range objects can be reused there is no guarantee of their order.
- const geometryInfoList = this._geometryInfo;
- const indices = geometryInfoList
- .map( ( e, i ) => i )
- .sort( ( a, b ) => {
-
- return geometryInfoList[ a ].vertexStart - geometryInfoList[ b ].vertexStart;
-
- } );
-
- const geometry = this.geometry;
- for ( let i = 0, l = geometryInfoList.length; i < l; i ++ ) {
-
- // if a geometry range is inactive then don't copy anything
- const index = indices[ i ];
- const geometryInfo = geometryInfoList[ index ];
- if ( geometryInfo.active === false ) {
-
- continue;
-
- }
-
- // if a geometry contains an index buffer then shift it, as well
- if ( geometry.index !== null ) {
-
- if ( geometryInfo.indexStart !== nextIndexStart ) {
-
- const { indexStart, vertexStart, reservedIndexCount } = geometryInfo;
- const index = geometry.index;
- const array = index.array;
-
- // shift the index pointers based on how the vertex data will shift
- // adjusting the index must happen first so the original vertex start value is available
- const elementDelta = nextVertexStart - vertexStart;
- for ( let j = indexStart; j < indexStart + reservedIndexCount; j ++ ) {
-
- array[ j ] = array[ j ] + elementDelta;
-
- }
-
- index.array.copyWithin( nextIndexStart, indexStart, indexStart + reservedIndexCount );
- index.addUpdateRange( nextIndexStart, reservedIndexCount );
- index.needsUpdate = true;
-
- geometryInfo.indexStart = nextIndexStart;
-
- }
-
- nextIndexStart += geometryInfo.reservedIndexCount;
-
- }
-
- // if a geometry needs to be moved then copy attribute data to overwrite unused space
- if ( geometryInfo.vertexStart !== nextVertexStart ) {
-
- const { vertexStart, reservedVertexCount } = geometryInfo;
- const attributes = geometry.attributes;
- for ( const key in attributes ) {
-
- const attribute = attributes[ key ];
- const { array, itemSize } = attribute;
- array.copyWithin( nextVertexStart * itemSize, vertexStart * itemSize, ( vertexStart + reservedVertexCount ) * itemSize );
- attribute.addUpdateRange( nextVertexStart * itemSize, reservedVertexCount * itemSize );
- attribute.needsUpdate = true;
-
- }
-
- geometryInfo.vertexStart = nextVertexStart;
-
- }
-
- nextVertexStart += geometryInfo.reservedVertexCount;
- geometryInfo.start = geometry.index ? geometryInfo.indexStart : geometryInfo.vertexStart;
-
- }
-
- this._nextIndexStart = nextIndexStart;
- this._nextVertexStart = nextVertexStart;
- this._visibilityChanged = true;
-
- return this;
-
- }
-
- /**
- * Returns the bounding box for the given geometry.
- *
- * @param {number} geometryId - The ID of the geometry to return the bounding box for.
- * @param {Box3} target - The target object that is used to store the method's result.
- * @return {?Box3} The geometry's bounding box. Returns `null` if no geometry has been found for the given ID.
- */
- getBoundingBoxAt( geometryId, target ) {
-
- if ( geometryId >= this._geometryCount ) {
-
- return null;
-
- }
-
- // compute bounding box
- const geometry = this.geometry;
- const geometryInfo = this._geometryInfo[ geometryId ];
- if ( geometryInfo.boundingBox === null ) {
-
- const box = new Box3();
- const index = geometry.index;
- const position = geometry.attributes.position;
- for ( let i = geometryInfo.start, l = geometryInfo.start + geometryInfo.count; i < l; i ++ ) {
-
- let iv = i;
- if ( index ) {
-
- iv = index.getX( iv );
-
- }
-
- box.expandByPoint( _vector$5.fromBufferAttribute( position, iv ) );
-
- }
-
- geometryInfo.boundingBox = box;
-
- }
-
- target.copy( geometryInfo.boundingBox );
- return target;
-
- }
-
- /**
- * Returns the bounding sphere for the given geometry.
- *
- * @param {number} geometryId - The ID of the geometry to return the bounding sphere for.
- * @param {Sphere} target - The target object that is used to store the method's result.
- * @return {?Sphere} The geometry's bounding sphere. Returns `null` if no geometry has been found for the given ID.
- */
- getBoundingSphereAt( geometryId, target ) {
-
- if ( geometryId >= this._geometryCount ) {
-
- return null;
-
- }
-
- // compute bounding sphere
- const geometry = this.geometry;
- const geometryInfo = this._geometryInfo[ geometryId ];
- if ( geometryInfo.boundingSphere === null ) {
-
- const sphere = new Sphere();
- this.getBoundingBoxAt( geometryId, _box$1 );
- _box$1.getCenter( sphere.center );
-
- const index = geometry.index;
- const position = geometry.attributes.position;
-
- let maxRadiusSq = 0;
- for ( let i = geometryInfo.start, l = geometryInfo.start + geometryInfo.count; i < l; i ++ ) {
-
- let iv = i;
- if ( index ) {
-
- iv = index.getX( iv );
-
- }
-
- _vector$5.fromBufferAttribute( position, iv );
- maxRadiusSq = Math.max( maxRadiusSq, sphere.center.distanceToSquared( _vector$5 ) );
-
- }
-
- sphere.radius = Math.sqrt( maxRadiusSq );
- geometryInfo.boundingSphere = sphere;
-
- }
-
- target.copy( geometryInfo.boundingSphere );
- return target;
-
- }
-
- /**
- * Sets the given local transformation matrix to the defined instance.
- * Negatively scaled matrices are not supported.
- *
- * @param {number} instanceId - The ID of an instance to set the matrix of.
- * @param {Matrix4} matrix - A 4x4 matrix representing the local transformation of a single instance.
- * @return {BatchedMesh} A reference to this batched mesh.
- */
- setMatrixAt( instanceId, matrix ) {
-
- this.validateInstanceId( instanceId );
-
- const matricesTexture = this._matricesTexture;
- const matricesArray = this._matricesTexture.image.data;
- matrix.toArray( matricesArray, instanceId * 16 );
- matricesTexture.needsUpdate = true;
-
- return this;
-
- }
-
- /**
- * Returns the local transformation matrix of the defined instance.
- *
- * @param {number} instanceId - The ID of an instance to get the matrix of.
- * @param {Matrix4} matrix - The target object that is used to store the method's result.
- * @return {Matrix4} The instance's local transformation matrix.
- */
- getMatrixAt( instanceId, matrix ) {
-
- this.validateInstanceId( instanceId );
- return matrix.fromArray( this._matricesTexture.image.data, instanceId * 16 );
-
- }
-
- /**
- * Sets the given color to the defined instance.
- *
- * @param {number} instanceId - The ID of an instance to set the color of.
- * @param {Color|Vector4} color - The color to set the instance to. Use a `Vector4` to also define alpha.
- * @return {BatchedMesh} A reference to this batched mesh.
- */
- setColorAt( instanceId, color ) {
-
- this.validateInstanceId( instanceId );
-
- if ( this._colorsTexture === null ) {
-
- this._initColorsTexture();
-
- }
-
- color.toArray( this._colorsTexture.image.data, instanceId * 4 );
- this._colorsTexture.needsUpdate = true;
-
- return this;
-
- }
-
- /**
- * Returns the color of the defined instance.
- *
- * @param {number} instanceId - The ID of an instance to get the color of.
- * @param {Color|Vector4} color - The target object that is used to store the method's result.
- * @return {Color|Vector4} The instance's color. Use a `Vector4` to also retrieve alpha.
- */
- getColorAt( instanceId, color ) {
-
- this.validateInstanceId( instanceId );
- if ( this._colorsTexture === null ) {
-
- if ( color.isVector4 ) {
-
- return color.set( 1, 1, 1, 1 );
-
- } else {
-
- return color.setRGB( 1, 1, 1 );
-
- }
-
- } else {
-
- return color.fromArray( this._colorsTexture.image.data, instanceId * 4 );
-
- }
-
- }
-
- /**
- * Sets the visibility of the instance.
- *
- * @param {number} instanceId - The id of the instance to set the visibility of.
- * @param {boolean} visible - Whether the instance is visible or not.
- * @return {BatchedMesh} A reference to this batched mesh.
- */
- setVisibleAt( instanceId, visible ) {
-
- this.validateInstanceId( instanceId );
-
- if ( this._instanceInfo[ instanceId ].visible === visible ) {
-
- return this;
-
- }
-
- this._instanceInfo[ instanceId ].visible = visible;
- this._visibilityChanged = true;
-
- return this;
-
- }
-
- /**
- * Returns the visibility state of the defined instance.
- *
- * @param {number} instanceId - The ID of an instance to get the visibility state of.
- * @return {boolean} Whether the instance is visible or not.
- */
- getVisibleAt( instanceId ) {
-
- this.validateInstanceId( instanceId );
-
- return this._instanceInfo[ instanceId ].visible;
-
- }
-
- /**
- * Sets the geometry ID of the instance at the given index.
- *
- * @param {number} instanceId - The ID of the instance to set the geometry ID of.
- * @param {number} geometryId - The geometry ID to be use by the instance.
- * @return {BatchedMesh} A reference to this batched mesh.
- */
- setGeometryIdAt( instanceId, geometryId ) {
-
- this.validateInstanceId( instanceId );
- this.validateGeometryId( geometryId );
-
- this._instanceInfo[ instanceId ].geometryIndex = geometryId;
- this._visibilityChanged = true;
-
- return this;
-
- }
-
- /**
- * Returns the geometry ID of the defined instance.
- *
- * @param {number} instanceId - The ID of an instance to get the geometry ID of.
- * @return {number} The instance's geometry ID.
- */
- getGeometryIdAt( instanceId ) {
-
- this.validateInstanceId( instanceId );
-
- return this._instanceInfo[ instanceId ].geometryIndex;
-
- }
-
- /**
- * Get the range representing the subset of triangles related to the attached geometry,
- * indicating the starting offset and count, or `null` if invalid.
- *
- * @param {number} geometryId - The id of the geometry to get the range of.
- * @param {Object} [target] - The target object that is used to store the method's result.
- * @return {{
- * vertexStart:number,vertexCount:number,reservedVertexCount:number,
- * indexStart:number,indexCount:number,reservedIndexCount:number,
- * start:number,count:number
- * }} The result object with range data.
- */
- getGeometryRangeAt( geometryId, target = {} ) {
-
- this.validateGeometryId( geometryId );
-
- const geometryInfo = this._geometryInfo[ geometryId ];
- target.vertexStart = geometryInfo.vertexStart;
- target.vertexCount = geometryInfo.vertexCount;
- target.reservedVertexCount = geometryInfo.reservedVertexCount;
-
- target.indexStart = geometryInfo.indexStart;
- target.indexCount = geometryInfo.indexCount;
- target.reservedIndexCount = geometryInfo.reservedIndexCount;
-
- target.start = geometryInfo.start;
- target.count = geometryInfo.count;
-
- return target;
-
- }
-
- /**
- * Resizes the necessary buffers to support the provided number of instances.
- * If the provided arguments shrink the number of instances but there are not enough
- * unused Ids at the end of the list then an error is thrown.
- *
- * @param {number} maxInstanceCount - The max number of individual instances that can be added and rendered by the batch.
- */
- setInstanceCount( maxInstanceCount ) {
-
- // shrink the available instances as much as possible
- const availableInstanceIds = this._availableInstanceIds;
- const instanceInfo = this._instanceInfo;
- availableInstanceIds.sort( ascIdSort );
- while ( availableInstanceIds[ availableInstanceIds.length - 1 ] === instanceInfo.length - 1 ) {
-
- instanceInfo.pop();
- availableInstanceIds.pop();
-
- }
-
- // throw an error if it can't be shrunk to the desired size
- if ( maxInstanceCount < instanceInfo.length ) {
-
- throw new Error( `THREE.BatchedMesh: Instance ids outside the range ${ maxInstanceCount } are being used. Cannot shrink instance count.` );
-
- }
-
- // copy the multi draw counts
- const multiDrawCounts = new Int32Array( maxInstanceCount );
- const multiDrawStarts = new Int32Array( maxInstanceCount );
- copyArrayContents( this._multiDrawCounts, multiDrawCounts );
- copyArrayContents( this._multiDrawStarts, multiDrawStarts );
-
- this._multiDrawCounts = multiDrawCounts;
- this._multiDrawStarts = multiDrawStarts;
- this._maxInstanceCount = maxInstanceCount;
-
- // update texture data for instance sampling
- const indirectTexture = this._indirectTexture;
- const matricesTexture = this._matricesTexture;
- const colorsTexture = this._colorsTexture;
-
- indirectTexture.dispose();
- this._initIndirectTexture();
- copyArrayContents( indirectTexture.image.data, this._indirectTexture.image.data );
-
- matricesTexture.dispose();
- this._initMatricesTexture();
- copyArrayContents( matricesTexture.image.data, this._matricesTexture.image.data );
-
- if ( colorsTexture ) {
-
- colorsTexture.dispose();
- this._initColorsTexture();
- copyArrayContents( colorsTexture.image.data, this._colorsTexture.image.data );
-
- }
-
- }
-
- /**
- * Resizes the available space in the batch's vertex and index buffer attributes to the provided sizes.
- * If the provided arguments shrink the geometry buffers but there is not enough unused space at the
- * end of the geometry attributes then an error is thrown.
- *
- * @param {number} maxVertexCount - The maximum number of vertices to be used by all unique geometries to resize to.
- * @param {number} maxIndexCount - The maximum number of indices to be used by all unique geometries to resize to.
- */
- setGeometrySize( maxVertexCount, maxIndexCount ) {
-
- // Check if we can shrink to the requested vertex attribute size
- const validRanges = [ ...this._geometryInfo ].filter( info => info.active );
- const requiredVertexLength = Math.max( ...validRanges.map( range => range.vertexStart + range.reservedVertexCount ) );
- if ( requiredVertexLength > maxVertexCount ) {
-
- throw new Error( `THREE.BatchedMesh: Geometry vertex values are being used outside the range ${ maxIndexCount }. Cannot shrink further.` );
-
- }
-
- // Check if we can shrink to the requested index attribute size
- if ( this.geometry.index ) {
-
- const requiredIndexLength = Math.max( ...validRanges.map( range => range.indexStart + range.reservedIndexCount ) );
- if ( requiredIndexLength > maxIndexCount ) {
-
- throw new Error( `THREE.BatchedMesh: Geometry index values are being used outside the range ${ maxIndexCount }. Cannot shrink further.` );
-
- }
-
- }
-
- //
-
- // dispose of the previous geometry
- const oldGeometry = this.geometry;
- oldGeometry.dispose();
-
- // recreate the geometry needed based on the previous variant
- this._maxVertexCount = maxVertexCount;
- this._maxIndexCount = maxIndexCount;
-
- if ( this._geometryInitialized ) {
-
- this._geometryInitialized = false;
- this.geometry = new BufferGeometry();
- this._initializeGeometry( oldGeometry );
-
- }
-
- // copy data from the previous geometry
- const geometry = this.geometry;
- if ( oldGeometry.index ) {
-
- copyArrayContents( oldGeometry.index.array, geometry.index.array );
-
- }
-
- for ( const key in oldGeometry.attributes ) {
-
- copyArrayContents( oldGeometry.attributes[ key ].array, geometry.attributes[ key ].array );
-
- }
-
- }
-
- raycast( raycaster, intersects ) {
-
- const instanceInfo = this._instanceInfo;
- const geometryInfoList = this._geometryInfo;
- const matrixWorld = this.matrixWorld;
- const batchGeometry = this.geometry;
-
- // iterate over each geometry
- _mesh.material = this.material;
- _mesh.geometry.index = batchGeometry.index;
- _mesh.geometry.attributes = batchGeometry.attributes;
- if ( _mesh.geometry.boundingBox === null ) {
-
- _mesh.geometry.boundingBox = new Box3();
-
- }
-
- if ( _mesh.geometry.boundingSphere === null ) {
-
- _mesh.geometry.boundingSphere = new Sphere();
-
- }
-
- for ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {
-
- if ( ! instanceInfo[ i ].visible || ! instanceInfo[ i ].active ) {
-
- continue;
-
- }
-
- const geometryId = instanceInfo[ i ].geometryIndex;
- const geometryInfo = geometryInfoList[ geometryId ];
- _mesh.geometry.setDrawRange( geometryInfo.start, geometryInfo.count );
-
- // get the intersects
- this.getMatrixAt( i, _mesh.matrixWorld ).premultiply( matrixWorld );
- this.getBoundingBoxAt( geometryId, _mesh.geometry.boundingBox );
- this.getBoundingSphereAt( geometryId, _mesh.geometry.boundingSphere );
- _mesh.raycast( raycaster, _batchIntersects );
-
- // add batch id to the intersects
- for ( let j = 0, l = _batchIntersects.length; j < l; j ++ ) {
-
- const intersect = _batchIntersects[ j ];
- intersect.object = this;
- intersect.batchId = i;
- intersects.push( intersect );
-
- }
-
- _batchIntersects.length = 0;
-
- }
-
- _mesh.material = null;
- _mesh.geometry.index = null;
- _mesh.geometry.attributes = {};
- _mesh.geometry.setDrawRange( 0, Infinity );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.geometry = source.geometry.clone();
- this.perObjectFrustumCulled = source.perObjectFrustumCulled;
- this.sortObjects = source.sortObjects;
- this.boundingBox = source.boundingBox !== null ? source.boundingBox.clone() : null;
- this.boundingSphere = source.boundingSphere !== null ? source.boundingSphere.clone() : null;
-
- this._geometryInfo = source._geometryInfo.map( info => ( {
- ...info,
-
- boundingBox: info.boundingBox !== null ? info.boundingBox.clone() : null,
- boundingSphere: info.boundingSphere !== null ? info.boundingSphere.clone() : null,
- } ) );
- this._instanceInfo = source._instanceInfo.map( info => ( { ...info } ) );
-
- this._availableInstanceIds = source._availableInstanceIds.slice();
- this._availableGeometryIds = source._availableGeometryIds.slice();
-
- this._nextIndexStart = source._nextIndexStart;
- this._nextVertexStart = source._nextVertexStart;
- this._geometryCount = source._geometryCount;
-
- this._maxInstanceCount = source._maxInstanceCount;
- this._maxVertexCount = source._maxVertexCount;
- this._maxIndexCount = source._maxIndexCount;
-
- this._geometryInitialized = source._geometryInitialized;
- this._multiDrawCounts = source._multiDrawCounts.slice();
- this._multiDrawStarts = source._multiDrawStarts.slice();
- this._multiDrawBytesPerElement = source._multiDrawBytesPerElement;
-
- this._indirectTexture = source._indirectTexture.clone();
- this._indirectTexture.image.data = this._indirectTexture.image.data.slice();
-
- this._matricesTexture = source._matricesTexture.clone();
- this._matricesTexture.image.data = this._matricesTexture.image.data.slice();
-
- if ( this._colorsTexture !== null ) {
-
- this._colorsTexture = source._colorsTexture.clone();
- this._colorsTexture.image.data = this._colorsTexture.image.data.slice();
-
- }
-
- return this;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- // Assuming the geometry is not shared with other meshes
- this.geometry.dispose();
-
- this._matricesTexture.dispose();
- this._matricesTexture = null;
-
- this._indirectTexture.dispose();
- this._indirectTexture = null;
-
- if ( this._colorsTexture !== null ) {
-
- this._colorsTexture.dispose();
- this._colorsTexture = null;
-
- }
-
- }
-
- onBeforeRender( renderer, scene, camera, geometry, material/*, _group*/ ) {
-
- // if visibility has not changed and frustum culling and object sorting is not required
- // then skip iterating over all items
- if ( ! this._visibilityChanged && ! this.perObjectFrustumCulled && ! this.sortObjects ) {
-
- return;
-
- }
-
- // the indexed version of the multi draw function requires specifying the start
- // offset in bytes.
- const index = geometry.getIndex();
- let bytesPerElement = index === null ? 1 : index.array.BYTES_PER_ELEMENT;
-
-
- // the "wireframe" attribute implicitly creates a line attribute in the renderer, which is double
- // the vertices to draw (3 lines per triangle) so we multiply the draw counts / starts and make
- // assumptions about the index buffer byte size.
- let multiDrawMultiplier = 1;
- if ( material.wireframe ) {
-
- multiDrawMultiplier = 2;
- bytesPerElement = geometry.attributes.position.count > 65535 ? 4 : 2;
-
- }
-
- const instanceInfo = this._instanceInfo;
- const multiDrawStarts = this._multiDrawStarts;
- const multiDrawCounts = this._multiDrawCounts;
- const geometryInfoList = this._geometryInfo;
- const perObjectFrustumCulled = this.perObjectFrustumCulled;
- const indirectTexture = this._indirectTexture;
- const indirectArray = indirectTexture.image.data;
-
- const frustum = camera.isArrayCamera ? _frustumArray : _frustum;
- // prepare the frustum in the local frame
- if ( perObjectFrustumCulled ) {
-
- if ( camera.isArrayCamera ) {
-
- frustum.setFromArrayCamera( camera );
-
- } else {
-
- _matrix$1
- .multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse )
- .multiply( this.matrixWorld );
-
- frustum.setFromProjectionMatrix(
- _matrix$1,
- camera.coordinateSystem,
- camera.reversedDepth
- );
-
- }
-
- }
-
- let multiDrawCount = 0;
- if ( this.sortObjects ) {
-
- // get the camera position in the local frame
- _matrix$1.copy( this.matrixWorld ).invert();
- _vector$5.setFromMatrixPosition( camera.matrixWorld ).applyMatrix4( _matrix$1 );
- _forward$1.set( 0, 0, -1 ).transformDirection( camera.matrixWorld ).transformDirection( _matrix$1 );
-
- for ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {
-
- if ( instanceInfo[ i ].visible && instanceInfo[ i ].active ) {
-
- const geometryId = instanceInfo[ i ].geometryIndex;
-
- // get the bounds in world space
- this.getMatrixAt( i, _matrix$1 );
- this.getBoundingSphereAt( geometryId, _sphere$2 ).applyMatrix4( _matrix$1 );
-
- // determine whether the batched geometry is within the frustum
- let culled = false;
- if ( perObjectFrustumCulled ) {
-
- culled = ! frustum.intersectsSphere( _sphere$2 );
-
- }
-
- if ( ! culled ) {
-
- // get the distance from camera used for sorting
- const geometryInfo = geometryInfoList[ geometryId ];
- const z = _temp.subVectors( _sphere$2.center, _vector$5 ).dot( _forward$1 );
- _renderList.push( geometryInfo.start, geometryInfo.count, z, i );
-
- }
-
- }
-
- }
-
- // Sort the draw ranges and prep for rendering
- const list = _renderList.list;
- const customSort = this.customSort;
- if ( customSort === null ) {
-
- list.sort( material.transparent ? sortTransparent : sortOpaque );
-
- } else {
-
- customSort.call( this, list, camera );
-
- }
-
- for ( let i = 0, l = list.length; i < l; i ++ ) {
-
- const item = list[ i ];
- multiDrawStarts[ multiDrawCount ] = item.start * bytesPerElement * multiDrawMultiplier;
- multiDrawCounts[ multiDrawCount ] = item.count * multiDrawMultiplier;
- indirectArray[ multiDrawCount ] = item.index;
- multiDrawCount ++;
-
- }
-
- _renderList.reset();
-
- } else {
-
- for ( let i = 0, l = instanceInfo.length; i < l; i ++ ) {
-
- if ( instanceInfo[ i ].visible && instanceInfo[ i ].active ) {
-
- const geometryId = instanceInfo[ i ].geometryIndex;
-
- // determine whether the batched geometry is within the frustum
- let culled = false;
- if ( perObjectFrustumCulled ) {
-
- // get the bounds in world space
- this.getMatrixAt( i, _matrix$1 );
- this.getBoundingSphereAt( geometryId, _sphere$2 ).applyMatrix4( _matrix$1 );
- culled = ! frustum.intersectsSphere( _sphere$2 );
-
- }
-
- if ( ! culled ) {
-
- const geometryInfo = geometryInfoList[ geometryId ];
- multiDrawStarts[ multiDrawCount ] = geometryInfo.start * bytesPerElement * multiDrawMultiplier;
- multiDrawCounts[ multiDrawCount ] = geometryInfo.count * multiDrawMultiplier;
- indirectArray[ multiDrawCount ] = i;
- multiDrawCount ++;
-
- }
-
- }
-
- }
-
- }
-
- indirectTexture.needsUpdate = true;
- this._multiDrawCount = multiDrawCount;
- this._multiDrawBytesPerElement = bytesPerElement;
- this._visibilityChanged = false;
-
- }
-
- onBeforeShadow( renderer, object, camera, shadowCamera, geometry, depthMaterial/* , group */ ) {
-
- this.onBeforeRender( renderer, null, shadowCamera, geometry, depthMaterial );
-
- }
-
-}
-
-/**
- * A material for rendering line primitives.
- *
- * Materials define the appearance of renderable 3D objects.
- *
- * ```js
- * const material = new THREE.LineBasicMaterial( { color: 0xffffff } );
- * ```
- *
- * @augments Material
- */
-class LineBasicMaterial extends Material {
-
- /**
- * Constructs a new line basic material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isLineBasicMaterial = true;
-
- this.type = 'LineBasicMaterial';
-
- /**
- * Color of the material.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.color = new Color( 0xffffff );
-
- /**
- * Sets the color of the lines using data from a texture. The texture map
- * color is modulated by the diffuse `color`.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * Controls line thickness or lines.
- *
- * Can only be used with {@link SVGRenderer}. WebGL and WebGPU
- * ignore this setting and always render line primitives with a
- * width of one pixel.
- *
- * @type {number}
- * @default 1
- */
- this.linewidth = 1;
-
- /**
- * Defines appearance of line ends.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('butt'|'round'|'square')}
- * @default 'round'
- */
- this.linecap = 'round';
-
- /**
- * Defines appearance of line joints.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.linejoin = 'round';
-
- /**
- * Whether the material is affected by fog or not.
- *
- * @type {boolean}
- * @default true
- */
- this.fog = true;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.color.copy( source.color );
-
- this.map = source.map;
-
- this.linewidth = source.linewidth;
- this.linecap = source.linecap;
- this.linejoin = source.linejoin;
-
- this.fog = source.fog;
-
- return this;
-
- }
-
-}
-
-const _vStart = /*@__PURE__*/ new Vector3();
-const _vEnd = /*@__PURE__*/ new Vector3();
-
-const _inverseMatrix$1 = /*@__PURE__*/ new Matrix4();
-const _ray$1 = /*@__PURE__*/ new Ray();
-const _sphere$1 = /*@__PURE__*/ new Sphere();
-
-const _intersectPointOnRay = /*@__PURE__*/ new Vector3();
-const _intersectPointOnSegment = /*@__PURE__*/ new Vector3();
-
-/**
- * A continuous line. The line are rendered by connecting consecutive
- * vertices with straight lines.
- *
- * ```js
- * const material = new THREE.LineBasicMaterial( { color: 0x0000ff } );
- *
- * const points = [];
- * points.push( new THREE.Vector3( - 10, 0, 0 ) );
- * points.push( new THREE.Vector3( 0, 10, 0 ) );
- * points.push( new THREE.Vector3( 10, 0, 0 ) );
- *
- * const geometry = new THREE.BufferGeometry().setFromPoints( points );
- *
- * const line = new THREE.Line( geometry, material );
- * scene.add( line );
- * ```
- *
- * @augments Object3D
- */
-class Line extends Object3D {
-
- /**
- * Constructs a new line.
- *
- * @param {BufferGeometry} [geometry] - The line geometry.
- * @param {Material|Array} [material] - The line material.
- */
- constructor( geometry = new BufferGeometry(), material = new LineBasicMaterial() ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isLine = true;
-
- this.type = 'Line';
-
- /**
- * The line geometry.
- *
- * @type {BufferGeometry}
- */
- this.geometry = geometry;
-
- /**
- * The line material.
- *
- * @type {Material|Array}
- * @default LineBasicMaterial
- */
- this.material = material;
-
- /**
- * A dictionary representing the morph targets in the geometry. The key is the
- * morph targets name, the value its attribute index. This member is `undefined`
- * by default and only set when morph targets are detected in the geometry.
- *
- * @type {Object|undefined}
- * @default undefined
- */
- this.morphTargetDictionary = undefined;
-
- /**
- * An array of weights typically in the range `[0,1]` that specify how much of the morph
- * is applied. This member is `undefined` by default and only set when morph targets are
- * detected in the geometry.
- *
- * @type {Array|undefined}
- * @default undefined
- */
- this.morphTargetInfluences = undefined;
-
- this.updateMorphTargets();
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.material = Array.isArray( source.material ) ? source.material.slice() : source.material;
- this.geometry = source.geometry;
-
- return this;
-
- }
-
- /**
- * Computes an array of distance values which are necessary for rendering dashed lines.
- * For each vertex in the geometry, the method calculates the cumulative length from the
- * current point to the very beginning of the line.
- *
- * @return {Line} A reference to this line.
- */
- computeLineDistances() {
-
- const geometry = this.geometry;
-
- // we assume non-indexed geometry
-
- if ( geometry.index === null ) {
-
- const positionAttribute = geometry.attributes.position;
- const lineDistances = [ 0 ];
-
- for ( let i = 1, l = positionAttribute.count; i < l; i ++ ) {
-
- _vStart.fromBufferAttribute( positionAttribute, i - 1 );
- _vEnd.fromBufferAttribute( positionAttribute, i );
-
- lineDistances[ i ] = lineDistances[ i - 1 ];
- lineDistances[ i ] += _vStart.distanceTo( _vEnd );
-
- }
-
- geometry.setAttribute( 'lineDistance', new Float32BufferAttribute( lineDistances, 1 ) );
-
- } else {
-
- warn( 'Line.computeLineDistances(): Computation only possible with non-indexed BufferGeometry.' );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns `true` if this line intersects the given frustum.
- *
- * @param {Frustum|FrustumArray} frustum - The frustum to test.
- * @return {boolean} Whether this line intersects the given frustum or not.
- */
- intersectsFrustum( frustum ) {
-
- return frustum.intersectsObject( this );
-
- }
-
- /**
- * Computes intersection points between a casted ray and this line.
- *
- * @param {Raycaster} raycaster - The raycaster.
- * @param {Array} intersects - The target array that holds the intersection points.
- */
- raycast( raycaster, intersects ) {
-
- const geometry = this.geometry;
- const matrixWorld = this.matrixWorld;
- const threshold = raycaster.params.Line.threshold;
- const drawRange = geometry.drawRange;
-
- // Checking boundingSphere distance to ray
-
- if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
-
- _sphere$1.copy( geometry.boundingSphere );
- _sphere$1.applyMatrix4( matrixWorld );
- _sphere$1.radius += threshold;
-
- if ( raycaster.ray.intersectsSphere( _sphere$1 ) === false ) return;
-
- //
-
- _inverseMatrix$1.copy( matrixWorld ).invert();
- _ray$1.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$1 );
-
- const localThreshold = threshold / ( ( this.scale.x + this.scale.y + this.scale.z ) / 3 );
- const localThresholdSq = localThreshold * localThreshold;
-
- const step = this.isLineSegments ? 2 : 1;
-
- const index = geometry.index;
- const attributes = geometry.attributes;
- const positionAttribute = attributes.position;
-
- if ( index !== null ) {
-
- const start = Math.max( 0, drawRange.start );
- const end = Math.min( index.count, ( drawRange.start + drawRange.count ) );
-
- for ( let i = start, l = end - 1; i < l; i += step ) {
-
- const a = index.getX( i );
- const b = index.getX( i + 1 );
-
- const intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, a, b, i );
-
- if ( intersect ) {
-
- intersects.push( intersect );
-
- }
-
- }
-
- if ( this.isLineLoop ) {
-
- const a = index.getX( end - 1 );
- const b = index.getX( start );
-
- const intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, a, b, end - 1 );
-
- if ( intersect ) {
-
- intersects.push( intersect );
-
- }
-
- }
-
- } else {
-
- const start = Math.max( 0, drawRange.start );
- const end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) );
-
- for ( let i = start, l = end - 1; i < l; i += step ) {
-
- const intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, i, i + 1, i );
-
- if ( intersect ) {
-
- intersects.push( intersect );
-
- }
-
- }
-
- if ( this.isLineLoop ) {
-
- const intersect = checkIntersection( this, raycaster, _ray$1, localThresholdSq, end - 1, start, end - 1 );
-
- if ( intersect ) {
-
- intersects.push( intersect );
-
- }
-
- }
-
- }
-
- }
-
- /**
- * Sets the values of {@link Line#morphTargetDictionary} and {@link Line#morphTargetInfluences}
- * to make sure existing morph targets can influence this 3D object.
- */
- updateMorphTargets() {
-
- const geometry = this.geometry;
-
- const morphAttributes = geometry.morphAttributes;
- const keys = Object.keys( morphAttributes );
-
- if ( keys.length > 0 ) {
-
- const morphAttribute = morphAttributes[ keys[ 0 ] ];
-
- if ( morphAttribute !== undefined ) {
-
- this.morphTargetInfluences = [];
- this.morphTargetDictionary = {};
-
- for ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {
-
- const name = morphAttribute[ m ].name || String( m );
-
- this.morphTargetInfluences.push( 0 );
- this.morphTargetDictionary[ name ] = m;
-
- }
-
- }
-
- }
-
- }
-
-}
-
-function checkIntersection( object, raycaster, ray, thresholdSq, a, b, i ) {
-
- const positionAttribute = object.geometry.attributes.position;
-
- _vStart.fromBufferAttribute( positionAttribute, a );
- _vEnd.fromBufferAttribute( positionAttribute, b );
-
- const distSq = ray.distanceSqToSegment( _vStart, _vEnd, _intersectPointOnRay, _intersectPointOnSegment );
-
- if ( distSq > thresholdSq ) return;
-
- _intersectPointOnRay.applyMatrix4( object.matrixWorld ); // Move back to world space for distance calculation
-
- const distance = raycaster.ray.origin.distanceTo( _intersectPointOnRay );
-
- if ( distance < raycaster.near || distance > raycaster.far ) return;
-
- return {
-
- distance: distance,
- // What do we want? intersection point on the ray or on the segment??
- // point: raycaster.ray.at( distance ),
- point: _intersectPointOnSegment.clone().applyMatrix4( object.matrixWorld ),
- index: i,
- face: null,
- faceIndex: null,
- barycoord: null,
- object: object
-
- };
-
-}
-
-const _start = /*@__PURE__*/ new Vector3();
-const _end = /*@__PURE__*/ new Vector3();
-
-/**
- * A series of lines drawn between pairs of vertices.
- *
- * @augments Line
- */
-class LineSegments extends Line {
-
- /**
- * Constructs a new line segments.
- *
- * @param {BufferGeometry} [geometry] - The line geometry.
- * @param {Material|Array} [material] - The line material.
- */
- constructor( geometry, material ) {
-
- super( geometry, material );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isLineSegments = true;
-
- this.type = 'LineSegments';
-
- }
-
- computeLineDistances() {
-
- const geometry = this.geometry;
-
- // we assume non-indexed geometry
-
- if ( geometry.index === null ) {
-
- const positionAttribute = geometry.attributes.position;
- const lineDistances = [];
-
- for ( let i = 0, l = positionAttribute.count; i < l; i += 2 ) {
-
- _start.fromBufferAttribute( positionAttribute, i );
- _end.fromBufferAttribute( positionAttribute, i + 1 );
-
- lineDistances[ i ] = ( i === 0 ) ? 0 : lineDistances[ i - 1 ];
- lineDistances[ i + 1 ] = lineDistances[ i ] + _start.distanceTo( _end );
-
- }
-
- geometry.setAttribute( 'lineDistance', new Float32BufferAttribute( lineDistances, 1 ) );
-
- } else {
-
- warn( 'LineSegments.computeLineDistances(): Computation only possible with non-indexed BufferGeometry.' );
-
- }
-
- return this;
-
- }
-
-}
-
-/**
- * A continuous line. This is nearly the same as {@link Line} the only difference
- * is that the last vertex is connected with the first vertex in order to close
- * the line to form a loop.
- *
- * @augments Line
- */
-class LineLoop extends Line {
-
- /**
- * Constructs a new line loop.
- *
- * @param {BufferGeometry} [geometry] - The line geometry.
- * @param {Material|Array} [material] - The line material.
- */
- constructor( geometry, material ) {
-
- super( geometry, material );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isLineLoop = true;
-
- this.type = 'LineLoop';
-
- }
-
-}
-
-/**
- * A material for rendering point primitives.
- *
- * Materials define the appearance of renderable 3D objects.
- *
- * ```js
- * const vertices = [];
- *
- * for ( let i = 0; i < 10000; i ++ ) {
- * const x = THREE.MathUtils.randFloatSpread( 2000 );
- * const y = THREE.MathUtils.randFloatSpread( 2000 );
- * const z = THREE.MathUtils.randFloatSpread( 2000 );
- *
- * vertices.push( x, y, z );
- * }
- *
- * const geometry = new THREE.BufferGeometry();
- * geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
- * const material = new THREE.PointsMaterial( { color: 0x888888 } );
- * const points = new THREE.Points( geometry, material );
- * scene.add( points );
- * ```
- *
- * @augments Material
- */
-class PointsMaterial extends Material {
-
- /**
- * Constructs a new points material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isPointsMaterial = true;
-
- this.type = 'PointsMaterial';
-
- /**
- * Color of the material.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.color = new Color( 0xffffff );
-
- /**
- * The color map. May optionally include an alpha channel, typically combined
- * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
- * color is modulated by the diffuse `color`.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * The alpha map is a grayscale texture that controls the opacity across the
- * surface (black: fully transparent; white: fully opaque).
- *
- * Only the color of the texture is used, ignoring the alpha channel if one
- * exists. For RGB and RGBA textures, the renderer will use the green channel
- * when sampling this texture due to the extra bit of precision provided for
- * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
- * luminance/alpha textures will also still work as expected.
- *
- * `alphaMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.alphaMap = null;
-
- /**
- * Defines the size of the points in pixels.
- *
- * Might be capped if the value exceeds hardware dependent parameters like [gl.ALIASED_POINT_SIZE_RANGE](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getParamete).
- *
- * @type {number}
- * @default 1
- */
- this.size = 1;
-
- /**
- * Specifies whether size of individual points is attenuated by the camera depth (perspective camera only).
- *
- * @type {boolean}
- * @default true
- */
- this.sizeAttenuation = true;
-
- /**
- * Whether the material is affected by fog or not.
- *
- * @type {boolean}
- * @default true
- */
- this.fog = true;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.color.copy( source.color );
-
- this.map = source.map;
-
- this.alphaMap = source.alphaMap;
-
- this.size = source.size;
- this.sizeAttenuation = source.sizeAttenuation;
-
- this.fog = source.fog;
-
- return this;
-
- }
-
-}
-
-const _inverseMatrix = /*@__PURE__*/ new Matrix4();
-const _ray = /*@__PURE__*/ new Ray();
-const _sphere = /*@__PURE__*/ new Sphere();
-const _position$3 = /*@__PURE__*/ new Vector3();
-
-/**
- * A class for displaying points or point clouds.
- *
- * @augments Object3D
- */
-class Points extends Object3D {
-
- /**
- * Constructs a new point cloud.
- *
- * @param {BufferGeometry} [geometry] - The points geometry.
- * @param {Material|Array} [material] - The points material.
- */
- constructor( geometry = new BufferGeometry(), material = new PointsMaterial() ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isPoints = true;
-
- this.type = 'Points';
-
- /**
- * The points geometry.
- *
- * @type {BufferGeometry}
- */
- this.geometry = geometry;
-
- /**
- * The line material.
- *
- * @type {Material|Array}
- * @default PointsMaterial
- */
- this.material = material;
-
- /**
- * A dictionary representing the morph targets in the geometry. The key is the
- * morph targets name, the value its attribute index. This member is `undefined`
- * by default and only set when morph targets are detected in the geometry.
- *
- * @type {Object|undefined}
- * @default undefined
- */
- this.morphTargetDictionary = undefined;
-
- /**
- * An array of weights typically in the range `[0,1]` that specify how much of the morph
- * is applied. This member is `undefined` by default and only set when morph targets are
- * detected in the geometry.
- *
- * @type {Array|undefined}
- * @default undefined
- */
- this.morphTargetInfluences = undefined;
-
- this.updateMorphTargets();
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.material = Array.isArray( source.material ) ? source.material.slice() : source.material;
- this.geometry = source.geometry;
-
- return this;
-
- }
-
- /**
- * Returns `true` if this point cloud intersects the given frustum.
- *
- * @param {Frustum|FrustumArray} frustum - The frustum to test.
- * @return {boolean} Whether this point cloud intersects the given frustum or not.
- */
- intersectsFrustum( frustum ) {
-
- return frustum.intersectsObject( this );
-
- }
-
- /**
- * Computes intersection points between a casted ray and this point cloud.
- *
- * @param {Raycaster} raycaster - The raycaster.
- * @param {Array} intersects - The target array that holds the intersection points.
- */
- raycast( raycaster, intersects ) {
-
- const geometry = this.geometry;
- const matrixWorld = this.matrixWorld;
- const threshold = raycaster.params.Points.threshold;
- const drawRange = geometry.drawRange;
-
- // Checking boundingSphere distance to ray
-
- if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
-
- _sphere.copy( geometry.boundingSphere );
- _sphere.applyMatrix4( matrixWorld );
- _sphere.radius += threshold;
-
- if ( raycaster.ray.intersectsSphere( _sphere ) === false ) return;
-
- //
-
- _inverseMatrix.copy( matrixWorld ).invert();
- _ray.copy( raycaster.ray ).applyMatrix4( _inverseMatrix );
-
- const localThreshold = threshold / ( ( this.scale.x + this.scale.y + this.scale.z ) / 3 );
- const localThresholdSq = localThreshold * localThreshold;
-
- const index = geometry.index;
- const attributes = geometry.attributes;
- const positionAttribute = attributes.position;
-
- if ( index !== null ) {
-
- const start = Math.max( 0, drawRange.start );
- const end = Math.min( index.count, ( drawRange.start + drawRange.count ) );
-
- for ( let i = start, il = end; i < il; i ++ ) {
-
- const a = index.getX( i );
-
- _position$3.fromBufferAttribute( positionAttribute, a );
-
- testPoint( _position$3, a, localThresholdSq, matrixWorld, raycaster, intersects, this );
-
- }
-
- } else {
-
- const start = Math.max( 0, drawRange.start );
- const end = Math.min( positionAttribute.count, ( drawRange.start + drawRange.count ) );
-
- for ( let i = start, l = end; i < l; i ++ ) {
-
- _position$3.fromBufferAttribute( positionAttribute, i );
-
- testPoint( _position$3, i, localThresholdSq, matrixWorld, raycaster, intersects, this );
-
- }
-
- }
-
- }
-
- /**
- * Sets the values of {@link Points#morphTargetDictionary} and {@link Points#morphTargetInfluences}
- * to make sure existing morph targets can influence this 3D object.
- */
- updateMorphTargets() {
-
- const geometry = this.geometry;
-
- const morphAttributes = geometry.morphAttributes;
- const keys = Object.keys( morphAttributes );
-
- if ( keys.length > 0 ) {
-
- const morphAttribute = morphAttributes[ keys[ 0 ] ];
-
- if ( morphAttribute !== undefined ) {
-
- this.morphTargetInfluences = [];
- this.morphTargetDictionary = {};
-
- for ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {
-
- const name = morphAttribute[ m ].name || String( m );
-
- this.morphTargetInfluences.push( 0 );
- this.morphTargetDictionary[ name ] = m;
-
- }
-
- }
-
- }
-
- }
-
-}
-
-function testPoint( point, index, localThresholdSq, matrixWorld, raycaster, intersects, object ) {
-
- const rayPointDistanceSq = _ray.distanceSqToPoint( point );
-
- if ( rayPointDistanceSq < localThresholdSq ) {
-
- const intersectPoint = new Vector3();
-
- _ray.closestPointToPoint( point, intersectPoint );
- intersectPoint.applyMatrix4( matrixWorld );
-
- const distance = raycaster.ray.origin.distanceTo( intersectPoint );
-
- if ( distance < raycaster.near || distance > raycaster.far ) return;
-
- intersects.push( {
-
- distance: distance,
- distanceToRay: Math.sqrt( rayPointDistanceSq ),
- point: intersectPoint,
- index: index,
- face: null,
- faceIndex: null,
- barycoord: null,
- object: object
-
- } );
-
- }
-
-}
-
-/**
- * A texture for use with a video.
- *
- * ```js
- * // assuming you have created a HTML video element with id="video"
- * const video = document.getElementById( 'video' );
- * const texture = new THREE.VideoTexture( video );
- * ```
- *
- * Note: When using video textures with {@link WebGPURenderer}, {@link Texture#colorSpace} must be
- * set to THREE.SRGBColorSpace.
- *
- * Note: After the initial use of a texture, its dimensions, format, and type
- * cannot be changed. Instead, call {@link Texture#dispose} on the texture and instantiate a new one.
- *
- * @augments Texture
- */
-class VideoTexture extends Texture {
-
- /**
- * Constructs a new video texture.
- *
- * @param {HTMLVideoElement} video - The video element to use as a data source for the texture.
- * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
- * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
- * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
- * @param {number} [magFilter=LinearFilter] - The mag filter value.
- * @param {number} [minFilter=LinearFilter] - The min filter value.
- * @param {number} [format=RGBAFormat] - The texture format.
- * @param {number} [type=UnsignedByteType] - The texture type.
- * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
- */
- constructor( video, mapping, wrapS, wrapT, magFilter = LinearFilter, minFilter = LinearFilter, format, type, anisotropy ) {
-
- super( video, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isVideoTexture = true;
-
- /**
- * Whether to generate mipmaps (if possible) for a texture.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.generateMipmaps = false;
-
- /**
- * The video frame request callback identifier, which is a positive integer.
- *
- * Value of 0 represents no scheduled rVFC.
- *
- * @private
- * @type {number}
- */
- this._requestVideoFrameCallbackId = 0;
-
- const scope = this;
-
- function updateVideo() {
-
- scope.needsUpdate = true;
- scope._requestVideoFrameCallbackId = video.requestVideoFrameCallback( updateVideo );
-
- }
-
- if ( 'requestVideoFrameCallback' in video ) {
-
- this._requestVideoFrameCallbackId = video.requestVideoFrameCallback( updateVideo );
-
- }
-
- }
-
- clone() {
-
- return new this.constructor( this.image ).copy( this );
-
- }
-
- /**
- * This method is called automatically by the renderer and sets {@link Texture#needsUpdate}
- * to `true` every time a new frame is available.
- *
- * Only relevant if `requestVideoFrameCallback` is not supported in the browser.
- */
- update() {
-
- const video = this.image;
- const hasVideoFrameCallback = 'requestVideoFrameCallback' in video;
-
- if ( hasVideoFrameCallback === false && video.readyState >= video.HAVE_CURRENT_DATA ) {
-
- this.needsUpdate = true;
-
- }
-
- }
-
- dispose() {
-
- if ( this._requestVideoFrameCallbackId !== 0 ) {
-
- this.source.data.cancelVideoFrameCallback( this._requestVideoFrameCallbackId );
-
- this._requestVideoFrameCallbackId = 0;
-
- }
-
- super.dispose();
-
- }
-
-}
-
-/**
- * This class can be used as an alternative way to define video data. Instead of using
- * an instance of `HTMLVideoElement` like with `VideoTexture`, `VideoFrameTexture` expects each frame is
- * defined manually via {@link VideoFrameTexture#setFrame}. A typical use case for this module is when
- * video frames are decoded with the WebCodecs API.
- *
- * ```js
- * const texture = new THREE.VideoFrameTexture();
- * texture.setFrame( frame );
- * ```
- *
- * @augments VideoTexture
- */
-class VideoFrameTexture extends VideoTexture {
-
- /**
- * Constructs a new video frame texture.
- *
- * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
- * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
- * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
- * @param {number} [magFilter=LinearFilter] - The mag filter value.
- * @param {number} [minFilter=LinearFilter] - The min filter value.
- * @param {number} [format=RGBAFormat] - The texture format.
- * @param {number} [type=UnsignedByteType] - The texture type.
- * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
- */
- constructor( mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {
-
- super( {}, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isVideoFrameTexture = true;
-
- }
-
- /**
- * This method overwritten with an empty implementation since
- * this type of texture is updated via `setFrame()`.
- */
- update() {}
-
- clone() {
-
- return new this.constructor().copy( this ); // restoring Texture.clone()
-
- }
-
- /**
- * Sets the current frame of the video. This will automatically update the texture
- * so the data can be used for rendering.
- *
- * @param {VideoFrame} frame - The video frame.
- */
- setFrame( frame ) {
-
- this.image = frame;
- this.needsUpdate = true;
-
- }
-
-}
-
-/**
- * This class can only be used in combination with `copyFramebufferToTexture()` methods
- * of renderers. It extracts the contents of the current bound framebuffer and provides it
- * as a texture for further usage.
- *
- * ```js
- * const pixelRatio = window.devicePixelRatio;
- * const textureSize = 128 * pixelRatio;
- *
- * const frameTexture = new FramebufferTexture( textureSize, textureSize );
- *
- * // calculate start position for copying part of the frame data
- * const vector = new Vector2();
- * vector.x = ( window.innerWidth * pixelRatio / 2 ) - ( textureSize / 2 );
- * vector.y = ( window.innerHeight * pixelRatio / 2 ) - ( textureSize / 2 );
- *
- * renderer.render( scene, camera );
- *
- * // copy part of the rendered frame into the framebuffer texture
- * renderer.copyFramebufferToTexture( frameTexture, vector );
- * ```
- *
- * @augments Texture
- */
-class FramebufferTexture extends Texture {
-
- /**
- * Constructs a new framebuffer texture.
- *
- * @param {number} [width] - The width of the texture.
- * @param {number} [height] - The height of the texture.
- */
- constructor( width, height ) {
-
- super( { width, height } );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isFramebufferTexture = true;
-
- /**
- * How the texture is sampled when a texel covers more than one pixel.
- *
- * Overwritten and set to `NearestFilter` by default to disable filtering.
- *
- * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
- * @default NearestFilter
- */
- this.magFilter = NearestFilter;
-
- /**
- * How the texture is sampled when a texel covers less than one pixel.
- *
- * Overwritten and set to `NearestFilter` by default to disable filtering.
- *
- * @type {(NearestFilter|NearestMipmapNearestFilter|NearestMipmapLinearFilter|LinearFilter|LinearMipmapNearestFilter|LinearMipmapLinearFilter)}
- * @default NearestFilter
- */
- this.minFilter = NearestFilter;
-
- /**
- * Whether to generate mipmaps (if possible) for a texture.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.generateMipmaps = false;
-
- this.needsUpdate = true;
-
- }
-
-}
-
-/**
- * Creates a texture based on data in compressed form.
- *
- * These texture are usually loaded with {@link CompressedTextureLoader}.
- *
- * @augments Texture
- */
-class CompressedTexture extends Texture {
-
- /**
- * Constructs a new compressed texture.
- *
- * @param {Array} mipmaps - This array holds for all mipmaps (including the bases mip)
- * the data and dimensions.
- * @param {number} width - The width of the texture.
- * @param {number} height - The height of the texture.
- * @param {number} [format=RGBAFormat] - The texture format.
- * @param {number} [type=UnsignedByteType] - The texture type.
- * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
- * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
- * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
- * @param {number} [magFilter=LinearFilter] - The mag filter value.
- * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.
- * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
- * @param {string} [colorSpace=NoColorSpace] - The color space.
- */
- constructor( mipmaps, width, height, format, type, mapping, wrapS, wrapT, magFilter, minFilter, anisotropy, colorSpace ) {
-
- super( null, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCompressedTexture = true;
-
- /**
- * The image property of a compressed texture just defines its dimensions.
- *
- * @type {{width:number,height:number}}
- */
- this.image = { width: width, height: height };
-
- /**
- * This array holds for all mipmaps (including the bases mip) the data and dimensions.
- *
- * @type {Array}
- */
- this.mipmaps = mipmaps;
-
- /**
- * If set to `true`, the texture is flipped along the vertical axis when
- * uploaded to the GPU.
- *
- * Overwritten and set to `false` by default since it is not possible to
- * flip compressed textures.
- *
- * @type {boolean}
- * @default false
- * @readonly
- */
- this.flipY = false;
-
- /**
- * Whether to generate mipmaps (if possible) for a texture.
- *
- * Overwritten and set to `false` by default since it is not
- * possible to generate mipmaps for compressed data. Mipmaps
- * must be embedded in the compressed texture file.
- *
- * @type {boolean}
- * @default false
- * @readonly
- */
- this.generateMipmaps = false;
-
- }
-
-}
-
-/**
- * Creates a texture 2D array based on data in compressed form.
- *
- * These texture are usually loaded with {@link CompressedTextureLoader}.
- *
- * @augments CompressedTexture
- */
-class CompressedArrayTexture extends CompressedTexture {
-
- /**
- * Constructs a new compressed array texture.
- *
- * @param {Array} mipmaps - This array holds for all mipmaps (including the bases mip)
- * the data and dimensions.
- * @param {number} width - The width of the texture.
- * @param {number} height - The height of the texture.
- * @param {number} depth - The depth of the texture.
- * @param {number} [format=RGBAFormat] - The min filter value.
- * @param {number} [type=UnsignedByteType] - The min filter value.
- */
- constructor( mipmaps, width, height, depth, format, type ) {
-
- super( mipmaps, width, height, format, type );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCompressedArrayTexture = true;
-
- /**
- * The image property of a compressed texture just defines its dimensions.
- *
- * @name CompressedArrayTexture#image
- * @type {{width:number,height:number,depth:number}}
- */
- this.image.depth = depth;
-
- /**
- * This defines how the texture is wrapped in the depth and corresponds to
- * *W* in UVW mapping.
- *
- * @type {(RepeatWrapping|ClampToEdgeWrapping|MirroredRepeatWrapping)}
- * @default ClampToEdgeWrapping
- */
- this.wrapR = ClampToEdgeWrapping;
-
- /**
- * A set of all layers which need to be updated in the texture.
- *
- * @type {Set}
- */
- this.layerUpdates = new Set();
-
- }
-
- /**
- * Copies the values of the given texture to this instance.
- *
- * @param {CompressedArrayTexture} source - The texture to copy.
- * @return {CompressedArrayTexture} A reference to this instance.
- */
- copy( source ) {
-
- super.copy( source );
-
- this.wrapR = source.wrapR;
-
- return this;
-
- }
-
- /**
- * Describes that a specific layer of the texture needs to be updated.
- * Normally when {@link Texture#needsUpdate} is set to `true`, the
- * entire compressed texture array is sent to the GPU. Marking specific
- * layers will only transmit subsets of all mipmaps associated with a
- * specific depth in the array which is often much more performant.
- *
- * @param {number} layerIndex - The layer index that should be updated.
- */
- addLayerUpdate( layerIndex ) {
-
- this.layerUpdates.add( layerIndex );
-
- }
-
- /**
- * Resets the layer updates registry.
- */
- clearLayerUpdates() {
-
- this.layerUpdates.clear();
-
- }
-
-}
-
-/**
- * Creates a cube texture based on data in compressed form.
- *
- * These texture are usually loaded with {@link CompressedTextureLoader}.
- *
- * @augments CompressedTexture
- */
-class CompressedCubeTexture extends CompressedTexture {
-
- /**
- * Constructs a new compressed texture.
- *
- * @param {Array} images - An array of compressed textures.
- * @param {number} [format=RGBAFormat] - The texture format.
- * @param {number} [type=UnsignedByteType] - The texture type.
- */
- constructor( images, format, type ) {
-
- super( undefined, images[ 0 ].width, images[ 0 ].height, format, type, CubeReflectionMapping );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCompressedCubeTexture = true;
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCubeTexture = true;
-
- this.image = images;
-
- }
-
-}
-
-/**
- * Creates a cube texture made up of six images.
- *
- * ```js
- * const loader = new THREE.CubeTextureLoader();
- * loader.setPath( 'textures/cube/pisa/' );
- *
- * const textureCube = loader.load( [
- * 'px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png'
- * ] );
- *
- * const material = new THREE.MeshBasicMaterial( { color: 0xffffff, envMap: textureCube } );
- * ```
- *
- * @augments Texture
- */
-class CubeTexture extends Texture {
-
- /**
- * Constructs a new cube texture.
- *
- * @param {Array} [images=[]] - An array holding a image for each side of a cube.
- * @param {number} [mapping=CubeReflectionMapping] - The texture mapping.
- * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
- * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
- * @param {number} [magFilter=LinearFilter] - The mag filter value.
- * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.
- * @param {number} [format=RGBAFormat] - The texture format.
- * @param {number} [type=UnsignedByteType] - The texture type.
- * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
- * @param {string} [colorSpace=NoColorSpace] - The color space value.
- */
- constructor( images = [], mapping = CubeReflectionMapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace ) {
-
- super( images, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, colorSpace );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCubeTexture = true;
-
- /**
- * If set to `true`, the texture is flipped along the vertical axis when
- * uploaded to the GPU.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.flipY = false;
-
- }
-
- /**
- * Alias for {@link CubeTexture#image}.
- *
- * @type {Array}
- */
- get images() {
-
- return this.image;
-
- }
-
- set images( value ) {
-
- this.image = value;
-
- }
-
-}
-
-/**
- * Creates a texture from a canvas element.
- *
- * This is almost the same as the base texture class, except that it sets {@link Texture#needsUpdate}
- * to `true` immediately since a canvas can directly be used for rendering.
- *
- * @augments Texture
- */
-class CanvasTexture extends Texture {
-
- /**
- * Constructs a new texture.
- *
- * @param {HTMLCanvasElement} [canvas] - The HTML canvas element.
- * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
- * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
- * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
- * @param {number} [magFilter=LinearFilter] - The mag filter value.
- * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.
- * @param {number} [format=RGBAFormat] - The texture format.
- * @param {number} [type=UnsignedByteType] - The texture type.
- * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
- */
- constructor( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {
-
- super( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCanvasTexture = true;
-
- this.needsUpdate = true;
-
- }
-
-}
-
-/**
- * Creates a texture from an HTML element.
- *
- * This is almost the same as the base texture class, except that it sets {@link Texture#needsUpdate}
- * to `true` immediately and listens for the parent canvas's paint events to trigger updates.
- *
- * @augments Texture
- */
-class HTMLTexture extends Texture {
-
- /**
- * Constructs a new texture.
- *
- * @param {HTMLElement} [element] - The HTML element.
- * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
- * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
- * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
- * @param {number} [magFilter=LinearFilter] - The mag filter value.
- * @param {number} [minFilter=LinearMipmapLinearFilter] - The min filter value.
- * @param {number} [format=RGBAFormat] - The texture format.
- * @param {number} [type=UnsignedByteType] - The texture type.
- * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
- */
- constructor( element, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {
-
- super( element, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isHTMLTexture = true;
- this.generateMipmaps = false;
-
- this.needsUpdate = true;
-
- const parent = element ? element.parentNode : null;
-
- if ( parent !== null && 'requestPaint' in parent ) {
-
- parent.onpaint = () => {
-
- this.needsUpdate = true;
-
- };
-
- parent.requestPaint();
-
- }
-
- }
-
- dispose() {
-
- const parent = this.image ? this.image.parentNode : null;
-
- if ( parent !== null && 'onpaint' in parent ) {
-
- parent.onpaint = null;
-
- }
-
- super.dispose();
-
- }
-
-}
-
-/**
- * This class can be used to automatically save the depth information of a
- * rendering into a texture.
- *
- * @augments Texture
- */
-class DepthTexture extends Texture {
-
- /**
- * Constructs a new depth texture.
- *
- * @param {number} width - The width of the texture.
- * @param {number} height - The height of the texture.
- * @param {number} [type=UnsignedIntType] - The texture type.
- * @param {number} [mapping=Texture.DEFAULT_MAPPING] - The texture mapping.
- * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
- * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
- * @param {number} [magFilter=LinearFilter] - The mag filter value.
- * @param {number} [minFilter=LinearFilter] - The min filter value.
- * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
- * @param {number} [format=DepthFormat] - The texture format.
- * @param {number} [depth=1] - The depth of the texture.
- */
- constructor( width, height, type = UnsignedIntType, mapping, wrapS, wrapT, magFilter = NearestFilter, minFilter = NearestFilter, anisotropy, format = DepthFormat, depth = 1 ) {
-
- if ( format !== DepthFormat && format !== DepthStencilFormat ) {
-
- throw new Error( 'THREE.DepthTexture: format must be either THREE.DepthFormat or THREE.DepthStencilFormat' );
-
- }
-
- const image = { width: width, height: height, depth: depth };
-
- super( image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isDepthTexture = true;
-
- /**
- * If set to `true`, the texture is flipped along the vertical axis when
- * uploaded to the GPU.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.flipY = false;
-
- /**
- * Whether to generate mipmaps (if possible) for a texture.
- *
- * Overwritten and set to `false` by default.
- *
- * @type {boolean}
- * @default false
- */
- this.generateMipmaps = false;
-
- /**
- * Code corresponding to the depth compare function.
- *
- * @type {?(NeverCompare|LessCompare|EqualCompare|LessEqualCompare|GreaterCompare|NotEqualCompare|GreaterEqualCompare|AlwaysCompare)}
- * @default null
- */
- this.compareFunction = null;
-
- }
-
-
- copy( source ) {
-
- super.copy( source );
-
- this.source = new TextureSource( Object.assign( {}, source.image ) ); // see #30540
- this.compareFunction = source.compareFunction;
-
- return this;
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.compareFunction = this.compareFunction;
-
- return data;
-
- }
-
-}
-
-/**
- * This class can be used to automatically save the depth information of a
- * cube rendering into a cube texture with depth format. Used for PointLight shadows.
- *
- * @augments DepthTexture
- */
-class CubeDepthTexture extends DepthTexture {
-
- /**
- * Constructs a new cube depth texture.
- *
- * @param {number} size - The size (width and height) of each cube face.
- * @param {number} [type=UnsignedIntType] - The texture type.
- * @param {number} [mapping=CubeReflectionMapping] - The texture mapping.
- * @param {number} [wrapS=ClampToEdgeWrapping] - The wrapS value.
- * @param {number} [wrapT=ClampToEdgeWrapping] - The wrapT value.
- * @param {number} [magFilter=NearestFilter] - The mag filter value.
- * @param {number} [minFilter=NearestFilter] - The min filter value.
- * @param {number} [anisotropy=Texture.DEFAULT_ANISOTROPY] - The anisotropy value.
- * @param {number} [format=DepthFormat] - The texture format.
- */
- constructor( size, type = UnsignedIntType, mapping = CubeReflectionMapping, wrapS, wrapT, magFilter = NearestFilter, minFilter = NearestFilter, anisotropy, format = DepthFormat ) {
-
- // Create 6 identical image descriptors for the cube faces
- const image = { width: size, height: size, depth: 1 };
- const images = [ image, image, image, image, image, image ];
-
- // Call DepthTexture constructor with width, height
- super( size, size, type, mapping, wrapS, wrapT, magFilter, minFilter, anisotropy, format );
-
- // Replace the single image with the array of 6 images
- this.image = images;
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCubeDepthTexture = true;
-
- /**
- * Set to true for cube texture handling in WebGLTextures.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCubeTexture = true;
-
- }
-
- /**
- * Alias for {@link CubeDepthTexture#image}.
- *
- * @type {Array}
- */
- get images() {
-
- return this.image;
-
- }
-
- set images( value ) {
-
- this.image = value;
-
- }
-
-}
-
-/**
- * Represents a texture created externally with the same renderer context.
- *
- * This may be a texture from a protected media stream, device camera feed,
- * or other data feeds like a depth sensor.
- *
- * @augments Texture
- */
-class ExternalTexture extends Texture {
-
- /**
- * Creates a new raw texture.
- *
- * @param {?(WebGLTexture|GPUTexture)} [sourceTexture=null] - The external texture.
- */
- constructor( sourceTexture = null ) {
-
- super();
-
- /**
- * The external source texture.
- *
- * @type {?(WebGLTexture|GPUTexture)}
- * @default null
- */
- this.sourceTexture = sourceTexture;
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isExternalTexture = true;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.sourceTexture = source.sourceTexture;
-
- return this;
-
- }
-
-}
-
-/**
- * A geometry class for a rectangular cuboid with a given width, height, and depth.
- * On creation, the cuboid is centred on the origin, with each edge parallel to one
- * of the axes.
- *
- * ```js
- * const geometry = new THREE.BoxGeometry( 1, 1, 1 );
- * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
- * const cube = new THREE.Mesh( geometry, material );
- * scene.add( cube );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#BoxGeometry
- */
-class BoxGeometry extends BufferGeometry {
-
- /**
- * Constructs a new box geometry.
- *
- * @param {number} [width=1] - The width. That is, the length of the edges parallel to the X axis.
- * @param {number} [height=1] - The height. That is, the length of the edges parallel to the Y axis.
- * @param {number} [depth=1] - The depth. That is, the length of the edges parallel to the Z axis.
- * @param {number} [widthSegments=1] - Number of segmented rectangular faces along the width of the sides.
- * @param {number} [heightSegments=1] - Number of segmented rectangular faces along the height of the sides.
- * @param {number} [depthSegments=1] - Number of segmented rectangular faces along the depth of the sides.
- */
- constructor( width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 ) {
-
- super();
-
- this.type = 'BoxGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- width: width,
- height: height,
- depth: depth,
- widthSegments: widthSegments,
- heightSegments: heightSegments,
- depthSegments: depthSegments
- };
-
- const scope = this;
-
- // segments
-
- widthSegments = Math.floor( widthSegments );
- heightSegments = Math.floor( heightSegments );
- depthSegments = Math.floor( depthSegments );
-
- // buffers
-
- const indices = [];
- const vertices = [];
- const normals = [];
- const uvs = [];
-
- // helper variables
-
- let numberOfVertices = 0;
- let groupStart = 0;
-
- // build each side of the box geometry
-
- buildPlane( 'z', 'y', 'x', -1, -1, depth, height, width, depthSegments, heightSegments, 0 ); // px
- buildPlane( 'z', 'y', 'x', 1, -1, depth, height, - width, depthSegments, heightSegments, 1 ); // nx
- buildPlane( 'x', 'z', 'y', 1, 1, width, depth, height, widthSegments, depthSegments, 2 ); // py
- buildPlane( 'x', 'z', 'y', 1, -1, width, depth, - height, widthSegments, depthSegments, 3 ); // ny
- buildPlane( 'x', 'y', 'z', 1, -1, width, height, depth, widthSegments, heightSegments, 4 ); // pz
- buildPlane( 'x', 'y', 'z', -1, -1, width, height, - depth, widthSegments, heightSegments, 5 ); // nz
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
- function buildPlane( u, v, w, udir, vdir, width, height, depth, gridX, gridY, materialIndex ) {
-
- const segmentWidth = width / gridX;
- const segmentHeight = height / gridY;
-
- const widthHalf = width / 2;
- const heightHalf = height / 2;
- const depthHalf = depth / 2;
-
- const gridX1 = gridX + 1;
- const gridY1 = gridY + 1;
-
- let vertexCounter = 0;
- let groupCount = 0;
-
- const vector = new Vector3();
-
- // generate vertices, normals and uvs
-
- for ( let iy = 0; iy < gridY1; iy ++ ) {
-
- const y = iy * segmentHeight - heightHalf;
-
- for ( let ix = 0; ix < gridX1; ix ++ ) {
-
- const x = ix * segmentWidth - widthHalf;
-
- // set values to correct vector component
-
- vector[ u ] = x * udir;
- vector[ v ] = y * vdir;
- vector[ w ] = depthHalf;
-
- // now apply vector to vertex buffer
-
- vertices.push( vector.x, vector.y, vector.z );
-
- // set values to correct vector component
-
- vector[ u ] = 0;
- vector[ v ] = 0;
- vector[ w ] = depth > 0 ? 1 : -1;
-
- // now apply vector to normal buffer
-
- normals.push( vector.x, vector.y, vector.z );
-
- // uvs
-
- uvs.push( ix / gridX );
- uvs.push( 1 - ( iy / gridY ) );
-
- // counters
-
- vertexCounter += 1;
-
- }
-
- }
-
- // indices
-
- // 1. you need three indices to draw a single face
- // 2. a single segment consists of two faces
- // 3. so we need to generate six (2*3) indices per segment
-
- for ( let iy = 0; iy < gridY; iy ++ ) {
-
- for ( let ix = 0; ix < gridX; ix ++ ) {
-
- const a = numberOfVertices + ix + gridX1 * iy;
- const b = numberOfVertices + ix + gridX1 * ( iy + 1 );
- const c = numberOfVertices + ( ix + 1 ) + gridX1 * ( iy + 1 );
- const d = numberOfVertices + ( ix + 1 ) + gridX1 * iy;
-
- // faces
-
- indices.push( a, b, d );
- indices.push( b, c, d );
-
- // increase counter
-
- groupCount += 6;
-
- }
-
- }
-
- // add a group to the geometry. this will ensure multi material support
-
- scope.addGroup( groupStart, groupCount, materialIndex );
-
- // calculate new start value for groups
-
- groupStart += groupCount;
-
- // update total number of vertices
-
- numberOfVertices += vertexCounter;
-
- }
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {BoxGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new BoxGeometry( data.width, data.height, data.depth, data.widthSegments, data.heightSegments, data.depthSegments );
-
- }
-
-}
-
-/**
- * A geometry class for representing a capsule.
- *
- * ```js
- * const geometry = new THREE.CapsuleGeometry( 1, 1, 4, 8, 1 );
- * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
- * const capsule = new THREE.Mesh( geometry, material );
- * scene.add( capsule );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#CapsuleGeometry
- */
-class CapsuleGeometry extends BufferGeometry {
-
- /**
- * Constructs a new capsule geometry.
- *
- * @param {number} [radius=1] - Radius of the capsule.
- * @param {number} [height=1] - Height of the middle section.
- * @param {number} [capSegments=4] - Number of curve segments used to build each cap.
- * @param {number} [radialSegments=8] - Number of segmented faces around the circumference of the capsule. Must be an integer >= 3.
- * @param {number} [heightSegments=1] - Number of rows of faces along the height of the middle section. Must be an integer >= 1.
- */
- constructor( radius = 1, height = 1, capSegments = 4, radialSegments = 8, heightSegments = 1 ) {
-
- super();
-
- this.type = 'CapsuleGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radius: radius,
- height: height,
- capSegments: capSegments,
- radialSegments: radialSegments,
- heightSegments: heightSegments,
- };
-
- height = Math.max( 0, height );
- capSegments = Math.max( 1, Math.floor( capSegments ) );
- radialSegments = Math.max( 3, Math.floor( radialSegments ) );
- heightSegments = Math.max( 1, Math.floor( heightSegments ) );
-
- // buffers
-
- const indices = [];
- const vertices = [];
- const normals = [];
- const uvs = [];
-
- // helper variables
-
- const halfHeight = height / 2;
- const capArcLength = ( Math.PI / 2 ) * radius;
- const cylinderPartLength = height;
- const totalArcLength = 2 * capArcLength + cylinderPartLength;
-
- const numVerticalSegments = capSegments * 2 + heightSegments;
- const verticesPerRow = radialSegments + 1;
-
- const normal = new Vector3();
- const vertex = new Vector3();
-
- // generate vertices, normals, and uvs
-
- for ( let iy = 0; iy <= numVerticalSegments; iy ++ ) {
-
- let currentArcLength = 0;
- let profileY = 0;
- let profileRadius = 0;
- let normalYComponent = 0;
-
- if ( iy <= capSegments ) {
-
- // bottom cap
- const segmentProgress = iy / capSegments;
- const angle = ( segmentProgress * Math.PI ) / 2;
- profileY = - halfHeight - radius * Math.cos( angle );
- profileRadius = radius * Math.sin( angle );
- normalYComponent = - radius * Math.cos( angle );
- currentArcLength = segmentProgress * capArcLength;
-
- } else if ( iy <= capSegments + heightSegments ) {
-
- // middle section
- const segmentProgress = ( iy - capSegments ) / heightSegments;
- profileY = - halfHeight + segmentProgress * height;
- profileRadius = radius;
- normalYComponent = 0;
- currentArcLength = capArcLength + segmentProgress * cylinderPartLength;
-
- } else {
-
- // top cap
- const segmentProgress =
- ( iy - capSegments - heightSegments ) / capSegments;
- const angle = ( segmentProgress * Math.PI ) / 2;
- profileY = halfHeight + radius * Math.sin( angle );
- profileRadius = radius * Math.cos( angle );
- normalYComponent = radius * Math.sin( angle );
- currentArcLength =
- capArcLength + cylinderPartLength + segmentProgress * capArcLength;
-
- }
-
- const v = Math.max( 0, Math.min( 1, currentArcLength / totalArcLength ) );
-
-
- // special case for the poles
-
- let uOffset = 0;
-
- if ( iy === 0 ) {
-
- uOffset = 0.5 / radialSegments;
-
- } else if ( iy === numVerticalSegments ) {
-
- uOffset = -0.5 / radialSegments;
-
- }
-
- for ( let ix = 0; ix <= radialSegments; ix ++ ) {
-
- const u = ix / radialSegments;
- const theta = u * Math.PI * 2;
-
- const sinTheta = Math.sin( theta );
- const cosTheta = Math.cos( theta );
-
- // vertex
-
- vertex.x = - profileRadius * cosTheta;
- vertex.y = profileY;
- vertex.z = profileRadius * sinTheta;
- vertices.push( vertex.x, vertex.y, vertex.z );
-
- // normal
-
- normal.set(
- - profileRadius * cosTheta,
- normalYComponent,
- profileRadius * sinTheta
- );
- normal.normalize();
- normals.push( normal.x, normal.y, normal.z );
-
- // uv
-
- uvs.push( u + uOffset, v );
-
- }
-
- if ( iy > 0 ) {
-
- const prevIndexRow = ( iy - 1 ) * verticesPerRow;
- for ( let ix = 0; ix < radialSegments; ix ++ ) {
-
- const i1 = prevIndexRow + ix;
- const i2 = prevIndexRow + ix + 1;
- const i3 = iy * verticesPerRow + ix;
- const i4 = iy * verticesPerRow + ix + 1;
-
- indices.push( i1, i2, i3 );
- indices.push( i2, i4, i3 );
-
- }
-
- }
-
- }
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {CapsuleGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new CapsuleGeometry( data.radius, data.height, data.capSegments, data.radialSegments, data.heightSegments );
-
- }
-
-}
-
-/**
- * A simple shape of Euclidean geometry. It is constructed from a
- * number of triangular segments that are oriented around a central point and
- * extend as far out as a given radius. It is built counter-clockwise from a
- * start angle and a given central angle. It can also be used to create
- * regular polygons, where the number of segments determines the number of
- * sides.
- *
- * ```js
- * const geometry = new THREE.CircleGeometry( 5, 32 );
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const circle = new THREE.Mesh( geometry, material );
- * scene.add( circle )
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#CircleGeometry
- */
-class CircleGeometry extends BufferGeometry {
-
- /**
- * Constructs a new circle geometry.
- *
- * @param {number} [radius=1] - Radius of the circle.
- * @param {number} [segments=32] - Number of segments (triangles), minimum = `3`.
- * @param {number} [thetaStart=0] - Start angle for first segment in radians.
- * @param {number} [thetaLength=Math.PI*2] - The central angle, often called theta,
- * of the circular sector in radians. The default value results in a complete circle.
- */
- constructor( radius = 1, segments = 32, thetaStart = 0, thetaLength = Math.PI * 2 ) {
-
- super();
-
- this.type = 'CircleGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radius: radius,
- segments: segments,
- thetaStart: thetaStart,
- thetaLength: thetaLength
- };
-
- segments = Math.max( 3, segments );
-
- // buffers
-
- const indices = [];
- const vertices = [];
- const normals = [];
- const uvs = [];
-
- // helper variables
-
- const vertex = new Vector3();
- const uv = new Vector2();
-
- // center point
-
- vertices.push( 0, 0, 0 );
- normals.push( 0, 0, 1 );
- uvs.push( 0.5, 0.5 );
-
- for ( let s = 0, i = 3; s <= segments; s ++, i += 3 ) {
-
- const segment = thetaStart + s / segments * thetaLength;
-
- // vertex
-
- vertex.x = radius * Math.cos( segment );
- vertex.y = radius * Math.sin( segment );
-
- vertices.push( vertex.x, vertex.y, vertex.z );
-
- // normal
-
- normals.push( 0, 0, 1 );
-
- // uvs
-
- uv.x = ( vertices[ i ] / radius + 1 ) / 2;
- uv.y = ( vertices[ i + 1 ] / radius + 1 ) / 2;
-
- uvs.push( uv.x, uv.y );
-
- }
-
- // indices
-
- for ( let i = 1; i <= segments; i ++ ) {
-
- indices.push( i, i + 1, 0 );
-
- }
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {CircleGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new CircleGeometry( data.radius, data.segments, data.thetaStart, data.thetaLength );
-
- }
-
-}
-
-/**
- * A geometry class for representing a cylinder.
- *
- * ```js
- * const geometry = new THREE.CylinderGeometry( 5, 5, 20, 32 );
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const cylinder = new THREE.Mesh( geometry, material );
- * scene.add( cylinder );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#CylinderGeometry
- */
-class CylinderGeometry extends BufferGeometry {
-
- /**
- * Constructs a new cylinder geometry.
- *
- * @param {number} [radiusTop=1] - Radius of the cylinder at the top.
- * @param {number} [radiusBottom=1] - Radius of the cylinder at the bottom.
- * @param {number} [height=1] - Height of the cylinder.
- * @param {number} [radialSegments=32] - Number of segmented faces around the circumference of the cylinder.
- * @param {number} [heightSegments=1] - Number of rows of faces along the height of the cylinder.
- * @param {boolean} [openEnded=false] - Whether the base of the cylinder is open or capped.
- * @param {number} [thetaStart=0] - Start angle for first segment, in radians.
- * @param {number} [thetaLength=Math.PI*2] - The central angle, often called theta, of the circular sector, in radians.
- * The default value results in a complete cylinder.
- */
- constructor( radiusTop = 1, radiusBottom = 1, height = 1, radialSegments = 32, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2 ) {
-
- super();
-
- this.type = 'CylinderGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radiusTop: radiusTop,
- radiusBottom: radiusBottom,
- height: height,
- radialSegments: radialSegments,
- heightSegments: heightSegments,
- openEnded: openEnded,
- thetaStart: thetaStart,
- thetaLength: thetaLength
- };
-
- const scope = this;
-
- radialSegments = Math.floor( radialSegments );
- heightSegments = Math.floor( heightSegments );
-
- // buffers
-
- const indices = [];
- const vertices = [];
- const normals = [];
- const uvs = [];
-
- // helper variables
-
- let index = 0;
- const indexArray = [];
- const halfHeight = height / 2;
- let groupStart = 0;
-
- // generate geometry
-
- generateTorso();
-
- if ( openEnded === false ) {
-
- if ( radiusTop > 0 ) generateCap( true );
- if ( radiusBottom > 0 ) generateCap( false );
-
- }
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
- function generateTorso() {
-
- const normal = new Vector3();
- const vertex = new Vector3();
-
- let groupCount = 0;
-
- // this will be used to calculate the normal
- const slope = ( radiusBottom - radiusTop ) / height;
-
- // generate vertices, normals and uvs
-
- for ( let y = 0; y <= heightSegments; y ++ ) {
-
- const indexRow = [];
-
- const v = y / heightSegments;
-
- // calculate the radius of the current row
-
- const radius = v * ( radiusBottom - radiusTop ) + radiusTop;
-
- for ( let x = 0; x <= radialSegments; x ++ ) {
-
- const u = x / radialSegments;
-
- const theta = u * thetaLength + thetaStart;
-
- const sinTheta = Math.sin( theta );
- const cosTheta = Math.cos( theta );
-
- // vertex
-
- vertex.x = radius * sinTheta;
- vertex.y = - v * height + halfHeight;
- vertex.z = radius * cosTheta;
- vertices.push( vertex.x, vertex.y, vertex.z );
-
- // normal
-
- normal.set( sinTheta, slope, cosTheta ).normalize();
- normals.push( normal.x, normal.y, normal.z );
-
- // uv
-
- uvs.push( u, 1 - v );
-
- // save index of vertex in respective row
-
- indexRow.push( index ++ );
-
- }
-
- // now save vertices of the row in our index array
-
- indexArray.push( indexRow );
-
- }
-
- // generate indices
-
- for ( let x = 0; x < radialSegments; x ++ ) {
-
- for ( let y = 0; y < heightSegments; y ++ ) {
-
- // we use the index array to access the correct indices
-
- const a = indexArray[ y ][ x ];
- const b = indexArray[ y + 1 ][ x ];
- const c = indexArray[ y + 1 ][ x + 1 ];
- const d = indexArray[ y ][ x + 1 ];
-
- // faces
-
- if ( radiusTop > 0 || y !== 0 ) {
-
- indices.push( a, b, d );
- groupCount += 3;
-
- }
-
- if ( radiusBottom > 0 || y !== heightSegments - 1 ) {
-
- indices.push( b, c, d );
- groupCount += 3;
-
- }
-
- }
-
- }
-
- // add a group to the geometry. this will ensure multi material support
-
- scope.addGroup( groupStart, groupCount, 0 );
-
- // calculate new start value for groups
-
- groupStart += groupCount;
-
- }
-
- function generateCap( top ) {
-
- // save the index of the first center vertex
- const centerIndexStart = index;
-
- const uv = new Vector2();
- const vertex = new Vector3();
-
- let groupCount = 0;
-
- const radius = ( top === true ) ? radiusTop : radiusBottom;
- const sign = ( top === true ) ? 1 : -1;
-
- // first we generate the center vertex data of the cap.
- // because the geometry needs one set of uvs per face,
- // we must generate a center vertex per face/segment
-
- for ( let x = 1; x <= radialSegments; x ++ ) {
-
- // vertex
-
- vertices.push( 0, halfHeight * sign, 0 );
-
- // normal
-
- normals.push( 0, sign, 0 );
-
- // uv
-
- uvs.push( 0.5, 0.5 );
-
- // increase index
-
- index ++;
-
- }
-
- // save the index of the last center vertex
- const centerIndexEnd = index;
-
- // now we generate the surrounding vertices, normals and uvs
-
- for ( let x = 0; x <= radialSegments; x ++ ) {
-
- const u = x / radialSegments;
- const theta = u * thetaLength + thetaStart;
-
- const cosTheta = Math.cos( theta );
- const sinTheta = Math.sin( theta );
-
- // vertex
-
- vertex.x = radius * sinTheta;
- vertex.y = halfHeight * sign;
- vertex.z = radius * cosTheta;
- vertices.push( vertex.x, vertex.y, vertex.z );
-
- // normal
-
- normals.push( 0, sign, 0 );
-
- // uv
-
- uv.x = ( cosTheta * 0.5 ) + 0.5;
- uv.y = ( sinTheta * 0.5 * sign ) + 0.5;
- uvs.push( uv.x, uv.y );
-
- // increase index
-
- index ++;
-
- }
-
- // generate indices
-
- for ( let x = 0; x < radialSegments; x ++ ) {
-
- const c = centerIndexStart + x;
- const i = centerIndexEnd + x;
-
- if ( top === true ) {
-
- // face top
-
- indices.push( i, i + 1, c );
-
- } else {
-
- // face bottom
-
- indices.push( i + 1, i, c );
-
- }
-
- groupCount += 3;
-
- }
-
- // add a group to the geometry. this will ensure multi material support
-
- scope.addGroup( groupStart, groupCount, top === true ? 1 : 2 );
-
- // calculate new start value for groups
-
- groupStart += groupCount;
-
- }
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {CylinderGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new CylinderGeometry( data.radiusTop, data.radiusBottom, data.height, data.radialSegments, data.heightSegments, data.openEnded, data.thetaStart, data.thetaLength );
-
- }
-
-}
-
-/**
- * A geometry class for representing a cone.
- *
- * ```js
- * const geometry = new THREE.ConeGeometry( 5, 20, 32 );
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const cone = new THREE.Mesh(geometry, material );
- * scene.add( cone );
- * ```
- *
- * @augments CylinderGeometry
- * @demo scenes/geometry-browser.html#ConeGeometry
- */
-class ConeGeometry extends CylinderGeometry {
-
- /**
- * Constructs a new cone geometry.
- *
- * @param {number} [radius=1] - Radius of the cone base.
- * @param {number} [height=1] - Height of the cone.
- * @param {number} [radialSegments=32] - Number of segmented faces around the circumference of the cone.
- * @param {number} [heightSegments=1] - Number of rows of faces along the height of the cone.
- * @param {boolean} [openEnded=false] - Whether the base of the cone is open or capped.
- * @param {number} [thetaStart=0] - Start angle for first segment, in radians.
- * @param {number} [thetaLength=Math.PI*2] - The central angle, often called theta, of the circular sector, in radians.
- * The default value results in a complete cone.
- */
- constructor( radius = 1, height = 1, radialSegments = 32, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2 ) {
-
- super( 0, radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength );
-
- this.type = 'ConeGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radius: radius,
- height: height,
- radialSegments: radialSegments,
- heightSegments: heightSegments,
- openEnded: openEnded,
- thetaStart: thetaStart,
- thetaLength: thetaLength
- };
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {ConeGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new ConeGeometry( data.radius, data.height, data.radialSegments, data.heightSegments, data.openEnded, data.thetaStart, data.thetaLength );
-
- }
-
-}
-
-/**
- * A polyhedron is a solid in three dimensions with flat faces. This class
- * will take an array of vertices, project them onto a sphere, and then
- * divide them up to the desired level of detail.
- *
- * @augments BufferGeometry
- */
-class PolyhedronGeometry extends BufferGeometry {
-
- /**
- * Constructs a new polyhedron geometry.
- *
- * @param {Array} [vertices] - A flat array of vertices describing the base shape.
- * @param {Array} [indices] - A flat array of indices describing the base shape.
- * @param {number} [radius=1] - The radius of the shape.
- * @param {number} [detail=0] - How many levels to subdivide the geometry. The more detail, the smoother the shape.
- */
- constructor( vertices = [], indices = [], radius = 1, detail = 0 ) {
-
- super();
-
- this.type = 'PolyhedronGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- vertices: vertices,
- indices: indices,
- radius: radius,
- detail: detail
- };
-
- // default buffer data
-
- const vertexBuffer = [];
- const uvBuffer = [];
-
- // the subdivision creates the vertex buffer data
-
- subdivide( detail );
-
- // all vertices should lie on a conceptual sphere with a given radius
-
- applyRadius( radius );
-
- // finally, create the uv data
-
- generateUVs();
-
- // build non-indexed geometry
-
- this.setAttribute( 'position', new Float32BufferAttribute( vertexBuffer, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( vertexBuffer.slice(), 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvBuffer, 2 ) );
-
- if ( detail === 0 ) {
-
- this.computeVertexNormals(); // flat normals
-
- } else {
-
- this.normalizeNormals(); // smooth normals
-
- }
-
- // helper functions
-
- function subdivide( detail ) {
-
- const a = new Vector3();
- const b = new Vector3();
- const c = new Vector3();
-
- // iterate over all faces and apply a subdivision with the given detail value
-
- for ( let i = 0; i < indices.length; i += 3 ) {
-
- // get the vertices of the face
-
- getVertexByIndex( indices[ i + 0 ], a );
- getVertexByIndex( indices[ i + 1 ], b );
- getVertexByIndex( indices[ i + 2 ], c );
-
- // perform subdivision
-
- subdivideFace( a, b, c, detail );
-
- }
-
- }
-
- function subdivideFace( a, b, c, detail ) {
-
- const cols = detail + 1;
-
- // we use this multidimensional array as a data structure for creating the subdivision
-
- const v = [];
-
- // construct all of the vertices for this subdivision
-
- for ( let i = 0; i <= cols; i ++ ) {
-
- v[ i ] = [];
-
- const aj = a.clone().lerp( c, i / cols );
- const bj = b.clone().lerp( c, i / cols );
-
- const rows = cols - i;
-
- for ( let j = 0; j <= rows; j ++ ) {
-
- if ( j === 0 && i === cols ) {
-
- v[ i ][ j ] = aj;
-
- } else {
-
- v[ i ][ j ] = aj.clone().lerp( bj, j / rows );
-
- }
-
- }
-
- }
-
- // construct all of the faces
-
- for ( let i = 0; i < cols; i ++ ) {
-
- for ( let j = 0; j < 2 * ( cols - i ) - 1; j ++ ) {
-
- const k = Math.floor( j / 2 );
-
- if ( j % 2 === 0 ) {
-
- pushVertex( v[ i ][ k + 1 ] );
- pushVertex( v[ i + 1 ][ k ] );
- pushVertex( v[ i ][ k ] );
-
- } else {
-
- pushVertex( v[ i ][ k + 1 ] );
- pushVertex( v[ i + 1 ][ k + 1 ] );
- pushVertex( v[ i + 1 ][ k ] );
-
- }
-
- }
-
- }
-
- }
-
- function applyRadius( radius ) {
-
- const vertex = new Vector3();
-
- // iterate over the entire buffer and apply the radius to each vertex
-
- for ( let i = 0; i < vertexBuffer.length; i += 3 ) {
-
- vertex.x = vertexBuffer[ i + 0 ];
- vertex.y = vertexBuffer[ i + 1 ];
- vertex.z = vertexBuffer[ i + 2 ];
-
- vertex.normalize().multiplyScalar( radius );
-
- vertexBuffer[ i + 0 ] = vertex.x;
- vertexBuffer[ i + 1 ] = vertex.y;
- vertexBuffer[ i + 2 ] = vertex.z;
-
- }
-
- }
-
- function generateUVs() {
-
- const vertex = new Vector3();
-
- for ( let i = 0; i < vertexBuffer.length; i += 3 ) {
-
- vertex.x = vertexBuffer[ i + 0 ];
- vertex.y = vertexBuffer[ i + 1 ];
- vertex.z = vertexBuffer[ i + 2 ];
-
- const u = azimuth( vertex ) / 2 / Math.PI + 0.5;
- const v = inclination( vertex ) / Math.PI + 0.5;
- uvBuffer.push( u, 1 - v );
-
- }
-
- correctUVs();
-
- correctSeam();
-
- }
-
- function correctSeam() {
-
- // handle case when face straddles the seam, see #3269
-
- for ( let i = 0; i < uvBuffer.length; i += 6 ) {
-
- // uv data of a single face
-
- const x0 = uvBuffer[ i + 0 ];
- const x1 = uvBuffer[ i + 2 ];
- const x2 = uvBuffer[ i + 4 ];
-
- const max = Math.max( x0, x1, x2 );
- const min = Math.min( x0, x1, x2 );
-
- // 0.9 is somewhat arbitrary
-
- if ( max > 0.9 && min < 0.1 ) {
-
- if ( x0 < 0.2 ) uvBuffer[ i + 0 ] += 1;
- if ( x1 < 0.2 ) uvBuffer[ i + 2 ] += 1;
- if ( x2 < 0.2 ) uvBuffer[ i + 4 ] += 1;
-
- }
-
- }
-
- }
-
- function pushVertex( vertex ) {
-
- vertexBuffer.push( vertex.x, vertex.y, vertex.z );
-
- }
-
- function getVertexByIndex( index, vertex ) {
-
- const stride = index * 3;
-
- vertex.x = vertices[ stride + 0 ];
- vertex.y = vertices[ stride + 1 ];
- vertex.z = vertices[ stride + 2 ];
-
- }
-
- function correctUVs() {
-
- const a = new Vector3();
- const b = new Vector3();
- const c = new Vector3();
-
- const centroid = new Vector3();
-
- const uvA = new Vector2();
- const uvB = new Vector2();
- const uvC = new Vector2();
-
- for ( let i = 0, j = 0; i < vertexBuffer.length; i += 9, j += 6 ) {
-
- a.set( vertexBuffer[ i + 0 ], vertexBuffer[ i + 1 ], vertexBuffer[ i + 2 ] );
- b.set( vertexBuffer[ i + 3 ], vertexBuffer[ i + 4 ], vertexBuffer[ i + 5 ] );
- c.set( vertexBuffer[ i + 6 ], vertexBuffer[ i + 7 ], vertexBuffer[ i + 8 ] );
-
- uvA.set( uvBuffer[ j + 0 ], uvBuffer[ j + 1 ] );
- uvB.set( uvBuffer[ j + 2 ], uvBuffer[ j + 3 ] );
- uvC.set( uvBuffer[ j + 4 ], uvBuffer[ j + 5 ] );
-
- centroid.copy( a ).add( b ).add( c ).divideScalar( 3 );
-
- const azi = azimuth( centroid );
-
- correctUV( uvA, j + 0, a, azi );
- correctUV( uvB, j + 2, b, azi );
- correctUV( uvC, j + 4, c, azi );
-
- }
-
- }
-
- function correctUV( uv, stride, vector, azimuth ) {
-
- if ( ( azimuth < 0 ) && ( uv.x === 1 ) ) {
-
- uvBuffer[ stride ] = uv.x - 1;
-
- }
-
- if ( ( vector.x === 0 ) && ( vector.z === 0 ) ) {
-
- uvBuffer[ stride ] = azimuth / 2 / Math.PI + 0.5;
-
- }
-
- }
-
- // Angle around the Y axis, counter-clockwise when looking from above.
-
- function azimuth( vector ) {
-
- return Math.atan2( vector.z, - vector.x );
-
- }
-
-
- // Angle above the XZ plane.
-
- function inclination( vector ) {
-
- return Math.atan2( - vector.y, Math.sqrt( ( vector.x * vector.x ) + ( vector.z * vector.z ) ) );
-
- }
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {PolyhedronGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new PolyhedronGeometry( data.vertices, data.indices, data.radius, data.detail );
-
- }
-
-}
-
-/**
- * A geometry class for representing a dodecahedron.
- *
- * ```js
- * const geometry = new THREE.DodecahedronGeometry();
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const dodecahedron = new THREE.Mesh( geometry, material );
- * scene.add( dodecahedron );
- * ```
- *
- * @augments PolyhedronGeometry
- * @demo scenes/geometry-browser.html#DodecahedronGeometry
- */
-class DodecahedronGeometry extends PolyhedronGeometry {
-
- /**
- * Constructs a new dodecahedron geometry.
- *
- * @param {number} [radius=1] - Radius of the dodecahedron.
- * @param {number} [detail=0] - Setting this to a value greater than `0` adds vertices making it no longer a dodecahedron.
- */
- constructor( radius = 1, detail = 0 ) {
-
- const t = ( 1 + Math.sqrt( 5 ) ) / 2;
- const r = 1 / t;
-
- const vertices = [
-
- // (±1, ±1, ±1)
- -1, -1, -1, -1, -1, 1,
- -1, 1, -1, -1, 1, 1,
- 1, -1, -1, 1, -1, 1,
- 1, 1, -1, 1, 1, 1,
-
- // (0, ±1/φ, ±φ)
- 0, - r, - t, 0, - r, t,
- 0, r, - t, 0, r, t,
-
- // (±1/φ, ±φ, 0)
- - r, - t, 0, - r, t, 0,
- r, - t, 0, r, t, 0,
-
- // (±φ, 0, ±1/φ)
- - t, 0, - r, t, 0, - r,
- - t, 0, r, t, 0, r
- ];
-
- const indices = [
- 3, 11, 7, 3, 7, 15, 3, 15, 13,
- 7, 19, 17, 7, 17, 6, 7, 6, 15,
- 17, 4, 8, 17, 8, 10, 17, 10, 6,
- 8, 0, 16, 8, 16, 2, 8, 2, 10,
- 0, 12, 1, 0, 1, 18, 0, 18, 16,
- 6, 10, 2, 6, 2, 13, 6, 13, 15,
- 2, 16, 18, 2, 18, 3, 2, 3, 13,
- 18, 1, 9, 18, 9, 11, 18, 11, 3,
- 4, 14, 12, 4, 12, 0, 4, 0, 8,
- 11, 9, 5, 11, 5, 19, 11, 19, 7,
- 19, 5, 14, 19, 14, 4, 19, 4, 17,
- 1, 12, 14, 1, 14, 5, 1, 5, 9
- ];
-
- super( vertices, indices, radius, detail );
-
- this.type = 'DodecahedronGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radius: radius,
- detail: detail
- };
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {DodecahedronGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new DodecahedronGeometry( data.radius, data.detail );
-
- }
-
-}
-
-const _v0 = /*@__PURE__*/ new Vector3();
-const _v1$1 = /*@__PURE__*/ new Vector3();
-const _normal = /*@__PURE__*/ new Vector3();
-const _triangle = /*@__PURE__*/ new Triangle();
-
-/**
- * Can be used as a helper object to view the edges of a geometry.
- *
- * ```js
- * const geometry = new THREE.BoxGeometry();
- * const edges = new THREE.EdgesGeometry( geometry );
- * const line = new THREE.LineSegments( edges );
- * scene.add( line );
- * ```
- *
- * Note: It is not yet possible to serialize/deserialize instances of this class.
- *
- * @augments BufferGeometry
- */
-class EdgesGeometry extends BufferGeometry {
-
- /**
- * Constructs a new edges geometry.
- *
- * @param {?BufferGeometry} [geometry=null] - The geometry.
- * @param {number} [thresholdAngle=1] - An edge is only rendered if the angle (in degrees)
- * between the face normals of the adjoining faces exceeds this value.
- */
- constructor( geometry = null, thresholdAngle = 1 ) {
-
- super();
-
- this.type = 'EdgesGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- geometry: geometry,
- thresholdAngle: thresholdAngle
- };
-
- if ( geometry !== null ) {
-
- const precisionPoints = 4;
- const precision = Math.pow( 10, precisionPoints );
- const thresholdDot = Math.cos( DEG2RAD * thresholdAngle );
-
- const indexAttr = geometry.getIndex();
- const positionAttr = geometry.getAttribute( 'position' );
- const indexCount = indexAttr ? indexAttr.count : positionAttr.count;
-
- const indexArr = [ 0, 0, 0 ];
- const vertKeys = [ 'a', 'b', 'c' ];
- const hashes = new Array( 3 );
-
- const edgeData = {};
- const vertices = [];
- for ( let i = 0; i < indexCount; i += 3 ) {
-
- if ( indexAttr ) {
-
- indexArr[ 0 ] = indexAttr.getX( i );
- indexArr[ 1 ] = indexAttr.getX( i + 1 );
- indexArr[ 2 ] = indexAttr.getX( i + 2 );
-
- } else {
-
- indexArr[ 0 ] = i;
- indexArr[ 1 ] = i + 1;
- indexArr[ 2 ] = i + 2;
-
- }
-
- const { a, b, c } = _triangle;
- a.fromBufferAttribute( positionAttr, indexArr[ 0 ] );
- b.fromBufferAttribute( positionAttr, indexArr[ 1 ] );
- c.fromBufferAttribute( positionAttr, indexArr[ 2 ] );
- _triangle.getNormal( _normal );
-
- // create hashes for the edge from the vertices
- hashes[ 0 ] = `${ Math.round( a.x * precision ) },${ Math.round( a.y * precision ) },${ Math.round( a.z * precision ) }`;
- hashes[ 1 ] = `${ Math.round( b.x * precision ) },${ Math.round( b.y * precision ) },${ Math.round( b.z * precision ) }`;
- hashes[ 2 ] = `${ Math.round( c.x * precision ) },${ Math.round( c.y * precision ) },${ Math.round( c.z * precision ) }`;
-
- // skip degenerate triangles
- if ( hashes[ 0 ] === hashes[ 1 ] || hashes[ 1 ] === hashes[ 2 ] || hashes[ 2 ] === hashes[ 0 ] ) {
-
- continue;
-
- }
-
- // iterate over every edge
- for ( let j = 0; j < 3; j ++ ) {
-
- // get the first and next vertex making up the edge
- const jNext = ( j + 1 ) % 3;
- const vecHash0 = hashes[ j ];
- const vecHash1 = hashes[ jNext ];
- const v0 = _triangle[ vertKeys[ j ] ];
- const v1 = _triangle[ vertKeys[ jNext ] ];
-
- const hash = `${ vecHash0 }_${ vecHash1 }`;
- const reverseHash = `${ vecHash1 }_${ vecHash0 }`;
-
- if ( reverseHash in edgeData && edgeData[ reverseHash ] ) {
-
- // if we found a sibling edge add it into the vertex array if
- // it meets the angle threshold and delete the edge from the map.
- if ( _normal.dot( edgeData[ reverseHash ].normal ) <= thresholdDot ) {
-
- vertices.push( v0.x, v0.y, v0.z );
- vertices.push( v1.x, v1.y, v1.z );
-
- }
-
- edgeData[ reverseHash ] = null;
-
- } else if ( ! ( hash in edgeData ) ) {
-
- // if we've already got an edge here then skip adding a new one
- edgeData[ hash ] = {
-
- index0: indexArr[ j ],
- index1: indexArr[ jNext ],
- normal: _normal.clone(),
-
- };
-
- }
-
- }
-
- }
-
- // iterate over all remaining, unmatched edges and add them to the vertex array
- for ( const key in edgeData ) {
-
- if ( edgeData[ key ] ) {
-
- const { index0, index1 } = edgeData[ key ];
- _v0.fromBufferAttribute( positionAttr, index0 );
- _v1$1.fromBufferAttribute( positionAttr, index1 );
-
- vertices.push( _v0.x, _v0.y, _v0.z );
- vertices.push( _v1$1.x, _v1$1.y, _v1$1.z );
-
- }
-
- }
-
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
-
- }
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
-}
-
-/**
- * An abstract base class for creating an analytic curve object that contains methods
- * for interpolation.
- *
- * @abstract
- */
-class Curve {
-
- /**
- * Constructs a new curve.
- */
- constructor() {
-
- /**
- * The type property is used for detecting the object type
- * in context of serialization/deserialization.
- *
- * @type {string}
- * @readonly
- */
- this.type = 'Curve';
-
- /**
- * This value determines the amount of divisions when calculating the
- * cumulative segment lengths of a curve via {@link Curve#getLengths}. To ensure
- * precision when using methods like {@link Curve#getSpacedPoints}, it is
- * recommended to increase the value of this property if the curve is very large.
- *
- * @type {number}
- * @default 200
- */
- this.arcLengthDivisions = 200;
-
- /**
- * Must be set to `true` if the curve parameters have changed.
- *
- * @type {boolean}
- * @default false
- */
- this.needsUpdate = false;
-
- /**
- * An internal cache that holds precomputed curve length values.
- *
- * @private
- * @type {?Array}
- * @default null
- */
- this.cacheArcLengths = null;
-
- }
-
- /**
- * This method returns a vector in 2D or 3D space (depending on the curve definition)
- * for the given interpolation factor.
- *
- * @abstract
- * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
- * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.
- * @return {(Vector2|Vector3)} The position on the curve. It can be a 2D or 3D vector depending on the curve definition.
- */
- getPoint( /* t, optionalTarget */ ) {
-
- warn( 'Curve: .getPoint() not implemented.' );
-
- }
-
- /**
- * This method returns a vector in 2D or 3D space (depending on the curve definition)
- * for the given interpolation factor. Unlike {@link Curve#getPoint}, this method honors the length
- * of the curve which equidistant samples.
- *
- * @param {number} u - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
- * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.
- * @return {(Vector2|Vector3)} The position on the curve. It can be a 2D or 3D vector depending on the curve definition.
- */
- getPointAt( u, optionalTarget ) {
-
- const t = this.getUtoTmapping( u );
- return this.getPoint( t, optionalTarget );
-
- }
-
- /**
- * This method samples the curve via {@link Curve#getPoint} and returns an array of points representing
- * the curve shape.
- *
- * @param {number} [divisions=5] - The number of divisions.
- * @return {Array<(Vector2|Vector3)>} An array holding the sampled curve values. The number of points is `divisions + 1`.
- */
- getPoints( divisions = 5 ) {
-
- const points = [];
-
- for ( let d = 0; d <= divisions; d ++ ) {
-
- points.push( this.getPoint( d / divisions ) );
-
- }
-
- return points;
-
- }
-
- // Get sequence of points using getPointAt( u )
-
- /**
- * This method samples the curve via {@link Curve#getPointAt} and returns an array of points representing
- * the curve shape. Unlike {@link Curve#getPoints}, this method returns equi-spaced points across the entire
- * curve.
- *
- * @param {number} [divisions=5] - The number of divisions.
- * @return {Array<(Vector2|Vector3)>} An array holding the sampled curve values. The number of points is `divisions + 1`.
- */
- getSpacedPoints( divisions = 5 ) {
-
- const points = [];
-
- for ( let d = 0; d <= divisions; d ++ ) {
-
- points.push( this.getPointAt( d / divisions ) );
-
- }
-
- return points;
-
- }
-
- /**
- * Returns the total arc length of the curve.
- *
- * @return {number} The length of the curve.
- */
- getLength() {
-
- const lengths = this.getLengths();
- return lengths[ lengths.length - 1 ];
-
- }
-
- /**
- * Returns an array of cumulative segment lengths of the curve.
- *
- * @param {number} [divisions=this.arcLengthDivisions] - The number of divisions.
- * @return {Array} An array holding the cumulative segment lengths.
- */
- getLengths( divisions = this.arcLengthDivisions ) {
-
- if ( this.cacheArcLengths &&
- ( this.cacheArcLengths.length === divisions + 1 ) &&
- ! this.needsUpdate ) {
-
- return this.cacheArcLengths;
-
- }
-
- this.needsUpdate = false;
-
- const cache = [];
- let current, last = this.getPoint( 0 );
- let sum = 0;
-
- cache.push( 0 );
-
- for ( let p = 1; p <= divisions; p ++ ) {
-
- current = this.getPoint( p / divisions );
- sum += current.distanceTo( last );
- cache.push( sum );
- last = current;
-
- }
-
- this.cacheArcLengths = cache;
-
- return cache; // { sums: cache, sum: sum }; Sum is in the last element.
-
- }
-
- /**
- * Update the cumulative segment distance cache. The method must be called
- * every time curve parameters are changed. If an updated curve is part of a
- * composed curve like {@link CurvePath}, this method must be called on the
- * composed curve, too.
- */
- updateArcLengths() {
-
- this.needsUpdate = true;
- this.getLengths();
-
- }
-
- /**
- * Given an interpolation factor in the range `[0,1]`, this method returns an updated
- * interpolation factor in the same range that can be ued to sample equidistant points
- * from a curve.
- *
- * @param {number} u - The interpolation factor.
- * @param {?number} distance - An optional distance on the curve.
- * @return {number} The updated interpolation factor.
- */
- getUtoTmapping( u, distance = null ) {
-
- const arcLengths = this.getLengths();
-
- let i = 0;
- const il = arcLengths.length;
-
- let targetArcLength; // The targeted u distance value to get
-
- if ( distance ) {
-
- targetArcLength = distance;
-
- } else {
-
- targetArcLength = u * arcLengths[ il - 1 ];
-
- }
-
- // binary search for the index with largest value smaller than target u distance
-
- let low = 0, high = il - 1, comparison;
-
- while ( low <= high ) {
-
- i = Math.floor( low + ( high - low ) / 2 ); // less likely to overflow, though probably not issue here, JS doesn't really have integers, all numbers are floats
-
- comparison = arcLengths[ i ] - targetArcLength;
-
- if ( comparison < 0 ) {
-
- low = i + 1;
-
- } else if ( comparison > 0 ) {
-
- high = i - 1;
-
- } else {
-
- high = i;
- break;
-
- // DONE
-
- }
-
- }
-
- i = high;
-
- if ( arcLengths[ i ] === targetArcLength ) {
-
- return i / ( il - 1 );
-
- }
-
- // we could get finer grain at lengths, or use simple interpolation between two points
-
- const lengthBefore = arcLengths[ i ];
- const lengthAfter = arcLengths[ i + 1 ];
-
- const segmentLength = lengthAfter - lengthBefore;
-
- // determine where we are between the 'before' and 'after' points
-
- const segmentFraction = ( targetArcLength - lengthBefore ) / segmentLength;
-
- // add that fractional amount to t
-
- const t = ( i + segmentFraction ) / ( il - 1 );
-
- return t;
-
- }
-
- /**
- * Returns a unit vector tangent for the given interpolation factor.
- * If the derived curve does not implement its tangent derivation,
- * two points a small delta apart will be used to find its gradient
- * which seems to give a reasonable approximation.
- *
- * @param {number} t - The interpolation factor.
- * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.
- * @return {(Vector2|Vector3)} The tangent vector.
- */
- getTangent( t, optionalTarget ) {
-
- const delta = 0.0001;
- let t1 = t - delta;
- let t2 = t + delta;
-
- // Capping in case of danger
-
- if ( t1 < 0 ) t1 = 0;
- if ( t2 > 1 ) t2 = 1;
-
- const pt1 = this.getPoint( t1 );
- const pt2 = this.getPoint( t2 );
-
- const tangent = optionalTarget || ( ( pt1.isVector2 ) ? new Vector2() : new Vector3() );
-
- tangent.copy( pt2 ).sub( pt1 ).normalize();
-
- return tangent;
-
- }
-
- /**
- * Same as {@link Curve#getTangent} but with equidistant samples.
- *
- * @param {number} u - The interpolation factor.
- * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.
- * @return {(Vector2|Vector3)} The tangent vector.
- * @see {@link Curve#getPointAt}
- */
- getTangentAt( u, optionalTarget ) {
-
- const t = this.getUtoTmapping( u );
- return this.getTangent( t, optionalTarget );
-
- }
-
- /**
- * Generates the Frenet Frames. Requires a curve definition in 3D space. Used
- * in geometries like {@link TubeGeometry} or {@link ExtrudeGeometry}.
- *
- * @param {number} segments - The number of segments.
- * @param {boolean} [closed=false] - Whether the curve is closed or not.
- * @return {{tangents: Array, normals: Array, binormals: Array}} The Frenet Frames.
- */
- computeFrenetFrames( segments, closed = false ) {
-
- // see http://www.cs.indiana.edu/pub/techreports/TR425.pdf
-
- const normal = new Vector3();
-
- const tangents = [];
- const normals = [];
- const binormals = [];
-
- const vec = new Vector3();
- const mat = new Matrix4();
-
- // compute the tangent vectors for each segment on the curve
-
- for ( let i = 0; i <= segments; i ++ ) {
-
- const u = i / segments;
-
- tangents[ i ] = this.getTangentAt( u, new Vector3() );
-
- }
-
- // select an initial normal vector perpendicular to the first tangent vector,
- // and in the direction of the minimum tangent xyz component
-
- normals[ 0 ] = new Vector3();
- binormals[ 0 ] = new Vector3();
- let min = Number.MAX_VALUE;
- const tx = Math.abs( tangents[ 0 ].x );
- const ty = Math.abs( tangents[ 0 ].y );
- const tz = Math.abs( tangents[ 0 ].z );
-
- if ( tx <= min ) {
-
- min = tx;
- normal.set( 1, 0, 0 );
-
- }
-
- if ( ty <= min ) {
-
- min = ty;
- normal.set( 0, 1, 0 );
-
- }
-
- if ( tz <= min ) {
-
- normal.set( 0, 0, 1 );
-
- }
-
- vec.crossVectors( tangents[ 0 ], normal ).normalize();
-
- normals[ 0 ].crossVectors( tangents[ 0 ], vec );
- binormals[ 0 ].crossVectors( tangents[ 0 ], normals[ 0 ] );
-
-
- // compute the slowly-varying normal and binormal vectors for each segment on the curve
-
- for ( let i = 1; i <= segments; i ++ ) {
-
- normals[ i ] = normals[ i - 1 ].clone();
-
- binormals[ i ] = binormals[ i - 1 ].clone();
-
- vec.crossVectors( tangents[ i - 1 ], tangents[ i ] );
-
- if ( vec.length() > Number.EPSILON ) {
-
- vec.normalize();
-
- const theta = Math.acos( clamp( tangents[ i - 1 ].dot( tangents[ i ] ), -1, 1 ) ); // clamp for floating pt errors
-
- normals[ i ].applyMatrix4( mat.makeRotationAxis( vec, theta ) );
-
- }
-
- binormals[ i ].crossVectors( tangents[ i ], normals[ i ] );
-
- }
-
- // if the curve is closed, postprocess the vectors so the first and last normal vectors are the same
-
- if ( closed === true ) {
-
- let theta = Math.acos( clamp( normals[ 0 ].dot( normals[ segments ] ), -1, 1 ) );
- theta /= segments;
-
- if ( tangents[ 0 ].dot( vec.crossVectors( normals[ 0 ], normals[ segments ] ) ) > 0 ) {
-
- theta = - theta;
-
- }
-
- for ( let i = 1; i <= segments; i ++ ) {
-
- // twist a little...
- normals[ i ].applyMatrix4( mat.makeRotationAxis( tangents[ i ], theta * i ) );
- binormals[ i ].crossVectors( tangents[ i ], normals[ i ] );
-
- }
-
- }
-
- return {
- tangents: tangents,
- normals: normals,
- binormals: binormals
- };
-
- }
-
- /**
- * Returns a new curve with copied values from this instance.
- *
- * @return {Curve} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Copies the values of the given curve to this instance.
- *
- * @param {Curve} source - The curve to copy.
- * @return {Curve} A reference to this curve.
- */
- copy( source ) {
-
- this.arcLengthDivisions = source.arcLengthDivisions;
-
- return this;
-
- }
-
- /**
- * Serializes the curve into JSON.
- *
- * @return {Object} A JSON object representing the serialized curve.
- * @see {@link ObjectLoader#parse}
- */
- toJSON() {
-
- const data = {
- metadata: {
- version: 4.7,
- type: 'Curve',
- generator: 'Curve.toJSON'
- }
- };
-
- data.arcLengthDivisions = this.arcLengthDivisions;
- data.type = this.type;
-
- return data;
-
- }
-
- /**
- * Deserializes the curve from the given JSON.
- *
- * @param {Object} json - The JSON holding the serialized curve.
- * @return {Curve} A reference to this curve.
- */
- fromJSON( json ) {
-
- this.arcLengthDivisions = json.arcLengthDivisions;
-
- return this;
-
- }
-
-}
-
-/**
- * A curve representing an ellipse.
- *
- * ```js
- * const curve = new THREE.EllipseCurve(
- * 0, 0,
- * 10, 10,
- * 0, 2 * Math.PI,
- * false,
- * 0
- * );
- *
- * const points = curve.getPoints( 50 );
- * const geometry = new THREE.BufferGeometry().setFromPoints( points );
- *
- * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
- *
- * // Create the final object to add to the scene
- * const ellipse = new THREE.Line( geometry, material );
- * ```
- *
- * @augments Curve
- */
-class EllipseCurve extends Curve {
-
- /**
- * Constructs a new ellipse curve.
- *
- * @param {number} [aX=0] - The X center of the ellipse.
- * @param {number} [aY=0] - The Y center of the ellipse.
- * @param {number} [xRadius=1] - The radius of the ellipse in the x direction.
- * @param {number} [yRadius=1] - The radius of the ellipse in the y direction.
- * @param {number} [aStartAngle=0] - The start angle of the curve in radians starting from the positive X axis.
- * @param {number} [aEndAngle=Math.PI*2] - The end angle of the curve in radians starting from the positive X axis.
- * @param {boolean} [aClockwise=false] - Whether the ellipse is drawn clockwise or not.
- * @param {number} [aRotation=0] - The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.
- */
- constructor( aX = 0, aY = 0, xRadius = 1, yRadius = 1, aStartAngle = 0, aEndAngle = Math.PI * 2, aClockwise = false, aRotation = 0 ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isEllipseCurve = true;
-
- this.type = 'EllipseCurve';
-
- /**
- * The X center of the ellipse.
- *
- * @type {number}
- * @default 0
- */
- this.aX = aX;
-
- /**
- * The Y center of the ellipse.
- *
- * @type {number}
- * @default 0
- */
- this.aY = aY;
-
- /**
- * The radius of the ellipse in the x direction.
- * Setting the this value equal to the {@link EllipseCurve#yRadius} will result in a circle.
- *
- * @type {number}
- * @default 1
- */
- this.xRadius = xRadius;
-
- /**
- * The radius of the ellipse in the y direction.
- * Setting the this value equal to the {@link EllipseCurve#xRadius} will result in a circle.
- *
- * @type {number}
- * @default 1
- */
- this.yRadius = yRadius;
-
- /**
- * The start angle of the curve in radians starting from the positive X axis.
- *
- * @type {number}
- * @default 0
- */
- this.aStartAngle = aStartAngle;
-
- /**
- * The end angle of the curve in radians starting from the positive X axis.
- *
- * @type {number}
- * @default Math.PI*2
- */
- this.aEndAngle = aEndAngle;
-
- /**
- * Whether the ellipse is drawn clockwise or not.
- *
- * @type {boolean}
- * @default false
- */
- this.aClockwise = aClockwise;
-
- /**
- * The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.
- *
- * @type {number}
- * @default 0
- */
- this.aRotation = aRotation;
-
- }
-
- /**
- * Returns a point on the curve.
- *
- * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
- * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.
- * @return {Vector2} The position on the curve.
- */
- getPoint( t, optionalTarget = new Vector2() ) {
-
- const point = optionalTarget;
-
- const twoPi = Math.PI * 2;
- let deltaAngle = this.aEndAngle - this.aStartAngle;
- const samePoints = Math.abs( deltaAngle ) < Number.EPSILON;
-
- // ensures that deltaAngle is 0 .. 2 PI
- while ( deltaAngle < 0 ) deltaAngle += twoPi;
- while ( deltaAngle > twoPi ) deltaAngle -= twoPi;
-
- if ( deltaAngle < Number.EPSILON ) {
-
- if ( samePoints ) {
-
- deltaAngle = 0;
-
- } else {
-
- deltaAngle = twoPi;
-
- }
-
- }
-
- if ( this.aClockwise === true && ! samePoints ) {
-
- if ( deltaAngle === twoPi ) {
-
- deltaAngle = - twoPi;
-
- } else {
-
- deltaAngle = deltaAngle - twoPi;
-
- }
-
- }
-
- const angle = this.aStartAngle + t * deltaAngle;
- let x = this.aX + this.xRadius * Math.cos( angle );
- let y = this.aY + this.yRadius * Math.sin( angle );
-
- if ( this.aRotation !== 0 ) {
-
- const cos = Math.cos( this.aRotation );
- const sin = Math.sin( this.aRotation );
-
- const tx = x - this.aX;
- const ty = y - this.aY;
-
- // Rotate the point about the center of the ellipse.
- x = tx * cos - ty * sin + this.aX;
- y = tx * sin + ty * cos + this.aY;
-
- }
-
- return point.set( x, y );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.aX = source.aX;
- this.aY = source.aY;
-
- this.xRadius = source.xRadius;
- this.yRadius = source.yRadius;
-
- this.aStartAngle = source.aStartAngle;
- this.aEndAngle = source.aEndAngle;
-
- this.aClockwise = source.aClockwise;
-
- this.aRotation = source.aRotation;
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.aX = this.aX;
- data.aY = this.aY;
-
- data.xRadius = this.xRadius;
- data.yRadius = this.yRadius;
-
- data.aStartAngle = this.aStartAngle;
- data.aEndAngle = this.aEndAngle;
-
- data.aClockwise = this.aClockwise;
-
- data.aRotation = this.aRotation;
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.aX = json.aX;
- this.aY = json.aY;
-
- this.xRadius = json.xRadius;
- this.yRadius = json.yRadius;
-
- this.aStartAngle = json.aStartAngle;
- this.aEndAngle = json.aEndAngle;
-
- this.aClockwise = json.aClockwise;
-
- this.aRotation = json.aRotation;
-
- return this;
-
- }
-
-}
-
-/**
- * A curve representing an arc.
- *
- * @augments EllipseCurve
- */
-class ArcCurve extends EllipseCurve {
-
- /**
- * Constructs a new arc curve.
- *
- * @param {number} [aX=0] - The X center of the ellipse.
- * @param {number} [aY=0] - The Y center of the ellipse.
- * @param {number} [aRadius=1] - The radius of the ellipse in the x direction.
- * @param {number} [aStartAngle=0] - The start angle of the curve in radians starting from the positive X axis.
- * @param {number} [aEndAngle=Math.PI*2] - The end angle of the curve in radians starting from the positive X axis.
- * @param {boolean} [aClockwise=false] - Whether the ellipse is drawn clockwise or not.
- */
- constructor( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {
-
- super( aX, aY, aRadius, aRadius, aStartAngle, aEndAngle, aClockwise );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isArcCurve = true;
-
- this.type = 'ArcCurve';
-
- }
-
-}
-
-function CubicPoly() {
-
- /**
- * Centripetal CatmullRom Curve - which is useful for avoiding
- * cusps and self-intersections in non-uniform catmull rom curves.
- * http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf
- *
- * curve.type accepts centripetal(default), chordal and catmullrom
- * curve.tension is used for catmullrom which defaults to 0.5
- */
-
- /*
- Based on an optimized c++ solution in
- - http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections/
- - http://ideone.com/NoEbVM
-
- This CubicPoly class could be used for reusing some variables and calculations,
- but for three.js curve use, it could be possible inlined and flatten into a single function call
- which can be placed in CurveUtils.
- */
-
- let c0 = 0, c1 = 0, c2 = 0, c3 = 0;
-
- /*
- * Compute coefficients for a cubic polynomial
- * p(s) = c0 + c1*s + c2*s^2 + c3*s^3
- * such that
- * p(0) = x0, p(1) = x1
- * and
- * p'(0) = t0, p'(1) = t1.
- */
- function init( x0, x1, t0, t1 ) {
-
- c0 = x0;
- c1 = t0;
- c2 = -3 * x0 + 3 * x1 - 2 * t0 - t1;
- c3 = 2 * x0 - 2 * x1 + t0 + t1;
-
- }
-
- return {
-
- initCatmullRom: function ( x0, x1, x2, x3, tension ) {
-
- init( x1, x2, tension * ( x2 - x0 ), tension * ( x3 - x1 ) );
-
- },
-
- initNonuniformCatmullRom: function ( x0, x1, x2, x3, dt0, dt1, dt2 ) {
-
- // compute tangents when parameterized in [t1,t2]
- let t1 = ( x1 - x0 ) / dt0 - ( x2 - x0 ) / ( dt0 + dt1 ) + ( x2 - x1 ) / dt1;
- let t2 = ( x2 - x1 ) / dt1 - ( x3 - x1 ) / ( dt1 + dt2 ) + ( x3 - x2 ) / dt2;
-
- // rescale tangents for parametrization in [0,1]
- t1 *= dt1;
- t2 *= dt1;
-
- init( x1, x2, t1, t2 );
-
- },
-
- calc: function ( t ) {
-
- const t2 = t * t;
- const t3 = t2 * t;
- return c0 + c1 * t + c2 * t2 + c3 * t3;
-
- }
-
- };
-
-}
-
-//
-
-const tmp = /*@__PURE__*/ new Vector3();
-const tmp2 = /*@__PURE__*/ new Vector3();
-const px = /*@__PURE__*/ new CubicPoly();
-const py = /*@__PURE__*/ new CubicPoly();
-const pz = /*@__PURE__*/ new CubicPoly();
-
-/**
- * A curve representing a Catmull-Rom spline.
- *
- * ```js
- * //Create a closed wavey loop
- * const curve = new THREE.CatmullRomCurve3( [
- * new THREE.Vector3( -10, 0, 10 ),
- * new THREE.Vector3( -5, 5, 5 ),
- * new THREE.Vector3( 0, 0, 0 ),
- * new THREE.Vector3( 5, -5, 5 ),
- * new THREE.Vector3( 10, 0, 10 )
- * ] );
- *
- * const points = curve.getPoints( 50 );
- * const geometry = new THREE.BufferGeometry().setFromPoints( points );
- *
- * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
- *
- * // Create the final object to add to the scene
- * const curveObject = new THREE.Line( geometry, material );
- * ```
- *
- * @augments Curve
- */
-class CatmullRomCurve3 extends Curve {
-
- /**
- * Constructs a new Catmull-Rom curve.
- *
- * @param {Array} [points] - An array of 3D points defining the curve.
- * @param {boolean} [closed=false] - Whether the curve is closed or not.
- * @param {('centripetal'|'chordal'|'catmullrom')} [curveType='centripetal'] - The curve type.
- * @param {number} [tension=0.5] - Tension of the curve.
- */
- constructor( points = [], closed = false, curveType = 'centripetal', tension = 0.5 ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCatmullRomCurve3 = true;
-
- this.type = 'CatmullRomCurve3';
-
- /**
- * An array of 3D points defining the curve.
- *
- * @type {Array}
- */
- this.points = points;
-
- /**
- * Whether the curve is closed or not.
- *
- * @type {boolean}
- * @default false
- */
- this.closed = closed;
-
- /**
- * The curve type.
- *
- * @type {('centripetal'|'chordal'|'catmullrom')}
- * @default 'centripetal'
- */
- this.curveType = curveType;
-
- /**
- * Tension of the curve.
- *
- * @type {number}
- * @default 0.5
- */
- this.tension = tension;
-
- }
-
- /**
- * Returns a point on the curve.
- *
- * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
- * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.
- * @return {Vector3} The position on the curve.
- */
- getPoint( t, optionalTarget = new Vector3() ) {
-
- const point = optionalTarget;
-
- const points = this.points;
- const l = points.length;
-
- const p = ( l - ( this.closed ? 0 : 1 ) ) * t;
- let intPoint = Math.floor( p );
- let weight = p - intPoint;
-
- if ( this.closed ) {
-
- intPoint += intPoint > 0 ? 0 : ( Math.floor( Math.abs( intPoint ) / l ) + 1 ) * l;
-
- } else if ( weight === 0 && intPoint === l - 1 ) {
-
- intPoint = l - 2;
- weight = 1;
-
- }
-
- let p0, p3; // 4 points (p1 & p2 defined below)
-
- if ( this.closed || intPoint > 0 ) {
-
- p0 = points[ ( intPoint - 1 ) % l ];
-
- } else {
-
- // extrapolate first point
- tmp2.subVectors( points[ 0 ], points[ 1 ] ).add( points[ 0 ] );
- p0 = tmp2;
-
- }
-
- const p1 = points[ intPoint % l ];
- const p2 = points[ ( intPoint + 1 ) % l ];
-
- if ( this.closed || intPoint + 2 < l ) {
-
- p3 = points[ ( intPoint + 2 ) % l ];
-
- } else {
-
- // extrapolate last point
- tmp.subVectors( points[ l - 1 ], points[ l - 2 ] ).add( points[ l - 1 ] );
- p3 = tmp;
-
- }
-
- if ( this.curveType === 'centripetal' || this.curveType === 'chordal' ) {
-
- // init Centripetal / Chordal Catmull-Rom
- const pow = this.curveType === 'chordal' ? 0.5 : 0.25;
- let dt0 = Math.pow( p0.distanceToSquared( p1 ), pow );
- let dt1 = Math.pow( p1.distanceToSquared( p2 ), pow );
- let dt2 = Math.pow( p2.distanceToSquared( p3 ), pow );
-
- // safety check for repeated points
- if ( dt1 < 1e-4 ) dt1 = 1.0;
- if ( dt0 < 1e-4 ) dt0 = dt1;
- if ( dt2 < 1e-4 ) dt2 = dt1;
-
- px.initNonuniformCatmullRom( p0.x, p1.x, p2.x, p3.x, dt0, dt1, dt2 );
- py.initNonuniformCatmullRom( p0.y, p1.y, p2.y, p3.y, dt0, dt1, dt2 );
- pz.initNonuniformCatmullRom( p0.z, p1.z, p2.z, p3.z, dt0, dt1, dt2 );
-
- } else if ( this.curveType === 'catmullrom' ) {
-
- px.initCatmullRom( p0.x, p1.x, p2.x, p3.x, this.tension );
- py.initCatmullRom( p0.y, p1.y, p2.y, p3.y, this.tension );
- pz.initCatmullRom( p0.z, p1.z, p2.z, p3.z, this.tension );
-
- }
-
- point.set(
- px.calc( weight ),
- py.calc( weight ),
- pz.calc( weight )
- );
-
- return point;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.points = [];
-
- for ( let i = 0, l = source.points.length; i < l; i ++ ) {
-
- const point = source.points[ i ];
-
- this.points.push( point.clone() );
-
- }
-
- this.closed = source.closed;
- this.curveType = source.curveType;
- this.tension = source.tension;
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.points = [];
-
- for ( let i = 0, l = this.points.length; i < l; i ++ ) {
-
- const point = this.points[ i ];
- data.points.push( point.toArray() );
-
- }
-
- data.closed = this.closed;
- data.curveType = this.curveType;
- data.tension = this.tension;
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.points = [];
-
- for ( let i = 0, l = json.points.length; i < l; i ++ ) {
-
- const point = json.points[ i ];
- this.points.push( new Vector3().fromArray( point ) );
-
- }
-
- this.closed = json.closed;
- this.curveType = json.curveType;
- this.tension = json.tension;
-
- return this;
-
- }
-
-}
-
-/**
- * Interpolations contains spline and Bézier functions internally used by concrete curve classes.
- *
- * Bezier Curves formulas obtained from: https://en.wikipedia.org/wiki/B%C3%A9zier_curve
- *
- * @module Interpolations
- */
-
-/**
- * Computes a point on a Catmull-Rom spline.
- *
- * @param {number} t - The interpolation factor.
- * @param {number} p0 - The first control point.
- * @param {number} p1 - The second control point.
- * @param {number} p2 - The third control point.
- * @param {number} p3 - The fourth control point.
- * @return {number} The calculated point on a Catmull-Rom spline.
- */
-function CatmullRom( t, p0, p1, p2, p3 ) {
-
- const v0 = ( p2 - p0 ) * 0.5;
- const v1 = ( p3 - p1 ) * 0.5;
- const t2 = t * t;
- const t3 = t * t2;
- return ( 2 * p1 - 2 * p2 + v0 + v1 ) * t3 + ( -3 * p1 + 3 * p2 - 2 * v0 - v1 ) * t2 + v0 * t + p1;
-
-}
-
-//
-
-function QuadraticBezierP0( t, p ) {
-
- const k = 1 - t;
- return k * k * p;
-
-}
-
-function QuadraticBezierP1( t, p ) {
-
- return 2 * ( 1 - t ) * t * p;
-
-}
-
-function QuadraticBezierP2( t, p ) {
-
- return t * t * p;
-
-}
-
-/**
- * Computes a point on a Quadratic Bezier curve.
- *
- * @param {number} t - The interpolation factor.
- * @param {number} p0 - The first control point.
- * @param {number} p1 - The second control point.
- * @param {number} p2 - The third control point.
- * @return {number} The calculated point on a Quadratic Bezier curve.
- */
-function QuadraticBezier( t, p0, p1, p2 ) {
-
- return QuadraticBezierP0( t, p0 ) + QuadraticBezierP1( t, p1 ) +
- QuadraticBezierP2( t, p2 );
-
-}
-
-//
-
-function CubicBezierP0( t, p ) {
-
- const k = 1 - t;
- return k * k * k * p;
-
-}
-
-function CubicBezierP1( t, p ) {
-
- const k = 1 - t;
- return 3 * k * k * t * p;
-
-}
-
-function CubicBezierP2( t, p ) {
-
- return 3 * ( 1 - t ) * t * t * p;
-
-}
-
-function CubicBezierP3( t, p ) {
-
- return t * t * t * p;
-
-}
-
-/**
- * Computes a point on a Cubic Bezier curve.
- *
- * @param {number} t - The interpolation factor.
- * @param {number} p0 - The first control point.
- * @param {number} p1 - The second control point.
- * @param {number} p2 - The third control point.
- * @param {number} p3 - The fourth control point.
- * @return {number} The calculated point on a Cubic Bezier curve.
- */
-function CubicBezier( t, p0, p1, p2, p3 ) {
-
- return CubicBezierP0( t, p0 ) + CubicBezierP1( t, p1 ) + CubicBezierP2( t, p2 ) +
- CubicBezierP3( t, p3 );
-
-}
-
-/**
- * A curve representing a 2D Cubic Bezier curve.
- *
- * ```js
- * const curve = new THREE.CubicBezierCurve(
- * new THREE.Vector2( - 0, 0 ),
- * new THREE.Vector2( - 5, 15 ),
- * new THREE.Vector2( 20, 15 ),
- * new THREE.Vector2( 10, 0 )
- * );
- *
- * const points = curve.getPoints( 50 );
- * const geometry = new THREE.BufferGeometry().setFromPoints( points );
- *
- * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
- *
- * // Create the final object to add to the scene
- * const curveObject = new THREE.Line( geometry, material );
- * ```
- *
- * @augments Curve
- */
-class CubicBezierCurve extends Curve {
-
- /**
- * Constructs a new Cubic Bezier curve.
- *
- * @param {Vector2} [v0] - The start point.
- * @param {Vector2} [v1] - The first control point.
- * @param {Vector2} [v2] - The second control point.
- * @param {Vector2} [v3] - The end point.
- */
- constructor( v0 = new Vector2(), v1 = new Vector2(), v2 = new Vector2(), v3 = new Vector2() ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCubicBezierCurve = true;
-
- this.type = 'CubicBezierCurve';
-
- /**
- * The start point.
- *
- * @type {Vector2}
- */
- this.v0 = v0;
-
- /**
- * The first control point.
- *
- * @type {Vector2}
- */
- this.v1 = v1;
-
- /**
- * The second control point.
- *
- * @type {Vector2}
- */
- this.v2 = v2;
-
- /**
- * The end point.
- *
- * @type {Vector2}
- */
- this.v3 = v3;
-
- }
-
- /**
- * Returns a point on the curve.
- *
- * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
- * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.
- * @return {Vector2} The position on the curve.
- */
- getPoint( t, optionalTarget = new Vector2() ) {
-
- const point = optionalTarget;
-
- const v0 = this.v0, v1 = this.v1, v2 = this.v2, v3 = this.v3;
-
- point.set(
- CubicBezier( t, v0.x, v1.x, v2.x, v3.x ),
- CubicBezier( t, v0.y, v1.y, v2.y, v3.y )
- );
-
- return point;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.v0.copy( source.v0 );
- this.v1.copy( source.v1 );
- this.v2.copy( source.v2 );
- this.v3.copy( source.v3 );
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.v0 = this.v0.toArray();
- data.v1 = this.v1.toArray();
- data.v2 = this.v2.toArray();
- data.v3 = this.v3.toArray();
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.v0.fromArray( json.v0 );
- this.v1.fromArray( json.v1 );
- this.v2.fromArray( json.v2 );
- this.v3.fromArray( json.v3 );
-
- return this;
-
- }
-
-}
-
-/**
- * A curve representing a 3D Cubic Bezier curve.
- *
- * @augments Curve
- */
-class CubicBezierCurve3 extends Curve {
-
- /**
- * Constructs a new Cubic Bezier curve.
- *
- * @param {Vector3} [v0] - The start point.
- * @param {Vector3} [v1] - The first control point.
- * @param {Vector3} [v2] - The second control point.
- * @param {Vector3} [v3] - The end point.
- */
- constructor( v0 = new Vector3(), v1 = new Vector3(), v2 = new Vector3(), v3 = new Vector3() ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCubicBezierCurve3 = true;
-
- this.type = 'CubicBezierCurve3';
-
- /**
- * The start point.
- *
- * @type {Vector3}
- */
- this.v0 = v0;
-
- /**
- * The first control point.
- *
- * @type {Vector3}
- */
- this.v1 = v1;
-
- /**
- * The second control point.
- *
- * @type {Vector3}
- */
- this.v2 = v2;
-
- /**
- * The end point.
- *
- * @type {Vector3}
- */
- this.v3 = v3;
-
- }
-
- /**
- * Returns a point on the curve.
- *
- * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
- * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.
- * @return {Vector3} The position on the curve.
- */
- getPoint( t, optionalTarget = new Vector3() ) {
-
- const point = optionalTarget;
-
- const v0 = this.v0, v1 = this.v1, v2 = this.v2, v3 = this.v3;
-
- point.set(
- CubicBezier( t, v0.x, v1.x, v2.x, v3.x ),
- CubicBezier( t, v0.y, v1.y, v2.y, v3.y ),
- CubicBezier( t, v0.z, v1.z, v2.z, v3.z )
- );
-
- return point;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.v0.copy( source.v0 );
- this.v1.copy( source.v1 );
- this.v2.copy( source.v2 );
- this.v3.copy( source.v3 );
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.v0 = this.v0.toArray();
- data.v1 = this.v1.toArray();
- data.v2 = this.v2.toArray();
- data.v3 = this.v3.toArray();
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.v0.fromArray( json.v0 );
- this.v1.fromArray( json.v1 );
- this.v2.fromArray( json.v2 );
- this.v3.fromArray( json.v3 );
-
- return this;
-
- }
-
-}
-
-/**
- * A curve representing a 2D line segment.
- *
- * @augments Curve
- */
-class LineCurve extends Curve {
-
- /**
- * Constructs a new line curve.
- *
- * @param {Vector2} [v1] - The start point.
- * @param {Vector2} [v2] - The end point.
- */
- constructor( v1 = new Vector2(), v2 = new Vector2() ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isLineCurve = true;
-
- this.type = 'LineCurve';
-
- /**
- * The start point.
- *
- * @type {Vector2}
- */
- this.v1 = v1;
-
- /**
- * The end point.
- *
- * @type {Vector2}
- */
- this.v2 = v2;
-
- }
-
- /**
- * Returns a point on the line.
- *
- * @param {number} t - A interpolation factor representing a position on the line. Must be in the range `[0,1]`.
- * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.
- * @return {Vector2} The position on the line.
- */
- getPoint( t, optionalTarget = new Vector2() ) {
-
- const point = optionalTarget;
-
- if ( t === 1 ) {
-
- point.copy( this.v2 );
-
- } else {
-
- point.copy( this.v2 ).sub( this.v1 );
- point.multiplyScalar( t ).add( this.v1 );
-
- }
-
- return point;
-
- }
-
- // Line curve is linear, so we can overwrite default getPointAt
- getPointAt( u, optionalTarget ) {
-
- return this.getPoint( u, optionalTarget );
-
- }
-
- getTangent( t, optionalTarget = new Vector2() ) {
-
- return optionalTarget.subVectors( this.v2, this.v1 ).normalize();
-
- }
-
- getTangentAt( u, optionalTarget ) {
-
- return this.getTangent( u, optionalTarget );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.v1.copy( source.v1 );
- this.v2.copy( source.v2 );
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.v1 = this.v1.toArray();
- data.v2 = this.v2.toArray();
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.v1.fromArray( json.v1 );
- this.v2.fromArray( json.v2 );
-
- return this;
-
- }
-
-}
-
-/**
- * A curve representing a 3D line segment.
- *
- * @augments Curve
- */
-class LineCurve3 extends Curve {
-
- /**
- * Constructs a new line curve.
- *
- * @param {Vector3} [v1] - The start point.
- * @param {Vector3} [v2] - The end point.
- */
- constructor( v1 = new Vector3(), v2 = new Vector3() ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isLineCurve3 = true;
-
- this.type = 'LineCurve3';
-
- /**
- * The start point.
- *
- * @type {Vector3}
- */
- this.v1 = v1;
-
- /**
- * The end point.
- *
- * @type {Vector2}
- */
- this.v2 = v2;
-
- }
-
- /**
- * Returns a point on the line.
- *
- * @param {number} t - A interpolation factor representing a position on the line. Must be in the range `[0,1]`.
- * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.
- * @return {Vector3} The position on the line.
- */
- getPoint( t, optionalTarget = new Vector3() ) {
-
- const point = optionalTarget;
-
- if ( t === 1 ) {
-
- point.copy( this.v2 );
-
- } else {
-
- point.copy( this.v2 ).sub( this.v1 );
- point.multiplyScalar( t ).add( this.v1 );
-
- }
-
- return point;
-
- }
-
- // Line curve is linear, so we can overwrite default getPointAt
- getPointAt( u, optionalTarget ) {
-
- return this.getPoint( u, optionalTarget );
-
- }
-
- getTangent( t, optionalTarget = new Vector3() ) {
-
- return optionalTarget.subVectors( this.v2, this.v1 ).normalize();
-
- }
-
- getTangentAt( u, optionalTarget ) {
-
- return this.getTangent( u, optionalTarget );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.v1.copy( source.v1 );
- this.v2.copy( source.v2 );
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.v1 = this.v1.toArray();
- data.v2 = this.v2.toArray();
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.v1.fromArray( json.v1 );
- this.v2.fromArray( json.v2 );
-
- return this;
-
- }
-
-}
-
-/**
- * A curve representing a 2D Quadratic Bezier curve.
- *
- * ```js
- * const curve = new THREE.QuadraticBezierCurve(
- * new THREE.Vector2( - 10, 0 ),
- * new THREE.Vector2( 20, 15 ),
- * new THREE.Vector2( 10, 0 )
- * )
- *
- * const points = curve.getPoints( 50 );
- * const geometry = new THREE.BufferGeometry().setFromPoints( points );
- *
- * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
- *
- * // Create the final object to add to the scene
- * const curveObject = new THREE.Line( geometry, material );
- * ```
- *
- * @augments Curve
- */
-class QuadraticBezierCurve extends Curve {
-
- /**
- * Constructs a new Quadratic Bezier curve.
- *
- * @param {Vector2} [v0] - The start point.
- * @param {Vector2} [v1] - The control point.
- * @param {Vector2} [v2] - The end point.
- */
- constructor( v0 = new Vector2(), v1 = new Vector2(), v2 = new Vector2() ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isQuadraticBezierCurve = true;
-
- this.type = 'QuadraticBezierCurve';
-
- /**
- * The start point.
- *
- * @type {Vector2}
- */
- this.v0 = v0;
-
- /**
- * The control point.
- *
- * @type {Vector2}
- */
- this.v1 = v1;
-
- /**
- * The end point.
- *
- * @type {Vector2}
- */
- this.v2 = v2;
-
- }
-
- /**
- * Returns a point on the curve.
- *
- * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
- * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.
- * @return {Vector2} The position on the curve.
- */
- getPoint( t, optionalTarget = new Vector2() ) {
-
- const point = optionalTarget;
-
- const v0 = this.v0, v1 = this.v1, v2 = this.v2;
-
- point.set(
- QuadraticBezier( t, v0.x, v1.x, v2.x ),
- QuadraticBezier( t, v0.y, v1.y, v2.y )
- );
-
- return point;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.v0.copy( source.v0 );
- this.v1.copy( source.v1 );
- this.v2.copy( source.v2 );
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.v0 = this.v0.toArray();
- data.v1 = this.v1.toArray();
- data.v2 = this.v2.toArray();
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.v0.fromArray( json.v0 );
- this.v1.fromArray( json.v1 );
- this.v2.fromArray( json.v2 );
-
- return this;
-
- }
-
-}
-
-/**
- * A curve representing a 3D Quadratic Bezier curve.
- *
- * @augments Curve
- */
-class QuadraticBezierCurve3 extends Curve {
-
- /**
- * Constructs a new Quadratic Bezier curve.
- *
- * @param {Vector3} [v0] - The start point.
- * @param {Vector3} [v1] - The control point.
- * @param {Vector3} [v2] - The end point.
- */
- constructor( v0 = new Vector3(), v1 = new Vector3(), v2 = new Vector3() ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isQuadraticBezierCurve3 = true;
-
- this.type = 'QuadraticBezierCurve3';
-
- /**
- * The start point.
- *
- * @type {Vector3}
- */
- this.v0 = v0;
-
- /**
- * The control point.
- *
- * @type {Vector3}
- */
- this.v1 = v1;
-
- /**
- * The end point.
- *
- * @type {Vector3}
- */
- this.v2 = v2;
-
- }
-
- /**
- * Returns a point on the curve.
- *
- * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
- * @param {Vector3} [optionalTarget] - The optional target vector the result is written to.
- * @return {Vector3} The position on the curve.
- */
- getPoint( t, optionalTarget = new Vector3() ) {
-
- const point = optionalTarget;
-
- const v0 = this.v0, v1 = this.v1, v2 = this.v2;
-
- point.set(
- QuadraticBezier( t, v0.x, v1.x, v2.x ),
- QuadraticBezier( t, v0.y, v1.y, v2.y ),
- QuadraticBezier( t, v0.z, v1.z, v2.z )
- );
-
- return point;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.v0.copy( source.v0 );
- this.v1.copy( source.v1 );
- this.v2.copy( source.v2 );
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.v0 = this.v0.toArray();
- data.v1 = this.v1.toArray();
- data.v2 = this.v2.toArray();
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.v0.fromArray( json.v0 );
- this.v1.fromArray( json.v1 );
- this.v2.fromArray( json.v2 );
-
- return this;
-
- }
-
-}
-
-/**
- * A curve representing a 2D spline curve.
- *
- * ```js
- * // Create a sine-like wave
- * const curve = new THREE.SplineCurve( [
- * new THREE.Vector2( -10, 0 ),
- * new THREE.Vector2( -5, 5 ),
- * new THREE.Vector2( 0, 0 ),
- * new THREE.Vector2( 5, -5 ),
- * new THREE.Vector2( 10, 0 )
- * ] );
- *
- * const points = curve.getPoints( 50 );
- * const geometry = new THREE.BufferGeometry().setFromPoints( points );
- *
- * const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
- *
- * // Create the final object to add to the scene
- * const splineObject = new THREE.Line( geometry, material );
- * ```
- *
- * @augments Curve
- */
-class SplineCurve extends Curve {
-
- /**
- * Constructs a new 2D spline curve.
- *
- * @param {Array} [points] - An array of 2D points defining the curve.
- */
- constructor( points = [] ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isSplineCurve = true;
-
- this.type = 'SplineCurve';
-
- /**
- * An array of 2D points defining the curve.
- *
- * @type {Array}
- */
- this.points = points;
-
- }
-
- /**
- * Returns a point on the curve.
- *
- * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
- * @param {Vector2} [optionalTarget] - The optional target vector the result is written to.
- * @return {Vector2} The position on the curve.
- */
- getPoint( t, optionalTarget = new Vector2() ) {
-
- const point = optionalTarget;
-
- const points = this.points;
- const p = ( points.length - 1 ) * t;
-
- const intPoint = Math.floor( p );
- const weight = p - intPoint;
-
- const p0 = points[ intPoint === 0 ? intPoint : intPoint - 1 ];
- const p1 = points[ intPoint ];
- const p2 = points[ intPoint > points.length - 2 ? points.length - 1 : intPoint + 1 ];
- const p3 = points[ intPoint > points.length - 3 ? points.length - 1 : intPoint + 2 ];
-
- point.set(
- CatmullRom( weight, p0.x, p1.x, p2.x, p3.x ),
- CatmullRom( weight, p0.y, p1.y, p2.y, p3.y )
- );
-
- return point;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.points = [];
-
- for ( let i = 0, l = source.points.length; i < l; i ++ ) {
-
- const point = source.points[ i ];
-
- this.points.push( point.clone() );
-
- }
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.points = [];
-
- for ( let i = 0, l = this.points.length; i < l; i ++ ) {
-
- const point = this.points[ i ];
- data.points.push( point.toArray() );
-
- }
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.points = [];
-
- for ( let i = 0, l = json.points.length; i < l; i ++ ) {
-
- const point = json.points[ i ];
- this.points.push( new Vector2().fromArray( point ) );
-
- }
-
- return this;
-
- }
-
-}
-
-var Curves = /*#__PURE__*/Object.freeze({
- __proto__: null,
- ArcCurve: ArcCurve,
- CatmullRomCurve3: CatmullRomCurve3,
- CubicBezierCurve: CubicBezierCurve,
- CubicBezierCurve3: CubicBezierCurve3,
- EllipseCurve: EllipseCurve,
- LineCurve: LineCurve,
- LineCurve3: LineCurve3,
- QuadraticBezierCurve: QuadraticBezierCurve,
- QuadraticBezierCurve3: QuadraticBezierCurve3,
- SplineCurve: SplineCurve
-});
-
-/**
- * A base class extending {@link Curve}. `CurvePath` is simply an
- * array of connected curves, but retains the API of a curve.
- *
- * @augments Curve
- */
-class CurvePath extends Curve {
-
- /**
- * Constructs a new curve path.
- */
- constructor() {
-
- super();
-
- this.type = 'CurvePath';
-
- /**
- * An array of curves defining the
- * path.
- *
- * @type {Array}
- */
- this.curves = [];
-
- /**
- * Whether the path should automatically be closed
- * by a line curve.
- *
- * @type {boolean}
- * @default false
- */
- this.autoClose = false;
-
- }
-
- /**
- * Adds a curve to this curve path.
- *
- * @param {Curve} curve - The curve to add.
- */
- add( curve ) {
-
- this.curves.push( curve );
-
- }
-
- /**
- * Adds a line curve to close the path.
- *
- * @return {CurvePath} A reference to this curve path.
- */
- closePath() {
-
- // Add a line curve if start and end of lines are not connected
- const startPoint = this.curves[ 0 ].getPoint( 0 );
- const endPoint = this.curves[ this.curves.length - 1 ].getPoint( 1 );
-
- if ( ! startPoint.equals( endPoint ) ) {
-
- const lineType = ( startPoint.isVector2 === true ) ? 'LineCurve' : 'LineCurve3';
- this.curves.push( new Curves[ lineType ]( endPoint, startPoint ) );
-
- }
-
- return this;
-
- }
-
- /**
- * This method returns a vector in 2D or 3D space (depending on the curve definitions)
- * for the given interpolation factor.
- *
- * @param {number} t - A interpolation factor representing a position on the curve. Must be in the range `[0,1]`.
- * @param {(Vector2|Vector3)} [optionalTarget] - The optional target vector the result is written to.
- * @return {?(Vector2|Vector3)} The position on the curve. It can be a 2D or 3D vector depending on the curve definition.
- */
- getPoint( t, optionalTarget ) {
-
- // To get accurate point with reference to
- // entire path distance at time t,
- // following has to be done:
-
- // 1. Length of each sub path have to be known
- // 2. Locate and identify type of curve
- // 3. Get t for the curve
- // 4. Return curve.getPointAt(t')
-
- const d = t * this.getLength();
- const curveLengths = this.getCurveLengths();
- let i = 0;
-
- // To think about boundaries points.
-
- while ( i < curveLengths.length ) {
-
- if ( curveLengths[ i ] >= d ) {
-
- const diff = curveLengths[ i ] - d;
- const curve = this.curves[ i ];
-
- const segmentLength = curve.getLength();
- const u = segmentLength === 0 ? 0 : 1 - diff / segmentLength;
-
- return curve.getPointAt( u, optionalTarget );
-
- }
-
- i ++;
-
- }
-
- return null;
-
- // loop where sum != 0, sum > d , sum+1 } The curve lengths.
- */
- getCurveLengths() {
-
- // Compute lengths and cache them
- // We cannot overwrite getLengths() because UtoT mapping uses it.
- // We use cache values if curves and cache array are same length
-
- if ( this.cacheLengths && this.cacheLengths.length === this.curves.length ) {
-
- return this.cacheLengths;
-
- }
-
- // Get length of sub-curve
- // Push sums into cached array
-
- const lengths = [];
- let sums = 0;
-
- for ( let i = 0, l = this.curves.length; i < l; i ++ ) {
-
- sums += this.curves[ i ].getLength();
- lengths.push( sums );
-
- }
-
- this.cacheLengths = lengths;
-
- return lengths;
-
- }
-
- getSpacedPoints( divisions = 40 ) {
-
- const points = [];
-
- for ( let i = 0; i <= divisions; i ++ ) {
-
- points.push( this.getPoint( i / divisions ) );
-
- }
-
- if ( this.autoClose ) {
-
- points.push( points[ 0 ] );
-
- }
-
- return points;
-
- }
-
- getPoints( divisions = 12 ) {
-
- const points = [];
- let last;
-
- for ( let i = 0, curves = this.curves; i < curves.length; i ++ ) {
-
- const curve = curves[ i ];
- const resolution = curve.isEllipseCurve ? divisions * 2
- : ( curve.isLineCurve || curve.isLineCurve3 ) ? 1
- : curve.isSplineCurve ? divisions * curve.points.length
- : divisions;
-
- const pts = curve.getPoints( resolution );
-
- for ( let j = 0; j < pts.length; j ++ ) {
-
- const point = pts[ j ];
-
- if ( last && last.equals( point ) ) continue; // ensures no consecutive points are duplicates
-
- points.push( point );
- last = point;
-
- }
-
- }
-
- if ( this.autoClose && points.length > 1 && ! points[ points.length - 1 ].equals( points[ 0 ] ) ) {
-
- points.push( points[ 0 ] );
-
- }
-
- return points;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.curves = [];
-
- for ( let i = 0, l = source.curves.length; i < l; i ++ ) {
-
- const curve = source.curves[ i ];
-
- this.curves.push( curve.clone() );
-
- }
-
- this.autoClose = source.autoClose;
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.autoClose = this.autoClose;
- data.curves = [];
-
- for ( let i = 0, l = this.curves.length; i < l; i ++ ) {
-
- const curve = this.curves[ i ];
- data.curves.push( curve.toJSON() );
-
- }
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.autoClose = json.autoClose;
- this.curves = [];
-
- for ( let i = 0, l = json.curves.length; i < l; i ++ ) {
-
- const curve = json.curves[ i ];
- this.curves.push( new Curves[ curve.type ]().fromJSON( curve ) );
-
- }
-
- return this;
-
- }
-
-}
-
-/**
- * A 2D path representation. The class provides methods for creating paths
- * and contours of 2D shapes similar to the 2D Canvas API.
- *
- * ```js
- * const path = new THREE.Path();
- *
- * path.lineTo( 0, 0.8 );
- * path.quadraticCurveTo( 0, 1, 0.2, 1 );
- * path.lineTo( 1, 1 );
- *
- * const points = path.getPoints();
- *
- * const geometry = new THREE.BufferGeometry().setFromPoints( points );
- * const material = new THREE.LineBasicMaterial( { color: 0xffffff } );
- *
- * const line = new THREE.Line( geometry, material );
- * scene.add( line );
- * ```
- *
- * @augments CurvePath
- */
-class Path extends CurvePath {
-
- /**
- * Constructs a new path.
- *
- * @param {Array} [points] - An array of 2D points defining the path.
- */
- constructor( points ) {
-
- super();
-
- this.type = 'Path';
-
- /**
- * The current offset of the path. Any new curve added will start here.
- *
- * @type {Vector2}
- */
- this.currentPoint = new Vector2();
-
- if ( points ) {
-
- this.setFromPoints( points );
-
- }
-
- }
-
- /**
- * Creates a path from the given list of points. The points are added
- * to the path as instances of {@link LineCurve}.
- *
- * @param {Array} points - An array of 2D points.
- * @return {Path} A reference to this path.
- */
- setFromPoints( points ) {
-
- this.moveTo( points[ 0 ].x, points[ 0 ].y );
-
- for ( let i = 1, l = points.length; i < l; i ++ ) {
-
- this.lineTo( points[ i ].x, points[ i ].y );
-
- }
-
- return this;
-
- }
-
- /**
- * Moves {@link Path#currentPoint} to the given point.
- *
- * @param {number} x - The x coordinate.
- * @param {number} y - The y coordinate.
- * @return {Path} A reference to this path.
- */
- moveTo( x, y ) {
-
- this.currentPoint.set( x, y ); // TODO consider referencing vectors instead of copying?
-
- return this;
-
- }
-
- /**
- * Adds an instance of {@link LineCurve} to the path by connecting
- * the current point with the given one.
- *
- * @param {number} x - The x coordinate of the end point.
- * @param {number} y - The y coordinate of the end point.
- * @return {Path} A reference to this path.
- */
- lineTo( x, y ) {
-
- const curve = new LineCurve( this.currentPoint.clone(), new Vector2( x, y ) );
- this.curves.push( curve );
-
- this.currentPoint.set( x, y );
-
- return this;
-
- }
-
- /**
- * Adds an instance of {@link QuadraticBezierCurve} to the path by connecting
- * the current point with the given one.
- *
- * @param {number} aCPx - The x coordinate of the control point.
- * @param {number} aCPy - The y coordinate of the control point.
- * @param {number} aX - The x coordinate of the end point.
- * @param {number} aY - The y coordinate of the end point.
- * @return {Path} A reference to this path.
- */
- quadraticCurveTo( aCPx, aCPy, aX, aY ) {
-
- const curve = new QuadraticBezierCurve(
- this.currentPoint.clone(),
- new Vector2( aCPx, aCPy ),
- new Vector2( aX, aY )
- );
-
- this.curves.push( curve );
-
- this.currentPoint.set( aX, aY );
-
- return this;
-
- }
-
- /**
- * Adds an instance of {@link CubicBezierCurve} to the path by connecting
- * the current point with the given one.
- *
- * @param {number} aCP1x - The x coordinate of the first control point.
- * @param {number} aCP1y - The y coordinate of the first control point.
- * @param {number} aCP2x - The x coordinate of the second control point.
- * @param {number} aCP2y - The y coordinate of the second control point.
- * @param {number} aX - The x coordinate of the end point.
- * @param {number} aY - The y coordinate of the end point.
- * @return {Path} A reference to this path.
- */
- bezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY ) {
-
- const curve = new CubicBezierCurve(
- this.currentPoint.clone(),
- new Vector2( aCP1x, aCP1y ),
- new Vector2( aCP2x, aCP2y ),
- new Vector2( aX, aY )
- );
-
- this.curves.push( curve );
-
- this.currentPoint.set( aX, aY );
-
- return this;
-
- }
-
- /**
- * Adds an instance of {@link SplineCurve} to the path by connecting
- * the current point with the given list of points.
- *
- * @param {Array} pts - An array of points in 2D space.
- * @return {Path} A reference to this path.
- */
- splineThru( pts ) {
-
- const npts = [ this.currentPoint.clone() ].concat( pts );
-
- const curve = new SplineCurve( npts );
- this.curves.push( curve );
-
- this.currentPoint.copy( pts[ pts.length - 1 ] );
-
- return this;
-
- }
-
- /**
- * Adds an arc as an instance of {@link EllipseCurve} to the path, positioned relative
- * to the current point.
- *
- * @param {number} [aX=0] - The x coordinate of the center of the arc offsetted from the previous curve.
- * @param {number} [aY=0] - The y coordinate of the center of the arc offsetted from the previous curve.
- * @param {number} [aRadius=1] - The radius of the arc.
- * @param {number} [aStartAngle=0] - The start angle in radians.
- * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.
- * @param {boolean} [aClockwise=false] - Whether to sweep the arc clockwise or not.
- * @return {Path} A reference to this path.
- */
- arc( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {
-
- const x0 = this.currentPoint.x;
- const y0 = this.currentPoint.y;
-
- this.absarc( aX + x0, aY + y0, aRadius,
- aStartAngle, aEndAngle, aClockwise );
-
- return this;
-
- }
-
- /**
- * Adds an absolutely positioned arc as an instance of {@link EllipseCurve} to the path.
- *
- * @param {number} [aX=0] - The x coordinate of the center of the arc.
- * @param {number} [aY=0] - The y coordinate of the center of the arc.
- * @param {number} [aRadius=1] - The radius of the arc.
- * @param {number} [aStartAngle=0] - The start angle in radians.
- * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.
- * @param {boolean} [aClockwise=false] - Whether to sweep the arc clockwise or not.
- * @return {Path} A reference to this path.
- */
- absarc( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {
-
- this.absellipse( aX, aY, aRadius, aRadius, aStartAngle, aEndAngle, aClockwise );
-
- return this;
-
- }
-
- /**
- * Adds an ellipse as an instance of {@link EllipseCurve} to the path, positioned relative
- * to the current point
- *
- * @param {number} [aX=0] - The x coordinate of the center of the ellipse offsetted from the previous curve.
- * @param {number} [aY=0] - The y coordinate of the center of the ellipse offsetted from the previous curve.
- * @param {number} [xRadius=1] - The radius of the ellipse in the x axis.
- * @param {number} [yRadius=1] - The radius of the ellipse in the y axis.
- * @param {number} [aStartAngle=0] - The start angle in radians.
- * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.
- * @param {boolean} [aClockwise=false] - Whether to sweep the ellipse clockwise or not.
- * @param {number} [aRotation=0] - The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.
- * @return {Path} A reference to this path.
- */
- ellipse( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation ) {
-
- const x0 = this.currentPoint.x;
- const y0 = this.currentPoint.y;
-
- this.absellipse( aX + x0, aY + y0, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation );
-
- return this;
-
- }
-
- /**
- * Adds an absolutely positioned ellipse as an instance of {@link EllipseCurve} to the path.
- *
- * @param {number} [aX=0] - The x coordinate of the absolute center of the ellipse.
- * @param {number} [aY=0] - The y coordinate of the absolute center of the ellipse.
- * @param {number} [xRadius=1] - The radius of the ellipse in the x axis.
- * @param {number} [yRadius=1] - The radius of the ellipse in the y axis.
- * @param {number} [aStartAngle=0] - The start angle in radians.
- * @param {number} [aEndAngle=Math.PI*2] - The end angle in radians.
- * @param {boolean} [aClockwise=false] - Whether to sweep the ellipse clockwise or not.
- * @param {number} [aRotation=0] - The rotation angle of the ellipse in radians, counterclockwise from the positive X axis.
- * @return {Path} A reference to this path.
- */
- absellipse( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation ) {
-
- const curve = new EllipseCurve( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation );
-
- if ( this.curves.length > 0 ) {
-
- // if a previous curve is present, attempt to join
- const firstPoint = curve.getPoint( 0 );
-
- if ( ! firstPoint.equals( this.currentPoint ) ) {
-
- this.lineTo( firstPoint.x, firstPoint.y );
-
- }
-
- }
-
- this.curves.push( curve );
-
- const lastPoint = curve.getPoint( 1 );
- this.currentPoint.copy( lastPoint );
-
- return this;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.currentPoint.copy( source.currentPoint );
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.currentPoint = this.currentPoint.toArray();
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.currentPoint.fromArray( json.currentPoint );
-
- return this;
-
- }
-
-}
-
-/**
- * Defines an arbitrary 2d shape plane using paths with optional holes. It
- * can be used with {@link ExtrudeGeometry}, {@link ShapeGeometry}, to get
- * points, or to get triangulated faces.
- *
- * ```js
- * const heartShape = new THREE.Shape();
- *
- * heartShape.moveTo( 25, 25 );
- * heartShape.bezierCurveTo( 25, 25, 20, 0, 0, 0 );
- * heartShape.bezierCurveTo( - 30, 0, - 30, 35, - 30, 35 );
- * heartShape.bezierCurveTo( - 30, 55, - 10, 77, 25, 95 );
- * heartShape.bezierCurveTo( 60, 77, 80, 55, 80, 35 );
- * heartShape.bezierCurveTo( 80, 35, 80, 0, 50, 0 );
- * heartShape.bezierCurveTo( 35, 0, 25, 25, 25, 25 );
- *
- * const extrudeSettings = {
- * depth: 8,
- * bevelEnabled: true,
- * bevelSegments: 2,
- * steps: 2,
- * bevelSize: 1,
- * bevelThickness: 1
- * };
- *
- * const geometry = new THREE.ExtrudeGeometry( heartShape, extrudeSettings );
- * const mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial() );
- * ```
- *
- * @augments Path
- */
-class Shape extends Path {
-
- /**
- * Constructs a new shape.
- *
- * @param {Array} [points] - An array of 2D points defining the shape.
- */
- constructor( points ) {
-
- super( points );
-
- /**
- * The UUID of the shape.
- *
- * @type {string}
- * @readonly
- */
- this.uuid = generateUUID();
-
- this.type = 'Shape';
-
- /**
- * Defines the holes in the shape. Hole definitions must use the
- * opposite winding order (CW/CCW) than the outer shape.
- *
- * @type {Array}
- * @readonly
- */
- this.holes = [];
-
- }
-
- /**
- * Returns an array representing each contour of the holes
- * as a list of 2D points.
- *
- * @param {number} divisions - The fineness of the result.
- * @return {Array>} The holes as a series of 2D points.
- */
- getPointsHoles( divisions ) {
-
- const holesPts = [];
-
- for ( let i = 0, l = this.holes.length; i < l; i ++ ) {
-
- holesPts[ i ] = this.holes[ i ].getPoints( divisions );
-
- }
-
- return holesPts;
-
- }
-
- // get points of shape and holes (keypoints based on segments parameter)
-
- /**
- * Returns an object that holds contour data for the shape and its holes as
- * arrays of 2D points.
- *
- * @param {number} divisions - The fineness of the result.
- * @return {{shape:Array,holes:Array>}} An object with contour data.
- */
- extractPoints( divisions ) {
-
- return {
-
- shape: this.getPoints( divisions ),
- holes: this.getPointsHoles( divisions )
-
- };
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.holes = [];
-
- for ( let i = 0, l = source.holes.length; i < l; i ++ ) {
-
- const hole = source.holes[ i ];
-
- this.holes.push( hole.clone() );
-
- }
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.uuid = this.uuid;
- data.holes = [];
-
- for ( let i = 0, l = this.holes.length; i < l; i ++ ) {
-
- const hole = this.holes[ i ];
- data.holes.push( hole.toJSON() );
-
- }
-
- return data;
-
- }
-
- fromJSON( json ) {
-
- super.fromJSON( json );
-
- this.uuid = json.uuid;
- this.holes = [];
-
- for ( let i = 0, l = json.holes.length; i < l; i ++ ) {
-
- const hole = json.holes[ i ];
- this.holes.push( new Path().fromJSON( hole ) );
-
- }
-
- return this;
-
- }
-
-}
-
-/* eslint-disable */
-// copy of mapbox/earcut version 3.0.2
-// https://github.com/mapbox/earcut/tree/v3.0.2
-
-function earcut(data, holeIndices, dim = 2) {
-
- const hasHoles = holeIndices && holeIndices.length;
- const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
- let outerNode = linkedList(data, 0, outerLen, dim, true);
- const triangles = [];
-
- if (!outerNode || outerNode.next === outerNode.prev) return triangles;
-
- let minX, minY, invSize;
-
- if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
-
- // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
- if (data.length > 80 * dim) {
- minX = data[0];
- minY = data[1];
- let maxX = minX;
- let maxY = minY;
-
- for (let i = dim; i < outerLen; i += dim) {
- const x = data[i];
- const y = data[i + 1];
- if (x < minX) minX = x;
- if (y < minY) minY = y;
- if (x > maxX) maxX = x;
- if (y > maxY) maxY = y;
- }
-
- // minX, minY and invSize are later used to transform coords into integers for z-order calculation
- invSize = Math.max(maxX - minX, maxY - minY);
- invSize = invSize !== 0 ? 32767 / invSize : 0;
- }
-
- earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0);
-
- return triangles;
-}
-
-// create a circular doubly linked list from polygon points in the specified winding order
-function linkedList(data, start, end, dim, clockwise) {
- let last;
-
- if (clockwise === (signedArea(data, start, end, dim) > 0)) {
- for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
- } else {
- for (let i = end - dim; i >= start; i -= dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
- }
-
- if (last && equals(last, last.next)) {
- removeNode(last);
- last = last.next;
- }
-
- return last;
-}
-
-// eliminate colinear or duplicate points
-function filterPoints(start, end) {
- if (!start) return start;
- if (!end) end = start;
-
- let p = start,
- again;
- do {
- again = false;
-
- if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
- removeNode(p);
- p = end = p.prev;
- if (p === p.next) break;
- again = true;
-
- } else {
- p = p.next;
- }
- } while (again || p !== end);
-
- return end;
-}
-
-// main ear slicing loop which triangulates a polygon (given as a linked list)
-function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
- if (!ear) return;
-
- // interlink polygon nodes in z-order
- if (!pass && invSize) indexCurve(ear, minX, minY, invSize);
-
- let stop = ear;
-
- // iterate through ears, slicing them one by one
- while (ear.prev !== ear.next) {
- const prev = ear.prev;
- const next = ear.next;
-
- if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
- triangles.push(prev.i, ear.i, next.i); // cut off the triangle
-
- removeNode(ear);
-
- // skipping the next vertex leads to less sliver triangles
- ear = next.next;
- stop = next.next;
-
- continue;
- }
-
- ear = next;
-
- // if we looped through the whole remaining polygon and can't find any more ears
- if (ear === stop) {
- // try filtering points and slicing again
- if (!pass) {
- earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);
-
- // if this didn't work, try curing all small self-intersections locally
- } else if (pass === 1) {
- ear = cureLocalIntersections(filterPoints(ear), triangles);
- earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);
-
- // as a last resort, try splitting the remaining polygon into two
- } else if (pass === 2) {
- splitEarcut(ear, triangles, dim, minX, minY, invSize);
- }
-
- break;
- }
- }
-}
-
-// check whether a polygon node forms a valid ear with adjacent nodes
-function isEar(ear) {
- const a = ear.prev,
- b = ear,
- c = ear.next;
-
- if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
-
- // now make sure we don't have other points inside the potential ear
- const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
-
- // triangle bbox
- const x0 = Math.min(ax, bx, cx),
- y0 = Math.min(ay, by, cy),
- x1 = Math.max(ax, bx, cx),
- y1 = Math.max(ay, by, cy);
-
- let p = c.next;
- while (p !== a) {
- if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
- area(p.prev, p, p.next) >= 0) return false;
- p = p.next;
- }
-
- return true;
-}
-
-function isEarHashed(ear, minX, minY, invSize) {
- const a = ear.prev,
- b = ear,
- c = ear.next;
-
- if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
-
- const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
-
- // triangle bbox
- const x0 = Math.min(ax, bx, cx),
- y0 = Math.min(ay, by, cy),
- x1 = Math.max(ax, bx, cx),
- y1 = Math.max(ay, by, cy);
-
- // z-order range for the current triangle bbox;
- const minZ = zOrder(x0, y0, minX, minY, invSize),
- maxZ = zOrder(x1, y1, minX, minY, invSize);
-
- let p = ear.prevZ,
- n = ear.nextZ;
-
- // look for points inside the triangle in both directions
- while (p && p.z >= minZ && n && n.z <= maxZ) {
- if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
- p = p.prevZ;
-
- if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
- n = n.nextZ;
- }
-
- // look for remaining points in decreasing z-order
- while (p && p.z >= minZ) {
- if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
- p = p.prevZ;
- }
-
- // look for remaining points in increasing z-order
- while (n && n.z <= maxZ) {
- if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
- pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
- n = n.nextZ;
- }
-
- return true;
-}
-
-// go through all polygon nodes and cure small local self-intersections
-function cureLocalIntersections(start, triangles) {
- let p = start;
- do {
- const a = p.prev,
- b = p.next.next;
-
- if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
-
- triangles.push(a.i, p.i, b.i);
-
- // remove two nodes involved
- removeNode(p);
- removeNode(p.next);
-
- p = start = b;
- }
- p = p.next;
- } while (p !== start);
-
- return filterPoints(p);
-}
-
-// try splitting polygon into two and triangulate them independently
-function splitEarcut(start, triangles, dim, minX, minY, invSize) {
- // look for a valid diagonal that divides the polygon into two
- let a = start;
- do {
- let b = a.next.next;
- while (b !== a.prev) {
- if (a.i !== b.i && isValidDiagonal(a, b)) {
- // split the polygon in two by the diagonal
- let c = splitPolygon(a, b);
-
- // filter colinear points around the cuts
- a = filterPoints(a, a.next);
- c = filterPoints(c, c.next);
-
- // run earcut on each half
- earcutLinked(a, triangles, dim, minX, minY, invSize, 0);
- earcutLinked(c, triangles, dim, minX, minY, invSize, 0);
- return;
- }
- b = b.next;
- }
- a = a.next;
- } while (a !== start);
-}
-
-// link every hole into the outer loop, producing a single-ring polygon without holes
-function eliminateHoles(data, holeIndices, outerNode, dim) {
- const queue = [];
-
- for (let i = 0, len = holeIndices.length; i < len; i++) {
- const start = holeIndices[i] * dim;
- const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
- const list = linkedList(data, start, end, dim, false);
- if (list === list.next) list.steiner = true;
- queue.push(getLeftmost(list));
- }
-
- queue.sort(compareXYSlope);
-
- // process holes from left to right
- for (let i = 0; i < queue.length; i++) {
- outerNode = eliminateHole(queue[i], outerNode);
- }
-
- return outerNode;
-}
-
-function compareXYSlope(a, b) {
- let result = a.x - b.x;
- // when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
- // the bridge to the outer shell is always the point that they meet at.
- if (result === 0) {
- result = a.y - b.y;
- if (result === 0) {
- const aSlope = (a.next.y - a.y) / (a.next.x - a.x);
- const bSlope = (b.next.y - b.y) / (b.next.x - b.x);
- result = aSlope - bSlope;
- }
- }
- return result;
-}
-
-// find a bridge between vertices that connects hole with an outer ring and link it
-function eliminateHole(hole, outerNode) {
- const bridge = findHoleBridge(hole, outerNode);
- if (!bridge) {
- return outerNode;
- }
-
- const bridgeReverse = splitPolygon(bridge, hole);
-
- // filter collinear points around the cuts
- filterPoints(bridgeReverse, bridgeReverse.next);
- return filterPoints(bridge, bridge.next);
-}
-
-// David Eberly's algorithm for finding a bridge between hole and outer polygon
-function findHoleBridge(hole, outerNode) {
- let p = outerNode;
- const hx = hole.x;
- const hy = hole.y;
- let qx = -Infinity;
- let m;
-
- // find a segment intersected by a ray from the hole's leftmost point to the left;
- // segment's endpoint with lesser x will be potential connection point
- // unless they intersect at a vertex, then choose the vertex
- if (equals(hole, p)) return p;
- do {
- if (equals(hole, p.next)) return p.next;
- else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
- const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
- if (x <= hx && x > qx) {
- qx = x;
- m = p.x < p.next.x ? p : p.next;
- if (x === hx) return m; // hole touches outer segment; pick leftmost endpoint
- }
- }
- p = p.next;
- } while (p !== outerNode);
-
- if (!m) return null;
-
- // look for points inside the triangle of hole point, segment intersection and endpoint;
- // if there are no points found, we have a valid connection;
- // otherwise choose the point of the minimum angle with the ray as connection point
-
- const stop = m;
- const mx = m.x;
- const my = m.y;
- let tanMin = Infinity;
-
- p = m;
-
- do {
- if (hx >= p.x && p.x >= mx && hx !== p.x &&
- pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
-
- const tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
-
- if (locallyInside(p, hole) &&
- (tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))) {
- m = p;
- tanMin = tan;
- }
- }
-
- p = p.next;
- } while (p !== stop);
-
- return m;
-}
-
-// whether sector in vertex m contains sector in vertex p in the same coordinates
-function sectorContainsSector(m, p) {
- return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
-}
-
-// interlink polygon nodes in z-order
-function indexCurve(start, minX, minY, invSize) {
- let p = start;
- do {
- if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
- p.prevZ = p.prev;
- p.nextZ = p.next;
- p = p.next;
- } while (p !== start);
-
- p.prevZ.nextZ = null;
- p.prevZ = null;
-
- sortLinked(p);
-}
-
-// Simon Tatham's linked list merge sort algorithm
-// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
-function sortLinked(list) {
- let numMerges;
- let inSize = 1;
-
- do {
- let p = list;
- let e;
- list = null;
- let tail = null;
- numMerges = 0;
-
- while (p) {
- numMerges++;
- let q = p;
- let pSize = 0;
- for (let i = 0; i < inSize; i++) {
- pSize++;
- q = q.nextZ;
- if (!q) break;
- }
- let qSize = inSize;
-
- while (pSize > 0 || (qSize > 0 && q)) {
-
- if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
- e = p;
- p = p.nextZ;
- pSize--;
- } else {
- e = q;
- q = q.nextZ;
- qSize--;
- }
-
- if (tail) tail.nextZ = e;
- else list = e;
-
- e.prevZ = tail;
- tail = e;
- }
-
- p = q;
- }
-
- tail.nextZ = null;
- inSize *= 2;
-
- } while (numMerges > 1);
-
- return list;
-}
-
-// z-order of a point given coords and inverse of the longer side of data bbox
-function zOrder(x, y, minX, minY, invSize) {
- // coords are transformed into non-negative 15-bit integer range
- x = (x - minX) * invSize | 0;
- y = (y - minY) * invSize | 0;
-
- x = (x | (x << 8)) & 0x00FF00FF;
- x = (x | (x << 4)) & 0x0F0F0F0F;
- x = (x | (x << 2)) & 0x33333333;
- x = (x | (x << 1)) & 0x55555555;
-
- y = (y | (y << 8)) & 0x00FF00FF;
- y = (y | (y << 4)) & 0x0F0F0F0F;
- y = (y | (y << 2)) & 0x33333333;
- y = (y | (y << 1)) & 0x55555555;
-
- return x | (y << 1);
-}
-
-// find the leftmost node of a polygon ring
-function getLeftmost(start) {
- let p = start,
- leftmost = start;
- do {
- if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;
- p = p.next;
- } while (p !== start);
-
- return leftmost;
-}
-
-// check if a point lies within a convex triangle
-function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
- return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&
- (ax - px) * (by - py) >= (bx - px) * (ay - py) &&
- (bx - px) * (cy - py) >= (cx - px) * (by - py);
-}
-
-// check if a point lies within a convex triangle but false if its equal to the first point of the triangle
-function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
- return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
-}
-
-// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
-function isValidDiagonal(a, b) {
- return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // doesn't intersect other edges
- (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
- (area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
- equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case
-}
-
-// signed area of a triangle
-function area(p, q, r) {
- return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
-}
-
-// check if two points are equal
-function equals(p1, p2) {
- return p1.x === p2.x && p1.y === p2.y;
-}
-
-// check if two segments intersect
-function intersects(p1, q1, p2, q2) {
- const o1 = sign(area(p1, q1, p2));
- const o2 = sign(area(p1, q1, q2));
- const o3 = sign(area(p2, q2, p1));
- const o4 = sign(area(p2, q2, q1));
-
- if (o1 !== o2 && o3 !== o4) return true; // general case
-
- if (o1 === 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
- if (o2 === 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
- if (o3 === 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
- if (o4 === 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2
-
- return false;
-}
-
-// for collinear points p, q, r, check if point q lies on segment pr
-function onSegment(p, q, r) {
- return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);
-}
-
-function sign(num) {
- return num > 0 ? 1 : num < 0 ? -1 : 0;
-}
-
-// check if a polygon diagonal intersects any polygon segments
-function intersectsPolygon(a, b) {
- let p = a;
- do {
- if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
- intersects(p, p.next, a, b)) return true;
- p = p.next;
- } while (p !== a);
-
- return false;
-}
-
-// check if a polygon diagonal is locally inside the polygon
-function locallyInside(a, b) {
- return area(a.prev, a, a.next) < 0 ?
- area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 :
- area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
-}
-
-// check if the middle point of a polygon diagonal is inside the polygon
-function middleInside(a, b) {
- let p = a;
- let inside = false;
- const px = (a.x + b.x) / 2;
- const py = (a.y + b.y) / 2;
- do {
- if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
- (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
- inside = !inside;
- p = p.next;
- } while (p !== a);
-
- return inside;
-}
-
-// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
-// if one belongs to the outer ring and another to a hole, it merges it into a single ring
-function splitPolygon(a, b) {
- const a2 = createNode(a.i, a.x, a.y),
- b2 = createNode(b.i, b.x, b.y),
- an = a.next,
- bp = b.prev;
-
- a.next = b;
- b.prev = a;
-
- a2.next = an;
- an.prev = a2;
-
- b2.next = a2;
- a2.prev = b2;
-
- bp.next = b2;
- b2.prev = bp;
-
- return b2;
-}
-
-// create a node and optionally link it with previous one (in a circular doubly linked list)
-function insertNode(i, x, y, last) {
- const p = createNode(i, x, y);
-
- if (!last) {
- p.prev = p;
- p.next = p;
-
- } else {
- p.next = last.next;
- p.prev = last;
- last.next.prev = p;
- last.next = p;
- }
- return p;
-}
-
-function removeNode(p) {
- p.next.prev = p.prev;
- p.prev.next = p.next;
-
- if (p.prevZ) p.prevZ.nextZ = p.nextZ;
- if (p.nextZ) p.nextZ.prevZ = p.prevZ;
-}
-
-function createNode(i, x, y) {
- return {
- i, // vertex index in coordinates array
- x, y, // vertex coordinates
- prev: null, // previous and next vertex nodes in a polygon ring
- next: null,
- z: 0, // z-order curve value
- prevZ: null, // previous and next nodes in z-order
- nextZ: null,
- steiner: false // indicates whether this is a steiner point
- };
-}
-
-function signedArea(data, start, end, dim) {
- let sum = 0;
- for (let i = start, j = end - dim; i < end; i += dim) {
- sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
- j = i;
- }
- return sum;
-}
-
-/**
- * An implementation of the earcut polygon triangulation algorithm.
- * The code is a port of [mapbox/earcut](https://github.com/mapbox/earcut).
- *
- * @see https://github.com/mapbox/earcut
- */
-class Earcut {
-
- /**
- * Triangulates the given shape definition by returning an array of triangles.
- *
- * @param {Array} data - An array with 2D points.
- * @param {Array} holeIndices - An array with indices defining holes.
- * @param {number} [dim=2] - The number of coordinates per vertex in the input array.
- * @return {Array} An array representing the triangulated faces. Each face is defined by three consecutive numbers
- * representing vertex indices.
- */
- static triangulate( data, holeIndices, dim = 2 ) {
-
- return earcut( data, holeIndices, dim );
-
- }
-
-}
-
-/**
- * A class containing utility functions for shapes.
- *
- * @hideconstructor
- */
-class ShapeUtils {
-
- /**
- * Calculate area of a ( 2D ) contour polygon.
- *
- * @param {Array} contour - An array of 2D points.
- * @return {number} The area.
- */
- static area( contour ) {
-
- const n = contour.length;
- let a = 0.0;
-
- for ( let p = n - 1, q = 0; q < n; p = q ++ ) {
-
- a += contour[ p ].x * contour[ q ].y - contour[ q ].x * contour[ p ].y;
-
- }
-
- return a * 0.5;
-
- }
-
- /**
- * Returns `true` if the given contour uses a clockwise winding order.
- *
- * @param {Array} pts - An array of 2D points defining a polygon.
- * @return {boolean} Whether the given contour uses a clockwise winding order or not.
- */
- static isClockWise( pts ) {
-
- return ShapeUtils.area( pts ) < 0;
-
- }
-
- /**
- * Triangulates the given shape definition.
- *
- * @param {Array} contour - An array of 2D points defining the contour.
- * @param {Array>} holes - An array that holds arrays of 2D points defining the holes.
- * @return {Array>} An array that holds for each face definition an array with three indices.
- */
- static triangulateShape( contour, holes ) {
-
- const vertices = []; // flat array of vertices like [ x0,y0, x1,y1, x2,y2, ... ]
- const holeIndices = []; // array of hole indices
- const faces = []; // final array of vertex indices like [ [ a,b,d ], [ b,c,d ] ]
-
- removeDupEndPts( contour );
- addContour( vertices, contour );
-
- //
-
- let holeIndex = contour.length;
-
- holes.forEach( removeDupEndPts );
-
- for ( let i = 0; i < holes.length; i ++ ) {
-
- holeIndices.push( holeIndex );
- holeIndex += holes[ i ].length;
- addContour( vertices, holes[ i ] );
-
- }
-
- //
-
- const triangles = Earcut.triangulate( vertices, holeIndices );
-
- //
-
- for ( let i = 0; i < triangles.length; i += 3 ) {
-
- faces.push( triangles.slice( i, i + 3 ) );
-
- }
-
- return faces;
-
- }
-
-}
-
-function removeDupEndPts( points ) {
-
- const l = points.length;
-
- if ( l > 2 && points[ l - 1 ].equals( points[ 0 ] ) ) {
-
- points.pop();
-
- }
-
-}
-
-function addContour( vertices, contour ) {
-
- for ( let i = 0; i < contour.length; i ++ ) {
-
- vertices.push( contour[ i ].x );
- vertices.push( contour[ i ].y );
-
- }
-
-}
-
-/**
- * Creates extruded geometry from a path shape.
- *
- * ```js
- * const length = 12, width = 8;
- *
- * const shape = new THREE.Shape();
- * shape.moveTo( 0,0 );
- * shape.lineTo( 0, width );
- * shape.lineTo( length, width );
- * shape.lineTo( length, 0 );
- * shape.lineTo( 0, 0 );
- *
- * const geometry = new THREE.ExtrudeGeometry( shape );
- * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
- * const mesh = new THREE.Mesh( geometry, material ) ;
- * scene.add( mesh );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#ExtrudeGeometry
- */
-class ExtrudeGeometry extends BufferGeometry {
-
- /**
- * Constructs a new extrude geometry.
- *
- * @param {Shape|Array} [shapes] - A shape or an array of shapes.
- * @param {ExtrudeGeometry~Options} [options] - The extrude settings.
- */
- constructor( shapes = new Shape( [ new Vector2( 0.5, 0.5 ), new Vector2( -0.5, 0.5 ), new Vector2( -0.5, -0.5 ), new Vector2( 0.5, -0.5 ) ] ), options = {} ) {
-
- super();
-
- this.type = 'ExtrudeGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- shapes: shapes,
- options: options
- };
-
- shapes = Array.isArray( shapes ) ? shapes : [ shapes ];
-
- const scope = this;
-
- const verticesArray = [];
- const uvArray = [];
-
- for ( let i = 0, l = shapes.length; i < l; i ++ ) {
-
- const shape = shapes[ i ];
- addShape( shape );
-
- }
-
- // build geometry
-
- this.setAttribute( 'position', new Float32BufferAttribute( verticesArray, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvArray, 2 ) );
-
- this.computeVertexNormals();
-
- // functions
-
- function addShape( shape ) {
-
- const placeholder = [];
-
- // options
-
- const curveSegments = options.curveSegments !== undefined ? options.curveSegments : 12;
- const steps = options.steps !== undefined ? options.steps : 1;
- const depth = options.depth !== undefined ? options.depth : 1;
-
- let bevelEnabled = options.bevelEnabled !== undefined ? options.bevelEnabled : true;
- let bevelThickness = options.bevelThickness !== undefined ? options.bevelThickness : 0.2;
- let bevelSize = options.bevelSize !== undefined ? options.bevelSize : bevelThickness - 0.1;
- let bevelOffset = options.bevelOffset !== undefined ? options.bevelOffset : 0;
- let bevelSegments = options.bevelSegments !== undefined ? options.bevelSegments : 3;
-
- const extrudePath = options.extrudePath;
-
- const uvgen = options.UVGenerator !== undefined ? options.UVGenerator : WorldUVGenerator;
-
- //
-
- let extrudePts, extrudeByPath = false;
- let splineTube, binormal, normal, position2;
-
- if ( extrudePath ) {
-
- extrudePts = extrudePath.getSpacedPoints( steps );
-
- extrudeByPath = true;
- bevelEnabled = false; // bevels not supported for path extrusion
-
- // SETUP TNB variables
-
- const isClosed = extrudePath.isCatmullRomCurve3 ? extrudePath.closed : false;
-
- splineTube = extrudePath.computeFrenetFrames( steps, isClosed );
-
- // log(splineTube, 'splineTube', splineTube.normals.length, 'steps', steps, 'extrudePts', extrudePts.length);
-
- binormal = new Vector3();
- normal = new Vector3();
- position2 = new Vector3();
-
- }
-
- // Safeguards if bevels are not enabled
-
- if ( ! bevelEnabled ) {
-
- bevelSegments = 0;
- bevelThickness = 0;
- bevelSize = 0;
- bevelOffset = 0;
-
- }
-
- // Variables initialization
-
- const shapePoints = shape.extractPoints( curveSegments );
-
- let vertices = shapePoints.shape;
- const holes = shapePoints.holes;
-
- const reverse = ! ShapeUtils.isClockWise( vertices );
-
- if ( reverse ) {
-
- vertices = vertices.reverse();
-
- // Maybe we should also check if holes are in the opposite direction, just to be safe ...
-
- for ( let h = 0, hl = holes.length; h < hl; h ++ ) {
-
- const ahole = holes[ h ];
-
- if ( ShapeUtils.isClockWise( ahole ) ) {
-
- holes[ h ] = ahole.reverse();
-
- }
-
- }
-
- }
-
- /**Merges index-adjacent points that are within a threshold distance of each other. Array is modified in-place. Threshold distance is empirical, and scaled based on the magnitude of point coordinates.
- * @param {Array} points
- */
- function mergeOverlappingPoints( points ) {
-
- const THRESHOLD = 1e-10;
- const THRESHOLD_SQ = THRESHOLD * THRESHOLD;
- let prevPos = points[ 0 ];
- for ( let i = 1; i <= points.length; i ++ ) {
-
- const currentIndex = i % points.length;
- const currentPos = points[ currentIndex ];
- const dx = currentPos.x - prevPos.x;
- const dy = currentPos.y - prevPos.y;
- const distSq = dx * dx + dy * dy;
-
- const scalingFactorSqrt = Math.max(
- Math.abs( currentPos.x ),
- Math.abs( currentPos.y ),
- Math.abs( prevPos.x ),
- Math.abs( prevPos.y )
- );
- const thresholdSqScaled = THRESHOLD_SQ * scalingFactorSqrt * scalingFactorSqrt;
- if ( distSq <= thresholdSqScaled ) {
-
- points.splice( currentIndex, 1 );
- i --;
- continue;
-
- }
-
- prevPos = currentPos;
-
- }
-
- }
-
- mergeOverlappingPoints( vertices );
- holes.forEach( mergeOverlappingPoints );
-
- const numHoles = holes.length;
-
- /* Vertices */
-
- const contour = vertices; // vertices has all points but contour has only points of circumference
-
- for ( let h = 0; h < numHoles; h ++ ) {
-
- const ahole = holes[ h ];
-
- vertices = vertices.concat( ahole );
-
- }
-
-
- function scalePt2( pt, vec, size ) {
-
- if ( ! vec ) error( 'ExtrudeGeometry: vec does not exist' );
-
- return pt.clone().addScaledVector( vec, size );
-
- }
-
- const vlen = vertices.length;
-
-
- // Find directions for point movement
-
-
- function getBevelVec( inPt, inPrev, inNext ) {
-
- // computes for inPt the corresponding point inPt' on a new contour
- // shifted by 1 unit (length of normalized vector) to the left
- // if we walk along contour clockwise, this new contour is outside the old one
- //
- // inPt' is the intersection of the two lines parallel to the two
- // adjacent edges of inPt at a distance of 1 unit on the left side.
-
- let v_trans_x, v_trans_y, shrink_by; // resulting translation vector for inPt
-
- // good reading for geometry algorithms (here: line-line intersection)
- // http://geomalgorithms.com/a05-_intersect-1.html
-
- const v_prev_x = inPt.x - inPrev.x,
- v_prev_y = inPt.y - inPrev.y;
- const v_next_x = inNext.x - inPt.x,
- v_next_y = inNext.y - inPt.y;
-
- const v_prev_lensq = ( v_prev_x * v_prev_x + v_prev_y * v_prev_y );
-
- // check for collinear edges
- const collinear0 = ( v_prev_x * v_next_y - v_prev_y * v_next_x );
-
- if ( Math.abs( collinear0 ) > Number.EPSILON ) {
-
- // not collinear
-
- // length of vectors for normalizing
-
- const v_prev_len = Math.sqrt( v_prev_lensq );
- const v_next_len = Math.sqrt( v_next_x * v_next_x + v_next_y * v_next_y );
-
- // shift adjacent points by unit vectors to the left
-
- const ptPrevShift_x = ( inPrev.x - v_prev_y / v_prev_len );
- const ptPrevShift_y = ( inPrev.y + v_prev_x / v_prev_len );
-
- const ptNextShift_x = ( inNext.x - v_next_y / v_next_len );
- const ptNextShift_y = ( inNext.y + v_next_x / v_next_len );
-
- // scaling factor for v_prev to intersection point
-
- const sf = ( ( ptNextShift_x - ptPrevShift_x ) * v_next_y -
- ( ptNextShift_y - ptPrevShift_y ) * v_next_x ) /
- ( v_prev_x * v_next_y - v_prev_y * v_next_x );
-
- // vector from inPt to intersection point
-
- v_trans_x = ( ptPrevShift_x + v_prev_x * sf - inPt.x );
- v_trans_y = ( ptPrevShift_y + v_prev_y * sf - inPt.y );
-
- // Don't normalize!, otherwise sharp corners become ugly
- // but prevent crazy spikes
- const v_trans_lensq = ( v_trans_x * v_trans_x + v_trans_y * v_trans_y );
- if ( v_trans_lensq <= 2 ) {
-
- return new Vector2( v_trans_x, v_trans_y );
-
- } else {
-
- shrink_by = Math.sqrt( v_trans_lensq / 2 );
-
- }
-
- } else {
-
- // handle special case of collinear edges
-
- let direction_eq = false; // assumes: opposite
-
- if ( v_prev_x > Number.EPSILON ) {
-
- if ( v_next_x > Number.EPSILON ) {
-
- direction_eq = true;
-
- }
-
- } else {
-
- if ( v_prev_x < - Number.EPSILON ) {
-
- if ( v_next_x < - Number.EPSILON ) {
-
- direction_eq = true;
-
- }
-
- } else {
-
- if ( Math.sign( v_prev_y ) === Math.sign( v_next_y ) ) {
-
- direction_eq = true;
-
- }
-
- }
-
- }
-
- if ( direction_eq ) {
-
- // log("Warning: lines are a straight sequence");
- v_trans_x = - v_prev_y;
- v_trans_y = v_prev_x;
- shrink_by = Math.sqrt( v_prev_lensq );
-
- } else {
-
- // log("Warning: lines are a straight spike");
- v_trans_x = v_prev_x;
- v_trans_y = v_prev_y;
- shrink_by = Math.sqrt( v_prev_lensq / 2 );
-
- }
-
- }
-
- return new Vector2( v_trans_x / shrink_by, v_trans_y / shrink_by );
-
- }
-
-
- const contourMovements = [];
-
- for ( let i = 0, il = contour.length, j = il - 1, k = i + 1; i < il; i ++, j ++, k ++ ) {
-
- if ( j === il ) j = 0;
- if ( k === il ) k = 0;
-
- // (j)---(i)---(k)
- // log('i,j,k', i, j , k)
-
- contourMovements[ i ] = getBevelVec( contour[ i ], contour[ j ], contour[ k ] );
-
- }
-
- const holesMovements = [];
- let oneHoleMovements, verticesMovements = contourMovements.concat();
-
- for ( let h = 0, hl = numHoles; h < hl; h ++ ) {
-
- const ahole = holes[ h ];
-
- oneHoleMovements = [];
-
- for ( let i = 0, il = ahole.length, j = il - 1, k = i + 1; i < il; i ++, j ++, k ++ ) {
-
- if ( j === il ) j = 0;
- if ( k === il ) k = 0;
-
- // (j)---(i)---(k)
- oneHoleMovements[ i ] = getBevelVec( ahole[ i ], ahole[ j ], ahole[ k ] );
-
- }
-
- holesMovements.push( oneHoleMovements );
- verticesMovements = verticesMovements.concat( oneHoleMovements );
-
- }
-
- let faces;
-
- if ( bevelSegments === 0 ) {
-
- faces = ShapeUtils.triangulateShape( contour, holes );
-
- } else {
-
- const contractedContourVertices = [];
- const expandedHoleVertices = [];
-
- // Loop bevelSegments, 1 for the front, 1 for the back
-
- for ( let b = 0; b < bevelSegments; b ++ ) {
-
- //for ( b = bevelSegments; b > 0; b -- ) {
-
- const t = b / bevelSegments;
- const z = bevelThickness * Math.cos( t * Math.PI / 2 );
- const bs = bevelSize * Math.sin( t * Math.PI / 2 ) + bevelOffset;
-
- // contract shape
-
- for ( let i = 0, il = contour.length; i < il; i ++ ) {
-
- const vert = scalePt2( contour[ i ], contourMovements[ i ], bs );
-
- v( vert.x, vert.y, - z );
- if ( t === 0 ) contractedContourVertices.push( vert );
-
- }
-
- // expand holes
-
- for ( let h = 0, hl = numHoles; h < hl; h ++ ) {
-
- const ahole = holes[ h ];
- oneHoleMovements = holesMovements[ h ];
- const oneHoleVertices = [];
- for ( let i = 0, il = ahole.length; i < il; i ++ ) {
-
- const vert = scalePt2( ahole[ i ], oneHoleMovements[ i ], bs );
-
- v( vert.x, vert.y, - z );
- if ( t === 0 ) oneHoleVertices.push( vert );
-
- }
-
- if ( t === 0 ) expandedHoleVertices.push( oneHoleVertices );
-
- }
-
- }
-
- faces = ShapeUtils.triangulateShape( contractedContourVertices, expandedHoleVertices );
-
- }
-
- const flen = faces.length;
-
- const bs = bevelSize + bevelOffset;
-
- // Back facing vertices
-
- for ( let i = 0; i < vlen; i ++ ) {
-
- const vert = bevelEnabled ? scalePt2( vertices[ i ], verticesMovements[ i ], bs ) : vertices[ i ];
-
- if ( ! extrudeByPath ) {
-
- v( vert.x, vert.y, 0 );
-
- } else {
-
- // v( vert.x, vert.y + extrudePts[ 0 ].y, extrudePts[ 0 ].x );
-
- normal.copy( splineTube.normals[ 0 ] ).multiplyScalar( vert.x );
- binormal.copy( splineTube.binormals[ 0 ] ).multiplyScalar( vert.y );
-
- position2.copy( extrudePts[ 0 ] ).add( normal ).add( binormal );
-
- v( position2.x, position2.y, position2.z );
-
- }
-
- }
-
- // Add stepped vertices...
- // Including front facing vertices
-
- for ( let s = 1; s <= steps; s ++ ) {
-
- for ( let i = 0; i < vlen; i ++ ) {
-
- const vert = bevelEnabled ? scalePt2( vertices[ i ], verticesMovements[ i ], bs ) : vertices[ i ];
-
- if ( ! extrudeByPath ) {
-
- v( vert.x, vert.y, depth / steps * s );
-
- } else {
-
- // v( vert.x, vert.y + extrudePts[ s - 1 ].y, extrudePts[ s - 1 ].x );
-
- normal.copy( splineTube.normals[ s ] ).multiplyScalar( vert.x );
- binormal.copy( splineTube.binormals[ s ] ).multiplyScalar( vert.y );
-
- position2.copy( extrudePts[ s ] ).add( normal ).add( binormal );
-
- v( position2.x, position2.y, position2.z );
-
- }
-
- }
-
- }
-
-
- // Add bevel segments planes
-
- //for ( b = 1; b <= bevelSegments; b ++ ) {
- for ( let b = bevelSegments - 1; b >= 0; b -- ) {
-
- const t = b / bevelSegments;
- const z = bevelThickness * Math.cos( t * Math.PI / 2 );
- const bs = bevelSize * Math.sin( t * Math.PI / 2 ) + bevelOffset;
-
- // contract shape
-
- for ( let i = 0, il = contour.length; i < il; i ++ ) {
-
- const vert = scalePt2( contour[ i ], contourMovements[ i ], bs );
- v( vert.x, vert.y, depth + z );
-
- }
-
- // expand holes
-
- for ( let h = 0, hl = holes.length; h < hl; h ++ ) {
-
- const ahole = holes[ h ];
- oneHoleMovements = holesMovements[ h ];
-
- for ( let i = 0, il = ahole.length; i < il; i ++ ) {
-
- const vert = scalePt2( ahole[ i ], oneHoleMovements[ i ], bs );
-
- if ( ! extrudeByPath ) {
-
- v( vert.x, vert.y, depth + z );
-
- } else {
-
- v( vert.x, vert.y + extrudePts[ steps - 1 ].y, extrudePts[ steps - 1 ].x + z );
-
- }
-
- }
-
- }
-
- }
-
- /* Faces */
-
- // Top and bottom faces
-
- buildLidFaces();
-
- // Sides faces
-
- buildSideFaces();
-
-
- ///// Internal functions
-
- function buildLidFaces() {
-
- const start = verticesArray.length / 3;
-
- if ( bevelEnabled ) {
-
- let layer = 0; // steps + 1
- let offset = vlen * layer;
-
- // Bottom faces
-
- for ( let i = 0; i < flen; i ++ ) {
-
- const face = faces[ i ];
- f3( face[ 2 ] + offset, face[ 1 ] + offset, face[ 0 ] + offset );
-
- }
-
- layer = steps + bevelSegments * 2;
- offset = vlen * layer;
-
- // Top faces
-
- for ( let i = 0; i < flen; i ++ ) {
-
- const face = faces[ i ];
- f3( face[ 0 ] + offset, face[ 1 ] + offset, face[ 2 ] + offset );
-
- }
-
- } else {
-
- // Bottom faces
-
- for ( let i = 0; i < flen; i ++ ) {
-
- const face = faces[ i ];
- f3( face[ 2 ], face[ 1 ], face[ 0 ] );
-
- }
-
- // Top faces
-
- for ( let i = 0; i < flen; i ++ ) {
-
- const face = faces[ i ];
- f3( face[ 0 ] + vlen * steps, face[ 1 ] + vlen * steps, face[ 2 ] + vlen * steps );
-
- }
-
- }
-
- scope.addGroup( start, verticesArray.length / 3 - start, 0 );
-
- }
-
- // Create faces for the z-sides of the shape
-
- function buildSideFaces() {
-
- const start = verticesArray.length / 3;
- let layeroffset = 0;
- sidewalls( contour, layeroffset );
- layeroffset += contour.length;
-
- for ( let h = 0, hl = holes.length; h < hl; h ++ ) {
-
- const ahole = holes[ h ];
- sidewalls( ahole, layeroffset );
-
- //, true
- layeroffset += ahole.length;
-
- }
-
-
- scope.addGroup( start, verticesArray.length / 3 - start, 1 );
-
-
- }
-
- function sidewalls( contour, layeroffset ) {
-
- let i = contour.length;
-
- while ( -- i >= 0 ) {
-
- const j = i;
- let k = i - 1;
- if ( k < 0 ) k = contour.length - 1;
-
- //log('b', i,j, i-1, k,vertices.length);
-
- for ( let s = 0, sl = ( steps + bevelSegments * 2 ); s < sl; s ++ ) {
-
- const slen1 = vlen * s;
- const slen2 = vlen * ( s + 1 );
-
- const a = layeroffset + j + slen1,
- b = layeroffset + k + slen1,
- c = layeroffset + k + slen2,
- d = layeroffset + j + slen2;
-
- f4( a, b, c, d );
-
- }
-
- }
-
- }
-
- function v( x, y, z ) {
-
- placeholder.push( x );
- placeholder.push( y );
- placeholder.push( z );
-
- }
-
-
- function f3( a, b, c ) {
-
- addVertex( a );
- addVertex( b );
- addVertex( c );
-
- const nextIndex = verticesArray.length / 3;
- const uvs = uvgen.generateTopUV( scope, verticesArray, nextIndex - 3, nextIndex - 2, nextIndex - 1 );
-
- addUV( uvs[ 0 ] );
- addUV( uvs[ 1 ] );
- addUV( uvs[ 2 ] );
-
- }
-
- function f4( a, b, c, d ) {
-
- addVertex( a );
- addVertex( b );
- addVertex( d );
-
- addVertex( b );
- addVertex( c );
- addVertex( d );
-
-
- const nextIndex = verticesArray.length / 3;
- const uvs = uvgen.generateSideWallUV( scope, verticesArray, nextIndex - 6, nextIndex - 3, nextIndex - 2, nextIndex - 1 );
-
- addUV( uvs[ 0 ] );
- addUV( uvs[ 1 ] );
- addUV( uvs[ 3 ] );
-
- addUV( uvs[ 1 ] );
- addUV( uvs[ 2 ] );
- addUV( uvs[ 3 ] );
-
- }
-
- function addVertex( index ) {
-
- verticesArray.push( placeholder[ index * 3 + 0 ] );
- verticesArray.push( placeholder[ index * 3 + 1 ] );
- verticesArray.push( placeholder[ index * 3 + 2 ] );
-
- }
-
-
- function addUV( vector2 ) {
-
- uvArray.push( vector2.x );
- uvArray.push( vector2.y );
-
- }
-
- }
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- const shapes = this.parameters.shapes;
- const options = this.parameters.options;
-
- return toJSON$1( shapes, options, data );
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @param {Array} shapes - An array of shapes.
- * @return {ExtrudeGeometry} A new instance.
- */
- static fromJSON( data, shapes ) {
-
- const geometryShapes = [];
-
- for ( let j = 0, jl = data.shapes.length; j < jl; j ++ ) {
-
- const shape = shapes[ data.shapes[ j ] ];
-
- geometryShapes.push( shape );
-
- }
-
- const extrudePath = data.options.extrudePath;
-
- if ( extrudePath !== undefined ) {
-
- data.options.extrudePath = new Curves[ extrudePath.type ]().fromJSON( extrudePath );
-
- }
-
- return new ExtrudeGeometry( geometryShapes, data.options );
-
- }
-
-}
-
-const WorldUVGenerator = {
-
- generateTopUV: function ( geometry, vertices, indexA, indexB, indexC ) {
-
- const a_x = vertices[ indexA * 3 ];
- const a_y = vertices[ indexA * 3 + 1 ];
- const b_x = vertices[ indexB * 3 ];
- const b_y = vertices[ indexB * 3 + 1 ];
- const c_x = vertices[ indexC * 3 ];
- const c_y = vertices[ indexC * 3 + 1 ];
-
- return [
- new Vector2( a_x, a_y ),
- new Vector2( b_x, b_y ),
- new Vector2( c_x, c_y )
- ];
-
- },
-
- generateSideWallUV: function ( geometry, vertices, indexA, indexB, indexC, indexD ) {
-
- const a_x = vertices[ indexA * 3 ];
- const a_y = vertices[ indexA * 3 + 1 ];
- const a_z = vertices[ indexA * 3 + 2 ];
- const b_x = vertices[ indexB * 3 ];
- const b_y = vertices[ indexB * 3 + 1 ];
- const b_z = vertices[ indexB * 3 + 2 ];
- const c_x = vertices[ indexC * 3 ];
- const c_y = vertices[ indexC * 3 + 1 ];
- const c_z = vertices[ indexC * 3 + 2 ];
- const d_x = vertices[ indexD * 3 ];
- const d_y = vertices[ indexD * 3 + 1 ];
- const d_z = vertices[ indexD * 3 + 2 ];
-
- if ( Math.abs( a_y - b_y ) < Math.abs( a_x - b_x ) ) {
-
- return [
- new Vector2( a_x, 1 - a_z ),
- new Vector2( b_x, 1 - b_z ),
- new Vector2( c_x, 1 - c_z ),
- new Vector2( d_x, 1 - d_z )
- ];
-
- } else {
-
- return [
- new Vector2( a_y, 1 - a_z ),
- new Vector2( b_y, 1 - b_z ),
- new Vector2( c_y, 1 - c_z ),
- new Vector2( d_y, 1 - d_z )
- ];
-
- }
-
- }
-
-};
-
-function toJSON$1( shapes, options, data ) {
-
- data.shapes = [];
-
- if ( Array.isArray( shapes ) ) {
-
- for ( let i = 0, l = shapes.length; i < l; i ++ ) {
-
- const shape = shapes[ i ];
-
- data.shapes.push( shape.uuid );
-
- }
-
- } else {
-
- data.shapes.push( shapes.uuid );
-
- }
-
- data.options = Object.assign( {}, options );
-
- if ( options.extrudePath !== undefined ) data.options.extrudePath = options.extrudePath.toJSON();
-
- return data;
-
-}
-
-/**
- * A geometry class for representing an icosahedron.
- *
- * ```js
- * const geometry = new THREE.IcosahedronGeometry();
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const icosahedron = new THREE.Mesh( geometry, material );
- * scene.add( icosahedron );
- * ```
- *
- * @augments PolyhedronGeometry
- * @demo scenes/geometry-browser.html#IcosahedronGeometry
- */
-class IcosahedronGeometry extends PolyhedronGeometry {
-
- /**
- * Constructs a new icosahedron geometry.
- *
- * @param {number} [radius=1] - Radius of the icosahedron.
- * @param {number} [detail=0] - Setting this to a value greater than `0` adds vertices making it no longer a icosahedron.
- */
- constructor( radius = 1, detail = 0 ) {
-
- const t = ( 1 + Math.sqrt( 5 ) ) / 2;
-
- const vertices = [
- -1, t, 0, 1, t, 0, -1, - t, 0, 1, - t, 0,
- 0, -1, t, 0, 1, t, 0, -1, - t, 0, 1, - t,
- t, 0, -1, t, 0, 1, - t, 0, -1, - t, 0, 1
- ];
-
- const indices = [
- 0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11,
- 1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8,
- 3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9,
- 4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1
- ];
-
- super( vertices, indices, radius, detail );
-
- this.type = 'IcosahedronGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radius: radius,
- detail: detail
- };
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {IcosahedronGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new IcosahedronGeometry( data.radius, data.detail );
-
- }
-
-}
-
-/**
- * Creates meshes with axial symmetry like vases. The lathe rotates around the Y axis.
- *
- * ```js
- * const points = [];
- * for ( let i = 0; i < 10; i ++ ) {
- * points.push( new THREE.Vector2( Math.sin( i * 0.2 ) * 10 + 5, ( i - 5 ) * 2 ) );
- * }
- * const geometry = new THREE.LatheGeometry( points );
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const lathe = new THREE.Mesh( geometry, material );
- * scene.add( lathe );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#LatheGeometry
- */
-class LatheGeometry extends BufferGeometry {
-
- /**
- * Constructs a new lathe geometry.
- *
- * @param {Array} [points] - An array of points in 2D space. The x-coordinate of each point
- * must be greater than zero.
- * @param {number} [segments=12] - The number of circumference segments to generate.
- * @param {number} [phiStart=0] - The starting angle in radians.
- * @param {number} [phiLength=Math.PI*2] - The radian (0 to 2PI) range of the lathed section 2PI is a
- * closed lathe, less than 2PI is a portion.
- */
- constructor( points = [ new Vector2( 0, -0.5 ), new Vector2( 0.5, 0 ), new Vector2( 0, 0.5 ) ], segments = 12, phiStart = 0, phiLength = Math.PI * 2 ) {
-
- super();
-
- this.type = 'LatheGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- points: points,
- segments: segments,
- phiStart: phiStart,
- phiLength: phiLength
- };
-
- segments = Math.floor( segments );
-
- // clamp phiLength so it's in range of [ 0, 2PI ]
-
- phiLength = clamp( phiLength, 0, Math.PI * 2 );
-
- // buffers
-
- const indices = [];
- const vertices = [];
- const uvs = [];
- const initNormals = [];
- const normals = [];
-
- // helper variables
-
- const inverseSegments = 1.0 / segments;
- const vertex = new Vector3();
- const uv = new Vector2();
- const normal = new Vector3();
- const curNormal = new Vector3();
- const prevNormal = new Vector3();
- let dx = 0;
- let dy = 0;
-
- // pre-compute normals for initial "meridian"
-
- for ( let j = 0; j <= ( points.length - 1 ); j ++ ) {
-
- switch ( j ) {
-
- case 0: // special handling for 1st vertex on path
-
- dx = points[ j + 1 ].x - points[ j ].x;
- dy = points[ j + 1 ].y - points[ j ].y;
-
- normal.x = dy * 1.0;
- normal.y = - dx;
- normal.z = dy * 0.0;
-
- prevNormal.copy( normal );
-
- normal.normalize();
-
- initNormals.push( normal.x, normal.y, normal.z );
-
- break;
-
- case ( points.length - 1 ): // special handling for last Vertex on path
-
- initNormals.push( prevNormal.x, prevNormal.y, prevNormal.z );
-
- break;
-
- default: // default handling for all vertices in between
-
- dx = points[ j + 1 ].x - points[ j ].x;
- dy = points[ j + 1 ].y - points[ j ].y;
-
- normal.x = dy * 1.0;
- normal.y = - dx;
- normal.z = dy * 0.0;
-
- curNormal.copy( normal );
-
- normal.x += prevNormal.x;
- normal.y += prevNormal.y;
- normal.z += prevNormal.z;
-
- normal.normalize();
-
- initNormals.push( normal.x, normal.y, normal.z );
-
- prevNormal.copy( curNormal );
-
- }
-
- }
-
- // generate vertices, uvs and normals
-
- for ( let i = 0; i <= segments; i ++ ) {
-
- const phi = phiStart + i * inverseSegments * phiLength;
-
- const sin = Math.sin( phi );
- const cos = Math.cos( phi );
-
- for ( let j = 0; j <= ( points.length - 1 ); j ++ ) {
-
- // vertex
-
- vertex.x = points[ j ].x * sin;
- vertex.y = points[ j ].y;
- vertex.z = points[ j ].x * cos;
-
- vertices.push( vertex.x, vertex.y, vertex.z );
-
- // uv
-
- uv.x = i / segments;
- uv.y = j / ( points.length - 1 );
-
- uvs.push( uv.x, uv.y );
-
- // normal
-
- const x = initNormals[ 3 * j + 0 ] * sin;
- const y = initNormals[ 3 * j + 1 ];
- const z = initNormals[ 3 * j + 0 ] * cos;
-
- normals.push( x, y, z );
-
- }
-
- }
-
- // indices
-
- for ( let i = 0; i < segments; i ++ ) {
-
- for ( let j = 0; j < ( points.length - 1 ); j ++ ) {
-
- const base = j + i * points.length;
-
- const a = base;
- const b = base + points.length;
- const c = base + points.length + 1;
- const d = base + 1;
-
- // faces
-
- indices.push( a, b, d );
- indices.push( c, d, b );
-
- }
-
- }
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {LatheGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new LatheGeometry( data.points, data.segments, data.phiStart, data.phiLength );
-
- }
-
-}
-
-/**
- * A geometry class for representing an octahedron.
- *
- * ```js
- * const geometry = new THREE.OctahedronGeometry();
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const octahedron = new THREE.Mesh( geometry, material );
- * scene.add( octahedron );
- * ```
- *
- * @augments PolyhedronGeometry
- * @demo scenes/geometry-browser.html#OctahedronGeometry
- */
-class OctahedronGeometry extends PolyhedronGeometry {
-
- /**
- * Constructs a new octahedron geometry.
- *
- * @param {number} [radius=1] - Radius of the octahedron.
- * @param {number} [detail=0] - Setting this to a value greater than `0` adds vertices making it no longer a octahedron.
- */
- constructor( radius = 1, detail = 0 ) {
-
- const vertices = [
- 1, 0, 0, -1, 0, 0, 0, 1, 0,
- 0, -1, 0, 0, 0, 1, 0, 0, -1
- ];
-
- const indices = [
- 0, 2, 4, 0, 4, 3, 0, 3, 5,
- 0, 5, 2, 1, 2, 5, 1, 5, 3,
- 1, 3, 4, 1, 4, 2
- ];
-
- super( vertices, indices, radius, detail );
-
- this.type = 'OctahedronGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radius: radius,
- detail: detail
- };
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {OctahedronGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new OctahedronGeometry( data.radius, data.detail );
-
- }
-
-}
-
-/**
- * A geometry class for representing a plane.
- *
- * ```js
- * const geometry = new THREE.PlaneGeometry( 1, 1 );
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00, side: THREE.DoubleSide } );
- * const plane = new THREE.Mesh( geometry, material );
- * scene.add( plane );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#PlaneGeometry
- */
-class PlaneGeometry extends BufferGeometry {
-
- /**
- * Constructs a new plane geometry.
- *
- * @param {number} [width=1] - The width along the X axis.
- * @param {number} [height=1] - The height along the Y axis
- * @param {number} [widthSegments=1] - The number of segments along the X axis.
- * @param {number} [heightSegments=1] - The number of segments along the Y axis.
- */
- constructor( width = 1, height = 1, widthSegments = 1, heightSegments = 1 ) {
-
- super();
-
- this.type = 'PlaneGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- width: width,
- height: height,
- widthSegments: widthSegments,
- heightSegments: heightSegments
- };
-
- const width_half = width / 2;
- const height_half = height / 2;
-
- const gridX = Math.floor( widthSegments );
- const gridY = Math.floor( heightSegments );
-
- const gridX1 = gridX + 1;
- const gridY1 = gridY + 1;
-
- const segment_width = width / gridX;
- const segment_height = height / gridY;
-
- //
-
- const indices = [];
- const vertices = [];
- const normals = [];
- const uvs = [];
-
- for ( let iy = 0; iy < gridY1; iy ++ ) {
-
- const y = iy * segment_height - height_half;
-
- for ( let ix = 0; ix < gridX1; ix ++ ) {
-
- const x = ix * segment_width - width_half;
-
- vertices.push( x, - y, 0 );
-
- normals.push( 0, 0, 1 );
-
- uvs.push( ix / gridX );
- uvs.push( 1 - ( iy / gridY ) );
-
- }
-
- }
-
- for ( let iy = 0; iy < gridY; iy ++ ) {
-
- for ( let ix = 0; ix < gridX; ix ++ ) {
-
- const a = ix + gridX1 * iy;
- const b = ix + gridX1 * ( iy + 1 );
- const c = ( ix + 1 ) + gridX1 * ( iy + 1 );
- const d = ( ix + 1 ) + gridX1 * iy;
-
- indices.push( a, b, d );
- indices.push( b, c, d );
-
- }
-
- }
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {PlaneGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new PlaneGeometry( data.width, data.height, data.widthSegments, data.heightSegments );
-
- }
-
-}
-
-/**
- * A class for generating a two-dimensional ring geometry.
- *
- * ```js
- * const geometry = new THREE.RingGeometry( 1, 5, 32 );
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00, side: THREE.DoubleSide } );
- * const mesh = new THREE.Mesh( geometry, material );
- * scene.add( mesh );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#RingGeometry
- */
-class RingGeometry extends BufferGeometry {
-
- /**
- * Constructs a new ring geometry.
- *
- * @param {number} [innerRadius=0.5] - The inner radius of the ring.
- * @param {number} [outerRadius=1] - The outer radius of the ring.
- * @param {number} [thetaSegments=32] - Number of segments. A higher number means the ring will be more round. Minimum is `3`.
- * @param {number} [phiSegments=1] - Number of segments per ring segment. Minimum is `1`.
- * @param {number} [thetaStart=0] - Starting angle in radians.
- * @param {number} [thetaLength=Math.PI*2] - Central angle in radians.
- */
- constructor( innerRadius = 0.5, outerRadius = 1, thetaSegments = 32, phiSegments = 1, thetaStart = 0, thetaLength = Math.PI * 2 ) {
-
- super();
-
- this.type = 'RingGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- innerRadius: innerRadius,
- outerRadius: outerRadius,
- thetaSegments: thetaSegments,
- phiSegments: phiSegments,
- thetaStart: thetaStart,
- thetaLength: thetaLength
- };
-
- thetaSegments = Math.max( 3, thetaSegments );
- phiSegments = Math.max( 1, phiSegments );
-
- // buffers
-
- const indices = [];
- const vertices = [];
- const normals = [];
- const uvs = [];
-
- // some helper variables
-
- let radius = innerRadius;
- const radiusStep = ( ( outerRadius - innerRadius ) / phiSegments );
- const vertex = new Vector3();
- const uv = new Vector2();
-
- // generate vertices, normals and uvs
-
- for ( let j = 0; j <= phiSegments; j ++ ) {
-
- for ( let i = 0; i <= thetaSegments; i ++ ) {
-
- // values are generate from the inside of the ring to the outside
-
- const segment = thetaStart + i / thetaSegments * thetaLength;
-
- // vertex
-
- vertex.x = radius * Math.cos( segment );
- vertex.y = radius * Math.sin( segment );
-
- vertices.push( vertex.x, vertex.y, vertex.z );
-
- // normal
-
- normals.push( 0, 0, 1 );
-
- // uv
-
- uv.x = ( vertex.x / outerRadius + 1 ) / 2;
- uv.y = ( vertex.y / outerRadius + 1 ) / 2;
-
- uvs.push( uv.x, uv.y );
-
- }
-
- // increase the radius for next row of vertices
-
- radius += radiusStep;
-
- }
-
- // indices
-
- for ( let j = 0; j < phiSegments; j ++ ) {
-
- const thetaSegmentLevel = j * ( thetaSegments + 1 );
-
- for ( let i = 0; i < thetaSegments; i ++ ) {
-
- const segment = i + thetaSegmentLevel;
-
- const a = segment;
- const b = segment + thetaSegments + 1;
- const c = segment + thetaSegments + 2;
- const d = segment + 1;
-
- // faces
-
- indices.push( a, b, d );
- indices.push( b, c, d );
-
- }
-
- }
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {RingGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new RingGeometry( data.innerRadius, data.outerRadius, data.thetaSegments, data.phiSegments, data.thetaStart, data.thetaLength );
-
- }
-
-}
-
-/**
- * Creates an one-sided polygonal geometry from one or more path shapes.
- *
- * ```js
- * const arcShape = new THREE.Shape()
- * .moveTo( 5, 1 )
- * .absarc( 1, 1, 4, 0, Math.PI * 2, false );
- *
- * const geometry = new THREE.ShapeGeometry( arcShape );
- * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00, side: THREE.DoubleSide } );
- * const mesh = new THREE.Mesh( geometry, material ) ;
- * scene.add( mesh );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#ShapeGeometry
- */
-class ShapeGeometry extends BufferGeometry {
-
- /**
- * Constructs a new shape geometry.
- *
- * @param {Shape|Array} [shapes] - A shape or an array of shapes.
- * @param {number} [curveSegments=12] - Number of segments per shape.
- */
- constructor( shapes = new Shape( [ new Vector2( 0, 0.5 ), new Vector2( -0.5, -0.5 ), new Vector2( 0.5, -0.5 ) ] ), curveSegments = 12 ) {
-
- super();
-
- this.type = 'ShapeGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- shapes: shapes,
- curveSegments: curveSegments
- };
-
- // buffers
-
- const indices = [];
- const vertices = [];
- const normals = [];
- const uvs = [];
-
- // helper variables
-
- let groupStart = 0;
- let groupCount = 0;
-
- // allow single and array values for "shapes" parameter
-
- if ( Array.isArray( shapes ) === false ) {
-
- addShape( shapes );
-
- } else {
-
- for ( let i = 0; i < shapes.length; i ++ ) {
-
- addShape( shapes[ i ] );
-
- this.addGroup( groupStart, groupCount, i ); // enables MultiMaterial support
-
- groupStart += groupCount;
- groupCount = 0;
-
- }
-
- }
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
-
- // helper functions
-
- function addShape( shape ) {
-
- const indexOffset = vertices.length / 3;
- const points = shape.extractPoints( curveSegments );
-
- let shapeVertices = points.shape;
- const shapeHoles = points.holes;
-
- // check direction of vertices
-
- if ( ShapeUtils.isClockWise( shapeVertices ) === false ) {
-
- shapeVertices = shapeVertices.reverse();
-
- }
-
- for ( let i = 0, l = shapeHoles.length; i < l; i ++ ) {
-
- const shapeHole = shapeHoles[ i ];
-
- if ( ShapeUtils.isClockWise( shapeHole ) === true ) {
-
- shapeHoles[ i ] = shapeHole.reverse();
-
- }
-
- }
-
- const faces = ShapeUtils.triangulateShape( shapeVertices, shapeHoles );
-
- // join vertices of inner and outer paths to a single array
-
- for ( let i = 0, l = shapeHoles.length; i < l; i ++ ) {
-
- const shapeHole = shapeHoles[ i ];
- shapeVertices = shapeVertices.concat( shapeHole );
-
- }
-
- // vertices, normals, uvs
-
- for ( let i = 0, l = shapeVertices.length; i < l; i ++ ) {
-
- const vertex = shapeVertices[ i ];
-
- vertices.push( vertex.x, vertex.y, 0 );
- normals.push( 0, 0, 1 );
- uvs.push( vertex.x, vertex.y ); // world uvs
-
- }
-
- // indices
-
- for ( let i = 0, l = faces.length; i < l; i ++ ) {
-
- const face = faces[ i ];
-
- const a = face[ 0 ] + indexOffset;
- const b = face[ 1 ] + indexOffset;
- const c = face[ 2 ] + indexOffset;
-
- indices.push( a, b, c );
- groupCount += 3;
-
- }
-
- }
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- const shapes = this.parameters.shapes;
-
- return toJSON( shapes, data );
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @param {Array} shapes - An array of shapes.
- * @return {ShapeGeometry} A new instance.
- */
- static fromJSON( data, shapes ) {
-
- const geometryShapes = [];
-
- for ( let j = 0, jl = data.shapes.length; j < jl; j ++ ) {
-
- const shape = shapes[ data.shapes[ j ] ];
-
- geometryShapes.push( shape );
-
- }
-
- return new ShapeGeometry( geometryShapes, data.curveSegments );
-
- }
-
-}
-
-function toJSON( shapes, data ) {
-
- data.shapes = [];
-
- if ( Array.isArray( shapes ) ) {
-
- for ( let i = 0, l = shapes.length; i < l; i ++ ) {
-
- const shape = shapes[ i ];
-
- data.shapes.push( shape.uuid );
-
- }
-
- } else {
-
- data.shapes.push( shapes.uuid );
-
- }
-
- return data;
-
-}
-
-/**
- * A class for generating a sphere geometry.
- *
- * ```js
- * const geometry = new THREE.SphereGeometry( 15, 32, 16 );
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const sphere = new THREE.Mesh( geometry, material );
- * scene.add( sphere );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#SphereGeometry
- */
-class SphereGeometry extends BufferGeometry {
-
- /**
- * Constructs a new sphere geometry.
- *
- * @param {number} [radius=1] - The sphere radius.
- * @param {number} [widthSegments=32] - The number of horizontal segments. Minimum value is `3`.
- * @param {number} [heightSegments=16] - The number of vertical segments. Minimum value is `2`.
- * @param {number} [phiStart=0] - The horizontal starting angle in radians.
- * @param {number} [phiLength=Math.PI*2] - The horizontal sweep angle size.
- * @param {number} [thetaStart=0] - The vertical starting angle in radians.
- * @param {number} [thetaLength=Math.PI] - The vertical sweep angle size.
- */
- constructor( radius = 1, widthSegments = 32, heightSegments = 16, phiStart = 0, phiLength = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI ) {
-
- super();
-
- this.type = 'SphereGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radius: radius,
- widthSegments: widthSegments,
- heightSegments: heightSegments,
- phiStart: phiStart,
- phiLength: phiLength,
- thetaStart: thetaStart,
- thetaLength: thetaLength
- };
-
- widthSegments = Math.max( 3, Math.floor( widthSegments ) );
- heightSegments = Math.max( 2, Math.floor( heightSegments ) );
-
- const thetaEnd = Math.min( thetaStart + thetaLength, Math.PI );
-
- let index = 0;
- const grid = [];
-
- const vertex = new Vector3();
- const normal = new Vector3();
-
- // buffers
-
- const indices = [];
- const vertices = [];
- const normals = [];
- const uvs = [];
-
- // generate vertices, normals and uvs
-
- for ( let iy = 0; iy <= heightSegments; iy ++ ) {
-
- const verticesRow = [];
-
- const v = iy / heightSegments;
- const theta = thetaStart + v * thetaLength;
-
- const y = radius * Math.cos( theta );
- const ringRadius = Math.sqrt( radius * radius - y * y );
-
- // special case for the poles
-
- let uOffset = 0;
-
- if ( iy === 0 && thetaStart === 0 ) {
-
- uOffset = 0.5 / widthSegments;
-
- } else if ( iy === heightSegments && thetaEnd === Math.PI ) {
-
- uOffset = -0.5 / widthSegments;
-
- }
-
- for ( let ix = 0; ix <= widthSegments; ix ++ ) {
-
- const u = ix / widthSegments;
- const phi = phiStart + u * phiLength;
-
- // vertex
-
- vertex.x = - ringRadius * Math.cos( phi );
- vertex.y = y;
- vertex.z = ringRadius * Math.sin( phi );
-
- vertices.push( vertex.x, vertex.y, vertex.z );
-
- // normal
-
- normal.copy( vertex ).normalize();
- normals.push( normal.x, normal.y, normal.z );
-
- // uv
-
- uvs.push( u + uOffset, 1 - v );
-
- verticesRow.push( index ++ );
-
- }
-
- grid.push( verticesRow );
-
- }
-
- // indices
-
- for ( let iy = 0; iy < heightSegments; iy ++ ) {
-
- for ( let ix = 0; ix < widthSegments; ix ++ ) {
-
- const a = grid[ iy ][ ix + 1 ];
- const b = grid[ iy ][ ix ];
- const c = grid[ iy + 1 ][ ix ];
- const d = grid[ iy + 1 ][ ix + 1 ];
-
- if ( iy !== 0 || thetaStart > 0 ) indices.push( a, b, d );
- if ( iy !== heightSegments - 1 || thetaEnd < Math.PI ) indices.push( b, c, d );
-
- }
-
- }
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {SphereGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new SphereGeometry( data.radius, data.widthSegments, data.heightSegments, data.phiStart, data.phiLength, data.thetaStart, data.thetaLength );
-
- }
-
-}
-
-/**
- * A geometry class for representing an tetrahedron.
- *
- * ```js
- * const geometry = new THREE.TetrahedronGeometry();
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const tetrahedron = new THREE.Mesh( geometry, material );
- * scene.add( tetrahedron );
- * ```
- *
- * @augments PolyhedronGeometry
- * @demo scenes/geometry-browser.html#TetrahedronGeometry
- */
-class TetrahedronGeometry extends PolyhedronGeometry {
-
- /**
- * Constructs a new tetrahedron geometry.
- *
- * @param {number} [radius=1] - Radius of the tetrahedron.
- * @param {number} [detail=0] - Setting this to a value greater than `0` adds vertices making it no longer a tetrahedron.
- */
- constructor( radius = 1, detail = 0 ) {
-
- const vertices = [
- 1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1
- ];
-
- const indices = [
- 2, 1, 0, 0, 3, 2, 1, 3, 0, 2, 3, 1
- ];
-
- super( vertices, indices, radius, detail );
-
- this.type = 'TetrahedronGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radius: radius,
- detail: detail
- };
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {TetrahedronGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new TetrahedronGeometry( data.radius, data.detail );
-
- }
-
-}
-
-/**
- * A geometry class for representing an torus.
- *
- * ```js
- * const geometry = new THREE.TorusGeometry( 10, 3, 16, 100 );
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const torus = new THREE.Mesh( geometry, material );
- * scene.add( torus );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#TorusGeometry
- */
-class TorusGeometry extends BufferGeometry {
-
- /**
- * Constructs a new torus geometry.
- *
- * @param {number} [radius=1] - Radius of the torus, from the center of the torus to the center of the tube.
- * @param {number} [tube=0.4] - Radius of the tube. Must be smaller than `radius`.
- * @param {number} [radialSegments=12] - The number of radial segments.
- * @param {number} [tubularSegments=48] - The number of tubular segments.
- * @param {number} [arc=Math.PI*2] - Central angle in radians.
- * @param {number} [thetaStart=0] - Start of the tubular sweep in radians.
- * @param {number} [thetaLength=Math.PI*2] - Length of the tubular sweep in radians.
- */
- constructor( radius = 1, tube = 0.4, radialSegments = 12, tubularSegments = 48, arc = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI * 2 ) {
-
- super();
-
- this.type = 'TorusGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radius: radius,
- tube: tube,
- radialSegments: radialSegments,
- tubularSegments: tubularSegments,
- arc: arc,
- thetaStart: thetaStart,
- thetaLength: thetaLength,
- };
-
- radialSegments = Math.floor( radialSegments );
- tubularSegments = Math.floor( tubularSegments );
-
- // buffers
-
- const indices = [];
- const vertices = [];
- const normals = [];
- const uvs = [];
-
- // helper variables
-
- const center = new Vector3();
- const vertex = new Vector3();
- const normal = new Vector3();
-
- // generate vertices, normals and uvs
-
- for ( let j = 0; j <= radialSegments; j ++ ) {
-
- const v = thetaStart + ( j / radialSegments ) * thetaLength;
-
- for ( let i = 0; i <= tubularSegments; i ++ ) {
-
- const u = i / tubularSegments * arc;
-
- // vertex
-
- vertex.x = ( radius + tube * Math.cos( v ) ) * Math.cos( u );
- vertex.y = ( radius + tube * Math.cos( v ) ) * Math.sin( u );
- vertex.z = tube * Math.sin( v );
-
- vertices.push( vertex.x, vertex.y, vertex.z );
-
- // normal
-
- center.x = radius * Math.cos( u );
- center.y = radius * Math.sin( u );
- normal.subVectors( vertex, center ).normalize();
-
- normals.push( normal.x, normal.y, normal.z );
-
- // uv
-
- uvs.push( i / tubularSegments );
- uvs.push( j / radialSegments );
-
- }
-
- }
-
- // generate indices
-
- for ( let j = 1; j <= radialSegments; j ++ ) {
-
- for ( let i = 1; i <= tubularSegments; i ++ ) {
-
- // indices
-
- const a = ( tubularSegments + 1 ) * j + i - 1;
- const b = ( tubularSegments + 1 ) * ( j - 1 ) + i - 1;
- const c = ( tubularSegments + 1 ) * ( j - 1 ) + i;
- const d = ( tubularSegments + 1 ) * j + i;
-
- // faces
-
- indices.push( a, b, d );
- indices.push( b, c, d );
-
- }
-
- }
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {TorusGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new TorusGeometry( data.radius, data.tube, data.radialSegments, data.tubularSegments, data.arc, data.thetaStart, data.thetaLength );
-
- }
-
-}
-
-/**
- * Creates a torus knot, the particular shape of which is defined by a pair
- * of coprime integers, p and q. If p and q are not coprime, the result will
- * be a torus link.
- *
- * ```js
- * const geometry = new THREE.TorusKnotGeometry( 10, 3, 100, 16 );
- * const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
- * const torusKnot = new THREE.Mesh( geometry, material );
- * scene.add( torusKnot );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#TorusKnotGeometry
- */
-class TorusKnotGeometry extends BufferGeometry {
-
- /**
- * Constructs a new torus knot geometry.
- *
- * @param {number} [radius=1] - Radius of the torus knot.
- * @param {number} [tube=0.4] - Radius of the tube.
- * @param {number} [tubularSegments=64] - The number of tubular segments.
- * @param {number} [radialSegments=8] - The number of radial segments.
- * @param {number} [p=2] - This value determines, how many times the geometry winds around its axis of rotational symmetry.
- * @param {number} [q=3] - This value determines, how many times the geometry winds around a circle in the interior of the torus.
- */
- constructor( radius = 1, tube = 0.4, tubularSegments = 64, radialSegments = 8, p = 2, q = 3 ) {
-
- super();
-
- this.type = 'TorusKnotGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- radius: radius,
- tube: tube,
- tubularSegments: tubularSegments,
- radialSegments: radialSegments,
- p: p,
- q: q
- };
-
- tubularSegments = Math.floor( tubularSegments );
- radialSegments = Math.floor( radialSegments );
-
- // buffers
-
- const indices = [];
- const vertices = [];
- const normals = [];
- const uvs = [];
-
- // helper variables
-
- const vertex = new Vector3();
- const normal = new Vector3();
-
- const P1 = new Vector3();
- const P2 = new Vector3();
-
- const B = new Vector3();
- const T = new Vector3();
- const N = new Vector3();
-
- // generate vertices, normals and uvs
-
- for ( let i = 0; i <= tubularSegments; ++ i ) {
-
- // the radian "u" is used to calculate the position on the torus curve of the current tubular segment
-
- const u = i / tubularSegments * p * Math.PI * 2;
-
- // now we calculate two points. P1 is our current position on the curve, P2 is a little farther ahead.
- // these points are used to create a special "coordinate space", which is necessary to calculate the correct vertex positions
-
- calculatePositionOnCurve( u, p, q, radius, P1 );
- calculatePositionOnCurve( u + 0.01, p, q, radius, P2 );
-
- // calculate orthonormal basis
-
- T.subVectors( P2, P1 );
- N.addVectors( P2, P1 );
- B.crossVectors( T, N );
- N.crossVectors( B, T );
-
- // normalize B, N. T can be ignored, we don't use it
-
- B.normalize();
- N.normalize();
-
- for ( let j = 0; j <= radialSegments; ++ j ) {
-
- // now calculate the vertices. they are nothing more than an extrusion of the torus curve.
- // because we extrude a shape in the xy-plane, there is no need to calculate a z-value.
-
- const v = j / radialSegments * Math.PI * 2;
- const cx = - tube * Math.cos( v );
- const cy = tube * Math.sin( v );
-
- // now calculate the final vertex position.
- // first we orient the extrusion with our basis vectors, then we add it to the current position on the curve
-
- vertex.x = P1.x + ( cx * N.x + cy * B.x );
- vertex.y = P1.y + ( cx * N.y + cy * B.y );
- vertex.z = P1.z + ( cx * N.z + cy * B.z );
-
- vertices.push( vertex.x, vertex.y, vertex.z );
-
- // normal (P1 is always the center/origin of the extrusion, thus we can use it to calculate the normal)
-
- normal.subVectors( vertex, P1 ).normalize();
-
- normals.push( normal.x, normal.y, normal.z );
-
- // uv
-
- uvs.push( i / tubularSegments );
- uvs.push( j / radialSegments );
-
- }
-
- }
-
- // generate indices
-
- for ( let j = 1; j <= tubularSegments; j ++ ) {
-
- for ( let i = 1; i <= radialSegments; i ++ ) {
-
- // indices
-
- const a = ( radialSegments + 1 ) * ( j - 1 ) + ( i - 1 );
- const b = ( radialSegments + 1 ) * j + ( i - 1 );
- const c = ( radialSegments + 1 ) * j + i;
- const d = ( radialSegments + 1 ) * ( j - 1 ) + i;
-
- // faces
-
- indices.push( a, b, d );
- indices.push( b, c, d );
-
- }
-
- }
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
- // this function calculates the current position on the torus curve
-
- function calculatePositionOnCurve( u, p, q, radius, position ) {
-
- const cu = Math.cos( u );
- const su = Math.sin( u );
- const quOverP = q / p * u;
- const cs = Math.cos( quOverP );
-
- position.x = radius * ( 2 + cs ) * 0.5 * cu;
- position.y = radius * ( 2 + cs ) * su * 0.5;
- position.z = radius * Math.sin( quOverP ) * 0.5;
-
- }
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {TorusKnotGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- return new TorusKnotGeometry( data.radius, data.tube, data.tubularSegments, data.radialSegments, data.p, data.q );
-
- }
-
-}
-
-/**
- * Creates a tube that extrudes along a 3D curve.
- *
- * ```js
- * class CustomSinCurve extends THREE.Curve {
- *
- * getPoint( t, optionalTarget = new THREE.Vector3() ) {
- *
- * const tx = t * 3 - 1.5;
- * const ty = Math.sin( 2 * Math.PI * t );
- * const tz = 0;
- *
- * return optionalTarget.set( tx, ty, tz );
- * }
- *
- * }
- *
- * const path = new CustomSinCurve( 10 );
- * const geometry = new THREE.TubeGeometry( path, 20, 2, 8, false );
- * const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
- * const mesh = new THREE.Mesh( geometry, material );
- * scene.add( mesh );
- * ```
- *
- * @augments BufferGeometry
- * @demo scenes/geometry-browser.html#TubeGeometry
- */
-class TubeGeometry extends BufferGeometry {
-
- /**
- * Constructs a new tube geometry.
- *
- * @param {Curve} [path=QuadraticBezierCurve3] - A 3D curve defining the path of the tube.
- * @param {number} [tubularSegments=64] - The number of segments that make up the tube.
- * @param {number} [radius=1] -The radius of the tube.
- * @param {number} [radialSegments=8] - The number of segments that make up the cross-section.
- * @param {boolean} [closed=false] - Whether the tube is closed or not.
- */
- constructor( path = new QuadraticBezierCurve3( new Vector3( -1, -1, 0 ), new Vector3( -1, 1, 0 ), new Vector3( 1, 1, 0 ) ), tubularSegments = 64, radius = 1, radialSegments = 8, closed = false ) {
-
- super();
-
- this.type = 'TubeGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- path: path,
- tubularSegments: tubularSegments,
- radius: radius,
- radialSegments: radialSegments,
- closed: closed
- };
-
- const frames = path.computeFrenetFrames( tubularSegments, closed );
-
- // expose internals
-
- this.tangents = frames.tangents;
- this.normals = frames.normals;
- this.binormals = frames.binormals;
-
- // helper variables
-
- const vertex = new Vector3();
- const normal = new Vector3();
- const uv = new Vector2();
- let P = new Vector3();
-
- // buffer
-
- const vertices = [];
- const normals = [];
- const uvs = [];
- const indices = [];
-
- // create buffer data
-
- generateBufferData();
-
- // build geometry
-
- this.setIndex( indices );
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
- this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
-
- // functions
-
- function generateBufferData() {
-
- for ( let i = 0; i < tubularSegments; i ++ ) {
-
- generateSegment( i );
-
- }
-
- // if the geometry is not closed, generate the last row of vertices and normals
- // at the regular position on the given path
- //
- // if the geometry is closed, duplicate the first row of vertices and normals (uvs will differ)
-
- generateSegment( ( closed === false ) ? tubularSegments : 0 );
-
- // uvs are generated in a separate function.
- // this makes it easy compute correct values for closed geometries
-
- generateUVs();
-
- // finally create faces
-
- generateIndices();
-
- }
-
- function generateSegment( i ) {
-
- // we use getPointAt to sample evenly distributed points from the given path
-
- P = path.getPointAt( i / tubularSegments, P );
-
- // retrieve corresponding normal and binormal
-
- const N = frames.normals[ i ];
- const B = frames.binormals[ i ];
-
- // generate normals and vertices for the current segment
-
- for ( let j = 0; j <= radialSegments; j ++ ) {
-
- const v = j / radialSegments * Math.PI * 2;
-
- const sin = Math.sin( v );
- const cos = - Math.cos( v );
-
- // normal
-
- normal.x = ( cos * N.x + sin * B.x );
- normal.y = ( cos * N.y + sin * B.y );
- normal.z = ( cos * N.z + sin * B.z );
- normal.normalize();
-
- normals.push( normal.x, normal.y, normal.z );
-
- // vertex
-
- vertex.x = P.x + radius * normal.x;
- vertex.y = P.y + radius * normal.y;
- vertex.z = P.z + radius * normal.z;
-
- vertices.push( vertex.x, vertex.y, vertex.z );
-
- }
-
- }
-
- function generateIndices() {
-
- for ( let j = 1; j <= tubularSegments; j ++ ) {
-
- for ( let i = 1; i <= radialSegments; i ++ ) {
-
- const a = ( radialSegments + 1 ) * ( j - 1 ) + ( i - 1 );
- const b = ( radialSegments + 1 ) * j + ( i - 1 );
- const c = ( radialSegments + 1 ) * j + i;
- const d = ( radialSegments + 1 ) * ( j - 1 ) + i;
-
- // faces
-
- indices.push( a, b, d );
- indices.push( b, c, d );
-
- }
-
- }
-
- }
-
- function generateUVs() {
-
- for ( let i = 0; i <= tubularSegments; i ++ ) {
-
- for ( let j = 0; j <= radialSegments; j ++ ) {
-
- uv.x = i / tubularSegments;
- uv.y = j / radialSegments;
-
- uvs.push( uv.x, uv.y );
-
- }
-
- }
-
- }
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.path = this.parameters.path.toJSON();
-
- return data;
-
- }
-
- /**
- * Factory method for creating an instance of this class from the given
- * JSON object.
- *
- * @param {Object} data - A JSON object representing the serialized geometry.
- * @return {TubeGeometry} A new instance.
- */
- static fromJSON( data ) {
-
- // This only works for built-in curves (e.g. CatmullRomCurve3).
- // User defined curves or instances of CurvePath will not be deserialized.
- return new TubeGeometry(
- new Curves[ data.path.type ]().fromJSON( data.path ),
- data.tubularSegments,
- data.radius,
- data.radialSegments,
- data.closed
- );
-
- }
-
-}
-
-/**
- * Can be used as a helper object to visualize a geometry as a wireframe.
- *
- * ```js
- * const geometry = new THREE.SphereGeometry();
- *
- * const wireframe = new THREE.WireframeGeometry( geometry );
- *
- * const line = new THREE.LineSegments( wireframe );
- * line.material.depthWrite = false;
- * line.material.opacity = 0.25;
- * line.material.transparent = true;
- *
- * scene.add( line );
- * ```
- *
- * Note: It is not yet possible to serialize/deserialize instances of this class.
- *
- * @augments BufferGeometry
- */
-class WireframeGeometry extends BufferGeometry {
-
- /**
- * Constructs a new wireframe geometry.
- *
- * @param {?BufferGeometry} [geometry=null] - The geometry.
- */
- constructor( geometry = null ) {
-
- super();
-
- this.type = 'WireframeGeometry';
-
- /**
- * Holds the constructor parameters that have been
- * used to generate the geometry. Any modification
- * after instantiation does not change the geometry.
- *
- * @type {Object}
- */
- this.parameters = {
- geometry: geometry
- };
-
- if ( geometry !== null ) {
-
- // buffer
-
- const vertices = [];
- const edges = new Set();
-
- // helper variables
-
- const start = new Vector3();
- const end = new Vector3();
-
- if ( geometry.index !== null ) {
-
- // indexed BufferGeometry
-
- const position = geometry.attributes.position;
- const indices = geometry.index;
- let groups = geometry.groups;
-
- if ( groups.length === 0 ) {
-
- groups = [ { start: 0, count: indices.count, materialIndex: 0 } ];
-
- }
-
- // create a data structure that contains all edges without duplicates
-
- for ( let o = 0, ol = groups.length; o < ol; ++ o ) {
-
- const group = groups[ o ];
-
- const groupStart = group.start;
- const groupCount = group.count;
-
- for ( let i = groupStart, l = ( groupStart + groupCount ); i < l; i += 3 ) {
-
- for ( let j = 0; j < 3; j ++ ) {
-
- const index1 = indices.getX( i + j );
- const index2 = indices.getX( i + ( j + 1 ) % 3 );
-
- start.fromBufferAttribute( position, index1 );
- end.fromBufferAttribute( position, index2 );
-
- if ( isUniqueEdge( start, end, edges ) === true ) {
-
- vertices.push( start.x, start.y, start.z );
- vertices.push( end.x, end.y, end.z );
-
- }
-
- }
-
- }
-
- }
-
- } else {
-
- // non-indexed BufferGeometry
-
- const position = geometry.attributes.position;
-
- for ( let i = 0, l = ( position.count / 3 ); i < l; i ++ ) {
-
- for ( let j = 0; j < 3; j ++ ) {
-
- // three edges per triangle, an edge is represented as (index1, index2)
- // e.g. the first triangle has the following edges: (0,1),(1,2),(2,0)
-
- const index1 = 3 * i + j;
- const index2 = 3 * i + ( ( j + 1 ) % 3 );
-
- start.fromBufferAttribute( position, index1 );
- end.fromBufferAttribute( position, index2 );
-
- if ( isUniqueEdge( start, end, edges ) === true ) {
-
- vertices.push( start.x, start.y, start.z );
- vertices.push( end.x, end.y, end.z );
-
- }
-
- }
-
- }
-
- }
-
- // build geometry
-
- this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
-
- }
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.parameters = Object.assign( {}, source.parameters );
-
- return this;
-
- }
-
-}
-
-function isUniqueEdge( start, end, edges ) {
-
- const hash1 = `${start.x},${start.y},${start.z}-${end.x},${end.y},${end.z}`;
- const hash2 = `${end.x},${end.y},${end.z}-${start.x},${start.y},${start.z}`; // coincident edge
-
- if ( edges.has( hash1 ) === true || edges.has( hash2 ) === true ) {
-
- return false;
-
- } else {
-
- edges.add( hash1 );
- edges.add( hash2 );
- return true;
-
- }
-
-}
-
-var Geometries = /*#__PURE__*/Object.freeze({
- __proto__: null,
- BoxGeometry: BoxGeometry,
- CapsuleGeometry: CapsuleGeometry,
- CircleGeometry: CircleGeometry,
- ConeGeometry: ConeGeometry,
- CylinderGeometry: CylinderGeometry,
- DodecahedronGeometry: DodecahedronGeometry,
- EdgesGeometry: EdgesGeometry,
- ExtrudeGeometry: ExtrudeGeometry,
- IcosahedronGeometry: IcosahedronGeometry,
- LatheGeometry: LatheGeometry,
- OctahedronGeometry: OctahedronGeometry,
- PlaneGeometry: PlaneGeometry,
- PolyhedronGeometry: PolyhedronGeometry,
- RingGeometry: RingGeometry,
- ShapeGeometry: ShapeGeometry,
- SphereGeometry: SphereGeometry,
- TetrahedronGeometry: TetrahedronGeometry,
- TorusGeometry: TorusGeometry,
- TorusKnotGeometry: TorusKnotGeometry,
- TubeGeometry: TubeGeometry,
- WireframeGeometry: WireframeGeometry
-});
-
-/**
- * This material can receive shadows, but otherwise is completely transparent.
- *
- * ```js
- * const geometry = new THREE.PlaneGeometry( 2000, 2000 );
- * geometry.rotateX( - Math.PI / 2 );
- *
- * const material = new THREE.ShadowMaterial();
- * material.opacity = 0.2;
- *
- * const plane = new THREE.Mesh( geometry, material );
- * plane.position.y = -200;
- * plane.receiveShadow = true;
- * scene.add( plane );
- * ```
- *
- * @augments Material
- */
-class ShadowMaterial extends Material {
-
- /**
- * Constructs a new shadow material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isShadowMaterial = true;
-
- this.type = 'ShadowMaterial';
-
- /**
- * Color of the material.
- *
- * @type {Color}
- * @default (0,0,0)
- */
- this.color = new Color( 0x000000 );
-
- /**
- * Overwritten since shadow materials are transparent
- * by default.
- *
- * @type {boolean}
- * @default true
- */
- this.transparent = true;
-
- /**
- * Whether the material is affected by fog or not.
- *
- * @type {boolean}
- * @default true
- */
- this.fog = true;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.color.copy( source.color );
-
- this.fog = source.fog;
-
- return this;
-
- }
-
-}
-
-/**
- * Provides utility functions for managing uniforms.
- *
- * @module UniformsUtils
- */
-
-/**
- * Clones the given uniform definitions by performing a deep-copy. That means
- * if the value of a uniform refers to an object like a Vector3 or Texture,
- * the cloned uniform will refer to a new object reference.
- *
- * @param {Object} src - An object representing uniform definitions.
- * @return {Object} The cloned uniforms.
- */
-function cloneUniforms( src ) {
-
- const dst = {};
-
- for ( const u in src ) {
-
- dst[ u ] = {};
-
- for ( const p in src[ u ] ) {
-
- const property = src[ u ][ p ];
-
- if ( isThreeObject( property ) ) {
-
- if ( property.isRenderTargetTexture ) {
-
- warn( 'UniformsUtils: Textures of render targets cannot be cloned via cloneUniforms() or mergeUniforms().' );
- dst[ u ][ p ] = null;
-
- } else {
-
- dst[ u ][ p ] = property.clone();
-
- }
-
- } else if ( Array.isArray( property ) ) {
-
- if ( isThreeObject( property[ 0 ] ) ) {
-
- const clonedProperty = [];
-
- for ( let i = 0, l = property.length; i < l; i ++ ) {
-
- clonedProperty[ i ] = property[ i ].clone();
-
- }
-
- dst[ u ][ p ] = clonedProperty;
-
- } else {
-
- dst[ u ][ p ] = property.slice();
-
- }
-
- } else {
-
- dst[ u ][ p ] = property;
-
- }
-
- }
-
- }
-
- return dst;
-
-}
-
-/**
- * Merges the given uniform definitions into a single object. Since the
- * method internally uses cloneUniforms(), it performs a deep-copy when
- * producing the merged uniform definitions.
- *
- * @param {Array} uniforms - An array of objects containing uniform definitions.
- * @return {Object} The merged uniforms.
- */
-function mergeUniforms( uniforms ) {
-
- const merged = {};
-
- for ( let u = 0; u < uniforms.length; u ++ ) {
-
- const tmp = cloneUniforms( uniforms[ u ] );
-
- for ( const p in tmp ) {
-
- merged[ p ] = tmp[ p ];
-
- }
-
- }
-
- return merged;
-
-}
-
-function isThreeObject( property ) {
-
- return ( property && ( property.isColor ||
- property.isMatrix3 || property.isMatrix4 ||
- property.isVector2 || property.isVector3 || property.isVector4 ||
- property.isTexture || property.isQuaternion ) );
-
-}
-
-function cloneUniformsGroups( src ) {
-
- const dst = [];
-
- for ( let u = 0; u < src.length; u ++ ) {
-
- dst.push( src[ u ].clone() );
-
- }
-
- return dst;
-
-}
-
-function getUnlitUniformColorSpace( renderer ) {
-
- const currentRenderTarget = renderer.getRenderTarget();
-
- if ( currentRenderTarget === null ) {
-
- // https://github.com/mrdoob/three.js/pull/23937#issuecomment-1111067398
- return renderer.outputColorSpace;
-
- }
-
- // https://github.com/mrdoob/three.js/issues/27868
- if ( currentRenderTarget.isXRRenderTarget === true ) {
-
- return currentRenderTarget.texture.colorSpace;
-
- }
-
- return ColorManagement.workingColorSpace;
-
-}
-
-// Legacy
-
-const UniformsUtils = { clone: cloneUniforms, merge: mergeUniforms };
-
-var default_vertex = "void main() {\n\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}";
-
-var default_fragment = "void main() {\n\tgl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 );\n}";
-
-/**
- * A material rendered with custom shaders. A shader is a small program written in GLSL.
- * that runs on the GPU. You may want to use a custom shader if you need to implement an
- * effect not included with any of the built-in materials.
- *
- * There are the following notes to bear in mind when using a `ShaderMaterial`:
- *
- * - `ShaderMaterial` can only be used with {@link WebGLRenderer}.
- * - Built in attributes and uniforms are passed to the shaders along with your code. If
- * you don't want that, use {@link RawShaderMaterial} instead.
- * - You can use the directive `#pragma unroll_loop_start` and `#pragma unroll_loop_end`
- * in order to unroll a `for` loop in GLSL by the shader preprocessor. The directive has
- * to be placed right above the loop. The loop formatting has to correspond to a defined standard.
- * - The loop has to be [normalized](https://en.wikipedia.org/wiki/Normalized_loop).
- * - The loop variable has to be *i*.
- * - The value `UNROLLED_LOOP_INDEX` will be replaced with the explicitly
- * value of *i* for the given iteration and can be used in preprocessor
- * statements.
- *
- * ```js
- * const material = new THREE.ShaderMaterial( {
- * uniforms: {
- * time: { value: 1.0 },
- * resolution: { value: new THREE.Vector2() }
- * },
- * vertexShader: document.getElementById( 'vertexShader' ).textContent,
- * fragmentShader: document.getElementById( 'fragmentShader' ).textContent
- * } );
- * ```
- *
- * @augments Material
- */
-class ShaderMaterial extends Material {
-
- /**
- * Constructs a new shader material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isShaderMaterial = true;
-
- this.type = 'ShaderMaterial';
-
- /**
- * Defines custom constants using `#define` directives within the GLSL code
- * for both the vertex shader and the fragment shader; each key/value pair
- * yields another directive.
- * ```js
- * defines: {
- * FOO: 15,
- * BAR: true
- * }
- * ```
- * Yields the lines:
- * ```
- * #define FOO 15
- * #define BAR true
- * ```
- *
- * @type {Object}
- */
- this.defines = {};
-
- /**
- * An object of the form:
- * ```js
- * {
- * "uniform1": { value: 1.0 },
- * "uniform2": { value: 2 }
- * }
- * ```
- * specifying the uniforms to be passed to the shader code; keys are uniform
- * names, values are definitions of the form
- * ```
- * {
- * value: 1.0
- * }
- * ```
- * where `value` is the value of the uniform. Names must match the name of
- * the uniform, as defined in the GLSL code. Note that uniforms are refreshed
- * on every frame, so updating the value of the uniform will immediately
- * update the value available to the GLSL code.
- *
- * @type {Object}
- */
- this.uniforms = {};
-
- /**
- * An array holding uniforms groups for configuring UBOs.
- *
- * @type {Array}
- */
- this.uniformsGroups = [];
-
- /**
- * Vertex shader GLSL code. This is the actual code for the shader.
- *
- * @type {string}
- */
- this.vertexShader = default_vertex;
-
- /**
- * Fragment shader GLSL code. This is the actual code for the shader.
- *
- * @type {string}
- */
- this.fragmentShader = default_fragment;
-
- /**
- * Controls line thickness or lines.
- *
- * WebGL and WebGPU ignore this setting and always render line primitives with a
- * width of one pixel.
- *
- * @type {number}
- * @default 1
- */
- this.linewidth = 1;
-
- /**
- * Renders the geometry as a wireframe.
- *
- * @type {boolean}
- * @default false
- */
- this.wireframe = false;
-
- /**
- * Controls the thickness of the wireframe.
- *
- * WebGL and WebGPU ignore this property and always render
- * 1 pixel wide lines.
- *
- * @type {number}
- * @default 1
- */
- this.wireframeLinewidth = 1;
-
- /**
- * Defines whether the material color is affected by global fog settings; `true`
- * to pass fog uniforms to the shader.
- *
- * Setting this property to `true` requires the definition of fog uniforms. It is
- * recommended to use `UniformsUtils.merge()` to combine the custom shader uniforms
- * with predefined fog uniforms.
- *
- * ```js
- * const material = new ShaderMaterial( {
- * uniforms: UniformsUtils.merge( [ UniformsLib[ 'fog' ], shaderUniforms ] );
- * vertexShader: vertexShader,
- * fragmentShader: fragmentShader,
- * fog: true
- * } );
- * ```
- *
- * @type {boolean}
- * @default false
- */
- this.fog = false;
-
- /**
- * Defines whether this material uses lighting; `true` to pass uniform data
- * related to lighting to this shader.
- *
- * @type {boolean}
- * @default false
- */
- this.lights = false;
-
- /**
- * Defines whether this material supports clipping; `true` to let the renderer
- * pass the clippingPlanes uniform.
- *
- * @type {boolean}
- * @default false
- */
- this.clipping = false;
-
- /**
- * Overwritten and set to `true` by default.
- *
- * @type {boolean}
- * @default true
- */
- this.forceSinglePass = true;
-
- /**
- * This object allows to enable certain WebGL 2 extensions.
- *
- * - clipCullDistance: set to `true` to use vertex shader clipping
- * - multiDraw: set to `true` to use vertex shader multi_draw / enable gl_DrawID
- *
- * @type {{clipCullDistance:false,multiDraw:false}}
- */
- this.extensions = {
- clipCullDistance: false, // set to use vertex shader clipping
- multiDraw: false // set to use vertex shader multi_draw / enable gl_DrawID
- };
-
- /**
- * When the rendered geometry doesn't include these attributes but the
- * material does, these default values will be passed to the shaders. This
- * avoids errors when buffer data is missing.
- *
- * - color: [ 1, 1, 1 ]
- * - uv: [ 0, 0 ]
- * - uv1: [ 0, 0 ]
- *
- * @type {Object}
- */
- this.defaultAttributeValues = {
- 'color': [ 1, 1, 1 ],
- 'uv': [ 0, 0 ],
- 'uv1': [ 0, 0 ]
- };
-
- /**
- * If set, this calls [gl.bindAttribLocation](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/bindAttribLocation)
- * to bind a generic vertex index to an attribute variable.
- *
- * @type {string|undefined}
- * @default undefined
- */
- this.index0AttributeName = undefined;
-
- /**
- * Can be used to force a uniform update while changing uniforms in
- * {@link Object3D#onBeforeRender}.
- *
- * @type {boolean}
- * @default false
- */
- this.uniformsNeedUpdate = false;
-
- /**
- * Defines the GLSL version of custom shader code.
- *
- * @type {?(GLSL1|GLSL3)}
- * @default null
- */
- this.glslVersion = null;
-
- if ( parameters !== undefined ) {
-
- this.setValues( parameters );
-
- }
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.fragmentShader = source.fragmentShader;
- this.vertexShader = source.vertexShader;
-
- this.uniforms = cloneUniforms( source.uniforms );
- this.uniformsGroups = cloneUniformsGroups( source.uniformsGroups );
-
- this.defines = Object.assign( {}, source.defines );
-
- this.wireframe = source.wireframe;
- this.wireframeLinewidth = source.wireframeLinewidth;
-
- this.fog = source.fog;
- this.lights = source.lights;
- this.clipping = source.clipping;
-
- this.extensions = Object.assign( {}, source.extensions );
-
- this.glslVersion = source.glslVersion;
-
- this.defaultAttributeValues = Object.assign( {}, source.defaultAttributeValues );
-
- this.index0AttributeName = source.index0AttributeName;
-
- this.uniformsNeedUpdate = source.uniformsNeedUpdate;
-
- return this;
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.glslVersion = this.glslVersion;
- data.uniforms = {};
-
- for ( const name in this.uniforms ) {
-
- const uniform = this.uniforms[ name ];
- const value = uniform.value;
-
- if ( value && value.isTexture ) {
-
- data.uniforms[ name ] = {
- type: 't',
- value: value.toJSON( meta ).uuid
- };
-
- } else if ( value && value.isColor ) {
-
- data.uniforms[ name ] = {
- type: 'c',
- value: value.getHex()
- };
-
- } else if ( value && value.isVector2 ) {
-
- data.uniforms[ name ] = {
- type: 'v2',
- value: value.toArray()
- };
-
- } else if ( value && value.isVector3 ) {
-
- data.uniforms[ name ] = {
- type: 'v3',
- value: value.toArray()
- };
-
- } else if ( value && value.isVector4 ) {
-
- data.uniforms[ name ] = {
- type: 'v4',
- value: value.toArray()
- };
-
- } else if ( value && value.isMatrix3 ) {
-
- data.uniforms[ name ] = {
- type: 'm3',
- value: value.toArray()
- };
-
- } else if ( value && value.isMatrix4 ) {
-
- data.uniforms[ name ] = {
- type: 'm4',
- value: value.toArray()
- };
-
- } else {
-
- data.uniforms[ name ] = {
- value: value
- };
-
- // note: the array variants v2v, v3v, v4v, m4v and tv are not supported so far
-
- }
-
- }
-
- if ( Object.keys( this.defines ).length > 0 ) data.defines = this.defines;
-
- data.vertexShader = this.vertexShader;
- data.fragmentShader = this.fragmentShader;
-
- data.lights = this.lights;
- data.clipping = this.clipping;
-
- const extensions = {};
-
- for ( const key in this.extensions ) {
-
- if ( this.extensions[ key ] === true ) extensions[ key ] = true;
-
- }
-
- if ( Object.keys( extensions ).length > 0 ) data.extensions = extensions;
-
- return data;
-
- }
-
- /**
- * Deserializes the material from the given JSON.
- *
- * @param {Object} json - The JSON holding the serialized material.
- * @param {Object} textures - A dictionary holding textures referenced by the material.
- * @return {ShaderMaterial} A reference to this material.
- */
- fromJSON( json, textures ) {
-
- super.fromJSON( json, textures );
-
- if ( json.uniforms !== undefined ) {
-
- for ( const name in json.uniforms ) {
-
- const uniform = json.uniforms[ name ];
-
- this.uniforms[ name ] = {};
-
- switch ( uniform.type ) {
-
- case 't':
- this.uniforms[ name ].value = textures[ uniform.value ] || null;
- break;
-
- case 'c':
- this.uniforms[ name ].value = new Color().setHex( uniform.value );
- break;
-
- case 'v2':
- this.uniforms[ name ].value = new Vector2().fromArray( uniform.value );
- break;
-
- case 'v3':
- this.uniforms[ name ].value = new Vector3().fromArray( uniform.value );
- break;
-
- case 'v4':
- this.uniforms[ name ].value = new Vector4().fromArray( uniform.value );
- break;
-
- case 'm3':
- this.uniforms[ name ].value = new Matrix3().fromArray( uniform.value );
- break;
-
- case 'm4':
- this.uniforms[ name ].value = new Matrix4().fromArray( uniform.value );
- break;
-
- default:
- this.uniforms[ name ].value = uniform.value;
-
- }
-
- }
-
- }
-
- if ( json.defines !== undefined ) this.defines = json.defines;
- if ( json.vertexShader !== undefined ) this.vertexShader = json.vertexShader;
- if ( json.fragmentShader !== undefined ) this.fragmentShader = json.fragmentShader;
- if ( json.glslVersion !== undefined ) this.glslVersion = json.glslVersion;
-
- if ( json.extensions !== undefined ) {
-
- for ( const key in json.extensions ) {
-
- this.extensions[ key ] = json.extensions[ key ];
-
- }
-
- }
-
- if ( json.lights !== undefined ) this.lights = json.lights;
- if ( json.clipping !== undefined ) this.clipping = json.clipping;
-
- return this;
-
- }
-
-}
-
-/**
- * This class works just like {@link ShaderMaterial}, except that definitions
- * of built-in uniforms and attributes are not automatically prepended to the
- * GLSL shader code.
- *
- * `RawShaderMaterial` can only be used with {@link WebGLRenderer}.
- *
- * @augments ShaderMaterial
- */
-class RawShaderMaterial extends ShaderMaterial {
-
- /**
- * Constructs a new raw shader material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super( parameters );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isRawShaderMaterial = true;
-
- this.type = 'RawShaderMaterial';
-
- }
-
-}
-
-/**
- * A standard physically based material, using Metallic-Roughness workflow.
- *
- * Physically based rendering (PBR) has recently become the standard in many
- * 3D applications, such as [Unity](https://blogs.unity3d.com/2014/10/29/physically-based-shading-in-unity-5-a-primer/),
- * [Unreal](https://docs.unrealengine.com/latest/INT/Engine/Rendering/Materials/PhysicallyBased/) and
- * [3D Studio Max](http://area.autodesk.com/blogs/the-3ds-max-blog/what039s-new-for-rendering-in-3ds-max-2017).
- *
- * This approach differs from older approaches in that instead of using
- * approximations for the way in which light interacts with a surface, a
- * physically correct model is used. The idea is that, instead of tweaking
- * materials to look good under specific lighting, a material can be created
- * that will react 'correctly' under all lighting scenarios.
- *
- * In practice this gives a more accurate and realistic looking result than
- * the {@link MeshLambertMaterial} or {@link MeshPhongMaterial}, at the cost of
- * being somewhat more computationally expensive. `MeshStandardMaterial` uses per-fragment
- * shading.
- *
- * Note that for best results you should always specify an environment map when using this material.
- *
- * For a non-technical introduction to the concept of PBR and how to set up a
- * PBR material, check out these articles by the people at [marmoset](https://www.marmoset.co):
- *
- * - [Basic Theory of Physically Based Rendering](https://www.marmoset.co/posts/basic-theory-of-physically-based-rendering/)
- * - [Physically Based Rendering and You Can Too](https://www.marmoset.co/posts/physically-based-rendering-and-you-can-too/)
- *
- * Technical details of the approach used in three.js (and most other PBR systems) can be found is this
- * [paper from Disney](https://media.disneyanimation.com/uploads/production/publication_asset/48/asset/s2012_pbs_disney_brdf_notes_v3.pdf)
- * (pdf), by Brent Burley.
- *
- * @augments Material
- * @demo scenes/material-browser.html#MeshStandardMaterial
- */
-class MeshStandardMaterial extends Material {
-
- /**
- * Constructs a new mesh standard material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMeshStandardMaterial = true;
-
- this.type = 'MeshStandardMaterial';
-
- this.defines = { 'STANDARD': '' };
-
- /**
- * Color of the material.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.color = new Color( 0xffffff ); // diffuse
-
- /**
- * How rough the material appears. `0.0` means a smooth mirror reflection, `1.0`
- * means fully diffuse. If `roughnessMap` is also provided,
- * both values are multiplied.
- *
- * @type {number}
- * @default 1
- */
- this.roughness = 1.0;
-
- /**
- * How much the material is like a metal. Non-metallic materials such as wood
- * or stone use `0.0`, metallic use `1.0`, with nothing (usually) in between.
- * A value between `0.0` and `1.0` could be used for a rusty metal look.
- * If `metalnessMap` is also provided, both values are multiplied.
- *
- * @type {number}
- * @default 0
- */
- this.metalness = 0.0;
-
- /**
- * The color map. May optionally include an alpha channel, typically combined
- * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
- * color is modulated by the diffuse `color`.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * The light map. Requires a second set of UVs.
- *
- * `lightMap` represents pre-baked illuminance data, and the texture must be assigned
- * a {@link Texture#colorSpace}. Most `lightMap` textures set
- * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats
- * such as `.exr` or `.hdr`.
- *
- * @type {?Texture}
- * @default null
- */
- this.lightMap = null;
-
- /**
- * Intensity of the baked light.
- *
- * @type {number}
- * @default 1
- */
- this.lightMapIntensity = 1.0;
-
- /**
- * The red channel of this texture is used as the ambient occlusion map.
- * Requires a second set of UVs.
- *
- * `aoMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.aoMap = null;
-
- /**
- * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`
- * disables ambient occlusion. Where intensity is `1` and the AO map's
- * red channel is also `1`, ambient light is fully occluded on a surface.
- *
- * @type {number}
- * @default 1
- */
- this.aoMapIntensity = 1.0;
-
- /**
- * Emissive (light) color of the material, essentially a solid color
- * unaffected by other lighting.
- *
- * @type {Color}
- * @default (0,0,0)
- */
- this.emissive = new Color( 0x000000 );
-
- /**
- * Intensity of the emissive light. Modulates the emissive color.
- *
- * @type {number}
- * @default 1
- */
- this.emissiveIntensity = 1.0;
-
- /**
- * Set emissive (glow) map. The emissive map color is modulated by the
- * emissive color and the emissive intensity. If you have an emissive map,
- * be sure to set the emissive color to something other than black.
- *
- * `emissiveMap` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `emissiveMap` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.emissiveMap = null;
-
- /**
- * The texture to create a bump map. The black and white values map to the
- * perceived depth in relation to the lights. Bump doesn't actually affect
- * the geometry of the object, only the lighting. If a normal map is defined
- * this will be ignored.
- *
- * `bumpMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.bumpMap = null;
-
- /**
- * How much the bump map affects the material. Typical range is `[0,1]`.
- *
- * @type {number}
- * @default 1
- */
- this.bumpScale = 1;
-
- /**
- * The texture to create a normal map. The RGB values affect the surface
- * normal for each pixel fragment and change the way the color is lit. Normal
- * maps do not change the actual shape of the surface, only the lighting. In
- * case the material has a normal map authored using the left handed
- * convention, the `y` component of `normalScale` should be negated to compensate
- * for the different handedness.
- *
- * `normalMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.normalMap = null;
-
- /**
- * The type of normal map.
- *
- * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
- * @default TangentSpaceNormalMap
- */
- this.normalMapType = TangentSpaceNormalMap;
-
- /**
- * How much the normal map affects the material. Typical value range is `[0,1]`.
- *
- * @type {Vector2}
- * @default (1,1)
- */
- this.normalScale = new Vector2( 1, 1 );
-
- /**
- * The displacement map affects the position of the mesh's vertices. Unlike
- * other maps which only affect the light and shade of the material the
- * displaced vertices can cast shadows, block other objects, and otherwise
- * act as real geometry. The displacement texture is an image where the value
- * of each pixel (white being the highest) is mapped against, and
- * repositions, the vertices of the mesh. For best results, pair a
- * displacement map with a matching normal map, since the renderer can
- * not recompute surface normals from the displaced vertices.
- *
- * `displacementMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.displacementMap = null;
-
- /**
- * How much the displacement map affects the mesh (where black is no
- * displacement, and white is maximum displacement). Without a displacement
- * map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementScale = 1;
-
- /**
- * The offset of the displacement map's values on the mesh's vertices.
- * The bias is added to the scaled sample of the displacement map.
- * Without a displacement map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementBias = 0;
-
- /**
- * The green channel of this texture is used to alter the roughness of the
- * material.
- *
- * `roughnessMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.roughnessMap = null;
-
- /**
- * The blue channel of this texture is used to alter the metalness of the
- * material.
- *
- * `metalnessMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.metalnessMap = null;
-
- /**
- * The alpha map is a grayscale texture that controls the opacity across the
- * surface (black: fully transparent; white: fully opaque).
- *
- * Only the color of the texture is used, ignoring the alpha channel if one
- * exists. For RGB and RGBA textures, the renderer will use the green channel
- * when sampling this texture due to the extra bit of precision provided for
- * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
- * luminance/alpha textures will also still work as expected.
- *
- * `alphaMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.alphaMap = null;
-
- /**
- * The environment map. To ensure a physically correct rendering, environment maps
- * are internally pre-processed with {@link PMREMGenerator}.
- *
- * `envMap` represents luminance data, and the texture must be assigned
- * a {@link Texture#colorSpace}. Most `envMap` textures set
- * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats
- * such as `.exr` or `.hdr`.
- *
- * @type {?Texture}
- * @default null
- */
- this.envMap = null;
-
- /**
- * The rotation of the environment map in radians.
- *
- * @type {Euler}
- * @default (0,0,0)
- */
- this.envMapRotation = new Euler();
-
- /**
- * Scales the effect of the environment map by multiplying its color.
- *
- * @type {number}
- * @default 1
- */
- this.envMapIntensity = 1.0;
-
- /**
- * Renders the geometry as a wireframe.
- *
- * @type {boolean}
- * @default false
- */
- this.wireframe = false;
-
- /**
- * Controls the thickness of the wireframe.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {number}
- * @default 1
- */
- this.wireframeLinewidth = 1;
-
- /**
- * Defines appearance of wireframe ends.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.wireframeLinecap = 'round';
-
- /**
- * Defines appearance of wireframe joints.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.wireframeLinejoin = 'round';
-
- /**
- * Whether the material is rendered with flat shading or not.
- *
- * @type {boolean}
- * @default false
- */
- this.flatShading = false;
-
- /**
- * Whether the material is affected by fog or not.
- *
- * @type {boolean}
- * @default true
- */
- this.fog = true;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.defines = { 'STANDARD': '' };
-
- this.color.copy( source.color );
- this.roughness = source.roughness;
- this.metalness = source.metalness;
-
- this.map = source.map;
-
- this.lightMap = source.lightMap;
- this.lightMapIntensity = source.lightMapIntensity;
-
- this.aoMap = source.aoMap;
- this.aoMapIntensity = source.aoMapIntensity;
-
- this.emissive.copy( source.emissive );
- this.emissiveMap = source.emissiveMap;
- this.emissiveIntensity = source.emissiveIntensity;
-
- this.bumpMap = source.bumpMap;
- this.bumpScale = source.bumpScale;
-
- this.normalMap = source.normalMap;
- this.normalMapType = source.normalMapType;
- this.normalScale.copy( source.normalScale );
-
- this.displacementMap = source.displacementMap;
- this.displacementScale = source.displacementScale;
- this.displacementBias = source.displacementBias;
-
- this.roughnessMap = source.roughnessMap;
-
- this.metalnessMap = source.metalnessMap;
-
- this.alphaMap = source.alphaMap;
-
- this.envMap = source.envMap;
- this.envMapRotation.copy( source.envMapRotation );
- this.envMapIntensity = source.envMapIntensity;
-
- this.wireframe = source.wireframe;
- this.wireframeLinewidth = source.wireframeLinewidth;
- this.wireframeLinecap = source.wireframeLinecap;
- this.wireframeLinejoin = source.wireframeLinejoin;
-
- this.flatShading = source.flatShading;
-
- this.fog = source.fog;
-
- return this;
-
- }
-
-}
-
-/**
- * An extension of the {@link MeshStandardMaterial}, providing more advanced
- * physically-based rendering properties:
- *
- * - Anisotropy: Ability to represent the anisotropic property of materials
- * as observable with brushed metals.
- * - Clearcoat: Some materials — like car paints, carbon fiber, and wet surfaces — require
- * a clear, reflective layer on top of another layer that may be irregular or rough.
- * Clearcoat approximates this effect, without the need for a separate transparent surface.
- * - Iridescence: Allows to render the effect where hue varies depending on the viewing
- * angle and illumination angle. This can be seen on soap bubbles, oil films, or on the
- * wings of many insects.
- * - Physically-based transparency: One limitation of {@link Material#opacity} is that highly
- * transparent materials are less reflective. Physically-based transmission provides a more
- * realistic option for thin, transparent surfaces like glass.
- * - Advanced reflectivity: More flexible reflectivity for non-metallic materials.
- * - Retroreflection: Redirects specular light back toward the light source for
- * safety materials like road markings and reflective tape.
- * - Sheen: Can be used for representing cloth and fabric materials.
- *
- * As a result of these complex shading features, `MeshPhysicalMaterial` has a
- * higher performance cost, per pixel, than other three.js materials. Most
- * effects are disabled by default, and add cost as they are enabled. For
- * best results, always specify an environment map when using this material.
- *
- * @augments MeshStandardMaterial
- * @demo scenes/material-browser.html#MeshPhysicalMaterial
- */
-class MeshPhysicalMaterial extends MeshStandardMaterial {
-
- /**
- * Constructs a new mesh physical material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMeshPhysicalMaterial = true;
-
- this.defines = {
-
- 'STANDARD': '',
- 'PHYSICAL': ''
-
- };
-
- this.type = 'MeshPhysicalMaterial';
-
- /**
- * The rotation of the anisotropy in tangent, bitangent space, measured in radians
- * counter-clockwise from the tangent. When `anisotropyMap` is present, this
- * property provides additional rotation to the vectors in the texture.
- *
- * @type {number}
- * @default 1
- */
- this.anisotropyRotation = 0;
-
- /**
- * Red and green channels represent the anisotropy direction in `[-1, 1]` tangent,
- * bitangent space, to be rotated by `anisotropyRotation`. The blue channel
- * contains strength as `[0, 1]` to be multiplied by `anisotropy`.
- *
- * `anisotropyMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.anisotropyMap = null;
-
- /**
- * The red channel of this texture is multiplied against `clearcoat`,
- * for per-pixel control over a coating's intensity.
- *
- * `clearcoatMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.clearcoatMap = null;
-
- /**
- * Roughness of the clear coat layer, from `0.0` to `1.0`.
- *
- * @type {number}
- * @default 0
- */
- this.clearcoatRoughness = 0.0;
-
- /**
- * The green channel of this texture is multiplied against
- * `clearcoatRoughness`, for per-pixel control over a coating's roughness.
- *
- * `clearcoatRoughnessMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.clearcoatRoughnessMap = null;
-
- /**
- * How much `clearcoatNormalMap` affects the clear coat layer, from
- * `(0,0)` to `(1,1)`.
- *
- * @type {Vector2}
- * @default (1,1)
- */
- this.clearcoatNormalScale = new Vector2( 1, 1 );
-
- /**
- * Can be used to enable independent normals for the clear coat layer.
- *
- * `clearcoatNormalMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.clearcoatNormalMap = null;
-
- /**
- * Index-of-refraction for non-metallic materials, from `1.0` to `2.333`.
- *
- * @type {number}
- * @default 1.5
- */
- this.ior = 1.5;
-
- /**
- * Degree of reflectivity, from `0.0` to `1.0`. Default is `0.5`, which
- * corresponds to an index-of-refraction of `1.5`.
- *
- * This models the reflectivity of non-metallic materials. It has no effect
- * when `metalness` is `1.0`
- *
- * @name MeshPhysicalMaterial#reflectivity
- * @type {number}
- * @default 0.5
- */
- Object.defineProperty( this, 'reflectivity', {
- get: function () {
-
- return ( clamp( 2.5 * ( this.ior - 1 ) / ( this.ior + 1 ), 0, 1 ) );
-
- },
- set: function ( reflectivity ) {
-
- this.ior = ( 1 + 0.4 * reflectivity ) / ( 1 - 0.4 * reflectivity );
-
- }
- } );
-
- /**
- * The red channel of this texture is multiplied against `iridescence`, for per-pixel
- * control over iridescence.
- *
- * `iridescenceMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.iridescenceMap = null;
-
- /**
- * Strength of the iridescence RGB color shift effect, represented by an index-of-refraction.
- * Between `1.0` to `2.333`.
- *
- * @type {number}
- * @default 1.3
- */
- this.iridescenceIOR = 1.3;
-
- /**
- *Array of exactly 2 elements, specifying minimum and maximum thickness of the iridescence layer.
- Thickness of iridescence layer has an equivalent effect of the one `thickness` has on `ior`.
- *
- * @type {Array}
- * @default [100,400]
- */
- this.iridescenceThicknessRange = [ 100, 400 ];
-
- /**
- * A texture that defines the thickness of the iridescence layer, stored in the green channel.
- * Minimum and maximum values of thickness are defined by `iridescenceThicknessRange` array:
- * - `0.0` in the green channel will result in thickness equal to first element of the array.
- * - `1.0` in the green channel will result in thickness equal to second element of the array.
- * - Values in-between will linearly interpolate between the elements of the array.
- *
- * `iridescenceThicknessMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.iridescenceThicknessMap = null;
-
- /**
- * The sheen tint.
- *
- * @type {Color}
- * @default (0,0,0)
- */
- this.sheenColor = new Color( 0x000000 );
-
- /**
- * The RGB channels of this texture are multiplied against `sheenColor`, for per-pixel control
- * over sheen tint.
- *
- * `sheenColorMap` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `sheenColorMap` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.sheenColorMap = null;
-
- /**
- * Roughness of the sheen layer, from `0.0` to `1.0`.
- *
- * @type {number}
- * @default 1
- */
- this.sheenRoughness = 1.0;
-
- /**
- * The alpha channel of this texture is multiplied against `sheenRoughness`, for per-pixel control
- * over sheen roughness.
- *
- * `sheenRoughnessMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.sheenRoughnessMap = null;
-
- /**
- * The red channel of this texture is multiplied against `transmission`, for per-pixel control over
- * optical transparency.
- *
- * `transmissionMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.transmissionMap = null;
-
- /**
- * The thickness of the volume beneath the surface. The value is given in the
- * coordinate space of the mesh. If the value is `0` the material is
- * thin-walled. Otherwise the material is a volume boundary.
- *
- * @type {number}
- * @default 0
- */
- this.thickness = 0;
-
- /**
- * A texture that defines the thickness, stored in the green channel. This will
- * be multiplied by `thickness`.
- *
- * `thicknessMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.thicknessMap = null;
-
- /**
- * Density of the medium given as the average distance that light travels in
- * the medium before interacting with a particle. The value is given in world
- * space units, and must be greater than zero.
- *
- * @type {number}
- * @default Infinity
- */
- this.attenuationDistance = Infinity;
-
- /**
- * The color that white light turns into due to absorption when reaching the
- * attenuation distance.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.attenuationColor = new Color( 1, 1, 1 );
-
- /**
- * A float that scales the amount of specular reflection for non-metals only.
- * When set to zero, the model is effectively Lambertian. From `0.0` to `1.0`.
- *
- * @type {number}
- * @default 1
- */
- this.specularIntensity = 1.0;
-
- /**
- * The alpha channel of this texture is multiplied against `specularIntensity`,
- * for per-pixel control over specular intensity.
- *
- * `specularIntensityMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.specularIntensityMap = null;
-
- /**
- * Tints the specular reflection at normal incidence for non-metals only.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.specularColor = new Color( 1, 1, 1 );
-
- /**
- * The RGB channels of this texture are multiplied against `specularColor`,
- * for per-pixel control over specular color.
- *
- * `specularColorMap` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `specularColorMap` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.specularColorMap = null;
-
- this._anisotropy = 0;
- this._clearcoat = 0;
- this._dispersion = 0;
- this._iridescence = 0;
- this._retroreflectivity = 0;
- this._sheen = 0.0;
- this._transmission = 0;
-
- this.setValues( parameters );
-
- }
-
- /**
- * The anisotropy strength, from `0.0` to `1.0`.
- *
- * @type {number}
- * @default 0
- */
- get anisotropy() {
-
- return this._anisotropy;
-
- }
-
- set anisotropy( value ) {
-
- if ( this._anisotropy > 0 !== value > 0 ) {
-
- this.version ++;
-
- }
-
- this._anisotropy = value;
-
- }
-
- /**
- * Represents the intensity of the clear coat layer, from `0.0` to `1.0`. Use
- * clear coat related properties to enable multilayer materials that have a
- * thin translucent layer over the base layer.
- *
- * @type {number}
- * @default 0
- */
- get clearcoat() {
-
- return this._clearcoat;
-
- }
-
- set clearcoat( value ) {
-
- if ( this._clearcoat > 0 !== value > 0 ) {
-
- this.version ++;
-
- }
-
- this._clearcoat = value;
-
- }
- /**
- * The intensity of the iridescence layer, simulating RGB color shift based on the angle between
- * the surface and the viewer, from `0.0` to `1.0`.
- *
- * @type {number}
- * @default 0
- */
- get iridescence() {
-
- return this._iridescence;
-
- }
-
- set iridescence( value ) {
-
- if ( this._iridescence > 0 !== value > 0 ) {
-
- this.version ++;
-
- }
-
- this._iridescence = value;
-
- }
-
- /**
- * Defines the strength of the angular separation of colors (chromatic aberration) transmitting
- * through a relatively clear volume. Any value zero or larger is valid, the typical range of
- * realistic values is `[0, 1]`. This property can be only be used with transmissive objects.
- *
- * @type {number}
- * @default 0
- */
- get dispersion() {
-
- return this._dispersion;
-
- }
-
- set dispersion( value ) {
-
- if ( this._dispersion > 0 !== value > 0 ) {
-
- this.version ++;
-
- }
-
- this._dispersion = value;
-
- }
-
- /**
- * The strength of retroreflection, from `0.0` to `1.0`. A value of `1.0`
- * evaluates the material's microfacet reflection with the view direction
- * reflected about the surface normal, redirecting the specular lobe back
- * toward the light source.
- *
- * @type {number}
- * @default 0
- */
- get retroreflectivity() {
-
- return this._retroreflectivity;
-
- }
-
- set retroreflectivity( value ) {
-
- if ( this._retroreflectivity > 0 !== value > 0 ) {
-
- this.version ++;
-
- }
-
- this._retroreflectivity = value;
-
- }
-
- /**
- * The intensity of the sheen layer, from `0.0` to `1.0`.
- *
- * @type {number}
- * @default 0
- */
- get sheen() {
-
- return this._sheen;
-
- }
-
- set sheen( value ) {
-
- if ( this._sheen > 0 !== value > 0 ) {
-
- this.version ++;
-
- }
-
- this._sheen = value;
-
- }
-
- /**
- * Degree of transmission (or optical transparency), from `0.0` to `1.0`.
- *
- * Thin, transparent or semitransparent, plastic or glass materials remain
- * largely reflective even if they are fully transmissive. The transmission
- * property can be used to model these materials.
- *
- * When transmission is non-zero, `opacity` should be set to `1`.
- *
- * @type {number}
- * @default 0
- */
- get transmission() {
-
- return this._transmission;
-
- }
-
- set transmission( value ) {
-
- if ( this._transmission > 0 !== value > 0 ) {
-
- this.version ++;
-
- }
-
- this._transmission = value;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.defines = {
-
- 'STANDARD': '',
- 'PHYSICAL': ''
-
- };
-
- this.anisotropy = source.anisotropy;
- this.anisotropyRotation = source.anisotropyRotation;
- this.anisotropyMap = source.anisotropyMap;
-
- this.clearcoat = source.clearcoat;
- this.clearcoatMap = source.clearcoatMap;
- this.clearcoatRoughness = source.clearcoatRoughness;
- this.clearcoatRoughnessMap = source.clearcoatRoughnessMap;
- this.clearcoatNormalMap = source.clearcoatNormalMap;
- this.clearcoatNormalScale.copy( source.clearcoatNormalScale );
-
- this.dispersion = source.dispersion;
- this.ior = source.ior;
-
- this.iridescence = source.iridescence;
- this.iridescenceMap = source.iridescenceMap;
- this.iridescenceIOR = source.iridescenceIOR;
- this.iridescenceThicknessRange = [ ...source.iridescenceThicknessRange ];
- this.iridescenceThicknessMap = source.iridescenceThicknessMap;
-
- this.retroreflectivity = source.retroreflectivity;
-
- this.sheen = source.sheen;
- this.sheenColor.copy( source.sheenColor );
- this.sheenColorMap = source.sheenColorMap;
- this.sheenRoughness = source.sheenRoughness;
- this.sheenRoughnessMap = source.sheenRoughnessMap;
-
- this.transmission = source.transmission;
- this.transmissionMap = source.transmissionMap;
-
- this.thickness = source.thickness;
- this.thicknessMap = source.thicknessMap;
- this.attenuationDistance = source.attenuationDistance;
- this.attenuationColor.copy( source.attenuationColor );
-
- this.specularIntensity = source.specularIntensity;
- this.specularIntensityMap = source.specularIntensityMap;
- this.specularColor.copy( source.specularColor );
- this.specularColorMap = source.specularColorMap;
-
- return this;
-
- }
-
-}
-
-/**
- * A material for shiny surfaces with specular highlights.
- *
- * The material uses a non-physically based [Blinn-Phong](https://en.wikipedia.org/wiki/Blinn-Phong_shading_model)
- * model for calculating reflectance. Unlike the Lambertian model used in the
- * {@link MeshLambertMaterial} this can simulate shiny surfaces with specular
- * highlights (such as varnished wood). `MeshPhongMaterial` uses per-fragment shading.
- *
- * Performance will generally be greater when using this material over the
- * {@link MeshStandardMaterial} or {@link MeshPhysicalMaterial}, at the cost of
- * some graphical accuracy.
- *
- * @augments Material
- * @demo scenes/material-browser.html#MeshPhongMaterial
- */
-class MeshPhongMaterial extends Material {
-
- /**
- * Constructs a new mesh phong material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMeshPhongMaterial = true;
-
- this.type = 'MeshPhongMaterial';
-
- /**
- * Color of the material.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.color = new Color( 0xffffff ); // diffuse
-
- /**
- * Specular color of the material. The default color is set to `0x111111` (very dark grey)
- *
- * This defines how shiny the material is and the color of its shine.
- *
- * @type {Color}
- */
- this.specular = new Color( 0x111111 );
-
- /**
- * How shiny the specular highlight is; a higher value gives a sharper highlight.
- *
- * @type {number}
- * @default 30
- */
- this.shininess = 30;
-
- /**
- * The color map. May optionally include an alpha channel, typically combined
- * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
- * color is modulated by the diffuse `color`.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * The light map. Requires a second set of UVs.
- *
- * `lightMap` represents pre-baked illuminance data, and the texture must be assigned
- * a {@link Texture#colorSpace}. Most `lightMap` textures set
- * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats
- * such as `.exr` or `.hdr`.
- *
- * @type {?Texture}
- * @default null
- */
- this.lightMap = null;
-
- /**
- * Intensity of the baked light.
- *
- * @type {number}
- * @default 1
- */
- this.lightMapIntensity = 1.0;
-
- /**
- * The red channel of this texture is used as the ambient occlusion map.
- * Requires a second set of UVs.
- *
- * `aoMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.aoMap = null;
-
- /**
- * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`
- * disables ambient occlusion. Where intensity is `1` and the AO map's
- * red channel is also `1`, ambient light is fully occluded on a surface.
- *
- * @type {number}
- * @default 1
- */
- this.aoMapIntensity = 1.0;
-
- /**
- * Emissive (light) color of the material, essentially a solid color
- * unaffected by other lighting.
- *
- * @type {Color}
- * @default (0,0,0)
- */
- this.emissive = new Color( 0x000000 );
-
- /**
- * Intensity of the emissive light. Modulates the emissive color.
- *
- * @type {number}
- * @default 1
- */
- this.emissiveIntensity = 1.0;
-
- /**
- * Set emissive (glow) map. The emissive map color is modulated by the
- * emissive color and the emissive intensity. If you have an emissive map,
- * be sure to set the emissive color to something other than black.
- *
- * `emissiveMap` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `emissiveMap` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.emissiveMap = null;
-
- /**
- * The texture to create a bump map. The black and white values map to the
- * perceived depth in relation to the lights. Bump doesn't actually affect
- * the geometry of the object, only the lighting. If a normal map is defined
- * this will be ignored.
- *
- * `bumpMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.bumpMap = null;
-
- /**
- * How much the bump map affects the material. Typical range is `[0,1]`.
- *
- * @type {number}
- * @default 1
- */
- this.bumpScale = 1;
-
- /**
- * The texture to create a normal map. The RGB values affect the surface
- * normal for each pixel fragment and change the way the color is lit. Normal
- * maps do not change the actual shape of the surface, only the lighting. In
- * case the material has a normal map authored using the left handed
- * convention, the `y` component of `normalScale` should be negated to compensate
- * for the different handedness.
- *
- * `normalMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.normalMap = null;
-
- /**
- * The type of normal map.
- *
- * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
- * @default TangentSpaceNormalMap
- */
- this.normalMapType = TangentSpaceNormalMap;
-
- /**
- * How much the normal map affects the material. Typical value range is `[0,1]`.
- *
- * @type {Vector2}
- * @default (1,1)
- */
- this.normalScale = new Vector2( 1, 1 );
-
- /**
- * The displacement map affects the position of the mesh's vertices. Unlike
- * other maps which only affect the light and shade of the material the
- * displaced vertices can cast shadows, block other objects, and otherwise
- * act as real geometry. The displacement texture is an image where the value
- * of each pixel (white being the highest) is mapped against, and
- * repositions, the vertices of the mesh. For best results, pair a
- * displacement map with a matching normal map, since the renderer can
- * not recompute surface normals from the displaced vertices.
- *
- * `displacementMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.displacementMap = null;
-
- /**
- * How much the displacement map affects the mesh (where black is no
- * displacement, and white is maximum displacement). Without a displacement
- * map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementScale = 1;
-
- /**
- * The offset of the displacement map's values on the mesh's vertices.
- * The bias is added to the scaled sample of the displacement map.
- * Without a displacement map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementBias = 0;
-
- /**
- * The specular map value affects both how much the specular surface
- * highlight contributes and how much of the environment map affects the
- * surface.
- *
- * `specularMap` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `specularMap` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.specularMap = null;
-
- /**
- * The alpha map is a grayscale texture that controls the opacity across the
- * surface (black: fully transparent; white: fully opaque).
- *
- * Only the color of the texture is used, ignoring the alpha channel if one
- * exists. For RGB and RGBA textures, the renderer will use the green channel
- * when sampling this texture due to the extra bit of precision provided for
- * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
- * luminance/alpha textures will also still work as expected.
- *
- * `alphaMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.alphaMap = null;
-
- /**
- * The environment map.
- *
- * `envMap` represents luminance data, and the texture must be assigned
- * a {@link Texture#colorSpace}. Most `envMap` textures set
- * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats
- * such as `.exr` or `.hdr`.
- *
- * @type {?Texture}
- * @default null
- */
- this.envMap = null;
-
- /**
- * The rotation of the environment map in radians.
- *
- * @type {Euler}
- * @default (0,0,0)
- */
- this.envMapRotation = new Euler();
-
- /**
- * How to combine the result of the surface's color with the environment map, if any.
- *
- * When set to `MixOperation`, the {@link MeshBasicMaterial#reflectivity} is used to
- * blend between the two colors.
- *
- * @type {(MultiplyOperation|MixOperation|AddOperation)}
- * @default MultiplyOperation
- */
- this.combine = MultiplyOperation;
-
- /**
- * How much the environment map affects the surface.
- * The valid range is between `0` (no reflections) and `1` (full reflections).
- *
- * @type {number}
- * @default 1
- */
- this.reflectivity = 1;
-
- /**
- * Scales the effect of the environment map by multiplying its color.
- *
- * @type {number}
- * @default 1
- */
- this.envMapIntensity = 1.0;
-
- /**
- * The index of refraction (IOR) of air (approximately 1) divided by the
- * index of refraction of the material. It is used with environment mapping
- * modes {@link CubeRefractionMapping} and {@link EquirectangularRefractionMapping}.
- * The refraction ratio should not exceed `1`.
- *
- * @type {number}
- * @default 0.98
- */
- this.refractionRatio = 0.98;
-
- /**
- * Renders the geometry as a wireframe.
- *
- * @type {boolean}
- * @default false
- */
- this.wireframe = false;
-
- /**
- * Controls the thickness of the wireframe.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {number}
- * @default 1
- */
- this.wireframeLinewidth = 1;
-
- /**
- * Defines appearance of wireframe ends.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.wireframeLinecap = 'round';
-
- /**
- * Defines appearance of wireframe joints.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.wireframeLinejoin = 'round';
-
- /**
- * Whether the material is rendered with flat shading or not.
- *
- * @type {boolean}
- * @default false
- */
- this.flatShading = false;
-
- /**
- * Whether the material is affected by fog or not.
- *
- * @type {boolean}
- * @default true
- */
- this.fog = true;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.color.copy( source.color );
- this.specular.copy( source.specular );
- this.shininess = source.shininess;
-
- this.map = source.map;
-
- this.lightMap = source.lightMap;
- this.lightMapIntensity = source.lightMapIntensity;
-
- this.aoMap = source.aoMap;
- this.aoMapIntensity = source.aoMapIntensity;
-
- this.emissive.copy( source.emissive );
- this.emissiveMap = source.emissiveMap;
- this.emissiveIntensity = source.emissiveIntensity;
-
- this.bumpMap = source.bumpMap;
- this.bumpScale = source.bumpScale;
-
- this.normalMap = source.normalMap;
- this.normalMapType = source.normalMapType;
- this.normalScale.copy( source.normalScale );
-
- this.displacementMap = source.displacementMap;
- this.displacementScale = source.displacementScale;
- this.displacementBias = source.displacementBias;
-
- this.specularMap = source.specularMap;
-
- this.alphaMap = source.alphaMap;
-
- this.envMap = source.envMap;
- this.envMapRotation.copy( source.envMapRotation );
- this.combine = source.combine;
- this.reflectivity = source.reflectivity;
- this.envMapIntensity = source.envMapIntensity;
- this.refractionRatio = source.refractionRatio;
-
- this.wireframe = source.wireframe;
- this.wireframeLinewidth = source.wireframeLinewidth;
- this.wireframeLinecap = source.wireframeLinecap;
- this.wireframeLinejoin = source.wireframeLinejoin;
-
- this.flatShading = source.flatShading;
-
- this.fog = source.fog;
-
- return this;
-
- }
-
-}
-
-/**
- * A material implementing toon shading.
- *
- * @augments Material
- * @demo scenes/material-browser.html#MeshToonMaterial
- */
-class MeshToonMaterial extends Material {
-
- /**
- * Constructs a new mesh toon material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMeshToonMaterial = true;
-
- this.defines = { 'TOON': '' };
-
- this.type = 'MeshToonMaterial';
-
- /**
- * Color of the material.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.color = new Color( 0xffffff );
-
- /**
- * The color map. May optionally include an alpha channel, typically combined
- * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
- * color is modulated by the diffuse `color`.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * Gradient map for toon shading. It's required to set
- * {@link Texture#minFilter} and {@link Texture#magFilter} to {@link NearestFilter}
- * when using this type of texture.
- *
- * `gradientMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.gradientMap = null;
-
- /**
- * The light map. Requires a second set of UVs.
- *
- * `lightMap` represents pre-baked illuminance data, and the texture must be assigned
- * a {@link Texture#colorSpace}. Most `lightMap` textures set
- * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats
- * such as `.exr` or `.hdr`.
- *
- * @type {?Texture}
- * @default null
- */
- this.lightMap = null;
-
- /**
- * Intensity of the baked light.
- *
- * @type {number}
- * @default 1
- */
- this.lightMapIntensity = 1.0;
-
- /**
- * The red channel of this texture is used as the ambient occlusion map.
- * Requires a second set of UVs.
- *
- * `aoMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.aoMap = null;
-
- /**
- * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`
- * disables ambient occlusion. Where intensity is `1` and the AO map's
- * red channel is also `1`, ambient light is fully occluded on a surface.
- *
- * @type {number}
- * @default 1
- */
- this.aoMapIntensity = 1.0;
-
- /**
- * Emissive (light) color of the material, essentially a solid color
- * unaffected by other lighting.
- *
- * @type {Color}
- * @default (0,0,0)
- */
- this.emissive = new Color( 0x000000 );
-
- /**
- * Intensity of the emissive light. Modulates the emissive color.
- *
- * @type {number}
- * @default 1
- */
- this.emissiveIntensity = 1.0;
-
- /**
- * Set emissive (glow) map. The emissive map color is modulated by the
- * emissive color and the emissive intensity. If you have an emissive map,
- * be sure to set the emissive color to something other than black.
- *
- * `emissiveMap` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `emissiveMap` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.emissiveMap = null;
-
- /**
- * The texture to create a bump map. The black and white values map to the
- * perceived depth in relation to the lights. Bump doesn't actually affect
- * the geometry of the object, only the lighting. If a normal map is defined
- * this will be ignored.
- *
- * `bumpMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.bumpMap = null;
-
- /**
- * How much the bump map affects the material. Typical range is `[0,1]`.
- *
- * @type {number}
- * @default 1
- */
- this.bumpScale = 1;
-
- /**
- * The texture to create a normal map. The RGB values affect the surface
- * normal for each pixel fragment and change the way the color is lit. Normal
- * maps do not change the actual shape of the surface, only the lighting. In
- * case the material has a normal map authored using the left handed
- * convention, the `y` component of `normalScale` should be negated to compensate
- * for the different handedness.
- *
- * `normalMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.normalMap = null;
-
- /**
- * The type of normal map.
- *
- * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
- * @default TangentSpaceNormalMap
- */
- this.normalMapType = TangentSpaceNormalMap;
-
- /**
- * How much the normal map affects the material. Typical value range is `[0,1]`.
- *
- * @type {Vector2}
- * @default (1,1)
- */
- this.normalScale = new Vector2( 1, 1 );
-
- /**
- * The displacement map affects the position of the mesh's vertices. Unlike
- * other maps which only affect the light and shade of the material the
- * displaced vertices can cast shadows, block other objects, and otherwise
- * act as real geometry. The displacement texture is an image where the value
- * of each pixel (white being the highest) is mapped against, and
- * repositions, the vertices of the mesh. For best results, pair a
- * displacement map with a matching normal map, since the renderer can
- * not recompute surface normals from the displaced vertices.
- *
- * `displacementMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.displacementMap = null;
-
- /**
- * How much the displacement map affects the mesh (where black is no
- * displacement, and white is maximum displacement). Without a displacement
- * map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementScale = 1;
-
- /**
- * The offset of the displacement map's values on the mesh's vertices.
- * The bias is added to the scaled sample of the displacement map.
- * Without a displacement map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementBias = 0;
-
- /**
- * The alpha map is a grayscale texture that controls the opacity across the
- * surface (black: fully transparent; white: fully opaque).
- *
- * Only the color of the texture is used, ignoring the alpha channel if one
- * exists. For RGB and RGBA textures, the renderer will use the green channel
- * when sampling this texture due to the extra bit of precision provided for
- * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
- * luminance/alpha textures will also still work as expected.
- *
- * `alphaMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.alphaMap = null;
-
- /**
- * Renders the geometry as a wireframe.
- *
- * @type {boolean}
- * @default false
- */
- this.wireframe = false;
-
- /**
- * Controls the thickness of the wireframe.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {number}
- * @default 1
- */
- this.wireframeLinewidth = 1;
-
- /**
- * Defines appearance of wireframe ends.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.wireframeLinecap = 'round';
-
- /**
- * Defines appearance of wireframe joints.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.wireframeLinejoin = 'round';
-
- /**
- * Whether the material is affected by fog or not.
- *
- * @type {boolean}
- * @default true
- */
- this.fog = true;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.color.copy( source.color );
-
- this.map = source.map;
- this.gradientMap = source.gradientMap;
-
- this.lightMap = source.lightMap;
- this.lightMapIntensity = source.lightMapIntensity;
-
- this.aoMap = source.aoMap;
- this.aoMapIntensity = source.aoMapIntensity;
-
- this.emissive.copy( source.emissive );
- this.emissiveMap = source.emissiveMap;
- this.emissiveIntensity = source.emissiveIntensity;
-
- this.bumpMap = source.bumpMap;
- this.bumpScale = source.bumpScale;
-
- this.normalMap = source.normalMap;
- this.normalMapType = source.normalMapType;
- this.normalScale.copy( source.normalScale );
-
- this.displacementMap = source.displacementMap;
- this.displacementScale = source.displacementScale;
- this.displacementBias = source.displacementBias;
-
- this.alphaMap = source.alphaMap;
-
- this.wireframe = source.wireframe;
- this.wireframeLinewidth = source.wireframeLinewidth;
- this.wireframeLinecap = source.wireframeLinecap;
- this.wireframeLinejoin = source.wireframeLinejoin;
-
- this.fog = source.fog;
-
- return this;
-
- }
-
-}
-
-/**
- * A material that maps the normal vectors to RGB colors.
- *
- * @augments Material
- * @demo scenes/material-browser.html#MeshNormalMaterial
- */
-class MeshNormalMaterial extends Material {
-
- /**
- * Constructs a new mesh normal material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMeshNormalMaterial = true;
-
- this.type = 'MeshNormalMaterial';
-
- /**
- * The texture to create a bump map. The black and white values map to the
- * perceived depth in relation to the lights. Bump doesn't actually affect
- * the geometry of the object, only the lighting. If a normal map is defined
- * this will be ignored.
- *
- * `bumpMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.bumpMap = null;
-
- /**
- * How much the bump map affects the material. Typical range is `[0,1]`.
- *
- * @type {number}
- * @default 1
- */
- this.bumpScale = 1;
-
- /**
- * The texture to create a normal map. The RGB values affect the surface
- * normal for each pixel fragment and change the way the color is lit. Normal
- * maps do not change the actual shape of the surface, only the lighting. In
- * case the material has a normal map authored using the left handed
- * convention, the `y` component of `normalScale` should be negated to compensate
- * for the different handedness.
- *
- * `normalMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.normalMap = null;
-
- /**
- * The type of normal map.
- *
- * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
- * @default TangentSpaceNormalMap
- */
- this.normalMapType = TangentSpaceNormalMap;
-
- /**
- * How much the normal map affects the material. Typical value range is `[0,1]`.
- *
- * @type {Vector2}
- * @default (1,1)
- */
- this.normalScale = new Vector2( 1, 1 );
-
- /**
- * The displacement map affects the position of the mesh's vertices. Unlike
- * other maps which only affect the light and shade of the material the
- * displaced vertices can cast shadows, block other objects, and otherwise
- * act as real geometry. The displacement texture is an image where the value
- * of each pixel (white being the highest) is mapped against, and
- * repositions, the vertices of the mesh. For best results, pair a
- * displacement map with a matching normal map, since the renderer can
- * not recompute surface normals from the displaced vertices.
- *
- * @type {?Texture}
- * @default null
- */
- this.displacementMap = null;
-
- /**
- * How much the displacement map affects the mesh (where black is no
- * displacement, and white is maximum displacement). Without a displacement
- * map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementScale = 1;
-
- /**
- * The offset of the displacement map's values on the mesh's vertices.
- * The bias is added to the scaled sample of the displacement map.
- * Without a displacement map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementBias = 0;
-
- /**
- * Renders the geometry as a wireframe.
- *
- * @type {boolean}
- * @default false
- */
- this.wireframe = false;
-
- /**
- * Controls the thickness of the wireframe.
- *
- * WebGL and WebGPU ignore this property and always render
- * 1 pixel wide lines.
- *
- * @type {number}
- * @default 1
- */
- this.wireframeLinewidth = 1;
-
- /**
- * Whether the material is rendered with flat shading or not.
- *
- * @type {boolean}
- * @default false
- */
- this.flatShading = false;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.bumpMap = source.bumpMap;
- this.bumpScale = source.bumpScale;
-
- this.normalMap = source.normalMap;
- this.normalMapType = source.normalMapType;
- this.normalScale.copy( source.normalScale );
-
- this.displacementMap = source.displacementMap;
- this.displacementScale = source.displacementScale;
- this.displacementBias = source.displacementBias;
-
- this.wireframe = source.wireframe;
- this.wireframeLinewidth = source.wireframeLinewidth;
-
- this.flatShading = source.flatShading;
-
- return this;
-
- }
-
-}
-
-/**
- * A material for non-shiny surfaces, without specular highlights.
- *
- * The material uses a non-physically based [Lambertian](https://en.wikipedia.org/wiki/Lambertian_reflectance)
- * model for calculating reflectance. This can simulate some surfaces (such
- * as untreated wood or stone) well, but cannot simulate shiny surfaces with
- * specular highlights (such as varnished wood). `MeshLambertMaterial` uses per-fragment
- * shading.
- *
- * Due to the simplicity of the reflectance and illumination models,
- * performance will be greater when using this material over the
- * {@link MeshPhongMaterial}, {@link MeshStandardMaterial} or
- * {@link MeshPhysicalMaterial}, at the cost of some graphical accuracy.
- *
- * @augments Material
- * @demo scenes/material-browser.html#MeshLambertMaterial
- */
-class MeshLambertMaterial extends Material {
-
- /**
- * Constructs a new mesh lambert material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMeshLambertMaterial = true;
-
- this.type = 'MeshLambertMaterial';
-
- /**
- * Color of the material.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.color = new Color( 0xffffff ); // diffuse
-
- /**
- * The color map. May optionally include an alpha channel, typically combined
- * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
- * color is modulated by the diffuse `color`.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * The light map. Requires a second set of UVs.
- *
- * `lightMap` represents pre-baked illuminance data, and the texture must be assigned
- * a {@link Texture#colorSpace}. Most `lightMap` textures set
- * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats
- * such as `.exr` or `.hdr`.
- *
- * @type {?Texture}
- * @default null
- */
- this.lightMap = null;
-
- /**
- * Intensity of the baked light.
- *
- * @type {number}
- * @default 1
- */
- this.lightMapIntensity = 1.0;
-
- /**
- * The red channel of this texture is used as the ambient occlusion map.
- * Requires a second set of UVs.
- *
- * `aoMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.aoMap = null;
-
- /**
- * Intensity of the ambient occlusion effect. Range is `[0,1]`, where `0`
- * disables ambient occlusion. Where intensity is `1` and the AO map's
- * red channel is also `1`, ambient light is fully occluded on a surface.
- *
- * @type {number}
- * @default 1
- */
- this.aoMapIntensity = 1.0;
-
- /**
- * Emissive (light) color of the material, essentially a solid color
- * unaffected by other lighting.
- *
- * @type {Color}
- * @default (0,0,0)
- */
- this.emissive = new Color( 0x000000 );
-
- /**
- * Intensity of the emissive light. Modulates the emissive color.
- *
- * @type {number}
- * @default 1
- */
- this.emissiveIntensity = 1.0;
-
- /**
- * Set emissive (glow) map. The emissive map color is modulated by the
- * emissive color and the emissive intensity. If you have an emissive map,
- * be sure to set the emissive color to something other than black.
- *
- * `emissiveMap` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `emissiveMap` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.emissiveMap = null;
-
- /**
- * The texture to create a bump map. The black and white values map to the
- * perceived depth in relation to the lights. Bump doesn't actually affect
- * the geometry of the object, only the lighting. If a normal map is defined
- * this will be ignored.
- *
- * `bumpMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.bumpMap = null;
-
- /**
- * How much the bump map affects the material. Typical range is `[0,1]`.
- *
- * @type {number}
- * @default 1
- */
- this.bumpScale = 1;
-
- /**
- * The texture to create a normal map. The RGB values affect the surface
- * normal for each pixel fragment and change the way the color is lit. Normal
- * maps do not change the actual shape of the surface, only the lighting. In
- * case the material has a normal map authored using the left handed
- * convention, the `y` component of `normalScale` should be negated to compensate
- * for the different handedness.
- *
- * `normalMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.normalMap = null;
-
- /**
- * The type of normal map.
- *
- * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
- * @default TangentSpaceNormalMap
- */
- this.normalMapType = TangentSpaceNormalMap;
-
- /**
- * How much the normal map affects the material. Typical value range is `[0,1]`.
- *
- * @type {Vector2}
- * @default (1,1)
- */
- this.normalScale = new Vector2( 1, 1 );
-
- /**
- * The displacement map affects the position of the mesh's vertices. Unlike
- * other maps which only affect the light and shade of the material the
- * displaced vertices can cast shadows, block other objects, and otherwise
- * act as real geometry. The displacement texture is an image where the value
- * of each pixel (white being the highest) is mapped against, and
- * repositions, the vertices of the mesh. For best results, pair a
- * displacement map with a matching normal map, since the renderer can
- * not recompute surface normals from the displaced vertices.
- *
- * `displacementMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.displacementMap = null;
-
- /**
- * How much the displacement map affects the mesh (where black is no
- * displacement, and white is maximum displacement). Without a displacement
- * map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementScale = 1;
-
- /**
- * The offset of the displacement map's values on the mesh's vertices.
- * The bias is added to the scaled sample of the displacement map.
- * Without a displacement map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementBias = 0;
-
- /**
- * Specular map used by the material.
- *
- * `specularMap` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `specularMap` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.specularMap = null;
-
- /**
- * The alpha map is a grayscale texture that controls the opacity across the
- * surface (black: fully transparent; white: fully opaque).
- *
- * Only the color of the texture is used, ignoring the alpha channel if one
- * exists. For RGB and RGBA textures, the renderer will use the green channel
- * when sampling this texture due to the extra bit of precision provided for
- * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
- * luminance/alpha textures will also still work as expected.
- *
- * `alphaMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.alphaMap = null;
-
- /**
- * The environment map.
- *
- * `envMap` represents luminance data, and the texture must be assigned
- * a {@link Texture#colorSpace}. Most `envMap` textures set
- * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats
- * such as `.exr` or `.hdr`.
- *
- * @type {?Texture}
- * @default null
- */
- this.envMap = null;
-
- /**
- * The rotation of the environment map in radians.
- *
- * @type {Euler}
- * @default (0,0,0)
- */
- this.envMapRotation = new Euler();
-
- /**
- * How to combine the result of the surface's color with the environment map, if any.
- *
- * When set to `MixOperation`, the {@link MeshBasicMaterial#reflectivity} is used to
- * blend between the two colors.
- *
- * @type {(MultiplyOperation|MixOperation|AddOperation)}
- * @default MultiplyOperation
- */
- this.combine = MultiplyOperation;
-
- /**
- * How much the environment map affects the surface.
- * The valid range is between `0` (no reflections) and `1` (full reflections).
- *
- * @type {number}
- * @default 1
- */
- this.reflectivity = 1;
-
- /**
- * Scales the effect of the environment map by multiplying its color.
- *
- * @type {number}
- * @default 1
- */
- this.envMapIntensity = 1.0;
-
- /**
- * The index of refraction (IOR) of air (approximately 1) divided by the
- * index of refraction of the material. It is used with environment mapping
- * modes {@link CubeRefractionMapping} and {@link EquirectangularRefractionMapping}.
- * The refraction ratio should not exceed `1`.
- *
- * @type {number}
- * @default 0.98
- */
- this.refractionRatio = 0.98;
-
- /**
- * Renders the geometry as a wireframe.
- *
- * @type {boolean}
- * @default false
- */
- this.wireframe = false;
-
- /**
- * Controls the thickness of the wireframe.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {number}
- * @default 1
- */
- this.wireframeLinewidth = 1;
-
- /**
- * Defines appearance of wireframe ends.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.wireframeLinecap = 'round';
-
- /**
- * Defines appearance of wireframe joints.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {('round'|'bevel'|'miter')}
- * @default 'round'
- */
- this.wireframeLinejoin = 'round';
-
- /**
- * Whether the material is rendered with flat shading or not.
- *
- * @type {boolean}
- * @default false
- */
- this.flatShading = false;
-
- /**
- * Whether the material is affected by fog or not.
- *
- * @type {boolean}
- * @default true
- */
- this.fog = true;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.color.copy( source.color );
-
- this.map = source.map;
-
- this.lightMap = source.lightMap;
- this.lightMapIntensity = source.lightMapIntensity;
-
- this.aoMap = source.aoMap;
- this.aoMapIntensity = source.aoMapIntensity;
-
- this.emissive.copy( source.emissive );
- this.emissiveMap = source.emissiveMap;
- this.emissiveIntensity = source.emissiveIntensity;
-
- this.bumpMap = source.bumpMap;
- this.bumpScale = source.bumpScale;
-
- this.normalMap = source.normalMap;
- this.normalMapType = source.normalMapType;
- this.normalScale.copy( source.normalScale );
-
- this.displacementMap = source.displacementMap;
- this.displacementScale = source.displacementScale;
- this.displacementBias = source.displacementBias;
-
- this.specularMap = source.specularMap;
-
- this.alphaMap = source.alphaMap;
-
- this.envMap = source.envMap;
- this.envMapRotation.copy( source.envMapRotation );
- this.combine = source.combine;
- this.reflectivity = source.reflectivity;
- this.envMapIntensity = source.envMapIntensity;
- this.refractionRatio = source.refractionRatio;
-
- this.wireframe = source.wireframe;
- this.wireframeLinewidth = source.wireframeLinewidth;
- this.wireframeLinecap = source.wireframeLinecap;
- this.wireframeLinejoin = source.wireframeLinejoin;
-
- this.flatShading = source.flatShading;
-
- this.fog = source.fog;
-
- return this;
-
- }
-
-}
-
-/**
- * A material for drawing geometry by depth. Depth is based off of the camera
- * near and far plane. White is nearest, black is farthest.
- *
- * @augments Material
- * @demo scenes/material-browser.html#MeshDepthMaterial
- */
-class MeshDepthMaterial extends Material {
-
- /**
- * Constructs a new mesh depth material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMeshDepthMaterial = true;
-
- this.type = 'MeshDepthMaterial';
-
- /**
- * Type for depth packing.
- *
- * @type {(BasicDepthPacking|RGBADepthPacking|RGBDepthPacking|RGDepthPacking)}
- * @default BasicDepthPacking
- */
- this.depthPacking = BasicDepthPacking;
-
- /**
- * The color map. May optionally include an alpha channel, typically combined
- * with {@link Material#transparent} or {@link Material#alphaTest}.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * The alpha map is a grayscale texture that controls the opacity across the
- * surface (black: fully transparent; white: fully opaque).
- *
- * Only the color of the texture is used, ignoring the alpha channel if one
- * exists. For RGB and RGBA textures, the renderer will use the green channel
- * when sampling this texture due to the extra bit of precision provided for
- * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
- * luminance/alpha textures will also still work as expected.
- *
- * `alphaMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.alphaMap = null;
-
- /**
- * The displacement map affects the position of the mesh's vertices. Unlike
- * other maps which only affect the light and shade of the material the
- * displaced vertices can cast shadows, block other objects, and otherwise
- * act as real geometry. The displacement texture is an image where the value
- * of each pixel (white being the highest) is mapped against, and
- * repositions, the vertices of the mesh.
- *
- * `displacementMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.displacementMap = null;
-
- /**
- * How much the displacement map affects the mesh (where black is no
- * displacement, and white is maximum displacement). Without a displacement
- * map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementScale = 1;
-
- /**
- * The offset of the displacement map's values on the mesh's vertices.
- * The bias is added to the scaled sample of the displacement map.
- * Without a displacement map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementBias = 0;
-
- /**
- * Renders the geometry as a wireframe.
- *
- * @type {boolean}
- * @default false
- */
- this.wireframe = false;
-
- /**
- * Controls the thickness of the wireframe.
- *
- * WebGL and WebGPU ignore this property and always render
- * 1 pixel wide lines.
- *
- * @type {number}
- * @default 1
- */
- this.wireframeLinewidth = 1;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.depthPacking = source.depthPacking;
-
- this.map = source.map;
-
- this.alphaMap = source.alphaMap;
-
- this.displacementMap = source.displacementMap;
- this.displacementScale = source.displacementScale;
- this.displacementBias = source.displacementBias;
-
- this.wireframe = source.wireframe;
- this.wireframeLinewidth = source.wireframeLinewidth;
-
- return this;
-
- }
-
-}
-
-/**
- * A material used internally for implementing shadow mapping with
- * point lights.
- *
- * Can also be used to customize the shadow casting of an object by assigning
- * an instance of `MeshDistanceMaterial` to {@link Object3D#customDistanceMaterial}.
- * The following examples demonstrates this approach in order to ensure
- * transparent parts of objects do not cast shadows.
- *
- * @augments Material
- */
-class MeshDistanceMaterial extends Material {
-
- /**
- * Constructs a new mesh distance material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMeshDistanceMaterial = true;
-
- this.type = 'MeshDistanceMaterial';
-
- /**
- * The color map. May optionally include an alpha channel, typically combined
- * with {@link Material#transparent} or {@link Material#alphaTest}.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * The alpha map is a grayscale texture that controls the opacity across the
- * surface (black: fully transparent; white: fully opaque).
- *
- * Only the color of the texture is used, ignoring the alpha channel if one
- * exists. For RGB and RGBA textures, the renderer will use the green channel
- * when sampling this texture due to the extra bit of precision provided for
- * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
- * luminance/alpha textures will also still work as expected.
- *
- * `alphaMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.alphaMap = null;
-
- /**
- * The displacement map affects the position of the mesh's vertices. Unlike
- * other maps which only affect the light and shade of the material the
- * displaced vertices can cast shadows, block other objects, and otherwise
- * act as real geometry. The displacement texture is an image where the value
- * of each pixel (white being the highest) is mapped against, and
- * repositions, the vertices of the mesh.
- *
- * `displacementMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.displacementMap = null;
-
- /**
- * How much the displacement map affects the mesh (where black is no
- * displacement, and white is maximum displacement). Without a displacement
- * map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementScale = 1;
-
- /**
- * The offset of the displacement map's values on the mesh's vertices.
- * The bias is added to the scaled sample of the displacement map.
- * Without a displacement map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementBias = 0;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.map = source.map;
-
- this.alphaMap = source.alphaMap;
-
- this.displacementMap = source.displacementMap;
- this.displacementScale = source.displacementScale;
- this.displacementBias = source.displacementBias;
-
- return this;
-
- }
-
-}
-
-/**
- * This material is defined by a MatCap (or Lit Sphere) texture, which encodes the
- * material color and shading.
- *
- * `MeshMatcapMaterial` does not respond to lights since the matcap image file encodes
- * baked lighting. It will cast a shadow onto an object that receives shadows
- * (and shadow clipping works), but it will not self-shadow or receive
- * shadows.
- *
- * @augments Material
- * @demo scenes/material-browser.html#MeshMatcapMaterial
- */
-class MeshMatcapMaterial extends Material {
-
- /**
- * Constructs a new mesh matcap material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isMeshMatcapMaterial = true;
-
- this.defines = { 'MATCAP': '' };
-
- this.type = 'MeshMatcapMaterial';
-
- /**
- * Color of the material.
- *
- * @type {Color}
- * @default (1,1,1)
- */
- this.color = new Color( 0xffffff ); // diffuse
-
- /**
- * The matcap map.
- *
- * `matcap` represents luminance data, and the texture must be assigned
- * a {@link Texture#colorSpace}. HDR `matcap` textures (e.g. `.exr`)
- * typically set `texture.colorSpace = LinearSRGBColorSpace`, while LDR
- * `matcap` textures (e.g. `.png`, `.jpg`, `.webp`) typically set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.matcap = null;
-
- /**
- * The color map. May optionally include an alpha channel, typically combined
- * with {@link Material#transparent} or {@link Material#alphaTest}. The texture map
- * color is modulated by the diffuse `color`.
- *
- * `map` represents color data, and the texture must be assigned a
- * {@link Texture#colorSpace}. Most `map` textures set
- * `texture.colorSpace = SRGBColorSpace`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * The texture to create a bump map. The black and white values map to the
- * perceived depth in relation to the lights. Bump doesn't actually affect
- * the geometry of the object, only the lighting. If a normal map is defined
- * this will be ignored.
- *
- * `bumpMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.bumpMap = null;
-
- /**
- * How much the bump map affects the material. Typical range is `[0,1]`.
- *
- * @type {number}
- * @default 1
- */
- this.bumpScale = 1;
-
- /**
- * The texture to create a normal map. The RGB values affect the surface
- * normal for each pixel fragment and change the way the color is lit. Normal
- * maps do not change the actual shape of the surface, only the lighting. In
- * case the material has a normal map authored using the left handed
- * convention, the `y` component of `normalScale` should be negated to compensate
- * for the different handedness.
- *
- * `normalMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.normalMap = null;
-
- /**
- * The type of normal map.
- *
- * @type {(TangentSpaceNormalMap|ObjectSpaceNormalMap)}
- * @default TangentSpaceNormalMap
- */
- this.normalMapType = TangentSpaceNormalMap;
-
- /**
- * How much the normal map affects the material. Typical value range is `[0,1]`.
- *
- * @type {Vector2}
- * @default (1,1)
- */
- this.normalScale = new Vector2( 1, 1 );
-
- /**
- * The displacement map affects the position of the mesh's vertices. Unlike
- * other maps which only affect the light and shade of the material the
- * displaced vertices can cast shadows, block other objects, and otherwise
- * act as real geometry. The displacement texture is an image where the value
- * of each pixel (white being the highest) is mapped against, and
- * repositions, the vertices of the mesh. For best results, pair a
- * displacement map with a matching normal map, since the renderer can
- * not recompute surface normals from the displaced vertices.
- *
- * `displacementMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.displacementMap = null;
-
- /**
- * How much the displacement map affects the mesh (where black is no
- * displacement, and white is maximum displacement). Without a displacement
- * map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementScale = 1;
-
- /**
- * The offset of the displacement map's values on the mesh's vertices.
- * The bias is added to the scaled sample of the displacement map.
- * Without a displacement map set, this value is not applied.
- *
- * @type {number}
- * @default 0
- */
- this.displacementBias = 0;
-
- /**
- * The alpha map is a grayscale texture that controls the opacity across the
- * surface (black: fully transparent; white: fully opaque).
- *
- * Only the color of the texture is used, ignoring the alpha channel if one
- * exists. For RGB and RGBA textures, the renderer will use the green channel
- * when sampling this texture due to the extra bit of precision provided for
- * green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and
- * luminance/alpha textures will also still work as expected.
- *
- * `alphaMap` represents non-color data. Any texture assigned must have
- * `texture.colorSpace = NoColorSpace` (default).
- *
- * @type {?Texture}
- * @default null
- */
- this.alphaMap = null;
-
- /**
- * Renders the geometry as a wireframe.
- *
- * @type {boolean}
- * @default false
- */
- this.wireframe = false;
-
- /**
- * Controls the thickness of the wireframe.
- *
- * Can only be used with {@link SVGRenderer}.
- *
- * @type {number}
- * @default 1
- */
- this.wireframeLinewidth = 1;
-
- /**
- * Whether the material is rendered with flat shading or not.
- *
- * @type {boolean}
- * @default false
- */
- this.flatShading = false;
-
- /**
- * Whether the material is affected by fog or not.
- *
- * @type {boolean}
- * @default true
- */
- this.fog = true;
-
- this.setValues( parameters );
-
- }
-
-
- copy( source ) {
-
- super.copy( source );
-
- this.defines = { 'MATCAP': '' };
-
- this.color.copy( source.color );
-
- this.matcap = source.matcap;
-
- this.map = source.map;
-
- this.bumpMap = source.bumpMap;
- this.bumpScale = source.bumpScale;
-
- this.normalMap = source.normalMap;
- this.normalMapType = source.normalMapType;
- this.normalScale.copy( source.normalScale );
-
- this.displacementMap = source.displacementMap;
- this.displacementScale = source.displacementScale;
- this.displacementBias = source.displacementBias;
-
- this.alphaMap = source.alphaMap;
-
- this.wireframe = source.wireframe;
- this.wireframeLinewidth = source.wireframeLinewidth;
-
- this.flatShading = source.flatShading;
-
- this.fog = source.fog;
-
- return this;
-
- }
-
-}
-
-/**
- * A material for rendering line primitives.
- *
- * Materials define the appearance of renderable 3D objects.
- *
- * ```js
- * const material = new THREE.LineDashedMaterial( {
- * color: 0xffffff,
- * scale: 1,
- * dashSize: 3,
- * gapSize: 1,
- * } );
- * ```
- *
- * @augments LineBasicMaterial
- */
-class LineDashedMaterial extends LineBasicMaterial {
-
- /**
- * Constructs a new line dashed material.
- *
- * @param {Object} [parameters] - An object with one or more properties
- * defining the material's appearance. Any property of the material
- * (including any property from inherited materials) can be passed
- * in here. Color values can be passed any type of value accepted
- * by {@link Color#set}.
- */
- constructor( parameters ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isLineDashedMaterial = true;
- this.type = 'LineDashedMaterial';
-
- /**
- * The scale of the dashed part of a line.
- *
- * @type {number}
- * @default 1
- */
- this.scale = 1;
-
- /**
- * The size of the dash. This is both the gap with the stroke.
- *
- * @type {number}
- * @default 3
- */
- this.dashSize = 3;
-
- /**
- * The size of the gap.
- *
- * @type {number}
- * @default 1
- */
- this.gapSize = 1;
-
- this.setValues( parameters );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.scale = source.scale;
- this.dashSize = source.dashSize;
- this.gapSize = source.gapSize;
-
- return this;
-
- }
-
-}
-
-/**
- * Converts an array to a specific type.
- *
- * @param {TypedArray|Array} array - The array to convert.
- * @param {TypedArray.constructor} type - The constructor of a typed array that defines the new type.
- * @return {TypedArray} The converted array.
- */
-function convertArray( array, type ) {
-
- if ( ! array || array.constructor === type ) return array;
-
- if ( typeof type.BYTES_PER_ELEMENT === 'number' ) {
-
- return new type( array ); // create typed array
-
- }
-
- return Array.prototype.slice.call( array ); // create Array
-
-}
-
-/**
- * Returns `true` if the given keyframe track settings hold Bezier tangent data.
- *
- * @param {?Object} settings - The settings of a keyframe track.
- * @return {boolean} Whether both tangent arrays are defined or not.
- */
-function hasTangents( settings ) {
-
- return settings !== undefined && settings.inTangents !== undefined && settings.outTangents !== undefined;
-
-}
-
-/**
- * Returns an array by which times and values can be sorted.
- *
- * @param {Array} times - The keyframe time values.
- * @return {Array} The array.
- */
-function getKeyframeOrder( times ) {
-
- function compareTime( i, j ) {
-
- return times[ i ] - times[ j ];
-
- }
-
- const n = times.length;
- const result = new Array( n );
- for ( let i = 0; i !== n; ++ i ) result[ i ] = i;
-
- result.sort( compareTime );
-
- return result;
-
-}
-
-/**
- * Sorts the given array by the previously computed order via `getKeyframeOrder()`.
- *
- * @param {Array} values - The values to sort.
- * @param {number} stride - The stride.
- * @param {Array} order - The sort order.
- * @return {Array} The sorted values.
- */
-function sortedArray( values, stride, order ) {
-
- const nValues = values.length;
- const result = new values.constructor( nValues );
-
- for ( let i = 0, dstOffset = 0; dstOffset !== nValues; ++ i ) {
-
- const srcOffset = order[ i ] * stride;
-
- for ( let j = 0; j !== stride; ++ j ) {
-
- result[ dstOffset ++ ] = values[ srcOffset + j ];
-
- }
-
- }
-
- return result;
-
-}
-
-/**
- * Used for parsing AOS keyframe formats.
- *
- * @param {Array} jsonKeys - A list of JSON keyframes.
- * @param {Array} times - This array will be filled with keyframe times by this function.
- * @param {Array} values - This array will be filled with keyframe values by this function.
- * @param {string} valuePropertyName - The name of the property to use.
- */
-function flattenJSON( jsonKeys, times, values, valuePropertyName ) {
-
- let i = 1, key = jsonKeys[ 0 ];
-
- while ( key !== undefined && key[ valuePropertyName ] === undefined ) {
-
- key = jsonKeys[ i ++ ];
-
- }
-
- if ( key === undefined ) return; // no data
-
- let value = key[ valuePropertyName ];
- if ( value === undefined ) return; // no data
-
- if ( Array.isArray( value ) ) {
-
- do {
-
- value = key[ valuePropertyName ];
-
- if ( value !== undefined ) {
-
- times.push( key.time );
- values.push( ...value ); // push all elements
-
- }
-
- key = jsonKeys[ i ++ ];
-
- } while ( key !== undefined );
-
- } else if ( value.toArray !== undefined ) {
-
- // ...assume THREE.Math-ish
-
- do {
-
- value = key[ valuePropertyName ];
-
- if ( value !== undefined ) {
-
- times.push( key.time );
- value.toArray( values, values.length );
-
- }
-
- key = jsonKeys[ i ++ ];
-
- } while ( key !== undefined );
-
- } else {
-
- // otherwise push as-is
-
- do {
-
- value = key[ valuePropertyName ];
-
- if ( value !== undefined ) {
-
- times.push( key.time );
- values.push( value );
-
- }
-
- key = jsonKeys[ i ++ ];
-
- } while ( key !== undefined );
-
- }
-
-}
-
-/**
- * Creates a new clip, containing only the segment of the original clip between the given frames.
- *
- * @param {AnimationClip} sourceClip - The values to sort.
- * @param {string} name - The name of the clip.
- * @param {number} startFrame - The start frame.
- * @param {number} endFrame - The end frame.
- * @param {number} [fps=30] - The FPS.
- * @return {AnimationClip} The new sub clip.
- */
-function subclip( sourceClip, name, startFrame, endFrame, fps = 30 ) {
-
- const clip = sourceClip.clone();
-
- clip.name = name;
-
- const tracks = [];
-
- for ( let i = 0; i < clip.tracks.length; ++ i ) {
-
- const track = clip.tracks[ i ];
- const valueSize = track.getValueSize();
-
- const times = [];
- const values = [];
-
- for ( let j = 0; j < track.times.length; ++ j ) {
-
- const frame = track.times[ j ] * fps;
-
- if ( frame < startFrame || frame >= endFrame ) continue;
-
- times.push( track.times[ j ] );
-
- for ( let k = 0; k < valueSize; ++ k ) {
-
- values.push( track.values[ j * valueSize + k ] );
-
- }
-
- }
-
- if ( times.length === 0 ) continue;
-
- track.times = convertArray( times, track.times.constructor );
- track.values = convertArray( values, track.values.constructor );
-
- tracks.push( track );
-
- }
-
- clip.tracks = tracks;
-
- // find minimum .times value across all tracks in the trimmed clip
-
- let minStartTime = Infinity;
-
- for ( let i = 0; i < clip.tracks.length; ++ i ) {
-
- if ( minStartTime > clip.tracks[ i ].times[ 0 ] ) {
-
- minStartTime = clip.tracks[ i ].times[ 0 ];
-
- }
-
- }
-
- // shift all tracks such that clip begins at t=0
-
- for ( let i = 0; i < clip.tracks.length; ++ i ) {
-
- clip.tracks[ i ].shift( -1 * minStartTime );
-
- }
-
- clip.resetDuration();
-
- return clip;
-
-}
-
-/**
- * Converts the keyframes of the given animation clip to an additive format.
- *
- * @param {AnimationClip} targetClip - The clip to make additive.
- * @param {number} [referenceFrame=0] - The reference frame.
- * @param {AnimationClip} [referenceClip=targetClip] - The reference clip.
- * @param {number} [fps=30] - The FPS.
- * @return {AnimationClip} The updated clip which is now additive.
- */
-function makeClipAdditive( targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30 ) {
-
- if ( fps <= 0 ) fps = 30;
-
- const numTracks = referenceClip.tracks.length;
- const referenceTime = referenceFrame / fps;
-
- // Make each track's values relative to the values at the reference frame
- for ( let i = 0; i < numTracks; ++ i ) {
-
- const referenceTrack = referenceClip.tracks[ i ];
- const referenceTrackType = referenceTrack.ValueTypeName;
-
- // Skip this track if it's non-numeric
- if ( referenceTrackType === 'bool' || referenceTrackType === 'string' ) continue;
-
- // Find the track in the target clip whose name and type matches the reference track
- const targetTrack = targetClip.tracks.find( function ( track ) {
-
- return track.name === referenceTrack.name
- && track.ValueTypeName === referenceTrackType;
-
- } );
-
- if ( targetTrack === undefined ) continue;
-
- let referenceOffset = 0;
- const referenceValueSize = referenceTrack.getValueSize();
-
- if ( referenceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {
-
- referenceOffset = referenceValueSize / 3;
-
- }
-
- let targetOffset = 0;
- const targetValueSize = targetTrack.getValueSize();
-
- if ( targetTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {
-
- targetOffset = targetValueSize / 3;
-
- }
-
- const lastIndex = referenceTrack.times.length - 1;
- let referenceValue;
-
- // Find the value to subtract out of the track
- if ( referenceTime <= referenceTrack.times[ 0 ] ) {
-
- // Reference frame is earlier than the first keyframe, so just use the first keyframe
- const startIndex = referenceOffset;
- const endIndex = referenceValueSize - referenceOffset;
- referenceValue = referenceTrack.values.slice( startIndex, endIndex );
-
- } else if ( referenceTime >= referenceTrack.times[ lastIndex ] ) {
-
- // Reference frame is after the last keyframe, so just use the last keyframe
- const startIndex = lastIndex * referenceValueSize + referenceOffset;
- const endIndex = startIndex + referenceValueSize - referenceOffset;
- referenceValue = referenceTrack.values.slice( startIndex, endIndex );
-
- } else {
-
- // Interpolate to the reference value
- const interpolant = referenceTrack.createInterpolant();
- const startIndex = referenceOffset;
- const endIndex = referenceValueSize - referenceOffset;
- interpolant.evaluate( referenceTime );
- referenceValue = interpolant.resultBuffer.slice( startIndex, endIndex );
-
- }
-
- // Conjugate the quaternion
- if ( referenceTrackType === 'quaternion' ) {
-
- const referenceQuat = new Quaternion().fromArray( referenceValue ).normalize().conjugate();
- referenceQuat.toArray( referenceValue );
-
- }
-
- // Subtract the reference value from all of the track values
-
- const numTimes = targetTrack.times.length;
- for ( let j = 0; j < numTimes; ++ j ) {
-
- const valueStart = j * targetValueSize + targetOffset;
-
- if ( referenceTrackType === 'quaternion' ) {
-
- // Multiply the conjugate for quaternion track types
- Quaternion.multiplyQuaternionsFlat(
- targetTrack.values,
- valueStart,
- referenceValue,
- 0,
- targetTrack.values,
- valueStart
- );
-
- } else {
-
- const valueEnd = targetValueSize - targetOffset * 2;
-
- // Subtract each value for all other numeric track types
- for ( let k = 0; k < valueEnd; ++ k ) {
-
- targetTrack.values[ valueStart + k ] -= referenceValue[ k ];
-
- }
-
- }
-
- }
-
- }
-
- targetClip.blendMode = AdditiveAnimationBlendMode;
-
- return targetClip;
-
-}
-
-/**
- * A class with various methods to assist with animations.
- *
- * @hideconstructor
- */
-class AnimationUtils {
-
- /**
- * Converts an array to a specific type
- *
- * @static
- * @param {TypedArray|Array} array - The array to convert.
- * @param {TypedArray.constructor} type - The constructor of a type array.
- * @return {TypedArray} The converted array
- */
- static convertArray( array, type ) {
-
- return convertArray( array, type );
-
- }
-
- /**
- * Returns `true` if the given object is a typed array.
- *
- * @static
- * @param {any} object - The object to check.
- * @return {boolean} Whether the given object is a typed array.
- */
- static isTypedArray( object ) {
-
- return isTypedArray( object );
-
- }
-
- /**
- * Returns `true` if the given keyframe track settings hold Bezier tangent data.
- *
- * @static
- * @param {?Object} settings - The settings of a keyframe track.
- * @return {boolean} Whether both tangent arrays are defined or not.
- */
- static hasTangents( settings ) {
-
- return hasTangents( settings );
-
- }
-
- /**
- * Returns an array by which times and values can be sorted.
- *
- * @static
- * @param {Array} times - The keyframe time values.
- * @return {Array} The array.
- */
- static getKeyframeOrder( times ) {
-
- return getKeyframeOrder( times );
-
- }
-
- /**
- * Sorts the given array by the previously computed order via `getKeyframeOrder()`.
- *
- * @static
- * @param {Array} values - The values to sort.
- * @param {number} stride - The stride.
- * @param {Array} order - The sort order.
- * @return {Array} The sorted values.
- */
- static sortedArray( values, stride, order ) {
-
- return sortedArray( values, stride, order );
-
- }
-
- /**
- * Used for parsing AOS keyframe formats.
- *
- * @static
- * @param {Array} jsonKeys - A list of JSON keyframes.
- * @param {Array} times - This array will be filled with keyframe times by this method.
- * @param {Array} values - This array will be filled with keyframe values by this method.
- * @param {string} valuePropertyName - The name of the property to use.
- */
- static flattenJSON( jsonKeys, times, values, valuePropertyName ) {
-
- flattenJSON( jsonKeys, times, values, valuePropertyName );
-
- }
-
- /**
- * Creates a new clip, containing only the segment of the original clip between the given frames.
- *
- * @static
- * @param {AnimationClip} sourceClip - The values to sort.
- * @param {string} name - The name of the clip.
- * @param {number} startFrame - The start frame.
- * @param {number} endFrame - The end frame.
- * @param {number} [fps=30] - The FPS.
- * @return {AnimationClip} The new sub clip.
- */
- static subclip( sourceClip, name, startFrame, endFrame, fps = 30 ) {
-
- return subclip( sourceClip, name, startFrame, endFrame, fps );
-
- }
-
- /**
- * Converts the keyframes of the given animation clip to an additive format.
- *
- * @static
- * @param {AnimationClip} targetClip - The clip to make additive.
- * @param {number} [referenceFrame=0] - The reference frame.
- * @param {AnimationClip} [referenceClip=targetClip] - The reference clip.
- * @param {number} [fps=30] - The FPS.
- * @return {AnimationClip} The updated clip which is now additive.
- */
- static makeClipAdditive( targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30 ) {
-
- return makeClipAdditive( targetClip, referenceFrame, referenceClip, fps );
-
- }
-
-}
-
-/**
- * Abstract base class of interpolants over parametric samples.
- *
- * The parameter domain is one dimensional, typically the time or a path
- * along a curve defined by the data.
- *
- * The sample values can have any dimensionality and derived classes may
- * apply special interpretations to the data.
- *
- * This class provides the interval seek in a Template Method, deferring
- * the actual interpolation to derived classes.
- *
- * Time complexity is O(1) for linear access crossing at most two points
- * and O(log N) for random access, where N is the number of positions.
- *
- * References: {@link http://www.oodesign.com/template-method-pattern.html}
- *
- * @abstract
- */
-class Interpolant {
-
- /**
- * Constructs a new interpolant.
- *
- * @param {TypedArray} parameterPositions - The parameter positions hold the interpolation factors.
- * @param {TypedArray} sampleValues - The sample values.
- * @param {number} sampleSize - The sample size
- * @param {TypedArray} [resultBuffer] - The result buffer.
- */
- constructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {
-
- /**
- * The parameter positions.
- *
- * @type {TypedArray}
- */
- this.parameterPositions = parameterPositions;
-
- /**
- * A cache index.
- *
- * @private
- * @type {number}
- * @default 0
- */
- this._cachedIndex = 0;
-
- /**
- * The result buffer.
- *
- * @type {TypedArray}
- */
- this.resultBuffer = resultBuffer !== undefined ? resultBuffer : new sampleValues.constructor( sampleSize );
-
- /**
- * The sample values.
- *
- * @type {TypedArray}
- */
- this.sampleValues = sampleValues;
-
- /**
- * The value size.
- *
- * @type {TypedArray}
- */
- this.valueSize = sampleSize;
-
- /**
- * The interpolation settings.
- *
- * @type {?Object}
- * @default null
- */
- this.settings = null;
-
- /**
- * The default settings object.
- *
- * @type {Object}
- */
- this.DefaultSettings_ = {};
-
- }
-
- /**
- * Evaluate the interpolant at position `t`.
- *
- * @param {number} t - The interpolation factor.
- * @return {TypedArray} The result buffer.
- */
- evaluate( t ) {
-
- const pp = this.parameterPositions;
- let i1 = this._cachedIndex,
- t1 = pp[ i1 ],
- t0 = pp[ i1 - 1 ];
-
- validate_interval: {
-
- seek: {
-
- let right;
-
- linear_scan: {
-
- //- See http://jsperf.com/comparison-to-undefined/3
- //- slower code:
- //-
- //- if ( t >= t1 || t1 === undefined ) {
- forward_scan: if ( ! ( t < t1 ) ) {
-
- for ( let giveUpAt = i1 + 2; ; ) {
-
- if ( t1 === undefined ) {
-
- if ( t < t0 ) break forward_scan;
-
- // after end
-
- i1 = pp.length;
- this._cachedIndex = i1;
- return this.copySampleValue_( i1 - 1 );
-
- }
-
- if ( i1 === giveUpAt ) break; // this loop
-
- t0 = t1;
- t1 = pp[ ++ i1 ];
-
- if ( t < t1 ) {
-
- // we have arrived at the sought interval
- break seek;
-
- }
-
- }
-
- // prepare binary search on the right side of the index
- right = pp.length;
- break linear_scan;
-
- }
-
- //- slower code:
- //- if ( t < t0 || t0 === undefined ) {
- if ( ! ( t >= t0 ) ) {
-
- // looping?
-
- const t1global = pp[ 1 ];
-
- if ( t < t1global ) {
-
- i1 = 2; // + 1, using the scan for the details
- t0 = t1global;
-
- }
-
- // linear reverse scan
-
- for ( let giveUpAt = i1 - 2; ; ) {
-
- if ( t0 === undefined ) {
-
- // before start
-
- this._cachedIndex = 0;
- return this.copySampleValue_( 0 );
-
- }
-
- if ( i1 === giveUpAt ) break; // this loop
-
- t1 = t0;
- t0 = pp[ -- i1 - 1 ];
-
- if ( t >= t0 ) {
-
- // we have arrived at the sought interval
- break seek;
-
- }
-
- }
-
- // prepare binary search on the left side of the index
- right = i1;
- i1 = 0;
- break linear_scan;
-
- }
-
- // the interval is valid
-
- break validate_interval;
-
- } // linear scan
-
- // binary search
-
- while ( i1 < right ) {
-
- const mid = ( i1 + right ) >>> 1;
-
- if ( t < pp[ mid ] ) {
-
- right = mid;
-
- } else {
-
- i1 = mid + 1;
-
- }
-
- }
-
- t1 = pp[ i1 ];
- t0 = pp[ i1 - 1 ];
-
- // check boundary cases, again
-
- if ( t0 === undefined ) {
-
- this._cachedIndex = 0;
- return this.copySampleValue_( 0 );
-
- }
-
- if ( t1 === undefined ) {
-
- i1 = pp.length;
- this._cachedIndex = i1;
- return this.copySampleValue_( i1 - 1 );
-
- }
-
- } // seek
-
- this._cachedIndex = i1;
-
- this.intervalChanged_( i1, t0, t1 );
-
- } // validate_interval
-
- return this.interpolate_( i1, t0, t, t1 );
-
- }
-
- /**
- * Returns the interpolation settings.
- *
- * @return {Object} The interpolation settings.
- */
- getSettings_() {
-
- return this.settings || this.DefaultSettings_;
-
- }
-
- /**
- * Copies a sample value to the result buffer.
- *
- * @param {number} index - An index into the sample value buffer.
- * @return {TypedArray} The result buffer.
- */
- copySampleValue_( index ) {
-
- // copies a sample value to the result buffer
-
- const result = this.resultBuffer,
- values = this.sampleValues,
- stride = this.valueSize,
- offset = index * stride;
-
- for ( let i = 0; i !== stride; ++ i ) {
-
- result[ i ] = values[ offset + i ];
-
- }
-
- return result;
-
- }
-
- /**
- * Copies a sample value to the result buffer.
- *
- * @abstract
- * @param {number} i1 - An index into the sample value buffer.
- * @param {number} t0 - The previous interpolation factor.
- * @param {number} t - The current interpolation factor.
- * @param {number} t1 - The next interpolation factor.
- * @return {TypedArray} The result buffer.
- */
- interpolate_( /* i1, t0, t, t1 */ ) {
-
- throw new Error( 'THREE.Interpolant: Call to abstract method.' );
- // implementations shall return this.resultBuffer
-
- }
-
- /**
- * Optional method that is executed when the interval has changed.
- *
- * @param {number} i1 - An index into the sample value buffer.
- * @param {number} t0 - The previous interpolation factor.
- * @param {number} t - The current interpolation factor.
- */
- intervalChanged_( /* i1, t0, t1 */ ) {
-
- // empty
-
- }
-
-}
-
-/**
- * Fast and simple cubic spline interpolant.
- *
- * It was derived from a Hermitian construction setting the first derivative
- * at each sample position to the linear slope between neighboring positions
- * over their parameter interval.
- *
- * @augments Interpolant
- */
-class CubicInterpolant extends Interpolant {
-
- /**
- * Constructs a new cubic interpolant.
- *
- * @param {TypedArray} parameterPositions - The parameter positions hold the interpolation factors.
- * @param {TypedArray} sampleValues - The sample values.
- * @param {number} sampleSize - The sample size
- * @param {TypedArray} [resultBuffer] - The result buffer.
- */
- constructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {
-
- super( parameterPositions, sampleValues, sampleSize, resultBuffer );
-
- this._weightPrev = -0;
- this._offsetPrev = -0;
- this._weightNext = -0;
- this._offsetNext = -0;
-
- this.DefaultSettings_ = {
-
- endingStart: ZeroCurvatureEnding,
- endingEnd: ZeroCurvatureEnding
-
- };
-
- }
-
- intervalChanged_( i1, t0, t1 ) {
-
- const pp = this.parameterPositions;
- let iPrev = i1 - 2,
- iNext = i1 + 1,
-
- tPrev = pp[ iPrev ],
- tNext = pp[ iNext ];
-
- if ( tPrev === undefined ) {
-
- switch ( this.getSettings_().endingStart ) {
-
- case ZeroSlopeEnding:
-
- // f'(t0) = 0
- iPrev = i1;
- tPrev = 2 * t0 - t1;
-
- break;
-
- case WrapAroundEnding:
-
- // use the other end of the curve
- iPrev = pp.length - 2;
- tPrev = t0 + pp[ iPrev ] - pp[ iPrev + 1 ];
-
- break;
-
- default: // ZeroCurvatureEnding
-
- // f''(t0) = 0 a.k.a. Natural Spline
- iPrev = i1;
- tPrev = t1;
-
- }
-
- }
-
- if ( tNext === undefined ) {
-
- switch ( this.getSettings_().endingEnd ) {
-
- case ZeroSlopeEnding:
-
- // f'(tN) = 0
- iNext = i1;
- tNext = 2 * t1 - t0;
-
- break;
-
- case WrapAroundEnding:
-
- // use the other end of the curve
- iNext = 1;
- tNext = t1 + pp[ 1 ] - pp[ 0 ];
-
- break;
-
- default: // ZeroCurvatureEnding
-
- // f''(tN) = 0, a.k.a. Natural Spline
- iNext = i1 - 1;
- tNext = t0;
-
- }
-
- }
-
- const halfDt = ( t1 - t0 ) * 0.5,
- stride = this.valueSize;
-
- this._weightPrev = halfDt / ( t0 - tPrev );
- this._weightNext = halfDt / ( tNext - t1 );
- this._offsetPrev = iPrev * stride;
- this._offsetNext = iNext * stride;
-
- }
-
- interpolate_( i1, t0, t, t1 ) {
-
- const result = this.resultBuffer,
- values = this.sampleValues,
- stride = this.valueSize,
-
- o1 = i1 * stride, o0 = o1 - stride,
- oP = this._offsetPrev, oN = this._offsetNext,
- wP = this._weightPrev, wN = this._weightNext,
-
- p = ( t - t0 ) / ( t1 - t0 ),
- pp = p * p,
- ppp = pp * p;
-
- // evaluate polynomials
-
- const sP = - wP * ppp + 2 * wP * pp - wP * p;
- const s0 = ( 1 + wP ) * ppp + ( -1.5 - 2 * wP ) * pp + ( -0.5 + wP ) * p + 1;
- const s1 = ( -1 - wN ) * ppp + ( 1.5 + wN ) * pp + 0.5 * p;
- const sN = wN * ppp - wN * pp;
-
- // combine data linearly
-
- for ( let i = 0; i !== stride; ++ i ) {
-
- result[ i ] =
- sP * values[ oP + i ] +
- s0 * values[ o0 + i ] +
- s1 * values[ o1 + i ] +
- sN * values[ oN + i ];
-
- }
-
- return result;
-
- }
-
-}
-
-/**
- * A basic linear interpolant.
- *
- * @augments Interpolant
- */
-class LinearInterpolant extends Interpolant {
-
- /**
- * Constructs a new linear interpolant.
- *
- * @param {TypedArray} parameterPositions - The parameter positions hold the interpolation factors.
- * @param {TypedArray} sampleValues - The sample values.
- * @param {number} sampleSize - The sample size
- * @param {TypedArray} [resultBuffer] - The result buffer.
- */
- constructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {
-
- super( parameterPositions, sampleValues, sampleSize, resultBuffer );
-
- }
-
- interpolate_( i1, t0, t, t1 ) {
-
- const result = this.resultBuffer,
- values = this.sampleValues,
- stride = this.valueSize,
-
- offset1 = i1 * stride,
- offset0 = offset1 - stride,
-
- weight1 = ( t - t0 ) / ( t1 - t0 ),
- weight0 = 1 - weight1;
-
- for ( let i = 0; i !== stride; ++ i ) {
-
- result[ i ] =
- values[ offset0 + i ] * weight0 +
- values[ offset1 + i ] * weight1;
-
- }
-
- return result;
-
- }
-
-}
-
-/**
- * Interpolant that evaluates to the sample value at the position preceding
- * the parameter.
- *
- * @augments Interpolant
- */
-class DiscreteInterpolant extends Interpolant {
-
- /**
- * Constructs a new discrete interpolant.
- *
- * @param {TypedArray} parameterPositions - The parameter positions hold the interpolation factors.
- * @param {TypedArray} sampleValues - The sample values.
- * @param {number} sampleSize - The sample size
- * @param {TypedArray} [resultBuffer] - The result buffer.
- */
- constructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {
-
- super( parameterPositions, sampleValues, sampleSize, resultBuffer );
-
- }
-
- interpolate_( i1 /*, t0, t, t1 */ ) {
-
- return this.copySampleValue_( i1 - 1 );
-
- }
-
-}
-
-/**
- * A Bezier interpolant using cubic Bezier curves with 2D control points.
- *
- * This interpolant supports the COLLADA/Maya style of Bezier animation where
- * each keyframe has explicit in/out tangent control points specified as
- * 2D coordinates (time, value).
- *
- * Tangent data is read from `inTangents` and `outTangents` on the interpolant
- * (populated by `KeyframeTrack.InterpolantFactoryMethodBezier`).
- *
- * For a track with N keyframes and stride S:
- * - Each tangent array has N * S * 2 values
- * - Layout: [k0_c0_time, k0_c0_value, k0_c1_time, k0_c1_value, ..., k0_cS_time, k0_cS_value,
- * k1_c0_time, k1_c0_value, ...]
- *
- * @augments Interpolant
- */
-class BezierInterpolant extends Interpolant {
-
- interpolate_( i1, t0, t, t1 ) {
-
- const result = this.resultBuffer;
- const values = this.sampleValues;
- const stride = this.valueSize;
-
- const offset1 = i1 * stride;
- const offset0 = offset1 - stride;
-
- const inTangents = this.inTangents;
- const outTangents = this.outTangents;
-
- // If no tangent data, fall back to linear interpolation
- if ( ! inTangents || ! outTangents ) {
-
- const weight1 = ( t - t0 ) / ( t1 - t0 );
- const weight0 = 1 - weight1;
-
- for ( let i = 0; i !== stride; ++ i ) {
-
- result[ i ] = values[ offset0 + i ] * weight0 + values[ offset1 + i ] * weight1;
-
- }
-
- return result;
-
- }
-
- const tangentStride = stride * 2;
- const i0 = i1 - 1;
-
- for ( let i = 0; i !== stride; ++ i ) {
-
- const v0 = values[ offset0 + i ];
- const v1 = values[ offset1 + i ];
-
- // outTangent of previous keyframe (C0)
- const outTangentOffset = i0 * tangentStride + i * 2;
- const c0x = outTangents[ outTangentOffset ];
- const c0y = outTangents[ outTangentOffset + 1 ];
-
- // inTangent of current keyframe (C1)
- const inTangentOffset = i1 * tangentStride + i * 2;
- const c1x = inTangents[ inTangentOffset ];
- const c1y = inTangents[ inTangentOffset + 1 ];
-
- // Find the curve parameter s where the Bezier X(s) matches t, then evaluate Y(s)
- const s = solveBezierParameter( t, t0, c0x, c1x, t1 );
-
- result[ i ] = cubicBezier( s, v0, c0y, c1y, v1 );
-
- }
-
- return result;
-
- }
-
-}
-
-function cubicBezier( s, p0, p1, p2, p3 ) {
-
- const k = 1 - s;
-
- return k * k * k * p0 + 3 * k * k * s * p1 + 3 * k * s * s * p2 + s * s * s * p3;
-
-}
-
-function cubicBezierSlope( s, p0, p1, p2, p3 ) {
-
- const k = 1 - s;
-
- return 3 * k * k * ( p1 - p0 ) + 6 * k * s * ( p2 - p1 ) + 3 * s * s * ( p3 - p2 );
-
-}
-
-// Solves cubicBezier( s, x0, x1, x2, x3 ) = x for s in [0,1] using Newton-Raphson
-
-function solveBezierParameter( x, x0, x1, x2, x3 ) {
-
- let s = ( x - x0 ) / ( x3 - x0 );
-
- for ( let i = 0; i < 8; i ++ ) {
-
- const error = cubicBezier( s, x0, x1, x2, x3 ) - x;
- if ( Math.abs( error ) < 1e-10 ) break;
-
- const slope = cubicBezierSlope( s, x0, x1, x2, x3 );
- if ( Math.abs( slope ) < 1e-10 ) break;
-
- s = Math.max( 0, Math.min( 1, s - error / slope ) );
-
- }
-
- return s;
-
-}
-
-/**
- * Represents a timed sequence of keyframes, which are composed of lists of
- * times and related values, and which are used to animate a specific property
- * of an object.
- */
-class KeyframeTrack {
-
- /**
- * Constructs a new keyframe track.
- *
- * @param {string} name - The keyframe track's name.
- * @param {Array} times - A list of keyframe times.
- * @param {Array} values - A list of keyframe values.
- * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth|InterpolateBezier)} [interpolation] - The interpolation type.
- */
- constructor( name, times, values, interpolation ) {
-
- if ( name === undefined ) throw new Error( 'THREE.KeyframeTrack: track name is undefined' );
- if ( times === undefined || times.length === 0 ) throw new Error( 'THREE.KeyframeTrack: no keyframes in track named ' + name );
-
- /**
- * The track's name can refer to morph targets or bones or
- * possibly other values within an animated object. See {@link PropertyBinding#parseTrackName}
- * for the forms of strings that can be parsed for property binding.
- *
- * @type {string}
- */
- this.name = name;
-
- /**
- * The keyframe times.
- *
- * @type {Float32Array}
- */
- this.times = convertArray( times, this.TimeBufferType );
-
- /**
- * The keyframe values.
- *
- * @type {Float32Array}
- */
- this.values = convertArray( values, this.ValueBufferType );
-
- this.setInterpolation( interpolation || this.DefaultInterpolation );
-
- }
-
- /**
- * Converts the keyframe track to JSON.
- *
- * @static
- * @param {KeyframeTrack} track - The keyframe track to serialize.
- * @return {Object} The serialized keyframe track as JSON.
- */
- static toJSON( track ) {
-
- const trackType = track.constructor;
-
- let json;
-
- // derived classes can define a static toJSON method
- if ( trackType.toJSON !== this.toJSON ) {
-
- json = trackType.toJSON( track );
-
- } else {
-
- // by default, we assume the data can be serialized as-is
- json = {
-
- 'name': track.name,
- 'times': convertArray( track.times, Array ),
- 'values': convertArray( track.values, Array )
-
- };
-
- const interpolation = track.getInterpolation();
-
- if ( interpolation !== track.DefaultInterpolation ) {
-
- json.interpolation = interpolation;
-
- }
-
- if ( hasTangents( track.settings ) ) {
-
- json.settings = {
- inTangents: convertArray( track.settings.inTangents, Array ),
- outTangents: convertArray( track.settings.outTangents, Array )
- };
-
- }
-
- }
-
- json.type = track.ValueTypeName; // mandatory
-
- return json;
-
- }
-
- /**
- * Factory method for creating a new discrete interpolant.
- *
- * @static
- * @param {TypedArray} [result] - The result buffer.
- * @return {DiscreteInterpolant} The new interpolant.
- */
- InterpolantFactoryMethodDiscrete( result ) {
-
- return new DiscreteInterpolant( this.times, this.values, this.getValueSize(), result );
-
- }
-
- /**
- * Factory method for creating a new linear interpolant.
- *
- * @static
- * @param {TypedArray} [result] - The result buffer.
- * @return {LinearInterpolant} The new interpolant.
- */
- InterpolantFactoryMethodLinear( result ) {
-
- return new LinearInterpolant( this.times, this.values, this.getValueSize(), result );
-
- }
-
- /**
- * Factory method for creating a new smooth interpolant.
- *
- * @static
- * @param {TypedArray} [result] - The result buffer.
- * @return {CubicInterpolant} The new interpolant.
- */
- InterpolantFactoryMethodSmooth( result ) {
-
- return new CubicInterpolant( this.times, this.values, this.getValueSize(), result );
-
- }
-
- /**
- * Factory method for creating a new Bezier interpolant.
- *
- * The Bezier interpolant requires tangent data to be set via the `settings` property
- * on the track before creating the interpolant. The settings should contain:
- * - `inTangents`: Float32Array with [time, value] pairs per keyframe per component
- * - `outTangents`: Float32Array with [time, value] pairs per keyframe per component
- *
- * @static
- * @param {TypedArray} [result] - The result buffer.
- * @return {BezierInterpolant} The new interpolant.
- */
- InterpolantFactoryMethodBezier( result ) {
-
- const interpolant = new BezierInterpolant( this.times, this.values, this.getValueSize(), result );
-
- if ( this.settings ) {
-
- interpolant.inTangents = this.settings.inTangents;
- interpolant.outTangents = this.settings.outTangents;
-
- }
-
- return interpolant;
-
- }
-
- /**
- * Defines the interpolation factor method for this keyframe track.
- *
- * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth|InterpolateBezier)} interpolation - The interpolation type.
- * @return {KeyframeTrack} A reference to this keyframe track.
- */
- setInterpolation( interpolation ) {
-
- let factoryMethod;
-
- switch ( interpolation ) {
-
- case InterpolateDiscrete:
-
- factoryMethod = this.InterpolantFactoryMethodDiscrete;
-
- break;
-
- case InterpolateLinear:
-
- factoryMethod = this.InterpolantFactoryMethodLinear;
-
- break;
-
- case InterpolateSmooth:
-
- factoryMethod = this.InterpolantFactoryMethodSmooth;
-
- break;
-
- case InterpolateBezier:
-
- factoryMethod = this.InterpolantFactoryMethodBezier;
-
- break;
-
- }
-
- if ( factoryMethod === undefined ) {
-
- const message = 'unsupported interpolation for ' +
- this.ValueTypeName + ' keyframe track named ' + this.name;
-
- if ( this.createInterpolant === undefined ) {
-
- // fall back to default, unless the default itself is messed up
- if ( interpolation !== this.DefaultInterpolation ) {
-
- this.setInterpolation( this.DefaultInterpolation );
-
- } else {
-
- throw new Error( message ); // fatal, in this case
-
- }
-
- }
-
- warn( 'KeyframeTrack:', message );
- return this;
-
- }
-
- this.createInterpolant = factoryMethod;
-
- return this;
-
- }
-
- /**
- * Returns the current interpolation type.
- *
- * @return {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth|InterpolateBezier)} The interpolation type.
- */
- getInterpolation() {
-
- switch ( this.createInterpolant ) {
-
- case this.InterpolantFactoryMethodDiscrete:
-
- return InterpolateDiscrete;
-
- case this.InterpolantFactoryMethodLinear:
-
- return InterpolateLinear;
-
- case this.InterpolantFactoryMethodSmooth:
-
- return InterpolateSmooth;
-
- case this.InterpolantFactoryMethodBezier:
-
- return InterpolateBezier;
-
- }
-
- }
-
- /**
- * Returns the value size.
- *
- * @return {number} The value size.
- */
- getValueSize() {
-
- return this.values.length / this.times.length;
-
- }
-
- /**
- * Moves all keyframes either forward or backward in time.
- *
- * @param {number} timeOffset - The offset to move the time values.
- * @return {KeyframeTrack} A reference to this keyframe track.
- */
- shift( timeOffset ) {
-
- if ( timeOffset !== 0.0 ) {
-
- const times = this.times;
-
- for ( let i = 0, n = times.length; i !== n; ++ i ) {
-
- times[ i ] += timeOffset;
-
- }
-
- }
-
- return this;
-
- }
-
- /**
- * Scale all keyframe times by a factor (useful for frame - seconds conversions).
- *
- * @param {number} timeScale - The time scale.
- * @return {KeyframeTrack} A reference to this keyframe track.
- */
- scale( timeScale ) {
-
- if ( timeScale !== 1.0 ) {
-
- const times = this.times;
-
- for ( let i = 0, n = times.length; i !== n; ++ i ) {
-
- times[ i ] *= timeScale;
-
- }
-
- if ( hasTangents( this.settings ) ) {
-
- scaleTangentTimes( this.settings.inTangents, timeScale );
- scaleTangentTimes( this.settings.outTangents, timeScale );
-
- }
-
- }
-
- return this;
-
- }
-
- /**
- * Removes keyframes before and after animation without changing any values within the defined time range.
- *
- * Note: The method does not shift around keys to the start of the track time, because for interpolated
- * keys this will change their values
- *
- * @param {number} startTime - The start time.
- * @param {number} endTime - The end time.
- * @return {KeyframeTrack} A reference to this keyframe track.
- */
- trim( startTime, endTime ) {
-
- const times = this.times,
- nKeys = times.length;
-
- let from = 0,
- to = nKeys - 1;
-
- while ( from !== nKeys && times[ from ] < startTime ) {
-
- ++ from;
-
- }
-
- while ( to !== -1 && times[ to ] > endTime ) {
-
- -- to;
-
- }
-
- ++ to; // inclusive -> exclusive bound
-
- if ( from !== 0 || to !== nKeys ) {
-
- // empty tracks are forbidden, so keep at least one keyframe
- if ( from >= to ) {
-
- to = Math.max( to, 1 );
- from = to - 1;
-
- }
-
- const stride = this.getValueSize();
- this.times = times.slice( from, to );
- this.values = this.values.slice( from * stride, to * stride );
-
- }
-
- return this;
-
- }
-
- /**
- * Performs minimal validation on the keyframe track. Returns `true` if the values
- * are valid.
- *
- * @return {boolean} Whether the keyframes are valid or not.
- */
- validate() {
-
- let valid = true;
-
- const valueSize = this.getValueSize();
- if ( valueSize - Math.floor( valueSize ) !== 0 ) {
-
- error( 'KeyframeTrack: Invalid value size in track.', this );
- valid = false;
-
- }
-
- const times = this.times,
- values = this.values,
-
- nKeys = times.length;
-
- if ( nKeys === 0 ) {
-
- error( 'KeyframeTrack: Track is empty.', this );
- valid = false;
-
- }
-
- let prevTime = null;
-
- for ( let i = 0; i !== nKeys; i ++ ) {
-
- const currTime = times[ i ];
-
- if ( typeof currTime === 'number' && isNaN( currTime ) ) {
-
- error( 'KeyframeTrack: Time is not a valid number.', this, i, currTime );
- valid = false;
- break;
-
- }
-
- if ( prevTime !== null && prevTime > currTime ) {
-
- error( 'KeyframeTrack: Out of order keys.', this, i, currTime, prevTime );
- valid = false;
- break;
-
- }
-
- prevTime = currTime;
-
- }
-
- if ( values !== undefined ) {
-
- if ( isTypedArray( values ) ) {
-
- for ( let i = 0, n = values.length; i !== n; ++ i ) {
-
- const value = values[ i ];
-
- if ( isNaN( value ) ) {
-
- error( 'KeyframeTrack: Value is not a valid number.', this, i, value );
- valid = false;
- break;
-
- }
-
- }
-
- }
-
- }
-
- return valid;
-
- }
-
- /**
- * Optimizes this keyframe track by removing equivalent sequential keys (which are
- * common in morph target sequences).
- *
- * @return {KeyframeTrack} A reference to this keyframe track.
- */
- optimize() {
-
- // (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0)
-
- // times or values may be shared with other tracks, so overwriting is unsafe
- const times = this.times.slice(),
- values = this.values.slice(),
- stride = this.getValueSize(),
-
- smoothInterpolation = this.getInterpolation() === InterpolateSmooth,
-
- lastIndex = times.length - 1;
-
- let writeIndex = 1;
-
- for ( let i = 1; i < lastIndex; ++ i ) {
-
- let keep = false;
-
- const time = times[ i ];
- const timeNext = times[ i + 1 ];
-
- // remove adjacent keyframes scheduled at the same time
-
- if ( time !== timeNext && ( i !== 1 || time !== times[ 0 ] ) ) {
-
- if ( ! smoothInterpolation ) {
-
- // remove unnecessary keyframes same as their neighbors
-
- const offset = i * stride,
- offsetP = offset - stride,
- offsetN = offset + stride;
-
- for ( let j = 0; j !== stride; ++ j ) {
-
- const value = values[ offset + j ];
-
- if ( value !== values[ offsetP + j ] ||
- value !== values[ offsetN + j ] ) {
-
- keep = true;
- break;
-
- }
-
- }
-
- } else {
-
- keep = true;
-
- }
-
- }
-
- // in-place compaction
-
- if ( keep ) {
-
- if ( i !== writeIndex ) {
-
- times[ writeIndex ] = times[ i ];
-
- const readOffset = i * stride,
- writeOffset = writeIndex * stride;
-
- for ( let j = 0; j !== stride; ++ j ) {
-
- values[ writeOffset + j ] = values[ readOffset + j ];
-
- }
-
- }
-
- ++ writeIndex;
-
- }
-
- }
-
- // flush last keyframe (compaction looks ahead)
-
- if ( lastIndex > 0 ) {
-
- times[ writeIndex ] = times[ lastIndex ];
-
- for ( let readOffset = lastIndex * stride, writeOffset = writeIndex * stride, j = 0; j !== stride; ++ j ) {
-
- values[ writeOffset + j ] = values[ readOffset + j ];
-
- }
-
- ++ writeIndex;
-
- }
-
- if ( writeIndex !== times.length ) {
-
- this.times = times.slice( 0, writeIndex );
- this.values = values.slice( 0, writeIndex * stride );
-
- } else {
-
- this.times = times;
- this.values = values;
-
- }
-
- return this;
-
- }
-
- /**
- * Returns a new keyframe track with copied values from this instance.
- *
- * @return {KeyframeTrack} A clone of this instance.
- */
- clone() {
-
- const times = this.times.slice();
- const values = this.values.slice();
-
- const TypedKeyframeTrack = this.constructor;
- const track = new TypedKeyframeTrack( this.name, times, values );
-
- // Interpolant argument to constructor is not saved, so copy the factory method directly.
- track.createInterpolant = this.createInterpolant;
-
- if ( hasTangents( this.settings ) ) {
-
- track.settings = {
- inTangents: this.settings.inTangents.slice(),
- outTangents: this.settings.outTangents.slice()
- };
-
- }
-
- return track;
-
- }
-
-}
-
-function scaleTangentTimes( tangents, timeScale ) {
-
- // tangents are [ time, value ] pairs, so only every second entry is a time
-
- for ( let i = 0, n = tangents.length; i !== n; i += 2 ) {
-
- tangents[ i ] *= timeScale;
-
- }
-
-}
-
-/**
- * The value type name.
- *
- * @type {string}
- * @default ''
- */
-KeyframeTrack.prototype.ValueTypeName = '';
-
-/**
- * The time buffer type of this keyframe track.
- *
- * @type {TypedArray|Array}
- * @default Float32Array.constructor
- */
-KeyframeTrack.prototype.TimeBufferType = Float32Array;
-
-/**
- * The value buffer type of this keyframe track.
- *
- * @type {TypedArray|Array}
- * @default Float32Array.constructor
- */
-KeyframeTrack.prototype.ValueBufferType = Float32Array;
-
-/**
- * The default interpolation type of this keyframe track.
- *
- * @type {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth|InterpolateBezier)}
- * @default InterpolateLinear
- */
-KeyframeTrack.prototype.DefaultInterpolation = InterpolateLinear;
-
-/**
- * A track for boolean keyframe values.
- *
- * @augments KeyframeTrack
- */
-class BooleanKeyframeTrack extends KeyframeTrack {
-
- /**
- * Constructs a new boolean keyframe track.
- *
- * This keyframe track type has no `interpolation` parameter because the
- * interpolation is always discrete.
- *
- * @param {string} name - The keyframe track's name.
- * @param {Array} times - A list of keyframe times.
- * @param {Array} values - A list of keyframe values.
- */
- constructor( name, times, values ) {
-
- super( name, times, values );
-
- }
-
-}
-
-/**
- * The value type name.
- *
- * @type {string}
- * @default 'bool'
- */
-BooleanKeyframeTrack.prototype.ValueTypeName = 'bool';
-
-/**
- * The value buffer type of this keyframe track.
- *
- * @type {TypedArray|Array}
- * @default Array.constructor
- */
-BooleanKeyframeTrack.prototype.ValueBufferType = Array;
-
-/**
- * The default interpolation type of this keyframe track.
- *
- * @type {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)}
- * @default InterpolateDiscrete
- */
-BooleanKeyframeTrack.prototype.DefaultInterpolation = InterpolateDiscrete;
-BooleanKeyframeTrack.prototype.InterpolantFactoryMethodLinear = undefined;
-BooleanKeyframeTrack.prototype.InterpolantFactoryMethodSmooth = undefined;
-
-/**
- * A track for color keyframe values.
- *
- * @augments KeyframeTrack
- */
-class ColorKeyframeTrack extends KeyframeTrack {
-
- /**
- * Constructs a new color keyframe track.
- *
- * @param {string} name - The keyframe track's name.
- * @param {Array} times - A list of keyframe times.
- * @param {Array} values - A list of keyframe values.
- * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} [interpolation] - The interpolation type.
- */
- constructor( name, times, values, interpolation ) {
-
- super( name, times, values, interpolation );
-
- }
-
-}
-
-/**
- * The value type name.
- *
- * @type {string}
- * @default 'color'
- */
-ColorKeyframeTrack.prototype.ValueTypeName = 'color';
-
-/**
- * A track for numeric keyframe values.
- *
- * @augments KeyframeTrack
- */
-class NumberKeyframeTrack extends KeyframeTrack {
-
- /**
- * Constructs a new number keyframe track.
- *
- * @param {string} name - The keyframe track's name.
- * @param {Array} times - A list of keyframe times.
- * @param {Array} values - A list of keyframe values.
- * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} [interpolation] - The interpolation type.
- */
- constructor( name, times, values, interpolation ) {
-
- super( name, times, values, interpolation );
-
- }
-
-}
-
-/**
- * The value type name.
- *
- * @type {string}
- * @default 'number'
- */
-NumberKeyframeTrack.prototype.ValueTypeName = 'number';
-
-/**
- * Spherical linear unit quaternion interpolant.
- *
- * @augments Interpolant
- */
-class QuaternionLinearInterpolant extends Interpolant {
-
- /**
- * Constructs a new SLERP interpolant.
- *
- * @param {TypedArray} parameterPositions - The parameter positions hold the interpolation factors.
- * @param {TypedArray} sampleValues - The sample values.
- * @param {number} sampleSize - The sample size
- * @param {TypedArray} [resultBuffer] - The result buffer.
- */
- constructor( parameterPositions, sampleValues, sampleSize, resultBuffer ) {
-
- super( parameterPositions, sampleValues, sampleSize, resultBuffer );
-
- }
-
- interpolate_( i1, t0, t, t1 ) {
-
- const result = this.resultBuffer,
- values = this.sampleValues,
- stride = this.valueSize,
-
- alpha = ( t - t0 ) / ( t1 - t0 );
-
- let offset = i1 * stride;
-
- for ( let end = offset + stride; offset !== end; offset += 4 ) {
-
- Quaternion.slerpFlat( result, 0, values, offset - stride, values, offset, alpha );
-
- }
-
- return result;
-
- }
-
-}
-
-/**
- * A track for Quaternion keyframe values.
- *
- * @augments KeyframeTrack
- */
-class QuaternionKeyframeTrack extends KeyframeTrack {
-
- /**
- * Constructs a new Quaternion keyframe track.
- *
- * @param {string} name - The keyframe track's name.
- * @param {Array} times - A list of keyframe times.
- * @param {Array} values - A list of keyframe values.
- * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} [interpolation] - The interpolation type.
- */
- constructor( name, times, values, interpolation ) {
-
- super( name, times, values, interpolation );
-
- }
-
- /**
- * Overwritten so the method returns Quaternion based interpolant.
- *
- * @static
- * @param {TypedArray} [result] - The result buffer.
- * @return {QuaternionLinearInterpolant} The new interpolant.
- */
- InterpolantFactoryMethodLinear( result ) {
-
- return new QuaternionLinearInterpolant( this.times, this.values, this.getValueSize(), result );
-
- }
-
-}
-
-/**
- * The value type name.
- *
- * @type {string}
- * @default 'quaternion'
- */
-QuaternionKeyframeTrack.prototype.ValueTypeName = 'quaternion';
-// ValueBufferType is inherited
-// DefaultInterpolation is inherited;
-QuaternionKeyframeTrack.prototype.InterpolantFactoryMethodSmooth = undefined;
-
-/**
- * A track for string keyframe values.
- *
- * @augments KeyframeTrack
- */
-class StringKeyframeTrack extends KeyframeTrack {
-
- /**
- * Constructs a new string keyframe track.
- *
- * This keyframe track type has no `interpolation` parameter because the
- * interpolation is always discrete.
- *
- * @param {string} name - The keyframe track's name.
- * @param {Array} times - A list of keyframe times.
- * @param {Array} values - A list of keyframe values.
- */
- constructor( name, times, values ) {
-
- super( name, times, values );
-
- }
-
-}
-
-/**
- * The value type name.
- *
- * @type {string}
- * @default 'string'
- */
-StringKeyframeTrack.prototype.ValueTypeName = 'string';
-
-/**
- * The value buffer type of this keyframe track.
- *
- * @type {TypedArray|Array}
- * @default Array.constructor
- */
-StringKeyframeTrack.prototype.ValueBufferType = Array;
-
-/**
- * The default interpolation type of this keyframe track.
- *
- * @type {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)}
- * @default InterpolateDiscrete
- */
-StringKeyframeTrack.prototype.DefaultInterpolation = InterpolateDiscrete;
-StringKeyframeTrack.prototype.InterpolantFactoryMethodLinear = undefined;
-StringKeyframeTrack.prototype.InterpolantFactoryMethodSmooth = undefined;
-
-/**
- * A track for vector keyframe values.
- *
- * @augments KeyframeTrack
- */
-class VectorKeyframeTrack extends KeyframeTrack {
-
- /**
- * Constructs a new vector keyframe track.
- *
- * @param {string} name - The keyframe track's name.
- * @param {Array} times - A list of keyframe times.
- * @param {Array} values - A list of keyframe values.
- * @param {(InterpolateLinear|InterpolateDiscrete|InterpolateSmooth)} [interpolation] - The interpolation type.
- */
- constructor( name, times, values, interpolation ) {
-
- super( name, times, values, interpolation );
-
- }
-
-}
-
-/**
- * The value type name.
- *
- * @type {string}
- * @default 'vector'
- */
-VectorKeyframeTrack.prototype.ValueTypeName = 'vector';
-
-/**
- * A reusable set of keyframe tracks which represent an animation.
- */
-class AnimationClip {
-
- /**
- * Constructs a new animation clip.
- *
- * Note: Instead of instantiating an AnimationClip directly with the constructor, you can
- * use the static interface of this class for creating clips. In most cases though, animation clips
- * will automatically be created by loaders when importing animated 3D assets.
- *
- * @param {string} [name=''] - The clip's name.
- * @param {number} [duration=-1] - The clip's duration in seconds. If a negative value is passed,
- * the duration will be calculated from the passed keyframes.
- * @param {Array} tracks - An array of keyframe tracks.
- * @param {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)} [blendMode=NormalAnimationBlendMode] - Defines how the animation
- * is blended/combined when two or more animations are simultaneously played.
- */
- constructor( name = '', duration = -1, tracks = [], blendMode = NormalAnimationBlendMode ) {
-
- /**
- * The clip's name.
- *
- * @type {string}
- */
- this.name = name;
-
- /**
- * An array of keyframe tracks.
- *
- * @type {Array}
- */
- this.tracks = tracks;
-
- /**
- * The clip's duration in seconds.
- *
- * @type {number}
- */
- this.duration = duration;
-
- /**
- * Defines how the animation is blended/combined when two or more animations
- * are simultaneously played.
- *
- * @type {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)}
- */
- this.blendMode = blendMode;
-
- /**
- * The UUID of the animation clip.
- *
- * @type {string}
- * @readonly
- */
- this.uuid = generateUUID();
-
- /**
- * An object that can be used to store custom data about the animation clip.
- * It should not hold references to functions as these will not be cloned.
- *
- * @type {Object}
- */
- this.userData = {};
-
- // this means it should figure out its duration by scanning the tracks
- if ( this.duration < 0 ) {
-
- this.resetDuration();
-
- }
-
- }
-
- /**
- * Factory method for creating an animation clip from the given JSON.
- *
- * @static
- * @param {Object} json - The serialized animation clip.
- * @return {AnimationClip} The new animation clip.
- */
- static parse( json ) {
-
- const tracks = [],
- jsonTracks = json.tracks,
- frameTime = 1.0 / ( json.fps || 1.0 );
-
- for ( let i = 0, n = jsonTracks.length; i !== n; ++ i ) {
-
- tracks.push( parseKeyframeTrack( jsonTracks[ i ] ).scale( frameTime ) );
-
- }
-
- const clip = new this( json.name, json.duration, tracks, json.blendMode );
- clip.uuid = json.uuid;
-
- clip.userData = JSON.parse( json.userData || '{}' );
-
- return clip;
-
- }
-
- /**
- * Serializes the given animation clip into JSON.
- *
- * @static
- * @param {AnimationClip} clip - The animation clip to serialize.
- * @return {Object} The JSON object.
- */
- static toJSON( clip ) {
-
- const tracks = [],
- clipTracks = clip.tracks;
-
- const json = {
-
- 'name': clip.name,
- 'duration': clip.duration,
- 'tracks': tracks,
- 'uuid': clip.uuid,
- 'blendMode': clip.blendMode,
- 'userData': JSON.stringify( clip.userData ),
-
- };
-
- for ( let i = 0, n = clipTracks.length; i !== n; ++ i ) {
-
- tracks.push( KeyframeTrack.toJSON( clipTracks[ i ] ) );
-
- }
-
- return json;
-
- }
-
- /**
- * Returns a new animation clip from the passed morph targets array of a
- * geometry, taking a name and the number of frames per second.
- *
- * Note: The fps parameter is required, but the animation speed can be
- * overridden via {@link AnimationAction#setDuration}.
- *
- * @static
- * @param {string} name - The name of the animation clip.
- * @param {Array} morphTargetSequence - A sequence of morph targets.
- * @param {number} fps - The Frames-Per-Second value.
- * @param {boolean} noLoop - Whether the clip should be no loop or not.
- * @return {AnimationClip} The new animation clip.
- */
- static CreateFromMorphTargetSequence( name, morphTargetSequence, fps, noLoop ) {
-
- const numMorphTargets = morphTargetSequence.length;
- const tracks = [];
-
- for ( let i = 0; i < numMorphTargets; i ++ ) {
-
- let times = [];
- let values = [];
-
- times.push(
- ( i + numMorphTargets - 1 ) % numMorphTargets,
- i,
- ( i + 1 ) % numMorphTargets );
-
- values.push( 0, 1, 0 );
-
- const order = getKeyframeOrder( times );
- times = sortedArray( times, 1, order );
- values = sortedArray( values, 1, order );
-
- // if there is a key at the first frame, duplicate it as the
- // last frame as well for perfect loop.
- if ( ! noLoop && times[ 0 ] === 0 ) {
-
- times.push( numMorphTargets );
- values.push( values[ 0 ] );
-
- }
-
- tracks.push(
- new NumberKeyframeTrack(
- '.morphTargetInfluences[' + morphTargetSequence[ i ].name + ']',
- times, values
- ).scale( 1.0 / fps ) );
-
- }
-
- return new this( name, -1, tracks );
-
- }
-
- /**
- * Searches for an animation clip by name, taking as its first parameter
- * either an array of clips, or a mesh or geometry that contains an
- * array named "animations" property.
- *
- * @static
- * @param {(Array|Object3D)} objectOrClipArray - The array or object to search through.
- * @param {string} name - The name to search for.
- * @return {?AnimationClip} The found animation clip. Returns `null` if no clip has been found.
- */
- static findByName( objectOrClipArray, name ) {
-
- let clipArray = objectOrClipArray;
-
- if ( ! Array.isArray( objectOrClipArray ) ) {
-
- const o = objectOrClipArray;
- clipArray = o.geometry && o.geometry.animations || o.animations;
-
- }
-
- for ( let i = 0; i < clipArray.length; i ++ ) {
-
- if ( clipArray[ i ].name === name ) {
-
- return clipArray[ i ];
-
- }
-
- }
-
- return null;
-
- }
-
- /**
- * Returns an array of new AnimationClips created from the morph target
- * sequences of a geometry, trying to sort morph target names into
- * animation-group-based patterns like "Walk_001, Walk_002, Run_001, Run_002...".
- *
- * See {@link MD2Loader#parse} as an example for how the method should be used.
- *
- * @static
- * @param {Array} morphTargets - A sequence of morph targets.
- * @param {number} fps - The Frames-Per-Second value.
- * @param {boolean} noLoop - Whether the clip should be no loop or not.
- * @return {Array} An array of new animation clips.
- */
- static CreateClipsFromMorphTargetSequences( morphTargets, fps, noLoop ) {
-
- const animationToMorphTargets = {};
-
- // tested with https://regex101.com/ on trick sequences
- // such flamingo_flyA_003, flamingo_run1_003, crdeath0059
- const pattern = /^([\w-]*?)([\d]+)$/;
-
- // sort morph target names into animation groups based
- // patterns like Walk_001, Walk_002, Run_001, Run_002
- for ( let i = 0, il = morphTargets.length; i < il; i ++ ) {
-
- const morphTarget = morphTargets[ i ];
- const parts = morphTarget.name.match( pattern );
-
- if ( parts && parts.length > 1 ) {
-
- const name = parts[ 1 ];
-
- let animationMorphTargets = animationToMorphTargets[ name ];
-
- if ( ! animationMorphTargets ) {
-
- animationToMorphTargets[ name ] = animationMorphTargets = [];
-
- }
-
- animationMorphTargets.push( morphTarget );
-
- }
-
- }
-
- const clips = [];
-
- for ( const name in animationToMorphTargets ) {
-
- clips.push( this.CreateFromMorphTargetSequence( name, animationToMorphTargets[ name ], fps, noLoop ) );
-
- }
-
- return clips;
-
- }
-
- /**
- * Sets the duration of this clip to the duration of its longest keyframe track.
- *
- * @return {AnimationClip} A reference to this animation clip.
- */
- resetDuration() {
-
- const tracks = this.tracks;
- let duration = 0;
-
- for ( let i = 0, n = tracks.length; i !== n; ++ i ) {
-
- const track = this.tracks[ i ];
-
- duration = Math.max( duration, track.times[ track.times.length - 1 ] );
-
- }
-
- this.duration = duration;
-
- return this;
-
- }
-
- /**
- * Trims all tracks to the clip's duration.
- *
- * @return {AnimationClip} A reference to this animation clip.
- */
- trim() {
-
- for ( let i = 0; i < this.tracks.length; i ++ ) {
-
- this.tracks[ i ].trim( 0, this.duration );
-
- }
-
- return this;
-
- }
-
- /**
- * Performs minimal validation on each track in the clip. Returns `true` if all
- * tracks are valid.
- *
- * @return {boolean} Whether the clip's keyframes are valid or not.
- */
- validate() {
-
- let valid = true;
-
- for ( let i = 0; i < this.tracks.length; i ++ ) {
-
- valid = valid && this.tracks[ i ].validate();
-
- }
-
- return valid;
-
- }
-
- /**
- * Optimizes each track by removing equivalent sequential keys (which are
- * common in morph target sequences).
- *
- * @return {AnimationClip} A reference to this animation clip.
- */
- optimize() {
-
- for ( let i = 0; i < this.tracks.length; i ++ ) {
-
- this.tracks[ i ].optimize();
-
- }
-
- return this;
-
- }
-
- /**
- * Returns a new animation clip with copied values from this instance.
- *
- * @return {AnimationClip} A clone of this instance.
- */
- clone() {
-
- const tracks = [];
-
- for ( let i = 0; i < this.tracks.length; i ++ ) {
-
- tracks.push( this.tracks[ i ].clone() );
-
- }
-
- const clip = new this.constructor( this.name, this.duration, tracks, this.blendMode );
-
- clip.userData = JSON.parse( JSON.stringify( this.userData ) );
-
- return clip;
-
- }
-
- /**
- * Serializes this animation clip into JSON.
- *
- * @return {Object} The JSON object.
- */
- toJSON() {
-
- return this.constructor.toJSON( this );
-
- }
-
-}
-
-function getTrackTypeForValueTypeName( typeName ) {
-
- switch ( typeName.toLowerCase() ) {
-
- case 'scalar':
- case 'double':
- case 'float':
- case 'number':
- case 'integer':
-
- return NumberKeyframeTrack;
-
- case 'vector':
- case 'vector2':
- case 'vector3':
- case 'vector4':
-
- return VectorKeyframeTrack;
-
- case 'color':
-
- return ColorKeyframeTrack;
-
- case 'quaternion':
-
- return QuaternionKeyframeTrack;
-
- case 'bool':
- case 'boolean':
-
- return BooleanKeyframeTrack;
-
- case 'string':
-
- return StringKeyframeTrack;
-
- }
-
- throw new Error( 'THREE.KeyframeTrack: Unsupported typeName: ' + typeName );
-
-}
-
-function parseKeyframeTrack( json ) {
-
- if ( json.type === undefined ) {
-
- throw new Error( 'THREE.KeyframeTrack: track type undefined, can not parse' );
-
- }
-
- const trackType = getTrackTypeForValueTypeName( json.type );
-
- if ( json.times === undefined ) {
-
- const times = [], values = [];
-
- flattenJSON( json.keys, times, values, 'value' );
-
- json.times = times;
- json.values = values;
-
- }
-
- let track;
-
- // derived classes can define a static parse method
- if ( trackType.parse !== undefined ) {
-
- track = trackType.parse( json );
-
- } else {
-
- // by default, we assume a constructor compatible with the base
- track = new trackType( json.name, json.times, json.values, json.interpolation );
-
- }
-
- if ( hasTangents( json.settings ) ) {
-
- track.settings = {
- inTangents: convertArray( json.settings.inTangents, Float32Array ),
- outTangents: convertArray( json.settings.outTangents, Float32Array )
- };
-
- }
-
- return track;
-
-}
-
-/**
- * @class
- * @classdesc A simple caching system, used internally by {@link FileLoader}.
- * To enable caching across all loaders that use {@link FileLoader}, add `THREE.Cache.enabled = true.` once in your app.
- * @hideconstructor
- */
-const Cache = {
-
- /**
- * Whether caching is enabled or not.
- *
- * @static
- * @type {boolean}
- * @default false
- */
- enabled: false,
-
- /**
- * A dictionary that holds cached files.
- *
- * @static
- * @type {Object}
- */
- files: {},
-
- /**
- * Adds a cache entry with a key to reference the file. If this key already
- * holds a file, it is overwritten.
- *
- * @static
- * @param {string} key - The key to reference the cached file.
- * @param {Object} file - The file to be cached.
- */
- add: function ( key, file ) {
-
- if ( this.enabled === false ) return;
-
- if ( isBlobURL( key ) ) return;
-
- // log( 'Cache', 'Adding key:', key );
-
- this.files[ key ] = file;
-
- },
-
- /**
- * Gets the cached value for the given key.
- *
- * @static
- * @param {string} key - The key to reference the cached file.
- * @return {Object|undefined} The cached file. If the key does not exist `undefined` is returned.
- */
- get: function ( key ) {
-
- if ( this.enabled === false ) return;
-
- if ( isBlobURL( key ) ) return;
-
- // log( 'Cache', 'Checking key:', key );
-
- return this.files[ key ];
-
- },
-
- /**
- * Removes the cached file associated with the given key.
- *
- * @static
- * @param {string} key - The key to reference the cached file.
- */
- remove: function ( key ) {
-
- delete this.files[ key ];
-
- },
-
- /**
- * Remove all values from the cache.
- *
- * @static
- */
- clear: function () {
-
- this.files = {};
-
- }
-
-};
-
-/**
- * Returns true if the given cache key contains the blob: scheme.
- *
- * @private
- * @param {string} key - The cache key.
- * @return {boolean} Whether the given cache key contains the blob: scheme or not.
- */
-function isBlobURL( key ) {
-
- try {
-
- const urlString = key.slice( key.indexOf( ':' ) + 1 ); // remove type identifier
-
- const url = new URL( urlString );
- return url.protocol === 'blob:';
-
- } catch ( e ) {
-
- // If the string is not a valid URL, it throws an error
- return false;
-
- }
-
-}
-
-/**
- * Handles and keeps track of loaded and pending data. A default global
- * instance of this class is created and used by loaders if not supplied
- * manually.
- *
- * In general that should be sufficient, however there are times when it can
- * be useful to have separate loaders - for example if you want to show
- * separate loading bars for objects and textures.
- *
- * ```js
- * const manager = new THREE.LoadingManager();
- * manager.onLoad = () => console.log( 'Loading complete!' );
- *
- * const loader1 = new OBJLoader( manager );
- * const loader2 = new ColladaLoader( manager );
- * ```
- */
-class LoadingManager {
-
- /**
- * Constructs a new loading manager.
- *
- * @param {Function} [onLoad] - Executes when all items have been loaded.
- * @param {Function} [onProgress] - Executes when single items have been loaded.
- * @param {Function} [onError] - Executes when an error occurs.
- */
- constructor( onLoad, onProgress, onError ) {
-
- const scope = this;
-
- let isLoading = false;
- let itemsLoaded = 0;
- let itemsTotal = 0;
- let urlModifier = undefined;
- const handlers = [];
-
- // Refer to #5689 for the reason why we don't set .onStart
- // in the constructor
-
- /**
- * Executes when an item starts loading.
- *
- * @type {Function|undefined}
- * @default undefined
- */
- this.onStart = undefined;
-
- /**
- * Executes when all items have been loaded.
- *
- * @type {Function|undefined}
- * @default undefined
- */
- this.onLoad = onLoad;
-
- /**
- * Executes when single items have been loaded.
- *
- * @type {Function|undefined}
- * @default undefined
- */
- this.onProgress = onProgress;
-
- /**
- * Executes when an error occurs.
- *
- * @type {Function|undefined}
- * @default undefined
- */
- this.onError = onError;
-
- /**
- * Used for aborting ongoing requests in loaders using this manager.
- *
- * @private
- * @type {AbortController | null}
- */
- this._abortController = null;
-
- /**
- * This should be called by any loader using the manager when the loader
- * starts loading an item.
- *
- * @param {string} url - The URL to load.
- */
- this.itemStart = function ( url ) {
-
- itemsTotal ++;
-
- if ( isLoading === false ) {
-
- if ( scope.onStart !== undefined ) {
-
- scope.onStart( url, itemsLoaded, itemsTotal );
-
- }
-
- }
-
- isLoading = true;
-
- };
-
- /**
- * This should be called by any loader using the manager when the loader
- * ended loading an item.
- *
- * @param {string} url - The URL of the loaded item.
- */
- this.itemEnd = function ( url ) {
-
- itemsLoaded ++;
-
- if ( scope.onProgress !== undefined ) {
-
- scope.onProgress( url, itemsLoaded, itemsTotal );
-
- }
-
- if ( itemsLoaded === itemsTotal ) {
-
- isLoading = false;
-
- if ( scope.onLoad !== undefined ) {
-
- scope.onLoad();
-
- }
-
- }
-
- };
-
- /**
- * This should be called by any loader using the manager when the loader
- * encounters an error when loading an item.
- *
- * @param {string} url - The URL of the item that produces an error.
- */
- this.itemError = function ( url ) {
-
- if ( scope.onError !== undefined ) {
-
- scope.onError( url );
-
- }
-
- };
-
- /**
- * Given a URL, uses the URL modifier callback (if any) and returns a
- * resolved URL. If no URL modifier is set, returns the original URL.
- *
- * @param {string} url - The URL to load.
- * @return {string} The resolved URL.
- */
- this.resolveURL = function ( url ) {
-
- // Normalize to NFC so that Unicode URIs (e.g. from glTF)
- // are percent-encoded correctly per RFC 3987.
-
- url = url.normalize( 'NFC' );
-
- if ( urlModifier ) {
-
- return urlModifier( url );
-
- }
-
- return url;
-
- };
-
- /**
- * If provided, the callback will be passed each resource URL before a
- * request is sent. The callback may return the original URL, or a new URL to
- * override loading behavior. This behavior can be used to load assets from
- * .ZIP files, drag-and-drop APIs, and Data URIs.
- *
- * ```js
- * const blobs = {'fish.gltf': blob1, 'diffuse.png': blob2, 'normal.png': blob3};
- *
- * const manager = new THREE.LoadingManager();
- *
- * // Initialize loading manager with URL callback.
- * const objectURLs = [];
- * manager.setURLModifier( ( url ) => {
- *
- * url = URL.createObjectURL( blobs[ url ] );
- * objectURLs.push( url );
- * return url;
- *
- * } );
- *
- * // Load as usual, then revoke the blob URLs.
- * const loader = new GLTFLoader( manager );
- * loader.load( 'fish.gltf', (gltf) => {
- *
- * scene.add( gltf.scene );
- * objectURLs.forEach( ( url ) => URL.revokeObjectURL( url ) );
- *
- * } );
- * ```
- *
- * @param {function(string):string} transform - URL modifier callback. Called with an URL and must return a resolved URL.
- * @return {LoadingManager} A reference to this loading manager.
- */
- this.setURLModifier = function ( transform ) {
-
- urlModifier = transform;
-
- return this;
-
- };
-
- /**
- * Registers a loader with the given regular expression. Can be used to
- * define what loader should be used in order to load specific files. A
- * typical use case is to overwrite the default loader for textures.
- *
- * ```js
- * // add handler for TGA textures
- * manager.addHandler( /\.tga$/i, new TGALoader() );
- * ```
- *
- * @param {string} regex - A regular expression.
- * @param {Loader} loader - A loader that should handle matched cases.
- * @return {LoadingManager} A reference to this loading manager.
- */
- this.addHandler = function ( regex, loader ) {
-
- handlers.push( regex, loader );
-
- return this;
-
- };
-
- /**
- * Removes the loader for the given regular expression.
- *
- * @param {string} regex - A regular expression.
- * @return {LoadingManager} A reference to this loading manager.
- */
- this.removeHandler = function ( regex ) {
-
- const index = handlers.indexOf( regex );
-
- if ( index !== -1 ) {
-
- handlers.splice( index, 2 );
-
- }
-
- return this;
-
- };
-
- /**
- * Can be used to retrieve the registered loader for the given file path.
- *
- * @param {string} file - The file path.
- * @return {?Loader} The registered loader. Returns `null` if no loader was found.
- */
- this.getHandler = function ( file ) {
-
- for ( let i = 0, l = handlers.length; i < l; i += 2 ) {
-
- const regex = handlers[ i ];
- const loader = handlers[ i + 1 ];
-
- if ( regex.global ) regex.lastIndex = 0; // see #17920
-
- if ( regex.test( file ) ) {
-
- return loader;
-
- }
-
- }
-
- return null;
-
- };
-
- /**
- * Can be used to abort ongoing loading requests in loaders using this manager.
- * The abort only works if the loaders implement {@link Loader#abort} and `AbortSignal.any()`
- * is supported in the browser.
- *
- * @return {LoadingManager} A reference to this loading manager.
- */
- this.abort = function () {
-
-
- this.abortController.abort();
- this._abortController = null;
-
- return this;
-
- };
-
- }
-
- // TODO: Revert this back to a single member variable once this issue has been fixed
- // https://github.com/cloudflare/workerd/issues/3657
-
- /**
- * Used for aborting ongoing requests in loaders using this manager.
- *
- * @type {AbortController}
- */
- get abortController() {
-
- if ( ! this._abortController ) {
-
- this._abortController = new AbortController();
-
- }
-
- return this._abortController;
-
- }
-
-}
-
-/**
- * The global default loading manager.
- *
- * @constant
- * @type {LoadingManager}
- */
-const DefaultLoadingManager = /*@__PURE__*/ new LoadingManager();
-
-/**
- * Abstract base class for loaders.
- *
- * @abstract
- */
-class Loader {
-
- /**
- * Constructs a new loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- /**
- * The loading manager.
- *
- * @type {LoadingManager}
- * @default DefaultLoadingManager
- */
- this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager;
-
- /**
- * The crossOrigin string to implement CORS for loading the url from a
- * different domain that allows CORS.
- *
- * @type {string}
- * @default 'anonymous'
- */
- this.crossOrigin = 'anonymous';
-
- /**
- * Whether the XMLHttpRequest uses credentials.
- *
- * @type {boolean}
- * @default false
- */
- this.withCredentials = false;
-
- /**
- * The base path from which the asset will be loaded.
- *
- * @type {string}
- */
- this.path = '';
-
- /**
- * The base path from which additional resources like textures will be loaded.
- *
- * @type {string}
- */
- this.resourcePath = '';
-
- /**
- * The [request header](https://developer.mozilla.org/en-US/docs/Glossary/Request_header)
- * used in HTTP request.
- *
- * @type {Object}
- */
- this.requestHeader = {};
-
- if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {
-
- __THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) );
-
- }
-
- }
-
- /**
- * This method needs to be implemented by all concrete loaders. It holds the
- * logic for loading assets from the backend.
- *
- * @abstract
- * @param {string} url - The path/URL of the file to be loaded.
- * @param {Function} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} [onProgress] - Executed while the loading is in progress.
- * @param {onErrorCallback} [onError] - Executed when errors occur.
- */
- load( /* url, onLoad, onProgress, onError */ ) {}
-
- /**
- * A async version of {@link Loader#load}.
- *
- * @param {string} url - The path/URL of the file to be loaded.
- * @param {onProgressCallback} [onProgress] - Executed while the loading is in progress.
- * @return {Promise} A Promise that resolves when the asset has been loaded.
- */
- loadAsync( url, onProgress ) {
-
- const scope = this;
-
- return new Promise( function ( resolve, reject ) {
-
- scope.load( url, resolve, onProgress, reject );
-
- } );
-
- }
-
- /**
- * This method needs to be implemented by all concrete loaders. It holds the
- * logic for parsing the asset into three.js entities.
- *
- * @abstract
- * @param {any} data - The data to parse.
- */
- parse( /* data */ ) {}
-
- /**
- * Sets the `crossOrigin` String to implement CORS for loading the URL
- * from a different domain that allows CORS.
- *
- * @param {string} crossOrigin - The `crossOrigin` value.
- * @return {Loader} A reference to this instance.
- */
- setCrossOrigin( crossOrigin ) {
-
- this.crossOrigin = crossOrigin;
- return this;
-
- }
-
- /**
- * Whether the XMLHttpRequest uses credentials such as cookies, authorization
- * headers or TLS client certificates, see [XMLHttpRequest.withCredentials](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials).
- *
- * Note: This setting has no effect if you are loading files locally or from the same domain.
- *
- * @param {boolean} value - The `withCredentials` value.
- * @return {Loader} A reference to this instance.
- */
- setWithCredentials( value ) {
-
- this.withCredentials = value;
- return this;
-
- }
-
- /**
- * Sets the base path for the asset.
- *
- * @param {string} path - The base path.
- * @return {Loader} A reference to this instance.
- */
- setPath( path ) {
-
- this.path = path;
- return this;
-
- }
-
- /**
- * Sets the base path for dependent resources like textures.
- *
- * @param {string} resourcePath - The resource path.
- * @return {Loader} A reference to this instance.
- */
- setResourcePath( resourcePath ) {
-
- this.resourcePath = resourcePath;
- return this;
-
- }
-
- /**
- * Sets the given request header.
- *
- * @param {Object} requestHeader - A [request header](https://developer.mozilla.org/en-US/docs/Glossary/Request_header)
- * for configuring the HTTP request.
- * @return {Loader} A reference to this instance.
- */
- setRequestHeader( requestHeader ) {
-
- this.requestHeader = requestHeader;
- return this;
-
- }
-
- /**
- * This method can be implemented in loaders for aborting ongoing requests.
- *
- * @abstract
- * @return {Loader} A reference to this instance.
- */
- abort() {
-
- return this;
-
- }
-
-}
-
-/**
- * Callback for onProgress in loaders.
- *
- * @callback onProgressCallback
- * @param {ProgressEvent} event - An instance of `ProgressEvent` that represents the current loading status.
- */
-
-/**
- * Callback for onError in loaders.
- *
- * @callback onErrorCallback
- * @param {Error} error - The error which occurred during the loading process.
- */
-
-/**
- * The default material name that is used by loaders
- * when creating materials for loaded 3D objects.
- *
- * Note: Not all loaders might honor this setting.
- *
- * @static
- * @type {string}
- * @default '__DEFAULT'
- */
-Loader.DEFAULT_MATERIAL_NAME = '__DEFAULT';
-
-const loading = {};
-
-class HttpError extends Error {
-
- constructor( message, response ) {
-
- super( message );
- this.response = response;
-
- }
-
-}
-
-/**
- * A low level class for loading resources with the Fetch API, used internally by
- * most loaders. It can also be used directly to load any file type that does
- * not have a loader.
- *
- * This loader supports caching. If you want to use it, add `THREE.Cache.enabled = true;`
- * once to your application.
- *
- * ```js
- * const loader = new THREE.FileLoader();
- * const data = await loader.loadAsync( 'example.txt' );
- * ```
- *
- * @augments Loader
- */
-class FileLoader extends Loader {
-
- /**
- * Constructs a new file loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- /**
- * The expected mime type. Valid values can be found
- * [here](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#mimetype)
- *
- * @type {string}
- */
- this.mimeType = '';
-
- /**
- * The expected response type.
- *
- * @type {('arraybuffer'|'blob'|'document'|'json'|'')}
- * @default ''
- */
- this.responseType = '';
-
- /**
- * Used for aborting requests.
- *
- * @private
- * @type {AbortController}
- */
- this._abortController = new AbortController();
-
- }
-
- /**
- * Starts loading from the given URL and pass the loaded response to the `onLoad()` callback.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(any)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} [onProgress] - Executed while the loading is in progress.
- * @param {onErrorCallback} [onError] - Executed when errors occur.
- */
- load( url, onLoad, onProgress, onError ) {
-
- if ( url === undefined ) url = '';
-
- if ( this.path !== undefined ) url = this.path + url;
-
- url = this.manager.resolveURL( url );
-
- const cached = Cache.get( `file:${url}` );
-
- if ( cached !== undefined ) {
-
- this.manager.itemStart( url );
-
- setTimeout( () => {
-
- if ( onLoad ) onLoad( cached );
-
- this.manager.itemEnd( url );
-
- }, 0 );
-
- return;
-
- }
-
- // Check if request is duplicate
-
- if ( loading[ url ] !== undefined ) {
-
- loading[ url ].push( {
-
- onLoad: onLoad,
- onProgress: onProgress,
- onError: onError
-
- } );
-
- return;
-
- }
-
- // Initialise array for duplicate requests
- loading[ url ] = [];
-
- loading[ url ].push( {
- onLoad: onLoad,
- onProgress: onProgress,
- onError: onError,
- } );
-
- // create request
- const req = new Request( url, {
- headers: new Headers( this.requestHeader ),
- credentials: this.withCredentials ? 'include' : 'same-origin',
- signal: ( typeof AbortSignal.any === 'function' ) ? AbortSignal.any( [ this._abortController.signal, this.manager.abortController.signal ] ) : this._abortController.signal
- } );
-
- // record states ( avoid data race )
- const mimeType = this.mimeType;
- const responseType = this.responseType;
-
- // start the fetch
- fetch( req )
- .then( response => {
-
- if ( response.status === 200 || response.status === 0 ) {
-
- // Some browsers return HTTP Status 0 when using non-http protocol
- // e.g. 'file://' or 'data://'. Handle as success.
-
- if ( response.status === 0 ) {
-
- warn( 'FileLoader: HTTP Status 0 received.' );
-
- }
-
- // Workaround: Checking if response.body === undefined for Alipay browser #23548
-
- if ( typeof ReadableStream === 'undefined' || response.body === undefined || response.body.getReader === undefined ) {
-
- return response;
-
- }
-
- const callbacks = loading[ url ];
- const reader = response.body.getReader();
-
- // Nginx needs X-File-Size check
- // https://serverfault.com/questions/482875/why-does-nginx-remove-content-length-header-for-chunked-content
- const contentLength = response.headers.get( 'X-File-Size' ) || response.headers.get( 'Content-Length' );
- const total = contentLength ? parseInt( contentLength ) : 0;
- const lengthComputable = total !== 0;
- let loaded = 0;
-
- // periodically read data into the new stream tracking while download progress
- const stream = new ReadableStream( {
- start( controller ) {
-
- readData();
-
- function readData() {
-
- reader.read().then( ( { done, value } ) => {
-
- if ( done ) {
-
- controller.close();
-
- } else {
-
- loaded += value.byteLength;
-
- const event = new ProgressEvent( 'progress', { lengthComputable, loaded, total } );
- for ( let i = 0, il = callbacks.length; i < il; i ++ ) {
-
- const callback = callbacks[ i ];
- if ( callback.onProgress ) callback.onProgress( event );
-
- }
-
- controller.enqueue( value );
- readData();
-
- }
-
- }, ( e ) => {
-
- controller.error( e );
-
- } );
-
- }
-
- }
-
- } );
-
- return new Response( stream );
-
- } else {
-
- throw new HttpError( `fetch for "${response.url}" responded with ${response.status}: ${response.statusText}`, response );
-
- }
-
- } )
- .then( response => {
-
- switch ( responseType ) {
-
- case 'arraybuffer':
-
- return response.arrayBuffer();
-
- case 'blob':
-
- return response.blob();
-
- case 'document':
-
- return response.text()
- .then( text => {
-
- const parser = new DOMParser();
- return parser.parseFromString( text, mimeType );
-
- } );
-
- case 'json':
-
- return response.json();
-
- default:
-
- if ( mimeType === '' ) {
-
- return response.text();
-
- } else {
-
- // sniff encoding
- const re = /charset="?([^;"\s]*)"?/i;
- const exec = re.exec( mimeType );
- const label = exec && exec[ 1 ] ? exec[ 1 ].toLowerCase() : undefined;
- const decoder = new TextDecoder( label );
- return response.arrayBuffer().then( ab => decoder.decode( ab ) );
-
- }
-
- }
-
- } )
- .then( data => {
-
- // Add to cache only on HTTP success, so that we do not cache
- // error response bodies as proper responses to requests.
- Cache.add( `file:${url}`, data );
-
- const callbacks = loading[ url ];
- delete loading[ url ];
-
- for ( let i = 0, il = callbacks.length; i < il; i ++ ) {
-
- const callback = callbacks[ i ];
- if ( callback.onLoad ) callback.onLoad( data );
-
- }
-
- } )
- .catch( err => {
-
- // Abort errors and other errors are handled the same
-
- const callbacks = loading[ url ];
-
- if ( callbacks === undefined ) {
-
- // When onLoad was called and url was deleted in `loading`
- this.manager.itemError( url );
- throw err;
-
- }
-
- delete loading[ url ];
-
- for ( let i = 0, il = callbacks.length; i < il; i ++ ) {
-
- const callback = callbacks[ i ];
- if ( callback.onError ) callback.onError( err );
-
- }
-
- this.manager.itemError( url );
-
- } )
- .finally( () => {
-
- this.manager.itemEnd( url );
-
- } );
-
- this.manager.itemStart( url );
-
- }
-
- /**
- * Sets the expected response type.
- *
- * @param {('arraybuffer'|'blob'|'document'|'json'|'')} value - The response type.
- * @return {FileLoader} A reference to this file loader.
- */
- setResponseType( value ) {
-
- this.responseType = value;
- return this;
-
- }
-
- /**
- * Sets the expected mime type of the loaded file.
- *
- * @param {string} value - The mime type.
- * @return {FileLoader} A reference to this file loader.
- */
- setMimeType( value ) {
-
- this.mimeType = value;
- return this;
-
- }
-
- /**
- * Aborts ongoing fetch requests.
- *
- * @return {FileLoader} A reference to this instance.
- */
- abort() {
-
- this._abortController.abort();
- this._abortController = new AbortController();
-
- return this;
-
- }
-
-}
-
-/**
- * Class for loading animation clips in the JSON format. The files are internally
- * loaded via {@link FileLoader}.
- *
- * ```js
- * const loader = new THREE.AnimationLoader();
- * const animations = await loader.loadAsync( 'animations/animation.js' );
- * ```
- *
- * @augments Loader
- */
-class AnimationLoader extends Loader {
-
- /**
- * Constructs a new animation loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- }
-
- /**
- * Starts loading from the given URL and pass the loaded animations as an array
- * holding instances of {@link AnimationClip} to the `onLoad()` callback.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(Array)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
- * @param {onErrorCallback} onError - Executed when errors occur.
- */
- load( url, onLoad, onProgress, onError ) {
-
- const scope = this;
-
- const loader = new FileLoader( this.manager );
- loader.setPath( this.path );
- loader.setRequestHeader( this.requestHeader );
- loader.setWithCredentials( this.withCredentials );
- loader.load( url, function ( text ) {
-
- try {
-
- onLoad( scope.parse( JSON.parse( text ) ) );
-
- } catch ( e ) {
-
- if ( onError ) {
-
- onError( e );
-
- } else {
-
- error( e );
-
- }
-
- scope.manager.itemError( url );
-
- }
-
- }, onProgress, onError );
-
- }
-
- /**
- * Parses the given JSON object and returns an array of animation clips.
- *
- * @param {Object} json - The serialized animation clips.
- * @return {Array} The parsed animation clips.
- */
- parse( json ) {
-
- const animations = [];
-
- for ( let i = 0; i < json.length; i ++ ) {
-
- const clip = AnimationClip.parse( json[ i ] );
-
- animations.push( clip );
-
- }
-
- return animations;
-
- }
-
-}
-
-/**
- * Abstract base class for loading compressed texture formats S3TC, ASTC or ETC.
- * Textures are internally loaded via {@link FileLoader}.
- *
- * Derived classes have to implement the `parse()` method which holds the parsing
- * for the respective format.
- *
- * @abstract
- * @augments Loader
- */
-class CompressedTextureLoader extends Loader {
-
- /**
- * Constructs a new compressed texture loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- }
-
- /**
- * Starts loading from the given URL and passes the loaded compressed texture
- * to the `onLoad()` callback. The method also returns a new texture object which can
- * directly be used for material creation. If you do it this way, the texture
- * may pop up in your scene once the respective loading process is finished.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(CompressedTexture)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
- * @param {onErrorCallback} onError - Executed when errors occur.
- * @return {CompressedTexture} The compressed texture.
- */
- load( url, onLoad, onProgress, onError ) {
-
- const scope = this;
-
- const images = [];
-
- const texture = new CompressedTexture();
-
- const loader = new FileLoader( this.manager );
- loader.setPath( this.path );
- loader.setResponseType( 'arraybuffer' );
- loader.setRequestHeader( this.requestHeader );
- loader.setWithCredentials( scope.withCredentials );
-
- let loaded = 0;
-
- function loadTexture( i ) {
-
- loader.load( url[ i ], function ( buffer ) {
-
- const texDatas = scope.parse( buffer, true );
-
- images[ i ] = {
- width: texDatas.width,
- height: texDatas.height,
- format: texDatas.format,
- mipmaps: texDatas.mipmaps
- };
-
- loaded += 1;
-
- if ( loaded === 6 ) {
-
- if ( texDatas.mipmapCount === 1 ) texture.minFilter = LinearFilter;
-
- texture.image = images;
- texture.format = texDatas.format;
- texture.needsUpdate = true;
-
- if ( onLoad ) onLoad( texture );
-
- }
-
- }, onProgress, onError );
-
- }
-
- if ( Array.isArray( url ) ) {
-
- for ( let i = 0, il = url.length; i < il; ++ i ) {
-
- loadTexture( i );
-
- }
-
- } else {
-
- // compressed cubemap texture stored in a single DDS file
-
- loader.load( url, function ( buffer ) {
-
- const texDatas = scope.parse( buffer, true );
-
- if ( texDatas.isCubemap ) {
-
- const faces = texDatas.mipmaps.length / texDatas.mipmapCount;
-
- for ( let f = 0; f < faces; f ++ ) {
-
- images[ f ] = { mipmaps: [] };
-
- for ( let i = 0; i < texDatas.mipmapCount; i ++ ) {
-
- images[ f ].mipmaps.push( texDatas.mipmaps[ f * texDatas.mipmapCount + i ] );
- images[ f ].format = texDatas.format;
- images[ f ].width = texDatas.width;
- images[ f ].height = texDatas.height;
-
- }
-
- }
-
- texture.image = images;
-
- } else {
-
- texture.image.width = texDatas.width;
- texture.image.height = texDatas.height;
- texture.mipmaps = texDatas.mipmaps;
-
- }
-
- if ( texDatas.mipmapCount === 1 ) {
-
- texture.minFilter = LinearFilter;
-
- }
-
- texture.format = texDatas.format;
- texture.needsUpdate = true;
-
- if ( onLoad ) onLoad( texture );
-
- }, onProgress, onError );
-
- }
-
- return texture;
-
- }
-
-}
-
-const _loading = new WeakMap();
-
-/**
- * A loader for loading images. The class loads images with the HTML `Image` API.
- *
- * ```js
- * const loader = new THREE.ImageLoader();
- * const image = await loader.loadAsync( 'image.png' );
- * ```
- * Please note that `ImageLoader` has dropped support for progress
- * events in `r84`. For an `ImageLoader` that supports progress events, see
- * [this thread](https://github.com/mrdoob/three.js/issues/10439#issuecomment-275785639).
- *
- * @augments Loader
- */
-class ImageLoader extends Loader {
-
- /**
- * Constructs a new image loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- }
-
- /**
- * Starts loading from the given URL and passes the loaded image
- * to the `onLoad()` callback. The method also returns a new `Image` object which can
- * directly be used for texture creation. If you do it this way, the texture
- * may pop up in your scene once the respective loading process is finished.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(Image)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Unsupported in this loader.
- * @param {onErrorCallback} onError - Executed when errors occur.
- * @return {Image} The image.
- */
- load( url, onLoad, onProgress, onError ) {
-
- if ( this.path !== undefined ) url = this.path + url;
-
- url = this.manager.resolveURL( url );
-
- const scope = this;
-
- const cached = Cache.get( `image:${url}` );
-
- if ( cached !== undefined ) {
-
- if ( cached.complete === true ) {
-
- scope.manager.itemStart( url );
-
- setTimeout( function () {
-
- if ( onLoad ) onLoad( cached );
-
- scope.manager.itemEnd( url );
-
- }, 0 );
-
- } else {
-
- let arr = _loading.get( cached );
-
- if ( arr === undefined ) {
-
- arr = [];
- _loading.set( cached, arr );
-
- }
-
- arr.push( { onLoad, onError } );
-
- }
-
- return cached;
-
- }
-
- const image = createElementNS( 'img' );
-
- function onImageLoad() {
-
- removeEventListeners();
-
- if ( onLoad ) onLoad( this );
-
- //
-
- const callbacks = _loading.get( this ) || [];
-
- for ( let i = 0; i < callbacks.length; i ++ ) {
-
- const callback = callbacks[ i ];
- if ( callback.onLoad ) callback.onLoad( this );
-
- }
-
- _loading.delete( this );
-
- scope.manager.itemEnd( url );
-
- }
-
- function onImageError( event ) {
-
- removeEventListeners();
-
- if ( onError ) onError( event );
-
- Cache.remove( `image:${url}` );
-
- //
-
- const callbacks = _loading.get( this ) || [];
-
- for ( let i = 0; i < callbacks.length; i ++ ) {
-
- const callback = callbacks[ i ];
- if ( callback.onError ) callback.onError( event );
-
- }
-
- _loading.delete( this );
-
-
- scope.manager.itemError( url );
- scope.manager.itemEnd( url );
-
- }
-
- function removeEventListeners() {
-
- image.removeEventListener( 'load', onImageLoad, false );
- image.removeEventListener( 'error', onImageError, false );
-
- }
-
- image.addEventListener( 'load', onImageLoad, false );
- image.addEventListener( 'error', onImageError, false );
-
- if ( url.slice( 0, 5 ) !== 'data:' ) {
-
- if ( this.crossOrigin !== undefined ) image.crossOrigin = this.crossOrigin;
-
- }
-
- Cache.add( `image:${url}`, image );
- scope.manager.itemStart( url );
-
- image.src = url;
-
- return image;
-
- }
-
-}
-
-/**
- * Class for loading cube textures. Images are internally loaded via {@link ImageLoader}.
- *
- * The loader returns an instance of {@link CubeTexture} and expects the cube map to
- * be defined as six separate images representing the sides of a cube. Other cube map definitions
- * like vertical and horizontal cross, column and row layouts are not supported.
- *
- * Note that, by convention, cube maps are specified in a coordinate system
- * in which positive-x is to the right when looking up the positive-z axis --
- * in other words, using a left-handed coordinate system. Since three.js uses
- * a right-handed coordinate system, environment maps used in three.js will
- * have pos-x and neg-x swapped.
- *
- * The loaded cube texture is in sRGB color space. Meaning {@link Texture#colorSpace}
- * is set to `SRGBColorSpace` by default.
- *
- * ```js
- * const loader = new THREE.CubeTextureLoader().setPath( 'textures/cubeMaps/' );
- * const cubeTexture = await loader.loadAsync( [
- * 'px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png'
- * ] );
- * scene.background = cubeTexture;
- * ```
- *
- * @augments Loader
- */
-class CubeTextureLoader extends Loader {
-
- /**
- * Constructs a new cube texture loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- }
-
- /**
- * Starts loading from the given URL and pass the fully loaded cube texture
- * to the `onLoad()` callback. The method also returns a new cube texture object which can
- * directly be used for material creation. If you do it this way, the cube texture
- * may pop up in your scene once the respective loading process is finished.
- *
- * @param {Array} urls - Array of 6 URLs to images, one for each side of the
- * cube texture. The urls should be specified in the following order: pos-x,
- * neg-x, pos-y, neg-y, pos-z, neg-z. An array of data URIs are allowed as well.
- * @param {function(CubeTexture)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Unsupported in this loader.
- * @param {onErrorCallback} onError - Executed when errors occur.
- * @return {CubeTexture} The cube texture.
- */
- load( urls, onLoad, onProgress, onError ) {
-
- const texture = new CubeTexture();
- texture.colorSpace = SRGBColorSpace;
-
- const loader = new ImageLoader( this.manager );
- loader.setCrossOrigin( this.crossOrigin );
- loader.setPath( this.path );
-
- let loaded = 0;
-
- function loadTexture( i ) {
-
- loader.load( urls[ i ], function ( image ) {
-
- texture.images[ i ] = image;
-
- loaded ++;
-
- if ( loaded === 6 ) {
-
- texture.needsUpdate = true;
-
- if ( onLoad ) onLoad( texture );
-
- }
-
- }, undefined, onError );
-
- }
-
- for ( let i = 0; i < urls.length; ++ i ) {
-
- loadTexture( i );
-
- }
-
- return texture;
-
- }
-
-}
-
-/**
- * Abstract base class for loading binary texture formats RGBE, EXR or TGA.
- * Textures are internally loaded via {@link FileLoader}.
- *
- * Derived classes have to implement the `parse()` method which holds the parsing
- * for the respective format.
- *
- * @abstract
- * @augments Loader
- */
-class DataTextureLoader extends Loader {
-
- /**
- * Constructs a new data texture loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- }
-
- /**
- * Starts loading from the given URL and passes the loaded data texture
- * to the `onLoad()` callback. The method also returns a new texture object which can
- * directly be used for material creation. If you do it this way, the texture
- * may pop up in your scene once the respective loading process is finished.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(DataTexture)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
- * @param {onErrorCallback} onError - Executed when errors occur.
- * @return {DataTexture} The data texture.
- */
- load( url, onLoad, onProgress, onError ) {
-
- const scope = this;
-
- const texture = new DataTexture();
-
- const loader = new FileLoader( this.manager );
- loader.setResponseType( 'arraybuffer' );
- loader.setRequestHeader( this.requestHeader );
- loader.setPath( this.path );
- loader.setWithCredentials( scope.withCredentials );
- loader.load( url, function ( buffer ) {
-
- let texData;
-
- try {
-
- texData = scope.parse( buffer );
-
- } catch ( e ) {
-
- if ( onError !== undefined ) {
-
- onError( e );
-
- } else {
-
- error( e );
-
- }
-
- return;
-
- }
-
- scope._applyTexData( texture, texData );
-
- if ( onLoad ) onLoad( texture, texData );
-
- }, onProgress, onError );
-
-
- return texture;
-
- }
-
- /**
- * Parses the given buffer and returns a configured data texture. Use this method
- * for parsing texture data that is already in memory (e.g. drag and drop or data
- * loaded from a server) without going through {@link DataTextureLoader#load}.
- *
- * @param {ArrayBuffer} buffer - The raw texture data.
- * @return {DataTexture} The data texture.
- */
- createDataTexture( buffer ) {
-
- const texture = new DataTexture();
-
- this._applyTexData( texture, this.parse( buffer ) );
-
- return texture;
-
- }
-
- /**
- * Applies the given parsed texture data to the given data texture.
- *
- * @private
- * @param {DataTexture} texture - The data texture.
- * @param {DataTextureLoader~TexData} texData - The parsed texture data.
- */
- _applyTexData( texture, texData ) {
-
- if ( texData.image !== undefined ) {
-
- texture.image = texData.image;
-
- } else if ( texData.data !== undefined ) {
-
- texture.image.width = texData.width;
- texture.image.height = texData.height;
- texture.image.data = texData.data;
-
- }
-
- texture.wrapS = texData.wrapS !== undefined ? texData.wrapS : ClampToEdgeWrapping;
- texture.wrapT = texData.wrapT !== undefined ? texData.wrapT : ClampToEdgeWrapping;
-
- texture.magFilter = texData.magFilter !== undefined ? texData.magFilter : LinearFilter;
- texture.minFilter = texData.minFilter !== undefined ? texData.minFilter : LinearFilter;
-
- texture.anisotropy = texData.anisotropy !== undefined ? texData.anisotropy : 1;
-
- if ( texData.colorSpace !== undefined ) {
-
- texture.colorSpace = texData.colorSpace;
-
- }
-
- if ( texData.flipY !== undefined ) {
-
- texture.flipY = texData.flipY;
-
- }
-
- if ( texData.format !== undefined ) {
-
- texture.format = texData.format;
-
- }
-
- if ( texData.type !== undefined ) {
-
- texture.type = texData.type;
-
- }
-
- if ( texData.mipmaps !== undefined ) {
-
- texture.mipmaps = texData.mipmaps;
- texture.minFilter = LinearMipmapLinearFilter; // presumably...
-
- }
-
- if ( texData.mipmapCount === 1 ) {
-
- texture.minFilter = LinearFilter;
-
- }
-
- if ( texData.generateMipmaps !== undefined ) {
-
- texture.generateMipmaps = texData.generateMipmaps;
-
- }
-
- texture.needsUpdate = true;
-
- }
-
-}
-
-/**
- * Class for loading textures. Images are internally
- * loaded via {@link ImageLoader}.
- *
- * ```js
- * const loader = new THREE.TextureLoader();
- * const texture = await loader.loadAsync( 'textures/land_ocean_ice_cloud_2048.jpg' );
- *
- * const material = new THREE.MeshBasicMaterial( { map:texture } );
- * ```
- * Please note that `TextureLoader` has dropped support for progress
- * events in `r84`. For a `TextureLoader` that supports progress events, see
- * [this thread](https://github.com/mrdoob/three.js/issues/10439#issuecomment-293260145).
- *
- * @augments Loader
- */
-class TextureLoader extends Loader {
-
- /**
- * Constructs a new texture loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- }
-
- /**
- * Starts loading from the given URL and pass the fully loaded texture
- * to the `onLoad()` callback. The method also returns a new texture object which can
- * directly be used for material creation. If you do it this way, the texture
- * may pop up in your scene once the respective loading process is finished.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(Texture)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Unsupported in this loader.
- * @param {onErrorCallback} onError - Executed when errors occur.
- * @return {Texture} The texture.
- */
- load( url, onLoad, onProgress, onError ) {
-
- const texture = new Texture();
-
- const loader = new ImageLoader( this.manager );
- loader.setCrossOrigin( this.crossOrigin );
- loader.setPath( this.path );
-
- loader.load( url, function ( image ) {
-
- texture.image = image;
- texture.needsUpdate = true;
-
- if ( onLoad !== undefined ) {
-
- onLoad( texture );
-
- }
-
- }, onProgress, onError );
-
- return texture;
-
- }
-
-}
-
-/**
- * Abstract base class for lights - all other light types inherit the
- * properties and methods described here.
- *
- * @abstract
- * @augments Object3D
- */
-class Light extends Object3D {
-
- /**
- * Constructs a new light.
- *
- * @param {(number|Color|string)} [color=0xffffff] - The light's color.
- * @param {number} [intensity=1] - The light's strength/intensity.
- */
- constructor( color, intensity = 1 ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isLight = true;
-
- this.type = 'Light';
-
- /**
- * The light's color.
- *
- * @type {Color}
- */
- this.color = new Color( color );
-
- /**
- * The light's intensity.
- *
- * @type {number}
- * @default 1
- */
- this.intensity = intensity;
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.color.copy( source.color );
- this.intensity = source.intensity;
-
- return this;
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.object.color = this.color.getHex();
- data.object.intensity = this.intensity;
-
- return data;
-
- }
-
-}
-
-/**
- * A light source positioned directly above the scene, with color fading from
- * the sky color to the ground color.
- *
- * This light cannot be used to cast shadows.
- *
- * ```js
- * const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
- * scene.add( light );
- * ```
- *
- * @augments Light
- */
-class HemisphereLight extends Light {
-
- /**
- * Constructs a new hemisphere light.
- *
- * @param {(number|Color|string)} [skyColor=0xffffff] - The light's sky color.
- * @param {(number|Color|string)} [groundColor=0xffffff] - The light's ground color.
- * @param {number} [intensity=1] - The light's strength/intensity.
- */
- constructor( skyColor, groundColor, intensity ) {
-
- super( skyColor, intensity );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isHemisphereLight = true;
-
- this.type = 'HemisphereLight';
-
- this.position.copy( Object3D.DEFAULT_UP );
- this.updateMatrix();
-
- /**
- * The light's ground color.
- *
- * @type {Color}
- */
- this.groundColor = new Color( groundColor );
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.groundColor.copy( source.groundColor );
-
- return this;
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.object.groundColor = this.groundColor.getHex();
-
- return data;
-
- }
-
-}
-
-const _projScreenMatrix = /*@__PURE__*/ new Matrix4();
-const _lightPositionWorld = /*@__PURE__*/ new Vector3();
-const _lookTarget = /*@__PURE__*/ new Vector3();
-
-/**
- * Abstract base class for light shadow classes. These classes
- * represent the shadow configuration for different light types.
- *
- * @abstract
- */
-class LightShadow {
-
- /**
- * Constructs a new light shadow.
- *
- * @param {Camera} camera - The light's view of the world.
- */
- constructor( camera ) {
-
- /**
- * The light's view of the world.
- *
- * @type {Camera}
- */
- this.camera = camera;
-
- /**
- * The intensity of the shadow. The default is `1`.
- * Valid values are in the range `[0, 1]`.
- *
- * @type {number}
- * @default 1
- */
- this.intensity = 1;
-
- /**
- * Shadow map bias, how much to add or subtract from the normalized depth
- * when deciding whether a surface is in shadow.
- *
- * The default is `0`. Very tiny adjustments here (in the order of `0.0001`)
- * may help reduce artifacts in shadows.
- *
- * @type {number}
- * @default 0
- */
- this.bias = 0;
-
- /**
- * A node version of `bias`. Only supported with `WebGPURenderer`.
- *
- * If a bias node is defined, `bias` has no effect.
- *
- * @type {?Node}
- * @default null
- */
- this.biasNode = null;
-
- /**
- * Defines how much the position used to query the shadow map is offset along
- * the object normal. The default is `0`. Increasing this value can be used to
- * reduce shadow acne especially in large scenes where light shines onto
- * geometry at a shallow angle. The cost is that shadows may appear distorted.
- *
- * @type {number}
- * @default 0
- */
- this.normalBias = 0;
-
- /**
- * Setting this to values greater than 1 will blur the edges of the shadow.
- * High values will cause unwanted banding effects in the shadows - a greater
- * map size will allow for a higher value to be used here before these effects
- * become visible.
- *
- * The property has no effect when the shadow map type is `BasicShadowMap`.
- *
- * @type {number}
- * @default 1
- */
- this.radius = 1;
-
- /**
- * The amount of samples to use when blurring a VSM shadow map.
- *
- * @type {number}
- * @default 8
- */
- this.blurSamples = 8;
-
- /**
- * Defines the width and height of the shadow map. Higher values give better quality
- * shadows at the cost of computation time. Values must be powers of two.
- *
- * @type {Vector2}
- * @default (512,512)
- */
- this.mapSize = new Vector2( 512, 512 );
-
- /**
- * The type of shadow texture. The default is `UnsignedByteType`.
- *
- * @type {number}
- * @default UnsignedByteType
- */
- this.mapType = UnsignedByteType;
-
- /**
- * The depth map generated using the internal camera; a location beyond a
- * pixel's depth is in shadow. Computed internally during rendering.
- *
- * @type {?RenderTarget}
- * @default null
- */
- this.map = null;
-
- /**
- * The distribution map generated using the internal camera; an occlusion is
- * calculated based on the distribution of depths. Computed internally during
- * rendering.
- *
- * @type {?RenderTarget}
- * @default null
- */
- this.mapPass = null;
-
- /**
- * Model to shadow camera space, to compute location and depth in shadow map.
- * This is computed internally during rendering.
- *
- * @type {Matrix4}
- */
- this.matrix = new Matrix4();
-
- /**
- * Enables automatic updates of the light's shadow. If you do not require dynamic
- * lighting / shadows, you may set this to `false`.
- *
- * @type {boolean}
- * @default true
- */
- this.autoUpdate = true;
-
- /**
- * When set to `true`, shadow maps will be updated in the next `render` call.
- * If you have set {@link LightShadow#autoUpdate} to `false`, you will need to
- * set this property to `true` and then make a render call to update the light's shadow.
- *
- * @type {boolean}
- * @default false
- */
- this.needsUpdate = false;
-
- this._frustum = new Frustum();
- this._frameExtents = new Vector2( 1, 1 );
-
- this._viewportCount = 1;
-
- this._viewports = [
-
- new Vector4( 0, 0, 1, 1 )
-
- ];
-
- }
-
- /**
- * Used internally by the renderer to get the number of viewports that need
- * to be rendered for this shadow.
- *
- * @return {number} The viewport count.
- */
- getViewportCount() {
-
- return this._viewportCount;
-
- }
-
- /**
- * Used internally by the renderer to get the camera that renders the given viewport.
- *
- * @param {number} [viewportIndex=0] - The viewport index.
- * @return {Camera} The shadow camera.
- */
- getCamera( /* viewportIndex */ ) {
-
- return this.camera;
-
- }
-
- /**
- * Gets the shadow cameras frustum. Used internally by the renderer to cull objects.
- *
- * @return {Frustum} The shadow camera frustum.
- */
- getFrustum() {
-
- return this._frustum;
-
- }
-
- /**
- * Update the matrices for the camera and shadow, used internally by the renderer.
- *
- * @param {Light} light - The light for which the shadow is being rendered.
- */
- updateMatrices( light ) {
-
- const shadowCamera = this.camera;
- _lightPositionWorld.setFromMatrixPosition( light.matrixWorld );
- shadowCamera.position.copy( _lightPositionWorld );
-
- _lookTarget.setFromMatrixPosition( light.target.matrixWorld );
- shadowCamera.lookAt( _lookTarget );
- shadowCamera.updateMatrixWorld();
- this._updateMatrix( shadowCamera, this.matrix, this._frustum );
-
- }
-
- /**
- * Updates a shadow projection matrix and its corresponding frustum.
- *
- * @private
- * @param {Camera} shadowCamera - The shadow camera.
- * @param {Matrix4} shadowMatrix - The target shadow matrix.
- * @param {Frustum} frustum - The target frustum.
- * @param {Vector4} [viewport] - The viewport within the shadow atlas.
- */
- _updateMatrix( shadowCamera, shadowMatrix, frustum, viewport ) {
-
- _projScreenMatrix.multiplyMatrices( shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse );
- frustum.setFromProjectionMatrix( _projScreenMatrix, shadowCamera.coordinateSystem, shadowCamera.reversedDepth );
-
- const frameExtents = this._frameExtents;
- const scaleX = viewport ? viewport.z / frameExtents.x : 1;
- const scaleY = viewport ? viewport.w / frameExtents.y : 1;
- const offsetX = viewport ? viewport.x / frameExtents.x : 0;
- const offsetY = viewport ? viewport.y / frameExtents.y : 0;
-
- if ( shadowCamera.coordinateSystem === WebGPUCoordinateSystem || shadowCamera.reversedDepth ) {
-
- shadowMatrix.set(
- 0.5 * scaleX, 0.0, 0.0, 0.5 * scaleX + offsetX,
- 0.0, 0.5 * scaleY, 0.0, 0.5 * scaleY + offsetY,
- 0.0, 0.0, 1.0, 0.0, // Identity Z (preserving the correct [0, 1] range from the projection matrix)
- 0.0, 0.0, 0.0, 1.0
- );
-
- } else {
-
- shadowMatrix.set(
- 0.5 * scaleX, 0.0, 0.0, 0.5 * scaleX + offsetX,
- 0.0, 0.5 * scaleY, 0.0, 0.5 * scaleY + offsetY,
- 0.0, 0.0, 0.5, 0.5,
- 0.0, 0.0, 0.0, 1.0
- );
-
- }
-
- shadowMatrix.multiply( _projScreenMatrix );
-
- }
-
- /**
- * Returns a viewport definition for the given viewport index.
- *
- * @param {number} viewportIndex - The viewport index.
- * @return {Vector4} The viewport.
- */
- getViewport( viewportIndex ) {
-
- return this._viewports[ viewportIndex ];
-
- }
-
- /**
- * Returns the frame extends.
- *
- * @return {Vector2} The frame extends.
- */
- getFrameExtents() {
-
- return this._frameExtents;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- if ( this.map ) {
-
- this.map.dispose();
-
- }
-
- if ( this.mapPass ) {
-
- this.mapPass.dispose();
-
- }
-
- }
-
- /**
- * Copies the values of the given light shadow instance to this instance.
- *
- * @param {LightShadow} source - The light shadow to copy.
- * @return {LightShadow} A reference to this light shadow instance.
- */
- copy( source ) {
-
- this.camera = source.camera.clone();
-
- this.intensity = source.intensity;
-
- this.bias = source.bias;
- this.radius = source.radius;
-
- this.autoUpdate = source.autoUpdate;
- this.needsUpdate = source.needsUpdate;
- this.normalBias = source.normalBias;
- this.blurSamples = source.blurSamples;
-
- this.mapSize.copy( source.mapSize );
-
- this.biasNode = source.biasNode;
-
- return this;
-
- }
-
- /**
- * Returns a new light shadow instance with copied values from this instance.
- *
- * @return {LightShadow} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Serializes the light shadow into JSON.
- *
- * @return {Object} A JSON object representing the serialized light shadow.
- * @see {@link ObjectLoader#parse}
- */
- toJSON() {
-
- const object = {};
-
- object.intensity = this.intensity;
- object.bias = this.bias;
- object.normalBias = this.normalBias;
- object.radius = this.radius;
- object.blurSamples = this.blurSamples;
- object.mapSize = this.mapSize.toArray();
-
- object.camera = this.camera.toJSON( false ).object;
- delete object.camera.matrix;
-
- return object;
-
- }
-
-}
-
-const _position$2 = /*@__PURE__*/ new Vector3();
-const _quaternion$2 = /*@__PURE__*/ new Quaternion();
-const _scale$2 = /*@__PURE__*/ new Vector3();
-
-/**
- * Abstract base class for cameras. This class should always be inherited
- * when you build a new camera.
- *
- * @abstract
- * @augments Object3D
- */
-class Camera extends Object3D {
-
- /**
- * Constructs a new camera.
- */
- constructor() {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isCamera = true;
-
- this.type = 'Camera';
-
- /**
- * The inverse of the camera's world matrix.
- *
- * @type {Matrix4}
- */
- this.matrixWorldInverse = new Matrix4();
-
- /**
- * The camera's projection matrix.
- *
- * @type {Matrix4}
- */
- this.projectionMatrix = new Matrix4();
-
- /**
- * The inverse of the camera's projection matrix.
- *
- * @type {Matrix4}
- */
- this.projectionMatrixInverse = new Matrix4();
-
- /**
- * The coordinate system in which the camera is used.
- *
- * @type {(WebGLCoordinateSystem|WebGPUCoordinateSystem)}
- */
- this.coordinateSystem = WebGLCoordinateSystem;
-
- this._reversedDepth = false;
-
- }
-
- /**
- * The flag that indicates whether the camera uses a reversed depth buffer.
- *
- * @type {boolean}
- * @default false
- */
- get reversedDepth() {
-
- return this._reversedDepth;
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.matrixWorldInverse.copy( source.matrixWorldInverse );
-
- this.projectionMatrix.copy( source.projectionMatrix );
- this.projectionMatrixInverse.copy( source.projectionMatrixInverse );
-
- this.coordinateSystem = source.coordinateSystem;
-
- return this;
-
- }
-
- /**
- * Returns a vector representing the ("look") direction of the 3D object in world space.
- *
- * This method is overwritten since cameras have a different forward vector compared to other
- * 3D objects. A camera looks down its local, negative z-axis by default.
- *
- * @param {Vector3} target - The target vector the result is stored to.
- * @return {Vector3} The 3D object's direction in world space.
- */
- getWorldDirection( target ) {
-
- return super.getWorldDirection( target ).negate();
-
- }
-
- updateMatrixWorld( force ) {
-
- super.updateMatrixWorld( force );
-
- // exclude scale from view matrix to be glTF conform
-
- this.matrixWorld.decompose( _position$2, _quaternion$2, _scale$2 );
-
- if ( _scale$2.x === 1 && _scale$2.y === 1 && _scale$2.z === 1 ) {
-
- this.matrixWorldInverse.copy( this.matrixWorld ).invert();
-
- } else {
-
- this.matrixWorldInverse.compose( _position$2, _quaternion$2, _scale$2.set( 1, 1, 1 ) ).invert();
-
- }
-
- }
-
- updateWorldMatrix( updateParents, updateChildren, force = false ) {
-
- super.updateWorldMatrix( updateParents, updateChildren, force );
-
- // exclude scale from view matrix to be glTF conform
-
- this.matrixWorld.decompose( _position$2, _quaternion$2, _scale$2 );
-
- if ( _scale$2.x === 1 && _scale$2.y === 1 && _scale$2.z === 1 ) {
-
- this.matrixWorldInverse.copy( this.matrixWorld ).invert();
-
- } else {
-
- this.matrixWorldInverse.compose( _position$2, _quaternion$2, _scale$2.set( 1, 1, 1 ) ).invert();
-
- }
-
- }
-
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
-}
-
-const _v3$1 = /*@__PURE__*/ new Vector3();
-const _minTarget = /*@__PURE__*/ new Vector2();
-const _maxTarget = /*@__PURE__*/ new Vector2();
-
-/**
- * Camera that uses [perspective projection](https://en.wikipedia.org/wiki/Perspective_(graphical)).
- *
- * This projection mode is designed to mimic the way the human eye sees. It
- * is the most common projection mode used for rendering a 3D scene.
- *
- * ```js
- * const camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );
- * scene.add( camera );
- * ```
- *
- * @augments Camera
- */
-class PerspectiveCamera extends Camera {
-
- /**
- * Constructs a new perspective camera.
- *
- * @param {number} [fov=50] - The vertical field of view.
- * @param {number} [aspect=1] - The aspect ratio.
- * @param {number} [near=0.1] - The camera's near plane.
- * @param {number} [far=2000] - The camera's far plane.
- */
- constructor( fov = 50, aspect = 1, near = 0.1, far = 2000 ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isPerspectiveCamera = true;
-
- this.type = 'PerspectiveCamera';
-
- /**
- * The vertical field of view, from bottom to top of view,
- * in degrees.
- *
- * @type {number}
- * @default 50
- */
- this.fov = fov;
-
- /**
- * The zoom factor of the camera.
- *
- * @type {number}
- * @default 1
- */
- this.zoom = 1;
-
- /**
- * The camera's near plane. The valid range is greater than `0`
- * and less than the current value of {@link PerspectiveCamera#far}.
- *
- * Note that, unlike for the {@link OrthographicCamera}, `0` is not a
- * valid value for a perspective camera's near plane.
- *
- * @type {number}
- * @default 0.1
- */
- this.near = near;
-
- /**
- * The camera's far plane. Must be greater than the
- * current value of {@link PerspectiveCamera#near}.
- *
- * @type {number}
- * @default 2000
- */
- this.far = far;
-
- /**
- * Object distance used for stereoscopy and depth-of-field effects. This
- * parameter does not influence the projection matrix unless a
- * {@link StereoCamera} is being used.
- *
- * @type {number}
- * @default 10
- */
- this.focus = 10;
-
- /**
- * The aspect ratio, usually the canvas width / canvas height.
- *
- * @type {number}
- * @default 1
- */
- this.aspect = aspect;
-
- /**
- * Represents the frustum window specification. This property should not be edited
- * directly but via {@link PerspectiveCamera#setViewOffset} and {@link PerspectiveCamera#clearViewOffset}.
- *
- * @type {?Object}
- * @default null
- */
- this.view = null;
-
- /**
- * Film size used for the larger axis. Default is `35` (millimeters). This
- * parameter does not influence the projection matrix unless {@link PerspectiveCamera#filmOffset}
- * is set to a nonzero value.
- *
- * @type {number}
- * @default 35
- */
- this.filmGauge = 35;
-
- /**
- * Horizontal off-center offset in the same unit as {@link PerspectiveCamera#filmGauge}.
- *
- * @type {number}
- * @default 0
- */
- this.filmOffset = 0;
-
- this.updateProjectionMatrix();
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.fov = source.fov;
- this.zoom = source.zoom;
-
- this.near = source.near;
- this.far = source.far;
- this.focus = source.focus;
-
- this.aspect = source.aspect;
- this.view = source.view === null ? null : Object.assign( {}, source.view );
-
- this.filmGauge = source.filmGauge;
- this.filmOffset = source.filmOffset;
-
- return this;
-
- }
-
- /**
- * Sets the FOV by focal length in respect to the current {@link PerspectiveCamera#filmGauge}.
- *
- * The default film gauge is 35, so that the focal length can be specified for
- * a 35mm (full frame) camera.
- *
- * @param {number} focalLength - Values for focal length and film gauge must have the same unit.
- */
- setFocalLength( focalLength ) {
-
- /** see {@link http://www.bobatkins.com/photography/technical/field_of_view.html} */
- const vExtentSlope = 0.5 * this.getFilmHeight() / focalLength;
-
- this.fov = RAD2DEG * 2 * Math.atan( vExtentSlope );
- this.updateProjectionMatrix();
-
- }
-
- /**
- * Returns the focal length from the current {@link PerspectiveCamera#fov} and
- * {@link PerspectiveCamera#filmGauge}.
- *
- * @return {number} The computed focal length.
- */
- getFocalLength() {
-
- const vExtentSlope = Math.tan( DEG2RAD * 0.5 * this.fov );
-
- return 0.5 * this.getFilmHeight() / vExtentSlope;
-
- }
-
- /**
- * Returns the current vertical field of view angle in degrees considering {@link PerspectiveCamera#zoom}.
- *
- * @return {number} The effective FOV.
- */
- getEffectiveFOV() {
-
- return RAD2DEG * 2 * Math.atan(
- Math.tan( DEG2RAD * 0.5 * this.fov ) / this.zoom );
-
- }
-
- /**
- * Returns the width of the image on the film. If {@link PerspectiveCamera#aspect} is greater than or
- * equal to one (landscape format), the result equals {@link PerspectiveCamera#filmGauge}.
- *
- * @return {number} The film width.
- */
- getFilmWidth() {
-
- // film not completely covered in portrait format (aspect < 1)
- return this.filmGauge * Math.min( this.aspect, 1 );
-
- }
-
- /**
- * Returns the height of the image on the film. If {@link PerspectiveCamera#aspect} is greater than or
- * equal to one (landscape format), the result equals {@link PerspectiveCamera#filmGauge}.
- *
- * @return {number} The film width.
- */
- getFilmHeight() {
-
- // film not completely covered in landscape format (aspect > 1)
- return this.filmGauge / Math.max( this.aspect, 1 );
-
- }
-
- /**
- * Computes the 2D bounds of the camera's viewable rectangle at a given distance along the viewing direction.
- * Sets `minTarget` and `maxTarget` to the coordinates of the lower-left and upper-right corners of the view rectangle.
- *
- * @param {number} distance - The viewing distance.
- * @param {Vector2} minTarget - The lower-left corner of the view rectangle is written into this vector.
- * @param {Vector2} maxTarget - The upper-right corner of the view rectangle is written into this vector.
- */
- getViewBounds( distance, minTarget, maxTarget ) {
-
- _v3$1.set( -1, -1, 0.5 ).applyMatrix4( this.projectionMatrixInverse );
-
- minTarget.set( _v3$1.x, _v3$1.y ).multiplyScalar( - distance / _v3$1.z );
-
- _v3$1.set( 1, 1, 0.5 ).applyMatrix4( this.projectionMatrixInverse );
-
- maxTarget.set( _v3$1.x, _v3$1.y ).multiplyScalar( - distance / _v3$1.z );
-
- }
-
- /**
- * Computes the width and height of the camera's viewable rectangle at a given distance along the viewing direction.
- *
- * @param {number} distance - The viewing distance.
- * @param {Vector2} target - The target vector that is used to store result where x is width and y is height.
- * @returns {Vector2} The view size.
- */
- getViewSize( distance, target ) {
-
- this.getViewBounds( distance, _minTarget, _maxTarget );
-
- return target.subVectors( _maxTarget, _minTarget );
-
- }
-
- /**
- * Sets an offset in a larger frustum. This is useful for multi-window or
- * multi-monitor/multi-machine setups.
- *
- * For example, if you have 3x2 monitors and each monitor is 1920x1080 and
- * the monitors are in grid like this
- *```
- * +---+---+---+
- * | A | B | C |
- * +---+---+---+
- * | D | E | F |
- * +---+---+---+
- *```
- * then for each monitor you would call it like this:
- *```js
- * const w = 1920;
- * const h = 1080;
- * const fullWidth = w * 3;
- * const fullHeight = h * 2;
- *
- * // --A--
- * camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 0, w, h );
- * // --B--
- * camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 0, w, h );
- * // --C--
- * camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 0, w, h );
- * // --D--
- * camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 1, w, h );
- * // --E--
- * camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 1, w, h );
- * // --F--
- * camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 1, w, h );
- * ```
- *
- * Note there is no reason monitors have to be the same size or in a grid.
- *
- * @param {number} fullWidth - The full width of multiview setup.
- * @param {number} fullHeight - The full height of multiview setup.
- * @param {number} x - The horizontal offset of the subcamera.
- * @param {number} y - The vertical offset of the subcamera.
- * @param {number} width - The width of subcamera.
- * @param {number} height - The height of subcamera.
- */
- setViewOffset( fullWidth, fullHeight, x, y, width, height ) {
-
- this.aspect = fullWidth / fullHeight;
-
- if ( this.view === null ) {
-
- this.view = {
- enabled: true,
- fullWidth: 1,
- fullHeight: 1,
- offsetX: 0,
- offsetY: 0,
- width: 1,
- height: 1
- };
-
- }
-
- this.view.enabled = true;
- this.view.fullWidth = fullWidth;
- this.view.fullHeight = fullHeight;
- this.view.offsetX = x;
- this.view.offsetY = y;
- this.view.width = width;
- this.view.height = height;
-
- this.updateProjectionMatrix();
-
- }
-
- /**
- * Removes the view offset from the projection matrix.
- */
- clearViewOffset() {
-
- if ( this.view !== null ) {
-
- this.view.enabled = false;
-
- }
-
- this.updateProjectionMatrix();
-
- }
-
- /**
- * Updates the camera's projection matrix. Must be called after any change of
- * camera properties.
- */
- updateProjectionMatrix() {
-
- const near = this.near;
- let top = near * Math.tan( DEG2RAD * 0.5 * this.fov ) / this.zoom;
- let height = 2 * top;
- let width = this.aspect * height;
- let left = -0.5 * width;
- const view = this.view;
-
- if ( this.view !== null && this.view.enabled ) {
-
- const fullWidth = view.fullWidth,
- fullHeight = view.fullHeight;
-
- left += view.offsetX * width / fullWidth;
- top -= view.offsetY * height / fullHeight;
- width *= view.width / fullWidth;
- height *= view.height / fullHeight;
-
- }
-
- const skew = this.filmOffset;
- if ( skew !== 0 ) left += near * skew / this.getFilmWidth();
-
- this.projectionMatrix.makePerspective( left, left + width, top, top - height, near, this.far, this.coordinateSystem, this.reversedDepth );
-
- this.projectionMatrixInverse.copy( this.projectionMatrix ).invert();
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.object.fov = this.fov;
- data.object.zoom = this.zoom;
-
- data.object.near = this.near;
- data.object.far = this.far;
- data.object.focus = this.focus;
-
- data.object.aspect = this.aspect;
-
- if ( this.view !== null ) data.object.view = Object.assign( {}, this.view );
-
- data.object.filmGauge = this.filmGauge;
- data.object.filmOffset = this.filmOffset;
-
- return data;
-
- }
-
-}
-
-/**
- * Represents the shadow configuration of directional lights.
- *
- * @augments LightShadow
- */
-class SpotLightShadow extends LightShadow {
-
- /**
- * Constructs a new spot light shadow.
- */
- constructor() {
-
- super( new PerspectiveCamera( 50, 1, 0.5, 500 ) );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isSpotLightShadow = true;
-
- /**
- * Used to focus the shadow camera. The camera's field of view is set as a
- * percentage of the spotlight's field-of-view. Range is `[0, 1]`.
- *
- * @type {number}
- * @default 1
- */
- this.focus = 1;
-
- /**
- * Texture aspect ratio.
- *
- * @type {number}
- * @default 1
- */
- this.aspect = 1;
-
- }
-
- updateMatrices( light ) {
-
- const camera = this.camera;
-
- const fov = RAD2DEG * 2 * light.angle * this.focus;
- const aspect = ( this.mapSize.width / this.mapSize.height ) * this.aspect;
- const far = light.distance || camera.far;
-
- if ( fov !== camera.fov || aspect !== camera.aspect || far !== camera.far ) {
-
- camera.fov = fov;
- camera.aspect = aspect;
- camera.far = far;
- camera.updateProjectionMatrix();
-
- }
-
- super.updateMatrices( light );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.focus = source.focus;
- this.aspect = source.aspect;
-
- return this;
-
- }
-
- /**
- * Serializes the light shadow into JSON.
- *
- * @return {Object} A JSON object representing the serialized light shadow.
- * @see {@link ObjectLoader#parse}
- */
- toJSON() {
-
- const object = super.toJSON();
-
- object.focus = this.focus;
- object.aspect = this.aspect;
-
- return object;
-
- }
-
-}
-
-/**
- * This light gets emitted from a single point in one direction, along a cone
- * that increases in size the further from the light it gets.
- *
- * This light can cast shadows - see the {@link SpotLightShadow} for details.
- *
- * ```js
- * // white spotlight shining from the side, modulated by a texture
- * const spotLight = new THREE.SpotLight( 0xffffff );
- * spotLight.position.set( 100, 1000, 100 );
- * spotLight.map = new THREE.TextureLoader().load( url );
- *
- * spotLight.castShadow = true;
- * spotLight.shadow.mapSize.width = 1024;
- * spotLight.shadow.mapSize.height = 1024;
- * spotLight.shadow.camera.near = 500;
- * spotLight.shadow.camera.far = 4000;
- * spotLight.shadow.camera.fov = 30;s
- * ```
- *
- * @augments Light
- */
-class SpotLight extends Light {
-
- /**
- * Constructs a new spot light.
- *
- * @param {(number|Color|string)} [color=0xffffff] - The light's color.
- * @param {number} [intensity=1] - The light's strength/intensity measured in candela (cd).
- * @param {number} [distance=0] - Maximum range of the light. `0` means no limit.
- * @param {number} [angle=Math.PI/3] - Maximum angle of light dispersion from its direction whose upper bound is `Math.PI/2`.
- * @param {number} [penumbra=0] - Percent of the spotlight cone that is attenuated due to penumbra. Value range is `[0,1]`.
- * @param {number} [decay=2] - The amount the light dims along the distance of the light.
- */
- constructor( color, intensity, distance = 0, angle = Math.PI / 3, penumbra = 0, decay = 2 ) {
-
- super( color, intensity );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isSpotLight = true;
-
- this.type = 'SpotLight';
-
- this.position.copy( Object3D.DEFAULT_UP );
- this.updateMatrix();
-
- /**
- * The spot light points from its position to the
- * target's position.
- *
- * For the target's position to be changed to anything other
- * than the default, it must be added to the scene.
- *
- * It is also possible to set the target to be another 3D object
- * in the scene. The light will now track the target object.
- *
- * @type {Object3D}
- */
- this.target = new Object3D();
-
- /**
- * Maximum range of the light. `0` means no limit.
- *
- * @type {number}
- * @default 0
- */
- this.distance = distance;
-
- /**
- * Maximum angle of light dispersion from its direction whose upper bound is `Math.PI/2`.
- *
- * @type {number}
- * @default Math.PI/3
- */
- this.angle = angle;
-
- /**
- * Percent of the spotlight cone that is attenuated due to penumbra.
- * Value range is `[0,1]`.
- *
- * @type {number}
- * @default 0
- */
- this.penumbra = penumbra;
-
- /**
- * The amount the light dims along the distance of the light. In context of
- * physically-correct rendering the default value should not be changed.
- *
- * @type {number}
- * @default 2
- */
- this.decay = decay;
-
- /**
- * A texture used to modulate the color of the light. The spot light
- * color is mixed with the RGB value of this texture, with a ratio
- * corresponding to its alpha value. The cookie-like masking effect is
- * reproduced using pixel values (0, 0, 0, 1-cookie_value).
- *
- * *Warning*: This property is disabled if {@link Object3D#castShadow} is set to `false`.
- *
- * @type {?Texture}
- * @default null
- */
- this.map = null;
-
- /**
- * This property holds the light's shadow configuration.
- *
- * @type {SpotLightShadow}
- */
- this.shadow = new SpotLightShadow();
-
- }
-
- /**
- * The light's power. Power is the luminous power of the light measured in lumens (lm).
- * Changing the power will also change the light's intensity.
- *
- * @type {number}
- */
- get power() {
-
- // compute the light's luminous power (in lumens) from its intensity (in candela)
- // by convention for a spotlight, luminous power (lm) = π * luminous intensity (cd)
- return this.intensity * Math.PI;
-
- }
-
- set power( power ) {
-
- // set the light's intensity (in candela) from the desired luminous power (in lumens)
- this.intensity = power / Math.PI;
-
- }
-
- dispose() {
-
- super.dispose();
-
- this.shadow.dispose();
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.distance = source.distance;
- this.angle = source.angle;
- this.penumbra = source.penumbra;
- this.decay = source.decay;
-
- this.target = source.target.clone();
- this.map = source.map;
- this.shadow = source.shadow.clone();
-
- return this;
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.object.distance = this.distance;
- data.object.angle = this.angle;
- data.object.decay = this.decay;
- data.object.penumbra = this.penumbra;
-
- data.object.target = this.target.uuid;
-
- if ( this.map && this.map.isTexture ) data.object.map = this.map.toJSON( meta ).uuid;
-
- data.object.shadow = this.shadow.toJSON();
-
- return data;
-
- }
-
-}
-
-/**
- * Represents the shadow configuration of point lights.
- *
- * @augments LightShadow
- */
-class PointLightShadow extends LightShadow {
-
- /**
- * Constructs a new point light shadow.
- */
- constructor() {
-
- super( new PerspectiveCamera( 90, 1, 0.5, 500 ) );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isPointLightShadow = true;
-
- }
-
-}
-
-/**
- * A light that gets emitted from a single point in all directions. A common
- * use case for this is to replicate the light emitted from a bare
- * lightbulb.
- *
- * This light can cast shadows - see the {@link PointLightShadow} for details.
- *
- * ```js
- * const light = new THREE.PointLight( 0xff0000, 1, 100 );
- * light.position.set( 50, 50, 50 );
- * scene.add( light );
- * ```
- *
- * @augments Light
- */
-class PointLight extends Light {
-
- /**
- * Constructs a new point light.
- *
- * @param {(number|Color|string)} [color=0xffffff] - The light's color.
- * @param {number} [intensity=1] - The light's strength/intensity measured in candela (cd).
- * @param {number} [distance=0] - Maximum range of the light. `0` means no limit.
- * @param {number} [decay=2] - The amount the light dims along the distance of the light.
- */
- constructor( color, intensity, distance = 0, decay = 2 ) {
-
- super( color, intensity );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isPointLight = true;
-
- this.type = 'PointLight';
-
- /**
- * When distance is zero, light will attenuate according to inverse-square
- * law to infinite distance. When distance is non-zero, light will attenuate
- * according to inverse-square law until near the distance cutoff, where it
- * will then attenuate quickly and smoothly to 0. Inherently, cutoffs are not
- * physically correct.
- *
- * @type {number}
- * @default 0
- */
- this.distance = distance;
-
- /**
- * The amount the light dims along the distance of the light. In context of
- * physically-correct rendering the default value should not be changed.
- *
- * @type {number}
- * @default 2
- */
- this.decay = decay;
-
- /**
- * This property holds the light's shadow configuration.
- *
- * @type {PointLightShadow}
- */
- this.shadow = new PointLightShadow();
-
- }
-
- /**
- * The light's power. Power is the luminous power of the light measured in lumens (lm).
- * Changing the power will also change the light's intensity.
- *
- * @type {number}
- */
- get power() {
-
- // compute the light's luminous power (in lumens) from its intensity (in candela)
- // for an isotropic light source, luminous power (lm) = 4 π luminous intensity (cd)
- return this.intensity * 4 * Math.PI;
-
- }
-
- set power( power ) {
-
- // set the light's intensity (in candela) from the desired luminous power (in lumens)
- this.intensity = power / ( 4 * Math.PI );
-
- }
-
- dispose() {
-
- super.dispose();
-
- this.shadow.dispose();
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.distance = source.distance;
- this.decay = source.decay;
-
- this.shadow = source.shadow.clone();
-
- return this;
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.object.distance = this.distance;
- data.object.decay = this.decay;
-
- data.object.shadow = this.shadow.toJSON();
-
- return data;
-
- }
-
-}
-
-/**
- * Camera that uses [orthographic projection](https://en.wikipedia.org/wiki/Orthographic_projection).
- *
- * In this projection mode, an object's size in the rendered image stays
- * constant regardless of its distance from the camera. This can be useful
- * for rendering 2D scenes and UI elements, amongst other things.
- *
- * ```js
- * const camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );
- * scene.add( camera );
- * ```
- *
- * @augments Camera
- */
-class OrthographicCamera extends Camera {
-
- /**
- * Constructs a new orthographic camera.
- *
- * @param {number} [left=-1] - The left plane of the camera's frustum.
- * @param {number} [right=1] - The right plane of the camera's frustum.
- * @param {number} [top=1] - The top plane of the camera's frustum.
- * @param {number} [bottom=-1] - The bottom plane of the camera's frustum.
- * @param {number} [near=0.1] - The camera's near plane.
- * @param {number} [far=2000] - The camera's far plane.
- */
- constructor( left = -1, right = 1, top = 1, bottom = -1, near = 0.1, far = 2000 ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isOrthographicCamera = true;
-
- this.type = 'OrthographicCamera';
-
- /**
- * The zoom factor of the camera.
- *
- * @type {number}
- * @default 1
- */
- this.zoom = 1;
-
- /**
- * Represents the frustum window specification. This property should not be edited
- * directly but via {@link PerspectiveCamera#setViewOffset} and {@link PerspectiveCamera#clearViewOffset}.
- *
- * @type {?Object}
- * @default null
- */
- this.view = null;
-
- /**
- * The left plane of the camera's frustum.
- *
- * @type {number}
- * @default -1
- */
- this.left = left;
-
- /**
- * The right plane of the camera's frustum.
- *
- * @type {number}
- * @default 1
- */
- this.right = right;
-
- /**
- * The top plane of the camera's frustum.
- *
- * @type {number}
- * @default 1
- */
- this.top = top;
-
- /**
- * The bottom plane of the camera's frustum.
- *
- * @type {number}
- * @default -1
- */
- this.bottom = bottom;
-
- /**
- * The camera's near plane. The valid range is greater than `0`
- * and less than the current value of {@link OrthographicCamera#far}.
- *
- * Note that, unlike for the {@link PerspectiveCamera}, `0` is a
- * valid value for an orthographic camera's near plane.
- *
- * @type {number}
- * @default 0.1
- */
- this.near = near;
-
- /**
- * The camera's far plane. Must be greater than the
- * current value of {@link OrthographicCamera#near}.
- *
- * @type {number}
- * @default 2000
- */
- this.far = far;
-
- this.updateProjectionMatrix();
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.left = source.left;
- this.right = source.right;
- this.top = source.top;
- this.bottom = source.bottom;
- this.near = source.near;
- this.far = source.far;
-
- this.zoom = source.zoom;
- this.view = source.view === null ? null : Object.assign( {}, source.view );
-
- return this;
-
- }
-
- /**
- * Sets an offset in a larger frustum. This is useful for multi-window or
- * multi-monitor/multi-machine setups.
- *
- * @param {number} fullWidth - The full width of multiview setup.
- * @param {number} fullHeight - The full height of multiview setup.
- * @param {number} x - The horizontal offset of the subcamera.
- * @param {number} y - The vertical offset of the subcamera.
- * @param {number} width - The width of subcamera.
- * @param {number} height - The height of subcamera.
- * @see {@link PerspectiveCamera#setViewOffset}
- */
- setViewOffset( fullWidth, fullHeight, x, y, width, height ) {
-
- if ( this.view === null ) {
-
- this.view = {
- enabled: true,
- fullWidth: 1,
- fullHeight: 1,
- offsetX: 0,
- offsetY: 0,
- width: 1,
- height: 1
- };
-
- }
-
- this.view.enabled = true;
- this.view.fullWidth = fullWidth;
- this.view.fullHeight = fullHeight;
- this.view.offsetX = x;
- this.view.offsetY = y;
- this.view.width = width;
- this.view.height = height;
-
- this.updateProjectionMatrix();
-
- }
-
- /**
- * Removes the view offset from the projection matrix.
- */
- clearViewOffset() {
-
- if ( this.view !== null ) {
-
- this.view.enabled = false;
-
- }
-
- this.updateProjectionMatrix();
-
- }
-
- /**
- * Updates the camera's projection matrix. Must be called after any change of
- * camera properties.
- */
- updateProjectionMatrix() {
-
- const dx = ( this.right - this.left ) / ( 2 * this.zoom );
- const dy = ( this.top - this.bottom ) / ( 2 * this.zoom );
- const cx = ( this.right + this.left ) / 2;
- const cy = ( this.top + this.bottom ) / 2;
-
- let left = cx - dx;
- let right = cx + dx;
- let top = cy + dy;
- let bottom = cy - dy;
-
- if ( this.view !== null && this.view.enabled ) {
-
- const scaleW = ( this.right - this.left ) / this.view.fullWidth / this.zoom;
- const scaleH = ( this.top - this.bottom ) / this.view.fullHeight / this.zoom;
-
- left += scaleW * this.view.offsetX;
- right = left + scaleW * this.view.width;
- top -= scaleH * this.view.offsetY;
- bottom = top - scaleH * this.view.height;
-
- }
-
- this.projectionMatrix.makeOrthographic( left, right, top, bottom, this.near, this.far, this.coordinateSystem, this.reversedDepth );
-
- this.projectionMatrixInverse.copy( this.projectionMatrix ).invert();
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.object.zoom = this.zoom;
- data.object.left = this.left;
- data.object.right = this.right;
- data.object.top = this.top;
- data.object.bottom = this.bottom;
- data.object.near = this.near;
- data.object.far = this.far;
-
- if ( this.view !== null ) data.object.view = Object.assign( {}, this.view );
-
- return data;
-
- }
-
-}
-
-/**
- * Represents the shadow configuration of directional lights.
- *
- * @augments LightShadow
- */
-class DirectionalLightShadow extends LightShadow {
-
- /**
- * Constructs a new directional light shadow.
- */
- constructor() {
-
- super( new OrthographicCamera( -5, 5, 5, -5, 0.5, 500 ) );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isDirectionalLightShadow = true;
-
- }
-
-}
-
-/**
- * A light that gets emitted in a specific direction. This light will behave
- * as though it is infinitely far away and the rays produced from it are all
- * parallel. The common use case for this is to simulate daylight; the sun is
- * far enough away that its position can be considered to be infinite, and
- * all light rays coming from it are parallel.
- *
- * A common point of confusion for directional lights is that setting the
- * rotation has no effect. This is because three.js's DirectionalLight is the
- * equivalent to what is often called a 'Target Direct Light' in other
- * applications.
- *
- * This means that its direction is calculated as pointing from the light's
- * {@link Object3D#position} to the {@link DirectionalLight#target} position
- * (as opposed to a 'Free Direct Light' that just has a rotation
- * component).
- *
- * This light can cast shadows - see the {@link DirectionalLightShadow} for details.
- *
- * ```js
- * // White directional light at half intensity shining from the top.
- * const directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
- * scene.add( directionalLight );
- * ```
- *
- * @augments Light
- */
-class DirectionalLight extends Light {
-
- /**
- * Constructs a new directional light.
- *
- * @param {(number|Color|string)} [color=0xffffff] - The light's color.
- * @param {number} [intensity=1] - The light's strength/intensity.
- */
- constructor( color, intensity ) {
-
- super( color, intensity );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isDirectionalLight = true;
-
- this.type = 'DirectionalLight';
-
- this.position.copy( Object3D.DEFAULT_UP );
- this.updateMatrix();
-
- /**
- * The directional light points from its position to the
- * target's position.
- *
- * For the target's position to be changed to anything other
- * than the default, it must be added to the scene.
- *
- * It is also possible to set the target to be another 3D object
- * in the scene. The light will now track the target object.
- *
- * @type {Object3D}
- */
- this.target = new Object3D();
-
- /**
- * This property holds the light's shadow configuration.
- *
- * @type {DirectionalLightShadow}
- */
- this.shadow = new DirectionalLightShadow();
-
- }
-
- dispose() {
-
- super.dispose();
-
- this.shadow.dispose();
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.target = source.target.clone();
- this.shadow = source.shadow.clone();
-
- return this;
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.object.shadow = this.shadow.toJSON();
- data.object.target = this.target.uuid;
-
- return data;
-
- }
-
-}
-
-/**
- * This light globally illuminates all objects in the scene equally.
- *
- * It cannot be used to cast shadows as it does not have a direction.
- *
- * ```js
- * const light = new THREE.AmbientLight( 0x404040 ); // soft white light
- * scene.add( light );
- * ```
- *
- * @augments Light
- */
-class AmbientLight extends Light {
-
- /**
- * Constructs a new ambient light.
- *
- * @param {(number|Color|string)} [color=0xffffff] - The light's color.
- * @param {number} [intensity=1] - The light's strength/intensity.
- */
- constructor( color, intensity ) {
-
- super( color, intensity );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isAmbientLight = true;
-
- this.type = 'AmbientLight';
-
- }
-
-}
-
-/**
- * This class emits light uniformly across the face a rectangular plane.
- * This light type can be used to simulate light sources such as bright
- * windows or strip lighting.
- *
- * Important Notes:
- *
- * - There is no shadow support.
- * - Only PBR materials are supported.
- * - You have to include `RectAreaLightUniformsLib` (`WebGLRenderer`) or `RectAreaLightTexturesLib` (`WebGPURenderer`)
- * into your app and init the uniforms/textures.
- *
- * ```js
- * RectAreaLightUniformsLib.init(); // only relevant for WebGLRenderer
- * THREE.RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() ); // only relevant for WebGPURenderer
- *
- * const intensity = 1; const width = 10; const height = 10;
- * const rectLight = new THREE.RectAreaLight( 0xffffff, intensity, width, height );
- * rectLight.position.set( 5, 5, 0 );
- * rectLight.lookAt( 0, 0, 0 );
- * scene.add( rectLight )
- * ```
- *
- * @augments Light
- */
-class RectAreaLight extends Light {
-
- /**
- * Constructs a new area light.
- *
- * @param {(number|Color|string)} [color=0xffffff] - The light's color.
- * @param {number} [intensity=1] - The light's strength/intensity.
- * @param {number} [width=10] - The width of the light.
- * @param {number} [height=10] - The height of the light.
- */
- constructor( color, intensity, width = 10, height = 10 ) {
-
- super( color, intensity );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isRectAreaLight = true;
-
- this.type = 'RectAreaLight';
-
- /**
- * The width of the light.
- *
- * @type {number}
- * @default 10
- */
- this.width = width;
-
- /**
- * The height of the light.
- *
- * @type {number}
- * @default 10
- */
- this.height = height;
-
- }
-
- /**
- * The light's power. Power is the luminous power of the light measured in lumens (lm).
- * Changing the power will also change the light's intensity.
- *
- * @type {number}
- */
- get power() {
-
- // compute the light's luminous power (in lumens) from its intensity (in nits)
- return this.intensity * this.width * this.height * Math.PI;
-
- }
-
- set power( power ) {
-
- // set the light's intensity (in nits) from the desired luminous power (in lumens)
- this.intensity = power / ( this.width * this.height * Math.PI );
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.width = source.width;
- this.height = source.height;
-
- return this;
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.object.width = this.width;
- data.object.height = this.height;
-
- return data;
-
- }
-
-}
-
-/**
- * Represents a third-order spherical harmonics (SH). Light probes use this class
- * to encode lighting information.
- *
- * - Primary reference: {@link https://graphics.stanford.edu/papers/envmap/envmap.pdf}
- * - Secondary reference: {@link https://www.ppsloan.org/publications/StupidSH36.pdf}
- */
-class SphericalHarmonics3 {
-
- /**
- * Constructs a new spherical harmonics.
- */
- constructor() {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isSphericalHarmonics3 = true;
-
- /**
- * An array holding the (9) SH coefficients.
- *
- * @type {Array}
- */
- this.coefficients = [];
-
- for ( let i = 0; i < 9; i ++ ) {
-
- this.coefficients.push( new Vector3() );
-
- }
-
- }
-
- /**
- * Sets the given SH coefficients to this instance by copying
- * the values.
- *
- * @param {Array} coefficients - The SH coefficients.
- * @return {SphericalHarmonics3} A reference to this spherical harmonics.
- */
- set( coefficients ) {
-
- for ( let i = 0; i < 9; i ++ ) {
-
- this.coefficients[ i ].copy( coefficients[ i ] );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets all SH coefficients to `0`.
- *
- * @return {SphericalHarmonics3} A reference to this spherical harmonics.
- */
- zero() {
-
- for ( let i = 0; i < 9; i ++ ) {
-
- this.coefficients[ i ].set( 0, 0, 0 );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns the radiance in the direction of the given normal.
- *
- * @param {Vector3} normal - The normal vector (assumed to be unit length)
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The radiance.
- */
- getAt( normal, target ) {
-
- // normal is assumed to be unit length
-
- const x = normal.x, y = normal.y, z = normal.z;
-
- const coeff = this.coefficients;
-
- // band 0
- target.copy( coeff[ 0 ] ).multiplyScalar( 0.282095 );
-
- // band 1
- target.addScaledVector( coeff[ 1 ], 0.488603 * y );
- target.addScaledVector( coeff[ 2 ], 0.488603 * z );
- target.addScaledVector( coeff[ 3 ], 0.488603 * x );
-
- // band 2
- target.addScaledVector( coeff[ 4 ], 1.092548 * ( x * y ) );
- target.addScaledVector( coeff[ 5 ], 1.092548 * ( y * z ) );
- target.addScaledVector( coeff[ 6 ], 0.315392 * ( 3.0 * z * z - 1.0 ) );
- target.addScaledVector( coeff[ 7 ], 1.092548 * ( x * z ) );
- target.addScaledVector( coeff[ 8 ], 0.546274 * ( x * x - y * y ) );
-
- return target;
-
- }
-
- /**
- * Returns the irradiance (radiance convolved with cosine lobe) in the
- * direction of the given normal.
- *
- * @param {Vector3} normal - The normal vector (assumed to be unit length)
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The irradiance.
- */
- getIrradianceAt( normal, target ) {
-
- // normal is assumed to be unit length
-
- const x = normal.x, y = normal.y, z = normal.z;
-
- const coeff = this.coefficients;
-
- // band 0
- target.copy( coeff[ 0 ] ).multiplyScalar( 0.886227 ); // π * 0.282095
-
- // band 1
- target.addScaledVector( coeff[ 1 ], 2.0 * 0.511664 * y ); // ( 2 * π / 3 ) * 0.488603
- target.addScaledVector( coeff[ 2 ], 2.0 * 0.511664 * z );
- target.addScaledVector( coeff[ 3 ], 2.0 * 0.511664 * x );
-
- // band 2
- target.addScaledVector( coeff[ 4 ], 2.0 * 0.429043 * x * y ); // ( π / 4 ) * 1.092548
- target.addScaledVector( coeff[ 5 ], 2.0 * 0.429043 * y * z );
- target.addScaledVector( coeff[ 6 ], 0.743125 * z * z - 0.247708 ); // ( π / 4 ) * 0.315392 * 3
- target.addScaledVector( coeff[ 7 ], 2.0 * 0.429043 * x * z );
- target.addScaledVector( coeff[ 8 ], 0.429043 * ( x * x - y * y ) ); // ( π / 4 ) * 0.546274
-
- return target;
-
- }
-
- /**
- * Adds the given SH to this instance.
- *
- * @param {SphericalHarmonics3} sh - The SH to add.
- * @return {SphericalHarmonics3} A reference to this spherical harmonics.
- */
- add( sh ) {
-
- for ( let i = 0; i < 9; i ++ ) {
-
- this.coefficients[ i ].add( sh.coefficients[ i ] );
-
- }
-
- return this;
-
- }
-
- /**
- * A convenience method for performing {@link SphericalHarmonics3#add} and
- * {@link SphericalHarmonics3#scale} at once.
- *
- * @param {SphericalHarmonics3} sh - The SH to add.
- * @param {number} s - The scale factor.
- * @return {SphericalHarmonics3} A reference to this spherical harmonics.
- */
- addScaledSH( sh, s ) {
-
- for ( let i = 0; i < 9; i ++ ) {
-
- this.coefficients[ i ].addScaledVector( sh.coefficients[ i ], s );
-
- }
-
- return this;
-
- }
-
- /**
- * Scales this SH by the given scale factor.
- *
- * @param {number} s - The scale factor.
- * @return {SphericalHarmonics3} A reference to this spherical harmonics.
- */
- scale( s ) {
-
- for ( let i = 0; i < 9; i ++ ) {
-
- this.coefficients[ i ].multiplyScalar( s );
-
- }
-
- return this;
-
- }
-
- /**
- * Linear interpolates between the given SH and this instance by the given
- * alpha factor.
- *
- * @param {SphericalHarmonics3} sh - The SH to interpolate with.
- * @param {number} alpha - The alpha factor.
- * @return {SphericalHarmonics3} A reference to this spherical harmonics.
- */
- lerp( sh, alpha ) {
-
- for ( let i = 0; i < 9; i ++ ) {
-
- this.coefficients[ i ].lerp( sh.coefficients[ i ], alpha );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns `true` if this spherical harmonics is equal with the given one.
- *
- * @param {SphericalHarmonics3} sh - The spherical harmonics to test for equality.
- * @return {boolean} Whether this spherical harmonics is equal with the given one.
- */
- equals( sh ) {
-
- for ( let i = 0; i < 9; i ++ ) {
-
- if ( ! this.coefficients[ i ].equals( sh.coefficients[ i ] ) ) {
-
- return false;
-
- }
-
- }
-
- return true;
-
- }
-
- /**
- * Copies the values of the given spherical harmonics to this instance.
- *
- * @param {SphericalHarmonics3} sh - The spherical harmonics to copy.
- * @return {SphericalHarmonics3} A reference to this spherical harmonics.
- */
- copy( sh ) {
-
- return this.set( sh.coefficients );
-
- }
-
- /**
- * Returns a new spherical harmonics with copied values from this instance.
- *
- * @return {SphericalHarmonics3} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Sets the SH coefficients of this instance from the given array.
- *
- * @param {Array} array - An array holding the SH coefficients.
- * @param {number} [offset=0] - The array offset where to start copying.
- * @return {SphericalHarmonics3} A clone of this instance.
- */
- fromArray( array, offset = 0 ) {
-
- const coefficients = this.coefficients;
-
- for ( let i = 0; i < 9; i ++ ) {
-
- coefficients[ i ].fromArray( array, offset + ( i * 3 ) );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns an array with the SH coefficients, or copies them into the provided
- * array. The coefficients are represented as numbers.
- *
- * @param {Array} [array=[]] - The target array.
- * @param {number} [offset=0] - The array offset where to start copying.
- * @return {Array} An array with flat SH coefficients.
- */
- toArray( array = [], offset = 0 ) {
-
- const coefficients = this.coefficients;
-
- for ( let i = 0; i < 9; i ++ ) {
-
- coefficients[ i ].toArray( array, offset + ( i * 3 ) );
-
- }
-
- return array;
-
- }
-
- /**
- * Computes the SH basis for the given normal vector.
- *
- * @param {Vector3} normal - The normal.
- * @param {Array} shBasis - The target array holding the SH basis.
- */
- static getBasisAt( normal, shBasis ) {
-
- // normal is assumed to be unit length
-
- const x = normal.x, y = normal.y, z = normal.z;
-
- // band 0
- shBasis[ 0 ] = 0.282095;
-
- // band 1
- shBasis[ 1 ] = 0.488603 * y;
- shBasis[ 2 ] = 0.488603 * z;
- shBasis[ 3 ] = 0.488603 * x;
-
- // band 2
- shBasis[ 4 ] = 1.092548 * x * y;
- shBasis[ 5 ] = 1.092548 * y * z;
- shBasis[ 6 ] = 0.315392 * ( 3 * z * z - 1 );
- shBasis[ 7 ] = 1.092548 * x * z;
- shBasis[ 8 ] = 0.546274 * ( x * x - y * y );
-
- }
-
-}
-
-/**
- * Light probes are an alternative way of adding light to a 3D scene. Unlike
- * classical light sources (e.g. directional, point or spot lights), light
- * probes do not emit light. Instead they store information about light
- * passing through 3D space. During rendering, the light that hits a 3D
- * object is approximated by using the data from the light probe.
- *
- * Light probes are usually created from (radiance) environment maps. The
- * class {@link LightProbeGenerator} can be used to create light probes from
- * cube textures or render targets. However, light estimation data could also
- * be provided in other forms e.g. by WebXR. This enables the rendering of
- * augmented reality content that reacts to real world lighting.
- *
- * The current probe implementation in three.js supports so-called diffuse
- * light probes. This type of light probe is functionally equivalent to an
- * irradiance environment map.
- *
- * @augments Light
- */
-class LightProbe extends Light {
-
- /**
- * Constructs a new light probe.
- *
- * @param {SphericalHarmonics3} sh - The spherical harmonics which represents encoded lighting information.
- * @param {number} [intensity=1] - The light's strength/intensity.
- */
- constructor( sh = new SphericalHarmonics3(), intensity = 1 ) {
-
- super( undefined, intensity );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isLightProbe = true;
-
- /**
- * A light probe uses spherical harmonics to encode lighting information.
- *
- * @type {SphericalHarmonics3}
- */
- this.sh = sh;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.sh.copy( source.sh );
-
- return this;
-
- }
-
- toJSON( meta ) {
-
- const data = super.toJSON( meta );
-
- data.object.sh = this.sh.toArray();
-
- return data;
-
- }
-
-}
-
-const _customMaterials = {};
-
-/**
- * Class for loading materials. The files are internally
- * loaded via {@link FileLoader}.
- *
- * ```js
- * const loader = new THREE.MaterialLoader();
- * const material = await loader.loadAsync( 'material.json' );
- * ```
- * This loader does not support node materials. Use {@link NodeMaterialLoader} instead.
- *
- * @augments Loader
- */
-class MaterialLoader extends Loader {
-
- /**
- * Constructs a new material loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- /**
- * A dictionary holding textures used by the material.
- *
- * @type {Object}
- */
- this.textures = {};
-
- }
-
- /**
- * Starts loading from the given URL and pass the loaded material to the `onLoad()` callback.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(Material)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
- * @param {onErrorCallback} onError - Executed when errors occur.
- */
- load( url, onLoad, onProgress, onError ) {
-
- const scope = this;
-
- const loader = new FileLoader( scope.manager );
- loader.setPath( scope.path );
- loader.setRequestHeader( scope.requestHeader );
- loader.setWithCredentials( scope.withCredentials );
- loader.load( url, function ( text ) {
-
- try {
-
- onLoad( scope.parse( JSON.parse( text ) ) );
-
- } catch ( e ) {
-
- if ( onError ) {
-
- onError( e );
-
- } else {
-
- error( e );
-
- }
-
- scope.manager.itemError( url );
-
- }
-
- }, onProgress, onError );
-
- }
-
- /**
- * Parses the given JSON object and returns a material.
- *
- * @param {Object} json - The serialized material.
- * @return {Material} The parsed material.
- */
- parse( json ) {
-
- const material = this.createMaterialFromType( json.type );
-
- material.fromJSON( json, this.textures );
-
- return material;
-
- }
-
- /**
- * Textures are not embedded in the material JSON so they have
- * to be injected before the loading process starts.
- *
- * @param {Object} value - A dictionary holding textures for material properties.
- * @return {MaterialLoader} A reference to this material loader.
- */
- setTextures( value ) {
-
- this.textures = value;
- return this;
-
- }
-
- /**
- * Creates a material for the given type.
- *
- * @param {string} type - The material type.
- * @return {Material} The new material.
- */
- createMaterialFromType( type ) {
-
- return MaterialLoader.createMaterialFromType( type );
-
- }
-
- /**
- * Creates a material for the given type.
- *
- * @static
- * @param {string} type - The material type.
- * @return {Material} The new material.
- */
- static createMaterialFromType( type ) {
-
- const materialLib = {
- ShadowMaterial,
- SpriteMaterial,
- RawShaderMaterial,
- ShaderMaterial,
- PointsMaterial,
- MeshPhysicalMaterial,
- MeshStandardMaterial,
- MeshPhongMaterial,
- MeshToonMaterial,
- MeshNormalMaterial,
- MeshLambertMaterial,
- MeshDepthMaterial,
- MeshDistanceMaterial,
- MeshBasicMaterial,
- MeshMatcapMaterial,
- LineDashedMaterial,
- LineBasicMaterial,
- Material,
- ... _customMaterials
- };
-
- const MaterialType = materialLib[ type ];
-
- let materialInstance;
-
- if ( MaterialType === undefined ) {
-
- warnOnce( `MaterialLoader: Unknown material type "${ type }". Use .registerMaterial() before starting the deserialization process.` );
- materialInstance = new Material();
-
- } else {
-
- materialInstance = new MaterialType();
-
- }
-
- return materialInstance;
-
- }
-
- /**
- * Registers the given material at the internal
- * material library.
- *
- * @static
- * @param {string} type - The material type.
- * @param {Material.constructor} materialClass - The material class.
- */
- static registerMaterial( type, materialClass ) {
-
- _customMaterials[ type ] = materialClass;
-
- }
-
-}
-
-/**
- * A class with loader utility functions.
- */
-class LoaderUtils {
-
- /**
- * Extracts the base URL from the given URL.
- *
- * @param {string} url -The URL to extract the base URL from.
- * @return {string} The extracted base URL.
- */
- static extractUrlBase( url ) {
-
- const index = url.lastIndexOf( '/' );
-
- if ( index === -1 ) return './';
-
- return url.slice( 0, index + 1 );
-
- }
-
- /**
- * Resolves relative URLs against the given path. Absolute paths, data urls,
- * and blob URLs will be returned as is. Invalid URLs will return an empty
- * string.
- *
- * @param {string} url -The URL to resolve.
- * @param {string} path - The base path for relative URLs to be resolved against.
- * @return {string} The resolved URL.
- */
- static resolveURL( url, path ) {
-
- // Invalid URL
- if ( typeof url !== 'string' || url === '' ) return '';
-
- // Host Relative URL
- if ( /^https?:\/\//i.test( path ) && /^\//.test( url ) ) {
-
- path = path.replace( /(^https?:\/\/[^\/]+).*/i, '$1' );
-
- }
-
- // Absolute URL http://,https://,//
- if ( /^(https?:)?\/\//i.test( url ) ) return url;
-
- // Data URI
- if ( /^data:.*,.*$/i.test( url ) ) return url;
-
- // Blob URL
- if ( /^blob:.*$/i.test( url ) ) return url;
-
- // Relative URL
- return path + url;
-
- }
-
-}
-
-/**
- * An instanced version of a geometry.
- */
-class InstancedBufferGeometry extends BufferGeometry {
-
- /**
- * Constructs a new instanced buffer geometry.
- */
- constructor() {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isInstancedBufferGeometry = true;
-
- this.type = 'InstancedBufferGeometry';
-
- /**
- * The instance count.
- *
- * @type {number}
- * @default Infinity
- */
- this.instanceCount = Infinity;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.instanceCount = source.instanceCount;
-
- return this;
-
- }
-
- toJSON() {
-
- const data = super.toJSON();
-
- data.instanceCount = this.instanceCount;
-
- data.isInstancedBufferGeometry = true;
-
- return data;
-
- }
-
-}
-
-/**
- * Class for loading geometries. The files are internally
- * loaded via {@link FileLoader}.
- *
- * ```js
- * const loader = new THREE.BufferGeometryLoader();
- * const geometry = await loader.loadAsync( 'models/json/pressure.json' );
- *
- * const material = new THREE.MeshBasicMaterial( { color: 0xF5F5F5 } );
- * const object = new THREE.Mesh( geometry, material );
- * scene.add( object );
- * ```
- *
- * @augments Loader
- */
-class BufferGeometryLoader extends Loader {
-
- /**
- * Constructs a new geometry loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- }
-
- /**
- * Starts loading from the given URL and pass the loaded geometry to the `onLoad()` callback.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(BufferGeometry)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
- * @param {onErrorCallback} onError - Executed when errors occur.
- */
- load( url, onLoad, onProgress, onError ) {
-
- const scope = this;
-
- const loader = new FileLoader( scope.manager );
- loader.setPath( scope.path );
- loader.setRequestHeader( scope.requestHeader );
- loader.setWithCredentials( scope.withCredentials );
- loader.load( url, function ( text ) {
-
- try {
-
- onLoad( scope.parse( JSON.parse( text ) ) );
-
- } catch ( e ) {
-
- if ( onError ) {
-
- onError( e );
-
- } else {
-
- error( e );
-
- }
-
- scope.manager.itemError( url );
-
- }
-
- }, onProgress, onError );
-
- }
-
- /**
- * Parses the given JSON object and returns a geometry.
- *
- * @param {Object} json - The serialized geometry.
- * @return {BufferGeometry} The parsed geometry.
- */
- parse( json ) {
-
- const interleavedBufferMap = {};
- const arrayBufferMap = {};
-
- function getInterleavedBuffer( json, uuid ) {
-
- if ( interleavedBufferMap[ uuid ] !== undefined ) return interleavedBufferMap[ uuid ];
-
- const interleavedBuffers = json.interleavedBuffers;
- const interleavedBuffer = interleavedBuffers[ uuid ];
-
- const buffer = getArrayBuffer( json, interleavedBuffer.buffer );
-
- const array = getTypedArray( interleavedBuffer.type, buffer );
- const ib = new InterleavedBuffer( array, interleavedBuffer.stride );
- ib.uuid = interleavedBuffer.uuid;
-
- if ( interleavedBuffer.usage !== undefined ) ib.setUsage( interleavedBuffer.usage );
-
- interleavedBufferMap[ uuid ] = ib;
-
- return ib;
-
- }
-
- function getArrayBuffer( json, uuid ) {
-
- if ( arrayBufferMap[ uuid ] !== undefined ) return arrayBufferMap[ uuid ];
-
- const arrayBuffers = json.arrayBuffers;
- const arrayBuffer = arrayBuffers[ uuid ];
-
- const ab = new Uint32Array( arrayBuffer ).buffer;
-
- arrayBufferMap[ uuid ] = ab;
-
- return ab;
-
- }
-
- const geometry = json.isInstancedBufferGeometry ? new InstancedBufferGeometry() : new BufferGeometry();
-
- const index = json.data.index;
-
- if ( index !== undefined ) {
-
- const typedArray = getTypedArray( index.type, index.array );
- geometry.setIndex( new BufferAttribute( typedArray, 1 ) );
-
- }
-
- const attributes = json.data.attributes;
-
- for ( const key in attributes ) {
-
- const attribute = attributes[ key ];
- let bufferAttribute;
-
- if ( attribute.isInterleavedBufferAttribute ) {
-
- const interleavedBuffer = getInterleavedBuffer( json.data, attribute.data );
- bufferAttribute = new InterleavedBufferAttribute( interleavedBuffer, attribute.itemSize, attribute.offset, attribute.normalized );
-
- } else {
-
- const typedArray = getTypedArray( attribute.type, attribute.array );
- const bufferAttributeConstr = attribute.isInstancedBufferAttribute ? InstancedBufferAttribute : BufferAttribute;
- bufferAttribute = new bufferAttributeConstr( typedArray, attribute.itemSize, attribute.normalized );
-
- }
-
- if ( attribute.name !== undefined ) bufferAttribute.name = attribute.name;
- if ( attribute.usage !== undefined ) bufferAttribute.setUsage( attribute.usage );
- if ( attribute.gpuType !== undefined ) bufferAttribute.gpuType = attribute.gpuType;
-
- geometry.setAttribute( key, bufferAttribute );
-
- }
-
- const morphAttributes = json.data.morphAttributes;
-
- if ( morphAttributes ) {
-
- for ( const key in morphAttributes ) {
-
- const attributeArray = morphAttributes[ key ];
-
- const array = [];
-
- for ( let i = 0, il = attributeArray.length; i < il; i ++ ) {
-
- const attribute = attributeArray[ i ];
- let bufferAttribute;
-
- if ( attribute.isInterleavedBufferAttribute ) {
-
- const interleavedBuffer = getInterleavedBuffer( json.data, attribute.data );
- bufferAttribute = new InterleavedBufferAttribute( interleavedBuffer, attribute.itemSize, attribute.offset, attribute.normalized );
-
- } else {
-
- const typedArray = getTypedArray( attribute.type, attribute.array );
- bufferAttribute = new BufferAttribute( typedArray, attribute.itemSize, attribute.normalized );
-
- }
-
- if ( attribute.name !== undefined ) bufferAttribute.name = attribute.name;
- if ( attribute.usage !== undefined ) bufferAttribute.setUsage( attribute.usage );
- if ( attribute.gpuType !== undefined ) bufferAttribute.gpuType = attribute.gpuType;
- array.push( bufferAttribute );
-
- }
-
- geometry.morphAttributes[ key ] = array;
-
- }
-
- }
-
- const morphTargetsRelative = json.data.morphTargetsRelative;
-
- if ( morphTargetsRelative ) {
-
- geometry.morphTargetsRelative = true;
-
- }
-
- const groups = json.data.groups || json.data.drawcalls || json.data.offsets;
-
- if ( groups !== undefined ) {
-
- for ( let i = 0, n = groups.length; i !== n; ++ i ) {
-
- const group = groups[ i ];
-
- geometry.addGroup( group.start, group.count, group.materialIndex );
-
- }
-
- }
-
- const boundingSphere = json.data.boundingSphere;
-
- if ( boundingSphere !== undefined ) {
-
- geometry.boundingSphere = new Sphere().fromJSON( boundingSphere );
-
- }
-
- if ( json.name ) geometry.name = json.name;
- if ( json.userData ) geometry.userData = json.userData;
-
- return geometry;
-
- }
-
-}
-
-const _customGeometries = {};
-
-/**
- * A loader for loading a JSON resource in the [JSON Object/Scene format](https://github.com/mrdoob/three.js/wiki/JSON-Object-Scene-format-4).
- * The files are internally loaded via {@link FileLoader}.
- *
- * ```js
- * const loader = new THREE.ObjectLoader();
- * const obj = await loader.loadAsync( 'models/json/example.json' );
- * scene.add( obj );
- *
- * // Alternatively, to parse a previously loaded JSON structure
- * const object = await loader.parseAsync( a_json_object );
- * scene.add( object );
- * ```
- *
- * @augments Loader
- */
-class ObjectLoader extends Loader {
-
- /**
- * Constructs a new object loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- }
-
- /**
- * Starts loading from the given URL and pass the loaded 3D object to the `onLoad()` callback.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(Object3D)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
- * @param {onErrorCallback} onError - Executed when errors occur.
- */
- load( url, onLoad, onProgress, onError ) {
-
- const scope = this;
-
- const path = ( this.path === '' ) ? LoaderUtils.extractUrlBase( url ) : this.path;
- this.resourcePath = this.resourcePath || path;
-
- const loader = new FileLoader( this.manager );
- loader.setPath( this.path );
- loader.setRequestHeader( this.requestHeader );
- loader.setWithCredentials( this.withCredentials );
- loader.load( url, function ( text ) {
-
- let json = null;
-
- try {
-
- json = JSON.parse( text );
-
- } catch ( e ) {
-
- if ( onError !== undefined ) onError( e );
-
- error( 'ObjectLoader: Can\'t parse ' + url + '.', e.message );
-
- return;
-
- }
-
- const metadata = json.metadata;
-
- if ( metadata === undefined || metadata.type === undefined || metadata.type.toLowerCase() === 'geometry' ) {
-
- if ( onError !== undefined ) onError( new Error( 'THREE.ObjectLoader: Can\'t load ' + url ) );
-
- error( 'ObjectLoader: Can\'t load ' + url );
- return;
-
- }
-
- scope.parse( json, onLoad );
-
- }, onProgress, onError );
-
- }
-
- /**
- * Async version of {@link ObjectLoader#load}.
- *
- * @async
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
- * @return {Promise} A Promise that resolves with the loaded 3D object.
- */
- async loadAsync( url, onProgress ) {
-
- const scope = this;
-
- const path = ( this.path === '' ) ? LoaderUtils.extractUrlBase( url ) : this.path;
- this.resourcePath = this.resourcePath || path;
-
- const loader = new FileLoader( this.manager );
- loader.setPath( this.path );
- loader.setRequestHeader( this.requestHeader );
- loader.setWithCredentials( this.withCredentials );
-
- const text = await loader.loadAsync( url, onProgress );
-
- let json;
-
- try {
-
- json = JSON.parse( text );
-
- } catch ( e ) {
-
- throw new Error( 'THREE.ObjectLoader: Can\'t parse ' + url + '. ' + e.message );
-
- }
-
- const metadata = json.metadata;
-
- if ( metadata === undefined || metadata.type === undefined || metadata.type.toLowerCase() === 'geometry' ) {
-
- throw new Error( 'THREE.ObjectLoader: Can\'t load ' + url );
-
- }
-
- return await scope.parseAsync( json );
-
- }
-
- /**
- * Parses the given JSON. This is used internally by {@link ObjectLoader#load}
- * but can also be used directly to parse a previously loaded JSON structure.
- *
- * @param {Object} json - The serialized 3D object.
- * @param {onLoad} onLoad - Executed when all resources (e.g. textures) have been fully loaded.
- * @return {Object3D} The parsed 3D object.
- */
- parse( json, onLoad ) {
-
- const animations = this.parseAnimations( json.animations );
- const shapes = this.parseShapes( json.shapes );
- const geometries = this.parseGeometries( json.geometries, shapes );
-
- const images = this.parseImages( json.images, function () {
-
- if ( onLoad !== undefined ) onLoad( object );
-
- } );
-
- const textures = this.parseTextures( json.textures, images );
- const materials = this.parseMaterials( json.materials, textures );
-
- const object = this.parseObject( json.object, geometries, materials, textures, animations );
- const skeletons = this.parseSkeletons( json.skeletons, object );
-
- this.bindSkeletons( object, skeletons );
- this.bindLightTargets( object );
-
- //
-
- if ( onLoad !== undefined ) {
-
- let hasImages = false;
-
- for ( const uuid in images ) {
-
- if ( images[ uuid ].data instanceof HTMLImageElement ) {
-
- hasImages = true;
- break;
-
- }
-
- }
-
- if ( hasImages === false ) onLoad( object );
-
- }
-
- return object;
-
- }
-
- /**
- * Async version of {@link ObjectLoader#parse}.
- *
- * @param {Object} json - The serialized 3D object.
- * @return {Promise} A Promise that resolves with the parsed 3D object.
- */
- async parseAsync( json ) {
-
- const animations = this.parseAnimations( json.animations );
- const shapes = this.parseShapes( json.shapes );
- const geometries = this.parseGeometries( json.geometries, shapes );
-
- const images = await this.parseImagesAsync( json.images );
-
- const textures = this.parseTextures( json.textures, images );
- const materials = this.parseMaterials( json.materials, textures );
-
- const object = this.parseObject( json.object, geometries, materials, textures, animations );
- const skeletons = this.parseSkeletons( json.skeletons, object );
-
- this.bindSkeletons( object, skeletons );
- this.bindLightTargets( object );
-
- return object;
-
- }
-
- /**
- * Registers the given geometry at the internal
- * geometry library.
- *
- * @static
- * @param {string} type - The geometry type.
- * @param {BufferGeometry.constructor} geometryClass - The geometry class.
- */
- static registerGeometry( type, geometryClass ) {
-
- _customGeometries[ type ] = geometryClass;
-
- }
-
- // internals
-
- parseShapes( json ) {
-
- const shapes = {};
-
- if ( json !== undefined ) {
-
- for ( let i = 0, l = json.length; i < l; i ++ ) {
-
- const shape = new Shape().fromJSON( json[ i ] );
-
- shapes[ shape.uuid ] = shape;
-
- }
-
- }
-
- return shapes;
-
- }
-
- parseSkeletons( json, object ) {
-
- const skeletons = {};
- const bones = {};
-
- // generate bone lookup table
-
- object.traverse( function ( child ) {
-
- if ( child.isBone ) bones[ child.uuid ] = child;
-
- } );
-
- // create skeletons
-
- if ( json !== undefined ) {
-
- for ( let i = 0, l = json.length; i < l; i ++ ) {
-
- const skeleton = new Skeleton().fromJSON( json[ i ], bones );
-
- skeletons[ skeleton.uuid ] = skeleton;
-
- }
-
- }
-
- return skeletons;
-
- }
-
- parseGeometries( json, shapes ) {
-
- const geometries = {};
-
- if ( json !== undefined ) {
-
- const bufferGeometryLoader = new BufferGeometryLoader();
-
- for ( let i = 0, l = json.length; i < l; i ++ ) {
-
- let geometry;
- const data = json[ i ];
-
- switch ( data.type ) {
-
- case 'BufferGeometry':
- case 'InstancedBufferGeometry':
-
- geometry = bufferGeometryLoader.parse( data );
- break;
-
- default:
-
- if ( data.type in Geometries ) {
-
- geometry = Geometries[ data.type ].fromJSON( data, shapes );
-
- } else if ( data.type in _customGeometries ) {
-
- geometry = _customGeometries[ data.type ].fromJSON( data, shapes );
-
- } else {
-
- warn( `ObjectLoader: Unknown geometry type "${ data.type }". Use .registerGeometry() before starting the deserialization process.` );
-
- }
-
- }
-
- geometry.uuid = data.uuid;
-
- if ( data.name !== undefined ) geometry.name = data.name;
- if ( data.userData !== undefined ) geometry.userData = data.userData;
-
- geometries[ data.uuid ] = geometry;
-
- }
-
- }
-
- return geometries;
-
- }
-
- parseMaterials( json, textures ) {
-
- const cache = {}; // MultiMaterial
- const materials = {};
-
- if ( json !== undefined ) {
-
- const loader = new MaterialLoader();
- loader.setTextures( textures );
-
- for ( let i = 0, l = json.length; i < l; i ++ ) {
-
- const data = json[ i ];
-
- if ( cache[ data.uuid ] === undefined ) {
-
- cache[ data.uuid ] = loader.parse( data );
-
- }
-
- materials[ data.uuid ] = cache[ data.uuid ];
-
- }
-
- }
-
- return materials;
-
- }
-
- parseAnimations( json ) {
-
- const animations = {};
-
- if ( json !== undefined ) {
-
- for ( let i = 0; i < json.length; i ++ ) {
-
- const data = json[ i ];
-
- const clip = AnimationClip.parse( data );
-
- animations[ clip.uuid ] = clip;
-
- }
-
- }
-
- return animations;
-
- }
-
- parseImages( json, onLoad ) {
-
- const scope = this;
- const images = {};
-
- let loader;
-
- function loadImage( url ) {
-
- url = scope.manager.resolveURL( url );
-
- scope.manager.itemStart( url );
-
- return loader.load( url, function () {
-
- scope.manager.itemEnd( url );
-
- }, undefined, function () {
-
- scope.manager.itemError( url );
- scope.manager.itemEnd( url );
-
- } );
-
- }
-
- function deserializeImage( image ) {
-
- if ( typeof image === 'string' ) {
-
- const url = image;
-
- const path = /^(\/\/)|([a-z]+:(\/\/)?)/i.test( url ) ? url : scope.resourcePath + url;
-
- return loadImage( path );
-
- } else {
-
- if ( image.data ) {
-
- return {
- data: getTypedArray( image.type, image.data ),
- width: image.width,
- height: image.height
- };
-
- } else {
-
- return null;
-
- }
-
- }
-
- }
-
- if ( json !== undefined && json.length > 0 ) {
-
- const manager = new LoadingManager( onLoad );
-
- loader = new ImageLoader( manager );
- loader.setCrossOrigin( this.crossOrigin );
-
- for ( let i = 0, il = json.length; i < il; i ++ ) {
-
- const image = json[ i ];
- const url = image.url;
-
- if ( Array.isArray( url ) ) {
-
- // load array of images e.g CubeTexture
-
- const imageArray = [];
-
- for ( let j = 0, jl = url.length; j < jl; j ++ ) {
-
- const currentUrl = url[ j ];
-
- const deserializedImage = deserializeImage( currentUrl );
-
- if ( deserializedImage !== null ) {
-
- if ( deserializedImage instanceof HTMLImageElement ) {
-
- imageArray.push( deserializedImage );
-
- } else {
-
- // special case: handle array of data textures for cube textures
-
- imageArray.push( new DataTexture( deserializedImage.data, deserializedImage.width, deserializedImage.height ) );
-
- }
-
- }
-
- }
-
- images[ image.uuid ] = new TextureSource( imageArray );
-
- } else {
-
- // load single image
-
- const deserializedImage = deserializeImage( image.url );
- images[ image.uuid ] = new TextureSource( deserializedImage );
-
-
- }
-
- }
-
- }
-
- return images;
-
- }
-
- async parseImagesAsync( json ) {
-
- const scope = this;
- const images = {};
-
- let loader;
-
- async function deserializeImage( image ) {
-
- if ( typeof image === 'string' ) {
-
- const url = image;
-
- const path = /^(\/\/)|([a-z]+:(\/\/)?)/i.test( url ) ? url : scope.resourcePath + url;
-
- return await loader.loadAsync( path );
-
- } else {
-
- if ( image.data ) {
-
- return {
- data: getTypedArray( image.type, image.data ),
- width: image.width,
- height: image.height
- };
-
- } else {
-
- return null;
-
- }
-
- }
-
- }
-
- if ( json !== undefined && json.length > 0 ) {
-
- loader = new ImageLoader( this.manager );
- loader.setCrossOrigin( this.crossOrigin );
-
- for ( let i = 0, il = json.length; i < il; i ++ ) {
-
- const image = json[ i ];
- const url = image.url;
-
- if ( Array.isArray( url ) ) {
-
- // load array of images e.g CubeTexture
-
- const imageArray = [];
-
- for ( let j = 0, jl = url.length; j < jl; j ++ ) {
-
- const currentUrl = url[ j ];
-
- const deserializedImage = await deserializeImage( currentUrl );
-
- if ( deserializedImage !== null ) {
-
- if ( deserializedImage instanceof HTMLImageElement ) {
-
- imageArray.push( deserializedImage );
-
- } else {
-
- // special case: handle array of data textures for cube textures
-
- imageArray.push( new DataTexture( deserializedImage.data, deserializedImage.width, deserializedImage.height ) );
-
- }
-
- }
-
- }
-
- images[ image.uuid ] = new TextureSource( imageArray );
-
- } else {
-
- // load single image
-
- const deserializedImage = await deserializeImage( image.url );
- images[ image.uuid ] = new TextureSource( deserializedImage );
-
- }
-
- }
-
- }
-
- return images;
-
- }
-
- parseTextures( json, images ) {
-
- function parseConstant( value, type ) {
-
- if ( typeof value === 'number' ) return value;
-
- warn( 'ObjectLoader.parseTexture: Constant should be in numeric form.', value );
-
- return type[ value ];
-
- }
-
- const textures = {};
-
- if ( json !== undefined ) {
-
- for ( let i = 0, l = json.length; i < l; i ++ ) {
-
- const data = json[ i ];
-
- if ( data.image === undefined ) {
-
- warn( 'ObjectLoader: No "image" specified for', data.uuid );
-
- }
-
- if ( images[ data.image ] === undefined ) {
-
- warn( 'ObjectLoader: Undefined image', data.image );
-
- }
-
- const source = images[ data.image ];
- const image = source.data;
-
- let texture;
-
- if ( Array.isArray( image ) ) {
-
- texture = new CubeTexture();
-
- if ( image.length === 6 ) texture.needsUpdate = true;
-
- } else {
-
- if ( image && image.data ) {
-
- texture = new DataTexture();
-
- } else {
-
- texture = new Texture();
-
- }
-
- if ( image ) texture.needsUpdate = true; // textures can have undefined image data
-
- }
-
- texture.source = source;
-
- texture.uuid = data.uuid;
-
- if ( data.name !== undefined ) texture.name = data.name;
-
- if ( data.mapping !== undefined ) texture.mapping = parseConstant( data.mapping, TEXTURE_MAPPING );
- if ( data.channel !== undefined ) texture.channel = data.channel;
-
- if ( data.offset !== undefined ) texture.offset.fromArray( data.offset );
- if ( data.repeat !== undefined ) texture.repeat.fromArray( data.repeat );
- if ( data.center !== undefined ) texture.center.fromArray( data.center );
- if ( data.rotation !== undefined ) texture.rotation = data.rotation;
-
- if ( data.wrap !== undefined ) {
-
- texture.wrapS = parseConstant( data.wrap[ 0 ], TEXTURE_WRAPPING );
- texture.wrapT = parseConstant( data.wrap[ 1 ], TEXTURE_WRAPPING );
-
- }
-
- if ( data.format !== undefined ) texture.format = data.format;
- if ( data.internalFormat !== undefined ) texture.internalFormat = data.internalFormat;
- if ( data.type !== undefined ) texture.type = data.type;
- if ( data.colorSpace !== undefined ) texture.colorSpace = data.colorSpace;
-
- if ( data.minFilter !== undefined ) texture.minFilter = parseConstant( data.minFilter, TEXTURE_FILTER );
- if ( data.magFilter !== undefined ) texture.magFilter = parseConstant( data.magFilter, TEXTURE_FILTER );
- if ( data.anisotropy !== undefined ) texture.anisotropy = data.anisotropy;
-
- if ( data.flipY !== undefined ) texture.flipY = data.flipY;
-
- if ( data.generateMipmaps !== undefined ) texture.generateMipmaps = data.generateMipmaps;
- if ( data.premultiplyAlpha !== undefined ) texture.premultiplyAlpha = data.premultiplyAlpha;
- if ( data.unpackAlignment !== undefined ) texture.unpackAlignment = data.unpackAlignment;
- if ( data.compareFunction !== undefined ) texture.compareFunction = data.compareFunction;
- if ( data.normalized !== undefined ) texture.normalized = data.normalized;
-
- if ( data.userData !== undefined ) texture.userData = data.userData;
-
- textures[ data.uuid ] = texture;
-
- }
-
- }
-
- return textures;
-
- }
-
- parseObject( data, geometries, materials, textures, animations ) {
-
- let object;
-
- function getGeometry( name ) {
-
- if ( geometries[ name ] === undefined ) {
-
- warn( 'ObjectLoader: Undefined geometry', name );
-
- }
-
- return geometries[ name ];
-
- }
-
- function getMaterial( name ) {
-
- if ( name === undefined ) return undefined;
-
- if ( Array.isArray( name ) ) {
-
- const array = [];
-
- for ( let i = 0, l = name.length; i < l; i ++ ) {
-
- const uuid = name[ i ];
-
- if ( materials[ uuid ] === undefined ) {
-
- warn( 'ObjectLoader: Undefined material', uuid );
-
- }
-
- array.push( materials[ uuid ] );
-
- }
-
- return array;
-
- }
-
- if ( materials[ name ] === undefined ) {
-
- warn( 'ObjectLoader: Undefined material', name );
-
- }
-
- return materials[ name ];
-
- }
-
- function getTexture( uuid ) {
-
- if ( textures[ uuid ] === undefined ) {
-
- warn( 'ObjectLoader: Undefined texture', uuid );
-
- }
-
- return textures[ uuid ];
-
- }
-
- let geometry, material;
-
- switch ( data.type ) {
-
- case 'Scene':
-
- object = new Scene();
-
- if ( data.background !== undefined ) {
-
- if ( Number.isInteger( data.background ) ) {
-
- object.background = new Color( data.background );
-
- } else {
-
- object.background = getTexture( data.background );
-
- }
-
- }
-
- if ( data.environment !== undefined ) {
-
- object.environment = getTexture( data.environment );
-
- }
-
- if ( data.fog !== undefined ) {
-
- if ( data.fog.type === 'Fog' ) {
-
- object.fog = new Fog( data.fog.color, data.fog.near, data.fog.far );
-
- } else if ( data.fog.type === 'FogExp2' ) {
-
- object.fog = new FogExp2( data.fog.color, data.fog.density );
-
- }
-
- if ( data.fog.name !== '' ) {
-
- object.fog.name = data.fog.name;
-
- }
-
- }
-
- if ( data.backgroundBlurriness !== undefined ) object.backgroundBlurriness = data.backgroundBlurriness;
- if ( data.backgroundIntensity !== undefined ) object.backgroundIntensity = data.backgroundIntensity;
- if ( data.backgroundRotation !== undefined ) object.backgroundRotation.fromArray( data.backgroundRotation );
-
- if ( data.environmentIntensity !== undefined ) object.environmentIntensity = data.environmentIntensity;
- if ( data.environmentRotation !== undefined ) object.environmentRotation.fromArray( data.environmentRotation );
-
- break;
-
- case 'PerspectiveCamera':
-
- object = new PerspectiveCamera( data.fov, data.aspect, data.near, data.far );
-
- if ( data.focus !== undefined ) object.focus = data.focus;
- if ( data.zoom !== undefined ) object.zoom = data.zoom;
- if ( data.filmGauge !== undefined ) object.filmGauge = data.filmGauge;
- if ( data.filmOffset !== undefined ) object.filmOffset = data.filmOffset;
- if ( data.view !== undefined ) object.view = Object.assign( {}, data.view );
-
- break;
-
- case 'OrthographicCamera':
-
- object = new OrthographicCamera( data.left, data.right, data.top, data.bottom, data.near, data.far );
-
- if ( data.zoom !== undefined ) object.zoom = data.zoom;
- if ( data.view !== undefined ) object.view = Object.assign( {}, data.view );
-
- break;
-
- case 'AmbientLight':
-
- object = new AmbientLight( data.color, data.intensity );
-
- break;
-
- case 'DirectionalLight':
-
- object = new DirectionalLight( data.color, data.intensity );
- object.target = data.target || '';
-
- break;
-
- case 'PointLight':
-
- object = new PointLight( data.color, data.intensity, data.distance, data.decay );
-
- break;
-
- case 'RectAreaLight':
-
- object = new RectAreaLight( data.color, data.intensity, data.width, data.height );
-
- break;
-
- case 'SpotLight':
-
- object = new SpotLight( data.color, data.intensity, data.distance, data.angle, data.penumbra, data.decay );
- object.target = data.target || '';
-
- break;
-
- case 'HemisphereLight':
-
- object = new HemisphereLight( data.color, data.groundColor, data.intensity );
-
- break;
-
- case 'LightProbe':
-
- const sh = new SphericalHarmonics3().fromArray( data.sh );
- object = new LightProbe( sh, data.intensity );
-
- break;
-
- case 'SkinnedMesh':
-
- geometry = getGeometry( data.geometry );
- material = getMaterial( data.material );
-
- object = new SkinnedMesh( geometry, material );
-
- if ( data.bindMode !== undefined ) object.bindMode = data.bindMode;
- if ( data.bindMatrix !== undefined ) object.bindMatrix.fromArray( data.bindMatrix );
- if ( data.skeleton !== undefined ) object.skeleton = data.skeleton;
-
- break;
-
- case 'Mesh':
-
- geometry = getGeometry( data.geometry );
- material = getMaterial( data.material );
-
- object = new Mesh( geometry, material );
-
- break;
-
- case 'InstancedMesh':
-
- geometry = getGeometry( data.geometry );
- material = getMaterial( data.material );
- const count = data.count;
- const instanceMatrix = data.instanceMatrix;
- const instanceColor = data.instanceColor;
-
- object = new InstancedMesh( geometry, material, count );
- object.instanceMatrix = new InstancedBufferAttribute( new Float32Array( instanceMatrix.array ), 16 );
- if ( instanceColor !== undefined ) object.instanceColor = new InstancedBufferAttribute( new Float32Array( instanceColor.array ), instanceColor.itemSize );
-
- break;
-
- case 'BatchedMesh':
-
- geometry = getGeometry( data.geometry );
- material = getMaterial( data.material );
-
- object = new BatchedMesh( data.maxInstanceCount, data.maxVertexCount, data.maxIndexCount, material );
- object.geometry = geometry;
- object.perObjectFrustumCulled = data.perObjectFrustumCulled;
- object.sortObjects = data.sortObjects;
-
- object._drawRanges = data.drawRanges;
- object._reservedRanges = data.reservedRanges;
-
- object._geometryInfo = data.geometryInfo.map( info => {
-
- let box = null;
- let sphere = null;
- if ( info.boundingBox !== undefined ) {
-
- box = new Box3().fromJSON( info.boundingBox );
-
- }
-
- if ( info.boundingSphere !== undefined ) {
-
- sphere = new Sphere().fromJSON( info.boundingSphere );
-
- }
-
- return {
- ...info,
- boundingBox: box,
- boundingSphere: sphere
- };
-
- } );
- object._instanceInfo = data.instanceInfo;
-
- object._availableInstanceIds = data._availableInstanceIds;
- object._availableGeometryIds = data._availableGeometryIds;
-
- object._nextIndexStart = data.nextIndexStart;
- object._nextVertexStart = data.nextVertexStart;
- object._geometryCount = data.geometryCount;
-
- object._maxInstanceCount = data.maxInstanceCount;
- object._maxVertexCount = data.maxVertexCount;
- object._maxIndexCount = data.maxIndexCount;
-
- object._geometryInitialized = data.geometryInitialized;
-
- object._matricesTexture = getTexture( data.matricesTexture.uuid );
-
- object._indirectTexture = getTexture( data.indirectTexture.uuid );
-
- if ( data.colorsTexture !== undefined ) {
-
- object._colorsTexture = getTexture( data.colorsTexture.uuid );
-
- }
-
- if ( data.boundingSphere !== undefined ) {
-
- object.boundingSphere = new Sphere().fromJSON( data.boundingSphere );
-
- }
-
- if ( data.boundingBox !== undefined ) {
-
- object.boundingBox = new Box3().fromJSON( data.boundingBox );
-
- }
-
- break;
-
- case 'LOD':
-
- object = new LOD();
-
- break;
-
- case 'Line':
-
- object = new Line( getGeometry( data.geometry ), getMaterial( data.material ) );
-
- break;
-
- case 'LineLoop':
-
- object = new LineLoop( getGeometry( data.geometry ), getMaterial( data.material ) );
-
- break;
-
- case 'LineSegments':
-
- object = new LineSegments( getGeometry( data.geometry ), getMaterial( data.material ) );
-
- break;
-
- case 'PointCloud':
- case 'Points':
-
- object = new Points( getGeometry( data.geometry ), getMaterial( data.material ) );
-
- break;
-
- case 'Sprite':
-
- object = new Sprite( getMaterial( data.material ) );
-
- break;
-
- case 'Group':
-
- object = new Group();
-
- break;
-
- case 'Bone':
-
- object = new Bone();
-
- break;
-
- default:
-
- object = new Object3D();
-
- }
-
- object.uuid = data.uuid;
-
- if ( data.name !== undefined ) object.name = data.name;
-
- if ( data.matrix !== undefined ) {
-
- object.matrix.fromArray( data.matrix );
-
- if ( data.matrixAutoUpdate !== undefined ) object.matrixAutoUpdate = data.matrixAutoUpdate;
- if ( object.matrixAutoUpdate ) object.matrix.decompose( object.position, object.quaternion, object.scale );
-
- } else {
-
- if ( data.position !== undefined ) object.position.fromArray( data.position );
- if ( data.rotation !== undefined ) object.rotation.fromArray( data.rotation );
- if ( data.quaternion !== undefined ) object.quaternion.fromArray( data.quaternion );
- if ( data.scale !== undefined ) object.scale.fromArray( data.scale );
-
- }
-
- if ( data.up !== undefined ) object.up.fromArray( data.up );
-
- if ( data.pivot !== undefined ) object.pivot = new Vector3().fromArray( data.pivot );
-
- if ( data.morphTargetDictionary !== undefined ) object.morphTargetDictionary = Object.assign( {}, data.morphTargetDictionary );
- if ( data.morphTargetInfluences !== undefined ) object.morphTargetInfluences = data.morphTargetInfluences.slice();
-
- if ( data.castShadow !== undefined ) object.castShadow = data.castShadow;
- if ( data.receiveShadow !== undefined ) object.receiveShadow = data.receiveShadow;
-
- if ( data.shadow ) {
-
- if ( data.shadow.intensity !== undefined ) object.shadow.intensity = data.shadow.intensity;
- if ( data.shadow.bias !== undefined ) object.shadow.bias = data.shadow.bias;
- if ( data.shadow.normalBias !== undefined ) object.shadow.normalBias = data.shadow.normalBias;
- if ( data.shadow.radius !== undefined ) object.shadow.radius = data.shadow.radius;
- if ( data.shadow.blurSamples !== undefined ) object.shadow.blurSamples = data.shadow.blurSamples;
- if ( data.shadow.focus !== undefined ) object.shadow.focus = data.shadow.focus;
- if ( data.shadow.aspect !== undefined ) object.shadow.aspect = data.shadow.aspect;
- if ( data.shadow.mapSize !== undefined ) object.shadow.mapSize.fromArray( data.shadow.mapSize );
- if ( data.shadow.camera !== undefined ) object.shadow.camera = this.parseObject( data.shadow.camera );
-
- }
-
- if ( data.visible !== undefined ) object.visible = data.visible;
- if ( data.frustumCulled !== undefined ) object.frustumCulled = data.frustumCulled;
- if ( data.renderOrder !== undefined ) object.renderOrder = data.renderOrder;
- if ( data.static !== undefined ) object.static = data.static;
- if ( data.userData !== undefined ) object.userData = data.userData;
- if ( data.layers !== undefined ) object.layers.mask = data.layers;
-
- if ( data.children !== undefined ) {
-
- const children = data.children;
-
- for ( let i = 0; i < children.length; i ++ ) {
-
- object.add( this.parseObject( children[ i ], geometries, materials, textures, animations ) );
-
- }
-
- }
-
- if ( data.animations !== undefined ) {
-
- const objectAnimations = data.animations;
-
- for ( let i = 0; i < objectAnimations.length; i ++ ) {
-
- const uuid = objectAnimations[ i ];
-
- object.animations.push( animations[ uuid ] );
-
- }
-
- }
-
- if ( data.type === 'LOD' ) {
-
- if ( data.autoUpdate !== undefined ) object.autoUpdate = data.autoUpdate;
-
- const levels = data.levels;
-
- for ( let l = 0; l < levels.length; l ++ ) {
-
- const level = levels[ l ];
- const child = object.getObjectByProperty( 'uuid', level.object );
-
- if ( child !== undefined ) {
-
- object.addLevel( child, level.distance, level.hysteresis );
-
- }
-
- }
-
- }
-
- return object;
-
- }
-
- bindSkeletons( object, skeletons ) {
-
- if ( Object.keys( skeletons ).length === 0 ) return;
-
- object.traverse( function ( child ) {
-
- if ( child.isSkinnedMesh === true && child.skeleton !== undefined ) {
-
- const skeleton = skeletons[ child.skeleton ];
-
- if ( skeleton === undefined ) {
-
- warn( 'ObjectLoader: No skeleton found with UUID:', child.skeleton );
-
- } else {
-
- child.bind( skeleton, child.bindMatrix );
-
- }
-
- }
-
- } );
-
- }
-
- bindLightTargets( object ) {
-
- object.traverse( function ( child ) {
-
- if ( child.isDirectionalLight || child.isSpotLight ) {
-
- const uuid = child.target;
-
- const target = object.getObjectByProperty( 'uuid', uuid );
-
- if ( target !== undefined ) {
-
- child.target = target;
-
- } else {
-
- child.target = new Object3D();
-
- }
-
- }
-
- } );
-
- }
-
-}
-
-const TEXTURE_MAPPING = {
- UVMapping: UVMapping,
- CubeReflectionMapping: CubeReflectionMapping,
- CubeRefractionMapping: CubeRefractionMapping,
- EquirectangularReflectionMapping: EquirectangularReflectionMapping,
- EquirectangularRefractionMapping: EquirectangularRefractionMapping,
- CubeUVReflectionMapping: CubeUVReflectionMapping
-};
-
-const TEXTURE_WRAPPING = {
- RepeatWrapping: RepeatWrapping,
- ClampToEdgeWrapping: ClampToEdgeWrapping,
- MirroredRepeatWrapping: MirroredRepeatWrapping
-};
-
-const TEXTURE_FILTER = {
- NearestFilter: NearestFilter,
- NearestMipmapNearestFilter: NearestMipmapNearestFilter,
- NearestMipmapLinearFilter: NearestMipmapLinearFilter,
- LinearFilter: LinearFilter,
- LinearMipmapNearestFilter: LinearMipmapNearestFilter,
- LinearMipmapLinearFilter: LinearMipmapLinearFilter
-};
-
-const _errorMap = new WeakMap();
-
-/**
- * A loader for loading images as an [ImageBitmap](https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap).
- * An `ImageBitmap` provides an asynchronous and resource efficient pathway to prepare
- * textures for rendering.
- *
- * Note that {@link Texture#flipY} and {@link Texture#premultiplyAlpha} are ignored with image bitmaps.
- * These options need to be configured via {@link ImageBitmapLoader#setOptions} prior to loading,
- * unlike regular images which can be configured on the Texture to set these options on GPU upload instead.
- *
- * To match the default behaviour of {@link Texture}, the following options are needed:
- *
- * ```js
- * { imageOrientation: 'flipY', premultiplyAlpha: 'none' }
- * ```
- *
- * Also note that unlike {@link FileLoader}, this loader will only avoid multiple concurrent requests to the same URL if {@link Cache} is enabled.
- *
- * ```js
- * const loader = new THREE.ImageBitmapLoader();
- * loader.setOptions( { imageOrientation: 'flipY' } ); // set options if needed
- * const imageBitmap = await loader.loadAsync( 'image.png' );
- *
- * const texture = new THREE.Texture( imageBitmap );
- * texture.needsUpdate = true;
- * ```
- *
- * @augments Loader
- */
-class ImageBitmapLoader extends Loader {
-
- /**
- * Constructs a new image bitmap loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isImageBitmapLoader = true;
-
- if ( typeof createImageBitmap === 'undefined' ) {
-
- warn( 'ImageBitmapLoader: createImageBitmap() not supported.' );
-
- }
-
- if ( typeof fetch === 'undefined' ) {
-
- warn( 'ImageBitmapLoader: fetch() not supported.' );
-
- }
-
- /**
- * Represents the loader options.
- *
- * @type {Object}
- * @default {premultiplyAlpha:'none'}
- */
- this.options = { premultiplyAlpha: 'none' };
-
- /**
- * Used for aborting requests.
- *
- * @private
- * @type {AbortController}
- */
- this._abortController = new AbortController();
-
- }
-
- /**
- * Sets the given loader options. The structure of the object must match the `options` parameter of
- * [createImageBitmap](https://developer.mozilla.org/en-US/docs/Web/API/Window/createImageBitmap).
- *
- * Note: When caching is enabled, the cache key is based on the URL only. Loading the same URL with
- * different options will return the cached result of the first request.
- *
- * @param {Object} options - The loader options to set.
- * @return {ImageBitmapLoader} A reference to this image bitmap loader.
- */
- setOptions( options ) {
-
- this.options = options;
-
- return this;
-
- }
-
- /**
- * Starts loading from the given URL and pass the loaded image bitmap to the `onLoad()` callback.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(ImageBitmap)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Unsupported in this loader.
- * @param {onErrorCallback} onError - Executed when errors occur.
- */
- load( url, onLoad, onProgress, onError ) {
-
- if ( url === undefined ) url = '';
-
- if ( this.path !== undefined ) url = this.path + url;
-
- url = this.manager.resolveURL( url );
-
- const scope = this;
-
- const cached = Cache.get( `image-bitmap:${url}` );
-
- if ( cached !== undefined ) {
-
- scope.manager.itemStart( url );
-
- // If cached is a promise, wait for it to resolve
- if ( cached.then ) {
-
- cached.then( imageBitmap => {
-
- // check if there is an error for the cached promise
-
- if ( _errorMap.has( cached ) === true ) {
-
- if ( onError ) onError( _errorMap.get( cached ) );
-
- scope.manager.itemError( url );
- scope.manager.itemEnd( url );
-
- } else {
-
- if ( onLoad ) onLoad( imageBitmap );
-
- scope.manager.itemEnd( url );
-
- }
-
- } );
-
- return;
-
- }
-
- // If cached is not a promise (i.e., it's already an imageBitmap)
- setTimeout( function () {
-
- if ( onLoad ) onLoad( cached );
-
- scope.manager.itemEnd( url );
-
- }, 0 );
-
- return;
-
- }
-
- const fetchOptions = {};
- fetchOptions.credentials = ( this.crossOrigin === 'anonymous' ) ? 'same-origin' : 'include';
- fetchOptions.headers = this.requestHeader;
- fetchOptions.signal = ( typeof AbortSignal.any === 'function' ) ? AbortSignal.any( [ this._abortController.signal, this.manager.abortController.signal ] ) : this._abortController.signal;
-
- const promise = fetch( url, fetchOptions ).then( function ( res ) {
-
- return res.blob();
-
- } ).then( function ( blob ) {
-
- return createImageBitmap( blob, Object.assign( {}, scope.options, { colorSpaceConversion: 'none' } ) );
-
- } ).then( function ( imageBitmap ) {
-
- Cache.add( `image-bitmap:${url}`, imageBitmap );
-
- if ( onLoad ) onLoad( imageBitmap );
-
- scope.manager.itemEnd( url );
-
- return imageBitmap; // see #34150
-
- } ).catch( function ( e ) {
-
- if ( onError ) onError( e );
-
- _errorMap.set( promise, e );
-
- Cache.remove( `image-bitmap:${url}` );
-
- scope.manager.itemError( url );
- scope.manager.itemEnd( url );
-
- } );
-
- Cache.add( `image-bitmap:${url}`, promise );
- scope.manager.itemStart( url );
-
- }
-
- /**
- * Aborts ongoing fetch requests.
- *
- * @return {ImageBitmapLoader} A reference to this instance.
- */
- abort() {
-
- this._abortController.abort();
- this._abortController = new AbortController();
-
- return this;
-
- }
-
-}
-
-let _context;
-
-/**
- * Manages the global audio context in the engine.
- *
- * @hideconstructor
- */
-class AudioContext {
-
- /**
- * Returns the global native audio context.
- *
- * @return {Window.AudioContext} The native audio context.
- */
- static getContext() {
-
- if ( _context === undefined ) {
-
- _context = new ( window.AudioContext || window.webkitAudioContext )();
-
- }
-
- return _context;
-
- }
-
- /**
- * Allows to set the global native audio context from outside.
- *
- * @param {Window.AudioContext} value - The native context to set.
- */
- static setContext( value ) {
-
- _context = value;
-
- }
-
-}
-
-/**
- * Class for loading audio buffers. Audios are internally
- * loaded via {@link FileLoader}.
- *
- * ```js
- * const audioListener = new THREE.AudioListener();
- * const ambientSound = new THREE.Audio( audioListener );
- *
- * const loader = new THREE.AudioLoader();
- * const audioBuffer = await loader.loadAsync( 'audio/ambient_ocean.ogg' );
- *
- * ambientSound.setBuffer( audioBuffer );
- * ambientSound.play();
- * ```
- *
- * @augments Loader
- */
-class AudioLoader extends Loader {
-
- /**
- * Constructs a new audio loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
-
- super( manager );
-
- }
-
- /**
- * Starts loading from the given URL and passes the loaded audio buffer
- * to the `onLoad()` callback.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(AudioBuffer)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
- * @param {onErrorCallback} onError - Executed when errors occur.
- */
- load( url, onLoad, onProgress, onError ) {
-
- const scope = this;
-
- const loader = new FileLoader( this.manager );
- loader.setResponseType( 'arraybuffer' );
- loader.setPath( this.path );
- loader.setRequestHeader( this.requestHeader );
- loader.setWithCredentials( this.withCredentials );
- loader.load( url, function ( buffer ) {
-
- try {
-
- // Create a copy of the buffer. The `decodeAudioData` method
- // detaches the buffer when complete, preventing reuse.
- const bufferCopy = buffer.slice( 0 );
-
- const context = AudioContext.getContext();
-
- const decodeUrl = url + '#decode';
- scope.manager.itemStart( decodeUrl ); // prevent loading manager from completing too early, see #33378
-
- context.decodeAudioData( bufferCopy, function ( audioBuffer ) {
-
- onLoad( audioBuffer );
- scope.manager.itemEnd( decodeUrl );
-
- } ).catch( function ( e ) {
-
- handleError( e );
- scope.manager.itemEnd( decodeUrl );
-
- } );
-
- } catch ( e ) {
-
- handleError( e );
-
- }
-
- }, onProgress, onError );
-
- function handleError( e ) {
-
- if ( onError ) {
-
- onError( e );
-
- } else {
-
- error( e );
-
- }
-
- scope.manager.itemError( url );
-
- }
-
- }
-
-}
-
-const _eyeRight = /*@__PURE__*/ new Matrix4();
-const _eyeLeft = /*@__PURE__*/ new Matrix4();
-const _projectionMatrix = /*@__PURE__*/ new Matrix4();
-
-/**
- * A special type of camera that uses two perspective cameras with
- * stereoscopic projection. Can be used for rendering stereo effects
- * like [3D Anaglyph](https://en.wikipedia.org/wiki/Anaglyph_3D) or
- * [Parallax Barrier](https://en.wikipedia.org/wiki/parallax_barrier).
- */
-class StereoCamera {
-
- /**
- * Constructs a new stereo camera.
- */
- constructor() {
-
- /**
- * The type property is used for detecting the object type
- * in context of serialization/deserialization.
- *
- * @type {string}
- * @readonly
- */
- this.type = 'StereoCamera';
-
- /**
- * The aspect.
- *
- * @type {number}
- * @default 1
- */
- this.aspect = 1;
-
- /**
- * The eye separation which represents the distance
- * between the left and right camera.
- *
- * @type {number}
- * @default 0.064
- */
- this.eyeSep = 0.064;
-
- /**
- * The camera representing the left eye. This is added to layer `1` so objects to be
- * rendered by the left camera must also be added to this layer.
- *
- * @type {PerspectiveCamera}
- */
- this.cameraL = new PerspectiveCamera();
- this.cameraL.layers.enable( 1 );
- this.cameraL.matrixAutoUpdate = false;
-
- /**
- * The camera representing the right eye. This is added to layer `2` so objects to be
- * rendered by the right camera must also be added to this layer.
- *
- * @type {PerspectiveCamera}
- */
- this.cameraR = new PerspectiveCamera();
- this.cameraR.layers.enable( 2 );
- this.cameraR.matrixAutoUpdate = false;
-
- this._cache = {
- focus: null,
- fov: null,
- aspect: null,
- near: null,
- far: null,
- zoom: null,
- eyeSep: null
- };
-
- }
-
- /**
- * Updates the stereo camera based on the given perspective camera.
- *
- * @param {PerspectiveCamera} camera - The perspective camera.
- */
- update( camera ) {
-
- const cache = this._cache;
-
- const needsUpdate = cache.focus !== camera.focus || cache.fov !== camera.fov ||
- cache.aspect !== camera.aspect * this.aspect || cache.near !== camera.near ||
- cache.far !== camera.far || cache.zoom !== camera.zoom || cache.eyeSep !== this.eyeSep;
-
- if ( needsUpdate ) {
-
- cache.focus = camera.focus;
- cache.fov = camera.fov;
- cache.aspect = camera.aspect * this.aspect;
- cache.near = camera.near;
- cache.far = camera.far;
- cache.zoom = camera.zoom;
- cache.eyeSep = this.eyeSep;
-
- // Off-axis stereoscopic effect based on
- // http://paulbourke.net/stereographics/stereorender/
-
- _projectionMatrix.copy( camera.projectionMatrix );
- const eyeSepHalf = cache.eyeSep / 2;
- const eyeSepOnProjection = eyeSepHalf * cache.near / cache.focus;
- const ymax = ( cache.near * Math.tan( DEG2RAD * cache.fov * 0.5 ) ) / cache.zoom;
- let xmin, xmax;
-
- // translate xOffset
-
- _eyeLeft.elements[ 12 ] = - eyeSepHalf;
- _eyeRight.elements[ 12 ] = eyeSepHalf;
-
- // for left eye
-
- xmin = - ymax * cache.aspect + eyeSepOnProjection;
- xmax = ymax * cache.aspect + eyeSepOnProjection;
-
- _projectionMatrix.elements[ 0 ] = 2 * cache.near / ( xmax - xmin );
- _projectionMatrix.elements[ 8 ] = ( xmax + xmin ) / ( xmax - xmin );
-
- this.cameraL.projectionMatrix.copy( _projectionMatrix );
-
- // for right eye
-
- xmin = - ymax * cache.aspect - eyeSepOnProjection;
- xmax = ymax * cache.aspect - eyeSepOnProjection;
-
- _projectionMatrix.elements[ 0 ] = 2 * cache.near / ( xmax - xmin );
- _projectionMatrix.elements[ 8 ] = ( xmax + xmin ) / ( xmax - xmin );
-
- this.cameraR.projectionMatrix.copy( _projectionMatrix );
-
- }
-
- this.cameraL.matrix.copy( camera.matrixWorld ).multiply( _eyeLeft );
- this.cameraL.matrixWorldNeedsUpdate = true;
-
- this.cameraR.matrix.copy( camera.matrixWorld ).multiply( _eyeRight );
- this.cameraR.matrixWorldNeedsUpdate = true;
-
- }
-
-}
-
-const fov = -90; // negative fov is not an error
-const aspect = 1;
-
-/**
- * A special type of camera that is positioned in 3D space to render its surroundings into a
- * cube render target. The render target can then be used as an environment map for rendering
- * realtime reflections in your scene.
- *
- * ```js
- * // Create cube render target
- * const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256, { generateMipmaps: true, minFilter: THREE.LinearMipmapLinearFilter } );
- *
- * // Create cube camera
- * const cubeCamera = new THREE.CubeCamera( 1, 100000, cubeRenderTarget );
- * scene.add( cubeCamera );
- *
- * // Create car
- * const chromeMaterial = new THREE.MeshLambertMaterial( { color: 0xffffff, envMap: cubeRenderTarget.texture } );
- * const car = new THREE.Mesh( carGeometry, chromeMaterial );
- * scene.add( car );
- *
- * // Update the render target cube
- * car.visible = false;
- * cubeCamera.position.copy( car.position );
- * cubeCamera.update( renderer, scene );
- *
- * // Render the scene
- * car.visible = true;
- * renderer.render( scene, camera );
- * ```
- *
- * @augments Object3D
- */
-class CubeCamera extends Object3D {
-
- /**
- * Constructs a new cube camera.
- *
- * @param {number} near - The camera's near plane.
- * @param {number} far - The camera's far plane.
- * @param {WebGLCubeRenderTarget} renderTarget - The cube render target.
- */
- constructor( near, far, renderTarget ) {
-
- super();
-
- this.type = 'CubeCamera';
-
- /**
- * A reference to the cube render target.
- *
- * @type {WebGLCubeRenderTarget}
- */
- this.renderTarget = renderTarget;
-
- /**
- * The current active coordinate system.
- *
- * @type {?(WebGLCoordinateSystem|WebGPUCoordinateSystem)}
- * @default null
- */
- this.coordinateSystem = null;
-
- /**
- * The current active mipmap level
- *
- * @type {number}
- * @default 0
- */
- this.activeMipmapLevel = 0;
-
- const cameraPX = new PerspectiveCamera( fov, aspect, near, far );
- cameraPX.layers = this.layers;
- this.add( cameraPX );
-
- const cameraNX = new PerspectiveCamera( fov, aspect, near, far );
- cameraNX.layers = this.layers;
- this.add( cameraNX );
-
- const cameraPY = new PerspectiveCamera( fov, aspect, near, far );
- cameraPY.layers = this.layers;
- this.add( cameraPY );
-
- const cameraNY = new PerspectiveCamera( fov, aspect, near, far );
- cameraNY.layers = this.layers;
- this.add( cameraNY );
-
- const cameraPZ = new PerspectiveCamera( fov, aspect, near, far );
- cameraPZ.layers = this.layers;
- this.add( cameraPZ );
-
- const cameraNZ = new PerspectiveCamera( fov, aspect, near, far );
- cameraNZ.layers = this.layers;
- this.add( cameraNZ );
-
- }
-
- /**
- * Must be called when the coordinate system of the cube camera is changed.
- */
- updateCoordinateSystem() {
-
- const coordinateSystem = this.coordinateSystem;
-
- const cameras = this.children.concat();
-
- const [ cameraPX, cameraNX, cameraPY, cameraNY, cameraPZ, cameraNZ ] = cameras;
-
- for ( const camera of cameras ) this.remove( camera );
-
- if ( coordinateSystem === WebGLCoordinateSystem ) {
-
- cameraPX.up.set( 0, 1, 0 );
- cameraPX.lookAt( 1, 0, 0 );
-
- cameraNX.up.set( 0, 1, 0 );
- cameraNX.lookAt( -1, 0, 0 );
-
- cameraPY.up.set( 0, 0, -1 );
- cameraPY.lookAt( 0, 1, 0 );
-
- cameraNY.up.set( 0, 0, 1 );
- cameraNY.lookAt( 0, -1, 0 );
-
- cameraPZ.up.set( 0, 1, 0 );
- cameraPZ.lookAt( 0, 0, 1 );
-
- cameraNZ.up.set( 0, 1, 0 );
- cameraNZ.lookAt( 0, 0, -1 );
-
- } else if ( coordinateSystem === WebGPUCoordinateSystem ) {
-
- cameraPX.up.set( 0, -1, 0 );
- cameraPX.lookAt( -1, 0, 0 );
-
- cameraNX.up.set( 0, -1, 0 );
- cameraNX.lookAt( 1, 0, 0 );
-
- cameraPY.up.set( 0, 0, 1 );
- cameraPY.lookAt( 0, 1, 0 );
-
- cameraNY.up.set( 0, 0, -1 );
- cameraNY.lookAt( 0, -1, 0 );
-
- cameraPZ.up.set( 0, -1, 0 );
- cameraPZ.lookAt( 0, 0, 1 );
-
- cameraNZ.up.set( 0, -1, 0 );
- cameraNZ.lookAt( 0, 0, -1 );
-
- } else {
-
- throw new Error( 'THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: ' + coordinateSystem );
-
- }
-
- for ( const camera of cameras ) {
-
- this.add( camera );
-
- camera.updateMatrixWorld();
-
- }
-
- }
-
- /**
- * Calling this method will render the given scene with the given renderer
- * into the cube render target of the camera.
- *
- * @param {(Renderer|WebGLRenderer)} renderer - The renderer.
- * @param {Scene} scene - The scene to render.
- */
- update( renderer, scene ) {
-
- if ( this.parent === null ) this.updateMatrixWorld();
-
- const { renderTarget, activeMipmapLevel } = this;
-
- if ( this.coordinateSystem !== renderer.coordinateSystem ) {
-
- this.coordinateSystem = renderer.coordinateSystem;
-
- this.updateCoordinateSystem();
-
- }
-
- const [ cameraPX, cameraNX, cameraPY, cameraNY, cameraPZ, cameraNZ ] = this.children;
-
- const currentRenderTarget = renderer.getRenderTarget();
- const currentActiveCubeFace = renderer.getActiveCubeFace();
- const currentActiveMipmapLevel = renderer.getActiveMipmapLevel();
-
- const currentXrEnabled = renderer.xr.enabled;
-
- renderer.xr.enabled = false;
-
- const generateMipmaps = renderTarget.texture.generateMipmaps;
-
- renderTarget.texture.generateMipmaps = false;
-
- // https://github.com/mrdoob/three.js/issues/31413#issuecomment-3095966812
-
- let reversedDepthBuffer = false;
-
- if ( renderer.isWebGLRenderer === true ) {
-
- reversedDepthBuffer = renderer.state.buffers.depth.getReversed();
-
- } else {
-
- reversedDepthBuffer = renderer.reversedDepthBuffer;
-
- }
-
- renderer.setRenderTarget( renderTarget, 0, activeMipmapLevel );
- if ( reversedDepthBuffer && renderer.autoClear === false ) renderer.clearDepth();
- renderer.render( scene, cameraPX );
-
- renderer.setRenderTarget( renderTarget, 1, activeMipmapLevel );
- if ( reversedDepthBuffer && renderer.autoClear === false ) renderer.clearDepth();
- renderer.render( scene, cameraNX );
-
- renderer.setRenderTarget( renderTarget, 2, activeMipmapLevel );
- if ( reversedDepthBuffer && renderer.autoClear === false ) renderer.clearDepth();
- renderer.render( scene, cameraPY );
-
- renderer.setRenderTarget( renderTarget, 3, activeMipmapLevel );
- if ( reversedDepthBuffer && renderer.autoClear === false ) renderer.clearDepth();
- renderer.render( scene, cameraNY );
-
- renderer.setRenderTarget( renderTarget, 4, activeMipmapLevel );
- if ( reversedDepthBuffer && renderer.autoClear === false ) renderer.clearDepth();
- renderer.render( scene, cameraPZ );
-
- // mipmaps are generated during the last call of render()
- // at this point, all sides of the cube render target are defined
-
- renderTarget.texture.generateMipmaps = generateMipmaps;
-
- renderer.setRenderTarget( renderTarget, 5, activeMipmapLevel );
- if ( reversedDepthBuffer && renderer.autoClear === false ) renderer.clearDepth();
- renderer.render( scene, cameraNZ );
-
- renderer.setRenderTarget( currentRenderTarget, currentActiveCubeFace, currentActiveMipmapLevel );
-
- renderer.xr.enabled = currentXrEnabled;
-
- renderTarget.texture.needsPMREMUpdate = true;
-
- }
-
-}
-
-/**
- * This type of camera can be used in order to efficiently render a scene with a
- * predefined set of cameras. This is an important performance aspect for
- * rendering VR scenes.
- *
- * An instance of `ArrayCamera` always has an array of sub cameras. It's mandatory
- * to define for each sub camera the `viewport` property which determines the
- * part of the viewport that is rendered with this camera.
- *
- * @augments PerspectiveCamera
- */
-class ArrayCamera extends PerspectiveCamera {
-
- /**
- * Constructs a new array camera.
- *
- * @param {Array} [array=[]] - An array of perspective sub cameras.
- */
- constructor( array = [] ) {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isArrayCamera = true;
-
- /**
- * Whether this camera is used with multiview rendering or not.
- *
- * @type {boolean}
- * @readonly
- * @default false
- */
- this.isMultiViewCamera = false;
-
- /**
- * An array of perspective sub cameras.
- *
- * @type {Array}
- */
- this.cameras = array;
-
- }
-
-}
-
-/**
- * This class is an alternative to {@link Clock} with a different API design and behavior.
- * The goal is to avoid the conceptual flaws that became apparent in `Clock` over time.
- *
- * - `Timer` has an `update()` method that updates its internal state. That makes it possible to
- * call `getDelta()` and `getElapsed()` multiple times per simulation step without getting different values.
- * - The class can make use of the Page Visibility API to avoid large time delta values when the app
- * is inactive (e.g. tab switched or browser hidden).
- *
- * ```js
- * const timer = new Timer();
- * timer.connect( document ); // use Page Visibility API
- * ```
- */
-class Timer {
-
- /**
- * Constructs a new timer.
- */
- constructor() {
-
- this._previousTime = 0;
- this._currentTime = 0;
- this._startTime = performance.now();
-
- this._delta = 0;
- this._elapsed = 0;
-
- this._timescale = 1;
-
- this._document = null;
- this._pageVisibilityHandler = null;
-
- }
-
- /**
- * Connect the timer to the given document.Calling this method is not mandatory to
- * use the timer but enables the usage of the Page Visibility API to avoid large time
- * delta values.
- *
- * @param {Document} document - The document.
- */
- connect( document ) {
-
- this._document = document;
-
- // use Page Visibility API to avoid large time delta values
-
- if ( document.hidden !== undefined ) {
-
- this._pageVisibilityHandler = handleVisibilityChange.bind( this );
-
- document.addEventListener( 'visibilitychange', this._pageVisibilityHandler, false );
-
- }
-
- }
-
- /**
- * Disconnects the timer from the DOM and also disables the usage of the Page Visibility API.
- */
- disconnect() {
-
- if ( this._pageVisibilityHandler !== null ) {
-
- this._document.removeEventListener( 'visibilitychange', this._pageVisibilityHandler );
- this._pageVisibilityHandler = null;
-
- }
-
- this._document = null;
-
- }
-
- /**
- * Returns the time delta in seconds.
- *
- * @return {number} The time delta in second.
- */
- getDelta() {
-
- return this._delta / 1000;
-
- }
-
- /**
- * Returns the elapsed time in seconds.
- *
- * @return {number} The elapsed time in second.
- */
- getElapsed() {
-
- return this._elapsed / 1000;
-
- }
-
- /**
- * Returns the timescale.
- *
- * @return {number} The timescale.
- */
- getTimescale() {
-
- return this._timescale;
-
- }
-
- /**
- * Sets the given timescale which scale the time delta computation
- * in `update()`.
- *
- * @param {number} timescale - The timescale to set.
- * @return {Timer} A reference to this timer.
- */
- setTimescale( timescale ) {
-
- this._timescale = timescale;
-
- return this;
-
- }
-
- /**
- * Resets the time computation for the current simulation step.
- *
- * @return {Timer} A reference to this timer.
- */
- reset() {
-
- this._currentTime = performance.now() - this._startTime;
-
- return this;
-
- }
-
- /**
- * Can be used to free all internal resources. Usually called when
- * the timer instance isn't required anymore.
- */
- dispose() {
-
- this.disconnect();
-
- }
-
- /**
- * Updates the internal state of the timer. This method should be called
- * once per simulation step and before you perform queries against the timer
- * (e.g. via `getDelta()`).
- *
- * @param {number} timestamp - The current time in milliseconds. Can be obtained
- * from the `requestAnimationFrame` callback argument. If not provided, the current
- * time will be determined with `performance.now`.
- * @return {Timer} A reference to this timer.
- */
- update( timestamp ) {
-
- if ( this._pageVisibilityHandler !== null && this._document.hidden === true ) {
-
- this._delta = 0;
-
- } else {
-
- this._previousTime = this._currentTime;
- this._currentTime = ( timestamp !== undefined ? timestamp : performance.now() ) - this._startTime;
-
- this._delta = ( this._currentTime - this._previousTime ) * this._timescale;
- this._elapsed += this._delta; // _elapsed is the accumulation of all previous deltas
-
- }
-
- return this;
-
- }
-
-}
-
-function handleVisibilityChange() {
-
- if ( this._document.hidden === false ) this.reset();
-
-}
-
-const _position$1 = /*@__PURE__*/ new Vector3();
-const _quaternion$1 = /*@__PURE__*/ new Quaternion();
-const _scale$1 = /*@__PURE__*/ new Vector3();
-
-const _forward = /*@__PURE__*/ new Vector3();
-const _up = /*@__PURE__*/ new Vector3();
-
-/**
- * The class represents a virtual listener of the all positional and non-positional audio effects
- * in the scene. A three.js application usually creates a single listener. It is a mandatory
- * constructor parameter for audios entities like {@link Audio} and {@link PositionalAudio}.
- *
- * In most cases, the listener object is a child of the camera. So the 3D transformation of the
- * camera represents the 3D transformation of the listener.
- *
- * @augments Object3D
- */
-class AudioListener extends Object3D {
-
- /**
- * Constructs a new audio listener.
- */
- constructor() {
-
- super();
-
- this.type = 'AudioListener';
-
- /**
- * The native audio context.
- *
- * @type {AudioContext}
- * @readonly
- */
- this.context = AudioContext.getContext();
-
- /**
- * The gain node used for volume control.
- *
- * @type {GainNode}
- * @readonly
- */
- this.gain = this.context.createGain();
- this.gain.connect( this.context.destination );
-
- /**
- * An optional filter.
- *
- * Defined via {@link AudioListener#setFilter}.
- *
- * @type {?AudioNode}
- * @default null
- * @readonly
- */
- this.filter = null;
-
- /**
- * Time delta values required for `linearRampToValueAtTime()` usage.
- *
- * @type {number}
- * @default 0
- * @readonly
- */
- this.timeDelta = 0;
-
- // private
-
- this._timer = new Timer();
-
- }
-
- /**
- * Returns the listener's input node.
- *
- * This method is used by other audio nodes to connect to this listener.
- *
- * @return {GainNode} The input node.
- */
- getInput() {
-
- return this.gain;
-
- }
-
- /**
- * Removes the current filter from this listener.
- *
- * @return {AudioListener} A reference to this listener.
- */
- removeFilter() {
-
- if ( this.filter !== null ) {
-
- this.gain.disconnect( this.filter );
- this.filter.disconnect( this.context.destination );
- this.gain.connect( this.context.destination );
- this.filter = null;
-
- }
-
- return this;
-
- }
-
- /**
- * Returns the current set filter.
- *
- * @return {?AudioNode} The filter.
- */
- getFilter() {
-
- return this.filter;
-
- }
-
- /**
- * Sets the given filter to this listener.
- *
- * @param {AudioNode} value - The filter to set.
- * @return {AudioListener} A reference to this listener.
- */
- setFilter( value ) {
-
- if ( this.filter !== null ) {
-
- this.gain.disconnect( this.filter );
- this.filter.disconnect( this.context.destination );
-
- } else {
-
- this.gain.disconnect( this.context.destination );
-
- }
-
- this.filter = value;
- this.gain.connect( this.filter );
- this.filter.connect( this.context.destination );
-
- return this;
-
- }
-
- /**
- * Returns the applications master volume.
- *
- * @return {number} The master volume.
- */
- getMasterVolume() {
-
- return this.gain.gain.value;
-
- }
-
- /**
- * Sets the applications master volume. This volume setting affects
- * all audio nodes in the scene.
- *
- * @param {number} value - The master volume to set.
- * @return {AudioListener} A reference to this listener.
- */
- setMasterVolume( value ) {
-
- this.gain.gain.setTargetAtTime( value, this.context.currentTime, 0.01 );
-
- return this;
-
- }
-
- updateMatrixWorld( force ) {
-
- super.updateMatrixWorld( force );
-
- this._timer.update();
-
- const listener = this.context.listener;
-
- this.timeDelta = this._timer.getDelta();
-
- this.matrixWorld.decompose( _position$1, _quaternion$1, _scale$1 );
-
- // the initial forward and up directions must be orthogonal
- _forward.set( 0, 0, -1 ).applyQuaternion( _quaternion$1 );
- _up.set( 0, 1, 0 ).applyQuaternion( _quaternion$1 );
-
- if ( listener.positionX ) {
-
- // code path for Chrome (see #14393)
-
- const endTime = this.context.currentTime + this.timeDelta;
-
- listener.positionX.linearRampToValueAtTime( _position$1.x, endTime );
- listener.positionY.linearRampToValueAtTime( _position$1.y, endTime );
- listener.positionZ.linearRampToValueAtTime( _position$1.z, endTime );
- listener.forwardX.linearRampToValueAtTime( _forward.x, endTime );
- listener.forwardY.linearRampToValueAtTime( _forward.y, endTime );
- listener.forwardZ.linearRampToValueAtTime( _forward.z, endTime );
- listener.upX.linearRampToValueAtTime( _up.x, endTime );
- listener.upY.linearRampToValueAtTime( _up.y, endTime );
- listener.upZ.linearRampToValueAtTime( _up.z, endTime );
-
- } else {
-
- listener.setPosition( _position$1.x, _position$1.y, _position$1.z );
- listener.setOrientation( _forward.x, _forward.y, _forward.z, _up.x, _up.y, _up.z );
-
- }
-
- }
-
-}
-
-/**
- * Represents a non-positional ( global ) audio object.
- *
- * This and related audio modules make use of the [Web Audio API](https://www.w3.org/TR/webaudio-1.1/).
- *
- * ```js
- * // create an AudioListener and add it to the camera
- * const listener = new THREE.AudioListener();
- * camera.add( listener );
- *
- * // create a global audio source
- * const sound = new THREE.Audio( listener );
- *
- * // load a sound and set it as the Audio object's buffer
- * const audioLoader = new THREE.AudioLoader();
- * audioLoader.load( 'sounds/ambient.ogg', function( buffer ) {
- * sound.setBuffer( buffer );
- * sound.setLoop( true );
- * sound.setVolume( 0.5 );
- * sound.play();
- * });
- * ```
- *
- * @augments Object3D
- */
-class Audio extends Object3D {
-
- /**
- * Constructs a new audio.
- *
- * @param {AudioListener} listener - The global audio listener.
- */
- constructor( listener ) {
-
- super();
-
- this.type = 'Audio';
-
- /**
- * The global audio listener.
- *
- * @type {AudioListener}
- * @readonly
- */
- this.listener = listener;
-
- /**
- * The audio context.
- *
- * @type {AudioContext}
- * @readonly
- */
- this.context = listener.context;
-
- /**
- * The gain node used for volume control.
- *
- * @type {GainNode}
- * @readonly
- */
- this.gain = this.context.createGain();
- this.gain.connect( listener.getInput() );
-
- /**
- * Whether to start playback automatically or not.
- *
- * @type {boolean}
- * @default false
- */
- this.autoplay = false;
-
- /**
- * A reference to an audio buffer.
- *
- * Defined via {@link Audio#setBuffer}.
- *
- * @type {?AudioBuffer}
- * @default null
- * @readonly
- */
- this.buffer = null;
-
- /**
- * Modify pitch, measured in cents. +/- 100 is a semitone.
- * +/- 1200 is an octave.
- *
- * Defined via {@link Audio#setDetune}.
- *
- * @type {number}
- * @default 0
- * @readonly
- */
- this.detune = 0;
-
- /**
- * Whether the audio should loop or not.
- *
- * Defined via {@link Audio#setLoop}.
- *
- * @type {boolean}
- * @default false
- * @readonly
- */
- this.loop = false;
-
- /**
- * Defines where in the audio buffer the replay should
- * start, in seconds.
- *
- * @type {number}
- * @default 0
- */
- this.loopStart = 0;
-
- /**
- * Defines where in the audio buffer the replay should
- * stop, in seconds.
- *
- * @type {number}
- * @default 0
- */
- this.loopEnd = 0;
-
- /**
- * An offset to the time within the audio buffer the playback
- * should begin, in seconds.
- *
- * @type {number}
- * @default 0
- */
- this.offset = 0;
-
- /**
- * Overrides the default duration of the audio.
- *
- * @type {undefined|number}
- * @default undefined
- */
- this.duration = undefined;
-
- /**
- * The playback speed.
- *
- * Defined via {@link Audio#setPlaybackRate}.
- *
- * @type {number}
- * @readonly
- * @default 1
- */
- this.playbackRate = 1;
-
- /**
- * Indicates whether the audio is playing or not.
- *
- * This flag will be automatically set when using {@link Audio#play},
- * {@link Audio#pause}, {@link Audio#stop}.
- *
- * @type {boolean}
- * @readonly
- * @default false
- */
- this.isPlaying = false;
-
- /**
- * Indicates whether the audio playback can be controlled
- * with method like {@link Audio#play} or {@link Audio#pause}.
- *
- * This flag will be automatically set when audio sources are
- * defined.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.hasPlaybackControl = true;
-
- /**
- * Holds a reference to the current audio source.
- *
- * The property is automatically by one of the `set*()` methods.
- *
- * @type {?AudioNode}
- * @readonly
- * @default null
- */
- this.source = null;
-
- /**
- * Defines the source type.
- *
- * The property is automatically set by one of the `set*()` methods.
- *
- * @type {('empty'|'audioNode'|'mediaNode'|'mediaStreamNode'|'buffer')}
- * @readonly
- * @default 'empty'
- */
- this.sourceType = 'empty';
-
- this._startedAt = 0;
- this._progress = 0;
- this._connected = false;
-
- /**
- * Can be used to apply a variety of low-order filters to create
- * more complex sound effects e.g. via `BiquadFilterNode`.
- *
- * The property is automatically set by {@link Audio#setFilters}.
- *
- * @type {Array}
- * @readonly
- */
- this.filters = [];
-
- }
-
- /**
- * Returns the output audio node.
- *
- * @return {GainNode} The output node.
- */
- getOutput() {
-
- return this.gain;
-
- }
-
- /**
- * Sets the given audio node as the source of this instance.
- *
- * {@link Audio#sourceType} is set to `audioNode` and {@link Audio#hasPlaybackControl} to `false`.
- *
- * @param {AudioNode} audioNode - The audio node like an instance of `OscillatorNode`.
- * @return {Audio} A reference to this instance.
- */
- setNodeSource( audioNode ) {
-
- this.hasPlaybackControl = false;
- this.sourceType = 'audioNode';
- this.source = audioNode;
- this.connect();
-
- return this;
-
- }
-
- /**
- * Sets the given media element as the source of this instance.
- *
- * {@link Audio#sourceType} is set to `mediaNode` and {@link Audio#hasPlaybackControl} to `false`.
- *
- * @param {HTMLMediaElement} mediaElement - The media element.
- * @return {Audio} A reference to this instance.
- */
- setMediaElementSource( mediaElement ) {
-
- this.hasPlaybackControl = false;
- this.sourceType = 'mediaNode';
- this.source = this.context.createMediaElementSource( mediaElement );
- this.connect();
-
- return this;
-
- }
-
- /**
- * Sets the given media stream as the source of this instance.
- *
- * {@link Audio#sourceType} is set to `mediaStreamNode` and {@link Audio#hasPlaybackControl} to `false`.
- *
- * @param {MediaStream} mediaStream - The media stream.
- * @return {Audio} A reference to this instance.
- */
- setMediaStreamSource( mediaStream ) {
-
- this.hasPlaybackControl = false;
- this.sourceType = 'mediaStreamNode';
- this.source = this.context.createMediaStreamSource( mediaStream );
- this.connect();
-
- return this;
-
- }
-
- /**
- * Sets the given audio buffer as the source of this instance.
- *
- * {@link Audio#sourceType} is set to `buffer` and {@link Audio#hasPlaybackControl} to `true`.
- *
- * @param {AudioBuffer} audioBuffer - The audio buffer.
- * @return {Audio} A reference to this instance.
- */
- setBuffer( audioBuffer ) {
-
- this.buffer = audioBuffer;
- this.sourceType = 'buffer';
-
- if ( this.autoplay ) this.play();
-
- return this;
-
- }
-
- /**
- * Starts the playback of the audio.
- *
- * Can only be used with compatible audio sources that allow playback control.
- *
- * @param {number} [delay=0] - The delay, in seconds, at which the audio should start playing.
- * @return {Audio|undefined} A reference to this instance.
- */
- play( delay = 0 ) {
-
- if ( this.isPlaying === true ) {
-
- warn( 'Audio: Audio is already playing.' );
- return;
-
- }
-
- if ( this.hasPlaybackControl === false ) {
-
- warn( 'Audio: this Audio has no playback control.' );
- return;
-
- }
-
- this._startedAt = this.context.currentTime + delay;
-
- const source = this.context.createBufferSource();
- source.buffer = this.buffer;
- source.loop = this.loop;
- source.loopStart = this.loopStart;
- source.loopEnd = this.loopEnd;
- source.onended = this.onEnded.bind( this );
- source.start( this._startedAt, this._progress + this.offset, this.duration );
-
- this.isPlaying = true;
-
- this.source = source;
-
- this.setDetune( this.detune );
- this.setPlaybackRate( this.playbackRate );
-
- return this.connect();
-
- }
-
- /**
- * Pauses the playback of the audio.
- *
- * Can only be used with compatible audio sources that allow playback control.
- *
- * @return {Audio|undefined} A reference to this instance.
- */
- pause() {
-
- if ( this.hasPlaybackControl === false ) {
-
- warn( 'Audio: this Audio has no playback control.' );
- return;
-
- }
-
- if ( this.isPlaying === true ) {
-
- // update current progress
-
- this._progress += Math.max( this.context.currentTime - this._startedAt, 0 ) * this.playbackRate;
-
- if ( this.loop === true ) {
-
- // ensure _progress does not exceed duration with looped audios
-
- this._progress = this._progress % ( this.duration || this.buffer.duration );
-
- }
-
- this.source.stop();
- this.source.onended = null;
-
- this.isPlaying = false;
-
- }
-
- return this;
-
- }
-
- /**
- * Stops the playback of the audio.
- *
- * Can only be used with compatible audio sources that allow playback control.
- *
- * @param {number} [delay=0] - The delay, in seconds, at which the audio should stop playing.
- * @return {Audio|undefined} A reference to this instance.
- */
- stop( delay = 0 ) {
-
- if ( this.hasPlaybackControl === false ) {
-
- warn( 'Audio: this Audio has no playback control.' );
- return;
-
- }
-
- this._progress = 0;
-
- if ( this.source !== null ) {
-
- this.source.stop( this.context.currentTime + delay );
- this.source.onended = null;
-
- }
-
- this.isPlaying = false;
-
- return this;
-
- }
-
- /**
- * Connects to the audio source. This is used internally on
- * initialisation and when setting / removing filters.
- *
- * @return {Audio} A reference to this instance.
- */
- connect() {
-
- if ( this.filters.length > 0 ) {
-
- this.source.connect( this.filters[ 0 ] );
-
- for ( let i = 1, l = this.filters.length; i < l; i ++ ) {
-
- this.filters[ i - 1 ].connect( this.filters[ i ] );
-
- }
-
- this.filters[ this.filters.length - 1 ].connect( this.getOutput() );
-
- } else {
-
- this.source.connect( this.getOutput() );
-
- }
-
- this._connected = true;
-
- return this;
-
- }
-
- /**
- * Disconnects to the audio source. This is used internally on
- * initialisation and when setting / removing filters.
- *
- * @return {Audio|undefined} A reference to this instance.
- */
- disconnect() {
-
- if ( this._connected === false ) {
-
- return;
-
- }
-
- if ( this.filters.length > 0 ) {
-
- this.source.disconnect( this.filters[ 0 ] );
-
- for ( let i = 1, l = this.filters.length; i < l; i ++ ) {
-
- this.filters[ i - 1 ].disconnect( this.filters[ i ] );
-
- }
-
- this.filters[ this.filters.length - 1 ].disconnect( this.getOutput() );
-
- } else {
-
- this.source.disconnect( this.getOutput() );
-
- }
-
- this._connected = false;
-
- return this;
-
- }
-
- /**
- * Returns the current set filters.
- *
- * @return {Array} The list of filters.
- */
- getFilters() {
-
- return this.filters;
-
- }
-
- /**
- * Sets an array of filters and connects them with the audio source.
- *
- * @param {Array} [value] - A list of filters.
- * @return {Audio} A reference to this instance.
- */
- setFilters( value ) {
-
- if ( ! value ) value = [];
-
- if ( this._connected === true ) {
-
- this.disconnect();
- this.filters = value.slice();
- this.connect();
-
- } else {
-
- this.filters = value.slice();
-
- }
-
- return this;
-
- }
-
- /**
- * Defines the detuning of oscillation in cents.
- *
- * @param {number} value - The detuning of oscillation in cents.
- * @return {Audio} A reference to this instance.
- */
- setDetune( value ) {
-
- this.detune = value;
-
- if ( this.isPlaying === true && this.source.detune !== undefined ) {
-
- this.source.detune.setTargetAtTime( this.detune, this.context.currentTime, 0.01 );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns the detuning of oscillation in cents.
- *
- * @return {number} The detuning of oscillation in cents.
- */
- getDetune() {
-
- return this.detune;
-
- }
-
- /**
- * Returns the first filter in the list of filters.
- *
- * @return {AudioNode|undefined} The first filter in the list of filters.
- */
- getFilter() {
-
- return this.getFilters()[ 0 ];
-
- }
-
- /**
- * Applies a single filter node to the audio.
- *
- * @param {AudioNode} [filter] - The filter to set.
- * @return {Audio} A reference to this instance.
- */
- setFilter( filter ) {
-
- return this.setFilters( filter ? [ filter ] : [] );
-
- }
-
- /**
- * Sets the playback rate.
- *
- * Can only be used with compatible audio sources that allow playback control.
- *
- * @param {number} [value] - The playback rate to set.
- * @return {Audio|undefined} A reference to this instance.
- */
- setPlaybackRate( value ) {
-
- if ( this.hasPlaybackControl === false ) {
-
- warn( 'Audio: this Audio has no playback control.' );
- return;
-
- }
-
- this.playbackRate = value;
-
- if ( this.isPlaying === true ) {
-
- this.source.playbackRate.setTargetAtTime( this.playbackRate, this.context.currentTime, 0.01 );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns the current playback rate.
-
- * @return {number} The playback rate.
- */
- getPlaybackRate() {
-
- return this.playbackRate;
-
- }
-
- /**
- * Automatically called when playback finished.
- */
- onEnded() {
-
- this.isPlaying = false;
- this._progress = 0;
-
- }
-
- /**
- * Returns the loop flag.
- *
- * Can only be used with compatible audio sources that allow playback control.
- *
- * @return {boolean} Whether the audio should loop or not.
- */
- getLoop() {
-
- if ( this.hasPlaybackControl === false ) {
-
- warn( 'Audio: this Audio has no playback control.' );
- return false;
-
- }
-
- return this.loop;
-
- }
-
- /**
- * Sets the loop flag.
- *
- * Can only be used with compatible audio sources that allow playback control.
- *
- * @param {boolean} value - Whether the audio should loop or not.
- * @return {Audio|undefined} A reference to this instance.
- */
- setLoop( value ) {
-
- if ( this.hasPlaybackControl === false ) {
-
- warn( 'Audio: this Audio has no playback control.' );
- return;
-
- }
-
- this.loop = value;
-
- if ( this.isPlaying === true ) {
-
- this.source.loop = this.loop;
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the loop start value which defines where in the audio buffer the replay should
- * start, in seconds.
- *
- * @param {number} value - The loop start value.
- * @return {Audio} A reference to this instance.
- */
- setLoopStart( value ) {
-
- this.loopStart = value;
-
- return this;
-
- }
-
- /**
- * Sets the loop end value which defines where in the audio buffer the replay should
- * stop, in seconds.
- *
- * @param {number} value - The loop end value.
- * @return {Audio} A reference to this instance.
- */
- setLoopEnd( value ) {
-
- this.loopEnd = value;
-
- return this;
-
- }
-
- /**
- * Returns the volume.
- *
- * @return {number} The volume.
- */
- getVolume() {
-
- return this.gain.gain.value;
-
- }
-
- /**
- * Sets the volume.
- *
- * @param {number} value - The volume to set.
- * @return {Audio} A reference to this instance.
- */
- setVolume( value ) {
-
- this.gain.gain.setTargetAtTime( value, this.context.currentTime, 0.01 );
-
- return this;
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- if ( source.sourceType !== 'buffer' ) {
-
- warn( 'Audio: Audio source type cannot be copied.' );
-
- return this;
-
- }
-
- this.autoplay = source.autoplay;
-
- this.buffer = source.buffer;
- this.detune = source.detune;
- this.loop = source.loop;
- this.loopStart = source.loopStart;
- this.loopEnd = source.loopEnd;
- this.offset = source.offset;
- this.duration = source.duration;
- this.playbackRate = source.playbackRate;
- this.hasPlaybackControl = source.hasPlaybackControl;
- this.sourceType = source.sourceType;
-
- this.filters = source.filters.slice();
-
- return this;
-
- }
-
- clone( recursive ) {
-
- return new this.constructor( this.listener ).copy( this, recursive );
-
- }
-
-}
-
-const _position = /*@__PURE__*/ new Vector3();
-const _quaternion = /*@__PURE__*/ new Quaternion();
-const _scale = /*@__PURE__*/ new Vector3();
-const _orientation = /*@__PURE__*/ new Vector3();
-
-/**
- * Represents a positional audio object.
- *
- * ```js
- * // create an AudioListener and add it to the camera
- * const listener = new THREE.AudioListener();
- * camera.add( listener );
- *
- * // create the PositionalAudio object (passing in the listener)
- * const sound = new THREE.PositionalAudio( listener );
- *
- * // load a sound and set it as the PositionalAudio object's buffer
- * const audioLoader = new THREE.AudioLoader();
- * audioLoader.load( 'sounds/song.ogg', function( buffer ) {
- * sound.setBuffer( buffer );
- * sound.setRefDistance( 20 );
- * sound.play();
- * });
- *
- * // create an object for the sound to play from
- * const sphere = new THREE.SphereGeometry( 20, 32, 16 );
- * const material = new THREE.MeshPhongMaterial( { color: 0xff2200 } );
- * const mesh = new THREE.Mesh( sphere, material );
- * scene.add( mesh );
- *
- * // finally add the sound to the mesh
- * mesh.add( sound );
- *
- * @augments Audio
- */
-class PositionalAudio extends Audio {
-
- /**
- * Constructs a positional audio.
- *
- * @param {AudioListener} listener - The global audio listener.
- */
- constructor( listener ) {
-
- super( listener );
-
- /**
- * The panner node represents the location, direction, and behavior of an audio
- * source in 3D space.
- *
- * @type {PannerNode}
- * @readonly
- */
- this.panner = this.context.createPanner();
- this.panner.panningModel = 'HRTF';
- this.panner.connect( this.gain );
-
- }
-
- connect() {
-
- super.connect();
-
- this.panner.connect( this.gain );
-
- return this;
-
- }
-
- disconnect() {
-
- super.disconnect();
-
- this.panner.disconnect( this.gain );
-
- return this;
-
- }
-
- getOutput() {
-
- return this.panner;
-
- }
-
- /**
- * Returns the current reference distance.
- *
- * @return {number} The reference distance.
- */
- getRefDistance() {
-
- return this.panner.refDistance;
-
- }
-
- /**
- * Defines the reference distance for reducing volume as the audio source moves
- * further from the listener – i.e. the distance at which the volume reduction
- * starts taking effect.
- *
- * @param {number} value - The reference distance to set.
- * @return {PositionalAudio} A reference to this instance.
- */
- setRefDistance( value ) {
-
- this.panner.refDistance = value;
-
- return this;
-
- }
-
- /**
- * Returns the current rolloff factor.
- *
- * @return {number} The rolloff factor.
- */
- getRolloffFactor() {
-
- return this.panner.rolloffFactor;
-
- }
-
- /**
- * Defines how quickly the volume is reduced as the source moves away from the listener.
- *
- * @param {number} value - The rolloff factor.
- * @return {PositionalAudio} A reference to this instance.
- */
- setRolloffFactor( value ) {
-
- this.panner.rolloffFactor = value;
-
- return this;
-
- }
-
- /**
- * Returns the current distance model.
- *
- * @return {('linear'|'inverse'|'exponential')} The distance model.
- */
- getDistanceModel() {
-
- return this.panner.distanceModel;
-
- }
-
- /**
- * Defines which algorithm to use to reduce the volume of the audio source
- * as it moves away from the listener.
- *
- * Read [the spec](https://www.w3.org/TR/webaudio-1.1/#enumdef-distancemodeltype)
- * for more details.
- *
- * @param {('linear'|'inverse'|'exponential')} value - The distance model to set.
- * @return {PositionalAudio} A reference to this instance.
- */
- setDistanceModel( value ) {
-
- this.panner.distanceModel = value;
-
- return this;
-
- }
-
- /**
- * Returns the current max distance.
- *
- * @return {number} The max distance.
- */
- getMaxDistance() {
-
- return this.panner.maxDistance;
-
- }
-
- /**
- * Defines the maximum distance between the audio source and the listener,
- * after which the volume is not reduced any further.
- *
- * This value is used only by the `linear` distance model.
- *
- * @param {number} value - The max distance.
- * @return {PositionalAudio} A reference to this instance.
- */
- setMaxDistance( value ) {
-
- this.panner.maxDistance = value;
-
- return this;
-
- }
-
- /**
- * Sets the directional cone in which the audio can be listened.
- *
- * @param {number} coneInnerAngle - An angle, in degrees, of a cone inside of which there will be no volume reduction.
- * @param {number} coneOuterAngle - An angle, in degrees, of a cone outside of which the volume will be reduced by a constant value, defined by the `coneOuterGain` parameter.
- * @param {number} coneOuterGain - The amount of volume reduction outside the cone defined by the `coneOuterAngle`. When set to `0`, no sound can be heard.
- * @return {PositionalAudio} A reference to this instance.
- */
- setDirectionalCone( coneInnerAngle, coneOuterAngle, coneOuterGain ) {
-
- this.panner.coneInnerAngle = coneInnerAngle;
- this.panner.coneOuterAngle = coneOuterAngle;
- this.panner.coneOuterGain = coneOuterGain;
-
- return this;
-
- }
-
- updateMatrixWorld( force ) {
-
- super.updateMatrixWorld( force );
-
- if ( this.hasPlaybackControl === true && this.isPlaying === false ) return;
-
- this.matrixWorld.decompose( _position, _quaternion, _scale );
-
- _orientation.set( 0, 0, 1 ).applyQuaternion( _quaternion );
-
- const panner = this.panner;
-
- if ( panner.positionX ) {
-
- // code path for Chrome and Firefox (see #14393)
-
- const endTime = this.context.currentTime + this.listener.timeDelta;
-
- panner.positionX.linearRampToValueAtTime( _position.x, endTime );
- panner.positionY.linearRampToValueAtTime( _position.y, endTime );
- panner.positionZ.linearRampToValueAtTime( _position.z, endTime );
- panner.orientationX.linearRampToValueAtTime( _orientation.x, endTime );
- panner.orientationY.linearRampToValueAtTime( _orientation.y, endTime );
- panner.orientationZ.linearRampToValueAtTime( _orientation.z, endTime );
-
- } else {
-
- panner.setPosition( _position.x, _position.y, _position.z );
- panner.setOrientation( _orientation.x, _orientation.y, _orientation.z );
-
- }
-
- }
-
-}
-
-/**
- * This class can be used to analyse audio data.
- *
- * ```js
- * // create an AudioListener and add it to the camera
- * const listener = new THREE.AudioListener();
- * camera.add( listener );
- *
- * // create an Audio source
- * const sound = new THREE.Audio( listener );
- *
- * // load a sound and set it as the Audio object's buffer
- * const audioLoader = new THREE.AudioLoader();
- * audioLoader.load( 'sounds/ambient.ogg', function( buffer ) {
- * sound.setBuffer( buffer );
- * sound.setLoop(true);
- * sound.setVolume(0.5);
- * sound.play();
- * });
- *
- * // create an AudioAnalyser, passing in the sound and desired fftSize
- * const analyser = new THREE.AudioAnalyser( sound, 32 );
- *
- * // get the average frequency of the sound
- * const data = analyser.getAverageFrequency();
- * ```
- */
-class AudioAnalyser {
-
- /**
- * Constructs a new audio analyzer.
- *
- * @param {Audio} audio - The audio to analyze.
- * @param {number} [fftSize=2048] - The window size in samples that is used when performing a Fast Fourier Transform (FFT) to get frequency domain data.
- */
- constructor( audio, fftSize = 2048 ) {
-
- /**
- * The global audio listener.
- *
- * @type {AnalyserNode}
- */
- this.analyser = audio.context.createAnalyser();
- this.analyser.fftSize = fftSize;
-
- /**
- * Holds the analyzed data.
- *
- * @type {Uint8Array}
- */
- this.data = new Uint8Array( this.analyser.frequencyBinCount );
-
- audio.getOutput().connect( this.analyser );
-
- }
-
- /**
- * Returns an array with frequency data of the audio.
- *
- * Each item in the array represents the decibel value for a specific frequency.
- * The frequencies are spread linearly from 0 to 1/2 of the sample rate.
- * For example, for 48000 sample rate, the last item of the array will represent
- * the decibel value for 24000 Hz.
- *
- * @return {Uint8Array} The frequency data.
- */
- getFrequencyData() {
-
- this.analyser.getByteFrequencyData( this.data );
-
- return this.data;
-
- }
-
- /**
- * Returns the average of the frequencies returned by {@link AudioAnalyser#getFrequencyData}.
- *
- * @return {number} The average frequency.
- */
- getAverageFrequency() {
-
- let value = 0;
- const data = this.getFrequencyData();
-
- for ( let i = 0; i < data.length; i ++ ) {
-
- value += data[ i ];
-
- }
-
- return value / data.length;
-
- }
-
-}
-
-/**
- * Buffered scene graph property that allows weighted accumulation; used internally.
- */
-class PropertyMixer {
-
- /**
- * Constructs a new property mixer.
- *
- * @param {PropertyBinding} binding - The property binding.
- * @param {string} typeName - The keyframe track type name.
- * @param {number} valueSize - The keyframe track value size.
- */
- constructor( binding, typeName, valueSize ) {
-
- /**
- * The property binding.
- *
- * @type {PropertyBinding}
- */
- this.binding = binding;
-
- /**
- * The keyframe track value size.
- *
- * @type {number}
- */
- this.valueSize = valueSize;
-
- let mixFunction,
- mixFunctionAdditive,
- setIdentity;
-
- // buffer layout: [ incoming | accu0 | accu1 | orig | addAccu | (optional work) ]
- //
- // interpolators can use .buffer as their .result
- // the data then goes to 'incoming'
- //
- // 'accu0' and 'accu1' are used frame-interleaved for
- // the cumulative result and are compared to detect
- // changes
- //
- // 'orig' stores the original state of the property
- //
- // 'add' is used for additive cumulative results
- //
- // 'work' is optional and is only present for quaternion types. It is used
- // to store intermediate quaternion multiplication results
-
- switch ( typeName ) {
-
- case 'quaternion':
- mixFunction = this._slerp;
- mixFunctionAdditive = this._slerpAdditive;
- setIdentity = this._setAdditiveIdentityQuaternion;
-
- this.buffer = new Float64Array( valueSize * 6 );
- this._workIndex = 5;
- break;
-
- case 'string':
- case 'bool':
- mixFunction = this._select;
-
- // Use the regular mix function and for additive on these types,
- // additive is not relevant for non-numeric types
- mixFunctionAdditive = this._select;
-
- setIdentity = this._setAdditiveIdentityOther;
-
- this.buffer = new Array( valueSize * 5 );
- break;
-
- default:
- mixFunction = this._lerp;
- mixFunctionAdditive = this._lerpAdditive;
- setIdentity = this._setAdditiveIdentityNumeric;
-
- this.buffer = new Float64Array( valueSize * 5 );
-
- }
-
- this._mixBufferRegion = mixFunction;
- this._mixBufferRegionAdditive = mixFunctionAdditive;
- this._setIdentity = setIdentity;
- this._origIndex = 3;
- this._addIndex = 4;
-
- /**
- * Accumulated weight of the property binding.
- *
- * @type {number}
- * @default 0
- */
- this.cumulativeWeight = 0;
-
- /**
- * Accumulated additive weight of the property binding.
- *
- * @type {number}
- * @default 0
- */
- this.cumulativeWeightAdditive = 0;
-
- /**
- * Number of active keyframe tracks currently using this property binding.
- *
- * @type {number}
- * @default 0
- */
- this.useCount = 0;
-
- /**
- * Number of keyframe tracks referencing this property binding.
- *
- * @type {number}
- * @default 0
- */
- this.referenceCount = 0;
-
- }
-
- /**
- * Accumulates data in the `incoming` region into `accu`.
- *
- * @param {number} accuIndex - The accumulation index.
- * @param {number} weight - The weight.
- */
- accumulate( accuIndex, weight ) {
-
- // note: happily accumulating nothing when weight = 0, the caller knows
- // the weight and shouldn't have made the call in the first place
-
- const buffer = this.buffer,
- stride = this.valueSize,
- offset = accuIndex * stride + stride;
-
- let currentWeight = this.cumulativeWeight;
-
- if ( currentWeight === 0 ) {
-
- // accuN := incoming * weight
-
- for ( let i = 0; i !== stride; ++ i ) {
-
- buffer[ offset + i ] = buffer[ i ];
-
- }
-
- currentWeight = weight;
-
- } else {
-
- // accuN := accuN + incoming * weight
-
- currentWeight += weight;
- const mix = weight / currentWeight;
- this._mixBufferRegion( buffer, offset, 0, mix, stride );
-
- }
-
- this.cumulativeWeight = currentWeight;
-
- }
-
- /**
- * Accumulates data in the `incoming` region into `add`.
- *
- * @param {number} weight - The weight.
- */
- accumulateAdditive( weight ) {
-
- const buffer = this.buffer,
- stride = this.valueSize,
- offset = stride * this._addIndex;
-
- if ( this.cumulativeWeightAdditive === 0 ) {
-
- // add = identity
-
- this._setIdentity();
-
- }
-
- // add := add + incoming * weight
-
- this._mixBufferRegionAdditive( buffer, offset, 0, weight, stride );
- this.cumulativeWeightAdditive += weight;
-
- }
-
- /**
- * Applies the state of `accu` to the binding when accus differ.
- *
- * @param {number} accuIndex - The accumulation index.
- */
- apply( accuIndex ) {
-
- const stride = this.valueSize,
- buffer = this.buffer,
- offset = accuIndex * stride + stride,
-
- weight = this.cumulativeWeight,
- weightAdditive = this.cumulativeWeightAdditive,
-
- binding = this.binding;
-
- this.cumulativeWeight = 0;
- this.cumulativeWeightAdditive = 0;
-
- if ( weight < 1 ) {
-
- // accuN := accuN + original * ( 1 - cumulativeWeight )
-
- const originalValueOffset = stride * this._origIndex;
-
- this._mixBufferRegion(
- buffer, offset, originalValueOffset, 1 - weight, stride );
-
- }
-
- if ( weightAdditive > 0 ) {
-
- // accuN := accuN + additive accuN
-
- this._mixBufferRegionAdditive( buffer, offset, this._addIndex * stride, 1, stride );
-
- }
-
- for ( let i = stride, e = stride + stride; i !== e; ++ i ) {
-
- if ( buffer[ i ] !== buffer[ i + stride ] ) {
-
- // value has changed -> update scene graph
-
- binding.setValue( buffer, offset );
- break;
-
- }
-
- }
-
- }
-
-
- /**
- * Remembers the state of the bound property and copy it to both accus.
- */
- saveOriginalState() {
-
- const binding = this.binding;
-
- const buffer = this.buffer,
- stride = this.valueSize,
-
- originalValueOffset = stride * this._origIndex;
-
- binding.getValue( buffer, originalValueOffset );
-
- // accu[0..1] := orig -- initially detect changes against the original
- for ( let i = stride, e = originalValueOffset; i !== e; ++ i ) {
-
- buffer[ i ] = buffer[ originalValueOffset + ( i % stride ) ];
-
- }
-
- // Add to identity for additive
- this._setIdentity();
-
- this.cumulativeWeight = 0;
- this.cumulativeWeightAdditive = 0;
-
- }
-
- /**
- * Applies the state previously taken via {@link PropertyMixer#saveOriginalState} to the binding.
- */
- restoreOriginalState() {
-
- const originalValueOffset = this.valueSize * 3;
- this.binding.setValue( this.buffer, originalValueOffset );
-
- }
-
- // internals
-
- _setAdditiveIdentityNumeric() {
-
- const startIndex = this._addIndex * this.valueSize;
- const endIndex = startIndex + this.valueSize;
-
- for ( let i = startIndex; i < endIndex; i ++ ) {
-
- this.buffer[ i ] = 0;
-
- }
-
- }
-
- _setAdditiveIdentityQuaternion() {
-
- this._setAdditiveIdentityNumeric();
- this.buffer[ this._addIndex * this.valueSize + 3 ] = 1;
-
- }
-
- _setAdditiveIdentityOther() {
-
- const startIndex = this._origIndex * this.valueSize;
- const targetIndex = this._addIndex * this.valueSize;
-
- for ( let i = 0; i < this.valueSize; i ++ ) {
-
- this.buffer[ targetIndex + i ] = this.buffer[ startIndex + i ];
-
- }
-
- }
-
-
- // mix functions
-
- _select( buffer, dstOffset, srcOffset, t, stride ) {
-
- if ( t >= 0.5 ) {
-
- for ( let i = 0; i !== stride; ++ i ) {
-
- buffer[ dstOffset + i ] = buffer[ srcOffset + i ];
-
- }
-
- }
-
- }
-
- _slerp( buffer, dstOffset, srcOffset, t ) {
-
- Quaternion.slerpFlat( buffer, dstOffset, buffer, dstOffset, buffer, srcOffset, t );
-
- }
-
- _slerpAdditive( buffer, dstOffset, srcOffset, t, stride ) {
-
- const workOffset = this._workIndex * stride;
-
- // Store result in intermediate buffer offset
- Quaternion.multiplyQuaternionsFlat( buffer, workOffset, buffer, dstOffset, buffer, srcOffset );
-
- // Slerp to the intermediate result
- Quaternion.slerpFlat( buffer, dstOffset, buffer, dstOffset, buffer, workOffset, t );
-
- }
-
- _lerp( buffer, dstOffset, srcOffset, t, stride ) {
-
- const s = 1 - t;
-
- for ( let i = 0; i !== stride; ++ i ) {
-
- const j = dstOffset + i;
-
- buffer[ j ] = buffer[ j ] * s + buffer[ srcOffset + i ] * t;
-
- }
-
- }
-
- _lerpAdditive( buffer, dstOffset, srcOffset, t, stride ) {
-
- for ( let i = 0; i !== stride; ++ i ) {
-
- const j = dstOffset + i;
-
- buffer[ j ] = buffer[ j ] + buffer[ srcOffset + i ] * t;
-
- }
-
- }
-
-}
-
-// Characters [].:/ are reserved for track binding syntax.
-const _RESERVED_CHARS_RE = '\\[\\]\\.:\\/';
-const _reservedRe = new RegExp( '[' + _RESERVED_CHARS_RE + ']', 'g' );
-
-// Attempts to allow node names from any language. ES5's `\w` regexp matches
-// only latin characters, and the unicode \p{L} is not yet supported. So
-// instead, we exclude reserved characters and match everything else.
-const _wordChar = '[^' + _RESERVED_CHARS_RE + ']';
-const _wordCharOrDot = '[^' + _RESERVED_CHARS_RE.replace( '\\.', '' ) + ']';
-
-// Parent directories, delimited by '/' or ':'. Currently unused, but must
-// be matched to parse the rest of the track name.
-const _directoryRe = /*@__PURE__*/ /((?:WC+[\/:])*)/.source.replace( 'WC', _wordChar );
-
-// Target node. May contain word characters (a-zA-Z0-9_) and '.' or '-'.
-const _nodeRe = /*@__PURE__*/ /(WCOD+)?/.source.replace( 'WCOD', _wordCharOrDot );
-
-// Object on target node, and accessor. May not contain reserved
-// characters. Accessor may contain any character except closing bracket.
-const _objectRe = /*@__PURE__*/ /(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace( 'WC', _wordChar );
-
-// Property and accessor. May not contain reserved characters. Accessor may
-// contain any non-bracket characters.
-const _propertyRe = /*@__PURE__*/ /\.(WC+)(?:\[(.+)\])?/.source.replace( 'WC', _wordChar );
-
-const _trackRe = new RegExp( ''
- + '^'
- + _directoryRe
- + _nodeRe
- + _objectRe
- + _propertyRe
- + '$'
-);
-
-const _supportedObjectNames = [ 'material', 'materials', 'bones', 'map' ];
-
-class Composite {
-
- constructor( targetGroup, path, optionalParsedPath ) {
-
- const parsedPath = optionalParsedPath || PropertyBinding.parseTrackName( path );
-
- this._targetGroup = targetGroup;
- this._bindings = targetGroup.subscribe_( path, parsedPath );
-
- }
-
- getValue( array, offset ) {
-
- this.bind(); // bind all binding
-
- const firstValidIndex = this._targetGroup.nCachedObjects_,
- binding = this._bindings[ firstValidIndex ];
-
- // and only call .getValue on the first
- if ( binding !== undefined ) binding.getValue( array, offset );
-
- }
-
- setValue( array, offset ) {
-
- const bindings = this._bindings;
-
- for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {
-
- bindings[ i ].setValue( array, offset );
-
- }
-
- }
-
- bind() {
-
- const bindings = this._bindings;
-
- for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {
-
- bindings[ i ].bind();
-
- }
-
- }
-
- unbind() {
-
- const bindings = this._bindings;
-
- for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {
-
- bindings[ i ].unbind();
-
- }
-
- }
-
-}
-
-// Note: This class uses a State pattern on a per-method basis:
-// 'bind' sets 'this.getValue' / 'setValue' and shadows the
-// prototype version of these methods with one that represents
-// the bound state. When the property is not found, the methods
-// become no-ops.
-
-
-/**
- * This holds a reference to a real property in the scene graph; used internally.
- */
-class PropertyBinding {
-
- /**
- * Constructs a new property binding.
- *
- * @param {Object} rootNode - The root node.
- * @param {string} path - The path.
- * @param {?Object} [parsedPath] - The parsed path.
- */
- constructor( rootNode, path, parsedPath ) {
-
- /**
- * The object path to the animated property.
- *
- * @type {string}
- */
- this.path = path;
-
- /**
- * An object holding information about the path.
- *
- * @type {Object}
- */
- this.parsedPath = parsedPath || PropertyBinding.parseTrackName( path );
-
- /**
- * The object owns the animated property.
- *
- * @type {?Object}
- */
- this.node = PropertyBinding.findNode( rootNode, this.parsedPath.nodeName );
-
- /**
- * The root node.
- *
- * @type {Object3D|Skeleton}
- */
- this.rootNode = rootNode;
-
- // initial state of these methods that calls 'bind'
- this.getValue = this._getValue_unbound;
- this.setValue = this._setValue_unbound;
-
- }
-
-
- /**
- * Factory method for creating a property binding from the given parameters.
- *
- * @static
- * @param {Object} root - The root node.
- * @param {string} path - The path.
- * @param {?Object} [parsedPath] - The parsed path.
- * @return {PropertyBinding|Composite} The created property binding or composite.
- */
- static create( root, path, parsedPath ) {
-
- if ( ! ( root && root.isAnimationObjectGroup ) ) {
-
- return new PropertyBinding( root, path, parsedPath );
-
- } else {
-
- return new PropertyBinding.Composite( root, path, parsedPath );
-
- }
-
- }
-
- /**
- * Replaces spaces with underscores and removes unsupported characters from
- * node names, to ensure compatibility with parseTrackName().
- *
- * @param {string} name - Node name to be sanitized.
- * @return {string} The sanitized node name.
- */
- static sanitizeNodeName( name ) {
-
- return name.replace( /\s/g, '_' ).replace( _reservedRe, '' );
-
- }
-
- /**
- * Parses the given track name (an object path to an animated property) and
- * returns an object with information about the path. Matches strings in the following forms:
- *
- * - nodeName.property
- * - nodeName.property[accessor]
- * - nodeName.material.property[accessor]
- * - uuid.property[accessor]
- * - uuid.objectName[objectIndex].propertyName[propertyIndex]
- * - parentName/nodeName.property
- * - parentName/parentName/nodeName.property[index]
- * - .bone[Armature.DEF_cog].position
- * - scene:helium_balloon_model:helium_balloon_model.position
- *
- * @static
- * @param {string} trackName - The track name to parse.
- * @return {Object} The parsed track name as an object.
- */
- static parseTrackName( trackName ) {
-
- const matches = _trackRe.exec( trackName );
-
- if ( matches === null ) {
-
- throw new Error( 'THREE.PropertyBinding: Cannot parse trackName: ' + trackName );
-
- }
-
- const results = {
- // directoryName: matches[ 1 ], // (tschw) currently unused
- nodeName: matches[ 2 ],
- objectName: matches[ 3 ],
- objectIndex: matches[ 4 ],
- propertyName: matches[ 5 ], // required
- propertyIndex: matches[ 6 ]
- };
-
- const lastDot = results.nodeName && results.nodeName.lastIndexOf( '.' );
-
- if ( lastDot !== undefined && lastDot !== -1 ) {
-
- const objectName = results.nodeName.substring( lastDot + 1 );
-
- // Object names must be checked against an allowlist. Otherwise, there
- // is no way to parse 'foo.bar.baz': 'baz' must be a property, but
- // 'bar' could be the objectName, or part of a nodeName (which can
- // include '.' characters).
- if ( _supportedObjectNames.indexOf( objectName ) !== -1 ) {
-
- results.nodeName = results.nodeName.substring( 0, lastDot );
- results.objectName = objectName;
-
- }
-
- }
-
- if ( results.propertyName === null || results.propertyName.length === 0 ) {
-
- throw new Error( 'THREE.PropertyBinding: can not parse propertyName from trackName: ' + trackName );
-
- }
-
- return results;
-
- }
-
- /**
- * Searches for a node in the hierarchy of the given root object by the given
- * node name.
- *
- * @static
- * @param {Object} root - The root object.
- * @param {string|number} nodeName - The name of the node.
- * @return {?Object} The found node. Returns `null` if no object was found.
- */
- static findNode( root, nodeName ) {
-
- if ( nodeName === undefined || nodeName === '' || nodeName === '.' || nodeName === -1 || nodeName === root.name || nodeName === root.uuid ) {
-
- return root;
-
- }
-
- // search into skeleton bones.
- if ( root.skeleton ) {
-
- const bone = root.skeleton.getBoneByName( nodeName );
-
- if ( bone !== undefined ) {
-
- return bone;
-
- }
-
- }
-
- // search into node subtree.
- if ( root.children ) {
-
- const searchNodeSubtree = function ( children ) {
-
- for ( let i = 0; i < children.length; i ++ ) {
-
- const childNode = children[ i ];
-
- if ( childNode.name === nodeName || childNode.uuid === nodeName ) {
-
- return childNode;
-
- }
-
- const result = searchNodeSubtree( childNode.children );
-
- if ( result ) return result;
-
- }
-
- return null;
-
- };
-
- const subTreeNode = searchNodeSubtree( root.children );
-
- if ( subTreeNode ) {
-
- return subTreeNode;
-
- }
-
- }
-
- return null;
-
- }
-
- // these are used to "bind" a nonexistent property
- _getValue_unavailable() {}
- _setValue_unavailable() {}
-
- // Getters
-
- _getValue_direct( buffer, offset ) {
-
- buffer[ offset ] = this.targetObject[ this.propertyName ];
-
- }
-
- _getValue_array( buffer, offset ) {
-
- const source = this.resolvedProperty;
-
- for ( let i = 0, n = source.length; i !== n; ++ i ) {
-
- buffer[ offset ++ ] = source[ i ];
-
- }
-
- }
-
- _getValue_arrayElement( buffer, offset ) {
-
- buffer[ offset ] = this.resolvedProperty[ this.propertyIndex ];
-
- }
-
- _getValue_toArray( buffer, offset ) {
-
- this.resolvedProperty.toArray( buffer, offset );
-
- }
-
- // Direct
-
- _setValue_direct( buffer, offset ) {
-
- this.targetObject[ this.propertyName ] = buffer[ offset ];
-
- }
-
- _setValue_direct_setNeedsUpdate( buffer, offset ) {
-
- this.targetObject[ this.propertyName ] = buffer[ offset ];
- this.targetObject.needsUpdate = true;
-
- }
-
- _setValue_direct_setMatrixWorldNeedsUpdate( buffer, offset ) {
-
- this.targetObject[ this.propertyName ] = buffer[ offset ];
- this.targetObject.matrixWorldNeedsUpdate = true;
-
- }
-
- // EntireArray
-
- _setValue_array( buffer, offset ) {
-
- const dest = this.resolvedProperty;
-
- for ( let i = 0, n = dest.length; i !== n; ++ i ) {
-
- dest[ i ] = buffer[ offset ++ ];
-
- }
-
- }
-
- _setValue_array_setNeedsUpdate( buffer, offset ) {
-
- const dest = this.resolvedProperty;
-
- for ( let i = 0, n = dest.length; i !== n; ++ i ) {
-
- dest[ i ] = buffer[ offset ++ ];
-
- }
-
- this.targetObject.needsUpdate = true;
-
- }
-
- _setValue_array_setMatrixWorldNeedsUpdate( buffer, offset ) {
-
- const dest = this.resolvedProperty;
-
- for ( let i = 0, n = dest.length; i !== n; ++ i ) {
-
- dest[ i ] = buffer[ offset ++ ];
-
- }
-
- this.targetObject.matrixWorldNeedsUpdate = true;
-
- }
-
- // ArrayElement
-
- _setValue_arrayElement( buffer, offset ) {
-
- this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
-
- }
-
- _setValue_arrayElement_setNeedsUpdate( buffer, offset ) {
-
- this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
- this.targetObject.needsUpdate = true;
-
- }
-
- _setValue_arrayElement_setMatrixWorldNeedsUpdate( buffer, offset ) {
-
- this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
- this.targetObject.matrixWorldNeedsUpdate = true;
-
- }
-
- // HasToFromArray
-
- _setValue_fromArray( buffer, offset ) {
-
- this.resolvedProperty.fromArray( buffer, offset );
-
- }
-
- _setValue_fromArray_setNeedsUpdate( buffer, offset ) {
-
- this.resolvedProperty.fromArray( buffer, offset );
- this.targetObject.needsUpdate = true;
-
- }
-
- _setValue_fromArray_setMatrixWorldNeedsUpdate( buffer, offset ) {
-
- this.resolvedProperty.fromArray( buffer, offset );
- this.targetObject.matrixWorldNeedsUpdate = true;
-
- }
-
- _getValue_unbound( targetArray, offset ) {
-
- this.bind();
- this.getValue( targetArray, offset );
-
- }
-
- _setValue_unbound( sourceArray, offset ) {
-
- this.bind();
- this.setValue( sourceArray, offset );
-
- }
-
- /**
- * Creates a getter / setter pair for the property tracked by this binding.
- */
- bind() {
-
- let targetObject = this.node;
- const parsedPath = this.parsedPath;
-
- const objectName = parsedPath.objectName;
- const propertyName = parsedPath.propertyName;
- let propertyIndex = parsedPath.propertyIndex;
-
- if ( ! targetObject ) {
-
- targetObject = PropertyBinding.findNode( this.rootNode, parsedPath.nodeName );
-
- this.node = targetObject;
-
- }
-
- // set fail state so we can just 'return' on error
- this.getValue = this._getValue_unavailable;
- this.setValue = this._setValue_unavailable;
-
- // ensure there is a value node
- if ( ! targetObject ) {
-
- warn( 'PropertyBinding: No target node found for track: ' + this.path + '.' );
- return;
-
- }
-
- if ( objectName ) {
-
- let objectIndex = parsedPath.objectIndex;
-
- // special cases were we need to reach deeper into the hierarchy to get the face materials....
- switch ( objectName ) {
-
- case 'materials':
-
- if ( ! targetObject.material ) {
-
- error( 'PropertyBinding: Can not bind to material as node does not have a material.', this );
- return;
-
- }
-
- if ( ! targetObject.material.materials ) {
-
- error( 'PropertyBinding: Can not bind to material.materials as node.material does not have a materials array.', this );
- return;
-
- }
-
- targetObject = targetObject.material.materials;
-
- break;
-
- case 'bones':
-
- if ( ! targetObject.skeleton ) {
-
- error( 'PropertyBinding: Can not bind to bones as node does not have a skeleton.', this );
- return;
-
- }
-
- // potential future optimization: skip this if propertyIndex is already an integer
- // and convert the integer string to a true integer.
-
- targetObject = targetObject.skeleton.bones;
-
- // support resolving morphTarget names into indices.
- for ( let i = 0; i < targetObject.length; i ++ ) {
-
- if ( targetObject[ i ].name === objectIndex ) {
-
- objectIndex = i;
- break;
-
- }
-
- }
-
- break;
-
- case 'map':
-
- if ( 'map' in targetObject ) {
-
- targetObject = targetObject.map;
- break;
-
- }
-
- if ( ! targetObject.material ) {
-
- error( 'PropertyBinding: Can not bind to material as node does not have a material.', this );
- return;
-
- }
-
- if ( ! targetObject.material.map ) {
-
- error( 'PropertyBinding: Can not bind to material.map as node.material does not have a map.', this );
- return;
-
- }
-
- targetObject = targetObject.material.map;
- break;
-
- default:
-
- if ( targetObject[ objectName ] === undefined ) {
-
- error( 'PropertyBinding: Can not bind to objectName of node undefined.', this );
- return;
-
- }
-
- targetObject = targetObject[ objectName ];
-
- }
-
-
- if ( objectIndex !== undefined ) {
-
- if ( targetObject[ objectIndex ] === undefined ) {
-
- error( 'PropertyBinding: Trying to bind to objectIndex of objectName, but is undefined.', this, targetObject );
- return;
-
- }
-
- targetObject = targetObject[ objectIndex ];
-
- }
-
- }
-
- // resolve property
- const nodeProperty = targetObject[ propertyName ];
-
- if ( nodeProperty === undefined ) {
-
- const nodeName = parsedPath.nodeName;
-
- error( 'PropertyBinding: Trying to update property for track: ' + nodeName +
- '.' + propertyName + ' but it wasn\'t found.', targetObject );
- return;
-
- }
-
- // determine versioning scheme
- let versioning = this.Versioning.None;
-
- this.targetObject = targetObject;
-
- if ( targetObject.isMaterial === true ) {
-
- versioning = this.Versioning.NeedsUpdate;
-
- } else if ( targetObject.isObject3D === true ) {
-
- versioning = this.Versioning.MatrixWorldNeedsUpdate;
-
- }
-
- // determine how the property gets bound
- let bindingType = this.BindingType.Direct;
-
- if ( propertyIndex !== undefined ) {
-
- // access a sub element of the property array (only primitives are supported right now)
-
- if ( propertyName === 'morphTargetInfluences' ) {
-
- // potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.
-
- // support resolving morphTarget names into indices.
- if ( ! targetObject.geometry ) {
-
- error( 'PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.', this );
- return;
-
- }
-
- if ( ! targetObject.geometry.morphAttributes ) {
-
- error( 'PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.morphAttributes.', this );
- return;
-
- }
-
- if ( targetObject.morphTargetDictionary[ propertyIndex ] !== undefined ) {
-
- propertyIndex = targetObject.morphTargetDictionary[ propertyIndex ];
-
- }
-
- }
-
- bindingType = this.BindingType.ArrayElement;
-
- this.resolvedProperty = nodeProperty;
- this.propertyIndex = propertyIndex;
-
- } else if ( nodeProperty.fromArray !== undefined && nodeProperty.toArray !== undefined ) {
-
- // must use copy for Object3D.Euler/Quaternion
-
- bindingType = this.BindingType.HasFromToArray;
-
- this.resolvedProperty = nodeProperty;
-
- } else if ( Array.isArray( nodeProperty ) ) {
-
- bindingType = this.BindingType.EntireArray;
-
- this.resolvedProperty = nodeProperty;
-
- } else {
-
- this.propertyName = propertyName;
-
- }
-
- // select getter / setter
- this.getValue = this.GetterByBindingType[ bindingType ];
- this.setValue = this.SetterByBindingTypeAndVersioning[ bindingType ][ versioning ];
-
- }
-
- /**
- * Unbinds the property.
- */
- unbind() {
-
- this.node = null;
-
- // back to the prototype version of getValue / setValue
- // note: avoiding to mutate the shape of 'this' via 'delete'
- this.getValue = this._getValue_unbound;
- this.setValue = this._setValue_unbound;
-
- }
-
-}
-
-PropertyBinding.Composite = Composite;
-
-PropertyBinding.prototype.BindingType = {
- Direct: 0,
- EntireArray: 1,
- ArrayElement: 2,
- HasFromToArray: 3
-};
-
-PropertyBinding.prototype.Versioning = {
- None: 0,
- NeedsUpdate: 1,
- MatrixWorldNeedsUpdate: 2
-};
-
-PropertyBinding.prototype.GetterByBindingType = [
-
- PropertyBinding.prototype._getValue_direct,
- PropertyBinding.prototype._getValue_array,
- PropertyBinding.prototype._getValue_arrayElement,
- PropertyBinding.prototype._getValue_toArray,
-
-];
-
-PropertyBinding.prototype.SetterByBindingTypeAndVersioning = [
-
- [
- // Direct
- PropertyBinding.prototype._setValue_direct,
- PropertyBinding.prototype._setValue_direct_setNeedsUpdate,
- PropertyBinding.prototype._setValue_direct_setMatrixWorldNeedsUpdate,
-
- ], [
-
- // EntireArray
-
- PropertyBinding.prototype._setValue_array,
- PropertyBinding.prototype._setValue_array_setNeedsUpdate,
- PropertyBinding.prototype._setValue_array_setMatrixWorldNeedsUpdate,
-
- ], [
-
- // ArrayElement
- PropertyBinding.prototype._setValue_arrayElement,
- PropertyBinding.prototype._setValue_arrayElement_setNeedsUpdate,
- PropertyBinding.prototype._setValue_arrayElement_setMatrixWorldNeedsUpdate,
-
- ], [
-
- // HasToFromArray
- PropertyBinding.prototype._setValue_fromArray,
- PropertyBinding.prototype._setValue_fromArray_setNeedsUpdate,
- PropertyBinding.prototype._setValue_fromArray_setMatrixWorldNeedsUpdate,
-
- ]
-
-];
-
-/**
- * A group of objects that receives a shared animation state.
- *
- * Usage:
- *
- * - Add objects you would otherwise pass as 'root' to the
- * constructor or the .clipAction method of AnimationMixer.
- * - Instead pass this object as 'root'.
- * - You can also add and remove objects later when the mixer is running.
- *
- * Note:
- *
- * - Objects of this class appear as one object to the mixer,
- * so cache control of the individual objects must be done on the group.
- *
- * Limitation:
- *
- * - The animated properties must be compatible among the all objects in the group.
- * - A single property can either be controlled through a target group or directly, but not both.
- */
-class AnimationObjectGroup {
-
- /**
- * Constructs a new animation group.
- *
- * @param {...Object3D} arguments - An arbitrary number of 3D objects that share the same animation state.
- */
- constructor() {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isAnimationObjectGroup = true;
-
- /**
- * The UUID of the 3D object.
- *
- * @type {string}
- * @readonly
- */
- this.uuid = generateUUID();
-
- // cached objects followed by the active ones
- this._objects = Array.prototype.slice.call( arguments );
-
- this.nCachedObjects_ = 0; // threshold
- // note: read by PropertyBinding.Composite
-
- const indices = {};
- this._indicesByUUID = indices; // for bookkeeping
-
- for ( let i = 0, n = arguments.length; i !== n; ++ i ) {
-
- indices[ arguments[ i ].uuid ] = i;
-
- }
-
- this._paths = []; // inside: string
- this._parsedPaths = []; // inside: { we don't care, here }
- this._bindings = []; // inside: Array< PropertyBinding >
- this._bindingsIndicesByPath = {}; // inside: indices in these arrays
-
- const scope = this;
-
- this.stats = {
-
- objects: {
- get total() {
-
- return scope._objects.length;
-
- },
- get inUse() {
-
- return this.total - scope.nCachedObjects_;
-
- }
- },
- get bindingsPerObject() {
-
- return scope._bindings.length;
-
- }
-
- };
-
- }
-
- /**
- * Adds an arbitrary number of objects to this animation group.
- *
- * @param {...Object3D} arguments - The 3D objects to add.
- */
- add() {
-
- const objects = this._objects,
- indicesByUUID = this._indicesByUUID,
- paths = this._paths,
- parsedPaths = this._parsedPaths,
- bindings = this._bindings,
- nBindings = bindings.length;
-
- let knownObject = undefined,
- nObjects = objects.length,
- nCachedObjects = this.nCachedObjects_;
-
- for ( let i = 0, n = arguments.length; i !== n; ++ i ) {
-
- const object = arguments[ i ],
- uuid = object.uuid;
- let index = indicesByUUID[ uuid ];
-
- if ( index === undefined ) {
-
- // unknown object -> add it to the ACTIVE region
-
- index = nObjects ++;
- indicesByUUID[ uuid ] = index;
- objects.push( object );
-
- // accounting is done, now do the same for all bindings
-
- for ( let j = 0, m = nBindings; j !== m; ++ j ) {
-
- bindings[ j ].push( new PropertyBinding( object, paths[ j ], parsedPaths[ j ] ) );
-
- }
-
- } else if ( index < nCachedObjects ) {
-
- knownObject = objects[ index ];
-
- // move existing object to the ACTIVE region
-
- const firstActiveIndex = -- nCachedObjects,
- lastCachedObject = objects[ firstActiveIndex ];
-
- indicesByUUID[ lastCachedObject.uuid ] = index;
- objects[ index ] = lastCachedObject;
-
- indicesByUUID[ uuid ] = firstActiveIndex;
- objects[ firstActiveIndex ] = object;
-
- // accounting is done, now do the same for all bindings
-
- for ( let j = 0, m = nBindings; j !== m; ++ j ) {
-
- const bindingsForPath = bindings[ j ],
- lastCached = bindingsForPath[ firstActiveIndex ];
-
- let binding = bindingsForPath[ index ];
-
- bindingsForPath[ index ] = lastCached;
-
- if ( binding === undefined ) {
-
- // since we do not bother to create new bindings
- // for objects that are cached, the binding may
- // or may not exist
-
- binding = new PropertyBinding( object, paths[ j ], parsedPaths[ j ] );
-
- }
-
- bindingsForPath[ firstActiveIndex ] = binding;
-
- }
-
- } else if ( objects[ index ] !== knownObject ) {
-
- error( 'AnimationObjectGroup: Different objects with the same UUID ' +
- 'detected. Clean the caches or recreate your infrastructure when reloading scenes.' );
-
- } // else the object is already where we want it to be
-
- } // for arguments
-
- this.nCachedObjects_ = nCachedObjects;
-
- }
-
- /**
- * Removes an arbitrary number of objects to this animation group
- *
- * @param {...Object3D} arguments - The 3D objects to remove.
- */
- remove() {
-
- const objects = this._objects,
- indicesByUUID = this._indicesByUUID,
- bindings = this._bindings,
- nBindings = bindings.length;
-
- let nCachedObjects = this.nCachedObjects_;
-
- for ( let i = 0, n = arguments.length; i !== n; ++ i ) {
-
- const object = arguments[ i ],
- uuid = object.uuid,
- index = indicesByUUID[ uuid ];
-
- if ( index !== undefined && index >= nCachedObjects ) {
-
- // move existing object into the CACHED region
-
- const lastCachedIndex = nCachedObjects ++,
- firstActiveObject = objects[ lastCachedIndex ];
-
- indicesByUUID[ firstActiveObject.uuid ] = index;
- objects[ index ] = firstActiveObject;
-
- indicesByUUID[ uuid ] = lastCachedIndex;
- objects[ lastCachedIndex ] = object;
-
- // accounting is done, now do the same for all bindings
-
- for ( let j = 0, m = nBindings; j !== m; ++ j ) {
-
- const bindingsForPath = bindings[ j ],
- firstActive = bindingsForPath[ lastCachedIndex ],
- binding = bindingsForPath[ index ];
-
- bindingsForPath[ index ] = firstActive;
- bindingsForPath[ lastCachedIndex ] = binding;
-
- }
-
- }
-
- } // for arguments
-
- this.nCachedObjects_ = nCachedObjects;
-
- }
-
- /**
- * Deallocates all memory resources for the passed 3D objects of this animation group.
- *
- * @param {...Object3D} arguments - The 3D objects to uncache.
- */
- uncache() {
-
- const objects = this._objects,
- indicesByUUID = this._indicesByUUID,
- bindings = this._bindings,
- nBindings = bindings.length;
-
- let nCachedObjects = this.nCachedObjects_,
- nObjects = objects.length;
-
- for ( let i = 0, n = arguments.length; i !== n; ++ i ) {
-
- const object = arguments[ i ],
- uuid = object.uuid,
- index = indicesByUUID[ uuid ];
-
- if ( index !== undefined ) {
-
- delete indicesByUUID[ uuid ];
-
- if ( index < nCachedObjects ) {
-
- // object is cached, shrink the CACHED region
-
- const firstActiveIndex = -- nCachedObjects,
- lastCachedObject = objects[ firstActiveIndex ],
- lastIndex = -- nObjects,
- lastObject = objects[ lastIndex ];
-
- if ( index !== firstActiveIndex ) {
-
- // last cached object takes this object's place
-
- indicesByUUID[ lastCachedObject.uuid ] = index;
-
- }
-
- objects[ index ] = lastCachedObject;
-
- if ( firstActiveIndex !== lastIndex ) {
-
- // last object goes to the activated slot and pop
-
- indicesByUUID[ lastObject.uuid ] = firstActiveIndex;
-
- }
-
- objects[ firstActiveIndex ] = lastObject;
- objects.pop();
-
- // accounting is done, now do the same for all bindings
-
- for ( let j = 0, m = nBindings; j !== m; ++ j ) {
-
- const bindingsForPath = bindings[ j ],
- lastCached = bindingsForPath[ firstActiveIndex ],
- last = bindingsForPath[ lastIndex ];
-
- bindingsForPath[ index ] = lastCached;
- bindingsForPath[ firstActiveIndex ] = last;
- bindingsForPath.pop();
-
- }
-
- } else {
-
- // object is active, just swap with the last and pop
-
- const lastIndex = -- nObjects,
- lastObject = objects[ lastIndex ];
-
- if ( index !== lastIndex ) {
-
- indicesByUUID[ lastObject.uuid ] = index;
-
- }
-
- objects[ index ] = lastObject;
- objects.pop();
-
- // accounting is done, now do the same for all bindings
-
- for ( let j = 0, m = nBindings; j !== m; ++ j ) {
-
- const bindingsForPath = bindings[ j ];
-
- bindingsForPath[ index ] = bindingsForPath[ lastIndex ];
- bindingsForPath.pop();
-
- }
-
- } // cached or active
-
- } // if object is known
-
- } // for arguments
-
- this.nCachedObjects_ = nCachedObjects;
-
- }
-
- // Internal interface used by befriended PropertyBinding.Composite:
-
- subscribe_( path, parsedPath ) {
-
- // returns an array of bindings for the given path that is changed
- // according to the contained objects in the group
-
- const indicesByPath = this._bindingsIndicesByPath;
- let index = indicesByPath[ path ];
- const bindings = this._bindings;
-
- if ( index !== undefined ) return bindings[ index ];
-
- const paths = this._paths,
- parsedPaths = this._parsedPaths,
- objects = this._objects,
- nObjects = objects.length,
- nCachedObjects = this.nCachedObjects_,
- bindingsForPath = new Array( nObjects );
-
- index = bindings.length;
-
- indicesByPath[ path ] = index;
-
- paths.push( path );
- parsedPaths.push( parsedPath );
- bindings.push( bindingsForPath );
-
- for ( let i = nCachedObjects, n = objects.length; i !== n; ++ i ) {
-
- const object = objects[ i ];
- bindingsForPath[ i ] = new PropertyBinding( object, path, parsedPath );
-
- }
-
- return bindingsForPath;
-
- }
-
- unsubscribe_( path ) {
-
- // tells the group to forget about a property path and no longer
- // update the array previously obtained with 'subscribe_'
-
- const indicesByPath = this._bindingsIndicesByPath,
- index = indicesByPath[ path ];
-
- if ( index !== undefined ) {
-
- const paths = this._paths,
- parsedPaths = this._parsedPaths,
- bindings = this._bindings,
- lastBindingsIndex = bindings.length - 1,
- lastBindings = bindings[ lastBindingsIndex ],
- lastBindingsPath = paths[ lastBindingsIndex ];
-
- indicesByPath[ lastBindingsPath ] = index;
-
- bindings[ index ] = lastBindings;
- bindings.pop();
-
- parsedPaths[ index ] = parsedPaths[ lastBindingsIndex ];
- parsedPaths.pop();
-
- paths[ index ] = paths[ lastBindingsIndex ];
- paths.pop();
-
- }
-
- }
-
-}
-
-/**
- * An instance of `AnimationAction` schedules the playback of an animation which is
- * stored in {@link AnimationClip}.
- */
-class AnimationAction {
-
- /**
- * Constructs a new animation action.
- *
- * @param {AnimationMixer} mixer - The mixer that is controlled by this action.
- * @param {AnimationClip} clip - The animation clip that holds the actual keyframes.
- * @param {?Object3D} [localRoot=null] - The root object on which this action is performed.
- * @param {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)} [blendMode] - The blend mode.
- */
- constructor( mixer, clip, localRoot = null, blendMode = clip.blendMode ) {
-
- this._mixer = mixer;
- this._clip = clip;
- this._localRoot = localRoot;
-
- /**
- * Defines how the animation is blended/combined when two or more animations
- * are simultaneously played.
- *
- * @type {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)}
- */
- this.blendMode = blendMode;
-
- const tracks = clip.tracks,
- nTracks = tracks.length,
- interpolants = new Array( nTracks );
-
- const interpolantSettings = {
- endingStart: ZeroCurvatureEnding,
- endingEnd: ZeroCurvatureEnding
- };
-
- for ( let i = 0; i !== nTracks; ++ i ) {
-
- const interpolant = tracks[ i ].createInterpolant( null );
- interpolants[ i ] = interpolant;
- interpolant.settings = interpolantSettings;
-
- }
-
- this._interpolantSettings = interpolantSettings;
-
- this._interpolants = interpolants; // bound by the mixer
-
- // inside: PropertyMixer (managed by the mixer)
- this._propertyBindings = new Array( nTracks );
-
- this._cacheIndex = null; // for the memory manager
- this._byClipCacheIndex = null; // for the memory manager
-
- this._timeScaleInterpolant = null;
- this._restoreTimeScale = null;
- this._weightInterpolant = null;
-
- /**
- * The loop mode, set via {@link AnimationAction#setLoop}.
- *
- * @type {(LoopRepeat|LoopOnce|LoopPingPong)}
- * @default LoopRepeat
- */
- this.loop = LoopRepeat;
- this._loopCount = -1;
-
- // global mixer time when the action is to be started
- // it's set back to 'null' upon start of the action
- this._startTime = null;
-
- /**
- * The local time of this action (in seconds, starting with `0`).
- *
- * The value gets clamped or wrapped to `[0,clip.duration]` (according to the
- * loop state).
- *
- * @type {number}
- * @default Infinity
- */
- this.time = 0;
-
- /**
- * Scaling factor for the {@link AnimationAction#time}. A value of `0` causes the
- * animation to pause. Negative values cause the animation to play backwards.
- *
- * @type {number}
- * @default 1
- */
- this.timeScale = 1;
- this._effectiveTimeScale = 1;
-
- /**
- * The degree of influence of this action (in the interval `[0, 1]`). Values
- * between `0` (no impact) and `1` (full impact) can be used to blend between
- * several actions.
- *
- * @type {number}
- * @default 1
- */
- this.weight = 1;
- this._effectiveWeight = 1;
-
- /**
- * The number of repetitions of the performed clip over the course of this action.
- * Can be set via {@link AnimationAction#setLoop}.
- *
- * Setting this number has no effect if {@link AnimationAction#loop} is set to
- * `THREE:LoopOnce`.
- *
- * @type {number}
- * @default Infinity
- */
- this.repetitions = Infinity;
-
- /**
- * If set to `true`, the playback of the action is paused.
- *
- * @type {boolean}
- * @default false
- */
- this.paused = false;
-
- /**
- * If set to `false`, the action is disabled so it has no impact.
- *
- * When the action is re-enabled, the animation continues from its current
- * time (setting `enabled` to `false` doesn't reset the action).
- *
- * @type {boolean}
- * @default true
- */
- this.enabled = true;
-
- /**
- * If set to true the animation will automatically be paused on its last frame.
- *
- * If set to false, {@link AnimationAction#enabled} will automatically be switched
- * to `false` when the last loop of the action has finished, so that this action has
- * no further impact.
- *
- * Note: This member has no impact if the action is interrupted (it
- * has only an effect if its last loop has really finished).
- *
- * @type {boolean}
- * @default false
- */
- this.clampWhenFinished = false;
-
- /**
- * Enables smooth interpolation without separate clips for start, loop and end.
- *
- * @type {boolean}
- * @default true
- */
- this.zeroSlopeAtStart = true;
-
- /**
- * Enables smooth interpolation without separate clips for start, loop and end.
- *
- * @type {boolean}
- * @default true
- */
- this.zeroSlopeAtEnd = true;
-
- }
-
- /**
- * Starts the playback of the animation.
- *
- * @return {AnimationAction} A reference to this animation action.
- */
- play() {
-
- this._mixer._activateAction( this );
-
- return this;
-
- }
-
- /**
- * Stops the playback of the animation.
- *
- * @return {AnimationAction} A reference to this animation action.
- */
- stop() {
-
- this._mixer._deactivateAction( this );
-
- return this.reset();
-
- }
-
- /**
- * Resets the playback of the animation.
- *
- * @return {AnimationAction} A reference to this animation action.
- */
- reset() {
-
- this.paused = false;
- this.enabled = true;
-
- this.time = 0; // restart clip
- this._loopCount = -1;// forget previous loops
- this._startTime = null;// forget scheduling
-
- return this.stopFading().stopWarping();
-
- }
-
- /**
- * Returns `true` if the animation is running.
- *
- * @return {boolean} Whether the animation is running or not.
- */
- isRunning() {
-
- return this.enabled && ! this.paused && this.timeScale !== 0 &&
- this._startTime === null && this._mixer._isActiveAction( this );
-
- }
-
- /**
- * Returns `true` when {@link AnimationAction#play} has been called.
- *
- * @return {boolean} Whether the animation is scheduled or not.
- */
- isScheduled() {
-
- return this._mixer._isActiveAction( this );
-
- }
-
- /**
- * Defines the time when the animation should start.
- *
- * @param {number} time - The start time in seconds.
- * @return {AnimationAction} A reference to this animation action.
- */
- startAt( time ) {
-
- this._startTime = time;
-
- return this;
-
- }
-
- /**
- * Configures the loop settings for this action.
- *
- * @param {(LoopRepeat|LoopOnce|LoopPingPong)} mode - The loop mode.
- * @param {number} repetitions - The number of repetitions.
- * @return {AnimationAction} A reference to this animation action.
- */
- setLoop( mode, repetitions ) {
-
- this.loop = mode;
- this.repetitions = repetitions;
-
- return this;
-
- }
-
- /**
- * Sets the effective weight of this action.
- *
- * An action has no effect and thus an effective weight of zero when the
- * action is disabled.
- *
- * @param {number} weight - The weight to set.
- * @return {AnimationAction} A reference to this animation action.
- */
- setEffectiveWeight( weight ) {
-
- this.weight = weight;
-
- // note: same logic as when updated at runtime
- this._effectiveWeight = this.enabled ? weight : 0;
-
- return this.stopFading();
-
- }
-
- /**
- * Returns the effective weight of this action.
- *
- * @return {number} The effective weight.
- */
- getEffectiveWeight() {
-
- return this._effectiveWeight;
-
- }
-
- /**
- * Fades the animation in by increasing its weight gradually from `0` to `1`,
- * within the passed time interval.
- *
- * @param {number} duration - The duration of the fade.
- * @return {AnimationAction} A reference to this animation action.
- */
- fadeIn( duration ) {
-
- return this._scheduleFading( duration, 0, 1 );
-
- }
-
- /**
- * Fades the animation out by decreasing its weight gradually from `1` to `0`,
- * within the passed time interval.
- *
- * @param {number} duration - The duration of the fade.
- * @return {AnimationAction} A reference to this animation action.
- */
- fadeOut( duration ) {
-
- return this._scheduleFading( duration, 1, 0 );
-
- }
-
- /**
- * Causes this action to fade in and the given action to fade out,
- * within the passed time interval.
- *
- * @param {AnimationAction} fadeOutAction - The animation action to fade out.
- * @param {number} duration - The duration of the fade.
- * @param {boolean} [warp=false] - Whether warping should be used or not.
- * @return {AnimationAction} A reference to this animation action.
- */
- crossFadeFrom( fadeOutAction, duration, warp = false ) {
-
- fadeOutAction.fadeOut( duration );
- this.fadeIn( duration );
-
- if ( warp === true ) {
-
- const fadeInDuration = this._clip.duration,
- fadeOutDuration = fadeOutAction._clip.duration,
-
- startEndRatio = fadeOutDuration / fadeInDuration,
- endStartRatio = fadeInDuration / fadeOutDuration;
-
-
- fadeOutAction._restoreTimeScale = fadeOutAction.timeScale;
- this._restoreTimeScale = this.timeScale;
-
- fadeOutAction.warp( 1.0, startEndRatio, duration );
- this.warp( endStartRatio, 1.0, duration );
-
- }
-
- return this;
-
- }
-
- /**
- * Causes this action to fade out and the given action to fade in,
- * within the passed time interval.
- *
- * @param {AnimationAction} fadeInAction - The animation action to fade in.
- * @param {number} duration - The duration of the fade.
- * @param {boolean} [warp=false] - Whether warping should be used or not.
- * @return {AnimationAction} A reference to this animation action.
- */
- crossFadeTo( fadeInAction, duration, warp = false ) {
-
- return fadeInAction.crossFadeFrom( this, duration, warp );
-
- }
-
- /**
- * Stops any fading which is applied to this action.
- *
- * @return {AnimationAction} A reference to this animation action.
- */
- stopFading() {
-
- const weightInterpolant = this._weightInterpolant;
-
- if ( weightInterpolant !== null ) {
-
- this._weightInterpolant = null;
- this._mixer._takeBackControlInterpolant( weightInterpolant );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the effective time scale of this action.
- *
- * An action has no effect and thus an effective time scale of zero when the
- * action is paused.
- *
- * @param {number} timeScale - The time scale to set.
- * @return {AnimationAction} A reference to this animation action.
- */
- setEffectiveTimeScale( timeScale ) {
-
- this.timeScale = timeScale;
- this._effectiveTimeScale = this.paused ? 0 : timeScale;
-
- return this.stopWarping();
-
- }
-
- /**
- * Returns the effective time scale of this action.
- *
- * @return {number} The effective time scale.
- */
- getEffectiveTimeScale() {
-
- return this._effectiveTimeScale;
-
- }
-
- /**
- * Sets the duration for a single loop of this action.
- *
- * @param {number} duration - The duration to set.
- * @return {AnimationAction} A reference to this animation action.
- */
- setDuration( duration ) {
-
- this.timeScale = this._clip.duration / duration;
-
- return this.stopWarping();
-
- }
-
- /**
- * Synchronizes this action with the passed other action.
- *
- * @param {AnimationAction} action - The action to sync with.
- * @return {AnimationAction} A reference to this animation action.
- */
- syncWith( action ) {
-
- this.time = action.time;
- this.timeScale = action.timeScale;
-
- return this.stopWarping();
-
- }
-
- /**
- * Decelerates this animation's speed to `0` within the passed time interval.
- *
- * @param {number} duration - The duration.
- * @return {AnimationAction} A reference to this animation action.
- */
- halt( duration ) {
-
- return this.warp( this._effectiveTimeScale, 0, duration );
-
- }
-
- /**
- * Changes the playback speed, within the passed time interval, by modifying
- * {@link AnimationAction#timeScale} gradually from `startTimeScale` to
- * `endTimeScale`.
- *
- * @param {number} startTimeScale - The start time scale.
- * @param {number} endTimeScale - The end time scale.
- * @param {number} duration - The duration.
- * @return {AnimationAction} A reference to this animation action.
- */
- warp( startTimeScale, endTimeScale, duration ) {
-
- const mixer = this._mixer,
- now = mixer.time,
- timeScale = this.timeScale;
-
- let interpolant = this._timeScaleInterpolant;
-
- if ( interpolant === null ) {
-
- interpolant = mixer._lendControlInterpolant();
- this._timeScaleInterpolant = interpolant;
-
- }
-
- const times = interpolant.parameterPositions,
- values = interpolant.sampleValues;
-
- times[ 0 ] = now;
- times[ 1 ] = now + duration;
-
- values[ 0 ] = startTimeScale / timeScale;
- values[ 1 ] = endTimeScale / timeScale;
-
- return this;
-
- }
-
- /**
- * Stops any scheduled warping which is applied to this action.
- *
- * @return {AnimationAction} A reference to this animation action.
- */
- stopWarping() {
-
- const timeScaleInterpolant = this._timeScaleInterpolant;
-
- if ( timeScaleInterpolant !== null ) {
-
- this._timeScaleInterpolant = null;
- this._mixer._takeBackControlInterpolant( timeScaleInterpolant );
-
- }
-
- this._restoreTimeScale = null;
-
- return this;
-
- }
-
- /**
- * Returns the animation mixer of this animation action.
- *
- * @return {AnimationMixer} The animation mixer.
- */
- getMixer() {
-
- return this._mixer;
-
- }
-
- /**
- * Returns the animation clip of this animation action.
- *
- * @return {AnimationClip} The animation clip.
- */
- getClip() {
-
- return this._clip;
-
- }
-
- /**
- * Returns the root object of this animation action.
- *
- * @return {Object3D} The root object.
- */
- getRoot() {
-
- return this._localRoot || this._mixer._root;
-
- }
-
- // Internal
-
- _update( time, deltaTime, timeDirection, accuIndex ) {
-
- // called by the mixer
-
- if ( ! this.enabled ) {
-
- // call ._updateWeight() to update ._effectiveWeight
-
- this._updateWeight( time );
- return;
-
- }
-
- const startTime = this._startTime;
-
- if ( startTime !== null ) {
-
- // check for scheduled start of action
-
- const timeRunning = ( time - startTime ) * timeDirection;
- if ( timeRunning < 0 || timeDirection === 0 ) {
-
- deltaTime = 0;
-
- } else {
-
-
- this._startTime = null; // unschedule
- deltaTime = timeDirection * timeRunning;
-
- }
-
- }
-
- // apply time scale and advance time
-
- deltaTime *= this._updateTimeScale( time );
- const clipTime = this._updateTime( deltaTime );
-
- // note: _updateTime may disable the action resulting in
- // an effective weight of 0
-
- const weight = this._updateWeight( time );
-
- if ( weight > 0 ) {
-
- const interpolants = this._interpolants;
- const propertyMixers = this._propertyBindings;
-
- switch ( this.blendMode ) {
-
- case AdditiveAnimationBlendMode:
-
- for ( let j = 0, m = interpolants.length; j !== m; ++ j ) {
-
- interpolants[ j ].evaluate( clipTime );
- propertyMixers[ j ].accumulateAdditive( weight );
-
- }
-
- break;
-
- case NormalAnimationBlendMode:
- default:
-
- for ( let j = 0, m = interpolants.length; j !== m; ++ j ) {
-
- interpolants[ j ].evaluate( clipTime );
- propertyMixers[ j ].accumulate( accuIndex, weight );
-
- }
-
- }
-
- }
-
- }
-
- _updateWeight( time ) {
-
- let weight = 0;
-
- if ( this.enabled ) {
-
- weight = this.weight;
- const interpolant = this._weightInterpolant;
-
- if ( interpolant !== null ) {
-
- const interpolantValue = interpolant.evaluate( time )[ 0 ];
-
- weight *= interpolantValue;
-
- if ( time > interpolant.parameterPositions[ 1 ] ) {
-
- this.stopFading();
-
- if ( interpolantValue === 0 ) {
-
- // faded out, disable
- this.enabled = false;
-
- }
-
- }
-
- }
-
- }
-
- this._effectiveWeight = weight;
- return weight;
-
- }
-
- _updateTimeScale( time ) {
-
- let timeScale = 0;
-
- if ( ! this.paused ) {
-
- timeScale = this.timeScale;
-
- const interpolant = this._timeScaleInterpolant;
-
- if ( interpolant !== null ) {
-
- const interpolantValue = interpolant.evaluate( time )[ 0 ];
-
- timeScale *= interpolantValue;
-
- if ( time > interpolant.parameterPositions[ 1 ] ) {
-
- if ( timeScale === 0 ) {
-
- // motion has halted, pause
- this.paused = true;
-
- } else {
-
- if ( this._restoreTimeScale !== null ) {
-
- timeScale = this._restoreTimeScale;
-
- }
-
- // warp done - apply final time scale
- this.timeScale = timeScale;
-
- }
-
- this.stopWarping();
-
- }
-
- }
-
- }
-
- this._effectiveTimeScale = timeScale;
- return timeScale;
-
- }
-
- _updateTime( deltaTime ) {
-
- const duration = this._clip.duration;
- const loop = this.loop;
-
- let time = this.time + deltaTime;
- let loopCount = this._loopCount;
-
- const pingPong = ( loop === LoopPingPong );
-
- if ( deltaTime === 0 ) {
-
- if ( loopCount === -1 ) return time;
-
- return ( pingPong && ( loopCount & 1 ) === 1 ) ? duration - time : time;
-
- }
-
- if ( loop === LoopOnce ) {
-
- if ( loopCount === -1 ) {
-
- // just started
-
- this._loopCount = 0;
- this._setEndings( true, true, false );
-
- }
-
- handle_stop: {
-
- if ( time >= duration ) {
-
- time = duration;
-
- } else if ( time < 0 ) {
-
- time = 0;
-
- } else {
-
- this.time = time;
-
- break handle_stop;
-
- }
-
- if ( this.clampWhenFinished ) this.paused = true;
- else this.enabled = false;
-
- this.time = time;
-
- this._mixer.dispatchEvent( {
- type: 'finished', action: this,
- direction: deltaTime < 0 ? -1 : 1
- } );
-
- }
-
- } else { // repetitive Repeat or PingPong
-
- if ( loopCount === -1 ) {
-
- // just started
-
- if ( deltaTime >= 0 ) {
-
- loopCount = 0;
-
- this._setEndings( true, this.repetitions === 0, pingPong );
-
- } else {
-
- // when looping in reverse direction, the initial
- // transition through zero counts as a repetition,
- // so leave loopCount at -1
-
- this._setEndings( this.repetitions === 0, true, pingPong );
-
- }
-
- }
-
- if ( time >= duration || time < 0 ) {
-
- // wrap around
-
- const loopDelta = Math.floor( time / duration ); // signed
- time -= duration * loopDelta;
-
- loopCount += Math.abs( loopDelta );
-
- const pending = this.repetitions - loopCount;
-
- if ( pending <= 0 ) {
-
- // have to stop (switch state, clamp time, fire event)
-
- if ( this.clampWhenFinished ) this.paused = true;
- else this.enabled = false;
-
- time = deltaTime > 0 ? duration : 0;
-
- this.time = time;
-
- this._mixer.dispatchEvent( {
- type: 'finished', action: this,
- direction: deltaTime > 0 ? 1 : -1
- } );
-
- } else {
-
- // keep running
-
- if ( pending === 1 ) {
-
- // entering the last round
-
- const atStart = deltaTime < 0;
- this._setEndings( atStart, ! atStart, pingPong );
-
- } else {
-
- this._setEndings( false, false, pingPong );
-
- }
-
- this._loopCount = loopCount;
-
- this.time = time;
-
- this._mixer.dispatchEvent( {
- type: 'loop', action: this, loopDelta: loopDelta
- } );
-
- }
-
- } else {
-
- this._loopCount = loopCount;
- this.time = time;
-
- }
-
- if ( pingPong && ( loopCount & 1 ) === 1 ) {
-
- // invert time for the "pong round"
-
- return duration - time;
-
- }
-
- }
-
- return time;
-
- }
-
- _setEndings( atStart, atEnd, pingPong ) {
-
- const settings = this._interpolantSettings;
-
- if ( pingPong ) {
-
- settings.endingStart = ZeroSlopeEnding;
- settings.endingEnd = ZeroSlopeEnding;
-
- } else {
-
- // assuming for LoopOnce atStart == atEnd == true
-
- if ( atStart ) {
-
- settings.endingStart = this.zeroSlopeAtStart ? ZeroSlopeEnding : ZeroCurvatureEnding;
-
- } else {
-
- settings.endingStart = WrapAroundEnding;
-
- }
-
- if ( atEnd ) {
-
- settings.endingEnd = this.zeroSlopeAtEnd ? ZeroSlopeEnding : ZeroCurvatureEnding;
-
- } else {
-
- settings.endingEnd = WrapAroundEnding;
-
- }
-
- }
-
- }
-
- _scheduleFading( duration, weightNow, weightThen ) {
-
- const mixer = this._mixer, now = mixer.time;
- let interpolant = this._weightInterpolant;
-
- if ( interpolant === null ) {
-
- interpolant = mixer._lendControlInterpolant();
- this._weightInterpolant = interpolant;
-
- }
-
- const times = interpolant.parameterPositions,
- values = interpolant.sampleValues;
-
- times[ 0 ] = now;
- values[ 0 ] = weightNow;
- times[ 1 ] = now + duration;
- values[ 1 ] = weightThen;
-
- return this;
-
- }
-
-}
-
-const _controlInterpolantsResultBuffer = new Float32Array( 1 );
-
-/**
- * `AnimationMixer` is a player for animations on a particular object in
- * the scene. When multiple objects in the scene are animated independently,
- * one `AnimationMixer` may be used for each object.
- */
-class AnimationMixer extends EventDispatcher {
-
- /**
- * Constructs a new animation mixer.
- *
- * @param {Object3D} root - The object whose animations shall be played by this mixer.
- */
- constructor( root ) {
-
- super();
-
- this._root = root;
- this._initMemoryManager();
- this._accuIndex = 0;
-
- /**
- * The global mixer time (in seconds; starting with `0` on the mixer's creation).
- *
- * @type {number}
- * @default 0
- */
- this.time = 0;
-
- /**
- * A scaling factor for the global time.
- *
- * Note: Setting this member to `0` and later back to `1` is a
- * possibility to pause/unpause all actions that are controlled by this
- * mixer.
- *
- * @type {number}
- * @default 1
- */
- this.timeScale = 1.0;
-
- if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {
-
- __THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) );
-
- }
-
- }
-
- _bindAction( action, prototypeAction ) {
-
- const root = action._localRoot || this._root,
- tracks = action._clip.tracks,
- nTracks = tracks.length,
- bindings = action._propertyBindings,
- interpolants = action._interpolants,
- rootUuid = root.uuid,
- bindingsByRoot = this._bindingsByRootAndName;
-
- let bindingsByName = bindingsByRoot[ rootUuid ];
-
- if ( bindingsByName === undefined ) {
-
- bindingsByName = {};
- bindingsByRoot[ rootUuid ] = bindingsByName;
-
- }
-
- for ( let i = 0; i !== nTracks; ++ i ) {
-
- const track = tracks[ i ],
- trackName = track.name;
-
- let binding = bindingsByName[ trackName ];
-
- if ( binding !== undefined ) {
-
- ++ binding.referenceCount;
- bindings[ i ] = binding;
-
- } else {
-
- binding = bindings[ i ];
-
- if ( binding !== undefined ) {
-
- // existing binding, make sure the cache knows
-
- if ( binding._cacheIndex === null ) {
-
- ++ binding.referenceCount;
- this._addInactiveBinding( binding, rootUuid, trackName );
-
- }
-
- continue;
-
- }
-
- const path = prototypeAction && prototypeAction.
- _propertyBindings[ i ].binding.parsedPath;
-
- binding = new PropertyMixer(
- PropertyBinding.create( root, trackName, path ),
- track.ValueTypeName, track.getValueSize() );
-
- ++ binding.referenceCount;
- this._addInactiveBinding( binding, rootUuid, trackName );
-
- bindings[ i ] = binding;
-
- }
-
- interpolants[ i ].resultBuffer = binding.buffer;
-
- }
-
- }
-
- _activateAction( action ) {
-
- if ( ! this._isActiveAction( action ) ) {
-
- if ( action._cacheIndex === null ) {
-
- // this action has been forgotten by the cache, but the user
- // appears to be still using it -> rebind
-
- const rootUuid = ( action._localRoot || this._root ).uuid,
- clipUuid = action._clip.uuid,
- actionsForClip = this._actionsByClip[ clipUuid ];
-
- this._bindAction( action,
- actionsForClip && actionsForClip.knownActions[ 0 ] );
-
- this._addInactiveAction( action, clipUuid, rootUuid );
-
- }
-
- const bindings = action._propertyBindings;
-
- // increment reference counts / sort out state
- for ( let i = 0, n = bindings.length; i !== n; ++ i ) {
-
- const binding = bindings[ i ];
-
- if ( binding.useCount ++ === 0 ) {
-
- this._lendBinding( binding );
- binding.saveOriginalState();
-
- }
-
- }
-
- this._lendAction( action );
-
- }
-
- }
-
- _deactivateAction( action ) {
-
- if ( this._isActiveAction( action ) ) {
-
- const bindings = action._propertyBindings;
-
- // decrement reference counts / sort out state
- for ( let i = 0, n = bindings.length; i !== n; ++ i ) {
-
- const binding = bindings[ i ];
-
- if ( -- binding.useCount === 0 ) {
-
- binding.restoreOriginalState();
- this._takeBackBinding( binding );
-
- }
-
- }
-
- this._takeBackAction( action );
-
- }
-
- }
-
- // Memory manager
-
- _initMemoryManager() {
-
- this._actions = []; // 'nActiveActions' followed by inactive ones
- this._nActiveActions = 0;
-
- this._actionsByClip = {};
- // inside:
- // {
- // knownActions: Array< AnimationAction > - used as prototypes
- // actionByRoot: AnimationAction - lookup
- // }
-
-
- this._bindings = []; // 'nActiveBindings' followed by inactive ones
- this._nActiveBindings = 0;
-
- this._bindingsByRootAndName = {}; // inside: Map< name, PropertyMixer >
-
-
- this._controlInterpolants = []; // same game as above
- this._nActiveControlInterpolants = 0;
-
- const scope = this;
-
- this.stats = {
-
- actions: {
- get total() {
-
- return scope._actions.length;
-
- },
- get inUse() {
-
- return scope._nActiveActions;
-
- }
- },
- bindings: {
- get total() {
-
- return scope._bindings.length;
-
- },
- get inUse() {
-
- return scope._nActiveBindings;
-
- }
- },
- controlInterpolants: {
- get total() {
-
- return scope._controlInterpolants.length;
-
- },
- get inUse() {
-
- return scope._nActiveControlInterpolants;
-
- }
- }
-
- };
-
- }
-
- // Memory management for AnimationAction objects
-
- _isActiveAction( action ) {
-
- const index = action._cacheIndex;
- return index !== null && index < this._nActiveActions;
-
- }
-
- _addInactiveAction( action, clipUuid, rootUuid ) {
-
- const actions = this._actions,
- actionsByClip = this._actionsByClip;
-
- let actionsForClip = actionsByClip[ clipUuid ];
-
- if ( actionsForClip === undefined ) {
-
- actionsForClip = {
-
- knownActions: [ action ],
- actionByRoot: {}
-
- };
-
- action._byClipCacheIndex = 0;
-
- actionsByClip[ clipUuid ] = actionsForClip;
-
- } else {
-
- const knownActions = actionsForClip.knownActions;
-
- action._byClipCacheIndex = knownActions.length;
- knownActions.push( action );
-
- }
-
- action._cacheIndex = actions.length;
- actions.push( action );
-
- actionsForClip.actionByRoot[ rootUuid ] = action;
-
- }
-
- _removeInactiveAction( action ) {
-
- const actions = this._actions,
- lastInactiveAction = actions[ actions.length - 1 ],
- cacheIndex = action._cacheIndex;
-
- lastInactiveAction._cacheIndex = cacheIndex;
- actions[ cacheIndex ] = lastInactiveAction;
- actions.pop();
-
- action._cacheIndex = null;
-
-
- const clipUuid = action._clip.uuid,
- actionsByClip = this._actionsByClip,
- actionsForClip = actionsByClip[ clipUuid ],
- knownActionsForClip = actionsForClip.knownActions,
-
- lastKnownAction =
- knownActionsForClip[ knownActionsForClip.length - 1 ],
-
- byClipCacheIndex = action._byClipCacheIndex;
-
- lastKnownAction._byClipCacheIndex = byClipCacheIndex;
- knownActionsForClip[ byClipCacheIndex ] = lastKnownAction;
- knownActionsForClip.pop();
-
- action._byClipCacheIndex = null;
-
-
- const actionByRoot = actionsForClip.actionByRoot,
- rootUuid = ( action._localRoot || this._root ).uuid;
-
- delete actionByRoot[ rootUuid ];
-
- if ( knownActionsForClip.length === 0 ) {
-
- delete actionsByClip[ clipUuid ];
-
- }
-
- this._removeInactiveBindingsForAction( action );
-
- }
-
- _removeInactiveBindingsForAction( action ) {
-
- const bindings = action._propertyBindings;
-
- for ( let i = 0, n = bindings.length; i !== n; ++ i ) {
-
- const binding = bindings[ i ];
-
- if ( -- binding.referenceCount === 0 ) {
-
- this._removeInactiveBinding( binding );
-
- }
-
- }
-
- }
-
- _lendAction( action ) {
-
- // [ active actions | inactive actions ]
- // [ active actions >| inactive actions ]
- // s a
- // <-swap->
- // a s
-
- const actions = this._actions,
- prevIndex = action._cacheIndex,
-
- lastActiveIndex = this._nActiveActions ++,
-
- firstInactiveAction = actions[ lastActiveIndex ];
-
- action._cacheIndex = lastActiveIndex;
- actions[ lastActiveIndex ] = action;
-
- firstInactiveAction._cacheIndex = prevIndex;
- actions[ prevIndex ] = firstInactiveAction;
-
- }
-
- _takeBackAction( action ) {
-
- // [ active actions | inactive actions ]
- // [ active actions |< inactive actions ]
- // a s
- // <-swap->
- // s a
-
- const actions = this._actions,
- prevIndex = action._cacheIndex,
-
- firstInactiveIndex = -- this._nActiveActions,
-
- lastActiveAction = actions[ firstInactiveIndex ];
-
- action._cacheIndex = firstInactiveIndex;
- actions[ firstInactiveIndex ] = action;
-
- lastActiveAction._cacheIndex = prevIndex;
- actions[ prevIndex ] = lastActiveAction;
-
- }
-
- // Memory management for PropertyMixer objects
-
- _addInactiveBinding( binding, rootUuid, trackName ) {
-
- const bindingsByRoot = this._bindingsByRootAndName,
- bindings = this._bindings;
-
- let bindingByName = bindingsByRoot[ rootUuid ];
-
- if ( bindingByName === undefined ) {
-
- bindingByName = {};
- bindingsByRoot[ rootUuid ] = bindingByName;
-
- }
-
- bindingByName[ trackName ] = binding;
-
- binding._cacheIndex = bindings.length;
- bindings.push( binding );
-
- }
-
- _removeInactiveBinding( binding ) {
-
- const bindings = this._bindings,
- propBinding = binding.binding,
- rootUuid = propBinding.rootNode.uuid,
- trackName = propBinding.path,
- bindingsByRoot = this._bindingsByRootAndName,
- bindingByName = bindingsByRoot[ rootUuid ],
-
- lastInactiveBinding = bindings[ bindings.length - 1 ],
- cacheIndex = binding._cacheIndex;
-
- lastInactiveBinding._cacheIndex = cacheIndex;
- bindings[ cacheIndex ] = lastInactiveBinding;
- bindings.pop();
-
- delete bindingByName[ trackName ];
-
- if ( Object.keys( bindingByName ).length === 0 ) {
-
- delete bindingsByRoot[ rootUuid ];
-
- }
-
- }
-
- _lendBinding( binding ) {
-
- const bindings = this._bindings,
- prevIndex = binding._cacheIndex,
-
- lastActiveIndex = this._nActiveBindings ++,
-
- firstInactiveBinding = bindings[ lastActiveIndex ];
-
- binding._cacheIndex = lastActiveIndex;
- bindings[ lastActiveIndex ] = binding;
-
- firstInactiveBinding._cacheIndex = prevIndex;
- bindings[ prevIndex ] = firstInactiveBinding;
-
- }
-
- _takeBackBinding( binding ) {
-
- const bindings = this._bindings,
- prevIndex = binding._cacheIndex,
-
- firstInactiveIndex = -- this._nActiveBindings,
-
- lastActiveBinding = bindings[ firstInactiveIndex ];
-
- binding._cacheIndex = firstInactiveIndex;
- bindings[ firstInactiveIndex ] = binding;
-
- lastActiveBinding._cacheIndex = prevIndex;
- bindings[ prevIndex ] = lastActiveBinding;
-
- }
-
-
- // Memory management of Interpolants for weight and time scale
-
- _lendControlInterpolant() {
-
- const interpolants = this._controlInterpolants,
- lastActiveIndex = this._nActiveControlInterpolants ++;
-
- let interpolant = interpolants[ lastActiveIndex ];
-
- if ( interpolant === undefined ) {
-
- interpolant = new LinearInterpolant(
- new Float32Array( 2 ), new Float32Array( 2 ),
- 1, _controlInterpolantsResultBuffer );
-
- interpolant.__cacheIndex = lastActiveIndex;
- interpolants[ lastActiveIndex ] = interpolant;
-
- }
-
- return interpolant;
-
- }
-
- _takeBackControlInterpolant( interpolant ) {
-
- const interpolants = this._controlInterpolants,
- prevIndex = interpolant.__cacheIndex,
-
- firstInactiveIndex = -- this._nActiveControlInterpolants,
-
- lastActiveInterpolant = interpolants[ firstInactiveIndex ];
-
- interpolant.__cacheIndex = firstInactiveIndex;
- interpolants[ firstInactiveIndex ] = interpolant;
-
- lastActiveInterpolant.__cacheIndex = prevIndex;
- interpolants[ prevIndex ] = lastActiveInterpolant;
-
- }
-
- /**
- * Returns an instance of {@link AnimationAction} for the passed clip.
- *
- * If an action fitting the clip and root parameters doesn't yet exist, it
- * will be created by this method. Calling this method several times with the
- * same clip and root parameters always returns the same action.
- *
- * @param {AnimationClip|string} clip - An animation clip or alternatively the name of the animation clip.
- * @param {Object3D} [optionalRoot] - An alternative root object.
- * @param {(NormalAnimationBlendMode|AdditiveAnimationBlendMode)} [blendMode] - The blend mode.
- * @return {?AnimationAction} The animation action.
- */
- clipAction( clip, optionalRoot, blendMode ) {
-
- const root = optionalRoot || this._root,
- rootUuid = root.uuid;
-
- let clipObject = typeof clip === 'string' ? AnimationClip.findByName( root, clip ) : clip;
-
- const clipUuid = clipObject !== null ? clipObject.uuid : clip;
-
- const actionsForClip = this._actionsByClip[ clipUuid ];
- let prototypeAction = null;
-
- if ( blendMode === undefined ) {
-
- if ( clipObject !== null ) {
-
- blendMode = clipObject.blendMode;
-
- } else {
-
- blendMode = NormalAnimationBlendMode;
-
- }
-
- }
-
- if ( actionsForClip !== undefined ) {
-
- const existingAction = actionsForClip.actionByRoot[ rootUuid ];
-
- if ( existingAction !== undefined && existingAction.blendMode === blendMode ) {
-
- return existingAction;
-
- }
-
- // we know the clip, so we don't have to parse all
- // the bindings again but can just copy
- prototypeAction = actionsForClip.knownActions[ 0 ];
-
- // also, take the clip from the prototype action
- if ( clipObject === null )
- clipObject = prototypeAction._clip;
-
- }
-
- // clip must be known when specified via string
- if ( clipObject === null ) return null;
-
- // allocate all resources required to run it
- const newAction = new AnimationAction( this, clipObject, optionalRoot, blendMode );
-
- this._bindAction( newAction, prototypeAction );
-
- // and make the action known to the memory manager
- this._addInactiveAction( newAction, clipUuid, rootUuid );
-
- return newAction;
-
- }
-
- /**
- * Returns an existing animation action for the passed clip.
- *
- * @param {AnimationClip|string} clip - An animation clip or alternatively the name of the animation clip.
- * @param {Object3D} [optionalRoot] - An alternative root object.
- * @return {?AnimationAction} The animation action. Returns `null` if no action was found.
- */
- existingAction( clip, optionalRoot ) {
-
- const root = optionalRoot || this._root,
- rootUuid = root.uuid,
-
- clipObject = typeof clip === 'string' ?
- AnimationClip.findByName( root, clip ) : clip,
-
- clipUuid = clipObject ? clipObject.uuid : clip,
-
- actionsForClip = this._actionsByClip[ clipUuid ];
-
- if ( actionsForClip !== undefined ) {
-
- return actionsForClip.actionByRoot[ rootUuid ] || null;
-
- }
-
- return null;
-
- }
-
- /**
- * Deactivates all previously scheduled actions on this mixer.
- *
- * @return {AnimationMixer} A reference to this animation mixer.
- */
- stopAllAction() {
-
- const actions = this._actions,
- nActions = this._nActiveActions;
-
- for ( let i = nActions - 1; i >= 0; -- i ) {
-
- actions[ i ].stop();
-
- }
-
- return this;
-
- }
-
- /**
- * Advances the global mixer time and updates the animation.
- *
- * This is usually done in the render loop by passing the delta
- * time from {@link Clock} or {@link Timer}.
- *
- * @param {number} deltaTime - The delta time in seconds.
- * @return {AnimationMixer} A reference to this animation mixer.
- */
- update( deltaTime ) {
-
- deltaTime *= this.timeScale;
-
- const actions = this._actions,
- nActions = this._nActiveActions,
-
- time = this.time += deltaTime,
- timeDirection = Math.sign( deltaTime ),
-
- accuIndex = this._accuIndex ^= 1;
-
- // run active actions
-
- for ( let i = 0; i !== nActions; ++ i ) {
-
- const action = actions[ i ];
-
- action._update( time, deltaTime, timeDirection, accuIndex );
-
- }
-
- // update scene graph
-
- const bindings = this._bindings,
- nBindings = this._nActiveBindings;
-
- for ( let i = 0; i !== nBindings; ++ i ) {
-
- bindings[ i ].apply( accuIndex );
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the global mixer to a specific time and updates the animation accordingly.
- *
- * This is useful when you need to jump to an exact time in an animation. The
- * input parameter will be scaled by {@link AnimationMixer#timeScale}
- *
- * @param {number} time - The time to set in seconds.
- * @return {AnimationMixer} A reference to this animation mixer.
- */
- setTime( time ) {
-
- this.time = 0; // Zero out time attribute for AnimationMixer object;
- for ( let i = 0; i < this._actions.length; i ++ ) {
-
- this._actions[ i ].time = 0; // Zero out time attribute for all associated AnimationAction objects.
-
- }
-
- return this.update( time ); // Update used to set exact time. Returns "this" AnimationMixer object.
-
- }
-
- /**
- * Returns this mixer's root object.
- *
- * @return {Object3D} The mixer's root object.
- */
- getRoot() {
-
- return this._root;
-
- }
-
- /**
- * Deallocates all memory resources for a clip. Before using this method make
- * sure to call {@link AnimationAction#stop} for all related actions.
- *
- * @param {AnimationClip} clip - The clip to uncache.
- */
- uncacheClip( clip ) {
-
- const actions = this._actions,
- clipUuid = clip.uuid,
- actionsByClip = this._actionsByClip,
- actionsForClip = actionsByClip[ clipUuid ];
-
- if ( actionsForClip !== undefined ) {
-
- // note: just calling _removeInactiveAction would mess up the
- // iteration state and also require updating the state we can
- // just throw away
-
- const actionsToRemove = actionsForClip.knownActions;
-
- for ( let i = 0, n = actionsToRemove.length; i !== n; ++ i ) {
-
- const action = actionsToRemove[ i ];
-
- this._deactivateAction( action );
-
- const cacheIndex = action._cacheIndex,
- lastInactiveAction = actions[ actions.length - 1 ];
-
- action._cacheIndex = null;
- action._byClipCacheIndex = null;
-
- lastInactiveAction._cacheIndex = cacheIndex;
- actions[ cacheIndex ] = lastInactiveAction;
- actions.pop();
-
- this._removeInactiveBindingsForAction( action );
-
- }
-
- delete actionsByClip[ clipUuid ];
-
- }
-
- }
-
- /**
- * Deallocates all memory resources for a root object. Before using this
- * method make sure to call {@link AnimationAction#stop} for all related
- * actions or alternatively {@link AnimationMixer#stopAllAction} when the
- * mixer operates on a single root.
- *
- * @param {Object3D} root - The root object to uncache.
- */
- uncacheRoot( root ) {
-
- const rootUuid = root.uuid,
- actionsByClip = this._actionsByClip;
-
- for ( const clipUuid in actionsByClip ) {
-
- const actionByRoot = actionsByClip[ clipUuid ].actionByRoot,
- action = actionByRoot[ rootUuid ];
-
- if ( action !== undefined ) {
-
- this._deactivateAction( action );
- this._removeInactiveAction( action );
-
- }
-
- }
-
- const bindingsByRoot = this._bindingsByRootAndName,
- bindingByName = bindingsByRoot[ rootUuid ];
-
- if ( bindingByName !== undefined ) {
-
- for ( const trackName in bindingByName ) {
-
- const binding = bindingByName[ trackName ];
- binding.restoreOriginalState();
- this._removeInactiveBinding( binding );
-
- }
-
- }
-
- }
-
- /**
- * Deallocates all memory resources for an action. The action is identified by the
- * given clip and an optional root object. Before using this method make
- * sure to call {@link AnimationAction#stop} to deactivate the action.
- *
- * @param {AnimationClip|string} clip - An animation clip or alternatively the name of the animation clip.
- * @param {Object3D} [optionalRoot] - An alternative root object.
- */
- uncacheAction( clip, optionalRoot ) {
-
- const action = this.existingAction( clip, optionalRoot );
-
- if ( action !== null ) {
-
- this._deactivateAction( action );
- this._removeInactiveAction( action );
-
- }
-
- }
-
-}
-
-/**
- * Represents a 3D render target.
- *
- * @augments RenderTarget
- */
-class RenderTarget3D extends RenderTarget {
-
- /**
- * Constructs a new 3D render target.
- *
- * @param {number} [width=1] - The width of the render target.
- * @param {number} [height=1] - The height of the render target.
- * @param {number} [depth=1] - The height of the render target.
- * @param {RenderTarget~Options} [options] - The configuration object.
- */
- constructor( width = 1, height = 1, depth = 1, options = {} ) {
-
- super( width, height, options );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isRenderTarget3D = true;
-
- this.depth = depth;
-
- // overwrite attachments with 3D textures
-
- for ( let i = 0; i < this.textures.length; i ++ ) {
-
- const texture = new Data3DTexture( null, width, height, depth );
- texture.isRenderTargetTexture = true;
- texture.renderTarget = this;
-
- this.textures[ i ] = texture;
-
- }
-
- this._setTextureOptions( options );
-
- }
-
-}
-
-/**
- * Represents a uniform which is a global shader variable. They are passed to shader programs.
- *
- * When declaring a uniform of a {@link ShaderMaterial}, it is declared by value or by object.
- * ```js
- * uniforms: {
- * time: { value: 1.0 },
- * resolution: new Uniform( new Vector2() )
- * };
- * ```
- * Since this class can only be used in context of {@link ShaderMaterial}, it is only supported
- * in {@link WebGLRenderer}.
- */
-class Uniform {
-
- /**
- * Constructs a new uniform.
- *
- * @param {any} value - The uniform value.
- */
- constructor( value ) {
-
- /**
- * The uniform value.
- *
- * @type {any}
- */
- this.value = value;
-
- }
-
- /**
- * Returns a new uniform with copied values from this instance.
- * If the value has a `clone()` method, the value is cloned as well.
- *
- * @return {Uniform} A clone of this instance.
- */
- clone() {
-
- return new Uniform( this.value.clone === undefined ? this.value : this.value.clone() );
-
- }
-
-}
-
-let _id = 0;
-
-/**
- * A class for managing multiple uniforms in a single group. The renderer will process
- * such a definition as a single UBO.
- *
- * Since this class can only be used in context of {@link ShaderMaterial}, it is only supported
- * in {@link WebGLRenderer}.
- *
- * @augments EventDispatcher
- */
-class UniformsGroup extends EventDispatcher {
-
- /**
- * Constructs a new uniforms group.
- */
- constructor() {
-
- super();
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isUniformsGroup = true;
-
- /**
- * The ID of the 3D object.
- *
- * @name UniformsGroup#id
- * @type {number}
- * @readonly
- */
- Object.defineProperty( this, 'id', { value: _id ++ } );
-
- /**
- * The name of the uniforms group.
- *
- * @type {string}
- */
- this.name = '';
-
- /**
- * The buffer usage.
- *
- * @type {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)}
- * @default StaticDrawUsage
- */
- this.usage = StaticDrawUsage;
-
- /**
- * An array holding the uniforms.
- *
- * @type {Array}
- */
- this.uniforms = [];
-
- }
-
- /**
- * Adds the given uniform to this uniforms group.
- *
- * @param {Uniform} uniform - The uniform to add.
- * @return {UniformsGroup} A reference to this uniforms group.
- */
- add( uniform ) {
-
- this.uniforms.push( uniform );
-
- return this;
-
- }
-
- /**
- * Removes the given uniform from this uniforms group.
- *
- * @param {Uniform} uniform - The uniform to remove.
- * @return {UniformsGroup} A reference to this uniforms group.
- */
- remove( uniform ) {
-
- const index = this.uniforms.indexOf( uniform );
-
- if ( index !== -1 ) this.uniforms.splice( index, 1 );
-
- return this;
-
- }
-
- /**
- * Sets the name of this uniforms group.
- *
- * @param {string} name - The name to set.
- * @return {UniformsGroup} A reference to this uniforms group.
- */
- setName( name ) {
-
- this.name = name;
-
- return this;
-
- }
-
- /**
- * Sets the usage of this uniforms group.
- *
- * @param {(StaticDrawUsage|DynamicDrawUsage|StreamDrawUsage|StaticReadUsage|DynamicReadUsage|StreamReadUsage|StaticCopyUsage|DynamicCopyUsage|StreamCopyUsage)} value - The usage to set.
- * @return {UniformsGroup} A reference to this uniforms group.
- */
- setUsage( value ) {
-
- this.usage = value;
-
- return this;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- *
- * @fires Texture#dispose
- */
- dispose() {
-
- this.dispatchEvent( { type: 'dispose' } );
-
- }
-
- /**
- * Copies the values of the given uniforms group to this instance.
- *
- * @param {UniformsGroup} source - The uniforms group to copy.
- * @return {UniformsGroup} A reference to this uniforms group.
- */
- copy( source ) {
-
- this.name = source.name;
- this.usage = source.usage;
-
- const uniformsSource = source.uniforms;
-
- this.uniforms.length = 0;
-
- for ( let i = 0, l = uniformsSource.length; i < l; i ++ ) {
-
- const uniforms = Array.isArray( uniformsSource[ i ] ) ? uniformsSource[ i ] : [ uniformsSource[ i ] ];
-
- for ( let j = 0; j < uniforms.length; j ++ ) {
-
- this.uniforms.push( uniforms[ j ].clone() );
-
- }
-
- }
-
- return this;
-
- }
-
- /**
- * Returns a new uniforms group with copied values from this instance.
- *
- * @return {UniformsGroup} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
-}
-
-/**
- * An instanced version of an interleaved buffer.
- *
- * @augments InterleavedBuffer
- */
-class InstancedInterleavedBuffer extends InterleavedBuffer {
-
- /**
- * Constructs a new instanced interleaved buffer.
- *
- * @param {TypedArray} array - A typed array with a shared buffer storing attribute data.
- * @param {number} stride - The number of typed-array elements per vertex.
- * @param {number} [meshPerAttribute=1] - Defines how often a value of this interleaved buffer should be repeated.
- */
- constructor( array, stride, meshPerAttribute = 1 ) {
-
- super( array, stride );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isInstancedInterleavedBuffer = true;
-
- /**
- * Defines how often a value of this buffer attribute should be repeated,
- * see {@link InstancedBufferAttribute#meshPerAttribute}.
- *
- * @type {number}
- * @default 1
- */
- this.meshPerAttribute = meshPerAttribute;
-
- }
-
- copy( source ) {
-
- super.copy( source );
-
- this.meshPerAttribute = source.meshPerAttribute;
-
- return this;
-
- }
-
- clone( data ) {
-
- const ib = super.clone( data );
-
- ib.meshPerAttribute = this.meshPerAttribute;
-
- return ib;
-
- }
-
- toJSON( data ) {
-
- const json = super.toJSON( data );
-
- json.isInstancedInterleavedBuffer = true;
- json.meshPerAttribute = this.meshPerAttribute;
-
- return json;
-
- }
-
-}
-
-/**
- * An alternative version of a buffer attribute with more control over the VBO.
- *
- * The renderer does not construct a VBO for this kind of attribute. Instead, it uses
- * whatever VBO is passed in constructor and can later be altered via the `buffer` property.
- *
- * The most common use case for this class is when some kind of GPGPU calculation interferes
- * or even produces the VBOs in question.
- *
- * Notice that this class can only be used with {@link WebGLRenderer}.
- */
-class GLBufferAttribute {
-
- /**
- * Constructs a new GL buffer attribute.
- *
- * @param {WebGLBuffer} buffer - The native WebGL buffer.
- * @param {number} type - The native data type (e.g. `gl.FLOAT`).
- * @param {number} itemSize - The item size.
- * @param {number} elementSize - The corresponding size (in bytes) for the given `type` parameter.
- * @param {number} count - The expected number of vertices in VBO.
- * @param {boolean} [normalized=false] - Whether the data are normalized or not.
- */
- constructor( buffer, type, itemSize, elementSize, count, normalized = false ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isGLBufferAttribute = true;
-
- /**
- * The name of the buffer attribute.
- *
- * @type {string}
- */
- this.name = '';
-
- /**
- * The native WebGL buffer.
- *
- * @type {WebGLBuffer}
- */
- this.buffer = buffer;
-
- /**
- * The native data type.
- *
- * @type {number}
- */
- this.type = type;
-
- /**
- * The item size, see {@link BufferAttribute#itemSize}.
- *
- * @type {number}
- */
- this.itemSize = itemSize;
-
- /**
- * The corresponding size (in bytes) for the given `type` parameter.
- *
- * @type {number}
- */
- this.elementSize = elementSize;
-
- /**
- * The expected number of vertices in VBO.
- *
- * @type {number}
- */
- this.count = count;
-
- /**
- * Applies to integer data only. Indicates how the underlying data in the buffer maps to
- * the values in the GLSL code. For instance, if `buffer` contains data of `gl.UNSIGNED_SHORT`,
- * and `normalized` is `true`, the values `0 - +65535` in the buffer data will be mapped to
- * `0.0f - +1.0f` in the GLSL attribute. If `normalized` is `false`, the values will be converted
- * to floats unmodified, i.e. `65535` becomes `65535.0f`.
- *
- * @type {boolean}
- */
- this.normalized = normalized;
-
- /**
- * A version number, incremented every time the `needsUpdate` is set to `true`.
- *
- * @type {number}
- */
- this.version = 0;
-
- }
-
- /**
- * Flag to indicate that this attribute has changed and should be re-sent to
- * the GPU. Set this to `true` when you modify the value of the array.
- *
- * @type {number}
- * @default false
- * @param {boolean} value
- */
- set needsUpdate( value ) {
-
- if ( value === true ) this.version ++;
-
- }
-
- /**
- * Sets the given native WebGL buffer.
- *
- * @param {WebGLBuffer} buffer - The buffer to set.
- * @return {BufferAttribute} A reference to this instance.
- */
- setBuffer( buffer ) {
-
- this.buffer = buffer;
-
- return this;
-
- }
-
- /**
- * Sets the given native data type and element size.
- *
- * @param {number} type - The native data type (e.g. `gl.FLOAT`).
- * @param {number} elementSize - The corresponding size (in bytes) for the given `type` parameter.
- * @return {BufferAttribute} A reference to this instance.
- */
- setType( type, elementSize ) {
-
- this.type = type;
- this.elementSize = elementSize;
-
- return this;
-
- }
-
- /**
- * Sets the item size.
- *
- * @param {number} itemSize - The item size.
- * @return {BufferAttribute} A reference to this instance.
- */
- setItemSize( itemSize ) {
-
- this.itemSize = itemSize;
-
- return this;
-
- }
-
- /**
- * Sets the count (the expected number of vertices in VBO).
- *
- * @param {number} count - The count.
- * @return {BufferAttribute} A reference to this instance.
- */
- setCount( count ) {
-
- this.count = count;
-
- return this;
-
- }
-
-}
-
-const _matrix = /*@__PURE__*/ new Matrix4();
-
-/**
- * This class is designed to assist with raycasting. Raycasting is used for
- * mouse picking (working out what objects in the 3d space the mouse is over)
- * amongst other things.
- */
-class Raycaster {
-
- /**
- * Constructs a new raycaster.
- *
- * @param {Vector3} origin - The origin vector where the ray casts from.
- * @param {Vector3} direction - The (normalized) direction vector that gives direction to the ray.
- * @param {number} [near=0] - All results returned are further away than near. Near can't be negative.
- * @param {number} [far=Infinity] - All results returned are closer than far. Far can't be lower than near.
- */
- constructor( origin, direction, near = 0, far = Infinity ) {
-
- /**
- * The ray used for raycasting.
- *
- * @type {Ray}
- */
- this.ray = new Ray( origin, direction );
-
- /**
- * All results returned are further away than near. Near can't be negative.
- *
- * @type {number}
- * @default 0
- */
- this.near = near;
-
- /**
- * All results returned are closer than far. Far can't be lower than near.
- *
- * @type {number}
- * @default Infinity
- */
- this.far = far;
-
- /**
- * The camera to use when raycasting against view-dependent objects such as
- * billboarded objects like sprites. This field can be set manually or
- * is set when calling `setFromCamera()`.
- *
- * @type {?Camera}
- * @default null
- */
- this.camera = null;
-
- /**
- * Allows to selectively ignore 3D objects when performing intersection tests.
- * The following code example ensures that only 3D objects on layer `1` will be
- * honored by raycaster.
- * ```js
- * raycaster.layers.set( 1 );
- * object.layers.enable( 1 );
- * ```
- *
- * @type {Layers}
- */
- this.layers = new Layers();
-
-
- /**
- * A parameter object that configures the raycasting. It has the structure:
- *
- * ```
- * {
- * Mesh: {},
- * Line: { threshold: 1 },
- * LOD: {},
- * Points: { threshold: 1 },
- * Sprite: {}
- * }
- * ```
- * Where `threshold` is the precision of the raycaster when intersecting objects, in world units.
- *
- * @type {Object}
- */
- this.params = {
- Mesh: {},
- Line: { threshold: 1 },
- LOD: {},
- Points: { threshold: 1 },
- Sprite: {}
- };
-
- }
-
- /**
- * Updates the ray with a new origin and direction by copying the values from the arguments.
- *
- * @param {Vector3} origin - The origin vector where the ray casts from.
- * @param {Vector3} direction - The (normalized) direction vector that gives direction to the ray.
- */
- set( origin, direction ) {
-
- // direction is assumed to be normalized (for accurate distance calculations)
-
- this.ray.set( origin, direction );
-
- }
-
- /**
- * Uses the given coordinates and camera to compute a new origin and direction for the internal ray.
- *
- * @param {Vector2} coords - 2D coordinates of the mouse, in normalized device coordinates (NDC).
- * X and Y components should be between `-1` and `1`.
- * @param {Camera} camera - The camera from which the ray should originate.
- */
- setFromCamera( coords, camera ) {
-
- if ( camera.isPerspectiveCamera ) {
-
- this.ray.origin.setFromMatrixPosition( camera.matrixWorld );
- this.ray.direction.set( coords.x, coords.y, 0.5 ).unproject( camera ).sub( this.ray.origin ).normalize();
- this.camera = camera;
-
- } else if ( camera.isOrthographicCamera ) {
-
- this.ray.origin.set( coords.x, coords.y, camera.projectionMatrix.elements[ 14 ] ).unproject( camera ); // set origin in plane of camera
- this.ray.direction.set( 0, 0, -1 ).transformDirection( camera.matrixWorld );
- this.camera = camera;
-
- } else {
-
- error( 'Raycaster: Unsupported camera type: ' + camera.type );
-
- }
-
- }
-
- /**
- * Uses the given WebXR controller to compute a new origin and direction for the internal ray.
- *
- * @param {WebXRController} controller - The controller to copy the position and direction from.
- * @return {Raycaster} A reference to this raycaster.
- */
- setFromXRController( controller ) {
-
- _matrix.identity().extractRotation( controller.matrixWorld );
-
- this.ray.origin.setFromMatrixPosition( controller.matrixWorld );
- this.ray.direction.set( 0, 0, -1 ).applyMatrix4( _matrix );
-
- return this;
-
- }
-
- /**
- * The intersection point of a raycaster intersection test.
- * @typedef {Object} Raycaster~Intersection
- * @property {number} distance - The distance from the ray's origin to the intersection point.
- * @property {number} distanceToRay - Some 3D objects e.g. {@link Points} provide the distance of the
- * intersection to the nearest point on the ray. For other objects it will be `undefined`.
- * @property {Vector3} point - The intersection point, in world coordinates.
- * @property {Object} face - The face that has been intersected.
- * @property {number} faceIndex - The face index.
- * @property {Object3D} object - The 3D object that has been intersected.
- * @property {Vector2} uv - U,V coordinates at point of intersection.
- * @property {Vector2} uv1 - Second set of U,V coordinates at point of intersection.
- * @property {Vector3} normal - Interpolated normal vector at point of intersection.
- * @property {number} instanceId - The index number of the instance where the ray
- * intersects the {@link InstancedMesh}.
- */
-
- /**
- * Checks all intersection between the ray and the object with or without the
- * descendants. Intersections are returned sorted by distance, closest first.
- *
- * `Raycaster` delegates to the `raycast()` method of the passed 3D object, when
- * evaluating whether the ray intersects the object or not. This allows meshes to respond
- * differently to ray casting than lines or points.
- *
- * Note that for meshes, faces must be pointed towards the origin of the ray in order
- * to be detected; intersections of the ray passing through the back of a face will not
- * be detected. To raycast against both faces of an object, you'll want to set {@link Material#side}
- * to `THREE.DoubleSide`.
- *
- * Note that a ray hitting a triangle mesh exactly along an edge shared by two faces may be
- * reported by both faces, resulting in two coincident intersections (identical point and
- * distance) in the returned array.
- *
- * @param {Object3D} object - The 3D object to check for intersection with the ray.
- * @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants.
- * Otherwise it only checks intersection with the object.
- * @param {Array} [intersects=[]] The target array that holds the result of the method.
- * @return {Array} An array holding the intersection points.
- */
- intersectObject( object, recursive = true, intersects = [] ) {
-
- intersect( object, this, intersects, recursive );
-
- intersects.sort( ascSort );
-
- return intersects;
-
- }
-
- /**
- * Checks all intersection between the ray and the objects with or without
- * the descendants. Intersections are returned sorted by distance, closest first.
- *
- * @param {Array} objects - The 3D objects to check for intersection with the ray.
- * @param {boolean} [recursive=true] - If set to `true`, it also checks all descendants.
- * Otherwise it only checks intersection with the object.
- * @param {Array} [intersects=[]] The target array that holds the result of the method.
- * @return {Array} An array holding the intersection points.
- */
- intersectObjects( objects, recursive = true, intersects = [] ) {
-
- for ( let i = 0, l = objects.length; i < l; i ++ ) {
-
- intersect( objects[ i ], this, intersects, recursive );
-
- }
-
- intersects.sort( ascSort );
-
- return intersects;
-
- }
-
-}
-
-function ascSort( a, b ) {
-
- return a.distance - b.distance;
-
-}
-
-function intersect( object, raycaster, intersects, recursive ) {
-
- let propagate = true;
-
- if ( object.layers.test( raycaster.layers ) ) {
-
- const result = object.raycast( raycaster, intersects );
-
- if ( result === false ) propagate = false;
-
- }
-
- if ( propagate === true && recursive === true ) {
-
- const children = object.children;
-
- for ( let i = 0, l = children.length; i < l; i ++ ) {
-
- intersect( children[ i ], raycaster, intersects, true );
-
- }
-
- }
-
-}
-
-/**
- * Class for keeping track of time.
- *
- * @deprecated since r183.
- */
-class Clock {
-
- /**
- * Constructs a new clock.
- *
- * @deprecated since 183.
- * @param {boolean} [autoStart=true] - Whether to automatically start the clock when
- * `getDelta()` is called for the first time.
- */
- constructor( autoStart = true ) {
-
- /**
- * If set to `true`, the clock starts automatically when `getDelta()` is called
- * for the first time.
- *
- * @type {boolean}
- * @default true
- */
- this.autoStart = autoStart;
-
- /**
- * Holds the time at which the clock's `start()` method was last called.
- *
- * @type {number}
- * @default 0
- */
- this.startTime = 0;
-
- /**
- * Holds the time at which the clock's `start()`, `getElapsedTime()` or
- * `getDelta()` methods were last called.
- *
- * @type {number}
- * @default 0
- */
- this.oldTime = 0;
-
- /**
- * Keeps track of the total time that the clock has been running.
- *
- * @type {number}
- * @default 0
- */
- this.elapsedTime = 0;
-
- /**
- * Whether the clock is running or not.
- *
- * @type {boolean}
- * @default true
- */
- this.running = false;
-
- warn( 'Clock: This module has been deprecated. Please use THREE.Timer instead.' ); // @deprecated, r183
-
- }
-
- /**
- * Starts the clock. When `autoStart` is set to `true`, the method is automatically
- * called by the class.
- */
- start() {
-
- this.startTime = performance.now();
-
- this.oldTime = this.startTime;
- this.elapsedTime = 0;
- this.running = true;
-
- }
-
- /**
- * Stops the clock.
- */
- stop() {
-
- this.getElapsedTime();
- this.running = false;
- this.autoStart = false;
-
- }
-
- /**
- * Returns the elapsed time in seconds.
- *
- * @return {number} The elapsed time.
- */
- getElapsedTime() {
-
- this.getDelta();
- return this.elapsedTime;
-
- }
-
- /**
- * Returns the delta time in seconds.
- *
- * @return {number} The delta time.
- */
- getDelta() {
-
- let diff = 0;
-
- if ( this.autoStart && ! this.running ) {
-
- this.start();
- return 0;
-
- }
-
- if ( this.running ) {
-
- const newTime = performance.now();
-
- diff = ( newTime - this.oldTime ) / 1000;
- this.oldTime = newTime;
-
- this.elapsedTime += diff;
-
- }
-
- return diff;
-
- }
-
-}
-
-/**
- * This class can be used to represent points in 3D space as
- * [Spherical coordinates](https://en.wikipedia.org/wiki/Spherical_coordinate_system).
- */
-class Spherical {
-
- /**
- * Constructs a new spherical.
- *
- * @param {number} [radius=1] - The radius, or the Euclidean distance (straight-line distance) from the point to the origin.
- * @param {number} [phi=0] - The polar angle in radians from the y (up) axis.
- * @param {number} [theta=0] - The equator/azimuthal angle in radians around the y (up) axis.
- */
- constructor( radius = 1, phi = 0, theta = 0 ) {
-
- /**
- * The radius, or the Euclidean distance (straight-line distance) from the point to the origin.
- *
- * @type {number}
- * @default 1
- */
- this.radius = radius;
-
- /**
- * The polar angle in radians from the y (up) axis.
- *
- * @type {number}
- * @default 0
- */
- this.phi = phi;
-
- /**
- * The equator/azimuthal angle in radians around the y (up) axis.
- *
- * @type {number}
- * @default 0
- */
- this.theta = theta;
-
- }
-
- /**
- * Sets the spherical components by copying the given values.
- *
- * @param {number} radius - The radius.
- * @param {number} phi - The polar angle.
- * @param {number} theta - The azimuthal angle.
- * @return {Spherical} A reference to this spherical.
- */
- set( radius, phi, theta ) {
-
- this.radius = radius;
- this.phi = phi;
- this.theta = theta;
-
- return this;
-
- }
-
- /**
- * Copies the values of the given spherical to this instance.
- *
- * @param {Spherical} other - The spherical to copy.
- * @return {Spherical} A reference to this spherical.
- */
- copy( other ) {
-
- this.radius = other.radius;
- this.phi = other.phi;
- this.theta = other.theta;
-
- return this;
-
- }
-
- /**
- * Restricts the polar angle [page:.phi phi] to be between `0.000001` and pi -
- * `0.000001`.
- *
- * @return {Spherical} A reference to this spherical.
- */
- makeSafe() {
-
- const EPS = 0.000001;
- this.phi = clamp( this.phi, EPS, Math.PI - EPS );
-
- return this;
-
- }
-
- /**
- * Sets the spherical components from the given vector which is assumed to hold
- * Cartesian coordinates.
- *
- * @param {Vector3} v - The vector to set.
- * @return {Spherical} A reference to this spherical.
- */
- setFromVector3( v ) {
-
- return this.setFromCartesianCoords( v.x, v.y, v.z );
-
- }
-
- /**
- * Sets the spherical components from the given Cartesian coordinates.
- *
- * @param {number} x - The x value.
- * @param {number} y - The y value.
- * @param {number} z - The z value.
- * @return {Spherical} A reference to this spherical.
- */
- setFromCartesianCoords( x, y, z ) {
-
- this.radius = Math.sqrt( x * x + y * y + z * z );
-
- if ( this.radius === 0 ) {
-
- this.theta = 0;
- this.phi = 0;
-
- } else {
-
- this.theta = Math.atan2( x, z );
- this.phi = Math.acos( clamp( y / this.radius, -1, 1 ) );
-
- }
-
- return this;
-
- }
-
- /**
- * Returns a new spherical with copied values from this instance.
- *
- * @return {Spherical} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
-}
-
-/**
- * This class can be used to represent points in 3D space as
- * [Cylindrical coordinates](https://en.wikipedia.org/wiki/Cylindrical_coordinate_system).
- */
-class Cylindrical {
-
- /**
- * Constructs a new cylindrical.
- *
- * @param {number} [radius=1] - The distance from the origin to a point in the x-z plane.
- * @param {number} [theta=0] - A counterclockwise angle in the x-z plane measured in radians from the positive z-axis.
- * @param {number} [y=0] - The height above the x-z plane.
- */
- constructor( radius = 1, theta = 0, y = 0 ) {
-
- /**
- * The distance from the origin to a point in the x-z plane.
- *
- * @type {number}
- * @default 1
- */
- this.radius = radius;
-
- /**
- * A counterclockwise angle in the x-z plane measured in radians from the positive z-axis.
- *
- * @type {number}
- * @default 0
- */
- this.theta = theta;
-
- /**
- * The height above the x-z plane.
- *
- * @type {number}
- * @default 0
- */
- this.y = y;
-
- }
-
- /**
- * Sets the cylindrical components by copying the given values.
- *
- * @param {number} radius - The radius.
- * @param {number} theta - The theta angle.
- * @param {number} y - The height value.
- * @return {Cylindrical} A reference to this cylindrical.
- */
- set( radius, theta, y ) {
-
- this.radius = radius;
- this.theta = theta;
- this.y = y;
-
- return this;
-
- }
-
- /**
- * Copies the values of the given cylindrical to this instance.
- *
- * @param {Cylindrical} other - The cylindrical to copy.
- * @return {Cylindrical} A reference to this cylindrical.
- */
- copy( other ) {
-
- this.radius = other.radius;
- this.theta = other.theta;
- this.y = other.y;
-
- return this;
-
- }
-
- /**
- * Sets the cylindrical components from the given vector which is assumed to hold
- * Cartesian coordinates.
- *
- * @param {Vector3} v - The vector to set.
- * @return {Cylindrical} A reference to this cylindrical.
- */
- setFromVector3( v ) {
-
- return this.setFromCartesianCoords( v.x, v.y, v.z );
-
- }
-
- /**
- * Sets the cylindrical components from the given Cartesian coordinates.
- *
- * @param {number} x - The x value.
- * @param {number} y - The x value.
- * @param {number} z - The x value.
- * @return {Cylindrical} A reference to this cylindrical.
- */
- setFromCartesianCoords( x, y, z ) {
-
- this.radius = Math.sqrt( x * x + z * z );
- this.theta = Math.atan2( x, z );
- this.y = y;
-
- return this;
-
- }
-
- /**
- * Returns a new cylindrical with copied values from this instance.
- *
- * @return {Cylindrical} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
-}
-
-/**
- * Represents a 2x2 matrix.
- *
- * A Note on Row-Major and Column-Major Ordering:
- *
- * The constructor and {@link Matrix2#set} method take arguments in
- * [row-major](https://en.wikipedia.org/wiki/Row-_and_column-major_order#Column-major_order)
- * order, while internally they are stored in the {@link Matrix2#elements} array in column-major order.
- * This means that calling:
- * ```js
- * const m = new THREE.Matrix2();
- * m.set( 11, 12,
- * 21, 22 );
- * ```
- * will result in the elements array containing:
- * ```js
- * m.elements = [ 11, 21,
- * 12, 22 ];
- * ```
- * and internally all calculations are performed using column-major ordering.
- * However, as the actual ordering makes no difference mathematically and
- * most people are used to thinking about matrices in row-major order, the
- * three.js documentation shows matrices in row-major order. Just bear in
- * mind that if you are reading the source code, you'll have to take the
- * transpose of any matrices outlined here to make sense of the calculations.
- */
-class Matrix2 {
-
- static {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- Matrix2.prototype.isMatrix2 = true;
-
- }
-
- /**
- * Constructs a new 2x2 matrix. The arguments are supposed to be
- * in row-major order. If no arguments are provided, the constructor
- * initializes the matrix as an identity matrix.
- *
- * @param {number} [n11] - 1-1 matrix element.
- * @param {number} [n12] - 1-2 matrix element.
- * @param {number} [n21] - 2-1 matrix element.
- * @param {number} [n22] - 2-2 matrix element.
- */
- constructor( n11, n12, n21, n22 ) {
-
- /**
- * A column-major list of matrix values.
- *
- * @type {Array}
- */
- this.elements = [
- 1, 0,
- 0, 1,
- ];
-
- if ( n11 !== undefined ) {
-
- this.set( n11, n12, n21, n22 );
-
- }
-
- }
-
- /**
- * Sets this matrix to the 2x2 identity matrix.
- *
- * @return {Matrix2} A reference to this matrix.
- */
- identity() {
-
- this.set(
- 1, 0,
- 0, 1,
- );
-
- return this;
-
- }
-
- /**
- * Sets the elements of the matrix from the given array.
- *
- * @param {Array} array - The matrix elements in column-major order.
- * @param {number} [offset=0] - Index of the first element in the array.
- * @return {Matrix2} A reference to this matrix.
- */
- fromArray( array, offset = 0 ) {
-
- for ( let i = 0; i < 4; i ++ ) {
-
- this.elements[ i ] = array[ i + offset ];
-
- }
-
- return this;
-
- }
-
- /**
- * Sets the elements of the matrix.The arguments are supposed to be
- * in row-major order.
- *
- * @param {number} n11 - 1-1 matrix element.
- * @param {number} n12 - 1-2 matrix element.
- * @param {number} n21 - 2-1 matrix element.
- * @param {number} n22 - 2-2 matrix element.
- * @return {Matrix2} A reference to this matrix.
- */
- set( n11, n12, n21, n22 ) {
-
- const te = this.elements;
-
- te[ 0 ] = n11; te[ 2 ] = n12;
- te[ 1 ] = n21; te[ 3 ] = n22;
-
- return this;
-
- }
-
-}
-
-const _vector$4 = /*@__PURE__*/ new Vector2();
-
-/**
- * Represents an axis-aligned bounding box (AABB) in 2D space.
- */
-class Box2 {
-
- /**
- * Constructs a new bounding box.
- *
- * @param {Vector2} [min=(Infinity,Infinity)] - A vector representing the lower boundary of the box.
- * @param {Vector2} [max=(-Infinity,-Infinity)] - A vector representing the upper boundary of the box.
- */
- constructor( min = new Vector2( + Infinity, + Infinity ), max = new Vector2( - Infinity, - Infinity ) ) {
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isBox2 = true;
-
- /**
- * The lower boundary of the box.
- *
- * @type {Vector2}
- */
- this.min = min;
-
- /**
- * The upper boundary of the box.
- *
- * @type {Vector2}
- */
- this.max = max;
-
- }
-
- /**
- * Sets the lower and upper boundaries of this box.
- * Please note that this method only copies the values from the given objects.
- *
- * @param {Vector2} min - The lower boundary of the box.
- * @param {Vector2} max - The upper boundary of the box.
- * @return {Box2} A reference to this bounding box.
- */
- set( min, max ) {
-
- this.min.copy( min );
- this.max.copy( max );
-
- return this;
-
- }
-
- /**
- * Sets the upper and lower bounds of this box so it encloses the position data
- * in the given array.
- *
- * @param {Array} points - An array holding 2D position data as instances of {@link Vector2}.
- * @return {Box2} A reference to this bounding box.
- */
- setFromPoints( points ) {
-
- this.makeEmpty();
-
- for ( let i = 0, il = points.length; i < il; i ++ ) {
-
- this.expandByPoint( points[ i ] );
-
- }
-
- return this;
-
- }
-
- /**
- * Centers this box on the given center vector and sets this box's width, height and
- * depth to the given size values.
- *
- * @param {Vector2} center - The center of the box.
- * @param {Vector2} size - The x and y dimensions of the box.
- * @return {Box2} A reference to this bounding box.
- */
- setFromCenterAndSize( center, size ) {
-
- const halfSize = _vector$4.copy( size ).multiplyScalar( 0.5 );
- this.min.copy( center ).sub( halfSize );
- this.max.copy( center ).add( halfSize );
-
- return this;
-
- }
-
- /**
- * Returns a new box with copied values from this instance.
- *
- * @return {Box2} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
- /**
- * Copies the values of the given box to this instance.
- *
- * @param {Box2} box - The box to copy.
- * @return {Box2} A reference to this bounding box.
- */
- copy( box ) {
-
- this.min.copy( box.min );
- this.max.copy( box.max );
-
- return this;
-
- }
-
- /**
- * Makes this box empty which means in encloses a zero space in 2D.
- *
- * @return {Box2} A reference to this bounding box.
- */
- makeEmpty() {
-
- this.min.x = this.min.y = + Infinity;
- this.max.x = this.max.y = - Infinity;
-
- return this;
-
- }
-
- /**
- * Returns true if this box includes zero points within its bounds.
- * Note that a box with equal lower and upper bounds still includes one
- * point, the one both bounds share.
- *
- * @return {boolean} Whether this box is empty or not.
- */
- isEmpty() {
-
- // this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes
-
- return ( this.max.x < this.min.x ) || ( this.max.y < this.min.y );
-
- }
-
- /**
- * Returns the center point of this box.
- *
- * @param {Vector2} target - The target vector that is used to store the method's result.
- * @return {Vector2} The center point.
- */
- getCenter( target ) {
-
- return this.isEmpty() ? target.set( 0, 0 ) : target.addVectors( this.min, this.max ).multiplyScalar( 0.5 );
-
- }
-
- /**
- * Returns the dimensions of this box.
- *
- * @param {Vector2} target - The target vector that is used to store the method's result.
- * @return {Vector2} The size.
- */
- getSize( target ) {
-
- return this.isEmpty() ? target.set( 0, 0 ) : target.subVectors( this.max, this.min );
-
- }
-
- /**
- * Expands the boundaries of this box to include the given point.
- *
- * @param {Vector2} point - The point that should be included by the bounding box.
- * @return {Box2} A reference to this bounding box.
- */
- expandByPoint( point ) {
-
- this.min.min( point );
- this.max.max( point );
-
- return this;
-
- }
-
- /**
- * Expands this box equilaterally by the given vector. The width of this
- * box will be expanded by the x component of the vector in both
- * directions. The height of this box will be expanded by the y component of
- * the vector in both directions.
- *
- * @param {Vector2} vector - The vector that should expand the bounding box.
- * @return {Box2} A reference to this bounding box.
- */
- expandByVector( vector ) {
-
- this.min.sub( vector );
- this.max.add( vector );
-
- return this;
-
- }
-
- /**
- * Expands each dimension of the box by the given scalar. If negative, the
- * dimensions of the box will be contracted.
- *
- * @param {number} scalar - The scalar value that should expand the bounding box.
- * @return {Box2} A reference to this bounding box.
- */
- expandByScalar( scalar ) {
-
- this.min.addScalar( - scalar );
- this.max.addScalar( scalar );
-
- return this;
-
- }
-
- /**
- * Returns `true` if the given point lies within or on the boundaries of this box.
- *
- * @param {Vector2} point - The point to test.
- * @return {boolean} Whether the bounding box contains the given point or not.
- */
- containsPoint( point ) {
-
- return point.x >= this.min.x && point.x <= this.max.x &&
- point.y >= this.min.y && point.y <= this.max.y;
-
- }
-
- /**
- * Returns `true` if this bounding box includes the entirety of the given bounding box.
- * If this box and the given one are identical, this function also returns `true`.
- *
- * @param {Box2} box - The bounding box to test.
- * @return {boolean} Whether the bounding box contains the given bounding box or not.
- */
- containsBox( box ) {
-
- return this.min.x <= box.min.x && box.max.x <= this.max.x &&
- this.min.y <= box.min.y && box.max.y <= this.max.y;
-
- }
-
- /**
- * Returns a point as a proportion of this box's width and height.
- *
- * @param {Vector2} point - A point in 2D space.
- * @param {Vector2} target - The target vector that is used to store the method's result.
- * @return {Vector2} A point as a proportion of this box's width and height.
- */
- getParameter( point, target ) {
-
- // This can potentially have a divide by zero if the box
- // has a size dimension of 0.
-
- return target.set(
- ( point.x - this.min.x ) / ( this.max.x - this.min.x ),
- ( point.y - this.min.y ) / ( this.max.y - this.min.y )
- );
-
- }
-
- /**
- * Returns `true` if the given bounding box intersects with this bounding box.
- *
- * @param {Box2} box - The bounding box to test.
- * @return {boolean} Whether the given bounding box intersects with this bounding box.
- */
- intersectsBox( box ) {
-
- // using 4 splitting planes to rule out intersections
-
- return box.max.x >= this.min.x && box.min.x <= this.max.x &&
- box.max.y >= this.min.y && box.min.y <= this.max.y;
-
- }
-
- /**
- * Clamps the given point within the bounds of this box.
- *
- * @param {Vector2} point - The point to clamp.
- * @param {Vector2} target - The target vector that is used to store the method's result.
- * @return {Vector2} The clamped point.
- */
- clampPoint( point, target ) {
-
- return target.copy( point ).clamp( this.min, this.max );
-
- }
-
- /**
- * Returns the euclidean distance from any edge of this box to the specified point. If
- * the given point lies inside of this box, the distance will be `0`.
- *
- * @param {Vector2} point - The point to compute the distance to.
- * @return {number} The euclidean distance.
- */
- distanceToPoint( point ) {
-
- return this.clampPoint( point, _vector$4 ).distanceTo( point );
-
- }
-
- /**
- * Computes the intersection of this bounding box and the given one, setting the upper
- * bound of this box to the lesser of the two boxes' upper bounds and the
- * lower bound of this box to the greater of the two boxes' lower bounds. If
- * there's no overlap, makes this box empty.
- *
- * @param {Box2} box - The bounding box to intersect with.
- * @return {Box2} A reference to this bounding box.
- */
- intersect( box ) {
-
- this.min.max( box.min );
- this.max.min( box.max );
-
- if ( this.isEmpty() ) this.makeEmpty();
-
- return this;
-
- }
-
- /**
- * Computes the union of this box and another and the given one, setting the upper
- * bound of this box to the greater of the two boxes' upper bounds and the
- * lower bound of this box to the lesser of the two boxes' lower bounds.
- *
- * @param {Box2} box - The bounding box that will be unioned with this instance.
- * @return {Box2} A reference to this bounding box.
- */
- union( box ) {
-
- this.min.min( box.min );
- this.max.max( box.max );
-
- return this;
-
- }
-
- /**
- * Adds the given offset to both the upper and lower bounds of this bounding box,
- * effectively moving it in 2D space.
- *
- * @param {Vector2} offset - The offset that should be used to translate the bounding box.
- * @return {Box2} A reference to this bounding box.
- */
- translate( offset ) {
-
- this.min.add( offset );
- this.max.add( offset );
-
- return this;
-
- }
-
- /**
- * Returns `true` if this bounding box is equal with the given one.
- *
- * @param {Box2} box - The box to test for equality.
- * @return {boolean} Whether this bounding box is equal with the given one.
- */
- equals( box ) {
-
- return box.min.equals( this.min ) && box.max.equals( this.max );
-
- }
-
-}
-
-const _startP = /*@__PURE__*/ new Vector3();
-const _startEnd = /*@__PURE__*/ new Vector3();
-
-const _d1 = /*@__PURE__*/ new Vector3();
-const _d2 = /*@__PURE__*/ new Vector3();
-const _r = /*@__PURE__*/ new Vector3();
-const _c1 = /*@__PURE__*/ new Vector3();
-const _c2 = /*@__PURE__*/ new Vector3();
-
-/**
- * An analytical line segment in 3D space represented by a start and end point.
- */
-class Line3 {
-
- /**
- * Constructs a new line segment.
- *
- * @param {Vector3} [start=(0,0,0)] - Start of the line segment.
- * @param {Vector3} [end=(0,0,0)] - End of the line segment.
- */
- constructor( start = new Vector3(), end = new Vector3() ) {
-
- /**
- * Start of the line segment.
- *
- * @type {Vector3}
- */
- this.start = start;
-
- /**
- * End of the line segment.
- *
- * @type {Vector3}
- */
- this.end = end;
-
- }
-
- /**
- * Sets the start and end values by copying the given vectors.
- *
- * @param {Vector3} start - The start point.
- * @param {Vector3} end - The end point.
- * @return {Line3} A reference to this line segment.
- */
- set( start, end ) {
-
- this.start.copy( start );
- this.end.copy( end );
-
- return this;
-
- }
-
- /**
- * Copies the values of the given line segment to this instance.
- *
- * @param {Line3} line - The line segment to copy.
- * @return {Line3} A reference to this line segment.
- */
- copy( line ) {
-
- this.start.copy( line.start );
- this.end.copy( line.end );
-
- return this;
-
- }
-
- /**
- * Returns the center of the line segment.
- *
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The center point.
- */
- getCenter( target ) {
-
- return target.addVectors( this.start, this.end ).multiplyScalar( 0.5 );
-
- }
-
- /**
- * Returns the delta vector of the line segment's start and end point.
- *
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The delta vector.
- */
- delta( target ) {
-
- return target.subVectors( this.end, this.start );
-
- }
-
- /**
- * Returns the squared Euclidean distance between the line' start and end point.
- *
- * @return {number} The squared Euclidean distance.
- */
- distanceSq() {
-
- return this.start.distanceToSquared( this.end );
-
- }
-
- /**
- * Returns the Euclidean distance between the line' start and end point.
- *
- * @return {number} The Euclidean distance.
- */
- distance() {
-
- return this.start.distanceTo( this.end );
-
- }
-
- /**
- * Returns a vector at a certain position along the line segment.
- *
- * @param {number} t - A value between `[0,1]` to represent a position along the line segment.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The delta vector.
- */
- at( t, target ) {
-
- return this.delta( target ).multiplyScalar( t ).add( this.start );
-
- }
-
- /**
- * Returns a point parameter based on the closest point as projected on the line segment.
- *
- * @param {Vector3} point - The point for which to return a point parameter.
- * @param {boolean} clampToLine - Whether to clamp the result to the range `[0,1]` or not.
- * @return {number} The point parameter.
- */
- closestPointToPointParameter( point, clampToLine ) {
-
- _startP.subVectors( point, this.start );
- _startEnd.subVectors( this.end, this.start );
-
- const startEnd2 = _startEnd.dot( _startEnd );
-
- if ( startEnd2 === 0 ) return 0;
-
- const startEnd_startP = _startEnd.dot( _startP );
-
- let t = startEnd_startP / startEnd2;
-
- if ( clampToLine ) {
-
- t = clamp( t, 0, 1 );
-
- }
-
- return t;
-
- }
-
- /**
- * Returns the closest point on the line for a given point.
- *
- * @param {Vector3} point - The point to compute the closest point on the line for.
- * @param {boolean} clampToLine - Whether to clamp the result to the range `[0,1]` or not.
- * @param {Vector3} target - The target vector that is used to store the method's result.
- * @return {Vector3} The closest point on the line.
- */
- closestPointToPoint( point, clampToLine, target ) {
-
- const t = this.closestPointToPointParameter( point, clampToLine );
-
- return this.delta( target ).multiplyScalar( t ).add( this.start );
-
- }
-
- /**
- * Returns the closest squared distance between this line segment and the given one.
- *
- * @param {Line3} line - The line segment to compute the closest squared distance to.
- * @param {Vector3} [c1] - The closest point on this line segment.
- * @param {Vector3} [c2] - The closest point on the given line segment.
- * @return {number} The squared distance between this line segment and the given one.
- */
- distanceSqToLine3( line, c1 = _c1, c2 = _c2 ) {
-
- // from Real-Time Collision Detection by Christer Ericson, chapter 5.1.9
-
- // Computes closest points C1 and C2 of S1(s)=P1+s*(Q1-P1) and
- // S2(t)=P2+t*(Q2-P2), returning s and t. Function result is squared
- // distance between between S1(s) and S2(t)
-
- const EPSILON = 1e-8 * 1e-8; // must be squared since we compare squared length
- let s, t;
-
- const p1 = this.start;
- const p2 = line.start;
- const q1 = this.end;
- const q2 = line.end;
-
- _d1.subVectors( q1, p1 ); // Direction vector of segment S1
- _d2.subVectors( q2, p2 ); // Direction vector of segment S2
- _r.subVectors( p1, p2 );
-
- const a = _d1.dot( _d1 ); // Squared length of segment S1, always nonnegative
- const e = _d2.dot( _d2 ); // Squared length of segment S2, always nonnegative
- const f = _d2.dot( _r );
-
- // Check if either or both segments degenerate into points
-
- if ( a <= EPSILON && e <= EPSILON ) {
-
- // Both segments degenerate into points
-
- c1.copy( p1 );
- c2.copy( p2 );
-
- c1.sub( c2 );
-
- return c1.dot( c1 );
-
- }
-
- if ( a <= EPSILON ) {
-
- // First segment degenerates into a point
-
- s = 0;
- t = f / e; // s = 0 => t = (b*s + f) / e = f / e
- t = clamp( t, 0, 1 );
-
-
- } else {
-
- const c = _d1.dot( _r );
-
- if ( e <= EPSILON ) {
-
- // Second segment degenerates into a point
-
- t = 0;
- s = clamp( - c / a, 0, 1 ); // t = 0 => s = (b*t - c) / a = -c / a
-
- } else {
-
- // The general nondegenerate case starts here
-
- const b = _d1.dot( _d2 );
- const denom = a * e - b * b; // Always nonnegative
-
- // If segments not parallel, compute closest point on L1 to L2 and
- // clamp to segment S1. Else pick arbitrary s (here 0)
-
- if ( denom !== 0 ) {
-
- s = clamp( ( b * f - c * e ) / denom, 0, 1 );
-
- } else {
-
- s = 0;
-
- }
-
- // Compute point on L2 closest to S1(s) using
- // t = Dot((P1 + D1*s) - P2,D2) / Dot(D2,D2) = (b*s + f) / e
-
- t = ( b * s + f ) / e;
-
- // If t in [0,1] done. Else clamp t, recompute s for the new value
- // of t using s = Dot((P2 + D2*t) - P1,D1) / Dot(D1,D1)= (t*b - c) / a
- // and clamp s to [0, 1]
-
- if ( t < 0 ) {
-
- t = 0.;
- s = clamp( - c / a, 0, 1 );
-
- } else if ( t > 1 ) {
-
- t = 1;
- s = clamp( ( b - c ) / a, 0, 1 );
-
- }
-
- }
-
- }
-
- c1.copy( p1 ).addScaledVector( _d1, s );
- c2.copy( p2 ).addScaledVector( _d2, t );
-
- return c1.distanceToSquared( c2 );
-
- }
-
- /**
- * Applies a 4x4 transformation matrix to this line segment.
- *
- * @param {Matrix4} matrix - The transformation matrix.
- * @return {Line3} A reference to this line segment.
- */
- applyMatrix4( matrix ) {
-
- this.start.applyMatrix4( matrix );
- this.end.applyMatrix4( matrix );
-
- return this;
-
- }
-
- /**
- * Returns `true` if this line segment is equal with the given one.
- *
- * @param {Line3} line - The line segment to test for equality.
- * @return {boolean} Whether this line segment is equal with the given one.
- */
- equals( line ) {
-
- return line.start.equals( this.start ) && line.end.equals( this.end );
-
- }
-
- /**
- * Returns a new line segment with copied values from this instance.
- *
- * @return {Line3} A clone of this instance.
- */
- clone() {
-
- return new this.constructor().copy( this );
-
- }
-
-}
-
-const _vector$3 = /*@__PURE__*/ new Vector3();
-
-/**
- * This displays a cone shaped helper object for a {@link SpotLight}.
- *
- * When the spot light or its target are transformed or light properties are
- * changed, it's necessary to call the `update()` method of the respective helper.
- *
- * ```js
- * const spotLight = new THREE.SpotLight( 0xffffff );
- * spotLight.position.set( 10, 10, 10 );
- * scene.add( spotLight );
- *
- * const spotLightHelper = new THREE.SpotLightHelper( spotLight );
- * scene.add( spotLightHelper );
- * ```
- *
- * @augments Object3D
- */
-class SpotLightHelper extends Object3D {
-
- /**
- * Constructs a new spot light helper.
- *
- * @param {HemisphereLight} light - The light to be visualized.
- * @param {number|Color|string} [color] - The helper's color. If not set, the helper will take
- * the color of the light.
- */
- constructor( light, color ) {
-
- super();
-
- /**
- * The light being visualized.
- *
- * @type {SpotLight}
- */
- this.light = light;
-
- this.matrixAutoUpdate = false;
-
- /**
- * The color parameter passed in the constructor.
- * If not set, the helper will take the color of the light.
- *
- * @type {number|Color|string}
- */
- this.color = color;
-
- this.type = 'SpotLightHelper';
-
- const geometry = new BufferGeometry();
-
- const positions = [
- 0, 0, 0, 0, 0, 1,
- 0, 0, 0, 1, 0, 1,
- 0, 0, 0, -1, 0, 1,
- 0, 0, 0, 0, 1, 1,
- 0, 0, 0, 0, -1, 1
- ];
-
- for ( let i = 0, j = 1, l = 32; i < l; i ++, j ++ ) {
-
- const p1 = ( i / l ) * Math.PI * 2;
- const p2 = ( j / l ) * Math.PI * 2;
-
- positions.push(
- Math.cos( p1 ), Math.sin( p1 ), 1,
- Math.cos( p2 ), Math.sin( p2 ), 1
- );
-
- }
-
- geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
-
- const material = new LineBasicMaterial( { fog: false, toneMapped: false } );
-
- this.cone = new LineSegments( geometry, material );
- this.add( this.cone );
-
- this.update();
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.cone.geometry.dispose();
- this.cone.material.dispose();
-
- }
-
- /**
- * Updates the helper to match the position and direction of the
- * light being visualized.
- */
- update() {
-
- this.light.updateWorldMatrix( true, false );
- this.light.target.updateWorldMatrix( true, false );
-
- // update the local matrix based on the parent and light target transforms
- if ( this.parent ) {
-
- this.parent.updateWorldMatrix( true );
-
- this.matrix
- .copy( this.parent.matrixWorld )
- .invert()
- .multiply( this.light.matrixWorld );
-
- } else {
-
- this.matrix.copy( this.light.matrixWorld );
-
- }
-
- this.matrixWorldNeedsUpdate = true;
-
- const coneLength = this.light.distance ? this.light.distance : 1000;
- const coneWidth = coneLength * Math.tan( this.light.angle );
-
- this.cone.scale.set( coneWidth, coneWidth, coneLength );
-
- _vector$3.setFromMatrixPosition( this.light.target.matrixWorld );
-
- this.cone.lookAt( _vector$3 );
-
- if ( this.color !== undefined ) {
-
- this.cone.material.color.set( this.color );
-
- } else {
-
- this.cone.material.color.copy( this.light.color );
-
- }
-
- }
-
-}
-
-const _vector$2 = /*@__PURE__*/ new Vector3();
-const _boneMatrix = /*@__PURE__*/ new Matrix4();
-const _matrixWorldInv = /*@__PURE__*/ new Matrix4();
-
-/**
- * A helper object to assist with visualizing a {@link Skeleton}.
- *
- * ```js
- * const helper = new THREE.SkeletonHelper( skinnedMesh );
- * scene.add( helper );
- * ```
- *
- * @augments LineSegments
- */
-class SkeletonHelper extends LineSegments {
-
- /**
- * Constructs a new skeleton helper.
- *
- * @param {Object3D} object - Usually an instance of {@link SkinnedMesh}. However, any 3D object
- * can be used if it represents a hierarchy of bones (see {@link Bone}).
- */
- constructor( object ) {
-
- const bones = getBoneList( object );
-
- const geometry = new BufferGeometry();
-
- const vertices = [];
- const colors = [];
-
- for ( let i = 0; i < bones.length; i ++ ) {
-
- const bone = bones[ i ];
-
- if ( bone.parent && bone.parent.isBone ) {
-
- vertices.push( 0, 0, 0 );
- vertices.push( 0, 0, 0 );
- colors.push( 0, 0, 0 );
- colors.push( 0, 0, 0 );
-
- }
-
- }
-
- geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );
-
- const material = new LineBasicMaterial( { vertexColors: true, depthTest: false, depthWrite: false, toneMapped: false, transparent: true } );
-
- super( geometry, material );
-
- /**
- * This flag can be used for type testing.
- *
- * @type {boolean}
- * @readonly
- * @default true
- */
- this.isSkeletonHelper = true;
-
- this.type = 'SkeletonHelper';
-
- /**
- * The object being visualized.
- *
- * @type {Object3D}
- */
- this.root = object;
-
- /**
- * The list of bones that the helper visualizes.
- *
- * @type {Array}
- */
- this.bones = bones;
-
- this.matrix = object.matrixWorld;
- this.matrixAutoUpdate = false;
-
- // colors
-
- const color1 = new Color( 0x0000ff );
- const color2 = new Color( 0x00ff00 );
-
- this.setColors( color1, color2 );
-
- }
-
- updateMatrixWorld( force ) {
-
- const bones = this.bones;
-
- const geometry = this.geometry;
- const position = geometry.getAttribute( 'position' );
-
- _matrixWorldInv.copy( this.root.matrixWorld ).invert();
-
- for ( let i = 0, j = 0; i < bones.length; i ++ ) {
-
- const bone = bones[ i ];
-
- if ( bone.parent && bone.parent.isBone ) {
-
- _boneMatrix.multiplyMatrices( _matrixWorldInv, bone.matrixWorld );
- _vector$2.setFromMatrixPosition( _boneMatrix );
- position.setXYZ( j, _vector$2.x, _vector$2.y, _vector$2.z );
-
- _boneMatrix.multiplyMatrices( _matrixWorldInv, bone.parent.matrixWorld );
- _vector$2.setFromMatrixPosition( _boneMatrix );
- position.setXYZ( j + 1, _vector$2.x, _vector$2.y, _vector$2.z );
-
- j += 2;
-
- }
-
- }
-
- geometry.getAttribute( 'position' ).needsUpdate = true;
-
- super.updateMatrixWorld( force );
-
- }
-
- /**
- * Defines the colors of the helper.
- *
- * @param {Color} color1 - The first line color for each bone.
- * @param {Color} color2 - The second line color for each bone.
- * @return {SkeletonHelper} A reference to this helper.
- */
- setColors( color1, color2 ) {
-
- const geometry = this.geometry;
- const colorAttribute = geometry.getAttribute( 'color' );
-
- for ( let i = 0; i < colorAttribute.count; i += 2 ) {
-
- colorAttribute.setXYZ( i, color1.r, color1.g, color1.b );
- colorAttribute.setXYZ( i + 1, color2.r, color2.g, color2.b );
-
- }
-
- colorAttribute.needsUpdate = true;
-
- return this;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.geometry.dispose();
- this.material.dispose();
-
- }
-
-}
-
-
-function getBoneList( object ) {
-
- const boneList = [];
-
- if ( object.isBone === true ) {
-
- boneList.push( object );
-
- }
-
- for ( let i = 0; i < object.children.length; i ++ ) {
-
- boneList.push( ...getBoneList( object.children[ i ] ) );
-
- }
-
- return boneList;
-
-}
-
-/**
- * This displays a helper object consisting of a spherical mesh for
- * visualizing an instance of {@link PointLight}.
- *
- * ```js
- * const pointLight = new THREE.PointLight( 0xff0000, 1, 100 );
- * pointLight.position.set( 10, 10, 10 );
- * scene.add( pointLight );
- *
- * const sphereSize = 1;
- * const pointLightHelper = new THREE.PointLightHelper( pointLight, sphereSize );
- * scene.add( pointLightHelper );
- * ```
- *
- * @augments Mesh
- */
-class PointLightHelper extends Mesh {
-
- /**
- * Constructs a new point light helper.
- *
- * @param {PointLight} light - The light to be visualized.
- * @param {number} [sphereSize=1] - The size of the sphere helper.
- * @param {number|Color|string} [color] - The helper's color. If not set, the helper will take
- * the color of the light.
- */
- constructor( light, sphereSize, color ) {
-
- const geometry = new SphereGeometry( sphereSize, 4, 2 );
- const material = new MeshBasicMaterial( { wireframe: true, fog: false, toneMapped: false } );
-
- super( geometry, material );
-
- /**
- * The light being visualized.
- *
- * @type {PointLight}
- */
- this.light = light;
-
- /**
- * The color parameter passed in the constructor.
- * If not set, the helper will take the color of the light.
- *
- * @type {number|Color|string}
- */
- this.color = color;
-
- this.type = 'PointLightHelper';
-
- this.matrix = this.light.matrixWorld;
- this.matrixAutoUpdate = false;
-
- this.update();
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.geometry.dispose();
- this.material.dispose();
-
- }
-
- /**
- * Updates the helper to match the position of the
- * light being visualized.
- */
- update() {
-
- this.matrixWorldNeedsUpdate = true;
-
- this.light.updateWorldMatrix( true, false );
-
- if ( this.color !== undefined ) {
-
- this.material.color.set( this.color );
-
- } else {
-
- this.material.color.copy( this.light.color );
-
- }
-
- /*
- const d = this.light.distance;
-
- if ( d === 0.0 ) {
-
- this.lightDistance.visible = false;
-
- } else {
-
- this.lightDistance.visible = true;
- this.lightDistance.scale.set( d, d, d );
-
- }
- */
-
- }
-
-}
-
-const _vector$1 = /*@__PURE__*/ new Vector3();
-const _color1 = /*@__PURE__*/ new Color();
-const _color2 = /*@__PURE__*/ new Color();
-
-/**
- * Creates a visual aid consisting of a spherical mesh for a
- * given {@link HemisphereLight}.
- *
- * When the hemisphere light is transformed or its light properties are changed,
- * it's necessary to call the `update()` method of the respective helper.
- *
- * ```js
- * const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
- * const helper = new THREE.HemisphereLightHelper( light, 5 );
- * scene.add( helper );
- * ```
- *
- * @augments Object3D
- */
-class HemisphereLightHelper extends Object3D {
-
- /**
- * Constructs a new hemisphere light helper.
- *
- * @param {HemisphereLight} light - The light to be visualized.
- * @param {number} [size=1] - The size of the mesh used to visualize the light.
- * @param {number|Color|string} [color] - The helper's color. If not set, the helper will take
- * the color of the light.
- */
- constructor( light, size, color ) {
-
- super();
-
- /**
- * The light being visualized.
- *
- * @type {HemisphereLight}
- */
- this.light = light;
-
- this.matrix = light.matrixWorld;
- this.matrixAutoUpdate = false;
-
- /**
- * The color parameter passed in the constructor.
- * If not set, the helper will take the color of the light.
- *
- * @type {number|Color|string}
- */
- this.color = color;
-
- this.type = 'HemisphereLightHelper';
-
- const geometry = new OctahedronGeometry( size );
- geometry.rotateY( Math.PI * 0.5 );
-
- this.material = new MeshBasicMaterial( { wireframe: true, fog: false, toneMapped: false } );
- if ( this.color === undefined ) this.material.vertexColors = true;
-
- const position = geometry.getAttribute( 'position' );
- const colors = new Float32Array( position.count * 3 );
-
- geometry.setAttribute( 'color', new BufferAttribute( colors, 3 ) );
-
- this.add( new Mesh( geometry, this.material ) );
-
- this.update();
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.children[ 0 ].geometry.dispose();
- this.children[ 0 ].material.dispose();
-
- }
-
- /**
- * Updates the helper to match the position and direction of the
- * light being visualized.
- */
- update() {
-
- const mesh = this.children[ 0 ];
-
- if ( this.color !== undefined ) {
-
- this.material.color.set( this.color );
-
- } else {
-
- const colors = mesh.geometry.getAttribute( 'color' );
-
- _color1.copy( this.light.color );
- _color2.copy( this.light.groundColor );
-
- for ( let i = 0, l = colors.count; i < l; i ++ ) {
-
- const color = ( i < ( l / 2 ) ) ? _color1 : _color2;
-
- colors.setXYZ( i, color.r, color.g, color.b );
-
- }
-
- colors.needsUpdate = true;
-
- }
-
- this.matrixWorldNeedsUpdate = true;
-
- this.light.updateWorldMatrix( true, false );
-
- mesh.lookAt( _vector$1.setFromMatrixPosition( this.light.matrixWorld ).negate() );
-
- }
-
-}
-
-/**
- * The helper is an object to define grids. Grids are two-dimensional
- * arrays of lines.
- *
- * ```js
- * const size = 10;
- * const divisions = 10;
- *
- * const gridHelper = new THREE.GridHelper( size, divisions );
- * scene.add( gridHelper );
- * ```
- *
- * @augments LineSegments
- */
-class GridHelper extends LineSegments {
-
- /**
- * Constructs a new grid helper.
- *
- * @param {number} [size=10] - The size of the grid.
- * @param {number} [divisions=10] - The number of divisions across the grid.
- * @param {number|Color|string} [color1=0x444444] - The color of the center line.
- * @param {number|Color|string} [color2=0x888888] - The color of the lines of the grid.
- */
- constructor( size = 10, divisions = 10, color1 = 0x444444, color2 = 0x888888 ) {
-
- color1 = new Color( color1 );
- color2 = new Color( color2 );
-
- const center = divisions / 2;
- const step = size / divisions;
- const halfSize = size / 2;
-
- const vertices = [], colors = [];
-
- for ( let i = 0, j = 0, k = - halfSize; i <= divisions; i ++, k += step ) {
-
- vertices.push( - halfSize, 0, k, halfSize, 0, k );
- vertices.push( k, 0, - halfSize, k, 0, halfSize );
-
- const color = i === center ? color1 : color2;
-
- color.toArray( colors, j ); j += 3;
- color.toArray( colors, j ); j += 3;
- color.toArray( colors, j ); j += 3;
- color.toArray( colors, j ); j += 3;
-
- }
-
- const geometry = new BufferGeometry();
- geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );
-
- const material = new LineBasicMaterial( { vertexColors: true, toneMapped: false } );
-
- super( geometry, material );
-
- this.type = 'GridHelper';
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.geometry.dispose();
- this.material.dispose();
-
- }
-
-}
-
-/**
- * This helper is an object to define polar grids. Grids are
- * two-dimensional arrays of lines.
- *
- * ```js
- * const radius = 10;
- * const sectors = 16;
- * const rings = 8;
- * const divisions = 64;
- *
- * const helper = new THREE.PolarGridHelper( radius, sectors, rings, divisions );
- * scene.add( helper );
- * ```
- *
- * @augments LineSegments
- */
-class PolarGridHelper extends LineSegments {
-
- /**
- * Constructs a new polar grid helper.
- *
- * @param {number} [radius=10] - The radius of the polar grid. This can be any positive number.
- * @param {number} [sectors=16] - The number of sectors the grid will be divided into. This can be any positive integer.
- * @param {number} [rings=16] - The number of rings. This can be any positive integer.
- * @param {number} [divisions=64] - The number of line segments used for each circle. This can be any positive integer.
- * @param {number|Color|string} [color1=0x444444] - The first color used for grid elements.
- * @param {number|Color|string} [color2=0x888888] - The second color used for grid elements.
- */
- constructor( radius = 10, sectors = 16, rings = 8, divisions = 64, color1 = 0x444444, color2 = 0x888888 ) {
-
- color1 = new Color( color1 );
- color2 = new Color( color2 );
-
- const vertices = [];
- const colors = [];
-
- // create the sectors
-
- if ( sectors > 1 ) {
-
- for ( let i = 0; i < sectors; i ++ ) {
-
- const v = ( i / sectors ) * ( Math.PI * 2 );
-
- const x = Math.sin( v ) * radius;
- const z = Math.cos( v ) * radius;
-
- vertices.push( 0, 0, 0 );
- vertices.push( x, 0, z );
-
- const color = ( i & 1 ) ? color1 : color2;
-
- colors.push( color.r, color.g, color.b );
- colors.push( color.r, color.g, color.b );
-
- }
-
- }
-
- // create the rings
-
- for ( let i = 0; i < rings; i ++ ) {
-
- const color = ( i & 1 ) ? color1 : color2;
-
- const r = radius - ( radius / rings * i );
-
- for ( let j = 0; j < divisions; j ++ ) {
-
- // first vertex
-
- let v = ( j / divisions ) * ( Math.PI * 2 );
-
- let x = Math.sin( v ) * r;
- let z = Math.cos( v ) * r;
-
- vertices.push( x, 0, z );
- colors.push( color.r, color.g, color.b );
-
- // second vertex
-
- v = ( ( j + 1 ) / divisions ) * ( Math.PI * 2 );
-
- x = Math.sin( v ) * r;
- z = Math.cos( v ) * r;
-
- vertices.push( x, 0, z );
- colors.push( color.r, color.g, color.b );
-
- }
-
- }
-
- const geometry = new BufferGeometry();
- geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );
-
- const material = new LineBasicMaterial( { vertexColors: true, toneMapped: false } );
-
- super( geometry, material );
-
- this.type = 'PolarGridHelper';
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.geometry.dispose();
- this.material.dispose();
-
- }
-
-}
-
-const _v1 = /*@__PURE__*/ new Vector3();
-const _v2 = /*@__PURE__*/ new Vector3();
-const _v3 = /*@__PURE__*/ new Vector3();
-
-/**
- * Helper object to assist with visualizing a {@link DirectionalLight}'s
- * effect on the scene. This consists of a plane and a line representing the
- * light's position and direction.
- *
- * When the directional light or its target are transformed or light properties
- * are changed, it's necessary to call the `update()` method of the respective helper.
- *
- * ```js
- * const light = new THREE.DirectionalLight( 0xFFFFFF );
- * scene.add( light );
- *
- * const helper = new THREE.DirectionalLightHelper( light, 5 );
- * scene.add( helper );
- * ```
- *
- * @augments Object3D
- */
-class DirectionalLightHelper extends Object3D {
-
- /**
- * Constructs a new directional light helper.
- *
- * @param {DirectionalLight} light - The light to be visualized.
- * @param {number} [size=1] - The dimensions of the plane.
- * @param {number|Color|string} [color] - The helper's color. If not set, the helper will take
- * the color of the light.
- */
- constructor( light, size, color ) {
-
- super();
-
- /**
- * The light being visualized.
- *
- * @type {DirectionalLight}
- */
- this.light = light;
-
- this.matrix = light.matrixWorld;
- this.matrixAutoUpdate = false;
-
- /**
- * The color parameter passed in the constructor.
- * If not set, the helper will take the color of the light.
- *
- * @type {number|Color|string}
- */
- this.color = color;
-
- this.type = 'DirectionalLightHelper';
-
- if ( size === undefined ) size = 1;
-
- let geometry = new BufferGeometry();
- geometry.setAttribute( 'position', new Float32BufferAttribute( [
- - size, size, 0,
- size, size, 0,
- size, - size, 0,
- - size, - size, 0,
- - size, size, 0
- ], 3 ) );
-
- const material = new LineBasicMaterial( { fog: false, toneMapped: false } );
-
- /**
- * Contains the line showing the location of the directional light.
- *
- * @type {Line}
- */
- this.lightPlane = new Line( geometry, material );
- this.add( this.lightPlane );
-
- geometry = new BufferGeometry();
- geometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 0, 0, 1 ], 3 ) );
-
- /**
- * Represents the target line of the directional light.
- *
- * @type {Line}
- */
- this.targetLine = new Line( geometry, material );
- this.add( this.targetLine );
-
- this.update();
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.lightPlane.geometry.dispose();
- this.lightPlane.material.dispose();
- this.targetLine.geometry.dispose();
- this.targetLine.material.dispose();
-
- }
-
- /**
- * Updates the helper to match the position and direction of the
- * light being visualized.
- */
- update() {
-
- this.matrixWorldNeedsUpdate = true;
-
- this.light.updateWorldMatrix( true, false );
- this.light.target.updateWorldMatrix( true, false );
-
- _v1.setFromMatrixPosition( this.light.matrixWorld );
- _v2.setFromMatrixPosition( this.light.target.matrixWorld );
- _v3.subVectors( _v2, _v1 );
-
- this.lightPlane.lookAt( _v2 );
-
- if ( this.color !== undefined ) {
-
- this.lightPlane.material.color.set( this.color );
- this.targetLine.material.color.set( this.color );
-
- } else {
-
- this.lightPlane.material.color.copy( this.light.color );
- this.targetLine.material.color.copy( this.light.color );
-
- }
-
- this.targetLine.lookAt( _v2 );
- this.targetLine.scale.z = _v3.length();
-
- }
-
-}
-
-const _vector = /*@__PURE__*/ new Vector3();
-const _camera = /*@__PURE__*/ new Camera();
-
-/**
- * This helps with visualizing what a camera contains in its frustum. It
- * visualizes the frustum of a camera using a line segments.
- *
- * Based on frustum visualization in [lightgl.js shadowmap example](https://github.com/evanw/lightgl.js/blob/master/tests/shadowmap.html).
- *
- * `CameraHelper` must be a child of the scene.
- *
- * When the camera is transformed or its projection matrix is changed, it's necessary
- * to call the `update()` method of the respective helper.
- *
- * ```js
- * const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
- * const helper = new THREE.CameraHelper( camera );
- * scene.add( helper );
- * ```
- *
- * @augments LineSegments
- */
-class CameraHelper extends LineSegments {
-
- /**
- * Constructs a new arrow helper.
- *
- * @param {Camera} camera - The camera to visualize.
- */
- constructor( camera ) {
-
- const geometry = new BufferGeometry();
- const material = new LineBasicMaterial( { color: 0xffffff, vertexColors: true, toneMapped: false } );
-
- const vertices = [];
- const colors = [];
-
- const pointMap = {};
-
- // near
-
- addLine( 'n1', 'n2' );
- addLine( 'n2', 'n4' );
- addLine( 'n4', 'n3' );
- addLine( 'n3', 'n1' );
-
- // far
-
- addLine( 'f1', 'f2' );
- addLine( 'f2', 'f4' );
- addLine( 'f4', 'f3' );
- addLine( 'f3', 'f1' );
-
- // sides
-
- addLine( 'n1', 'f1' );
- addLine( 'n2', 'f2' );
- addLine( 'n3', 'f3' );
- addLine( 'n4', 'f4' );
-
- // cone
-
- addLine( 'p', 'n1' );
- addLine( 'p', 'n2' );
- addLine( 'p', 'n3' );
- addLine( 'p', 'n4' );
-
- // up
-
- addLine( 'u1', 'u2' );
- addLine( 'u2', 'u3' );
- addLine( 'u3', 'u1' );
-
- // target
-
- addLine( 'c', 't' );
- addLine( 'p', 'c' );
-
- // cross
-
- addLine( 'cn1', 'cn2' );
- addLine( 'cn3', 'cn4' );
-
- addLine( 'cf1', 'cf2' );
- addLine( 'cf3', 'cf4' );
-
- function addLine( a, b ) {
-
- addPoint( a );
- addPoint( b );
-
- }
-
- function addPoint( id ) {
-
- vertices.push( 0, 0, 0 );
- colors.push( 0, 0, 0 );
-
- if ( pointMap[ id ] === undefined ) {
-
- pointMap[ id ] = [];
-
- }
-
- pointMap[ id ].push( ( vertices.length / 3 ) - 1 );
-
- }
-
- geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );
-
- super( geometry, material );
-
- this.type = 'CameraHelper';
-
- /**
- * The camera being visualized.
- *
- * @type {Camera}
- */
- this.camera = camera;
- if ( this.camera.updateProjectionMatrix ) this.camera.updateProjectionMatrix();
-
- this.matrix = camera.matrixWorld;
- this.matrixAutoUpdate = false;
-
- /**
- * This contains the points used to visualize the camera.
- *
- * @type {Object>}
- */
- this.pointMap = pointMap;
-
- this.update();
-
- // colors
-
- const colorFrustum = new Color( 0xffaa00 );
- const colorCone = new Color( 0xff0000 );
- const colorUp = new Color( 0x00aaff );
- const colorTarget = new Color( 0xffffff );
- const colorCross = new Color( 0x333333 );
-
- this.setColors( colorFrustum, colorCone, colorUp, colorTarget, colorCross );
-
- }
-
- /**
- * Defines the colors of the helper.
- *
- * @param {Color} frustum - The frustum line color.
- * @param {Color} cone - The cone line color.
- * @param {Color} up - The up line color.
- * @param {Color} target - The target line color.
- * @param {Color} cross - The cross line color.
- * @return {CameraHelper} A reference to this helper.
- */
- setColors( frustum, cone, up, target, cross ) {
-
- const geometry = this.geometry;
-
- const colorAttribute = geometry.getAttribute( 'color' );
-
- // near
-
- colorAttribute.setXYZ( 0, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 1, frustum.r, frustum.g, frustum.b ); // n1, n2
- colorAttribute.setXYZ( 2, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 3, frustum.r, frustum.g, frustum.b ); // n2, n4
- colorAttribute.setXYZ( 4, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 5, frustum.r, frustum.g, frustum.b ); // n4, n3
- colorAttribute.setXYZ( 6, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 7, frustum.r, frustum.g, frustum.b ); // n3, n1
-
- // far
-
- colorAttribute.setXYZ( 8, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 9, frustum.r, frustum.g, frustum.b ); // f1, f2
- colorAttribute.setXYZ( 10, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 11, frustum.r, frustum.g, frustum.b ); // f2, f4
- colorAttribute.setXYZ( 12, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 13, frustum.r, frustum.g, frustum.b ); // f4, f3
- colorAttribute.setXYZ( 14, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 15, frustum.r, frustum.g, frustum.b ); // f3, f1
-
- // sides
-
- colorAttribute.setXYZ( 16, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 17, frustum.r, frustum.g, frustum.b ); // n1, f1
- colorAttribute.setXYZ( 18, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 19, frustum.r, frustum.g, frustum.b ); // n2, f2
- colorAttribute.setXYZ( 20, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 21, frustum.r, frustum.g, frustum.b ); // n3, f3
- colorAttribute.setXYZ( 22, frustum.r, frustum.g, frustum.b ); colorAttribute.setXYZ( 23, frustum.r, frustum.g, frustum.b ); // n4, f4
-
- // cone
-
- colorAttribute.setXYZ( 24, cone.r, cone.g, cone.b ); colorAttribute.setXYZ( 25, cone.r, cone.g, cone.b ); // p, n1
- colorAttribute.setXYZ( 26, cone.r, cone.g, cone.b ); colorAttribute.setXYZ( 27, cone.r, cone.g, cone.b ); // p, n2
- colorAttribute.setXYZ( 28, cone.r, cone.g, cone.b ); colorAttribute.setXYZ( 29, cone.r, cone.g, cone.b ); // p, n3
- colorAttribute.setXYZ( 30, cone.r, cone.g, cone.b ); colorAttribute.setXYZ( 31, cone.r, cone.g, cone.b ); // p, n4
-
- // up
-
- colorAttribute.setXYZ( 32, up.r, up.g, up.b ); colorAttribute.setXYZ( 33, up.r, up.g, up.b ); // u1, u2
- colorAttribute.setXYZ( 34, up.r, up.g, up.b ); colorAttribute.setXYZ( 35, up.r, up.g, up.b ); // u2, u3
- colorAttribute.setXYZ( 36, up.r, up.g, up.b ); colorAttribute.setXYZ( 37, up.r, up.g, up.b ); // u3, u1
-
- // target
-
- colorAttribute.setXYZ( 38, target.r, target.g, target.b ); colorAttribute.setXYZ( 39, target.r, target.g, target.b ); // c, t
- colorAttribute.setXYZ( 40, cross.r, cross.g, cross.b ); colorAttribute.setXYZ( 41, cross.r, cross.g, cross.b ); // p, c
-
- // cross
-
- colorAttribute.setXYZ( 42, cross.r, cross.g, cross.b ); colorAttribute.setXYZ( 43, cross.r, cross.g, cross.b ); // cn1, cn2
- colorAttribute.setXYZ( 44, cross.r, cross.g, cross.b ); colorAttribute.setXYZ( 45, cross.r, cross.g, cross.b ); // cn3, cn4
-
- colorAttribute.setXYZ( 46, cross.r, cross.g, cross.b ); colorAttribute.setXYZ( 47, cross.r, cross.g, cross.b ); // cf1, cf2
- colorAttribute.setXYZ( 48, cross.r, cross.g, cross.b ); colorAttribute.setXYZ( 49, cross.r, cross.g, cross.b ); // cf3, cf4
-
- colorAttribute.needsUpdate = true;
-
- return this;
-
- }
-
- /**
- * Updates the helper based on the projection matrix of the camera.
- */
- update() {
-
- const geometry = this.geometry;
- const pointMap = this.pointMap;
-
- const w = 1, h = 1;
-
- let nearZ, farZ;
-
- // we need just camera projection matrix inverse
- // world matrix must be identity
-
- _camera.projectionMatrixInverse.copy( this.camera.projectionMatrixInverse );
-
- // Adjust z values based on coordinate system
-
- if ( this.camera.reversedDepth === true ) {
-
- nearZ = 1;
- farZ = 0;
-
- } else {
-
- if ( this.camera.coordinateSystem === WebGLCoordinateSystem ) {
-
- nearZ = -1;
- farZ = 1;
-
- } else if ( this.camera.coordinateSystem === WebGPUCoordinateSystem ) {
-
- nearZ = 0;
- farZ = 1;
-
- } else {
-
- throw new Error( 'THREE.CameraHelper.update(): Invalid coordinate system: ' + this.camera.coordinateSystem );
-
- }
-
- }
-
-
- // center / target
- setPoint( 'c', pointMap, geometry, _camera, 0, 0, nearZ );
- setPoint( 't', pointMap, geometry, _camera, 0, 0, farZ );
-
- // near
-
- setPoint( 'n1', pointMap, geometry, _camera, - w, - h, nearZ );
- setPoint( 'n2', pointMap, geometry, _camera, w, - h, nearZ );
- setPoint( 'n3', pointMap, geometry, _camera, - w, h, nearZ );
- setPoint( 'n4', pointMap, geometry, _camera, w, h, nearZ );
-
- // far
-
- setPoint( 'f1', pointMap, geometry, _camera, - w, - h, farZ );
- setPoint( 'f2', pointMap, geometry, _camera, w, - h, farZ );
- setPoint( 'f3', pointMap, geometry, _camera, - w, h, farZ );
- setPoint( 'f4', pointMap, geometry, _camera, w, h, farZ );
-
- // up
-
- setPoint( 'u1', pointMap, geometry, _camera, w * 0.7, h * 1.1, nearZ );
- setPoint( 'u2', pointMap, geometry, _camera, - w * 0.7, h * 1.1, nearZ );
- setPoint( 'u3', pointMap, geometry, _camera, 0, h * 2, nearZ );
-
- // cross
-
- setPoint( 'cf1', pointMap, geometry, _camera, - w, 0, farZ );
- setPoint( 'cf2', pointMap, geometry, _camera, w, 0, farZ );
- setPoint( 'cf3', pointMap, geometry, _camera, 0, - h, farZ );
- setPoint( 'cf4', pointMap, geometry, _camera, 0, h, farZ );
-
- setPoint( 'cn1', pointMap, geometry, _camera, - w, 0, nearZ );
- setPoint( 'cn2', pointMap, geometry, _camera, w, 0, nearZ );
- setPoint( 'cn3', pointMap, geometry, _camera, 0, - h, nearZ );
- setPoint( 'cn4', pointMap, geometry, _camera, 0, h, nearZ );
-
- geometry.getAttribute( 'position' ).needsUpdate = true;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.geometry.dispose();
- this.material.dispose();
-
- }
-
-}
-
-
-function setPoint( point, pointMap, geometry, camera, x, y, z ) {
-
- _vector.set( x, y, z ).unproject( camera );
-
- const points = pointMap[ point ];
-
- if ( points !== undefined ) {
-
- const position = geometry.getAttribute( 'position' );
-
- for ( let i = 0, l = points.length; i < l; i ++ ) {
-
- position.setXYZ( points[ i ], _vector.x, _vector.y, _vector.z );
-
- }
-
- }
-
-}
-
-const _box = /*@__PURE__*/ new Box3();
-
-/**
- * Helper object to graphically show the world-axis-aligned bounding box
- * around an object. The actual bounding box is handled with {@link Box3},
- * this is just a visual helper for debugging. It can be automatically
- * resized with {@link BoxHelper#update} when the object it's created from
- * is transformed. Note that the object must have a geometry for this to work,
- * so it won't work with sprites.
- *
- * ```js
- * const sphere = new THREE.SphereGeometry();
- * const object = new THREE.Mesh( sphere, new THREE.MeshBasicMaterial( 0xff0000 ) );
- * const box = new THREE.BoxHelper( object, 0xffff00 );
- * scene.add( box );
- * ```
- *
- * @augments LineSegments
- */
-class BoxHelper extends LineSegments {
-
- /**
- * Constructs a new box helper.
- *
- * @param {Object3D} [object] - The 3D object to show the world-axis-aligned bounding box.
- * @param {number|Color|string} [color=0xffff00] - The box's color.
- */
- constructor( object, color = 0xffff00 ) {
-
- const indices = new Uint16Array( [ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] );
- const positions = new Float32Array( 8 * 3 );
-
- const geometry = new BufferGeometry();
- geometry.setIndex( new BufferAttribute( indices, 1 ) );
- geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
-
- super( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );
-
- /**
- * The 3D object being visualized.
- *
- * @type {Object3D}
- */
- this.object = object;
- this.type = 'BoxHelper';
-
- this.matrixAutoUpdate = false;
-
- this.update();
-
- }
-
- /**
- * Updates the helper's geometry to match the dimensions of the object,
- * including any children.
- */
- update() {
-
- if ( this.object !== undefined ) {
-
- _box.setFromObject( this.object );
-
- }
-
- if ( _box.isEmpty() ) return;
-
- const min = _box.min;
- const max = _box.max;
-
- /*
- 5____4
- 1/___0/|
- | 6__|_7
- 2/___3/
-
- 0: max.x, max.y, max.z
- 1: min.x, max.y, max.z
- 2: min.x, min.y, max.z
- 3: max.x, min.y, max.z
- 4: max.x, max.y, min.z
- 5: min.x, max.y, min.z
- 6: min.x, min.y, min.z
- 7: max.x, min.y, min.z
- */
-
- const position = this.geometry.attributes.position;
- const array = position.array;
-
- array[ 0 ] = max.x; array[ 1 ] = max.y; array[ 2 ] = max.z;
- array[ 3 ] = min.x; array[ 4 ] = max.y; array[ 5 ] = max.z;
- array[ 6 ] = min.x; array[ 7 ] = min.y; array[ 8 ] = max.z;
- array[ 9 ] = max.x; array[ 10 ] = min.y; array[ 11 ] = max.z;
- array[ 12 ] = max.x; array[ 13 ] = max.y; array[ 14 ] = min.z;
- array[ 15 ] = min.x; array[ 16 ] = max.y; array[ 17 ] = min.z;
- array[ 18 ] = min.x; array[ 19 ] = min.y; array[ 20 ] = min.z;
- array[ 21 ] = max.x; array[ 22 ] = min.y; array[ 23 ] = min.z;
-
- position.needsUpdate = true;
-
- this.geometry.computeBoundingSphere();
-
- }
-
- /**
- * Updates the wireframe box for the passed object.
- *
- * @param {Object3D} object - The 3D object to create the helper for.
- * @return {BoxHelper} A reference to this instance.
- */
- setFromObject( object ) {
-
- this.object = object;
- this.update();
-
- return this;
-
- }
-
- copy( source, recursive ) {
-
- super.copy( source, recursive );
-
- this.object = source.object;
-
- return this;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.geometry.dispose();
- this.material.dispose();
-
- }
-
-}
-
-/**
- * A helper object to visualize an instance of {@link Box3}.
- *
- * ```js
- * const box = new THREE.Box3();
- * box.setFromCenterAndSize( new THREE.Vector3( 1, 1, 1 ), new THREE.Vector3( 2, 1, 3 ) );
- *
- * const helper = new THREE.Box3Helper( box, 0xffff00 );
- * scene.add( helper )
- * ```
- *
- * @augments LineSegments
- */
-class Box3Helper extends LineSegments {
-
- /**
- * Constructs a new box3 helper.
- *
- * @param {Box3} box - The box to visualize.
- * @param {number|Color|string} [color=0xffff00] - The box's color.
- */
- constructor( box, color = 0xffff00 ) {
-
- const indices = new Uint16Array( [ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] );
-
- const positions = [ 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1 ];
-
- const geometry = new BufferGeometry();
-
- geometry.setIndex( new BufferAttribute( indices, 1 ) );
-
- geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
-
- super( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );
-
- /**
- * The box being visualized.
- *
- * @type {Box3}
- */
- this.box = box;
-
- this.type = 'Box3Helper';
-
- this.geometry.computeBoundingSphere();
-
- }
-
- updateMatrixWorld( force ) {
-
- const box = this.box;
-
- if ( box.isEmpty() ) return;
-
- box.getCenter( this.position );
-
- box.getSize( this.scale );
-
- this.scale.multiplyScalar( 0.5 );
-
- super.updateMatrixWorld( force );
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.geometry.dispose();
- this.material.dispose();
-
- }
-
-}
-
-/**
- * A helper object to visualize an instance of {@link Plane}.
- *
- * ```js
- * const plane = new THREE.Plane( new THREE.Vector3( 1, 1, 0.2 ), 3 );
- * const helper = new THREE.PlaneHelper( plane, 1, 0xffff00 );
- * scene.add( helper );
- * ```
- *
- * @augments Line
- */
-class PlaneHelper extends Line {
-
- /**
- * Constructs a new plane helper.
- *
- * @param {Plane} plane - The plane to be visualized.
- * @param {number} [size=1] - The side length of plane helper.
- * @param {number|Color|string} [hex=0xffff00] - The helper's color.
- */
- constructor( plane, size = 1, hex = 0xffff00 ) {
-
- const color = hex;
-
- const positions = [ 1, -1, 0, -1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0, -1, -1, 0, 1, -1, 0, 1, 1, 0 ];
-
- const geometry = new BufferGeometry();
- geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
- geometry.computeBoundingSphere();
-
- super( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );
-
- this.type = 'PlaneHelper';
-
- /**
- * The plane being visualized.
- *
- * @type {Plane}
- */
- this.plane = plane;
-
- /**
- * The side length of plane helper.
- *
- * @type {number}
- * @default 1
- */
- this.size = size;
-
- const positions2 = [ 1, 1, 0, -1, 1, 0, -1, -1, 0, 1, 1, 0, -1, -1, 0, 1, -1, 0 ];
-
- const geometry2 = new BufferGeometry();
- geometry2.setAttribute( 'position', new Float32BufferAttribute( positions2, 3 ) );
- geometry2.computeBoundingSphere();
-
- this.add( new Mesh( geometry2, new MeshBasicMaterial( { color: color, opacity: 0.2, transparent: true, depthWrite: false, toneMapped: false } ) ) );
-
- }
-
- updateMatrixWorld( force ) {
-
- this.position.set( 0, 0, 0 );
-
- this.scale.set( 0.5 * this.size, 0.5 * this.size, 1 );
-
- this.lookAt( this.plane.normal );
-
- this.translateZ( - this.plane.constant );
-
- super.updateMatrixWorld( force );
-
- }
-
- /**
- * Updates the helper to match the position and direction of the
- * light being visualized.
- */
- dispose() {
-
- super.dispose();
-
- this.geometry.dispose();
- this.material.dispose();
- this.children[ 0 ].geometry.dispose();
- this.children[ 0 ].material.dispose();
-
- }
-
-}
-
-const _axis = /*@__PURE__*/ new Vector3();
-let _lineGeometry, _coneGeometry;
-
-/**
- * An 3D arrow object for visualizing directions.
- *
- * ```js
- * const dir = new THREE.Vector3( 1, 2, 0 );
- *
- * //normalize the direction vector (convert to vector of length 1)
- * dir.normalize();
- *
- * const origin = new THREE.Vector3( 0, 0, 0 );
- * const length = 1;
- * const hex = 0xffff00;
- *
- * const arrowHelper = new THREE.ArrowHelper( dir, origin, length, hex );
- * scene.add( arrowHelper );
- * ```
- *
- * @augments Object3D
- */
-class ArrowHelper extends Object3D {
-
- /**
- * Constructs a new arrow helper.
- *
- * @param {Vector3} [dir=(0, 0, 1)] - The (normalized) direction vector.
- * @param {Vector3} [origin=(0, 0, 0)] - Point at which the arrow starts.
- * @param {number} [length=1] - Length of the arrow in world units.
- * @param {(number|Color|string)} [color=0xffff00] - Color of the arrow.
- * @param {number} [headLength=length*0.2] - The length of the head of the arrow.
- * @param {number} [headWidth=headLength*0.2] - The width of the head of the arrow.
- */
- constructor( dir = new Vector3( 0, 0, 1 ), origin = new Vector3( 0, 0, 0 ), length = 1, color = 0xffff00, headLength = length * 0.2, headWidth = headLength * 0.2 ) {
-
- super();
-
- this.type = 'ArrowHelper';
-
- if ( _lineGeometry === undefined ) {
-
- _lineGeometry = new BufferGeometry();
- _lineGeometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 0, 1, 0 ], 3 ) );
-
- _coneGeometry = new ConeGeometry( 0.5, 1, 5, 1 );
- _coneGeometry.translate( 0, -0.5, 0 );
-
- }
-
- this.position.copy( origin );
-
- /**
- * The line part of the arrow helper.
- *
- * @type {Line}
- */
- this.line = new Line( _lineGeometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );
- this.line.matrixAutoUpdate = false;
- this.add( this.line );
-
- /**
- * The cone part of the arrow helper.
- *
- * @type {Mesh}
- */
- this.cone = new Mesh( _coneGeometry, new MeshBasicMaterial( { color: color, toneMapped: false } ) );
- this.cone.matrixAutoUpdate = false;
- this.add( this.cone );
-
- this.setDirection( dir );
- this.setLength( length, headLength, headWidth );
-
- }
-
- /**
- * Sets the direction of the helper.
- *
- * @param {Vector3} dir - The normalized direction vector.
- */
- setDirection( dir ) {
-
- // dir is assumed to be normalized
-
- if ( dir.y > 0.99999 ) {
-
- this.quaternion.set( 0, 0, 0, 1 );
-
- } else if ( dir.y < -0.99999 ) {
-
- this.quaternion.set( 1, 0, 0, 0 );
-
- } else {
-
- _axis.set( dir.z, 0, - dir.x ).normalize();
-
- const radians = Math.acos( dir.y );
-
- this.quaternion.setFromAxisAngle( _axis, radians );
-
- }
-
- }
-
- /**
- * Sets the length of the helper.
- *
- * @param {number} length - Length of the arrow in world units.
- * @param {number} [headLength=length*0.2] - The length of the head of the arrow.
- * @param {number} [headWidth=headLength*0.2] - The width of the head of the arrow.
- */
- setLength( length, headLength = length * 0.2, headWidth = headLength * 0.2 ) {
-
- this.line.scale.set( 1, Math.max( 0.0001, length - headLength ), 1 ); // see #17458
- this.line.updateMatrix();
-
- this.cone.scale.set( headWidth, headLength, headWidth );
- this.cone.position.y = length;
- this.cone.updateMatrix();
-
- }
-
- /**
- * Sets the color of the helper.
- *
- * @param {number|Color|string} color - The color to set.
- */
- setColor( color ) {
-
- this.line.material.color.set( color );
- this.cone.material.color.set( color );
-
- }
-
- copy( source ) {
-
- super.copy( source, false );
-
- this.line.copy( source.line );
- this.cone.copy( source.cone );
-
- return this;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.line.geometry.dispose();
- this.line.material.dispose();
- this.cone.geometry.dispose();
- this.cone.material.dispose();
-
- }
-
-}
-
-/**
- * An axis object to visualize the 3 axes in a simple way.
- * The X axis is red. The Y axis is green. The Z axis is blue.
- *
- * ```js
- * const axesHelper = new THREE.AxesHelper( 5 );
- * scene.add( axesHelper );
- * ```
- *
- * @augments LineSegments
- */
-class AxesHelper extends LineSegments {
-
- /**
- * Constructs a new axes helper.
- *
- * @param {number} [size=1] - Size of the lines representing the axes.
- */
- constructor( size = 1 ) {
-
- const vertices = [
- 0, 0, 0, size, 0, 0,
- 0, 0, 0, 0, size, 0,
- 0, 0, 0, 0, 0, size
- ];
-
- const colors = [
- 1, 0, 0, 1, 0.6, 0,
- 0, 1, 0, 0.6, 1, 0,
- 0, 0, 1, 0, 0.6, 1
- ];
-
- const geometry = new BufferGeometry();
- geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
- geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );
-
- const material = new LineBasicMaterial( { vertexColors: true, toneMapped: false } );
-
- super( geometry, material );
-
- this.type = 'AxesHelper';
-
- }
-
- /**
- * Defines the colors of the axes helper.
- *
- * @param {number|Color|string} xAxisColor - The color for the x axis.
- * @param {number|Color|string} yAxisColor - The color for the y axis.
- * @param {number|Color|string} zAxisColor - The color for the z axis.
- * @return {AxesHelper} A reference to this axes helper.
- */
- setColors( xAxisColor, yAxisColor, zAxisColor ) {
-
- const color = new Color();
- const array = this.geometry.attributes.color.array;
-
- color.set( xAxisColor );
- color.toArray( array, 0 );
- color.toArray( array, 3 );
-
- color.set( yAxisColor );
- color.toArray( array, 6 );
- color.toArray( array, 9 );
-
- color.set( zAxisColor );
- color.toArray( array, 12 );
- color.toArray( array, 15 );
-
- this.geometry.attributes.color.needsUpdate = true;
-
- return this;
-
- }
-
- /**
- * Frees the GPU-related resources allocated by this instance. Call this
- * method whenever this instance is no longer used in your app.
- */
- dispose() {
-
- super.dispose();
-
- this.geometry.dispose();
- this.material.dispose();
-
- }
-
-}
-
-/**
- * This class is used to convert a series of paths to an array of
- * shapes. It is specifically used in context of fonts and SVG.
- */
-class ShapePath {
-
- /**
- * Constructs a new shape path.
- */
- constructor() {
-
- this.type = 'ShapePath';
-
- /**
- * The color of the shape.
- *
- * @type {Color}
- */
- this.color = new Color();
-
- /**
- * The paths that have been generated for this shape.
- *
- * @type {Array}
- * @default null
- */
- this.subPaths = [];
-
- /**
- * The current path that is being generated.
- *
- * @type {?Path}
- * @default null
- */
- this.currentPath = null;
-
- /**
- * An object that can be used to store custom data about the shape path.
- * Mainly used by SVGLoader to store style information.
- *
- * @type {Object}
- */
- this.userData = {};
-
- }
-
- /**
- * Creates a new path and moves it current point to the given one.
- *
- * @param {number} x - The x coordinate.
- * @param {number} y - The y coordinate.
- * @return {ShapePath} A reference to this shape path.
- */
- moveTo( x, y ) {
-
- this.currentPath = new Path();
- this.subPaths.push( this.currentPath );
- this.currentPath.moveTo( x, y );
-
- return this;
-
- }
-
- /**
- * Adds an instance of {@link LineCurve} to the path by connecting
- * the current point with the given one.
- *
- * @param {number} x - The x coordinate of the end point.
- * @param {number} y - The y coordinate of the end point.
- * @return {ShapePath} A reference to this shape path.
- */
- lineTo( x, y ) {
-
- this.currentPath.lineTo( x, y );
-
- return this;
-
- }
-
- /**
- * Adds an instance of {@link QuadraticBezierCurve} to the path by connecting
- * the current point with the given one.
- *
- * @param {number} aCPx - The x coordinate of the control point.
- * @param {number} aCPy - The y coordinate of the control point.
- * @param {number} aX - The x coordinate of the end point.
- * @param {number} aY - The y coordinate of the end point.
- * @return {ShapePath} A reference to this shape path.
- */
- quadraticCurveTo( aCPx, aCPy, aX, aY ) {
-
- this.currentPath.quadraticCurveTo( aCPx, aCPy, aX, aY );
-
- return this;
-
- }
-
- /**
- * Adds an instance of {@link CubicBezierCurve} to the path by connecting
- * the current point with the given one.
- *
- * @param {number} aCP1x - The x coordinate of the first control point.
- * @param {number} aCP1y - The y coordinate of the first control point.
- * @param {number} aCP2x - The x coordinate of the second control point.
- * @param {number} aCP2y - The y coordinate of the second control point.
- * @param {number} aX - The x coordinate of the end point.
- * @param {number} aY - The y coordinate of the end point.
- * @return {ShapePath} A reference to this shape path.
- */
- bezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY ) {
-
- this.currentPath.bezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY );
-
- return this;
-
- }
-
- /**
- * Adds an instance of {@link SplineCurve} to the path by connecting
- * the current point with the given list of points.
- *
- * @param {Array} pts - An array of points in 2D space.
- * @return {ShapePath} A reference to this shape path.
- */
- splineThru( pts ) {
-
- this.currentPath.splineThru( pts );
-
- return this;
-
- }
-
- /**
- * Converts the paths into an array of shapes.
- *
- * @return {Array} An array of shapes.
- */
- toShapes() {
-
- // Point-in-polygon test using the even-odd ray-casting rule. Valid for
- // simple (non self-intersecting) polygons.
- function pointInPolygon( p, polygon ) {
-
- let inside = false;
- const n = polygon.length;
-
- for ( let i = 0, j = n - 1; i < n; j = i ++ ) {
-
- const a = polygon[ i ];
- const b = polygon[ j ];
-
- if ( ( a.y > p.y ) !== ( b.y > p.y ) &&
- p.x < ( b.x - a.x ) * ( p.y - a.y ) / ( b.y - a.y ) + a.x ) {
-
- inside = ! inside;
-
- }
-
- }
-
- return inside;
-
- }
-
- // Returns a point guaranteed to be strictly inside the given simple
- // polygon. First tries the bounding-box center; if that falls outside
- // the polygon, casts a horizontal ray at the center's y and picks the
- // midpoint between the first two sorted intercepts.
- //
- // Port of paper.js' Path#getInteriorPoint()
- // https://github.com/paperjs/paper.js/blob/develop/src/path/PathItem.Boolean.js
- function getInteriorPoint( polygon, boundingBox ) {
-
- const point = boundingBox.getCenter( new Vector2() );
-
- if ( pointInPolygon( point, polygon ) ) return point;
-
- const y = point.y;
- const intercepts = [];
- const n = polygon.length;
-
- for ( let i = 0; i < n; i ++ ) {
-
- const a = polygon[ i ];
- const b = polygon[ ( i + 1 ) % n ];
-
- // Half-open crossing rule — counts each vertex exactly once and
- // skips horizontal edges.
- if ( ( a.y > y ) !== ( b.y > y ) ) {
-
- const x = a.x + ( y - a.y ) * ( b.x - a.x ) / ( b.y - a.y );
- intercepts.push( x );
-
- }
-
- }
-
- if ( intercepts.length > 1 ) {
-
- intercepts.sort( ( a, b ) => a - b );
- point.x = ( intercepts[ 0 ] + intercepts[ 1 ] ) / 2;
-
- }
-
- return point;
-
- }
-
- // Resolve fill-rule. Defaults to 'nonzero'.
- let fillRule = ( this.userData.style && this.userData.style.fillRule ) || 'nonzero';
-
- if ( fillRule !== 'nonzero' && fillRule !== 'evenodd' ) {
-
- warn( 'Fill-rule "' + fillRule + '" is not supported, falling back to "nonzero".' );
- fillRule = 'nonzero';
-
- }
-
- // Predicate that decides whether a winding number falls inside the fill
- // region, per the SVG fill-rule spec. Works for negative windings too,
- // because JavaScript's bitwise AND preserves odd/even under two's
- // complement.
- const isInside = fillRule === 'nonzero'
- ? ( w => w !== 0 )
- : ( w => ( w & 1 ) !== 0 );
-
- // Build an entry per usable subpath. Self-winding follows the standard
- // convention used by ShapeUtils: counter-clockwise (signed area > 0)
- // contributes +1 to the winding number at an interior point,
- // clockwise contributes -1.
- const entries = [];
-
- for ( const subPath of this.subPaths ) {
-
- const points = subPath.getPoints();
- if ( points.length < 3 ) continue;
-
- const area = ShapeUtils.area( points );
- if ( area === 0 ) continue;
-
- const boundingBox = new Box2();
- for ( let i = 0; i < points.length; i ++ ) boundingBox.expandByPoint( points[ i ] );
-
- entries.push( {
- subPath: subPath,
- points: points,
- boundingBox: boundingBox,
- interiorPoint: getInteriorPoint( points, boundingBox ),
- absArea: Math.abs( area ),
- winding: area < 0 ? -1 : 1,
- container: null,
- exclude: false,
- role: null
- } );
-
- }
-
- // Sort by area descending. This guarantees that any subpath that could
- // contain `entries[i]` is located at a smaller index and has already
- // been processed when it's entries[i]'s turn. Port of paper.js'
- // reorientPaths() algorithm.
- entries.sort( ( a, b ) => b.absArea - a.absArea );
-
- // Walk already-processed entries from closest-in-size to largest,
- // stopping at the innermost container. Accumulate the container's
- // cumulative winding into this entry's winding so that the final value
- // equals the winding number at this entry's interior point.
- //
- // A subpath only contributes to the fill boundary when crossing it
- // actually flips the "insideness" per the fill rule; otherwise it's a
- // redundant overlap and gets excluded to avoid double-counting.
- for ( let i = 0; i < entries.length; i ++ ) {
-
- const entry = entries[ i ];
- let containerWinding = 0;
-
- for ( let j = i - 1; j >= 0; j -- ) {
-
- const candidate = entries[ j ];
-
- if ( ! candidate.boundingBox.containsBox( entry.boundingBox ) ) continue;
- if ( ! pointInPolygon( entry.interiorPoint, candidate.points ) ) continue;
-
- entry.container = candidate.exclude ? candidate.container : candidate;
- containerWinding = candidate.winding;
- entry.winding += containerWinding;
- break;
-
- }
-
- if ( isInside( entry.winding ) === isInside( containerWinding ) ) {
-
- entry.exclude = true;
-
- }
-
- }
-
- // Classify retained entries. An entry is an outer shape if it has no
- // container or if its container is itself a hole (a solid nested inside
- // a hole becomes a new top-level shape); otherwise it's a hole in its
- // container. Entries were already sorted outermost-first, so each
- // container's role is known by the time we look at it.
- for ( const entry of entries ) {
-
- if ( entry.exclude ) continue;
- entry.role = ( entry.container === null || entry.container.role === 'hole' ) ? 'outer' : 'hole';
-
- }
-
- // Build Shapes for outers first, then attach holes to their container's
- // Shape.
- const shapes = [];
- const shapeByEntry = new Map();
-
- for ( const entry of entries ) {
-
- if ( entry.exclude || entry.role !== 'outer' ) continue;
-
- const shape = new Shape();
- shape.curves = entry.subPath.curves;
- shapes.push( shape );
- shapeByEntry.set( entry, shape );
-
- }
-
- for ( const entry of entries ) {
-
- if ( entry.exclude || entry.role !== 'hole' ) continue;
-
- const shape = shapeByEntry.get( entry.container );
- if ( ! shape ) continue;
-
- const hole = new Path();
- hole.curves = entry.subPath.curves;
- shape.holes.push( hole );
-
- }
-
- return shapes;
-
-
- }
-
-}
-
-/**
- * Abstract base class for controls.
- *
- * @abstract
- * @augments EventDispatcher
- */
-class Controls extends EventDispatcher {
-
- /**
- * Constructs a new controls instance.
- *
- * @param {Object3D} object - The object that is managed by the controls.
- * @param {?HTMLElement} domElement - The HTML element used for event listeners.
- */
- constructor( object, domElement = null ) {
-
- super();
-
- /**
- * The object that is managed by the controls.
- *
- * @type {Object3D}
- */
- this.object = object;
-
- /**
- * The HTML element used for event listeners.
- *
- * @type {?HTMLElement}
- * @default null
- */
- this.domElement = domElement;
-
- /**
- * Whether the controls responds to user input or not.
- *
- * @type {boolean}
- * @default true
- */
- this.enabled = true;
-
- /**
- * The internal state of the controls.
- *
- * @type {number}
- * @default -1
- */
- this.state = -1;
-
- /**
- * This object defines the keyboard input of the controls.
- *
- * @type {Object}
- */
- this.keys = {};
-
- /**
- * This object defines what type of actions are assigned to the available mouse buttons.
- * It depends on the control implementation what kind of mouse buttons and actions are supported.
- *
- * @type {{LEFT: ?number, MIDDLE: ?number, RIGHT: ?number}}
- */
- this.mouseButtons = { LEFT: null, MIDDLE: null, RIGHT: null };
-
- /**
- * This object defines what type of actions are assigned to what kind of touch interaction.
- * It depends on the control implementation what kind of touch interaction and actions are supported.
- *
- * @type {{ONE: ?number, TWO: ?number}}
- */
- this.touches = { ONE: null, TWO: null };
-
- }
-
- /**
- * Connects the controls to the DOM. This method has so called "side effects" since
- * it adds the module's event listeners to the DOM.
- *
- * @param {HTMLElement} element - The DOM element to connect to.
- */
- connect( element ) {
-
- if ( this.domElement !== null ) this.disconnect();
-
- this.domElement = element;
-
- }
-
- /**
- * Disconnects the controls from the DOM.
- */
- disconnect() {}
-
- /**
- * Call this method if you no longer want use to the controls. It frees all internal
- * resources and removes all event listeners.
- */
- dispose() {}
-
- /**
- * Controls should implement this method if they have to update their internal state
- * per simulation step.
- *
- * @param {number} [delta] - The time delta in seconds.
- */
- update( /* delta */ ) {}
-
-}
-
-/**
- * Scales the texture as large as possible within its surface without cropping
- * or stretching the texture. The method preserves the original aspect ratio of
- * the texture. Akin to CSS `object-fit: contain`
- *
- * @param {Texture} texture - The texture.
- * @param {number} aspect - The texture's aspect ratio.
- * @return {Texture} The updated texture.
- */
-function contain( texture, aspect ) {
-
- const imageAspect = ( texture.image && texture.image.width ) ? texture.image.width / texture.image.height : 1;
-
- if ( imageAspect > aspect ) {
-
- texture.repeat.x = 1;
- texture.repeat.y = imageAspect / aspect;
-
- texture.offset.x = 0;
- texture.offset.y = ( 1 - texture.repeat.y ) / 2;
-
- } else {
-
- texture.repeat.x = aspect / imageAspect;
- texture.repeat.y = 1;
-
- texture.offset.x = ( 1 - texture.repeat.x ) / 2;
- texture.offset.y = 0;
-
- }
-
- return texture;
-
-}
-
-/**
- * Scales the texture to the smallest possible size to fill the surface, leaving
- * no empty space. The method preserves the original aspect ratio of the texture.
- * Akin to CSS `object-fit: cover`.
- *
- * @param {Texture} texture - The texture.
- * @param {number} aspect - The texture's aspect ratio.
- * @return {Texture} The updated texture.
- */
-function cover( texture, aspect ) {
-
- const imageAspect = ( texture.image && texture.image.width ) ? texture.image.width / texture.image.height : 1;
-
- if ( imageAspect > aspect ) {
-
- texture.repeat.x = aspect / imageAspect;
- texture.repeat.y = 1;
-
- texture.offset.x = ( 1 - texture.repeat.x ) / 2;
- texture.offset.y = 0;
-
- } else {
-
- texture.repeat.x = 1;
- texture.repeat.y = imageAspect / aspect;
-
- texture.offset.x = 0;
- texture.offset.y = ( 1 - texture.repeat.y ) / 2;
-
- }
-
- return texture;
-
-}
-
-/**
- * Configures the texture to the default transformation. Akin to CSS `object-fit: fill`.
- *
- * @param {Texture} texture - The texture.
- * @return {Texture} The updated texture.
- */
-function fill( texture ) {
-
- texture.repeat.x = 1;
- texture.repeat.y = 1;
-
- texture.offset.x = 0;
- texture.offset.y = 0;
-
- return texture;
-
-}
-
-/**
- * Determines how many bytes must be used to represent the texture.
- *
- * @param {number} width - The width of the texture.
- * @param {number} height - The height of the texture.
- * @param {number} format - The texture's format.
- * @param {number} type - The texture's type.
- * @return {number} The byte length.
- */
-function getByteLength( width, height, format, type ) {
-
- const typeByteLength = getTextureTypeByteLength( type );
-
- switch ( format ) {
-
- // https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glTexImage2D.xhtml
- case AlphaFormat:
- return width * height;
- case RedFormat:
- return ( ( width * height ) / typeByteLength.components ) * typeByteLength.byteLength;
- case RedIntegerFormat:
- return ( ( width * height ) / typeByteLength.components ) * typeByteLength.byteLength;
- case RGFormat:
- return ( ( width * height * 2 ) / typeByteLength.components ) * typeByteLength.byteLength;
- case RGIntegerFormat:
- return ( ( width * height * 2 ) / typeByteLength.components ) * typeByteLength.byteLength;
- case RGBFormat:
- return ( ( width * height * 3 ) / typeByteLength.components ) * typeByteLength.byteLength;
- case RGBAFormat:
- return ( ( width * height * 4 ) / typeByteLength.components ) * typeByteLength.byteLength;
- case RGBAIntegerFormat:
- return ( ( width * height * 4 ) / typeByteLength.components ) * typeByteLength.byteLength;
-
- // https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_s3tc_srgb/
- case RGB_S3TC_DXT1_Format:
- case RGBA_S3TC_DXT1_Format:
- return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 8;
- case RGBA_S3TC_DXT3_Format:
- case RGBA_S3TC_DXT5_Format:
- return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;
-
- // https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_pvrtc/
- case RGB_PVRTC_2BPPV1_Format:
- case RGBA_PVRTC_2BPPV1_Format:
- return ( Math.max( width, 16 ) * Math.max( height, 8 ) ) / 4;
- case RGB_PVRTC_4BPPV1_Format:
- case RGBA_PVRTC_4BPPV1_Format:
- return ( Math.max( width, 8 ) * Math.max( height, 8 ) ) / 2;
-
- // https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc/
- case RGB_ETC1_Format:
- case RGB_ETC2_Format:
- case R11_EAC_Format:
- case SIGNED_R11_EAC_Format:
- return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 8;
- case RGBA_ETC2_EAC_Format:
- case RG11_EAC_Format:
- case SIGNED_RG11_EAC_Format:
- return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;
-
- // https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/
- case RGBA_ASTC_4x4_Format:
- return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;
- case RGBA_ASTC_5x4_Format:
- return Math.floor( ( width + 4 ) / 5 ) * Math.floor( ( height + 3 ) / 4 ) * 16;
- case RGBA_ASTC_5x5_Format:
- return Math.floor( ( width + 4 ) / 5 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
- case RGBA_ASTC_6x5_Format:
- return Math.floor( ( width + 5 ) / 6 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
- case RGBA_ASTC_6x6_Format:
- return Math.floor( ( width + 5 ) / 6 ) * Math.floor( ( height + 5 ) / 6 ) * 16;
- case RGBA_ASTC_8x5_Format:
- return Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
- case RGBA_ASTC_8x6_Format:
- return Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 5 ) / 6 ) * 16;
- case RGBA_ASTC_8x8_Format:
- return Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 7 ) / 8 ) * 16;
- case RGBA_ASTC_10x5_Format:
- return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
- case RGBA_ASTC_10x6_Format:
- return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 5 ) / 6 ) * 16;
- case RGBA_ASTC_10x8_Format:
- return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 7 ) / 8 ) * 16;
- case RGBA_ASTC_10x10_Format:
- return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 9 ) / 10 ) * 16;
- case RGBA_ASTC_12x10_Format:
- return Math.floor( ( width + 11 ) / 12 ) * Math.floor( ( height + 9 ) / 10 ) * 16;
- case RGBA_ASTC_12x12_Format:
- return Math.floor( ( width + 11 ) / 12 ) * Math.floor( ( height + 11 ) / 12 ) * 16;
-
- // https://registry.khronos.org/webgl/extensions/EXT_texture_compression_bptc/
- case RGBA_BPTC_Format:
- case RGB_BPTC_SIGNED_Format:
- case RGB_BPTC_UNSIGNED_Format:
- return Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 16;
-
- // https://registry.khronos.org/webgl/extensions/EXT_texture_compression_rgtc/
- case RED_RGTC1_Format:
- case SIGNED_RED_RGTC1_Format:
- return Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 8;
- case RED_GREEN_RGTC2_Format:
- case SIGNED_RED_GREEN_RGTC2_Format:
- return Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 16;
-
- }
-
- throw new Error(
- `Unable to determine texture byte length for ${format} format.`,
- );
-
-}
-
-function getTextureTypeByteLength( type ) {
-
- switch ( type ) {
-
- case UnsignedByteType:
- case ByteType:
- return { byteLength: 1, components: 1 };
- case UnsignedShortType:
- case ShortType:
- case HalfFloatType:
- return { byteLength: 2, components: 1 };
- case UnsignedShort4444Type:
- case UnsignedShort5551Type:
- return { byteLength: 2, components: 4 };
- case UnsignedIntType:
- case IntType:
- case FloatType:
- return { byteLength: 4, components: 1 };
- case UnsignedInt5999Type:
- case UnsignedInt101111Type:
- return { byteLength: 4, components: 3 };
-
- }
-
- throw new Error( `THREE.TextureUtils: Unknown texture type ${type}.` );
-
-}
-
-/**
- * A class containing utility functions for textures.
- *
- * @hideconstructor
- */
-class TextureUtils {
-
- /**
- * Scales the texture as large as possible within its surface without cropping
- * or stretching the texture. The method preserves the original aspect ratio of
- * the texture. Akin to CSS `object-fit: contain`
- *
- * @param {Texture} texture - The texture.
- * @param {number} aspect - The texture's aspect ratio.
- * @return {Texture} The updated texture.
- */
- static contain( texture, aspect ) {
-
- return contain( texture, aspect );
-
- }
-
- /**
- * Scales the texture to the smallest possible size to fill the surface, leaving
- * no empty space. The method preserves the original aspect ratio of the texture.
- * Akin to CSS `object-fit: cover`.
- *
- * @param {Texture} texture - The texture.
- * @param {number} aspect - The texture's aspect ratio.
- * @return {Texture} The updated texture.
- */
- static cover( texture, aspect ) {
-
- return cover( texture, aspect );
-
- }
-
- /**
- * Configures the texture to the default transformation. Akin to CSS `object-fit: fill`.
- *
- * @param {Texture} texture - The texture.
- * @return {Texture} The updated texture.
- */
- static fill( texture ) {
-
- return fill( texture );
-
- }
-
- /**
- * Determines how many bytes must be used to represent the texture.
- *
- * @param {number} width - The width of the texture.
- * @param {number} height - The height of the texture.
- * @param {number} format - The texture's format.
- * @param {number} type - The texture's type.
- * @return {number} The byte length.
- */
- static getByteLength( width, height, format, type ) {
-
- return getByteLength( width, height, format, type );
-
- }
-
-}
-
-if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {
-
- __THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'register', { detail: {
- revision: REVISION,
- } } ) );
-
-}
-
-if ( typeof window !== 'undefined' ) {
-
- if ( window.__THREE__ ) {
-
- warn( 'WARNING: Multiple instances of Three.js being imported.' );
-
- } else {
-
- window.__THREE__ = REVISION;
-
- }
-
-}
-
-export { ACESFilmicToneMapping, AddEquation, AddOperation, AdditiveAnimationBlendMode, AdditiveBlending, AgXToneMapping, AlphaFormat, AlwaysCompare, AlwaysDepth, AlwaysStencilFunc, AmbientLight, AnimationAction, AnimationClip, AnimationLoader, AnimationMixer, AnimationObjectGroup, AnimationUtils, ArcCurve, ArrayCamera, ArrowHelper, AttachedBindMode, Audio, AudioAnalyser, AudioContext, AudioListener, AudioLoader, AxesHelper, BackSide, BasicDepthPacking, BasicShadowMap, BatchedMesh, BezierInterpolant, Bone, BooleanKeyframeTrack, Box2, Box3, Box3Helper, BoxGeometry, BoxHelper, BufferAttribute, BufferGeometry, BufferGeometryLoader, ByteType, Cache, Camera, CameraHelper, CanvasTexture, CapsuleGeometry, CatmullRomCurve3, CineonToneMapping, CircleGeometry, ClampToEdgeWrapping, Clock, Color, ColorKeyframeTrack, ColorManagement, Compatibility, CompressedArrayTexture, CompressedCubeTexture, CompressedTexture, CompressedTextureLoader, ConeGeometry, ConstantAlphaFactor, ConstantColorFactor, Controls, CubeCamera, CubeDepthTexture, CubeReflectionMapping, CubeRefractionMapping, CubeTexture, CubeTextureLoader, CubeUVReflectionMapping, CubicBezierCurve, CubicBezierCurve3, CubicInterpolant, CullFaceBack, CullFaceFront, CullFaceFrontBack, CullFaceNone, Curve, CurvePath, CustomBlending, CustomToneMapping, CylinderGeometry, Cylindrical, Data3DTexture, DataArrayTexture, DataTexture, DataTextureLoader, DataUtils, DecrementStencilOp, DecrementWrapStencilOp, DefaultLoadingManager, DepthFormat, DepthStencilFormat, DepthTexture, DetachedBindMode, DirectionalLight, DirectionalLightHelper, DiscreteInterpolant, DodecahedronGeometry, DoubleSide, DstAlphaFactor, DstColorFactor, DynamicCopyUsage, DynamicDrawUsage, DynamicReadUsage, EdgesGeometry, EllipseCurve, EqualCompare, EqualDepth, EqualStencilFunc, EquirectangularReflectionMapping, EquirectangularRefractionMapping, Euler, EventDispatcher, ExternalTexture, ExtrudeGeometry, FileLoader, Float16BufferAttribute, Float32BufferAttribute, FloatType, Fog, FogExp2, FramebufferTexture, FrontSide, Frustum, FrustumArray, GLBufferAttribute, GLSL1, GLSL3, GreaterCompare, GreaterDepth, GreaterEqualCompare, GreaterEqualDepth, GreaterEqualStencilFunc, GreaterStencilFunc, GridHelper, Group, HTMLTexture, HalfFloatType, HemisphereLight, HemisphereLightHelper, IcosahedronGeometry, ImageBitmapLoader, ImageLoader, ImageUtils, IncrementStencilOp, IncrementWrapStencilOp, InstancedBufferAttribute, InstancedBufferGeometry, InstancedInterleavedBuffer, InstancedMesh, Int16BufferAttribute, Int32BufferAttribute, Int8BufferAttribute, IntType, InterleavedBuffer, InterleavedBufferAttribute, Interpolant, InterpolateBezier, InterpolateDiscrete, InterpolateLinear, InterpolateSmooth, InterpolationSamplingMode, InterpolationSamplingType, InvertStencilOp, KeepStencilOp, KeyframeTrack, LOD, LatheGeometry, Layers, LessCompare, LessDepth, LessEqualCompare, LessEqualDepth, LessEqualStencilFunc, LessStencilFunc, Light, LightProbe, LightShadow, Line, Line3, LineBasicMaterial, LineCurve, LineCurve3, LineDashedMaterial, LineLoop, LineSegments, LinearFilter, LinearInterpolant, LinearMipMapLinearFilter, LinearMipMapNearestFilter, LinearMipmapLinearFilter, LinearMipmapNearestFilter, LinearSRGBColorSpace, LinearToneMapping, LinearTransfer, Loader, LoaderUtils, LoadingManager, LoopOnce, LoopPingPong, LoopRepeat, MOUSE, Material, MaterialBlending, MaterialLoader, MathUtils, Matrix2, Matrix3, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshDistanceMaterial, MeshLambertMaterial, MeshMatcapMaterial, MeshNormalMaterial, MeshPhongMaterial, MeshPhysicalMaterial, MeshStandardMaterial, MeshToonMaterial, MinEquation, MirroredRepeatWrapping, MixOperation, MultiplyBlending, MultiplyOperation, NearestFilter, NearestMipMapLinearFilter, NearestMipMapNearestFilter, NearestMipmapLinearFilter, NearestMipmapNearestFilter, NeutralToneMapping, NeverCompare, NeverDepth, NeverStencilFunc, NoBlending, NoColorSpace, NoNormalPacking, NoToneMapping, NormalAnimationBlendMode, NormalBlending, NormalGAPacking, NormalRGPacking, NotEqualCompare, NotEqualDepth, NotEqualStencilFunc, NumberKeyframeTrack, Object3D, ObjectLoader, ObjectSpaceNormalMap, OctahedronGeometry, OneFactor, OneMinusConstantAlphaFactor, OneMinusConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcAlphaFactor, OneMinusSrcColorFactor, OrthographicCamera, PCFShadowMap, PCFSoftShadowMap, Path, PerspectiveCamera, Plane, PlaneGeometry, PlaneHelper, PointLight, PointLightHelper, Points, PointsMaterial, PolarGridHelper, PolyhedronGeometry, PositionalAudio, PropertyBinding, PropertyMixer, QuadraticBezierCurve, QuadraticBezierCurve3, Quaternion, QuaternionKeyframeTrack, QuaternionLinearInterpolant, R11_EAC_Format, RAD2DEG, RED_GREEN_RGTC2_Format, RED_RGTC1_Format, REVISION, RG11_EAC_Format, RGBADepthPacking, RGBAFormat, RGBAIntegerFormat, RGBA_ASTC_10x10_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_BPTC_Format, RGBA_ETC2_EAC_Format, RGBA_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGBDepthPacking, RGBFormat, RGBIntegerFormat, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGB_PVRTC_2BPPV1_Format, RGB_PVRTC_4BPPV1_Format, RGB_S3TC_DXT1_Format, RGDepthPacking, RGFormat, RGIntegerFormat, RawShaderMaterial, Ray, Raycaster, RectAreaLight, RedFormat, RedIntegerFormat, ReinhardToneMapping, RenderObjectRefreshType, RenderTarget, RenderTarget3D, RepeatWrapping, ReplaceStencilOp, ReverseSubtractEquation, ReversedDepthFuncs, RingGeometry, SIGNED_R11_EAC_Format, SIGNED_RED_GREEN_RGTC2_Format, SIGNED_RED_RGTC1_Format, SIGNED_RG11_EAC_Format, SRGBColorSpace, SRGBTransfer, Scene, ShaderMaterial, ShadowMaterial, Shape, ShapeGeometry, ShapePath, ShapeUtils, ShortType, Skeleton, SkeletonHelper, SkinnedMesh, Source, Sphere, SphereGeometry, Spherical, SphericalHarmonics3, SplineCurve, SpotLight, SpotLightHelper, Sprite, SpriteMaterial, SrcAlphaFactor, SrcAlphaSaturateFactor, SrcColorFactor, StaticCopyUsage, StaticDrawUsage, StaticReadUsage, StereoCamera, StreamCopyUsage, StreamDrawUsage, StreamReadUsage, StringKeyframeTrack, SubtractEquation, SubtractiveBlending, TOUCH, TangentSpaceNormalMap, TetrahedronGeometry, Texture, TextureLoader, TextureSource, TextureUtils, Timer, TimestampQuery, TorusGeometry, TorusKnotGeometry, Triangle, TriangleFanDrawMode, TriangleStripDrawMode, TrianglesDrawMode, TubeGeometry, UVMapping, Uint16BufferAttribute, Uint32BufferAttribute, Uint8BufferAttribute, Uint8ClampedBufferAttribute, Uniform, UniformsGroup, UniformsUtils, UnsignedByteType, UnsignedInt101111Type, UnsignedInt248Type, UnsignedInt5999Type, UnsignedIntType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedShortType, VSMShadowMap, Vector2, Vector3, Vector4, VectorKeyframeTrack, VideoFrameTexture, VideoTexture, WebGL3DRenderTarget, WebGLArrayRenderTarget, WebGLCoordinateSystem, WebGLRenderTarget, WebGPUCoordinateSystem, WebXRController, WireframeGeometry, WrapAroundEnding, ZeroCurvatureEnding, ZeroFactor, ZeroSlopeEnding, ZeroStencilOp, cloneUniforms, createCanvasElement, createElementNS, error, getByteLength, getConsoleFunction, getUnlitUniformColorSpace, isTypedArray, log, mergeUniforms, probeAsync, setConsoleFunction, warn, warnOnce, yieldToMain };
diff --git a/bluebey-studio/vendor/three/build/three.module.js b/bluebey-studio/vendor/three/build/three.module.js
deleted file mode 100644
index c52087a..0000000
--- a/bluebey-studio/vendor/three/build/three.module.js
+++ /dev/null
@@ -1,19719 +0,0 @@
-/**
- * @license
- * Copyright 2010-2026 Three.js Authors
- * SPDX-License-Identifier: MIT
- */
-import { Matrix3, Vector2, Color, Vector3, mergeUniforms, CubeUVReflectionMapping, Mesh, BoxGeometry, ShaderMaterial, BackSide, cloneUniforms, Matrix4, ColorManagement, SRGBTransfer, PlaneGeometry, FrontSide, getUnlitUniformColorSpace, IntType, warn, HalfFloatType, UnsignedByteType, FloatType, RGBAFormat, Plane, CubeReflectionMapping, CubeRefractionMapping, BufferGeometry, OrthographicCamera, PerspectiveCamera, NoToneMapping, MeshBasicMaterial, NoBlending, WebGLRenderTarget, BufferAttribute, LinearSRGBColorSpace, LinearFilter, CubeTexture, LinearMipmapLinearFilter, CubeCamera, EquirectangularReflectionMapping, EquirectangularRefractionMapping, warnOnce, Uint32BufferAttribute, Uint16BufferAttribute, error, DataArrayTexture, Vector4, Float32BufferAttribute, RawShaderMaterial, CustomToneMapping, NeutralToneMapping, AgXToneMapping, ACESFilmicToneMapping, CineonToneMapping, ReinhardToneMapping, LinearToneMapping, Data3DTexture, GreaterEqualCompare, LessEqualCompare, DepthTexture, Texture, GLSL3, VSMShadowMap, PCFShadowMap, AddOperation, MixOperation, MultiplyOperation, LinearTransfer, UniformsUtils, DoubleSide, NormalBlending, TangentSpaceNormalMap, ObjectSpaceNormalMap, Layers, RGFormat, RG11_EAC_Format, RED_GREEN_RGTC2_Format, MeshDepthMaterial, MeshDistanceMaterial, PCFSoftShadowMap, DepthFormat, NearestFilter, CubeDepthTexture, UnsignedIntType, Frustum, LessEqualDepth, ReverseSubtractEquation, SubtractEquation, AddEquation, OneMinusConstantAlphaFactor, ConstantAlphaFactor, OneMinusConstantColorFactor, ConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcAlphaFactor, OneMinusSrcColorFactor, DstAlphaFactor, DstColorFactor, SrcAlphaSaturateFactor, SrcAlphaFactor, SrcColorFactor, OneFactor, ZeroFactor, NotEqualDepth, GreaterDepth, GreaterEqualDepth, EqualDepth, LessDepth, AlwaysDepth, NeverDepth, CullFaceNone, CullFaceBack, CullFaceFront, CustomBlending, MultiplyBlending, SubtractiveBlending, AdditiveBlending, ReversedDepthFuncs, MinEquation, MaxEquation, MirroredRepeatWrapping, ClampToEdgeWrapping, RepeatWrapping, LinearMipmapNearestFilter, NearestMipmapLinearFilter, NearestMipmapNearestFilter, NotEqualCompare, GreaterCompare, EqualCompare, LessCompare, AlwaysCompare, NeverCompare, NoColorSpace, DepthStencilFormat, getByteLength, UnsignedInt248Type, UnsignedShortType, createElementNS, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedInt5999Type, UnsignedInt101111Type, ByteType, ShortType, AlphaFormat, RGBFormat, RedFormat, RedIntegerFormat, RGIntegerFormat, RGBAIntegerFormat, RGB_S3TC_DXT1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGB_PVRTC_4BPPV1_Format, RGB_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_PVRTC_2BPPV1_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGBA_ETC2_EAC_Format, R11_EAC_Format, SIGNED_R11_EAC_Format, SIGNED_RG11_EAC_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_10x10_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_BPTC_Format, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RED_RGTC1_Format, SIGNED_RED_RGTC1_Format, SIGNED_RED_GREEN_RGTC2_Format, ExternalTexture, EventDispatcher, ArrayCamera, WebXRController, RAD2DEG, DataTexture, createCanvasElement, SRGBColorSpace, REVISION, log, WebGLCoordinateSystem, probeAsync } from './three.core.js';
-export { AdditiveAnimationBlendMode, AlwaysStencilFunc, AmbientLight, AnimationAction, AnimationClip, AnimationLoader, AnimationMixer, AnimationObjectGroup, AnimationUtils, ArcCurve, ArrowHelper, AttachedBindMode, Audio, AudioAnalyser, AudioContext, AudioListener, AudioLoader, AxesHelper, BasicDepthPacking, BasicShadowMap, BatchedMesh, BezierInterpolant, Bone, BooleanKeyframeTrack, Box2, Box3, Box3Helper, BoxHelper, BufferGeometryLoader, Cache, Camera, CameraHelper, CanvasTexture, CapsuleGeometry, CatmullRomCurve3, CircleGeometry, Clock, ColorKeyframeTrack, Compatibility, CompressedArrayTexture, CompressedCubeTexture, CompressedTexture, CompressedTextureLoader, ConeGeometry, Controls, CubeTextureLoader, CubicBezierCurve, CubicBezierCurve3, CubicInterpolant, CullFaceFrontBack, Curve, CurvePath, CylinderGeometry, Cylindrical, DataTextureLoader, DataUtils, DecrementStencilOp, DecrementWrapStencilOp, DefaultLoadingManager, DetachedBindMode, DirectionalLight, DirectionalLightHelper, DiscreteInterpolant, DodecahedronGeometry, DynamicCopyUsage, DynamicDrawUsage, DynamicReadUsage, EdgesGeometry, EllipseCurve, EqualStencilFunc, Euler, ExtrudeGeometry, FileLoader, Float16BufferAttribute, Fog, FogExp2, FramebufferTexture, FrustumArray, GLBufferAttribute, GLSL1, GreaterEqualStencilFunc, GreaterStencilFunc, GridHelper, Group, HTMLTexture, HemisphereLight, HemisphereLightHelper, IcosahedronGeometry, ImageBitmapLoader, ImageLoader, ImageUtils, IncrementStencilOp, IncrementWrapStencilOp, InstancedBufferAttribute, InstancedBufferGeometry, InstancedInterleavedBuffer, InstancedMesh, Int16BufferAttribute, Int32BufferAttribute, Int8BufferAttribute, InterleavedBuffer, InterleavedBufferAttribute, Interpolant, InterpolateBezier, InterpolateDiscrete, InterpolateLinear, InterpolateSmooth, InterpolationSamplingMode, InterpolationSamplingType, InvertStencilOp, KeepStencilOp, KeyframeTrack, LOD, LatheGeometry, LessEqualStencilFunc, LessStencilFunc, Light, LightProbe, LightShadow, Line, Line3, LineBasicMaterial, LineCurve, LineCurve3, LineDashedMaterial, LineLoop, LineSegments, LinearInterpolant, LinearMipMapLinearFilter, LinearMipMapNearestFilter, Loader, LoaderUtils, LoadingManager, LoopOnce, LoopPingPong, LoopRepeat, MOUSE, Material, MaterialBlending, MaterialLoader, MathUtils, Matrix2, MeshLambertMaterial, MeshMatcapMaterial, MeshNormalMaterial, MeshPhongMaterial, MeshPhysicalMaterial, MeshStandardMaterial, MeshToonMaterial, NearestMipMapLinearFilter, NearestMipMapNearestFilter, NeverStencilFunc, NoNormalPacking, NormalAnimationBlendMode, NormalGAPacking, NormalRGPacking, NotEqualStencilFunc, NumberKeyframeTrack, Object3D, ObjectLoader, OctahedronGeometry, Path, PlaneHelper, PointLight, PointLightHelper, Points, PointsMaterial, PolarGridHelper, PolyhedronGeometry, PositionalAudio, PropertyBinding, PropertyMixer, QuadraticBezierCurve, QuadraticBezierCurve3, Quaternion, QuaternionKeyframeTrack, QuaternionLinearInterpolant, RGBADepthPacking, RGBDepthPacking, RGBIntegerFormat, RGDepthPacking, Ray, Raycaster, RectAreaLight, RenderObjectRefreshType, RenderTarget, RenderTarget3D, ReplaceStencilOp, RingGeometry, Scene, ShadowMaterial, Shape, ShapeGeometry, ShapePath, ShapeUtils, Skeleton, SkeletonHelper, SkinnedMesh, Source, Sphere, SphereGeometry, Spherical, SphericalHarmonics3, SplineCurve, SpotLight, SpotLightHelper, Sprite, SpriteMaterial, StaticCopyUsage, StaticDrawUsage, StaticReadUsage, StereoCamera, StreamCopyUsage, StreamDrawUsage, StreamReadUsage, StringKeyframeTrack, TOUCH, TetrahedronGeometry, TextureLoader, TextureSource, TextureUtils, Timer, TimestampQuery, TorusGeometry, TorusKnotGeometry, Triangle, TriangleFanDrawMode, TriangleStripDrawMode, TrianglesDrawMode, TubeGeometry, UVMapping, Uint8BufferAttribute, Uint8ClampedBufferAttribute, Uniform, UniformsGroup, VectorKeyframeTrack, VideoFrameTexture, VideoTexture, WebGL3DRenderTarget, WebGLArrayRenderTarget, WebGPUCoordinateSystem, WireframeGeometry, WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, ZeroStencilOp, getConsoleFunction, setConsoleFunction } from './three.core.js';
-
-function WebGLAnimation() {
-
- let context = null;
- let isAnimating = false;
- let animationLoop = null;
- let requestId = null;
-
- function onAnimationFrame( time, frame ) {
-
- requestId = context.requestAnimationFrame( onAnimationFrame );
-
- animationLoop( time, frame );
-
- }
-
- return {
-
- start: function () {
-
- if ( isAnimating === true ) return;
- if ( animationLoop === null ) return;
- if ( context === null ) return;
-
- requestId = context.requestAnimationFrame( onAnimationFrame );
-
- isAnimating = true;
-
- },
-
- stop: function () {
-
- if ( context !== null ) context.cancelAnimationFrame( requestId );
-
- isAnimating = false;
-
- },
-
- setAnimationLoop: function ( callback ) {
-
- animationLoop = callback;
-
- },
-
- setContext: function ( value ) {
-
- context = value;
-
- }
-
- };
-
-}
-
-function WebGLAttributes( gl ) {
-
- const buffers = new WeakMap();
-
- function createBuffer( attribute, bufferType ) {
-
- const array = attribute.array;
- const usage = attribute.usage;
- const size = array.byteLength;
-
- const buffer = gl.createBuffer();
-
- gl.bindBuffer( bufferType, buffer );
- gl.bufferData( bufferType, array, usage );
-
- attribute.onUploadCallback();
-
- let type;
-
- if ( array instanceof Float32Array ) {
-
- type = gl.FLOAT;
-
- } else if ( typeof Float16Array !== 'undefined' && array instanceof Float16Array ) {
-
- type = gl.HALF_FLOAT;
-
- } else if ( array instanceof Uint16Array ) {
-
- if ( attribute.isFloat16BufferAttribute ) {
-
- type = gl.HALF_FLOAT;
-
- } else {
-
- type = gl.UNSIGNED_SHORT;
-
- }
-
- } else if ( array instanceof Int16Array ) {
-
- type = gl.SHORT;
-
- } else if ( array instanceof Uint32Array ) {
-
- type = gl.UNSIGNED_INT;
-
- } else if ( array instanceof Int32Array ) {
-
- type = gl.INT;
-
- } else if ( array instanceof Int8Array ) {
-
- type = gl.BYTE;
-
- } else if ( array instanceof Uint8Array ) {
-
- type = gl.UNSIGNED_BYTE;
-
- } else if ( array instanceof Uint8ClampedArray ) {
-
- type = gl.UNSIGNED_BYTE;
-
- } else {
-
- throw new Error( 'THREE.WebGLAttributes: Unsupported buffer data format: ' + array );
-
- }
-
- return {
- buffer: buffer,
- type: type,
- bytesPerElement: array.BYTES_PER_ELEMENT,
- version: attribute.version,
- size: size
- };
-
- }
-
- function updateBuffer( buffer, attribute, bufferType ) {
-
- const array = attribute.array;
- const updateRanges = attribute.updateRanges;
-
- gl.bindBuffer( bufferType, buffer );
-
- if ( updateRanges.length === 0 ) {
-
- // Not using update ranges
- gl.bufferSubData( bufferType, 0, array );
-
- } else {
-
- // Before applying update ranges, we merge any adjacent / overlapping
- // ranges to reduce load on `gl.bufferSubData`. Empirically, this has led
- // to performance improvements for applications which make heavy use of
- // update ranges. Likely due to GPU command overhead.
- //
- // Note that to reduce garbage collection between frames, we merge the
- // update ranges in-place. This is safe because this method will clear the
- // update ranges once updated.
-
- updateRanges.sort( ( a, b ) => a.start - b.start );
-
- // To merge the update ranges in-place, we work from left to right in the
- // existing updateRanges array, merging ranges. This may result in a final
- // array which is smaller than the original. This index tracks the last
- // index representing a merged range, any data after this index can be
- // trimmed once the merge algorithm is completed.
- let mergeIndex = 0;
-
- for ( let i = 1; i < updateRanges.length; i ++ ) {
-
- const previousRange = updateRanges[ mergeIndex ];
- const range = updateRanges[ i ];
-
- // We add one here to merge adjacent ranges. This is safe because ranges
- // operate over positive integers.
- if ( range.start <= previousRange.start + previousRange.count + 1 ) {
-
- previousRange.count = Math.max(
- previousRange.count,
- range.start + range.count - previousRange.start
- );
-
- } else {
-
- ++ mergeIndex;
- updateRanges[ mergeIndex ] = range;
-
- }
-
- }
-
- // Trim the array to only contain the merged ranges.
- updateRanges.length = mergeIndex + 1;
-
- for ( let i = 0, l = updateRanges.length; i < l; i ++ ) {
-
- const range = updateRanges[ i ];
-
- gl.bufferSubData( bufferType, range.start * array.BYTES_PER_ELEMENT,
- array, range.start, range.count );
-
- }
-
- attribute.clearUpdateRanges();
-
- }
-
- attribute.onUploadCallback();
-
- }
-
- //
-
- function get( attribute ) {
-
- if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;
-
- return buffers.get( attribute );
-
- }
-
- function remove( attribute ) {
-
- if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;
-
- const data = buffers.get( attribute );
-
- if ( data ) {
-
- gl.deleteBuffer( data.buffer );
-
- buffers.delete( attribute );
-
- }
-
- }
-
- function update( attribute, bufferType ) {
-
- if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;
-
- if ( attribute.isGLBufferAttribute ) {
-
- const cached = buffers.get( attribute );
-
- if ( ! cached || cached.version < attribute.version ) {
-
- buffers.set( attribute, {
- buffer: attribute.buffer,
- type: attribute.type,
- bytesPerElement: attribute.elementSize,
- version: attribute.version
- } );
-
- }
-
- return;
-
- }
-
- const data = buffers.get( attribute );
-
- if ( data === undefined ) {
-
- buffers.set( attribute, createBuffer( attribute, bufferType ) );
-
- } else if ( data.version < attribute.version ) {
-
- if ( data.size !== attribute.array.byteLength ) {
-
- throw new Error( 'THREE.WebGLAttributes: The size of the buffer attribute\'s array buffer does not match the original size. Resizing buffer attributes is not supported.' );
-
- }
-
- updateBuffer( data.buffer, attribute, bufferType );
-
- data.version = attribute.version;
-
- }
-
- }
-
- return {
-
- get: get,
- remove: remove,
- update: update
-
- };
-
-}
-
-var alphahash_fragment = "#ifdef USE_ALPHAHASH\n\tif ( diffuseColor.a < getAlphaHashThreshold( vPosition ) ) discard;\n#endif";
-
-var alphahash_pars_fragment = "#ifdef USE_ALPHAHASH\n\tconst float ALPHA_HASH_SCALE = 0.05;\n\tfloat hash2D( vec2 value ) {\n\t\treturn fract( 1.0e4 * sin( 17.0 * value.x + 0.1 * value.y ) * ( 0.1 + abs( sin( 13.0 * value.y + value.x ) ) ) );\n\t}\n\tfloat hash3D( vec3 value ) {\n\t\treturn hash2D( vec2( hash2D( value.xy ), value.z ) );\n\t}\n\tfloat getAlphaHashThreshold( vec3 position ) {\n\t\tfloat maxDeriv = max(\n\t\t\tlength( dFdx( position.xyz ) ),\n\t\t\tlength( dFdy( position.xyz ) )\n\t\t);\n\t\tfloat pixScale = 1.0 / ( ALPHA_HASH_SCALE * maxDeriv );\n\t\tvec2 pixScales = vec2(\n\t\t\texp2( floor( log2( pixScale ) ) ),\n\t\t\texp2( ceil( log2( pixScale ) ) )\n\t\t);\n\t\tvec2 alpha = vec2(\n\t\t\thash3D( floor( pixScales.x * position.xyz ) ),\n\t\t\thash3D( floor( pixScales.y * position.xyz ) )\n\t\t);\n\t\tfloat lerpFactor = fract( log2( pixScale ) );\n\t\tfloat x = ( 1.0 - lerpFactor ) * alpha.x + lerpFactor * alpha.y;\n\t\tfloat a = min( lerpFactor, 1.0 - lerpFactor );\n\t\tvec3 cases = vec3(\n\t\t\tx * x / ( 2.0 * a * ( 1.0 - a ) ),\n\t\t\t( x - 0.5 * a ) / ( 1.0 - a ),\n\t\t\t1.0 - ( ( 1.0 - x ) * ( 1.0 - x ) / ( 2.0 * a * ( 1.0 - a ) ) )\n\t\t);\n\t\tfloat threshold = ( x < ( 1.0 - a ) )\n\t\t\t? ( ( x < a ) ? cases.x : cases.y )\n\t\t\t: cases.z;\n\t\treturn clamp( threshold , 1.0e-6, 1.0 );\n\t}\n#endif";
-
-var alphamap_fragment = "#ifdef USE_ALPHAMAP\n\tdiffuseColor.a *= texture2D( alphaMap, vAlphaMapUv ).g;\n#endif";
-
-var alphamap_pars_fragment = "#ifdef USE_ALPHAMAP\n\tuniform sampler2D alphaMap;\n#endif";
-
-var alphatest_fragment = "#ifdef USE_ALPHATEST\n\t#ifdef ALPHA_TO_COVERAGE\n\tdiffuseColor.a = smoothstep( alphaTest, alphaTest + fwidth( diffuseColor.a ), diffuseColor.a );\n\tif ( diffuseColor.a == 0.0 ) discard;\n\t#else\n\tif ( diffuseColor.a < alphaTest ) discard;\n\t#endif\n#endif";
-
-var alphatest_pars_fragment = "#ifdef USE_ALPHATEST\n\tuniform float alphaTest;\n#endif";
-
-var aomap_fragment = "#ifdef USE_AOMAP\n\tfloat ambientOcclusion = ( texture2D( aoMap, vAoMapUv ).r - 1.0 ) * aoMapIntensity + 1.0;\n\treflectedLight.indirectDiffuse *= ambientOcclusion;\n\t#if defined( USE_CLEARCOAT ) \n\t\tclearcoatSpecularIndirect *= ambientOcclusion;\n\t#endif\n\t#if defined( USE_SHEEN ) \n\t\tsheenSpecularIndirect *= ambientOcclusion;\n\t#endif\n\t#if defined( USE_ENVMAP ) && defined( STANDARD )\n\t\tfloat dotNV = saturate( dot( geometryNormal, geometryViewDir ) );\n\t\treflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );\n\t#endif\n#endif";
-
-var aomap_pars_fragment = "#ifdef USE_AOMAP\n\tuniform sampler2D aoMap;\n\tuniform float aoMapIntensity;\n#endif";
-
-var batching_pars_vertex = "#ifdef USE_BATCHING\n\t#if ! defined( GL_ANGLE_multi_draw )\n\t#define gl_DrawID _gl_DrawID\n\tuniform int _gl_DrawID;\n\t#endif\n\tuniform highp sampler2D batchingTexture;\n\tuniform highp usampler2D batchingIdTexture;\n\tmat4 getBatchingMatrix( const in float i ) {\n\t\tint size = textureSize( batchingTexture, 0 ).x;\n\t\tint j = int( i ) * 4;\n\t\tint x = j % size;\n\t\tint y = j / size;\n\t\tvec4 v1 = texelFetch( batchingTexture, ivec2( x, y ), 0 );\n\t\tvec4 v2 = texelFetch( batchingTexture, ivec2( x + 1, y ), 0 );\n\t\tvec4 v3 = texelFetch( batchingTexture, ivec2( x + 2, y ), 0 );\n\t\tvec4 v4 = texelFetch( batchingTexture, ivec2( x + 3, y ), 0 );\n\t\treturn mat4( v1, v2, v3, v4 );\n\t}\n\tfloat getIndirectIndex( const in int i ) {\n\t\tint size = textureSize( batchingIdTexture, 0 ).x;\n\t\tint x = i % size;\n\t\tint y = i / size;\n\t\treturn float( texelFetch( batchingIdTexture, ivec2( x, y ), 0 ).r );\n\t}\n#endif\n#ifdef USE_BATCHING_COLOR\n\tuniform sampler2D batchingColorTexture;\n\tvec4 getBatchingColor( const in float i ) {\n\t\tint size = textureSize( batchingColorTexture, 0 ).x;\n\t\tint j = int( i );\n\t\tint x = j % size;\n\t\tint y = j / size;\n\t\treturn texelFetch( batchingColorTexture, ivec2( x, y ), 0 );\n\t}\n#endif";
-
-var batching_vertex = "#ifdef USE_BATCHING\n\tmat4 batchingMatrix = getBatchingMatrix( getIndirectIndex( gl_DrawID ) );\n#endif";
-
-var begin_vertex = "vec3 transformed = vec3( position );\n#ifdef USE_ALPHAHASH\n\tvPosition = vec3( position );\n#endif";
-
-var beginnormal_vertex = "vec3 objectNormal = vec3( normal );\n#ifdef USE_TANGENT\n\tvec3 objectTangent = vec3( tangent.xyz );\n#endif";
-
-var bsdfs = "float G_BlinnPhong_Implicit( ) {\n\treturn 0.25;\n}\nfloat D_BlinnPhong( const in float shininess, const in float dotNH ) {\n\treturn RECIPROCAL_PI * ( shininess * 0.5 + 1.0 ) * pow( dotNH, shininess );\n}\nvec3 BRDF_BlinnPhong( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in vec3 specularColor, const in float shininess ) {\n\tvec3 halfDir = normalize( lightDir + viewDir );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\n\tvec3 F = F_Schlick( specularColor, 1.0, dotVH );\n\tfloat G = G_BlinnPhong_Implicit( );\n\tfloat D = D_BlinnPhong( shininess, dotNH );\n\treturn F * ( G * D );\n} // validated";
-
-var iridescence_fragment = "#ifdef USE_IRIDESCENCE\n\tconst mat3 XYZ_TO_REC709 = mat3(\n\t\t 3.2404542, -0.9692660, 0.0556434,\n\t\t-1.5371385, 1.8760108, -0.2040259,\n\t\t-0.4985314, 0.0415560, 1.0572252\n\t);\n\tvec3 Fresnel0ToIor( vec3 fresnel0 ) {\n\t\tvec3 sqrtF0 = sqrt( fresnel0 );\n\t\treturn ( vec3( 1.0 ) + sqrtF0 ) / ( vec3( 1.0 ) - sqrtF0 );\n\t}\n\tvec3 IorToFresnel0( vec3 transmittedIor, float incidentIor ) {\n\t\treturn pow2( ( transmittedIor - vec3( incidentIor ) ) / ( transmittedIor + vec3( incidentIor ) ) );\n\t}\n\tfloat IorToFresnel0( float transmittedIor, float incidentIor ) {\n\t\treturn pow2( ( transmittedIor - incidentIor ) / ( transmittedIor + incidentIor ));\n\t}\n\tvec3 evalSensitivity( float OPD, vec3 shift ) {\n\t\tfloat phase = 2.0 * PI * OPD * 1.0e-9;\n\t\tvec3 val = vec3( 5.4856e-13, 4.4201e-13, 5.2481e-13 );\n\t\tvec3 pos = vec3( 1.6810e+06, 1.7953e+06, 2.2084e+06 );\n\t\tvec3 var = vec3( 4.3278e+09, 9.3046e+09, 6.6121e+09 );\n\t\tvec3 xyz = val * sqrt( 2.0 * PI * var ) * cos( pos * phase + shift ) * exp( - pow2( phase ) * var );\n\t\txyz.x += 9.7470e-14 * sqrt( 2.0 * PI * 4.5282e+09 ) * cos( 2.2399e+06 * phase + shift[ 0 ] ) * exp( - 4.5282e+09 * pow2( phase ) );\n\t\txyz /= 1.0685e-7;\n\t\tvec3 rgb = XYZ_TO_REC709 * xyz;\n\t\treturn rgb;\n\t}\n\tvec3 evalIridescence( float outsideIOR, float eta2, float cosTheta1, float thinFilmThickness, vec3 baseF0 ) {\n\t\tvec3 I;\n\t\tfloat iridescenceIOR = mix( outsideIOR, eta2, smoothstep( 0.0, 0.03, thinFilmThickness ) );\n\t\tfloat sinTheta2Sq = pow2( outsideIOR / iridescenceIOR ) * ( 1.0 - pow2( cosTheta1 ) );\n\t\tfloat cosTheta2Sq = 1.0 - sinTheta2Sq;\n\t\tif ( cosTheta2Sq < 0.0 ) {\n\t\t\treturn vec3( 1.0 );\n\t\t}\n\t\tfloat cosTheta2 = sqrt( cosTheta2Sq );\n\t\tfloat R0 = IorToFresnel0( iridescenceIOR, outsideIOR );\n\t\tfloat R12 = F_Schlick( R0, 1.0, cosTheta1 );\n\t\tfloat T121 = 1.0 - R12;\n\t\tfloat phi12 = 0.0;\n\t\tif ( iridescenceIOR < outsideIOR ) phi12 = PI;\n\t\tfloat phi21 = PI - phi12;\n\t\tvec3 baseIOR = Fresnel0ToIor( clamp( baseF0, 0.0, 0.9999 ) );\t\tvec3 R1 = IorToFresnel0( baseIOR, iridescenceIOR );\n\t\tvec3 R23 = F_Schlick( R1, 1.0, cosTheta2 );\n\t\tvec3 phi23 = vec3( 0.0 );\n\t\tif ( baseIOR[ 0 ] < iridescenceIOR ) phi23[ 0 ] = PI;\n\t\tif ( baseIOR[ 1 ] < iridescenceIOR ) phi23[ 1 ] = PI;\n\t\tif ( baseIOR[ 2 ] < iridescenceIOR ) phi23[ 2 ] = PI;\n\t\tfloat OPD = 2.0 * iridescenceIOR * thinFilmThickness * cosTheta2;\n\t\tvec3 phi = vec3( phi21 ) + phi23;\n\t\tvec3 R123 = clamp( R12 * R23, 1e-5, 0.9999 );\n\t\tvec3 r123 = sqrt( R123 );\n\t\tvec3 Rs = pow2( T121 ) * R23 / ( vec3( 1.0 ) - R123 );\n\t\tvec3 C0 = R12 + Rs;\n\t\tI = C0;\n\t\tvec3 Cm = Rs - T121;\n\t\tfor ( int m = 1; m <= 2; ++ m ) {\n\t\t\tCm *= r123;\n\t\t\tvec3 Sm = 2.0 * evalSensitivity( float( m ) * OPD, float( m ) * phi );\n\t\t\tI += Cm * Sm;\n\t\t}\n\t\treturn max( I, vec3( 0.0 ) );\n\t}\n#endif";
-
-var bumpmap_pars_fragment = "#ifdef USE_BUMPMAP\n\tuniform sampler2D bumpMap;\n\tuniform float bumpScale;\n\tvec2 dHdxy_fwd() {\n\t\tvec2 dSTdx = dFdx( vBumpMapUv );\n\t\tvec2 dSTdy = dFdy( vBumpMapUv );\n\t\tfloat Hll = bumpScale * texture2D( bumpMap, vBumpMapUv ).x;\n\t\tfloat dBx = bumpScale * texture2D( bumpMap, vBumpMapUv + dSTdx ).x - Hll;\n\t\tfloat dBy = bumpScale * texture2D( bumpMap, vBumpMapUv + dSTdy ).x - Hll;\n\t\treturn vec2( dBx, dBy );\n\t}\n\tvec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy, float faceDirection ) {\n\t\tvec3 vSigmaX = normalize( dFdx( surf_pos.xyz ) );\n\t\tvec3 vSigmaY = normalize( dFdy( surf_pos.xyz ) );\n\t\tvec3 vN = surf_norm;\n\t\tvec3 R1 = cross( vSigmaY, vN );\n\t\tvec3 R2 = cross( vN, vSigmaX );\n\t\tfloat fDet = dot( vSigmaX, R1 ) * faceDirection;\n\t\tvec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );\n\t\treturn normalize( abs( fDet ) * surf_norm - vGrad );\n\t}\n#endif";
-
-var clipping_planes_fragment = "#if NUM_CLIPPING_PLANES > 0\n\tvec4 plane;\n\t#ifdef ALPHA_TO_COVERAGE\n\t\tfloat distanceToPlane, distanceGradient;\n\t\tfloat clipOpacity = 1.0;\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tdistanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w;\n\t\t\tdistanceGradient = fwidth( distanceToPlane ) / 2.0;\n\t\t\tclipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane );\n\t\t\tif ( clipOpacity == 0.0 ) discard;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\t\tfloat unionClipOpacity = 1.0;\n\t\t\t#pragma unroll_loop_start\n\t\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\t\tplane = clippingPlanes[ i ];\n\t\t\t\tdistanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w;\n\t\t\t\tdistanceGradient = fwidth( distanceToPlane ) / 2.0;\n\t\t\t\tunionClipOpacity *= 1.0 - smoothstep( - distanceGradient, distanceGradient, distanceToPlane );\n\t\t\t}\n\t\t\t#pragma unroll_loop_end\n\t\t\tclipOpacity *= 1.0 - unionClipOpacity;\n\t\t#endif\n\t\tdiffuseColor.a *= clipOpacity;\n\t\tif ( diffuseColor.a == 0.0 ) discard;\n\t#else\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tif ( dot( vClipPosition, plane.xyz ) > plane.w ) discard;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\t\tbool clipped = true;\n\t\t\t#pragma unroll_loop_start\n\t\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\t\tplane = clippingPlanes[ i ];\n\t\t\t\tclipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped;\n\t\t\t}\n\t\t\t#pragma unroll_loop_end\n\t\t\tif ( clipped ) discard;\n\t\t#endif\n\t#endif\n#endif";
-
-var clipping_planes_pars_fragment = "#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n\tuniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];\n#endif";
-
-var clipping_planes_pars_vertex = "#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n#endif";
-
-var clipping_planes_vertex = "#if NUM_CLIPPING_PLANES > 0\n\tvClipPosition = - mvPosition.xyz;\n#endif";
-
-var color_fragment = "#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA )\n\tdiffuseColor *= vColor;\n#endif";
-
-var color_pars_fragment = "#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#endif";
-
-var color_pars_vertex = "#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )\n\tvarying vec4 vColor;\n#endif";
-
-var color_vertex = "#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )\n\tvColor = vec4( 1.0 );\n#endif\n#ifdef USE_COLOR_ALPHA\n\tvColor *= color;\n#elif defined( USE_COLOR )\n\tvColor.rgb *= color;\n#endif\n#ifdef USE_INSTANCING_COLOR\n\tvColor.rgb *= instanceColor.rgb;\n#endif\n#ifdef USE_BATCHING_COLOR\n\tvColor *= getBatchingColor( getIndirectIndex( gl_DrawID ) );\n#endif";
-
-var common = "#define PI 3.141592653589793\n#define PI2 6.283185307179586\n#define PI_HALF 1.5707963267948966\n#define RECIPROCAL_PI 0.3183098861837907\n#define RECIPROCAL_PI2 0.15915494309189535\n#define EPSILON 1e-6\n#ifndef saturate\n#define saturate( a ) clamp( a, 0.0, 1.0 )\n#endif\n#define whiteComplement( a ) ( 1.0 - saturate( a ) )\nfloat pow2( const in float x ) { return x*x; }\nvec3 pow2( const in vec3 x ) { return x*x; }\nfloat pow3( const in float x ) { return x*x*x; }\nfloat pow4( const in float x ) { float x2 = x*x; return x2*x2; }\nfloat max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); }\nfloat average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); }\nhighp float rand( const in vec2 uv ) {\n\tconst highp float a = 12.9898, b = 78.233, c = 43758.5453;\n\thighp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );\n\treturn fract( sin( sn ) * c );\n}\n#ifdef HIGH_PRECISION\n\tfloat precisionSafeLength( vec3 v ) { return length( v ); }\n#else\n\tfloat precisionSafeLength( vec3 v ) {\n\t\tfloat maxComponent = max3( abs( v ) );\n\t\treturn length( v / maxComponent ) * maxComponent;\n\t}\n#endif\nstruct IncidentLight {\n\tvec3 color;\n\tvec3 direction;\n\tbool visible;\n};\nstruct ReflectedLight {\n\tvec3 directDiffuse;\n\tvec3 directSpecular;\n\tvec3 indirectDiffuse;\n\tvec3 indirectSpecular;\n};\n#ifdef USE_ALPHAHASH\n\tvarying vec3 vPosition;\n#endif\nvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n}\n#define inverseTransformDirection transformDirectionByInverseViewMatrix\nvec3 transformNormalByInverseViewMatrix( in vec3 normal, in mat4 viewMatrix ) {\n\treturn normalize( ( vec4( normal, 0.0 ) * viewMatrix ).xyz );\n}\nvec3 transformDirectionByInverseViewMatrix( in vec3 dir, in mat4 viewMatrix ) {\n\treturn normalize( ( vec4( dir, 0.0 ) * viewMatrix ).xyz );\n}\nbool isPerspectiveMatrix( mat4 m ) {\n\treturn m[ 2 ][ 3 ] == - 1.0;\n}\nvec2 equirectUv( in vec3 dir ) {\n\tfloat u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5;\n\tfloat v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\n\treturn vec2( u, v );\n}\nvec3 BRDF_Lambert( const in vec3 diffuseColor ) {\n\treturn RECIPROCAL_PI * diffuseColor;\n}\nvec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) {\n\tfloat fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );\n\treturn f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );\n}\nfloat F_Schlick( const in float f0, const in float f90, const in float dotVH ) {\n\tfloat fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );\n\treturn f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );\n} // validated";
-
-var cube_uv_reflection_fragment = "#ifdef ENVMAP_TYPE_CUBE_UV\n\t#define cubeUV_minMipLevel 4.0\n\t#define cubeUV_minTileSize 16.0\n\tfloat getFace( vec3 direction ) {\n\t\tvec3 absDirection = abs( direction );\n\t\tfloat face = - 1.0;\n\t\tif ( absDirection.x > absDirection.z ) {\n\t\t\tif ( absDirection.x > absDirection.y )\n\t\t\t\tface = direction.x > 0.0 ? 0.0 : 3.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t} else {\n\t\t\tif ( absDirection.z > absDirection.y )\n\t\t\t\tface = direction.z > 0.0 ? 2.0 : 5.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t}\n\t\treturn face;\n\t}\n\tvec2 getUV( vec3 direction, float face ) {\n\t\tvec2 uv;\n\t\tif ( face == 0.0 ) {\n\t\t\tuv = vec2( direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 1.0 ) {\n\t\t\tuv = vec2( - direction.x, - direction.z ) / abs( direction.y );\n\t\t} else if ( face == 2.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.y ) / abs( direction.z );\n\t\t} else if ( face == 3.0 ) {\n\t\t\tuv = vec2( - direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 4.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.z ) / abs( direction.y );\n\t\t} else {\n\t\t\tuv = vec2( direction.x, direction.y ) / abs( direction.z );\n\t\t}\n\t\treturn 0.5 * ( uv + 1.0 );\n\t}\n\tvec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) {\n\t\tfloat face = getFace( direction );\n\t\tfloat filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 );\n\t\tmipInt = max( mipInt, cubeUV_minMipLevel );\n\t\tfloat faceSize = exp2( mipInt );\n\t\thighp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0;\n\t\tif ( face > 2.0 ) {\n\t\t\tuv.y += faceSize;\n\t\t\tface -= 3.0;\n\t\t}\n\t\tuv.x += face * faceSize;\n\t\tuv.x += filterInt * 3.0 * cubeUV_minTileSize;\n\t\tuv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize );\n\t\tuv.x *= CUBEUV_TEXEL_WIDTH;\n\t\tuv.y *= CUBEUV_TEXEL_HEIGHT;\n\t\t#ifdef texture2DGradEXT\n\t\t\treturn texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb;\n\t\t#else\n\t\t\treturn texture2D( envMap, uv ).rgb;\n\t\t#endif\n\t}\n\t#define cubeUV_r0 1.0\n\t#define cubeUV_m0 - 2.0\n\t#define cubeUV_r1 0.8\n\t#define cubeUV_m1 - 1.0\n\t#define cubeUV_r4 0.4\n\t#define cubeUV_m4 2.0\n\t#define cubeUV_r5 0.305\n\t#define cubeUV_m5 3.0\n\t#define cubeUV_r6 0.21\n\t#define cubeUV_m6 4.0\n\tfloat roughnessToMip( float roughness ) {\n\t\tfloat mip = 0.0;\n\t\tif ( roughness >= cubeUV_r1 ) {\n\t\t\tmip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0;\n\t\t} else if ( roughness >= cubeUV_r4 ) {\n\t\t\tmip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1;\n\t\t} else if ( roughness >= cubeUV_r5 ) {\n\t\t\tmip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4;\n\t\t} else if ( roughness >= cubeUV_r6 ) {\n\t\t\tmip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5;\n\t\t} else {\n\t\t\tmip = - 2.0 * log2( 1.16 * roughness );\t\t}\n\t\treturn mip;\n\t}\n\tvec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) {\n\t\tfloat mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP );\n\t\tfloat mipF = fract( mip );\n\t\tfloat mipInt = floor( mip );\n\t\tvec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt );\n\t\tif ( mipF == 0.0 ) {\n\t\t\treturn vec4( color0, 1.0 );\n\t\t} else {\n\t\t\tvec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 );\n\t\t\treturn vec4( mix( color0, color1, mipF ), 1.0 );\n\t\t}\n\t}\n#endif";
-
-var defaultnormal_vertex = "vec3 transformedNormal = objectNormal;\n#ifdef USE_TANGENT\n\tvec3 transformedTangent = objectTangent;\n#endif\n#ifdef USE_BATCHING\n\tmat3 bm = mat3( batchingMatrix );\n\ttransformedNormal /= vec3( dot( bm[ 0 ], bm[ 0 ] ), dot( bm[ 1 ], bm[ 1 ] ), dot( bm[ 2 ], bm[ 2 ] ) );\n\ttransformedNormal = bm * transformedNormal;\n\t#ifdef USE_TANGENT\n\t\ttransformedTangent = bm * transformedTangent;\n\t#endif\n#endif\n#ifdef USE_INSTANCING\n\tmat3 im = mat3( instanceMatrix );\n\ttransformedNormal /= vec3( dot( im[ 0 ], im[ 0 ] ), dot( im[ 1 ], im[ 1 ] ), dot( im[ 2 ], im[ 2 ] ) );\n\ttransformedNormal = im * transformedNormal;\n\t#ifdef USE_TANGENT\n\t\ttransformedTangent = im * transformedTangent;\n\t#endif\n#endif\ntransformedNormal = normalMatrix * transformedNormal;\n#ifdef FLIP_SIDED\n\ttransformedNormal = - transformedNormal;\n#endif\n#ifdef USE_TANGENT\n\ttransformedTangent = ( modelViewMatrix * vec4( transformedTangent, 0.0 ) ).xyz;\n#endif";
-
-var displacementmap_pars_vertex = "#ifdef USE_DISPLACEMENTMAP\n\tuniform sampler2D displacementMap;\n\tuniform float displacementScale;\n\tuniform float displacementBias;\n#endif";
-
-var displacementmap_vertex = "#ifdef USE_DISPLACEMENTMAP\n\ttransformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias );\n#endif";
-
-var emissivemap_fragment = "#ifdef USE_EMISSIVEMAP\n\tvec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv );\n\t#ifdef DECODE_VIDEO_TEXTURE_EMISSIVE\n\t\temissiveColor = sRGBTransferEOTF( emissiveColor );\n\t#endif\n\ttotalEmissiveRadiance *= emissiveColor.rgb;\n#endif";
-
-var emissivemap_pars_fragment = "#ifdef USE_EMISSIVEMAP\n\tuniform sampler2D emissiveMap;\n#endif";
-
-var colorspace_fragment = "gl_FragColor = linearToOutputTexel( gl_FragColor );";
-
-var colorspace_pars_fragment = "vec4 LinearTransferOETF( in vec4 value ) {\n\treturn value;\n}\nvec4 sRGBTransferEOTF( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a );\n}\nvec4 sRGBTransferOETF( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );\n}";
-
-var envmap_fragment = "#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvec3 cameraToFrag;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToFrag = normalize( vWorldPosition - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = transformNormalByInverseViewMatrix( normal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( cameraToFrag, worldNormal );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio );\n\t\t#endif\n\t#else\n\t\tvec3 reflectVec = vReflect;\n\t#endif\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 envColor = textureCube( envMap, envMapRotation * reflectVec );\n\t\t#ifdef ENVMAP_BLENDING_MULTIPLY\n\t\t\toutgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\n\t\t#elif defined( ENVMAP_BLENDING_MIX )\n\t\t\toutgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\n\t\t#elif defined( ENVMAP_BLENDING_ADD )\n\t\t\toutgoingLight += envColor.xyz * specularStrength * reflectivity;\n\t\t#endif\n\t#endif\n#endif";
-
-var envmap_common_pars_fragment = "#ifdef USE_ENVMAP\n\tuniform float envMapIntensity;\n\tuniform mat3 envMapRotation;\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tuniform samplerCube envMap;\n\t#else\n\t\tuniform sampler2D envMap;\n\t#endif\n#endif";
-
-var envmap_pars_fragment = "#ifdef USE_ENVMAP\n\tuniform float reflectivity;\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\tvarying vec3 vWorldPosition;\n\t\tuniform float refractionRatio;\n\t#else\n\t\tvarying vec3 vReflect;\n\t#endif\n#endif";
-
-var envmap_pars_vertex = "#ifdef USE_ENVMAP\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\t\n\t\tvarying vec3 vWorldPosition;\n\t#else\n\t\tvarying vec3 vReflect;\n\t\tuniform float refractionRatio;\n\t#endif\n#endif";
-
-var envmap_vertex = "#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvWorldPosition = worldPosition.xyz;\n\t#else\n\t\tvec3 cameraToVertex;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToVertex = normalize( worldPosition.xyz - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = transformNormalByInverseViewMatrix( transformedNormal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvReflect = reflect( cameraToVertex, worldNormal );\n\t\t#else\n\t\t\tvReflect = refract( cameraToVertex, worldNormal, refractionRatio );\n\t\t#endif\n\t#endif\n#endif";
-
-var fog_vertex = "#ifdef USE_FOG\n\tvFogDepth = - mvPosition.z;\n#endif";
-
-var fog_pars_vertex = "#ifdef USE_FOG\n\tvarying float vFogDepth;\n#endif";
-
-var fog_fragment = "#ifdef USE_FOG\n\t#ifdef FOG_EXP2\n\t\tfloat fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth );\n\t#else\n\t\tfloat fogFactor = smoothstep( fogNear, fogFar, vFogDepth );\n\t#endif\n\tgl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\n#endif";
-
-var fog_pars_fragment = "#ifdef USE_FOG\n\tuniform vec3 fogColor;\n\tvarying float vFogDepth;\n\t#ifdef FOG_EXP2\n\t\tuniform float fogDensity;\n\t#else\n\t\tuniform float fogNear;\n\t\tuniform float fogFar;\n\t#endif\n#endif";
-
-var gradientmap_pars_fragment = "#ifdef USE_GRADIENTMAP\n\tuniform sampler2D gradientMap;\n#endif\nvec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) {\n\tfloat dotNL = dot( normal, lightDirection );\n\tvec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 );\n\t#ifdef USE_GRADIENTMAP\n\t\treturn vec3( texture2D( gradientMap, coord ).r );\n\t#else\n\t\tvec2 fw = fwidth( coord ) * 0.5;\n\t\treturn mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) );\n\t#endif\n}";
-
-var lightmap_pars_fragment = "#ifdef USE_LIGHTMAP\n\tuniform sampler2D lightMap;\n\tuniform float lightMapIntensity;\n#endif";
-
-var lights_lambert_fragment = "LambertMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularStrength = specularStrength;";
-
-var lights_lambert_pars_fragment = "varying vec3 vViewPosition;\nstruct LambertMaterial {\n\tvec3 diffuseColor;\n\tfloat specularStrength;\n};\nvoid RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Lambert\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Lambert";
-
-var lights_pars_begin = "uniform bool receiveShadow;\nuniform vec3 ambientLightColor;\n#if defined( USE_LIGHT_PROBES )\n\tuniform vec3 lightProbe[ 9 ];\n#endif\nvec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {\n\tfloat x = normal.x, y = normal.y, z = normal.z;\n\tvec3 result = shCoefficients[ 0 ] * 0.886227;\n\tresult += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;\n\tresult += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;\n\tresult += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;\n\tresult += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;\n\tresult += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;\n\tresult += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );\n\tresult += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;\n\tresult += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );\n\treturn result;\n}\nvec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) {\n\tvec3 worldNormal = transformNormalByInverseViewMatrix( normal, viewMatrix );\n\tvec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe );\n\treturn irradiance;\n}\nvec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {\n\tvec3 irradiance = ambientLightColor;\n\treturn irradiance;\n}\nfloat getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {\n\tfloat distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 );\n\tif ( cutoffDistance > 0.0 ) {\n\t\tdistanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );\n\t}\n\treturn distanceFalloff;\n}\nfloat getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) {\n\treturn smoothstep( coneCosine, penumbraCosine, angleCosine );\n}\n#if NUM_SUN_LIGHTS > 0\n\tstruct SunLight {\n\t\tvec3 direction;\n\t\tvec3 color;\n\t};\n\tuniform SunLight sunLights[ NUM_SUN_LIGHTS ];\n\tvoid getSunLightInfo( const in SunLight sunLight, out IncidentLight light ) {\n\t\tlight.color = sunLight.color;\n\t\tlight.direction = sunLight.direction;\n\t\tlight.visible = true;\n\t}\n#endif\n#if NUM_DIR_LIGHTS > 0\n\tstruct DirectionalLight {\n\t\tvec3 direction;\n\t\tvec3 color;\n\t};\n\tuniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];\n\tvoid getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) {\n\t\tlight.color = directionalLight.color;\n\t\tlight.direction = directionalLight.direction;\n\t\tlight.visible = true;\n\t}\n#endif\n#if NUM_POINT_LIGHTS > 0\n\tstruct PointLight {\n\t\tvec3 position;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t};\n\tuniform PointLight pointLights[ NUM_POINT_LIGHTS ];\n\tvoid getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) {\n\t\tvec3 lVector = pointLight.position - geometryPosition;\n\t\tlight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tlight.color = pointLight.color;\n\t\tlight.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay );\n\t\tlight.visible = ( light.color != vec3( 0.0 ) );\n\t}\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\tstruct SpotLight {\n\t\tvec3 position;\n\t\tvec3 direction;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t\tfloat coneCos;\n\t\tfloat penumbraCos;\n\t};\n\tuniform SpotLight spotLights[ NUM_SPOT_LIGHTS ];\n\tvoid getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) {\n\t\tvec3 lVector = spotLight.position - geometryPosition;\n\t\tlight.direction = normalize( lVector );\n\t\tfloat angleCos = dot( light.direction, spotLight.direction );\n\t\tfloat spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos );\n\t\tif ( spotAttenuation > 0.0 ) {\n\t\t\tfloat lightDistance = length( lVector );\n\t\t\tlight.color = spotLight.color * spotAttenuation;\n\t\t\tlight.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay );\n\t\t\tlight.visible = ( light.color != vec3( 0.0 ) );\n\t\t} else {\n\t\t\tlight.color = vec3( 0.0 );\n\t\t\tlight.visible = false;\n\t\t}\n\t}\n#endif\n#if NUM_RECT_AREA_LIGHTS > 0\n\tstruct RectAreaLight {\n\t\tvec3 color;\n\t\tvec3 position;\n\t\tvec3 halfWidth;\n\t\tvec3 halfHeight;\n\t};\n\tuniform sampler2D ltc_1;\tuniform sampler2D ltc_2;\n\tuniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ];\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\tstruct HemisphereLight {\n\t\tvec3 direction;\n\t\tvec3 skyColor;\n\t\tvec3 groundColor;\n\t};\n\tuniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ];\n\tvec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) {\n\t\tfloat dotNL = dot( normal, hemiLight.direction );\n\t\tfloat hemiDiffuseWeight = 0.5 * dotNL + 0.5;\n\t\tvec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );\n\t\treturn irradiance;\n\t}\n#endif\n#include