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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoSource-DHj5tBsK.js","sources":["../src/core/proxy.ts","../src/core/projections.ts","../src/ui/format.ts","../src/core/vr-panel-layout.ts","../src/core/VRControls.ts","../src/core/StereoScene.ts","../src/core/LookControls.ts","../src/core/VideoSource.ts"],"sourcesContent":["export type StreamFormat = 'hls' | 'dash' | 'progressive';\r\n\r\nexport interface ProxyConfig {\r\n url: string;\r\n apiPassword?: string;\r\n headers?: Record<string, string>;\r\n}\r\n\r\n/** Detect streaming format from a URL by extension. */\r\nexport function detectFormat(rawUrl: string): StreamFormat {\r\n let pathname = rawUrl;\r\n try { pathname = new URL(rawUrl).pathname; } catch { /* relative/opaque: use raw */ }\r\n const lower = pathname.toLowerCase();\r\n if (lower.endsWith('.m3u8')) return 'hls';\r\n if (lower.endsWith('.mpd')) return 'dash';\r\n return 'progressive';\r\n}\r\n\r\nconst ENDPOINTS: Record<StreamFormat, string> = {\r\n progressive: '/proxy/stream',\r\n hls: '/proxy/hls/manifest.m3u8',\r\n dash: '/proxy/mpd/manifest.m3u8',\r\n};\r\n\r\n/**\r\n * Build the URL the `<video>`/hls.js should load. When `proxy` is omitted the raw\r\n * URL is returned (it must then be CORS-clean to be used as a WebGL texture).\r\n */\r\nexport function buildProxyUrl(rawUrl: string, proxy?: ProxyConfig): { url: string; format: StreamFormat } {\r\n const format = detectFormat(rawUrl);\r\n if (!proxy || !proxy.url) return { url: rawUrl, format };\r\n const base = proxy.url.replace(/\\/+$/, '');\r\n const params = new URLSearchParams();\r\n params.set('d', rawUrl);\r\n if (proxy.apiPassword) params.set('api_password', proxy.apiPassword);\r\n for (const [name, value] of Object.entries(proxy.headers ?? {})) {\r\n if (value != null && value !== '') params.set(`h_${name}`, value);\r\n }\r\n return { url: `${base}${ENDPOINTS[format]}?${params.toString()}`, format };\r\n}\r\n","import type { Projection } from '../types.js';\r\n\r\nexport type Split = 'mono' | 'sbs' | 'tb';\r\nexport type GeomKind = 'sphere180' | 'sphere360' | 'fisheye' | 'plane';\r\n\r\nexport interface ModeConfig {\r\n geom: GeomKind;\r\n split: Split;\r\n stereo: boolean;\r\n flat?: boolean;\r\n /** plane only: 'full' = displayW/H, 'per-eye' = (W/2)/H */\r\n aspect?: 'full' | 'per-eye';\r\n}\r\n\r\nexport const MODES: Record<Projection, ModeConfig> = {\r\n '180-sbs': { geom: 'sphere180', split: 'sbs', stereo: true },\r\n '180-mono': { geom: 'sphere180', split: 'mono', stereo: false },\r\n '360-mono': { geom: 'sphere360', split: 'mono', stereo: false },\r\n '360-sbs': { geom: 'sphere360', split: 'sbs', stereo: true },\r\n '360-tb': { geom: 'sphere360', split: 'tb', stereo: true },\r\n // DeoVR-style fisheye (~190°): a dome in front of the viewer, one circle per eye.\r\n 'fisheye190-sbs': { geom: 'fisheye', split: 'sbs', stereo: true },\r\n 'fisheye190-mono': { geom: 'fisheye', split: 'mono', stereo: false },\r\n 'flat-2d': { geom: 'plane', split: 'mono', stereo: false, flat: true, aspect: 'full' },\r\n 'flat-sbs-full': { geom: 'plane', split: 'sbs', stereo: true, flat: true, aspect: 'per-eye' },\r\n 'flat-sbs-half': { geom: 'plane', split: 'sbs', stereo: true, flat: true, aspect: 'full' },\r\n};\r\n\r\nexport const PROJECTIONS: { value: Projection; label: string }[] = [\r\n { value: '180-sbs', label: '180° SBS (VR180)' },\r\n { value: '180-mono', label: '180° Mono' },\r\n { value: '360-mono', label: '360° Mono' },\r\n { value: '360-sbs', label: '360° SBS' },\r\n { value: '360-tb', label: '360° Top-Bottom' },\r\n { value: 'fisheye190-sbs', label: 'Fisheye 190° SBS' },\r\n { value: 'fisheye190-mono', label: 'Fisheye 190° Mono' },\r\n { value: 'flat-2d', label: 'Flat 2D' },\r\n { value: 'flat-sbs-full', label: 'Flat 3D — Full SBS' },\r\n { value: 'flat-sbs-half', label: 'Flat 3D — Half SBS' },\r\n];\r\n\r\n/** Compact labels for the in-VR projection stepper (the full labels are too long for the panel). */\r\nexport const PROJECTION_SHORT: Record<Projection, string> = {\r\n '180-sbs': '180° SBS', '180-mono': '180° Mono',\r\n '360-mono': '360° Mono', '360-sbs': '360° SBS', '360-tb': '360° TB',\r\n 'fisheye190-sbs': 'Fisheye SBS', 'fisheye190-mono': 'Fisheye Mono',\r\n 'flat-2d': 'Flat 2D', 'flat-sbs-full': 'Flat SBS', 'flat-sbs-half': 'Flat SBS½',\r\n};\r\n\r\nexport function isFlatMode(p: Projection): boolean {\r\n return !!MODES[p]?.flat;\r\n}\r\n\r\n/** Guess a projection from a URL / filename; null when nothing recognizable. */\r\nexport function detectProjection(url: string): Projection | null {\r\n const s = String(url).toLowerCase();\r\n const tb = /(^|[^a-z])(tb|ou|top.?bottom|over.?under)([^a-z]|$)/.test(s);\r\n const sbs = /(sbs|side.?by.?side)/.test(s);\r\n // DeoVR fisheye (FISHEYE190 / MKX200 / RF52 / VRCA220) — stereo unless tagged mono.\r\n if (/(fisheye|mkx200|rf52|vrca220)/.test(s)) {\r\n return s.includes('mono') ? 'fisheye190-mono' : 'fisheye190-sbs';\r\n }\r\n if (s.includes('360')) {\r\n if (tb) return '360-tb';\r\n if (sbs) return '360-sbs';\r\n return '360-mono';\r\n }\r\n if (s.includes('180') || s.includes('vr180')) {\r\n return s.includes('mono') ? '180-mono' : '180-sbs';\r\n }\r\n if (sbs) return s.includes('half') ? 'flat-sbs-half' : 'flat-sbs-full';\r\n return null;\r\n}\r\n","/** Format seconds as m:ss or h:mm:ss. */\r\nexport function formatTime(seconds: number): string {\r\n if (!Number.isFinite(seconds) || seconds < 0) return '0:00';\r\n const s = Math.floor(seconds % 60);\r\n const m = Math.floor((seconds / 60) % 60);\r\n const h = Math.floor(seconds / 3600);\r\n const ss = String(s).padStart(2, '0');\r\n return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${ss}` : `${m}:${ss}`;\r\n}\r\n","/**\r\n * Geometry and hit-testing for the in-VR control panel, expressed purely in\r\n * canvas-pixel space — no three.js, no DOM. Both the painter (VRControls draws\r\n * to a 2D canvas) and the controller-ray hit-tester read the SAME layout, so a\r\n * button is never drawn in one place and clicked in another. Kept dependency-free\r\n * so it can be unit-tested.\r\n */\r\n\r\nexport const PANEL_W = 1024;\r\nexport const PANEL_H = 340;\r\n\r\nexport type VRRegion = 'play' | 'seek' | 'volume' | 'exit' | 'recenter' | 'passthrough' | 'projPrev' | 'projNext';\r\n\r\nexport interface Rect { x: number; y: number; w: number; h: number; }\r\n\r\nexport interface PanelLayout {\r\n width: number; height: number;\r\n title: Rect;\r\n recenter: Rect;\r\n passthrough: Rect;\r\n exit: Rect;\r\n play: Rect;\r\n volIcon: Rect;\r\n volBar: Rect;\r\n projPrev: Rect;\r\n projNext: Rect;\r\n projLabel: Rect; // display only (no hit-test) — the current projection name\r\n seekBar: Rect;\r\n timeCur: { x: number; y: number };\r\n timeDur: { x: number; y: number };\r\n}\r\n\r\n/** Where every control sits on the panel canvas. A single source of truth. */\r\nexport function panelLayout(): PanelLayout {\r\n const W = PANEL_W, H = PANEL_H, pad = 48;\r\n return {\r\n width: W, height: H,\r\n // Top-row icon buttons (reticle / eye / door-arrow). Compact so they read as icons, not labels.\r\n recenter: { x: 30, y: 26, w: 64, h: 52 },\r\n passthrough: { x: 108, y: 26, w: 64, h: 52 }, // toggle; shown only for alpha (passthrough) content\r\n exit: { x: W - 94, y: 26, w: 64, h: 52 },\r\n title: { x: 0, y: 92, w: W, h: 44 },\r\n play: { x: W / 2 - 44, y: 150, w: 88, h: 88 },\r\n volIcon: { x: pad, y: 178, w: 44, h: 44 },\r\n volBar: { x: pad + 60, y: 192, w: 200, h: 16 },\r\n // Projection stepper on the right: ◀ [label] ▶\r\n projPrev: { x: 592, y: 178, w: 46, h: 46 },\r\n projNext: { x: W - 94, y: 178, w: 46, h: 46 },\r\n projLabel: { x: 646, y: 178, w: W - 94 - 646, h: 46 },\r\n seekBar: { x: pad, y: 286, w: W - pad * 2, h: 14 },\r\n timeCur: { x: pad, y: 262 },\r\n timeDur: { x: W - pad, y: 262 },\r\n };\r\n}\r\n\r\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\r\n\r\nfunction inRect(r: Rect, x: number, y: number, padX = 0, padY = 0): boolean {\r\n return x >= r.x - padX && x <= r.x + r.w + padX && y >= r.y - padY && y <= r.y + r.h + padY;\r\n}\r\n\r\nexport interface VRHit {\r\n region: VRRegion;\r\n /** For bars, the 0..1 fraction along the track. For the volume icon (mute\r\n * toggle) it is undefined — a bare `volume` hit means \"toggle mute\". */\r\n value?: number;\r\n}\r\n\r\n/**\r\n * Map a canvas-pixel point to the control it lands on, or null for empty space.\r\n * The thin bars get generous vertical padding so aiming with a jittery controller\r\n * ray is forgiving.\r\n */\r\nexport function hitTest(x: number, y: number, layout: PanelLayout = panelLayout()): VRHit | null {\r\n const BAR_PAD = 26;\r\n if (inRect(layout.recenter, x, y)) return { region: 'recenter' };\r\n if (inRect(layout.passthrough, x, y)) return { region: 'passthrough' };\r\n if (inRect(layout.exit, x, y)) return { region: 'exit' };\r\n if (inRect(layout.projPrev, x, y)) return { region: 'projPrev' };\r\n if (inRect(layout.projNext, x, y)) return { region: 'projNext' };\r\n if (inRect(layout.play, x, y)) return { region: 'play' };\r\n if (inRect(layout.volIcon, x, y)) return { region: 'volume' }; // no value -> toggle mute\r\n if (inRect(layout.volBar, x, y, 12, BAR_PAD)) {\r\n return { region: 'volume', value: clamp01((x - layout.volBar.x) / layout.volBar.w) };\r\n }\r\n if (inRect(layout.seekBar, x, y, 12, BAR_PAD)) {\r\n return { region: 'seek', value: clamp01((x - layout.seekBar.x) / layout.seekBar.w) };\r\n }\r\n return null;\r\n}\r\n","import * as THREE from 'three';\r\nimport { formatTime } from '../ui/format.js';\r\nimport { PANEL_W, PANEL_H, panelLayout, hitTest, type PanelLayout, type VRRegion, type Rect } from './vr-panel-layout.js';\r\n\r\n/** The playback state + commands the panel needs. Kept small so the owner\r\n * (StereoScene) can wire it straight to the `<video>` element. */\r\nexport interface VRControlsActions {\r\n isPlaying(): boolean;\r\n currentTime(): number;\r\n duration(): number;\r\n volume(): number; // 0..1\r\n muted(): boolean;\r\n title(): string;\r\n togglePlay(): void;\r\n seekFraction(f: number): void;\r\n setVolume(v: number): void;\r\n toggleMute(): void;\r\n exitVR(): void;\r\n recenter(): void;\r\n adjustTilt(delta: number): void;\r\n adjustYaw(delta: number): void;\r\n adjustZoom(delta: number): void;\r\n /** Passthrough (alpha) content only: whether the toggle applies, its state, and the toggle. */\r\n passthroughAvailable(): boolean;\r\n passthroughEnabled(): boolean;\r\n togglePassthrough(): void;\r\n /** Projection stepper: the current mode's short label, and step by ±1 through the modes. */\r\n projectionLabel(): string;\r\n cycleProjection(dir: number): void;\r\n}\r\n\r\nconst ACCENT = '#4f8cff';\r\nconst TEXT = '#e8eaed';\r\nconst MUTED_TEXT = '#aab2c0';\r\n// Quest Touch (xr-standard) face buttons: 4 = A/X (lower), 5 = B/Y (upper).\r\nconst TOGGLE_BUTTONS = [4, 5];\r\nconst IDLE_HIDE_MS = 8000;\r\n// Deliberate controller motion (per frame) that re-summons the panel; tunable.\r\nconst MOTION_ROT = 0.05; // radians\r\nconst MOTION_POS = 0.02; // metres\r\nconst REVEAL_COOLDOWN_MS = 1500; // don't re-summon on motion right after hiding\r\n// Right thumbstick: X zooms (content fov), Y tilts (pitch); the grip grab-rotate also does\r\n// pitch/yaw. Tunable.\r\nconst STICK_DEADZONE = 0.15;\r\nconst ZOOM_RATE = 0.012; // content fov (stick X)\r\nconst TILT_RATE = 0.012; // radians/frame at full deflection (stick Y)\r\nconst GRAB_TAP_ANGLE = 0.08; // radians: a grip turned less than this is a tap (toggle), not a grab\r\n\r\n/** A world-locked control panel for immersive VR, drawn to a canvas texture and\r\n * driven by the motion controllers' laser + trigger. Lives only while an XR\r\n * session is active; StereoScene builds it on sessionstart and disposes it on end. */\r\nexport class VRControls {\r\n private readonly renderer: THREE.WebGLRenderer;\r\n private readonly scene: THREE.Scene;\r\n private readonly actions: VRControlsActions;\r\n private readonly layout: PanelLayout = panelLayout();\r\n\r\n private readonly canvas: HTMLCanvasElement;\r\n private readonly ctx: CanvasRenderingContext2D;\r\n private readonly texture: THREE.CanvasTexture;\r\n private readonly panel: THREE.Mesh;\r\n private readonly cursor: THREE.Mesh; // white dot at the laser/panel intersection\r\n\r\n private readonly raycaster = new THREE.Raycaster();\r\n private readonly tmpMatrix = new THREE.Matrix4();\r\n private readonly tmpVec = new THREE.Vector3();\r\n private readonly tmpQuat = new THREE.Quaternion();\r\n private readonly prevQuat: THREE.Quaternion[] = []; // last frame's controller poses, for motion reveal\r\n private readonly prevPos: THREE.Vector3[] = [];\r\n private revealCooldownUntil = 0;\r\n // Grip grab-rotate: while held, the controller's rotation drives content pitch/yaw.\r\n private grabbing: number | null = null;\r\n private readonly grabStartQuat = new THREE.Quaternion();\r\n private readonly prevGrabQuat = new THREE.Quaternion();\r\n private readonly grabQuat = new THREE.Quaternion();\r\n private readonly deltaQuat = new THREE.Quaternion();\r\n private readonly grabEuler = new THREE.Euler(0, 0, 0, 'YXZ');\r\n private readonly controllers: THREE.Group[] = [];\r\n private readonly lasers: THREE.Line[] = [];\r\n private readonly connected: boolean[] = []; // per slot: is a real tracked-pointer bound?\r\n private readonly cleanups: (() => void)[] = [];\r\n\r\n private visible = false;\r\n private hover: VRRegion | null = null;\r\n private paintKey = '';\r\n private idleAt = 0;\r\n private togglePrev = false;\r\n private placeFrames = 0; // re-lock the panel to the head pose for a few frames after summon\r\n\r\n constructor(opts: { renderer: THREE.WebGLRenderer; scene: THREE.Scene; actions: VRControlsActions }) {\r\n this.renderer = opts.renderer;\r\n this.scene = opts.scene;\r\n this.actions = opts.actions;\r\n\r\n this.canvas = document.createElement('canvas');\r\n this.canvas.width = PANEL_W; this.canvas.height = PANEL_H;\r\n this.ctx = this.canvas.getContext('2d')!;\r\n\r\n this.texture = new THREE.CanvasTexture(this.canvas);\r\n this.texture.colorSpace = THREE.SRGBColorSpace;\r\n this.texture.minFilter = THREE.LinearFilter;\r\n\r\n const pw = 1.0, ph = (pw * PANEL_H) / PANEL_W; // ~3:1, ~35° wide at 1.6 m\r\n this.panel = new THREE.Mesh(\r\n new THREE.PlaneGeometry(pw, ph),\r\n new THREE.MeshBasicMaterial({ map: this.texture, transparent: true, depthTest: false, side: THREE.DoubleSide }),\r\n );\r\n this.panel.renderOrder = 10; // always drawn on top of the video sphere\r\n this.panel.frustumCulled = false;\r\n this.panel.visible = false;\r\n this.scene.add(this.panel);\r\n\r\n this.cursor = new THREE.Mesh(\r\n new THREE.SphereGeometry(0.007, 16, 12),\r\n new THREE.MeshBasicMaterial({ color: 0xffffff, depthTest: false }),\r\n );\r\n this.cursor.renderOrder = 11; // on top of the panel\r\n this.cursor.frustumCulled = false;\r\n this.cursor.visible = false;\r\n this.scene.add(this.cursor);\r\n\r\n // Seed connected state from the session's current input sources (the 'connected'\r\n // event may have already fired before this object existed), then keep it live\r\n // via connect/disconnect. Only a bound tracked-pointer gets a laser — otherwise\r\n // the empty controller slot sits at the origin and draws a fixed phantom line.\r\n const session = this.renderer.xr.getSession();\r\n const trackedCount = session ? Array.from(session.inputSources).filter((s) => s.targetRayMode === 'tracked-pointer').length : 0;\r\n for (let i = 0; i < 2; i++) {\r\n const c = this.renderer.xr.getController(i);\r\n const laser = this.makeLaser();\r\n laser.visible = false;\r\n c.add(laser);\r\n this.scene.add(c);\r\n this.controllers.push(c);\r\n this.lasers.push(laser);\r\n this.connected.push(i < trackedCount);\r\n\r\n const select = () => this.onSelect(i);\r\n const grabStart = () => this.onGrabStart(i); // grip down: begin grab-rotate\r\n const grabEnd = () => this.onGrabEnd(i); // grip up: end grab, or toggle panel if it was a tap\r\n const connect = (e?: { data?: { targetRayMode?: string } }) => { this.connected[i] = !e?.data || e.data.targetRayMode === 'tracked-pointer'; };\r\n const disconnect = () => { this.connected[i] = false; };\r\n const ev = c as unknown as {\r\n addEventListener(t: string, l: (e?: { data?: { targetRayMode?: string } }) => void): void;\r\n removeEventListener(t: string, l: (e?: { data?: { targetRayMode?: string } }) => void): void;\r\n };\r\n ev.addEventListener('selectstart', select);\r\n ev.addEventListener('squeezestart', grabStart);\r\n ev.addEventListener('squeezeend', grabEnd);\r\n ev.addEventListener('connected', connect);\r\n ev.addEventListener('disconnected', disconnect);\r\n this.cleanups.push(() => {\r\n ev.removeEventListener('selectstart', select);\r\n ev.removeEventListener('squeezestart', grabStart);\r\n ev.removeEventListener('squeezeend', grabEnd);\r\n ev.removeEventListener('connected', connect);\r\n ev.removeEventListener('disconnected', disconnect);\r\n });\r\n }\r\n\r\n this.paint(true);\r\n // Not shown on entry — summoned by the grip button or by moving a controller.\r\n }\r\n\r\n private makeLaser(): THREE.Line {\r\n const geo = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -1)]);\r\n const mat = new THREE.LineBasicMaterial({ color: 0x4f8cff, transparent: true, opacity: 0.8 });\r\n return new THREE.Line(geo, mat);\r\n }\r\n\r\n /** Called once per frame from the render loop (before renderer.render). */\r\n update(time: number): void {\r\n if (!this.renderer.xr.getSession()) return;\r\n this.pollToggle();\r\n this.handleThumbstick();\r\n this.handleGrab();\r\n this.maybeRevealOnMotion(time);\r\n\r\n if (!this.visible) { for (const l of this.lasers) l.visible = false; this.cursor.visible = false; return; }\r\n\r\n // Re-lock to the head pose for a few frames after summon: the XR camera pose\r\n // isn't updated until renderer.render (which runs after this), so the pose read\r\n // on the very first frame is stale/identity. Settling over a few frames fixes it.\r\n if (this.placeFrames > 0) { this.place(); this.placeFrames--; }\r\n\r\n // Auto-hide after a spell of no aiming at the panel.\r\n if (this.idleAt && time > this.idleAt) { this.hide(); return; }\r\n\r\n // Raycast both controllers; the one pointing at the panel wins the hover.\r\n let hover: VRRegion | null = null;\r\n let cursorAt: THREE.Vector3 | null = null;\r\n for (let i = 0; i < this.controllers.length; i++) {\r\n const laser = this.lasers[i];\r\n const controller = this.controllers[i];\r\n // A controller slot with no bound input source is left at the world origin,\r\n // so its laser would draw as a fixed phantom line. Require both a live\r\n // connection and a real pose (off the origin) before drawing/raycasting it.\r\n const posed = this.tmpVec.setFromMatrixPosition(controller.matrixWorld).lengthSq() > 1e-6;\r\n if (!this.connected[i] || !posed) { laser.visible = false; continue; }\r\n const r = this.rayHit(controller);\r\n // Draw a laser only when it actually meets the panel. That's the aiming feedback\r\n // the user needs, and it means a controller that's set down (still a live input\r\n // source at its resting pose, but not aimed at the panel) never draws a phantom line.\r\n if (r.point) {\r\n laser.visible = true;\r\n laser.scale.z = r.distance;\r\n cursorAt = r.point; // laser is on the panel — show the dot there\r\n if (r.hit) hover = r.hit.region;\r\n } else {\r\n laser.visible = false;\r\n }\r\n }\r\n if (cursorAt) { this.cursor.position.copy(cursorAt); this.cursor.visible = true; }\r\n else this.cursor.visible = false;\r\n if (hover) this.idleAt = time + IDLE_HIDE_MS; // stay up while actively aimed at\r\n this.hover = hover;\r\n this.paint(false);\r\n }\r\n\r\n /** Ray from a controller against the panel: the region under it (if any) and\r\n * how far the laser reaches. */\r\n private rayHit(controller: THREE.Group): { hit: ReturnType<typeof hitTest>; distance: number; point: THREE.Vector3 | null } {\r\n this.tmpMatrix.identity().extractRotation(controller.matrixWorld);\r\n this.raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);\r\n this.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(this.tmpMatrix);\r\n const hits = this.raycaster.intersectObject(this.panel, false);\r\n if (!hits.length) return { hit: null, distance: 5, point: null };\r\n const h = hits[0];\r\n const hit = h.uv ? hitTest(h.uv.x * PANEL_W, (1 - h.uv.y) * PANEL_H, this.layout) : null;\r\n return { hit, distance: h.distance, point: h.point };\r\n }\r\n\r\n private onSelect(index: number): void {\r\n if (!this.visible || !this.connected[index]) return;\r\n const { hit } = this.rayHit(this.controllers[index]);\r\n if (!hit) return;\r\n this.idleAt = performanceNow() + IDLE_HIDE_MS;\r\n switch (hit.region) {\r\n case 'play': this.actions.togglePlay(); break;\r\n case 'exit': this.actions.exitVR(); break;\r\n case 'passthrough': if (this.actions.passthroughAvailable()) this.actions.togglePassthrough(); break;\r\n case 'projPrev': this.actions.cycleProjection(-1); break;\r\n case 'projNext': this.actions.cycleProjection(1); break;\r\n case 'recenter': this.actions.recenter(); this.placeFrames = 12; break; // re-place the panel in the new frame\r\n\r\n case 'seek': if (hit.value !== undefined) this.actions.seekFraction(hit.value); break;\r\n case 'volume':\r\n if (hit.value !== undefined) this.actions.setVolume(hit.value);\r\n else this.actions.toggleMute();\r\n break;\r\n }\r\n this.paint(true);\r\n }\r\n\r\n /** Re-summon the panel when a connected controller is deliberately moved (and\r\n * we're past the post-hide cooldown). Tracks each controller's pose per frame. */\r\n private maybeRevealOnMotion(time: number): void {\r\n if (this.grabbing !== null) return; // don't pop the panel while grab-rotating\r\n for (let i = 0; i < this.controllers.length; i++) {\r\n const c = this.controllers[i];\r\n const p = this.tmpVec.setFromMatrixPosition(c.matrixWorld);\r\n if (!this.connected[i] || p.lengthSq() <= 1e-6) continue; // skip unbound/unposed slots\r\n const q = this.tmpQuat.setFromRotationMatrix(c.matrixWorld);\r\n const prevQ = this.prevQuat[i], prevP = this.prevPos[i];\r\n if (prevQ && prevP && !this.visible && time > this.revealCooldownUntil\r\n && (q.angleTo(prevQ) > MOTION_ROT || p.distanceTo(prevP) > MOTION_POS)) {\r\n this.show();\r\n }\r\n if (!this.prevQuat[i]) this.prevQuat[i] = new THREE.Quaternion();\r\n if (!this.prevPos[i]) this.prevPos[i] = new THREE.Vector3();\r\n this.prevQuat[i].copy(q);\r\n this.prevPos[i].copy(p);\r\n }\r\n }\r\n\r\n /** Right thumbstick, applied every frame it's held: X zooms the content fov (push right =\r\n * zoom in), Y tilts the content pitch (push up = tilt up). The grip grab-rotate also does\r\n * pitch/yaw; the two coexist. */\r\n private handleThumbstick(): void {\r\n const session = this.renderer.xr.getSession();\r\n if (!session) return;\r\n const ramp = (v: number) => Math.sign(v) * (Math.abs(v) - STICK_DEADZONE) / (1 - STICK_DEADZONE);\r\n for (const src of session.inputSources) {\r\n if (src.handedness !== 'right' || !src.gamepad) continue;\r\n const ax = src.gamepad.axes;\r\n // xr-standard: thumbstick X/Y at axes[2]/[3]; fall back to [0]/[1] on 2-axis runtimes.\r\n const x = ax.length >= 4 ? (ax[2] ?? 0) : (ax[0] ?? 0);\r\n const y = ax.length >= 4 ? (ax[3] ?? 0) : (ax[1] ?? 0);\r\n if (Math.abs(x) > STICK_DEADZONE) this.actions.adjustZoom(ramp(x) * ZOOM_RATE);\r\n // Stick up reads as negative Y; tilt up = positive delta (matches the grab-rotate sign).\r\n if (Math.abs(y) > STICK_DEADZONE) this.actions.adjustTilt(-ramp(y) * TILT_RATE);\r\n break;\r\n }\r\n }\r\n\r\n private onGrabStart(i: number): void {\r\n if (!this.connected[i]) return;\r\n this.grabbing = i;\r\n this.controllers[i].getWorldQuaternion(this.grabStartQuat);\r\n this.prevGrabQuat.copy(this.grabStartQuat);\r\n }\r\n\r\n private onGrabEnd(i: number): void {\r\n if (this.grabbing !== i) return;\r\n this.grabbing = null;\r\n this.revealCooldownUntil = performanceNow() + REVEAL_COOLDOWN_MS;\r\n // A grip barely turned is a tap -> toggle the panel; a real turn was a grab.\r\n if (this.controllers[i].getWorldQuaternion(this.grabQuat).angleTo(this.grabStartQuat) < GRAB_TAP_ANGLE) this.toggle();\r\n }\r\n\r\n /** While the grip is held, apply the controller's frame-to-frame rotation to the\r\n * content's yaw and pitch — a 1:1 grab-and-turn. Roll is ignored. */\r\n private handleGrab(): void {\r\n if (this.grabbing === null) return;\r\n if (!this.connected[this.grabbing]) { this.grabbing = null; return; }\r\n const cur = this.controllers[this.grabbing].getWorldQuaternion(this.grabQuat);\r\n this.deltaQuat.copy(this.prevGrabQuat).invert().premultiply(cur); // cur * prev⁻¹ (world-space delta)\r\n this.grabEuler.setFromQuaternion(this.deltaQuat, 'YXZ');\r\n this.actions.adjustYaw(this.grabEuler.y);\r\n this.actions.adjustTilt(this.grabEuler.x);\r\n this.prevGrabQuat.copy(cur);\r\n }\r\n\r\n private pollToggle(): void {\r\n const session = this.renderer.xr.getSession();\r\n let pressed = false;\r\n for (const src of session?.inputSources ?? []) {\r\n const g = src.gamepad;\r\n if (g && TOGGLE_BUTTONS.some((b) => g.buttons[b]?.pressed)) pressed = true;\r\n }\r\n if (pressed && !this.togglePrev) this.toggle();\r\n this.togglePrev = pressed;\r\n }\r\n\r\n private toggle(): void { this.visible ? this.hide() : this.show(); }\r\n\r\n private show(): void {\r\n this.place();\r\n this.placeFrames = 12; // settle onto the real head pose over the next frames\r\n this.visible = true;\r\n this.panel.visible = true;\r\n this.idleAt = performanceNow() + IDLE_HIDE_MS;\r\n this.paint(true);\r\n }\r\n\r\n private hide(): void {\r\n this.visible = false;\r\n this.panel.visible = false;\r\n this.cursor.visible = false;\r\n for (const l of this.lasers) l.visible = false;\r\n this.revealCooldownUntil = performanceNow() + REVEAL_COOLDOWN_MS;\r\n }\r\n\r\n /** World-lock the panel in front of the current head pose (the \"recenter\"). Uses\r\n * yaw only (so head pitch/roll at summon doesn't skew it), drops it into the lower\r\n * field of view, and tilts its face up toward the viewer. Re-run on every summon. */\r\n private place(): void {\r\n const cam = this.renderer.xr.getCamera();\r\n const pos = cam.getWorldPosition(new THREE.Vector3());\r\n const quat = cam.getWorldQuaternion(new THREE.Quaternion());\r\n const yaw = new THREE.Euler().setFromQuaternion(quat, 'YXZ').y;\r\n const yawQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, yaw, 0, 'YXZ'));\r\n const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(yawQuat);\r\n this.panel.position.copy(pos).addScaledVector(forward, 1.5);\r\n this.panel.position.y -= 0.55; // near the lower field of view\r\n this.panel.quaternion.copy(yawQuat); // plane front (+Z) faces back toward the viewer\r\n this.panel.rotateX(-0.3); // sitting low, angle the face up toward the eyes\r\n }\r\n\r\n /** Trim text with a trailing ellipsis to fit maxWidth. Assumes ctx.font is set. */\r\n private ellipsize(text: string, maxWidth: number): string {\r\n const c = this.ctx;\r\n if (c.measureText(text).width <= maxWidth) return text;\r\n let t = text;\r\n while (t.length > 1 && c.measureText(t + '…').width > maxWidth) t = t.slice(0, -1);\r\n return t.replace(/\\s+$/, '') + '…';\r\n }\r\n\r\n private roundRect(x: number, y: number, w: number, h: number, r: number): void {\r\n const c = this.ctx;\r\n c.beginPath();\r\n c.moveTo(x + r, y);\r\n c.arcTo(x + w, y, x + w, y + h, r);\r\n c.arcTo(x + w, y + h, x, y + h, r);\r\n c.arcTo(x, y + h, x, y, r);\r\n c.arcTo(x, y, x + w, y, r);\r\n c.closePath();\r\n }\r\n\r\n /** Repaint the panel only when something visible changed (state or hover). */\r\n private paint(force: boolean): void {\r\n const cur = this.actions.currentTime(), dur = this.actions.duration();\r\n const vol = this.actions.volume(), muted = this.actions.muted();\r\n const ptOn = this.actions.passthroughAvailable() && this.actions.passthroughEnabled();\r\n const key = [this.actions.isPlaying(), Math.floor(cur), Math.floor(dur), vol.toFixed(2), muted, this.hover, this.actions.title(), this.actions.passthroughAvailable(), ptOn, this.actions.projectionLabel()].join('|');\r\n if (!force && key === this.paintKey) return;\r\n this.paintKey = key;\r\n\r\n const c = this.ctx, L = this.layout;\r\n c.clearRect(0, 0, PANEL_W, PANEL_H);\r\n\r\n // Slab\r\n this.roundRect(0, 0, PANEL_W, PANEL_H, 28);\r\n c.fillStyle = 'rgba(22,24,30,0.84)'; c.fill();\r\n c.lineWidth = 2; c.strokeStyle = 'rgba(255,255,255,0.08)'; c.stroke();\r\n\r\n // Title, centered and truncated with an ellipsis so it never runs into the Exit button\r\n const title = this.actions.title();\r\n if (title) {\r\n c.font = '600 30px system-ui,\"Segoe UI\",Roboto,sans-serif';\r\n c.fillStyle = TEXT; c.textAlign = 'center'; c.textBaseline = 'middle';\r\n const maxW = 2 * L.exit.x - 48 - PANEL_W; // keep the centered title clear of the top-right pill\r\n c.fillText(this.ellipsize(title, maxW), PANEL_W / 2, L.title.y + L.title.h / 2);\r\n }\r\n\r\n // Top-row icon buttons: recenter (reticle) and exit (door-arrow) are momentary (accent on\r\n // hover); passthrough (eye, slashed when off) is a toggle (accent when on, border on hover).\r\n this.drawIconButton(L.recenter, this.hover === 'recenter', false, (x, y, col) => this.iconRecenter(x, y, col));\r\n this.drawIconButton(L.exit, this.hover === 'exit', false, (x, y, col) => this.iconExit(x, y, col));\r\n if (this.actions.passthroughAvailable()) {\r\n const on = this.actions.passthroughEnabled();\r\n this.drawIconButton(L.passthrough, on, this.hover === 'passthrough', (x, y, col) => this.iconEye(x, y, !on, col));\r\n }\r\n\r\n // Play / pause\r\n const pc = { x: L.play.x + L.play.w / 2, y: L.play.y + L.play.h / 2 };\r\n c.beginPath(); c.arc(pc.x, pc.y, 44, 0, Math.PI * 2);\r\n c.fillStyle = this.hover === 'play' ? ACCENT : 'rgba(255,255,255,0.14)'; c.fill();\r\n c.fillStyle = '#fff';\r\n if (this.actions.isPlaying()) {\r\n c.fillRect(pc.x - 13, pc.y - 16, 9, 32);\r\n c.fillRect(pc.x + 4, pc.y - 16, 9, 32);\r\n } else {\r\n c.beginPath(); c.moveTo(pc.x - 12, pc.y - 17); c.lineTo(pc.x - 12, pc.y + 17); c.lineTo(pc.x + 18, pc.y); c.closePath(); c.fill();\r\n }\r\n\r\n // Volume: speaker + track\r\n this.drawSpeaker(L.volIcon, muted);\r\n const volFrac = muted ? 0 : Math.max(0, Math.min(1, vol));\r\n this.drawBar(L.volBar, volFrac, this.hover === 'volume');\r\n\r\n // Projection stepper (right): caption + ◀ [label] ▶\r\n const cxp = L.projLabel.x + L.projLabel.w / 2;\r\n c.fillStyle = MUTED_TEXT; c.font = '600 15px system-ui,sans-serif'; c.textAlign = 'center'; c.textBaseline = 'alphabetic';\r\n c.fillText('PROJECTION', cxp, L.projPrev.y - 8);\r\n this.drawIconButton(L.projPrev, this.hover === 'projPrev', false, (x, y, col) => this.iconArrow(x, y, -1, col));\r\n this.drawIconButton(L.projNext, this.hover === 'projNext', false, (x, y, col) => this.iconArrow(x, y, 1, col));\r\n c.fillStyle = TEXT; c.font = '600 26px system-ui,sans-serif'; c.textAlign = 'center'; c.textBaseline = 'middle';\r\n c.fillText(this.ellipsize(this.actions.projectionLabel(), L.projLabel.w - 12), cxp, L.projLabel.y + L.projLabel.h / 2);\r\n\r\n // Seek + times\r\n const seekFrac = dur > 0 ? Math.max(0, Math.min(1, cur / dur)) : 0;\r\n this.drawBar(L.seekBar, seekFrac, this.hover === 'seek', true);\r\n c.fillStyle = MUTED_TEXT; c.font = '22px system-ui,sans-serif'; c.textBaseline = 'alphabetic';\r\n c.textAlign = 'left'; c.fillText(formatTime(cur), L.timeCur.x, L.timeCur.y);\r\n c.textAlign = 'right'; c.fillText(dur > 0 ? formatTime(dur) : '--:--', L.timeDur.x, L.timeDur.y);\r\n\r\n this.texture.needsUpdate = true;\r\n }\r\n\r\n /** A rounded-square icon button: accent fill when `filled`, white border when `hover`; the\r\n * icon is drawn centred in `filled ? white : TEXT`. */\r\n private drawIconButton(r: Rect, filled: boolean, hover: boolean, draw: (cx: number, cy: number, color: string) => void): void {\r\n const c = this.ctx;\r\n this.roundRect(r.x, r.y, r.w, r.h, 14);\r\n c.fillStyle = filled ? ACCENT : 'rgba(255,255,255,0.12)'; c.fill();\r\n if (hover) { c.lineWidth = 3; c.strokeStyle = 'rgba(255,255,255,0.9)'; c.stroke(); }\r\n draw(r.x + r.w / 2, r.y + r.h / 2, filled ? '#fff' : TEXT);\r\n }\r\n\r\n /** Recenter — a reticle (ring + centre dot + crosshair ticks). */\r\n private iconRecenter(cx: number, cy: number, color: string): void {\r\n const c = this.ctx;\r\n c.strokeStyle = color; c.fillStyle = color; c.lineWidth = 2.5; c.lineCap = 'round';\r\n c.beginPath(); c.arc(cx, cy, 9, 0, Math.PI * 2); c.stroke();\r\n c.beginPath(); c.arc(cx, cy, 2.4, 0, Math.PI * 2); c.fill();\r\n c.beginPath();\r\n c.moveTo(cx, cy - 16); c.lineTo(cx, cy - 6);\r\n c.moveTo(cx, cy + 6); c.lineTo(cx, cy + 16);\r\n c.moveTo(cx - 16, cy); c.lineTo(cx - 6, cy);\r\n c.moveTo(cx + 6, cy); c.lineTo(cx + 16, cy);\r\n c.stroke();\r\n }\r\n\r\n /** Passthrough — an eye; a diagonal slash across it means passthrough is off. */\r\n private iconEye(cx: number, cy: number, off: boolean, color: string): void {\r\n const c = this.ctx, w = 16, h = 10;\r\n c.strokeStyle = color; c.fillStyle = color; c.lineWidth = 2.5; c.lineCap = 'round';\r\n c.beginPath();\r\n c.moveTo(cx - w, cy);\r\n c.quadraticCurveTo(cx, cy - h * 1.7, cx + w, cy);\r\n c.quadraticCurveTo(cx, cy + h * 1.7, cx - w, cy);\r\n c.stroke();\r\n c.beginPath(); c.arc(cx, cy, 4, 0, Math.PI * 2); c.fill();\r\n if (off) { c.beginPath(); c.moveTo(cx - w, cy + h); c.lineTo(cx + w, cy - h); c.stroke(); }\r\n }\r\n\r\n /** A solid triangle pointing left (dir −1) or right (dir +1) — the projection stepper arrows. */\r\n private iconArrow(cx: number, cy: number, dir: number, color: string): void {\r\n const c = this.ctx, w = 8, h = 10;\r\n c.fillStyle = color;\r\n c.beginPath();\r\n c.moveTo(cx + dir * w, cy);\r\n c.lineTo(cx - dir * w, cy - h);\r\n c.lineTo(cx - dir * w, cy + h);\r\n c.closePath(); c.fill();\r\n }\r\n\r\n /** Exit — a door frame with an arrow pointing out to the right. */\r\n private iconExit(cx: number, cy: number, color: string): void {\r\n const c = this.ctx, dx = cx - 15, dw = 11, dh = 22, a1 = cx + 16;\r\n c.strokeStyle = color; c.lineWidth = 2.5; c.lineCap = 'round'; c.lineJoin = 'round';\r\n c.beginPath();\r\n c.moveTo(dx + dw, cy - dh / 2); c.lineTo(dx, cy - dh / 2);\r\n c.lineTo(dx, cy + dh / 2); c.lineTo(dx + dw, cy + dh / 2);\r\n c.stroke();\r\n c.beginPath();\r\n c.moveTo(cx - 3, cy); c.lineTo(a1, cy);\r\n c.moveTo(a1 - 7, cy - 6); c.lineTo(a1, cy); c.lineTo(a1 - 7, cy + 6);\r\n c.stroke();\r\n }\r\n\r\n private drawBar(r: { x: number; y: number; w: number; h: number }, frac: number, hovered: boolean, knob = false): void {\r\n const c = this.ctx, rad = r.h / 2;\r\n this.roundRect(r.x, r.y, r.w, r.h, rad); c.fillStyle = 'rgba(255,255,255,0.22)'; c.fill();\r\n if (frac > 0) { this.roundRect(r.x, r.y, r.w * frac, r.h, rad); c.fillStyle = ACCENT; c.fill(); }\r\n if (knob || hovered) {\r\n c.beginPath(); c.arc(r.x + r.w * frac, r.y + r.h / 2, hovered ? 12 : 9, 0, Math.PI * 2);\r\n c.fillStyle = '#fff'; c.fill();\r\n }\r\n }\r\n\r\n private drawSpeaker(r: { x: number; y: number; w: number; h: number }, muted: boolean): void {\r\n const c = this.ctx, x = r.x, y = r.y + r.h / 2;\r\n c.fillStyle = muted ? MUTED_TEXT : TEXT;\r\n c.beginPath();\r\n c.moveTo(x, y - 7); c.lineTo(x + 10, y - 7); c.lineTo(x + 20, y - 16);\r\n c.lineTo(x + 20, y + 16); c.lineTo(x + 10, y + 7); c.lineTo(x, y + 7); c.closePath(); c.fill();\r\n c.strokeStyle = muted ? '#e06a6a' : TEXT; c.lineWidth = 3;\r\n if (muted) {\r\n c.beginPath(); c.moveTo(x + 26, y - 9); c.lineTo(x + 40, y + 9); c.moveTo(x + 40, y - 9); c.lineTo(x + 26, y + 9); c.stroke();\r\n } else {\r\n c.beginPath(); c.arc(x + 24, y, 8, -Math.PI / 3, Math.PI / 3); c.stroke();\r\n c.beginPath(); c.arc(x + 24, y, 15, -Math.PI / 3, Math.PI / 3); c.stroke();\r\n }\r\n }\r\n\r\n dispose(): void {\r\n for (const fn of this.cleanups) fn();\r\n for (let i = 0; i < this.controllers.length; i++) {\r\n const c = this.controllers[i];\r\n c.remove(this.lasers[i]);\r\n this.lasers[i].geometry.dispose();\r\n (this.lasers[i].material as THREE.Material).dispose();\r\n this.scene.remove(c);\r\n }\r\n this.scene.remove(this.panel);\r\n this.panel.geometry.dispose();\r\n (this.panel.material as THREE.Material).dispose();\r\n this.scene.remove(this.cursor);\r\n this.cursor.geometry.dispose();\r\n (this.cursor.material as THREE.Material).dispose();\r\n this.texture.dispose();\r\n }\r\n}\r\n\r\n/** performance.now(), but tolerant of environments without it (tests). */\r\nfunction performanceNow(): number {\r\n return typeof performance !== 'undefined' ? performance.now() : 0;\r\n}\r\n","import * as THREE from 'three';\r\nimport type { Projection } from '../types.js';\r\nimport { MODES, PROJECTIONS, PROJECTION_SHORT } from './projections.js';\r\nimport { VRControls } from './VRControls.js';\r\n\r\n\r\n/**\r\n * Maps a video onto the geometry for a chosen projection (inside-out 180/360\r\n * sphere or a flat screen plane). Stereo is done the way the reference VR180\r\n * players do it: a single mesh, with the shared video texture shifted per eye\r\n * in onBeforeRender (no per-eye meshes or camera layers). Sizes to its canvas's\r\n * container (not the window), for embedding.\r\n */\r\nexport class StereoScene {\r\n readonly renderer: THREE.WebGLRenderer;\r\n readonly scene = new THREE.Scene();\r\n readonly camera: THREE.PerspectiveCamera;\r\n readonly maxAnisotropy: number;\r\n\r\n private readonly canvas: HTMLCanvasElement;\r\n private readonly video: HTMLVideoElement;\r\n private readonly texture: THREE.VideoTexture;\r\n private readonly meshes: THREE.Mesh[] = [];\r\n // Holds the video mesh(es); tilt (pitch) and zoom (scale) are applied here so the\r\n // in-VR panel/controllers (added straight to the scene) aren't affected.\r\n private readonly rig = new THREE.Group();\r\n private tilt = 0; // radians (rig pitch)\r\n private fov = 1; // content field-of-view factor: 1 = full immersive, <1 = smaller/farther window\r\n private builtFov = 1; // fov the current geometry was built at (rebuild throttle)\r\n private readonly frameCbs: (() => void)[] = [];\r\n private alphaFisheye = false; // content has a DeoVR-style packed alpha matte\r\n private inPassthroughSession = false; // current XR session is immersive-ar (non-opaque blend)\r\n private passthroughOn = true; // passthrough on = real world visible (alpha content also keys the subject)\r\n // Uniforms shared with the alpha-fisheye shader; uPtShift/uTexel updated each eye in updateStereoUV.\r\n private alphaUniforms?: { uPtShift: { value: number }; uTexel: { value: THREE.Vector2 }; uAlphaEnabled: { value: number } };\r\n private vrControls?: VRControls;\r\n private readonly animate = (time?: number) => {\r\n for (const cb of this.frameCbs) cb();\r\n this.vrControls?.update(time ?? 0);\r\n // Upload the current video frame synchronously, before the render, every frame. This\r\n // is load-bearing on the Quest: without it, VideoTexture's own async (video-frame-\r\n // callback) upload can land mid-render during an XR eye pass and corrupt the triangles\r\n // being drawn — the \"hard triangle edges popping to black\". Reference VR180 players do\r\n // the same. (Yes, it re-uploads at headset rate; that's the cost of a stable image.)\r\n if (this.video.readyState >= this.video.HAVE_CURRENT_DATA) this.texture.needsUpdate = true;\r\n this.renderer.render(this.scene, this.camera);\r\n };\r\n private readonly ro: ResizeObserver;\r\n private currentMode: Projection;\r\n private currentSwap: boolean;\r\n // local-floor as OPTIONAL (widest device support — don't hard-fail headsets without it).\r\n // Deliberately NOT requesting 'layers': it flips three.js to an XRProjectionLayer depth\r\n // path that flickers on the Quest.\r\n private readonly vrSessionInit: XRSessionInit = { optionalFeatures: ['local-floor'] };\r\n // immersive-ar for passthrough. local-floor optional (widest support); the UA turns on\r\n // camera passthrough (alpha-blend / additive blend mode) for the session automatically.\r\n private readonly arSessionInit: XRSessionInit = { optionalFeatures: ['local-floor'] };\r\n\r\n constructor(opts: {\r\n canvas: HTMLCanvasElement; video: HTMLVideoElement;\r\n projection?: Projection; swapEyes?: boolean; fov?: number; supersampling?: number;\r\n }) {\r\n const { canvas, video, projection = '180-sbs', swapEyes = false, fov = 70, supersampling = 1.5 } = opts;\r\n this.canvas = canvas;\r\n this.video = video;\r\n this.currentMode = MODES[projection] ? projection : '180-sbs';\r\n this.currentSwap = swapEyes;\r\n\r\n // alpha:true so the framebuffer can be transparent for immersive-ar passthrough. Keep the\r\n // clear fully opaque black otherwise (VR + desktop are unaffected); AR flips clearAlpha to 0.\r\n this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });\r\n this.renderer.setClearColor(0x000000, 1);\r\n this.renderer.setPixelRatio(this.pixelRatioFor(supersampling));\r\n this.renderer.setSize(this.w(), this.h(), false);\r\n this.renderer.xr.enabled = true;\r\n\r\n this.camera = new THREE.PerspectiveCamera(fov, this.w() / this.h(), 0.1, 1000);\r\n this.camera.position.set(0, 0, 0);\r\n\r\n this.maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy();\r\n this.texture = new THREE.VideoTexture(video);\r\n this.texture.colorSpace = THREE.SRGBColorSpace;\r\n this.texture.minFilter = THREE.LinearFilter;\r\n this.texture.magFilter = THREE.LinearFilter;\r\n // Keep anisotropy at the default (1). Cranking it to max makes the equirect video\r\n // sphere shimmer/flicker at grazing angles (the upper part of the view) once the head\r\n // moves in VR — the reference VR180 players leave it at the default. maxAnisotropy is\r\n // still exposed on the instance for consumers who want to opt back in.\r\n this.texture.anisotropy = 1;\r\n\r\n this.rig.rotation.order = 'YXZ'; // yaw then pitch, so grab-rotate composes naturally\r\n this.scene.add(this.rig);\r\n this.applyProjection(this.currentMode, this.currentSwap);\r\n video.addEventListener('loadedmetadata', () => {\r\n if (MODES[this.currentMode].flat) this.applyProjection(this.currentMode, this.currentSwap);\r\n });\r\n\r\n this.renderer.setAnimationLoop(this.animate);\r\n\r\n // In-VR controls live only for the duration of an immersive session: the DOM\r\n // control bar isn't visible inside the headset, so we draw a panel in the scene.\r\n this.renderer.xr.addEventListener('sessionstart', () => {\r\n // Passthrough sessions (immersive-ar) report a non-opaque blend mode; start with\r\n // passthrough on (transparent clear + keyed matte) so the real world shows through.\r\n const blend = (this.renderer.xr.getSession() as unknown as { environmentBlendMode?: string })?.environmentBlendMode;\r\n this.inPassthroughSession = !!(blend && blend !== 'opaque');\r\n if (this.inPassthroughSession) this.setPassthrough(true);\r\n this.buildVRControls();\r\n });\r\n this.renderer.xr.addEventListener('sessionend', () => {\r\n this.renderer.setClearAlpha(1); // restore opaque clear for desktop / next VR session\r\n this.inPassthroughSession = false;\r\n this.vrControls?.dispose(); this.vrControls = undefined;\r\n this.video.pause(); // stop playback when leaving VR\r\n });\r\n\r\n this.ro = new ResizeObserver(() => this.resize());\r\n this.ro.observe(canvas);\r\n }\r\n\r\n private buildVRControls() {\r\n const v = this.video;\r\n this.vrControls = new VRControls({\r\n renderer: this.renderer,\r\n scene: this.scene,\r\n actions: {\r\n isPlaying: () => !v.paused,\r\n currentTime: () => v.currentTime,\r\n duration: () => v.duration || 0,\r\n volume: () => v.volume,\r\n muted: () => v.muted,\r\n title: () => this.vrTitle,\r\n togglePlay: () => { if (v.paused) void v.play(); else v.pause(); },\r\n seekFraction: (f) => { if (v.duration) v.currentTime = f * v.duration; },\r\n setVolume: (x) => { v.volume = x; if (x > 0) v.muted = false; },\r\n toggleMute: () => { v.muted = !v.muted; },\r\n exitVR: () => this.exitVR(),\r\n recenter: () => this.recenter(),\r\n adjustTilt: (d) => this.adjustTilt(d),\r\n adjustYaw: (d) => this.adjustYaw(d),\r\n adjustZoom: (d) => this.adjustZoom(d),\r\n passthroughAvailable: () => this.inPassthroughSession, // any AR session, not just alpha content\r\n passthroughEnabled: () => this.passthroughOn,\r\n togglePassthrough: () => this.setPassthrough(!this.passthroughOn),\r\n projectionLabel: () => this.projectionLabel(),\r\n cycleProjection: (d) => this.cycleProjection(d),\r\n },\r\n });\r\n }\r\n\r\n /** Recenter the view: make the viewer's current spot and facing the origin, so the\r\n * video front is straight ahead again. Resets yaw and horizontal position; keeps\r\n * head height and a level horizon (no pitch/roll). */\r\n recenter(): void {\r\n const xr = this.renderer.xr;\r\n if (!xr.isPresenting) return;\r\n const baseRef = xr.getReferenceSpace();\r\n if (!baseRef) return;\r\n const cam = xr.getCamera();\r\n const pos = cam.getWorldPosition(new THREE.Vector3());\r\n const yaw = new THREE.Euler().setFromQuaternion(cam.getWorldQuaternion(new THREE.Quaternion()), 'YXZ').y;\r\n const q = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, yaw, 0, 'YXZ'));\r\n const offset = new XRRigidTransform({ x: pos.x, y: 0, z: pos.z }, { x: q.x, y: q.y, z: q.z, w: q.w });\r\n xr.setReferenceSpace(baseRef.getOffsetReferenceSpace(offset));\r\n }\r\n\r\n /** Optional title shown on the in-VR control panel. */\r\n setVRTitle(t: string) { this.vrTitle = t; }\r\n private vrTitle = '';\r\n\r\n private w() { return this.canvas.clientWidth || 1; }\r\n private h() { return this.canvas.clientHeight || 1; }\r\n private pixelRatioFor(ss: number) { return Math.min(window.devicePixelRatio * ss, 4); }\r\n\r\n private planeAspect(mode: Projection) {\r\n const vw = this.video.videoWidth, vh = this.video.videoHeight;\r\n if (!vw || !vh) return 16 / 9;\r\n return MODES[mode].aspect === 'per-eye' ? (vw / 2) / vh : vw / vh;\r\n }\r\n\r\n private buildGeometry(mode: Projection): THREE.BufferGeometry {\r\n // 64×32 sphere, front gore centred (reference VR180 geometry). The zoom `fov` factor\r\n // shrinks the angular cap the video occupies (and the flat plane's size) so the\r\n // scene recedes into a smaller window and feels farther; fov = 1 is the full frame.\r\n const kind = MODES[mode].geom, f = this.fov;\r\n if (kind === 'sphere180') {\r\n const phiLen = Math.PI * f, thetaLen = Math.PI * f;\r\n const g = new THREE.SphereGeometry(50, 64, 32, -phiLen / 2, phiLen, Math.PI / 2 - thetaLen / 2, thetaLen);\r\n g.scale(-1, 1, 1); return g;\r\n }\r\n if (kind === 'sphere360') {\r\n const phiLen = 2 * Math.PI * f, thetaLen = Math.PI * f;\r\n const g = new THREE.SphereGeometry(50, 64, 32, Math.PI - phiLen / 2, phiLen, Math.PI / 2 - thetaLen / 2, thetaLen);\r\n g.scale(-1, 1, 1); return g;\r\n }\r\n if (kind === 'fisheye') return this.buildFisheyeDome();\r\n const h = 2.4 * f, w = h * this.planeAspect(mode);\r\n const g = new THREE.PlaneGeometry(w, h); g.translate(0, 0, -2); return g;\r\n }\r\n\r\n /**\r\n * A dome covering the fisheye field of view (FISHEYE190 ≈ 190° → a 95° half-angle cap in\r\n * front of the viewer), with equidistant-fisheye UVs: the angle from the forward axis maps\r\n * linearly to radius on the texture disc (centre 0.5,0.5, radius 0.5). UVs are authored in\r\n * full-frame [0,1] space; updateStereoUV's per-eye repeat/offset selects the L/R circle.\r\n * Forward is −z (the camera's default view direction), so no extra rotation is needed.\r\n */\r\n private buildFisheyeDome(halfAngleDeg = 95): THREE.BufferGeometry {\r\n const RINGS = 64, SECTORS = 128, R = 50;\r\n const thetaMax = THREE.MathUtils.degToRad(halfAngleDeg);\r\n const pos: number[] = [], uv: number[] = [], idx: number[] = [];\r\n for (let i = 0; i <= RINGS; i++) {\r\n const t = i / RINGS; // 0 at centre (dead ahead), 1 at the rim\r\n const theta = t * thetaMax;\r\n const discR = t * 0.5; // equidistant: disc radius ∝ angle from axis\r\n const sinT = Math.sin(theta), cosT = Math.cos(theta);\r\n for (let j = 0; j <= SECTORS; j++) {\r\n const phi = (j / SECTORS) * Math.PI * 2;\r\n const cp = Math.cos(phi), sp = Math.sin(phi);\r\n pos.push(sinT * cp * R, sinT * sp * R, -cosT * R); // forward = −z\r\n uv.push(0.5 + discR * cp, 0.5 + discR * sp);\r\n }\r\n }\r\n const stride = SECTORS + 1;\r\n for (let i = 0; i < RINGS; i++) {\r\n for (let j = 0; j < SECTORS; j++) {\r\n const a = i * stride + j, b = a + 1, c = a + stride, d = c + 1;\r\n idx.push(a, c, b, b, c, d);\r\n }\r\n }\r\n const g = new THREE.BufferGeometry();\r\n g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));\r\n g.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));\r\n g.setIndex(idx);\r\n g.computeVertexNormals();\r\n return g;\r\n }\r\n\r\n /**\r\n * Transparent material for fisheye content with a DeoVR-style packed alpha matte. Built by\r\n * patching MeshBasicMaterial (so three.js keeps handling colour management, the map sample and\r\n * flipY) and injecting DeoVR's exact alpha decode (ported from their WebXR player):\r\n *\r\n * ptUV = fract(vec2(discUV.x*0.2 + ptShift, discUV.y*0.4 + 0.8)); // ptShift 0.4 L / 0.9 R\r\n * alpha = smoothstep(0.0, 0.8, average of the 3×3 red neighbourhood at ptUV);\r\n *\r\n * The matte is a 0.4×-scaled copy of the disc's alpha stored per eye at that offset, wrapped\r\n * across the frame edges (hence it appears tucked into the corners). It's indexed by the SAME\r\n * disc UV as the colour, so it aligns exactly — including per-eye parallax. Outside the disc → clip.\r\n */\r\n private makeFisheyeAlphaMaterial(): THREE.MeshBasicMaterial {\r\n const mat = new THREE.MeshBasicMaterial({ map: this.texture, transparent: true, side: THREE.DoubleSide, depthWrite: false });\r\n const uniforms = {\r\n uPtShift: { value: 0.4 },\r\n uTexel: { value: new THREE.Vector2(1 / 8000, 1 / 4000) },\r\n uAlphaEnabled: { value: this.passthroughOn ? 1 : 0 }, // 0 → opaque dome (passthrough off)\r\n };\r\n this.alphaUniforms = uniforms;\r\n mat.onBeforeCompile = (shader) => {\r\n shader.uniforms.uPtShift = uniforms.uPtShift;\r\n shader.uniforms.uTexel = uniforms.uTexel;\r\n shader.uniforms.uAlphaEnabled = uniforms.uAlphaEnabled;\r\n shader.vertexShader = 'varying vec2 vDiscUv;\\n' +\r\n shader.vertexShader.replace('void main() {', 'void main() {\\n\\tvDiscUv = uv;');\r\n shader.fragmentShader =\r\n 'uniform float uPtShift;\\nuniform vec2 uTexel;\\nuniform float uAlphaEnabled;\\nvarying vec2 vDiscUv;\\n' +\r\n shader.fragmentShader.replace('#include <map_fragment>', `#include <map_fragment>\r\n {\r\n vec2 n = (vDiscUv - 0.5) * 2.0;\r\n if (dot(n, n) > 1.0) discard; // outside the fisheye circle\r\n vec2 mUv = fract(vec2(vDiscUv.x * 0.2 + uPtShift, vDiscUv.y * 0.4 + 0.8));\r\n float s = 0.0; // DeoVR getMask(): 3x3 red average\r\n for (int dy = -1; dy <= 1; dy++) {\r\n for (int dx = -1; dx <= 1; dx++) {\r\n s += texture2D(map, mUv + uTexel * vec2(float(dx), float(dy))).r;\r\n }\r\n }\r\n // uAlphaEnabled 1 → keyed matte (passthrough); 0 → fully opaque dome (no passthrough)\r\n diffuseColor.a *= mix(1.0, smoothstep(0.0, 0.8, s / 9.0), uAlphaEnabled);\r\n }`);\r\n };\r\n return mat;\r\n }\r\n\r\n /** Passthrough on/off (immersive-ar only): on = real world visible (transparent clear); off =\r\n * opaque black surround (VR-style). Alpha content additionally keys the subject via the matte;\r\n * without a matte the dome stays opaque and only the surround changes. */\r\n setPassthrough(on: boolean): void {\r\n this.passthroughOn = on;\r\n if (this.alphaUniforms) this.alphaUniforms.uAlphaEnabled.value = on ? 1 : 0;\r\n const blend = (this.renderer.xr.getSession() as unknown as { environmentBlendMode?: string })?.environmentBlendMode;\r\n if (blend && blend !== 'opaque') this.renderer.setClearAlpha(on ? 0 : 1);\r\n }\r\n\r\n /** Mark the current content as carrying a DeoVR-style packed alpha matte (fisheye only). */\r\n setAlphaMatte(on: boolean) {\r\n if (this.alphaFisheye === on) return;\r\n this.alphaFisheye = on;\r\n this.applyProjection(this.currentMode, this.currentSwap);\r\n }\r\n\r\n /** Shift the shared video texture for the eye/half being drawn. Runs from the mesh's\r\n * onBeforeRender, so it's called once per eye per frame (the reference approach). */\r\n private updateStereoUV(cam: THREE.Camera) {\r\n const map = this.texture;\r\n const split = MODES[this.currentMode].split;\r\n // Robust eye detection (the reference player's cascade): direct camera-ref match, then\r\n // view-matrix X, then projection asymmetry — so a frame never gets the wrong half.\r\n let isLeft = true;\r\n if (this.renderer.xr.isPresenting) {\r\n const c = cam as THREE.PerspectiveCamera;\r\n const cams = (this.renderer.xr.getCamera() as unknown as { cameras?: THREE.PerspectiveCamera[] }).cameras;\r\n if (cams && cams.length >= 2) {\r\n if (c === cams[0]) isLeft = true;\r\n else if (c === cams[1]) isLeft = false;\r\n else isLeft = Math.abs(c.matrixWorldInverse.elements[12] - cams[0].matrixWorldInverse.elements[12])\r\n <= Math.abs(c.matrixWorldInverse.elements[12] - cams[1].matrixWorldInverse.elements[12]);\r\n } else {\r\n isLeft = c.projectionMatrix.elements[8] <= 0;\r\n }\r\n }\r\n const second = !isLeft !== this.currentSwap; // this eye shows the 2nd half (right / bottom)\r\n if (split === 'sbs') { map.repeat.set(0.5, 1); map.offset.set(second ? 0.5 : 0, 0); }\r\n else if (split === 'tb') { map.repeat.set(1, 0.5); map.offset.set(0, second ? 0 : 0.5); }\r\n else { map.repeat.set(1, 1); map.offset.set(0, 0); }\r\n // Alpha-fisheye: each eye reads its own packed matte (x-offset 0.4 left / 0.9 right), and\r\n // the 3×3 getMask blur needs the video's texel size.\r\n if (this.alphaUniforms) {\r\n this.alphaUniforms.uPtShift.value = second ? 0.9 : 0.4;\r\n const vw = this.video.videoWidth, vh = this.video.videoHeight;\r\n if (vw && vh) this.alphaUniforms.uTexel.value.set(1 / vw, 1 / vh);\r\n }\r\n }\r\n\r\n private clearMeshes() {\r\n for (const m of this.meshes) { this.rig.remove(m); m.geometry.dispose(); (m.material as THREE.Material).dispose(); }\r\n this.meshes.length = 0;\r\n }\r\n\r\n private applyProjection(mode: Projection, swap: boolean) {\r\n if (!MODES[mode]) mode = '180-sbs';\r\n this.clearMeshes();\r\n this.currentMode = mode; this.currentSwap = swap;\r\n // One mesh (reference approach), plain MeshBasicMaterial. Stereo is handled per eye by\r\n // updateStereoUV via onBeforeRender — not by per-eye meshes or camera layers. Spheres are\r\n // scaled inside-out (FrontSide); the fisheye dome isn't, so it's viewed as DoubleSide.\r\n this.alphaUniforms = undefined;\r\n let material: THREE.Material;\r\n if (MODES[mode].geom === 'fisheye' && this.alphaFisheye) {\r\n material = this.makeFisheyeAlphaMaterial(); // transparent, reconstructs packed alpha\r\n } else {\r\n const side = MODES[mode].geom === 'fisheye' ? THREE.DoubleSide : THREE.FrontSide;\r\n material = new THREE.MeshBasicMaterial({ map: this.texture, side });\r\n }\r\n const mesh = new THREE.Mesh(this.buildGeometry(mode), material);\r\n mesh.layers.set(0); // desktop + both XR eyes see the single mesh\r\n if (MODES[mode].geom === 'sphere180') mesh.rotation.y = Math.PI / 2; // orient the 180 gore forward\r\n mesh.onBeforeRender = (_r, _s, cam) => this.updateStereoUV(cam);\r\n this.rig.add(mesh); this.meshes.push(mesh);\r\n }\r\n\r\n /** Pitch the video content up/down (radians, accumulated and clamped). */\r\n adjustTilt(delta: number) {\r\n this.tilt = Math.max(-0.9, Math.min(0.9, this.tilt + delta));\r\n this.rig.rotation.x = this.tilt;\r\n }\r\n /** Yaw the video content left/right (radians, accumulated, unclamped). */\r\n adjustYaw(delta: number) {\r\n this.rig.rotation.y += delta;\r\n }\r\n /** Zoom by changing the content field of view (positive delta = zoom in / more\r\n * immersive). Shrinking the fov maps the video into a smaller angular window so the\r\n * scene recedes and feels farther; growing it fills the view. Rebuilds the projection\r\n * geometry, throttled so it doesn't churn every frame. */\r\n adjustZoom(delta: number) {\r\n this.fov = Math.max(0.3, Math.min(1, this.fov + delta));\r\n if (Math.abs(this.fov - this.builtFov) >= 0.03) this.rebuildGeometry();\r\n }\r\n\r\n private rebuildGeometry() {\r\n for (const m of this.meshes) { const old = m.geometry; m.geometry = this.buildGeometry(this.currentMode); old.dispose(); }\r\n this.builtFov = this.fov;\r\n }\r\n\r\n setProjection(p: Projection) { this.applyProjection(p, this.currentSwap); }\r\n /** Player registers this so an in-VR projection change flows back through Player — keeping its\r\n * state, persistence, and the DOM controls in sync. Falls back to a direct geometry swap. */\r\n setProjectionRequester(cb: (p: Projection) => void) { this.projectionRequester = cb; }\r\n private projectionRequester?: (p: Projection) => void;\r\n /** Short label of the current projection, for the in-VR stepper. */\r\n projectionLabel(): string { return PROJECTION_SHORT[this.currentMode] ?? this.currentMode; }\r\n /** Step the projection by dir (±1) through the PROJECTIONS list (wraps around). */\r\n cycleProjection(dir: number): void {\r\n const list = PROJECTIONS.map((p) => p.value);\r\n const i = Math.max(0, list.indexOf(this.currentMode));\r\n const next = list[(i + dir + list.length) % list.length];\r\n (this.projectionRequester ?? ((p: Projection) => this.setProjection(p)))(next);\r\n }\r\n setSwapEyes(v: boolean) { this.applyProjection(this.currentMode, v); }\r\n setFov(deg: number) { this.camera.fov = deg; this.camera.updateProjectionMatrix(); }\r\n setSupersampling(ss: number) { this.renderer.setPixelRatio(this.pixelRatioFor(ss)); this.renderer.setSize(this.w(), this.h(), false); }\r\n getProjection() { return this.currentMode; }\r\n isFlat() { return !!MODES[this.currentMode].flat; }\r\n onFrame(cb: () => void) { this.frameCbs.push(cb); }\r\n pauseRendering() { this.renderer.setAnimationLoop(null); }\r\n resumeRendering() { this.renderer.setAnimationLoop(this.animate); }\r\n\r\n /** Enter immersive VR. Rejects with a readable message on failure so the caller can\r\n * surface it — instead of the silent no-op three.js's VRButton produces. Called from\r\n * a click handler so the request keeps the user's transient activation. */\r\n async enterVR(): Promise<void> {\r\n if (!navigator.xr) throw new Error('WebXR is unavailable here — open the page over HTTPS or localhost.');\r\n const session = await navigator.xr.requestSession('immersive-vr', this.vrSessionInit);\r\n await this.renderer.xr.setSession(session);\r\n }\r\n /** Enter immersive AR (passthrough). Same contract as enterVR — called from a click so it\r\n * keeps transient activation, rejects with a readable message. The UA composites our\r\n * transparent framebuffer over the camera feed. */\r\n async enterAR(): Promise<void> {\r\n if (!navigator.xr) throw new Error('WebXR is unavailable here — open the page over HTTPS or localhost.');\r\n const session = await navigator.xr.requestSession('immersive-ar', this.arSessionInit);\r\n await this.renderer.xr.setSession(session);\r\n }\r\n exitVR(): void {\r\n if (!this.video.paused) this.video.pause(); // pause first if playing, then leave VR\r\n void this.renderer.xr.getSession()?.end();\r\n }\r\n /** Arm the browser/headset's own \"Enter VR\" affordance (e.g. the Quest system button). */\r\n offerVR(): void {\r\n const offer = (navigator.xr as { offerSession?: (m: XRSessionMode, i: XRSessionInit) => Promise<XRSession> } | undefined)?.offerSession;\r\n if (!offer) return;\r\n const run = () => offer.call(navigator.xr, 'immersive-vr', this.vrSessionInit)\r\n .then((s) => this.renderer.xr.setSession(s))\r\n .catch(() => { /* the UA declined the offer or the user dismissed it */ });\r\n this.renderer.xr.addEventListener('sessionend', () => void run());\r\n void run();\r\n }\r\n\r\n resize = () => {\r\n if (this.renderer.xr.isPresenting) return; // never resize while the headset drives the size\r\n const w = this.w(), h = this.h();\r\n this.camera.aspect = w / h;\r\n this.camera.updateProjectionMatrix();\r\n this.renderer.setSize(w, h, false);\r\n };\r\n\r\n dispose() {\r\n this.ro.disconnect();\r\n this.renderer.setAnimationLoop(null);\r\n this.vrControls?.dispose();\r\n this.texture.dispose();\r\n this.clearMeshes();\r\n this.renderer.dispose();\r\n }\r\n}\r\n","import type { Camera } from 'three';\r\n\r\nconst DEG = Math.PI / 180;\r\n\r\n/** Clamp look angles (degrees) to keep the view inside the front hemisphere. */\r\nexport function clampAngles(lon: number, lat: number): { lon: number; lat: number } {\r\n return {\r\n lon: Math.max(-90, Math.min(90, lon)),\r\n lat: Math.max(-85, Math.min(85, lat)),\r\n };\r\n}\r\n\r\n/**\r\n * Pointer/touch drag → camera yaw/pitch, clamped to the front hemisphere.\r\n * Disabled while an XR session drives the head pose, and for flat-screen modes.\r\n */\r\nexport class LookControls {\r\n private lon = 0;\r\n private lat = 0;\r\n private dragging = false;\r\n private px = 0;\r\n private py = 0;\r\n private enabled = true;\r\n private readonly isPresenting: () => boolean;\r\n\r\n constructor(\r\n private readonly camera: Camera,\r\n private readonly dom: HTMLElement,\r\n opts: { isPresenting?: () => boolean } = {},\r\n ) {\r\n this.isPresenting = opts.isPresenting ?? (() => false);\r\n this.dom.addEventListener('pointerdown', this.onDown);\r\n this.dom.addEventListener('pointermove', this.onMove);\r\n this.dom.addEventListener('pointerup', this.onUp);\r\n this.dom.addEventListener('pointercancel', this.onUp);\r\n }\r\n\r\n private capture(fn: 'setPointerCapture' | 'releasePointerCapture', id: number) {\r\n try { this.dom[fn]?.(id); } catch { /* pointer not active */ }\r\n }\r\n\r\n private onDown = (e: PointerEvent) => {\r\n if (!this.enabled) return;\r\n this.dragging = true;\r\n this.px = e.clientX;\r\n this.py = e.clientY;\r\n this.capture('setPointerCapture', e.pointerId);\r\n };\r\n\r\n private onMove = (e: PointerEvent) => {\r\n if (!this.dragging) return;\r\n const dx = e.clientX - this.px;\r\n const dy = e.clientY - this.py;\r\n this.px = e.clientX;\r\n this.py = e.clientY;\r\n ({ lon: this.lon, lat: this.lat } = clampAngles(this.lon - dx * 0.15, this.lat + dy * 0.15));\r\n };\r\n\r\n private onUp = (e: PointerEvent) => {\r\n this.dragging = false;\r\n this.capture('releasePointerCapture', e.pointerId);\r\n };\r\n\r\n update() {\r\n if (this.isPresenting() || !this.enabled) return;\r\n const phi = (90 - this.lat) * DEG;\r\n const theta = this.lon * DEG;\r\n this.camera.lookAt(\r\n Math.sin(phi) * Math.sin(theta),\r\n Math.cos(phi),\r\n -Math.sin(phi) * Math.cos(theta),\r\n );\r\n }\r\n\r\n reset() { this.lon = 0; this.lat = 0; }\r\n\r\n setEnabled(v: boolean) {\r\n this.enabled = v;\r\n if (!v) { this.lon = 0; this.lat = 0; this.camera.lookAt(0, 0, -1); }\r\n }\r\n\r\n getAngles() { return { lon: this.lon, lat: this.lat }; }\r\n\r\n dispose() {\r\n this.dom.removeEventListener('pointerdown', this.onDown);\r\n this.dom.removeEventListener('pointermove', this.onMove);\r\n this.dom.removeEventListener('pointerup', this.onUp);\r\n this.dom.removeEventListener('pointercancel', this.onUp);\r\n }\r\n}\r\n","import type Hls from 'hls.js';\r\nimport type { StreamFormat } from './proxy.js';\r\n\r\n/**\r\n * Attaches a source to a `<video>`. Progressive files go straight on `video.src`;\r\n * HLS/DASH-as-HLS go through hls.js — which is imported dynamically, so consumers\r\n * who never play HLS don't pay for it (and it's an optional dependency).\r\n */\r\nexport class VideoSource {\r\n private hls: Hls | null = null;\r\n\r\n async attach(\r\n video: HTMLVideoElement,\r\n source: { url: string; format: StreamFormat },\r\n opts: { crossOrigin?: string | null } = {},\r\n ): Promise<void> {\r\n this.dispose();\r\n const co = opts.crossOrigin === undefined ? 'anonymous' : opts.crossOrigin;\r\n if (co === null) video.removeAttribute('crossorigin');\r\n else video.crossOrigin = co;\r\n video.playsInline = true;\r\n\r\n const { url, format } = source;\r\n const isHlsLike = format === 'hls' || format === 'dash';\r\n const nativeHls = video.canPlayType('application/vnd.apple.mpegurl') !== '';\r\n\r\n return new Promise<void>((resolve, reject) => {\r\n const cleanup = () => {\r\n video.removeEventListener('loadedmetadata', onLoaded);\r\n video.removeEventListener('error', onError);\r\n };\r\n const onLoaded = () => { cleanup(); resolve(); };\r\n const onError = () => {\r\n cleanup();\r\n const code = video.error ? video.error.code : 'unknown';\r\n reject(new Error(`Video failed to load (media error ${code}). Is the source reachable and CORS-clean?`));\r\n };\r\n video.addEventListener('loadedmetadata', onLoaded);\r\n video.addEventListener('error', onError);\r\n\r\n if (isHlsLike && !nativeHls) {\r\n import('hls.js').then(({ default: HlsCtor }) => {\r\n if (!HlsCtor.isSupported()) {\r\n cleanup();\r\n reject(new Error('HLS/DASH stream needs hls.js, which is unsupported in this browser.'));\r\n return;\r\n }\r\n this.hls = new HlsCtor({ enableWorker: true });\r\n this.hls.on(HlsCtor.Events.ERROR, (_e, data) => {\r\n if (data.fatal) { cleanup(); this.dispose(); reject(new Error(`HLS fatal error: ${data.type} / ${data.details}`)); }\r\n });\r\n this.hls.loadSource(url);\r\n this.hls.attachMedia(video);\r\n }).catch(() => {\r\n cleanup();\r\n reject(new Error('This looks like an HLS/DASH stream but hls.js could not be loaded. Install \"hls.js\".'));\r\n });\r\n } else {\r\n video.src = url;\r\n video.load();\r\n }\r\n });\r\n }\r\n\r\n dispose() {\r\n if (this.hls) { this.hls.destroy(); this.hls = null; }\r\n }\r\n}\r\n"],"names":["detectFormat","rawUrl","pathname","lower","ENDPOINTS","buildProxyUrl","proxy","format","base","params","name","value","MODES","PROJECTIONS","PROJECTION_SHORT","isFlatMode","p","_a","detectProjection","url","s","tb","sbs","formatTime","seconds","m","h","ss","PANEL_W","PANEL_H","panelLayout","W","H","pad","clamp01","v","inRect","r","x","y","padX","padY","hitTest","layout","ACCENT","TEXT","MUTED_TEXT","TOGGLE_BUTTONS","IDLE_HIDE_MS","MOTION_ROT","MOTION_POS","REVEAL_COOLDOWN_MS","STICK_DEADZONE","ZOOM_RATE","TILT_RATE","GRAB_TAP_ANGLE","VRControls","opts","THREE","pw","ph","session","trackedCount","i","c","laser","select","grabStart","grabEnd","connect","e","disconnect","ev","geo","mat","time","l","hover","cursorAt","controller","posed","hits","index","hit","performanceNow","q","prevQ","prevP","ramp","src","ax","cur","pressed","g","b","cam","pos","quat","yaw","yawQuat","forward","text","maxWidth","t","w","force","dur","vol","muted","ptOn","key","L","title","maxW","col","on","pc","volFrac","cxp","seekFrac","filled","draw","cx","cy","color","off","dir","dx","dw","dh","a1","frac","hovered","knob","rad","fn","StereoScene","cb","canvas","video","projection","swapEyes","fov","supersampling","blend","f","d","xr","baseRef","offset","mode","vw","vh","kind","phiLen","thetaLen","halfAngleDeg","thetaMax","uv","idx","theta","discR","sinT","cosT","j","phi","cp","sp","stride","a","uniforms","shader","map","split","isLeft","cams","second","swap","material","side","mesh","_r","_s","delta","old","list","next","deg","offer","run","DEG","clampAngles","lon","lat","LookControls","camera","dom","dy","id","_b","VideoSource","source","co","isHlsLike","nativeHls","resolve","reject","cleanup","onLoaded","onError","code","HlsCtor","_e","data"],"mappings":"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"}
@@ -16,13 +16,22 @@ export declare class StereoScene {
16
16
  private readonly video;
17
17
  private readonly texture;
18
18
  private readonly meshes;
19
+ private readonly rig;
20
+ private tilt;
21
+ private fov;
22
+ private builtFov;
19
23
  private readonly frameCbs;
24
+ private alphaFisheye;
25
+ private inPassthroughSession;
26
+ private passthroughOn;
27
+ private alphaUniforms?;
20
28
  private vrControls?;
21
29
  private readonly animate;
22
30
  private readonly ro;
23
31
  private currentMode;
24
32
  private currentSwap;
25
33
  private readonly vrSessionInit;
34
+ private readonly arSessionInit;
26
35
  constructor(opts: {
27
36
  canvas: HTMLCanvasElement;
28
37
  video: HTMLVideoElement;
@@ -44,12 +53,57 @@ export declare class StereoScene {
44
53
  private pixelRatioFor;
45
54
  private planeAspect;
46
55
  private buildGeometry;
56
+ /**
57
+ * A dome covering the fisheye field of view (FISHEYE190 ≈ 190° → a 95° half-angle cap in
58
+ * front of the viewer), with equidistant-fisheye UVs: the angle from the forward axis maps
59
+ * linearly to radius on the texture disc (centre 0.5,0.5, radius 0.5). UVs are authored in
60
+ * full-frame [0,1] space; updateStereoUV's per-eye repeat/offset selects the L/R circle.
61
+ * Forward is −z (the camera's default view direction), so no extra rotation is needed.
62
+ */
63
+ private buildFisheyeDome;
64
+ /**
65
+ * Transparent material for fisheye content with a DeoVR-style packed alpha matte. Built by
66
+ * patching MeshBasicMaterial (so three.js keeps handling colour management, the map sample and
67
+ * flipY) and injecting DeoVR's exact alpha decode (ported from their WebXR player):
68
+ *
69
+ * ptUV = fract(vec2(discUV.x*0.2 + ptShift, discUV.y*0.4 + 0.8)); // ptShift 0.4 L / 0.9 R
70
+ * alpha = smoothstep(0.0, 0.8, average of the 3×3 red neighbourhood at ptUV);
71
+ *
72
+ * The matte is a 0.4×-scaled copy of the disc's alpha stored per eye at that offset, wrapped
73
+ * across the frame edges (hence it appears tucked into the corners). It's indexed by the SAME
74
+ * disc UV as the colour, so it aligns exactly — including per-eye parallax. Outside the disc → clip.
75
+ */
76
+ private makeFisheyeAlphaMaterial;
77
+ /** Passthrough on/off (immersive-ar only): on = real world visible (transparent clear); off =
78
+ * opaque black surround (VR-style). Alpha content additionally keys the subject via the matte;
79
+ * without a matte the dome stays opaque and only the surround changes. */
80
+ setPassthrough(on: boolean): void;
81
+ /** Mark the current content as carrying a DeoVR-style packed alpha matte (fisheye only). */
82
+ setAlphaMatte(on: boolean): void;
47
83
  /** Shift the shared video texture for the eye/half being drawn. Runs from the mesh's
48
84
  * onBeforeRender, so it's called once per eye per frame (the reference approach). */
49
85
  private updateStereoUV;
50
86
  private clearMeshes;
51
87
  private applyProjection;
88
+ /** Pitch the video content up/down (radians, accumulated and clamped). */
89
+ adjustTilt(delta: number): void;
90
+ /** Yaw the video content left/right (radians, accumulated, unclamped). */
91
+ adjustYaw(delta: number): void;
92
+ /** Zoom by changing the content field of view (positive delta = zoom in / more
93
+ * immersive). Shrinking the fov maps the video into a smaller angular window so the
94
+ * scene recedes and feels farther; growing it fills the view. Rebuilds the projection
95
+ * geometry, throttled so it doesn't churn every frame. */
96
+ adjustZoom(delta: number): void;
97
+ private rebuildGeometry;
52
98
  setProjection(p: Projection): void;
99
+ /** Player registers this so an in-VR projection change flows back through Player — keeping its
100
+ * state, persistence, and the DOM controls in sync. Falls back to a direct geometry swap. */
101
+ setProjectionRequester(cb: (p: Projection) => void): void;
102
+ private projectionRequester?;
103
+ /** Short label of the current projection, for the in-VR stepper. */
104
+ projectionLabel(): string;
105
+ /** Step the projection by dir (±1) through the PROJECTIONS list (wraps around). */
106
+ cycleProjection(dir: number): void;
53
107
  setSwapEyes(v: boolean): void;
54
108
  setFov(deg: number): void;
55
109
  setSupersampling(ss: number): void;
@@ -62,6 +116,10 @@ export declare class StereoScene {
62
116
  * surface it — instead of the silent no-op three.js's VRButton produces. Called from
63
117
  * a click handler so the request keeps the user's transient activation. */
64
118
  enterVR(): Promise<void>;
119
+ /** Enter immersive AR (passthrough). Same contract as enterVR — called from a click so it
120
+ * keeps transient activation, rejects with a readable message. The UA composites our
121
+ * transparent framebuffer over the camera feed. */
122
+ enterAR(): Promise<void>;
65
123
  exitVR(): void;
66
124
  /** Arm the browser/headset's own "Enter VR" affordance (e.g. the Quest system button). */
67
125
  offerVR(): void;
@@ -14,6 +14,16 @@ export interface VRControlsActions {
14
14
  toggleMute(): void;
15
15
  exitVR(): void;
16
16
  recenter(): void;
17
+ adjustTilt(delta: number): void;
18
+ adjustYaw(delta: number): void;
19
+ adjustZoom(delta: number): void;
20
+ /** Passthrough (alpha) content only: whether the toggle applies, its state, and the toggle. */
21
+ passthroughAvailable(): boolean;
22
+ passthroughEnabled(): boolean;
23
+ togglePassthrough(): void;
24
+ /** Projection stepper: the current mode's short label, and step by ±1 through the modes. */
25
+ projectionLabel(): string;
26
+ cycleProjection(dir: number): void;
17
27
  }
18
28
  /** A world-locked control panel for immersive VR, drawn to a canvas texture and
19
29
  * driven by the motion controllers' laser + trigger. Lives only while an XR
@@ -35,6 +45,12 @@ export declare class VRControls {
35
45
  private readonly prevQuat;
36
46
  private readonly prevPos;
37
47
  private revealCooldownUntil;
48
+ private grabbing;
49
+ private readonly grabStartQuat;
50
+ private readonly prevGrabQuat;
51
+ private readonly grabQuat;
52
+ private readonly deltaQuat;
53
+ private readonly grabEuler;
38
54
  private readonly controllers;
39
55
  private readonly lasers;
40
56
  private readonly connected;
@@ -60,6 +76,15 @@ export declare class VRControls {
60
76
  /** Re-summon the panel when a connected controller is deliberately moved (and
61
77
  * we're past the post-hide cooldown). Tracks each controller's pose per frame. */
62
78
  private maybeRevealOnMotion;
79
+ /** Right thumbstick, applied every frame it's held: X zooms the content fov (push right =
80
+ * zoom in), Y tilts the content pitch (push up = tilt up). The grip grab-rotate also does
81
+ * pitch/yaw; the two coexist. */
82
+ private handleThumbstick;
83
+ private onGrabStart;
84
+ private onGrabEnd;
85
+ /** While the grip is held, apply the controller's frame-to-frame rotation to the
86
+ * content's yaw and pitch — a 1:1 grab-and-turn. Roll is ignored. */
87
+ private handleGrab;
63
88
  private pollToggle;
64
89
  private toggle;
65
90
  private show;
@@ -73,7 +98,17 @@ export declare class VRControls {
73
98
  private roundRect;
74
99
  /** Repaint the panel only when something visible changed (state or hover). */
75
100
  private paint;
76
- private drawPill;
101
+ /** A rounded-square icon button: accent fill when `filled`, white border when `hover`; the
102
+ * icon is drawn centred in `filled ? white : TEXT`. */
103
+ private drawIconButton;
104
+ /** Recenter — a reticle (ring + centre dot + crosshair ticks). */
105
+ private iconRecenter;
106
+ /** Passthrough — an eye; a diagonal slash across it means passthrough is off. */
107
+ private iconEye;
108
+ /** A solid triangle pointing left (dir −1) or right (dir +1) — the projection stepper arrows. */
109
+ private iconArrow;
110
+ /** Exit — a door frame with an arrow pointing out to the right. */
111
+ private iconExit;
77
112
  private drawBar;
78
113
  private drawSpeaker;
79
114
  dispose(): void;
@@ -1,7 +1,7 @@
1
1
  import { Projection } from '../types.js';
2
2
 
3
3
  export type Split = 'mono' | 'sbs' | 'tb';
4
- export type GeomKind = 'sphere180' | 'sphere360' | 'plane';
4
+ export type GeomKind = 'sphere180' | 'sphere360' | 'fisheye' | 'plane';
5
5
  export interface ModeConfig {
6
6
  geom: GeomKind;
7
7
  split: Split;
@@ -15,6 +15,8 @@ export declare const PROJECTIONS: {
15
15
  value: Projection;
16
16
  label: string;
17
17
  }[];
18
+ /** Compact labels for the in-VR projection stepper (the full labels are too long for the panel). */
19
+ export declare const PROJECTION_SHORT: Record<Projection, string>;
18
20
  export declare function isFlatMode(p: Projection): boolean;
19
21
  /** Guess a projection from a URL / filename; null when nothing recognizable. */
20
22
  export declare function detectProjection(url: string): Projection | null;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  export declare const PANEL_W = 1024;
9
9
  export declare const PANEL_H = 340;
10
- export type VRRegion = 'play' | 'seek' | 'volume' | 'exit' | 'recenter';
10
+ export type VRRegion = 'play' | 'seek' | 'volume' | 'exit' | 'recenter' | 'passthrough' | 'projPrev' | 'projNext';
11
11
  export interface Rect {
12
12
  x: number;
13
13
  y: number;
@@ -19,10 +19,14 @@ export interface PanelLayout {
19
19
  height: number;
20
20
  title: Rect;
21
21
  recenter: Rect;
22
+ passthrough: Rect;
22
23
  exit: Rect;
23
24
  play: Rect;
24
25
  volIcon: Rect;
25
26
  volBar: Rect;
27
+ projPrev: Rect;
28
+ projNext: Rect;
29
+ projLabel: Rect;
26
30
  seekBar: Rect;
27
31
  timeCur: {
28
32
  x: number;
package/dist/core.js CHANGED
@@ -1,4 +1,4 @@
1
- import { L as o, M as s, P as t, S as r, V as c, b as d, c as l, d as S, a as i, i as n } from "./VideoSource-Cx7XPpvZ.js";
1
+ import { L as o, M as s, P as t, S as r, V as c, b as d, c as l, d as S, a as i, i as n } from "./VideoSource-DHj5tBsK.js";
2
2
  export {
3
3
  o as LookControls,
4
4
  s as MODES,