three-vr-player 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Player.d.ts +4 -0
- package/dist/VideoSource-DHj5tBsK.js +768 -0
- package/dist/VideoSource-DHj5tBsK.js.map +1 -0
- package/dist/core/StereoScene.d.ts +58 -0
- package/dist/core/VRControls.d.ts +36 -1
- package/dist/core/projections.d.ts +3 -1
- package/dist/core/vr-panel-layout.d.ts +5 -1
- package/dist/core.js +1 -1
- package/dist/three-vr-player.js +48 -40
- package/dist/three-vr-player.js.map +1 -1
- package/dist/three-vr-player.standalone.js +175 -156
- package/dist/three-vr-player.standalone.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/package.json +3 -2
- package/dist/VideoSource-Cx7XPpvZ.js +0 -528
- package/dist/VideoSource-Cx7XPpvZ.js.map +0 -1
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
import * as a from "three";
|
|
2
|
+
function Q(h) {
|
|
3
|
+
let e = h;
|
|
4
|
+
try {
|
|
5
|
+
e = new URL(h).pathname;
|
|
6
|
+
} catch {
|
|
7
|
+
}
|
|
8
|
+
const t = e.toLowerCase();
|
|
9
|
+
return t.endsWith(".m3u8") ? "hls" : t.endsWith(".mpd") ? "dash" : "progressive";
|
|
10
|
+
}
|
|
11
|
+
const D = {
|
|
12
|
+
progressive: "/proxy/stream",
|
|
13
|
+
hls: "/proxy/hls/manifest.m3u8",
|
|
14
|
+
dash: "/proxy/mpd/manifest.m3u8"
|
|
15
|
+
};
|
|
16
|
+
function Y(h, e) {
|
|
17
|
+
const t = Q(h);
|
|
18
|
+
if (!e || !e.url) return { url: h, format: t };
|
|
19
|
+
const i = e.url.replace(/\/+$/, ""), s = new URLSearchParams();
|
|
20
|
+
s.set("d", h), e.apiPassword && s.set("api_password", e.apiPassword);
|
|
21
|
+
for (const [r, n] of Object.entries(e.headers ?? {}))
|
|
22
|
+
n != null && n !== "" && s.set(`h_${r}`, n);
|
|
23
|
+
return { url: `${i}${D[t]}?${s.toString()}`, format: t };
|
|
24
|
+
}
|
|
25
|
+
const S = {
|
|
26
|
+
"180-sbs": { geom: "sphere180", split: "sbs", stereo: !0 },
|
|
27
|
+
"180-mono": { geom: "sphere180", split: "mono", stereo: !1 },
|
|
28
|
+
"360-mono": { geom: "sphere360", split: "mono", stereo: !1 },
|
|
29
|
+
"360-sbs": { geom: "sphere360", split: "sbs", stereo: !0 },
|
|
30
|
+
"360-tb": { geom: "sphere360", split: "tb", stereo: !0 },
|
|
31
|
+
// DeoVR-style fisheye (~190°): a dome in front of the viewer, one circle per eye.
|
|
32
|
+
"fisheye190-sbs": { geom: "fisheye", split: "sbs", stereo: !0 },
|
|
33
|
+
"fisheye190-mono": { geom: "fisheye", split: "mono", stereo: !1 },
|
|
34
|
+
"flat-2d": { geom: "plane", split: "mono", stereo: !1, flat: !0, aspect: "full" },
|
|
35
|
+
"flat-sbs-full": { geom: "plane", split: "sbs", stereo: !0, flat: !0, aspect: "per-eye" },
|
|
36
|
+
"flat-sbs-half": { geom: "plane", split: "sbs", stereo: !0, flat: !0, aspect: "full" }
|
|
37
|
+
}, G = [
|
|
38
|
+
{ value: "180-sbs", label: "180° SBS (VR180)" },
|
|
39
|
+
{ value: "180-mono", label: "180° Mono" },
|
|
40
|
+
{ value: "360-mono", label: "360° Mono" },
|
|
41
|
+
{ value: "360-sbs", label: "360° SBS" },
|
|
42
|
+
{ value: "360-tb", label: "360° Top-Bottom" },
|
|
43
|
+
{ value: "fisheye190-sbs", label: "Fisheye 190° SBS" },
|
|
44
|
+
{ value: "fisheye190-mono", label: "Fisheye 190° Mono" },
|
|
45
|
+
{ value: "flat-2d", label: "Flat 2D" },
|
|
46
|
+
{ value: "flat-sbs-full", label: "Flat 3D — Full SBS" },
|
|
47
|
+
{ value: "flat-sbs-half", label: "Flat 3D — Half SBS" }
|
|
48
|
+
], V = {
|
|
49
|
+
"180-sbs": "180° SBS",
|
|
50
|
+
"180-mono": "180° Mono",
|
|
51
|
+
"360-mono": "360° Mono",
|
|
52
|
+
"360-sbs": "360° SBS",
|
|
53
|
+
"360-tb": "360° TB",
|
|
54
|
+
"fisheye190-sbs": "Fisheye SBS",
|
|
55
|
+
"fisheye190-mono": "Fisheye Mono",
|
|
56
|
+
"flat-2d": "Flat 2D",
|
|
57
|
+
"flat-sbs-full": "Flat SBS",
|
|
58
|
+
"flat-sbs-half": "Flat SBS½"
|
|
59
|
+
};
|
|
60
|
+
function Z(h) {
|
|
61
|
+
var e;
|
|
62
|
+
return !!((e = S[h]) != null && e.flat);
|
|
63
|
+
}
|
|
64
|
+
function J(h) {
|
|
65
|
+
const e = String(h).toLowerCase(), t = /(^|[^a-z])(tb|ou|top.?bottom|over.?under)([^a-z]|$)/.test(e), i = /(sbs|side.?by.?side)/.test(e);
|
|
66
|
+
return /(fisheye|mkx200|rf52|vrca220)/.test(e) ? e.includes("mono") ? "fisheye190-mono" : "fisheye190-sbs" : e.includes("360") ? t ? "360-tb" : i ? "360-sbs" : "360-mono" : e.includes("180") || e.includes("vr180") ? e.includes("mono") ? "180-mono" : "180-sbs" : i ? e.includes("half") ? "flat-sbs-half" : "flat-sbs-full" : null;
|
|
67
|
+
}
|
|
68
|
+
function k(h) {
|
|
69
|
+
if (!Number.isFinite(h) || h < 0) return "0:00";
|
|
70
|
+
const e = Math.floor(h % 60), t = Math.floor(h / 60 % 60), i = Math.floor(h / 3600), s = String(e).padStart(2, "0");
|
|
71
|
+
return i > 0 ? `${i}:${String(t).padStart(2, "0")}:${s}` : `${t}:${s}`;
|
|
72
|
+
}
|
|
73
|
+
const y = 1024, w = 340;
|
|
74
|
+
function O() {
|
|
75
|
+
const h = y, e = w, t = 48;
|
|
76
|
+
return {
|
|
77
|
+
width: h,
|
|
78
|
+
height: e,
|
|
79
|
+
// Top-row icon buttons (reticle / eye / door-arrow). Compact so they read as icons, not labels.
|
|
80
|
+
recenter: { x: 30, y: 26, w: 64, h: 52 },
|
|
81
|
+
passthrough: { x: 108, y: 26, w: 64, h: 52 },
|
|
82
|
+
// toggle; shown only for alpha (passthrough) content
|
|
83
|
+
exit: { x: h - 94, y: 26, w: 64, h: 52 },
|
|
84
|
+
title: { x: 0, y: 92, w: h, h: 44 },
|
|
85
|
+
play: { x: h / 2 - 44, y: 150, w: 88, h: 88 },
|
|
86
|
+
volIcon: { x: t, y: 178, w: 44, h: 44 },
|
|
87
|
+
volBar: { x: t + 60, y: 192, w: 200, h: 16 },
|
|
88
|
+
// Projection stepper on the right: ◀ [label] ▶
|
|
89
|
+
projPrev: { x: 592, y: 178, w: 46, h: 46 },
|
|
90
|
+
projNext: { x: h - 94, y: 178, w: 46, h: 46 },
|
|
91
|
+
projLabel: { x: 646, y: 178, w: h - 94 - 646, h: 46 },
|
|
92
|
+
seekBar: { x: t, y: 286, w: h - t * 2, h: 14 },
|
|
93
|
+
timeCur: { x: t, y: 262 },
|
|
94
|
+
timeDur: { x: h - t, y: 262 }
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const C = (h) => h < 0 ? 0 : h > 1 ? 1 : h;
|
|
98
|
+
function x(h, e, t, i = 0, s = 0) {
|
|
99
|
+
return e >= h.x - i && e <= h.x + h.w + i && t >= h.y - s && t <= h.y + h.h + s;
|
|
100
|
+
}
|
|
101
|
+
function W(h, e, t = O()) {
|
|
102
|
+
return x(t.recenter, h, e) ? { region: "recenter" } : x(t.passthrough, h, e) ? { region: "passthrough" } : x(t.exit, h, e) ? { region: "exit" } : x(t.projPrev, h, e) ? { region: "projPrev" } : x(t.projNext, h, e) ? { region: "projNext" } : x(t.play, h, e) ? { region: "play" } : x(t.volIcon, h, e) ? { region: "volume" } : x(t.volBar, h, e, 12, 26) ? { region: "volume", value: C((h - t.volBar.x) / t.volBar.w) } : x(t.seekBar, h, e, 12, 26) ? { region: "seek", value: C((h - t.seekBar.x) / t.seekBar.w) } : null;
|
|
103
|
+
}
|
|
104
|
+
const R = "#4f8cff", P = "#e8eaed", L = "#aab2c0", U = [4, 5], A = 8e3, N = 0.05, _ = 0.02, I = 1500, T = 0.15, q = 0.012, z = 0.012, H = 0.08;
|
|
105
|
+
class X {
|
|
106
|
+
// re-lock the panel to the head pose for a few frames after summon
|
|
107
|
+
constructor(e) {
|
|
108
|
+
this.layout = O(), this.raycaster = new a.Raycaster(), this.tmpMatrix = new a.Matrix4(), this.tmpVec = new a.Vector3(), this.tmpQuat = new a.Quaternion(), this.prevQuat = [], this.prevPos = [], this.revealCooldownUntil = 0, this.grabbing = null, this.grabStartQuat = new a.Quaternion(), this.prevGrabQuat = new a.Quaternion(), this.grabQuat = new a.Quaternion(), this.deltaQuat = new a.Quaternion(), this.grabEuler = new a.Euler(0, 0, 0, "YXZ"), this.controllers = [], this.lasers = [], this.connected = [], this.cleanups = [], this.visible = !1, this.hover = null, this.paintKey = "", this.idleAt = 0, this.togglePrev = !1, this.placeFrames = 0, this.renderer = e.renderer, this.scene = e.scene, this.actions = e.actions, this.canvas = document.createElement("canvas"), this.canvas.width = y, this.canvas.height = w, this.ctx = this.canvas.getContext("2d"), this.texture = new a.CanvasTexture(this.canvas), this.texture.colorSpace = a.SRGBColorSpace, this.texture.minFilter = a.LinearFilter;
|
|
109
|
+
const t = 1, i = t * w / y;
|
|
110
|
+
this.panel = new a.Mesh(
|
|
111
|
+
new a.PlaneGeometry(t, i),
|
|
112
|
+
new a.MeshBasicMaterial({ map: this.texture, transparent: !0, depthTest: !1, side: a.DoubleSide })
|
|
113
|
+
), this.panel.renderOrder = 10, this.panel.frustumCulled = !1, this.panel.visible = !1, this.scene.add(this.panel), this.cursor = new a.Mesh(
|
|
114
|
+
new a.SphereGeometry(7e-3, 16, 12),
|
|
115
|
+
new a.MeshBasicMaterial({ color: 16777215, depthTest: !1 })
|
|
116
|
+
), this.cursor.renderOrder = 11, this.cursor.frustumCulled = !1, this.cursor.visible = !1, this.scene.add(this.cursor);
|
|
117
|
+
const s = this.renderer.xr.getSession(), r = s ? Array.from(s.inputSources).filter((n) => n.targetRayMode === "tracked-pointer").length : 0;
|
|
118
|
+
for (let n = 0; n < 2; n++) {
|
|
119
|
+
const l = this.renderer.xr.getController(n), o = this.makeLaser();
|
|
120
|
+
o.visible = !1, l.add(o), this.scene.add(l), this.controllers.push(l), this.lasers.push(o), this.connected.push(n < r);
|
|
121
|
+
const c = () => this.onSelect(n), f = () => this.onGrabStart(n), u = () => this.onGrabEnd(n), g = (d) => {
|
|
122
|
+
this.connected[n] = !(d != null && d.data) || d.data.targetRayMode === "tracked-pointer";
|
|
123
|
+
}, v = () => {
|
|
124
|
+
this.connected[n] = !1;
|
|
125
|
+
}, p = l;
|
|
126
|
+
p.addEventListener("selectstart", c), p.addEventListener("squeezestart", f), p.addEventListener("squeezeend", u), p.addEventListener("connected", g), p.addEventListener("disconnected", v), this.cleanups.push(() => {
|
|
127
|
+
p.removeEventListener("selectstart", c), p.removeEventListener("squeezestart", f), p.removeEventListener("squeezeend", u), p.removeEventListener("connected", g), p.removeEventListener("disconnected", v);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
this.paint(!0);
|
|
131
|
+
}
|
|
132
|
+
makeLaser() {
|
|
133
|
+
const e = new a.BufferGeometry().setFromPoints([new a.Vector3(0, 0, 0), new a.Vector3(0, 0, -1)]), t = new a.LineBasicMaterial({ color: 5213439, transparent: !0, opacity: 0.8 });
|
|
134
|
+
return new a.Line(e, t);
|
|
135
|
+
}
|
|
136
|
+
/** Called once per frame from the render loop (before renderer.render). */
|
|
137
|
+
update(e) {
|
|
138
|
+
if (!this.renderer.xr.getSession()) return;
|
|
139
|
+
if (this.pollToggle(), this.handleThumbstick(), this.handleGrab(), this.maybeRevealOnMotion(e), !this.visible) {
|
|
140
|
+
for (const s of this.lasers) s.visible = !1;
|
|
141
|
+
this.cursor.visible = !1;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (this.placeFrames > 0 && (this.place(), this.placeFrames--), this.idleAt && e > this.idleAt) {
|
|
145
|
+
this.hide();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
let t = null, i = null;
|
|
149
|
+
for (let s = 0; s < this.controllers.length; s++) {
|
|
150
|
+
const r = this.lasers[s], n = this.controllers[s], l = this.tmpVec.setFromMatrixPosition(n.matrixWorld).lengthSq() > 1e-6;
|
|
151
|
+
if (!this.connected[s] || !l) {
|
|
152
|
+
r.visible = !1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const o = this.rayHit(n);
|
|
156
|
+
o.point ? (r.visible = !0, r.scale.z = o.distance, i = o.point, o.hit && (t = o.hit.region)) : r.visible = !1;
|
|
157
|
+
}
|
|
158
|
+
i ? (this.cursor.position.copy(i), this.cursor.visible = !0) : this.cursor.visible = !1, t && (this.idleAt = e + A), this.hover = t, this.paint(!1);
|
|
159
|
+
}
|
|
160
|
+
/** Ray from a controller against the panel: the region under it (if any) and
|
|
161
|
+
* how far the laser reaches. */
|
|
162
|
+
rayHit(e) {
|
|
163
|
+
this.tmpMatrix.identity().extractRotation(e.matrixWorld), this.raycaster.ray.origin.setFromMatrixPosition(e.matrixWorld), this.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(this.tmpMatrix);
|
|
164
|
+
const t = this.raycaster.intersectObject(this.panel, !1);
|
|
165
|
+
if (!t.length) return { hit: null, distance: 5, point: null };
|
|
166
|
+
const i = t[0];
|
|
167
|
+
return { hit: i.uv ? W(i.uv.x * y, (1 - i.uv.y) * w, this.layout) : null, distance: i.distance, point: i.point };
|
|
168
|
+
}
|
|
169
|
+
onSelect(e) {
|
|
170
|
+
if (!this.visible || !this.connected[e]) return;
|
|
171
|
+
const { hit: t } = this.rayHit(this.controllers[e]);
|
|
172
|
+
if (t) {
|
|
173
|
+
switch (this.idleAt = E() + A, t.region) {
|
|
174
|
+
case "play":
|
|
175
|
+
this.actions.togglePlay();
|
|
176
|
+
break;
|
|
177
|
+
case "exit":
|
|
178
|
+
this.actions.exitVR();
|
|
179
|
+
break;
|
|
180
|
+
case "passthrough":
|
|
181
|
+
this.actions.passthroughAvailable() && this.actions.togglePassthrough();
|
|
182
|
+
break;
|
|
183
|
+
case "projPrev":
|
|
184
|
+
this.actions.cycleProjection(-1);
|
|
185
|
+
break;
|
|
186
|
+
case "projNext":
|
|
187
|
+
this.actions.cycleProjection(1);
|
|
188
|
+
break;
|
|
189
|
+
case "recenter":
|
|
190
|
+
this.actions.recenter(), this.placeFrames = 12;
|
|
191
|
+
break;
|
|
192
|
+
case "seek":
|
|
193
|
+
t.value !== void 0 && this.actions.seekFraction(t.value);
|
|
194
|
+
break;
|
|
195
|
+
case "volume":
|
|
196
|
+
t.value !== void 0 ? this.actions.setVolume(t.value) : this.actions.toggleMute();
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
this.paint(!0);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Re-summon the panel when a connected controller is deliberately moved (and
|
|
203
|
+
* we're past the post-hide cooldown). Tracks each controller's pose per frame. */
|
|
204
|
+
maybeRevealOnMotion(e) {
|
|
205
|
+
if (this.grabbing === null)
|
|
206
|
+
for (let t = 0; t < this.controllers.length; t++) {
|
|
207
|
+
const i = this.controllers[t], s = this.tmpVec.setFromMatrixPosition(i.matrixWorld);
|
|
208
|
+
if (!this.connected[t] || s.lengthSq() <= 1e-6) continue;
|
|
209
|
+
const r = this.tmpQuat.setFromRotationMatrix(i.matrixWorld), n = this.prevQuat[t], l = this.prevPos[t];
|
|
210
|
+
n && l && !this.visible && e > this.revealCooldownUntil && (r.angleTo(n) > N || s.distanceTo(l) > _) && this.show(), this.prevQuat[t] || (this.prevQuat[t] = new a.Quaternion()), this.prevPos[t] || (this.prevPos[t] = new a.Vector3()), this.prevQuat[t].copy(r), this.prevPos[t].copy(s);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Right thumbstick, applied every frame it's held: X zooms the content fov (push right =
|
|
214
|
+
* zoom in), Y tilts the content pitch (push up = tilt up). The grip grab-rotate also does
|
|
215
|
+
* pitch/yaw; the two coexist. */
|
|
216
|
+
handleThumbstick() {
|
|
217
|
+
const e = this.renderer.xr.getSession();
|
|
218
|
+
if (!e) return;
|
|
219
|
+
const t = (i) => Math.sign(i) * (Math.abs(i) - T) / (1 - T);
|
|
220
|
+
for (const i of e.inputSources) {
|
|
221
|
+
if (i.handedness !== "right" || !i.gamepad) continue;
|
|
222
|
+
const s = i.gamepad.axes, r = s.length >= 4 ? s[2] ?? 0 : s[0] ?? 0, n = s.length >= 4 ? s[3] ?? 0 : s[1] ?? 0;
|
|
223
|
+
Math.abs(r) > T && this.actions.adjustZoom(t(r) * q), Math.abs(n) > T && this.actions.adjustTilt(-t(n) * z);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
onGrabStart(e) {
|
|
228
|
+
this.connected[e] && (this.grabbing = e, this.controllers[e].getWorldQuaternion(this.grabStartQuat), this.prevGrabQuat.copy(this.grabStartQuat));
|
|
229
|
+
}
|
|
230
|
+
onGrabEnd(e) {
|
|
231
|
+
this.grabbing === e && (this.grabbing = null, this.revealCooldownUntil = E() + I, this.controllers[e].getWorldQuaternion(this.grabQuat).angleTo(this.grabStartQuat) < H && this.toggle());
|
|
232
|
+
}
|
|
233
|
+
/** While the grip is held, apply the controller's frame-to-frame rotation to the
|
|
234
|
+
* content's yaw and pitch — a 1:1 grab-and-turn. Roll is ignored. */
|
|
235
|
+
handleGrab() {
|
|
236
|
+
if (this.grabbing === null) return;
|
|
237
|
+
if (!this.connected[this.grabbing]) {
|
|
238
|
+
this.grabbing = null;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const e = this.controllers[this.grabbing].getWorldQuaternion(this.grabQuat);
|
|
242
|
+
this.deltaQuat.copy(this.prevGrabQuat).invert().premultiply(e), this.grabEuler.setFromQuaternion(this.deltaQuat, "YXZ"), this.actions.adjustYaw(this.grabEuler.y), this.actions.adjustTilt(this.grabEuler.x), this.prevGrabQuat.copy(e);
|
|
243
|
+
}
|
|
244
|
+
pollToggle() {
|
|
245
|
+
const e = this.renderer.xr.getSession();
|
|
246
|
+
let t = !1;
|
|
247
|
+
for (const i of (e == null ? void 0 : e.inputSources) ?? []) {
|
|
248
|
+
const s = i.gamepad;
|
|
249
|
+
s && U.some((r) => {
|
|
250
|
+
var n;
|
|
251
|
+
return (n = s.buttons[r]) == null ? void 0 : n.pressed;
|
|
252
|
+
}) && (t = !0);
|
|
253
|
+
}
|
|
254
|
+
t && !this.togglePrev && this.toggle(), this.togglePrev = t;
|
|
255
|
+
}
|
|
256
|
+
toggle() {
|
|
257
|
+
this.visible ? this.hide() : this.show();
|
|
258
|
+
}
|
|
259
|
+
show() {
|
|
260
|
+
this.place(), this.placeFrames = 12, this.visible = !0, this.panel.visible = !0, this.idleAt = E() + A, this.paint(!0);
|
|
261
|
+
}
|
|
262
|
+
hide() {
|
|
263
|
+
this.visible = !1, this.panel.visible = !1, this.cursor.visible = !1;
|
|
264
|
+
for (const e of this.lasers) e.visible = !1;
|
|
265
|
+
this.revealCooldownUntil = E() + I;
|
|
266
|
+
}
|
|
267
|
+
/** World-lock the panel in front of the current head pose (the "recenter"). Uses
|
|
268
|
+
* yaw only (so head pitch/roll at summon doesn't skew it), drops it into the lower
|
|
269
|
+
* field of view, and tilts its face up toward the viewer. Re-run on every summon. */
|
|
270
|
+
place() {
|
|
271
|
+
const e = this.renderer.xr.getCamera(), t = e.getWorldPosition(new a.Vector3()), i = e.getWorldQuaternion(new a.Quaternion()), s = new a.Euler().setFromQuaternion(i, "YXZ").y, r = new a.Quaternion().setFromEuler(new a.Euler(0, s, 0, "YXZ")), n = new a.Vector3(0, 0, -1).applyQuaternion(r);
|
|
272
|
+
this.panel.position.copy(t).addScaledVector(n, 1.5), this.panel.position.y -= 0.55, this.panel.quaternion.copy(r), this.panel.rotateX(-0.3);
|
|
273
|
+
}
|
|
274
|
+
/** Trim text with a trailing ellipsis to fit maxWidth. Assumes ctx.font is set. */
|
|
275
|
+
ellipsize(e, t) {
|
|
276
|
+
const i = this.ctx;
|
|
277
|
+
if (i.measureText(e).width <= t) return e;
|
|
278
|
+
let s = e;
|
|
279
|
+
for (; s.length > 1 && i.measureText(s + "…").width > t; ) s = s.slice(0, -1);
|
|
280
|
+
return s.replace(/\s+$/, "") + "…";
|
|
281
|
+
}
|
|
282
|
+
roundRect(e, t, i, s, r) {
|
|
283
|
+
const n = this.ctx;
|
|
284
|
+
n.beginPath(), n.moveTo(e + r, t), n.arcTo(e + i, t, e + i, t + s, r), n.arcTo(e + i, t + s, e, t + s, r), n.arcTo(e, t + s, e, t, r), n.arcTo(e, t, e + i, t, r), n.closePath();
|
|
285
|
+
}
|
|
286
|
+
/** Repaint the panel only when something visible changed (state or hover). */
|
|
287
|
+
paint(e) {
|
|
288
|
+
const t = this.actions.currentTime(), i = this.actions.duration(), s = this.actions.volume(), r = this.actions.muted(), n = this.actions.passthroughAvailable() && this.actions.passthroughEnabled(), l = [this.actions.isPlaying(), Math.floor(t), Math.floor(i), s.toFixed(2), r, this.hover, this.actions.title(), this.actions.passthroughAvailable(), n, this.actions.projectionLabel()].join("|");
|
|
289
|
+
if (!e && l === this.paintKey) return;
|
|
290
|
+
this.paintKey = l;
|
|
291
|
+
const o = this.ctx, c = this.layout;
|
|
292
|
+
o.clearRect(0, 0, y, w), this.roundRect(0, 0, y, w, 28), o.fillStyle = "rgba(22,24,30,0.84)", o.fill(), o.lineWidth = 2, o.strokeStyle = "rgba(255,255,255,0.08)", o.stroke();
|
|
293
|
+
const f = this.actions.title();
|
|
294
|
+
if (f) {
|
|
295
|
+
o.font = '600 30px system-ui,"Segoe UI",Roboto,sans-serif', o.fillStyle = P, o.textAlign = "center", o.textBaseline = "middle";
|
|
296
|
+
const d = 2 * c.exit.x - 48 - y;
|
|
297
|
+
o.fillText(this.ellipsize(f, d), y / 2, c.title.y + c.title.h / 2);
|
|
298
|
+
}
|
|
299
|
+
if (this.drawIconButton(c.recenter, this.hover === "recenter", !1, (d, m, b) => this.iconRecenter(d, m, b)), this.drawIconButton(c.exit, this.hover === "exit", !1, (d, m, b) => this.iconExit(d, m, b)), this.actions.passthroughAvailable()) {
|
|
300
|
+
const d = this.actions.passthroughEnabled();
|
|
301
|
+
this.drawIconButton(c.passthrough, d, this.hover === "passthrough", (m, b, M) => this.iconEye(m, b, !d, M));
|
|
302
|
+
}
|
|
303
|
+
const u = { x: c.play.x + c.play.w / 2, y: c.play.y + c.play.h / 2 };
|
|
304
|
+
o.beginPath(), o.arc(u.x, u.y, 44, 0, Math.PI * 2), o.fillStyle = this.hover === "play" ? R : "rgba(255,255,255,0.14)", o.fill(), o.fillStyle = "#fff", this.actions.isPlaying() ? (o.fillRect(u.x - 13, u.y - 16, 9, 32), o.fillRect(u.x + 4, u.y - 16, 9, 32)) : (o.beginPath(), o.moveTo(u.x - 12, u.y - 17), o.lineTo(u.x - 12, u.y + 17), o.lineTo(u.x + 18, u.y), o.closePath(), o.fill()), this.drawSpeaker(c.volIcon, r);
|
|
305
|
+
const g = r ? 0 : Math.max(0, Math.min(1, s));
|
|
306
|
+
this.drawBar(c.volBar, g, this.hover === "volume");
|
|
307
|
+
const v = c.projLabel.x + c.projLabel.w / 2;
|
|
308
|
+
o.fillStyle = L, o.font = "600 15px system-ui,sans-serif", o.textAlign = "center", o.textBaseline = "alphabetic", o.fillText("PROJECTION", v, c.projPrev.y - 8), this.drawIconButton(c.projPrev, this.hover === "projPrev", !1, (d, m, b) => this.iconArrow(d, m, -1, b)), this.drawIconButton(c.projNext, this.hover === "projNext", !1, (d, m, b) => this.iconArrow(d, m, 1, b)), o.fillStyle = P, o.font = "600 26px system-ui,sans-serif", o.textAlign = "center", o.textBaseline = "middle", o.fillText(this.ellipsize(this.actions.projectionLabel(), c.projLabel.w - 12), v, c.projLabel.y + c.projLabel.h / 2);
|
|
309
|
+
const p = i > 0 ? Math.max(0, Math.min(1, t / i)) : 0;
|
|
310
|
+
this.drawBar(c.seekBar, p, this.hover === "seek", !0), o.fillStyle = L, o.font = "22px system-ui,sans-serif", o.textBaseline = "alphabetic", o.textAlign = "left", o.fillText(k(t), c.timeCur.x, c.timeCur.y), o.textAlign = "right", o.fillText(i > 0 ? k(i) : "--:--", c.timeDur.x, c.timeDur.y), this.texture.needsUpdate = !0;
|
|
311
|
+
}
|
|
312
|
+
/** A rounded-square icon button: accent fill when `filled`, white border when `hover`; the
|
|
313
|
+
* icon is drawn centred in `filled ? white : TEXT`. */
|
|
314
|
+
drawIconButton(e, t, i, s) {
|
|
315
|
+
const r = this.ctx;
|
|
316
|
+
this.roundRect(e.x, e.y, e.w, e.h, 14), r.fillStyle = t ? R : "rgba(255,255,255,0.12)", r.fill(), i && (r.lineWidth = 3, r.strokeStyle = "rgba(255,255,255,0.9)", r.stroke()), s(e.x + e.w / 2, e.y + e.h / 2, t ? "#fff" : P);
|
|
317
|
+
}
|
|
318
|
+
/** Recenter — a reticle (ring + centre dot + crosshair ticks). */
|
|
319
|
+
iconRecenter(e, t, i) {
|
|
320
|
+
const s = this.ctx;
|
|
321
|
+
s.strokeStyle = i, s.fillStyle = i, s.lineWidth = 2.5, s.lineCap = "round", s.beginPath(), s.arc(e, t, 9, 0, Math.PI * 2), s.stroke(), s.beginPath(), s.arc(e, t, 2.4, 0, Math.PI * 2), s.fill(), s.beginPath(), s.moveTo(e, t - 16), s.lineTo(e, t - 6), s.moveTo(e, t + 6), s.lineTo(e, t + 16), s.moveTo(e - 16, t), s.lineTo(e - 6, t), s.moveTo(e + 6, t), s.lineTo(e + 16, t), s.stroke();
|
|
322
|
+
}
|
|
323
|
+
/** Passthrough — an eye; a diagonal slash across it means passthrough is off. */
|
|
324
|
+
iconEye(e, t, i, s) {
|
|
325
|
+
const r = this.ctx, n = 16, l = 10;
|
|
326
|
+
r.strokeStyle = s, r.fillStyle = s, r.lineWidth = 2.5, r.lineCap = "round", r.beginPath(), r.moveTo(e - n, t), r.quadraticCurveTo(e, t - l * 1.7, e + n, t), r.quadraticCurveTo(e, t + l * 1.7, e - n, t), r.stroke(), r.beginPath(), r.arc(e, t, 4, 0, Math.PI * 2), r.fill(), i && (r.beginPath(), r.moveTo(e - n, t + l), r.lineTo(e + n, t - l), r.stroke());
|
|
327
|
+
}
|
|
328
|
+
/** A solid triangle pointing left (dir −1) or right (dir +1) — the projection stepper arrows. */
|
|
329
|
+
iconArrow(e, t, i, s) {
|
|
330
|
+
const r = this.ctx, n = 8, l = 10;
|
|
331
|
+
r.fillStyle = s, r.beginPath(), r.moveTo(e + i * n, t), r.lineTo(e - i * n, t - l), r.lineTo(e - i * n, t + l), r.closePath(), r.fill();
|
|
332
|
+
}
|
|
333
|
+
/** Exit — a door frame with an arrow pointing out to the right. */
|
|
334
|
+
iconExit(e, t, i) {
|
|
335
|
+
const s = this.ctx, r = e - 15, n = 11, l = 22, o = e + 16;
|
|
336
|
+
s.strokeStyle = i, s.lineWidth = 2.5, s.lineCap = "round", s.lineJoin = "round", s.beginPath(), s.moveTo(r + n, t - l / 2), s.lineTo(r, t - l / 2), s.lineTo(r, t + l / 2), s.lineTo(r + n, t + l / 2), s.stroke(), s.beginPath(), s.moveTo(e - 3, t), s.lineTo(o, t), s.moveTo(o - 7, t - 6), s.lineTo(o, t), s.lineTo(o - 7, t + 6), s.stroke();
|
|
337
|
+
}
|
|
338
|
+
drawBar(e, t, i, s = !1) {
|
|
339
|
+
const r = this.ctx, n = e.h / 2;
|
|
340
|
+
this.roundRect(e.x, e.y, e.w, e.h, n), r.fillStyle = "rgba(255,255,255,0.22)", r.fill(), t > 0 && (this.roundRect(e.x, e.y, e.w * t, e.h, n), r.fillStyle = R, r.fill()), (s || i) && (r.beginPath(), r.arc(e.x + e.w * t, e.y + e.h / 2, i ? 12 : 9, 0, Math.PI * 2), r.fillStyle = "#fff", r.fill());
|
|
341
|
+
}
|
|
342
|
+
drawSpeaker(e, t) {
|
|
343
|
+
const i = this.ctx, s = e.x, r = e.y + e.h / 2;
|
|
344
|
+
i.fillStyle = t ? L : P, i.beginPath(), i.moveTo(s, r - 7), i.lineTo(s + 10, r - 7), i.lineTo(s + 20, r - 16), i.lineTo(s + 20, r + 16), i.lineTo(s + 10, r + 7), i.lineTo(s, r + 7), i.closePath(), i.fill(), i.strokeStyle = t ? "#e06a6a" : P, i.lineWidth = 3, t ? (i.beginPath(), i.moveTo(s + 26, r - 9), i.lineTo(s + 40, r + 9), i.moveTo(s + 40, r - 9), i.lineTo(s + 26, r + 9), i.stroke()) : (i.beginPath(), i.arc(s + 24, r, 8, -Math.PI / 3, Math.PI / 3), i.stroke(), i.beginPath(), i.arc(s + 24, r, 15, -Math.PI / 3, Math.PI / 3), i.stroke());
|
|
345
|
+
}
|
|
346
|
+
dispose() {
|
|
347
|
+
for (const e of this.cleanups) e();
|
|
348
|
+
for (let e = 0; e < this.controllers.length; e++) {
|
|
349
|
+
const t = this.controllers[e];
|
|
350
|
+
t.remove(this.lasers[e]), this.lasers[e].geometry.dispose(), this.lasers[e].material.dispose(), this.scene.remove(t);
|
|
351
|
+
}
|
|
352
|
+
this.scene.remove(this.panel), this.panel.geometry.dispose(), this.panel.material.dispose(), this.scene.remove(this.cursor), this.cursor.geometry.dispose(), this.cursor.material.dispose(), this.texture.dispose();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function E() {
|
|
356
|
+
return typeof performance < "u" ? performance.now() : 0;
|
|
357
|
+
}
|
|
358
|
+
class K {
|
|
359
|
+
constructor(e) {
|
|
360
|
+
this.scene = new a.Scene(), this.meshes = [], this.rig = new a.Group(), this.tilt = 0, this.fov = 1, this.builtFov = 1, this.frameCbs = [], this.alphaFisheye = !1, this.inPassthroughSession = !1, this.passthroughOn = !0, this.animate = (o) => {
|
|
361
|
+
var c;
|
|
362
|
+
for (const f of this.frameCbs) f();
|
|
363
|
+
(c = this.vrControls) == null || c.update(o ?? 0), this.video.readyState >= this.video.HAVE_CURRENT_DATA && (this.texture.needsUpdate = !0), this.renderer.render(this.scene, this.camera);
|
|
364
|
+
}, this.vrSessionInit = { optionalFeatures: ["local-floor"] }, this.arSessionInit = { optionalFeatures: ["local-floor"] }, this.vrTitle = "", this.resize = () => {
|
|
365
|
+
if (this.renderer.xr.isPresenting) return;
|
|
366
|
+
const o = this.w(), c = this.h();
|
|
367
|
+
this.camera.aspect = o / c, this.camera.updateProjectionMatrix(), this.renderer.setSize(o, c, !1);
|
|
368
|
+
};
|
|
369
|
+
const { canvas: t, video: i, projection: s = "180-sbs", swapEyes: r = !1, fov: n = 70, supersampling: l = 1.5 } = e;
|
|
370
|
+
this.canvas = t, this.video = i, this.currentMode = S[s] ? s : "180-sbs", this.currentSwap = r, this.renderer = new a.WebGLRenderer({ canvas: t, antialias: !0, alpha: !0 }), this.renderer.setClearColor(0, 1), this.renderer.setPixelRatio(this.pixelRatioFor(l)), this.renderer.setSize(this.w(), this.h(), !1), this.renderer.xr.enabled = !0, this.camera = new a.PerspectiveCamera(n, this.w() / this.h(), 0.1, 1e3), this.camera.position.set(0, 0, 0), this.maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy(), this.texture = new a.VideoTexture(i), this.texture.colorSpace = a.SRGBColorSpace, this.texture.minFilter = a.LinearFilter, this.texture.magFilter = a.LinearFilter, this.texture.anisotropy = 1, this.rig.rotation.order = "YXZ", this.scene.add(this.rig), this.applyProjection(this.currentMode, this.currentSwap), i.addEventListener("loadedmetadata", () => {
|
|
371
|
+
S[this.currentMode].flat && this.applyProjection(this.currentMode, this.currentSwap);
|
|
372
|
+
}), this.renderer.setAnimationLoop(this.animate), this.renderer.xr.addEventListener("sessionstart", () => {
|
|
373
|
+
var c;
|
|
374
|
+
const o = (c = this.renderer.xr.getSession()) == null ? void 0 : c.environmentBlendMode;
|
|
375
|
+
this.inPassthroughSession = !!(o && o !== "opaque"), this.inPassthroughSession && this.setPassthrough(!0), this.buildVRControls();
|
|
376
|
+
}), this.renderer.xr.addEventListener("sessionend", () => {
|
|
377
|
+
var o;
|
|
378
|
+
this.renderer.setClearAlpha(1), this.inPassthroughSession = !1, (o = this.vrControls) == null || o.dispose(), this.vrControls = void 0, this.video.pause();
|
|
379
|
+
}), this.ro = new ResizeObserver(() => this.resize()), this.ro.observe(t);
|
|
380
|
+
}
|
|
381
|
+
buildVRControls() {
|
|
382
|
+
const e = this.video;
|
|
383
|
+
this.vrControls = new X({
|
|
384
|
+
renderer: this.renderer,
|
|
385
|
+
scene: this.scene,
|
|
386
|
+
actions: {
|
|
387
|
+
isPlaying: () => !e.paused,
|
|
388
|
+
currentTime: () => e.currentTime,
|
|
389
|
+
duration: () => e.duration || 0,
|
|
390
|
+
volume: () => e.volume,
|
|
391
|
+
muted: () => e.muted,
|
|
392
|
+
title: () => this.vrTitle,
|
|
393
|
+
togglePlay: () => {
|
|
394
|
+
e.paused ? e.play() : e.pause();
|
|
395
|
+
},
|
|
396
|
+
seekFraction: (t) => {
|
|
397
|
+
e.duration && (e.currentTime = t * e.duration);
|
|
398
|
+
},
|
|
399
|
+
setVolume: (t) => {
|
|
400
|
+
e.volume = t, t > 0 && (e.muted = !1);
|
|
401
|
+
},
|
|
402
|
+
toggleMute: () => {
|
|
403
|
+
e.muted = !e.muted;
|
|
404
|
+
},
|
|
405
|
+
exitVR: () => this.exitVR(),
|
|
406
|
+
recenter: () => this.recenter(),
|
|
407
|
+
adjustTilt: (t) => this.adjustTilt(t),
|
|
408
|
+
adjustYaw: (t) => this.adjustYaw(t),
|
|
409
|
+
adjustZoom: (t) => this.adjustZoom(t),
|
|
410
|
+
passthroughAvailable: () => this.inPassthroughSession,
|
|
411
|
+
// any AR session, not just alpha content
|
|
412
|
+
passthroughEnabled: () => this.passthroughOn,
|
|
413
|
+
togglePassthrough: () => this.setPassthrough(!this.passthroughOn),
|
|
414
|
+
projectionLabel: () => this.projectionLabel(),
|
|
415
|
+
cycleProjection: (t) => this.cycleProjection(t)
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
/** Recenter the view: make the viewer's current spot and facing the origin, so the
|
|
420
|
+
* video front is straight ahead again. Resets yaw and horizontal position; keeps
|
|
421
|
+
* head height and a level horizon (no pitch/roll). */
|
|
422
|
+
recenter() {
|
|
423
|
+
const e = this.renderer.xr;
|
|
424
|
+
if (!e.isPresenting) return;
|
|
425
|
+
const t = e.getReferenceSpace();
|
|
426
|
+
if (!t) return;
|
|
427
|
+
const i = e.getCamera(), s = i.getWorldPosition(new a.Vector3()), r = new a.Euler().setFromQuaternion(i.getWorldQuaternion(new a.Quaternion()), "YXZ").y, n = new a.Quaternion().setFromEuler(new a.Euler(0, r, 0, "YXZ")), l = new XRRigidTransform({ x: s.x, y: 0, z: s.z }, { x: n.x, y: n.y, z: n.z, w: n.w });
|
|
428
|
+
e.setReferenceSpace(t.getOffsetReferenceSpace(l));
|
|
429
|
+
}
|
|
430
|
+
/** Optional title shown on the in-VR control panel. */
|
|
431
|
+
setVRTitle(e) {
|
|
432
|
+
this.vrTitle = e;
|
|
433
|
+
}
|
|
434
|
+
w() {
|
|
435
|
+
return this.canvas.clientWidth || 1;
|
|
436
|
+
}
|
|
437
|
+
h() {
|
|
438
|
+
return this.canvas.clientHeight || 1;
|
|
439
|
+
}
|
|
440
|
+
pixelRatioFor(e) {
|
|
441
|
+
return Math.min(window.devicePixelRatio * e, 4);
|
|
442
|
+
}
|
|
443
|
+
planeAspect(e) {
|
|
444
|
+
const t = this.video.videoWidth, i = this.video.videoHeight;
|
|
445
|
+
return !t || !i ? 16 / 9 : S[e].aspect === "per-eye" ? t / 2 / i : t / i;
|
|
446
|
+
}
|
|
447
|
+
buildGeometry(e) {
|
|
448
|
+
const t = S[e].geom, i = this.fov;
|
|
449
|
+
if (t === "sphere180") {
|
|
450
|
+
const l = Math.PI * i, o = Math.PI * i, c = new a.SphereGeometry(50, 64, 32, -l / 2, l, Math.PI / 2 - o / 2, o);
|
|
451
|
+
return c.scale(-1, 1, 1), c;
|
|
452
|
+
}
|
|
453
|
+
if (t === "sphere360") {
|
|
454
|
+
const l = 2 * Math.PI * i, o = Math.PI * i, c = new a.SphereGeometry(50, 64, 32, Math.PI - l / 2, l, Math.PI / 2 - o / 2, o);
|
|
455
|
+
return c.scale(-1, 1, 1), c;
|
|
456
|
+
}
|
|
457
|
+
if (t === "fisheye") return this.buildFisheyeDome();
|
|
458
|
+
const s = 2.4 * i, r = s * this.planeAspect(e), n = new a.PlaneGeometry(r, s);
|
|
459
|
+
return n.translate(0, 0, -2), n;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* A dome covering the fisheye field of view (FISHEYE190 ≈ 190° → a 95° half-angle cap in
|
|
463
|
+
* front of the viewer), with equidistant-fisheye UVs: the angle from the forward axis maps
|
|
464
|
+
* linearly to radius on the texture disc (centre 0.5,0.5, radius 0.5). UVs are authored in
|
|
465
|
+
* full-frame [0,1] space; updateStereoUV's per-eye repeat/offset selects the L/R circle.
|
|
466
|
+
* Forward is −z (the camera's default view direction), so no extra rotation is needed.
|
|
467
|
+
*/
|
|
468
|
+
buildFisheyeDome(e = 95) {
|
|
469
|
+
const r = a.MathUtils.degToRad(e), n = [], l = [], o = [];
|
|
470
|
+
for (let u = 0; u <= 64; u++) {
|
|
471
|
+
const g = u / 64, v = g * r, p = g * 0.5, d = Math.sin(v), m = Math.cos(v);
|
|
472
|
+
for (let b = 0; b <= 128; b++) {
|
|
473
|
+
const M = b / 128 * Math.PI * 2, j = Math.cos(M), F = Math.sin(M);
|
|
474
|
+
n.push(d * j * 50, d * F * 50, -m * 50), l.push(0.5 + p * j, 0.5 + p * F);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const c = 129;
|
|
478
|
+
for (let u = 0; u < 64; u++)
|
|
479
|
+
for (let g = 0; g < 128; g++) {
|
|
480
|
+
const v = u * c + g, p = v + 1, d = v + c, m = d + 1;
|
|
481
|
+
o.push(v, d, p, p, d, m);
|
|
482
|
+
}
|
|
483
|
+
const f = new a.BufferGeometry();
|
|
484
|
+
return f.setAttribute("position", new a.Float32BufferAttribute(n, 3)), f.setAttribute("uv", new a.Float32BufferAttribute(l, 2)), f.setIndex(o), f.computeVertexNormals(), f;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Transparent material for fisheye content with a DeoVR-style packed alpha matte. Built by
|
|
488
|
+
* patching MeshBasicMaterial (so three.js keeps handling colour management, the map sample and
|
|
489
|
+
* flipY) and injecting DeoVR's exact alpha decode (ported from their WebXR player):
|
|
490
|
+
*
|
|
491
|
+
* ptUV = fract(vec2(discUV.x*0.2 + ptShift, discUV.y*0.4 + 0.8)); // ptShift 0.4 L / 0.9 R
|
|
492
|
+
* alpha = smoothstep(0.0, 0.8, average of the 3×3 red neighbourhood at ptUV);
|
|
493
|
+
*
|
|
494
|
+
* The matte is a 0.4×-scaled copy of the disc's alpha stored per eye at that offset, wrapped
|
|
495
|
+
* across the frame edges (hence it appears tucked into the corners). It's indexed by the SAME
|
|
496
|
+
* disc UV as the colour, so it aligns exactly — including per-eye parallax. Outside the disc → clip.
|
|
497
|
+
*/
|
|
498
|
+
makeFisheyeAlphaMaterial() {
|
|
499
|
+
const e = new a.MeshBasicMaterial({ map: this.texture, transparent: !0, side: a.DoubleSide, depthWrite: !1 }), t = {
|
|
500
|
+
uPtShift: { value: 0.4 },
|
|
501
|
+
uTexel: { value: new a.Vector2(1 / 8e3, 1 / 4e3) },
|
|
502
|
+
uAlphaEnabled: { value: this.passthroughOn ? 1 : 0 }
|
|
503
|
+
// 0 → opaque dome (passthrough off)
|
|
504
|
+
};
|
|
505
|
+
return this.alphaUniforms = t, e.onBeforeCompile = (i) => {
|
|
506
|
+
i.uniforms.uPtShift = t.uPtShift, i.uniforms.uTexel = t.uTexel, i.uniforms.uAlphaEnabled = t.uAlphaEnabled, i.vertexShader = `varying vec2 vDiscUv;
|
|
507
|
+
` + i.vertexShader.replace("void main() {", `void main() {
|
|
508
|
+
vDiscUv = uv;`), i.fragmentShader = `uniform float uPtShift;
|
|
509
|
+
uniform vec2 uTexel;
|
|
510
|
+
uniform float uAlphaEnabled;
|
|
511
|
+
varying vec2 vDiscUv;
|
|
512
|
+
` + i.fragmentShader.replace("#include <map_fragment>", `#include <map_fragment>
|
|
513
|
+
{
|
|
514
|
+
vec2 n = (vDiscUv - 0.5) * 2.0;
|
|
515
|
+
if (dot(n, n) > 1.0) discard; // outside the fisheye circle
|
|
516
|
+
vec2 mUv = fract(vec2(vDiscUv.x * 0.2 + uPtShift, vDiscUv.y * 0.4 + 0.8));
|
|
517
|
+
float s = 0.0; // DeoVR getMask(): 3x3 red average
|
|
518
|
+
for (int dy = -1; dy <= 1; dy++) {
|
|
519
|
+
for (int dx = -1; dx <= 1; dx++) {
|
|
520
|
+
s += texture2D(map, mUv + uTexel * vec2(float(dx), float(dy))).r;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// uAlphaEnabled 1 → keyed matte (passthrough); 0 → fully opaque dome (no passthrough)
|
|
524
|
+
diffuseColor.a *= mix(1.0, smoothstep(0.0, 0.8, s / 9.0), uAlphaEnabled);
|
|
525
|
+
}`);
|
|
526
|
+
}, e;
|
|
527
|
+
}
|
|
528
|
+
/** Passthrough on/off (immersive-ar only): on = real world visible (transparent clear); off =
|
|
529
|
+
* opaque black surround (VR-style). Alpha content additionally keys the subject via the matte;
|
|
530
|
+
* without a matte the dome stays opaque and only the surround changes. */
|
|
531
|
+
setPassthrough(e) {
|
|
532
|
+
var i;
|
|
533
|
+
this.passthroughOn = e, this.alphaUniforms && (this.alphaUniforms.uAlphaEnabled.value = e ? 1 : 0);
|
|
534
|
+
const t = (i = this.renderer.xr.getSession()) == null ? void 0 : i.environmentBlendMode;
|
|
535
|
+
t && t !== "opaque" && this.renderer.setClearAlpha(e ? 0 : 1);
|
|
536
|
+
}
|
|
537
|
+
/** Mark the current content as carrying a DeoVR-style packed alpha matte (fisheye only). */
|
|
538
|
+
setAlphaMatte(e) {
|
|
539
|
+
this.alphaFisheye !== e && (this.alphaFisheye = e, this.applyProjection(this.currentMode, this.currentSwap));
|
|
540
|
+
}
|
|
541
|
+
/** Shift the shared video texture for the eye/half being drawn. Runs from the mesh's
|
|
542
|
+
* onBeforeRender, so it's called once per eye per frame (the reference approach). */
|
|
543
|
+
updateStereoUV(e) {
|
|
544
|
+
const t = this.texture, i = S[this.currentMode].split;
|
|
545
|
+
let s = !0;
|
|
546
|
+
if (this.renderer.xr.isPresenting) {
|
|
547
|
+
const n = e, l = this.renderer.xr.getCamera().cameras;
|
|
548
|
+
l && l.length >= 2 ? n === l[0] ? s = !0 : n === l[1] ? s = !1 : s = Math.abs(n.matrixWorldInverse.elements[12] - l[0].matrixWorldInverse.elements[12]) <= Math.abs(n.matrixWorldInverse.elements[12] - l[1].matrixWorldInverse.elements[12]) : s = n.projectionMatrix.elements[8] <= 0;
|
|
549
|
+
}
|
|
550
|
+
const r = !s !== this.currentSwap;
|
|
551
|
+
if (i === "sbs" ? (t.repeat.set(0.5, 1), t.offset.set(r ? 0.5 : 0, 0)) : i === "tb" ? (t.repeat.set(1, 0.5), t.offset.set(0, r ? 0 : 0.5)) : (t.repeat.set(1, 1), t.offset.set(0, 0)), this.alphaUniforms) {
|
|
552
|
+
this.alphaUniforms.uPtShift.value = r ? 0.9 : 0.4;
|
|
553
|
+
const n = this.video.videoWidth, l = this.video.videoHeight;
|
|
554
|
+
n && l && this.alphaUniforms.uTexel.value.set(1 / n, 1 / l);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
clearMeshes() {
|
|
558
|
+
for (const e of this.meshes)
|
|
559
|
+
this.rig.remove(e), e.geometry.dispose(), e.material.dispose();
|
|
560
|
+
this.meshes.length = 0;
|
|
561
|
+
}
|
|
562
|
+
applyProjection(e, t) {
|
|
563
|
+
S[e] || (e = "180-sbs"), this.clearMeshes(), this.currentMode = e, this.currentSwap = t, this.alphaUniforms = void 0;
|
|
564
|
+
let i;
|
|
565
|
+
if (S[e].geom === "fisheye" && this.alphaFisheye)
|
|
566
|
+
i = this.makeFisheyeAlphaMaterial();
|
|
567
|
+
else {
|
|
568
|
+
const r = S[e].geom === "fisheye" ? a.DoubleSide : a.FrontSide;
|
|
569
|
+
i = new a.MeshBasicMaterial({ map: this.texture, side: r });
|
|
570
|
+
}
|
|
571
|
+
const s = new a.Mesh(this.buildGeometry(e), i);
|
|
572
|
+
s.layers.set(0), S[e].geom === "sphere180" && (s.rotation.y = Math.PI / 2), s.onBeforeRender = (r, n, l) => this.updateStereoUV(l), this.rig.add(s), this.meshes.push(s);
|
|
573
|
+
}
|
|
574
|
+
/** Pitch the video content up/down (radians, accumulated and clamped). */
|
|
575
|
+
adjustTilt(e) {
|
|
576
|
+
this.tilt = Math.max(-0.9, Math.min(0.9, this.tilt + e)), this.rig.rotation.x = this.tilt;
|
|
577
|
+
}
|
|
578
|
+
/** Yaw the video content left/right (radians, accumulated, unclamped). */
|
|
579
|
+
adjustYaw(e) {
|
|
580
|
+
this.rig.rotation.y += e;
|
|
581
|
+
}
|
|
582
|
+
/** Zoom by changing the content field of view (positive delta = zoom in / more
|
|
583
|
+
* immersive). Shrinking the fov maps the video into a smaller angular window so the
|
|
584
|
+
* scene recedes and feels farther; growing it fills the view. Rebuilds the projection
|
|
585
|
+
* geometry, throttled so it doesn't churn every frame. */
|
|
586
|
+
adjustZoom(e) {
|
|
587
|
+
this.fov = Math.max(0.3, Math.min(1, this.fov + e)), Math.abs(this.fov - this.builtFov) >= 0.03 && this.rebuildGeometry();
|
|
588
|
+
}
|
|
589
|
+
rebuildGeometry() {
|
|
590
|
+
for (const e of this.meshes) {
|
|
591
|
+
const t = e.geometry;
|
|
592
|
+
e.geometry = this.buildGeometry(this.currentMode), t.dispose();
|
|
593
|
+
}
|
|
594
|
+
this.builtFov = this.fov;
|
|
595
|
+
}
|
|
596
|
+
setProjection(e) {
|
|
597
|
+
this.applyProjection(e, this.currentSwap);
|
|
598
|
+
}
|
|
599
|
+
/** Player registers this so an in-VR projection change flows back through Player — keeping its
|
|
600
|
+
* state, persistence, and the DOM controls in sync. Falls back to a direct geometry swap. */
|
|
601
|
+
setProjectionRequester(e) {
|
|
602
|
+
this.projectionRequester = e;
|
|
603
|
+
}
|
|
604
|
+
/** Short label of the current projection, for the in-VR stepper. */
|
|
605
|
+
projectionLabel() {
|
|
606
|
+
return V[this.currentMode] ?? this.currentMode;
|
|
607
|
+
}
|
|
608
|
+
/** Step the projection by dir (±1) through the PROJECTIONS list (wraps around). */
|
|
609
|
+
cycleProjection(e) {
|
|
610
|
+
const t = G.map((r) => r.value), i = Math.max(0, t.indexOf(this.currentMode)), s = t[(i + e + t.length) % t.length];
|
|
611
|
+
(this.projectionRequester ?? ((r) => this.setProjection(r)))(s);
|
|
612
|
+
}
|
|
613
|
+
setSwapEyes(e) {
|
|
614
|
+
this.applyProjection(this.currentMode, e);
|
|
615
|
+
}
|
|
616
|
+
setFov(e) {
|
|
617
|
+
this.camera.fov = e, this.camera.updateProjectionMatrix();
|
|
618
|
+
}
|
|
619
|
+
setSupersampling(e) {
|
|
620
|
+
this.renderer.setPixelRatio(this.pixelRatioFor(e)), this.renderer.setSize(this.w(), this.h(), !1);
|
|
621
|
+
}
|
|
622
|
+
getProjection() {
|
|
623
|
+
return this.currentMode;
|
|
624
|
+
}
|
|
625
|
+
isFlat() {
|
|
626
|
+
return !!S[this.currentMode].flat;
|
|
627
|
+
}
|
|
628
|
+
onFrame(e) {
|
|
629
|
+
this.frameCbs.push(e);
|
|
630
|
+
}
|
|
631
|
+
pauseRendering() {
|
|
632
|
+
this.renderer.setAnimationLoop(null);
|
|
633
|
+
}
|
|
634
|
+
resumeRendering() {
|
|
635
|
+
this.renderer.setAnimationLoop(this.animate);
|
|
636
|
+
}
|
|
637
|
+
/** Enter immersive VR. Rejects with a readable message on failure so the caller can
|
|
638
|
+
* surface it — instead of the silent no-op three.js's VRButton produces. Called from
|
|
639
|
+
* a click handler so the request keeps the user's transient activation. */
|
|
640
|
+
async enterVR() {
|
|
641
|
+
if (!navigator.xr) throw new Error("WebXR is unavailable here — open the page over HTTPS or localhost.");
|
|
642
|
+
const e = await navigator.xr.requestSession("immersive-vr", this.vrSessionInit);
|
|
643
|
+
await this.renderer.xr.setSession(e);
|
|
644
|
+
}
|
|
645
|
+
/** Enter immersive AR (passthrough). Same contract as enterVR — called from a click so it
|
|
646
|
+
* keeps transient activation, rejects with a readable message. The UA composites our
|
|
647
|
+
* transparent framebuffer over the camera feed. */
|
|
648
|
+
async enterAR() {
|
|
649
|
+
if (!navigator.xr) throw new Error("WebXR is unavailable here — open the page over HTTPS or localhost.");
|
|
650
|
+
const e = await navigator.xr.requestSession("immersive-ar", this.arSessionInit);
|
|
651
|
+
await this.renderer.xr.setSession(e);
|
|
652
|
+
}
|
|
653
|
+
exitVR() {
|
|
654
|
+
var e;
|
|
655
|
+
this.video.paused || this.video.pause(), (e = this.renderer.xr.getSession()) == null || e.end();
|
|
656
|
+
}
|
|
657
|
+
/** Arm the browser/headset's own "Enter VR" affordance (e.g. the Quest system button). */
|
|
658
|
+
offerVR() {
|
|
659
|
+
var i;
|
|
660
|
+
const e = (i = navigator.xr) == null ? void 0 : i.offerSession;
|
|
661
|
+
if (!e) return;
|
|
662
|
+
const t = () => e.call(navigator.xr, "immersive-vr", this.vrSessionInit).then((s) => this.renderer.xr.setSession(s)).catch(() => {
|
|
663
|
+
});
|
|
664
|
+
this.renderer.xr.addEventListener("sessionend", () => void t()), t();
|
|
665
|
+
}
|
|
666
|
+
dispose() {
|
|
667
|
+
var e;
|
|
668
|
+
this.ro.disconnect(), this.renderer.setAnimationLoop(null), (e = this.vrControls) == null || e.dispose(), this.texture.dispose(), this.clearMeshes(), this.renderer.dispose();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const B = Math.PI / 180;
|
|
672
|
+
function $(h, e) {
|
|
673
|
+
return {
|
|
674
|
+
lon: Math.max(-90, Math.min(90, h)),
|
|
675
|
+
lat: Math.max(-85, Math.min(85, e))
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
class ee {
|
|
679
|
+
constructor(e, t, i = {}) {
|
|
680
|
+
this.camera = e, this.dom = t, this.lon = 0, this.lat = 0, this.dragging = !1, this.px = 0, this.py = 0, this.enabled = !0, this.onDown = (s) => {
|
|
681
|
+
this.enabled && (this.dragging = !0, this.px = s.clientX, this.py = s.clientY, this.capture("setPointerCapture", s.pointerId));
|
|
682
|
+
}, this.onMove = (s) => {
|
|
683
|
+
if (!this.dragging) return;
|
|
684
|
+
const r = s.clientX - this.px, n = s.clientY - this.py;
|
|
685
|
+
this.px = s.clientX, this.py = s.clientY, { lon: this.lon, lat: this.lat } = $(this.lon - r * 0.15, this.lat + n * 0.15);
|
|
686
|
+
}, this.onUp = (s) => {
|
|
687
|
+
this.dragging = !1, this.capture("releasePointerCapture", s.pointerId);
|
|
688
|
+
}, this.isPresenting = i.isPresenting ?? (() => !1), this.dom.addEventListener("pointerdown", this.onDown), this.dom.addEventListener("pointermove", this.onMove), this.dom.addEventListener("pointerup", this.onUp), this.dom.addEventListener("pointercancel", this.onUp);
|
|
689
|
+
}
|
|
690
|
+
capture(e, t) {
|
|
691
|
+
var i, s;
|
|
692
|
+
try {
|
|
693
|
+
(s = (i = this.dom)[e]) == null || s.call(i, t);
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
update() {
|
|
698
|
+
if (this.isPresenting() || !this.enabled) return;
|
|
699
|
+
const e = (90 - this.lat) * B, t = this.lon * B;
|
|
700
|
+
this.camera.lookAt(
|
|
701
|
+
Math.sin(e) * Math.sin(t),
|
|
702
|
+
Math.cos(e),
|
|
703
|
+
-Math.sin(e) * Math.cos(t)
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
reset() {
|
|
707
|
+
this.lon = 0, this.lat = 0;
|
|
708
|
+
}
|
|
709
|
+
setEnabled(e) {
|
|
710
|
+
this.enabled = e, e || (this.lon = 0, this.lat = 0, this.camera.lookAt(0, 0, -1));
|
|
711
|
+
}
|
|
712
|
+
getAngles() {
|
|
713
|
+
return { lon: this.lon, lat: this.lat };
|
|
714
|
+
}
|
|
715
|
+
dispose() {
|
|
716
|
+
this.dom.removeEventListener("pointerdown", this.onDown), this.dom.removeEventListener("pointermove", this.onMove), this.dom.removeEventListener("pointerup", this.onUp), this.dom.removeEventListener("pointercancel", this.onUp);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
class te {
|
|
720
|
+
constructor() {
|
|
721
|
+
this.hls = null;
|
|
722
|
+
}
|
|
723
|
+
async attach(e, t, i = {}) {
|
|
724
|
+
this.dispose();
|
|
725
|
+
const s = i.crossOrigin === void 0 ? "anonymous" : i.crossOrigin;
|
|
726
|
+
s === null ? e.removeAttribute("crossorigin") : e.crossOrigin = s, e.playsInline = !0;
|
|
727
|
+
const { url: r, format: n } = t, l = n === "hls" || n === "dash", o = e.canPlayType("application/vnd.apple.mpegurl") !== "";
|
|
728
|
+
return new Promise((c, f) => {
|
|
729
|
+
const u = () => {
|
|
730
|
+
e.removeEventListener("loadedmetadata", g), e.removeEventListener("error", v);
|
|
731
|
+
}, g = () => {
|
|
732
|
+
u(), c();
|
|
733
|
+
}, v = () => {
|
|
734
|
+
u();
|
|
735
|
+
const p = e.error ? e.error.code : "unknown";
|
|
736
|
+
f(new Error(`Video failed to load (media error ${p}). Is the source reachable and CORS-clean?`));
|
|
737
|
+
};
|
|
738
|
+
e.addEventListener("loadedmetadata", g), e.addEventListener("error", v), l && !o ? import("hls.js").then(({ default: p }) => {
|
|
739
|
+
if (!p.isSupported()) {
|
|
740
|
+
u(), f(new Error("HLS/DASH stream needs hls.js, which is unsupported in this browser."));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
this.hls = new p({ enableWorker: !0 }), this.hls.on(p.Events.ERROR, (d, m) => {
|
|
744
|
+
m.fatal && (u(), this.dispose(), f(new Error(`HLS fatal error: ${m.type} / ${m.details}`)));
|
|
745
|
+
}), this.hls.loadSource(r), this.hls.attachMedia(e);
|
|
746
|
+
}).catch(() => {
|
|
747
|
+
u(), f(new Error('This looks like an HLS/DASH stream but hls.js could not be loaded. Install "hls.js".'));
|
|
748
|
+
}) : (e.src = r, e.load());
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
dispose() {
|
|
752
|
+
this.hls && (this.hls.destroy(), this.hls = null);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
export {
|
|
756
|
+
ee as L,
|
|
757
|
+
S as M,
|
|
758
|
+
G as P,
|
|
759
|
+
K as S,
|
|
760
|
+
te as V,
|
|
761
|
+
J as a,
|
|
762
|
+
Y as b,
|
|
763
|
+
$ as c,
|
|
764
|
+
Q as d,
|
|
765
|
+
k as f,
|
|
766
|
+
Z as i
|
|
767
|
+
};
|
|
768
|
+
//# sourceMappingURL=VideoSource-DHj5tBsK.js.map
|