@displayxr/inline3d 1.40.0 → 1.40.1
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/CHANGELOG.md +18 -0
- package/call.d.ts +5 -4
- package/camera.d.ts +5 -4
- package/dist/call.js +4 -4
- package/js/call/options.js +3 -2
- package/js/camera/pair-cursor.js +70 -45
- package/package.json +1 -1
package/js/call/options.js
CHANGED
|
@@ -90,8 +90,9 @@ export function normalizeCallOptions(opts = {}) {
|
|
|
90
90
|
// peer's eyes and shift the eyes so it sits at the display plane. The depth slider stays an
|
|
91
91
|
// offset on top. Off = the pair as sent (plus any `hint`).
|
|
92
92
|
autoConverge: opts.autoConverge === undefined ? true : !!opts.autoConverge,
|
|
93
|
-
// `cursor: 'depth'` (runtime ADR-046 on a pair):
|
|
94
|
-
// the
|
|
93
|
+
// `cursor: 'depth'` (runtime ADR-046 on a pair): the arrow comes in front of a person who pops
|
|
94
|
+
// out of the screen under it, measured off each stereo tile's two halves; else the plain OS
|
|
95
|
+
// arrow on the plane. Off by default (zero cost).
|
|
95
96
|
cursor: resolvePairCursorOption(opts.cursor, '@displayxr/inline3d/call'),
|
|
96
97
|
mono3D: normalizeMono3D(opts.mono3D),
|
|
97
98
|
maxPeers: clampMaxPeers(opts.maxPeers === undefined ? DEFAULT_MAX_PEERS : opts.maxPeers),
|
package/js/camera/pair-cursor.js
CHANGED
|
@@ -5,23 +5,38 @@
|
|
|
5
5
|
// up are all in front), it is drawn on top of pixels that say they are nearer — the depth
|
|
6
6
|
// violation runtime ADR-046 is about. A pair has no scene to raycast and no depth buffer, but its
|
|
7
7
|
// two halves ARE the depth: this is ADR-046 Phase 3b done in the page. Block-match a cursor-sized
|
|
8
|
-
// footprint between the eyes
|
|
9
|
-
// halves with that disparity plus a small margin. The browser weaves the pair as usual, so the
|
|
10
|
-
// sprite lands just in front of the content under it.
|
|
8
|
+
// footprint between the eyes and take the nearest confident match.
|
|
11
9
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
10
|
+
// The cursor only ever comes FORWARD (David, 2026-10-10): it never goes inside the display. While
|
|
11
|
+
// what is under it sits at or behind the glass, it is simply the OS arrow, on the screen plane.
|
|
12
|
+
// Only when content comes out of the screen under it is the arrow drawn into both halves, with that
|
|
13
|
+
// content's disparity plus a small margin (the OS cursor hidden meanwhile), so it is never overlapped
|
|
14
|
+
// by what pops out. It is the normal arrow either way, not a special sprite.
|
|
15
|
+
//
|
|
16
|
+
// Same filter as the scene cursor (./inline3d-cursor-depth.js): fast rise, slow sink, the plane when
|
|
17
|
+
// nothing matches. Disparities here are in OUTPUT pixels of one eye of the woven buffer, + = crossed
|
|
18
|
+
// (in front of the glass), the sign the rest of camera/ uses. Loaded only when a page asks for
|
|
19
|
+
// `cursor: 'depth'` — zero cost otherwise.
|
|
16
20
|
|
|
17
|
-
import { CursorPointer, cursorFootprint, cursorFilterStep, CURSOR_DEFAULT_TUNING
|
|
21
|
+
import { CursorPointer, cursorFootprint, cursorFilterStep, CURSOR_DEFAULT_TUNING } from '../inline3d-cursor-depth.js';
|
|
18
22
|
import { matchBlock, MIN_NCC } from './disparity.js';
|
|
19
23
|
|
|
20
|
-
/**
|
|
24
|
+
/** The arrow's height in CSS px — the OS arrow's apparent size, so the swap to the drawn one is seamless. */
|
|
25
|
+
export const PAIR_CURSOR_CSS_PX = 19;
|
|
26
|
+
/** Arrow height as a fraction of the tile height when the tile's on-page size is unknown. */
|
|
21
27
|
export const PAIR_CURSOR_HEIGHT = 0.06;
|
|
22
|
-
/**
|
|
28
|
+
/** Below this many output px in front of the glass the drawn arrow hands back to the OS cursor. */
|
|
29
|
+
export const PAIR_CURSOR_SHOW_PX = 0.5;
|
|
30
|
+
/**
|
|
31
|
+
* The standard pointer arrow, hotspot at the tip (0, 0), unit = its height, x right, y down: the
|
|
32
|
+
* OS arrow's outline (a 19-px-tall Windows/macOS arrow, normalised).
|
|
33
|
+
*/
|
|
34
|
+
export const PAIR_CURSOR_ARROW = Object.freeze([
|
|
35
|
+
[0, 0], [0, 16 / 19], [4.2 / 19, 12.4 / 19], [6.9 / 19, 18.6 / 19], [9.5 / 19, 17.5 / 19], [6.8 / 19, 11.5 / 19], [11.7 / 19, 11.5 / 19],
|
|
36
|
+
]);
|
|
37
|
+
/** How far in front of out-of-screen content the arrow floats, as a fraction of the eye width (crossed). */
|
|
23
38
|
export const PAIR_CURSOR_MARGIN = 0.002;
|
|
24
|
-
/** Comfort
|
|
39
|
+
/** Comfort cap on how far in front the arrow comes, as a fraction of the eye width. */
|
|
25
40
|
export const PAIR_CURSOR_CLAMP = 0.08;
|
|
26
41
|
|
|
27
42
|
/**
|
|
@@ -33,9 +48,10 @@ export const PAIR_CURSOR_CLAMP = 0.08;
|
|
|
33
48
|
* @param {{rL: {sx:number, sy:number, sw:number, sh:number}, rR: {sx:number}, outW: number, mirror?: boolean}} geo
|
|
34
49
|
* each eye's source crop (relative to its half, eyeCropRect) and the output eye width; a
|
|
35
50
|
* mirrored view (the self view) flips the pointer, never the disparity.
|
|
36
|
-
* @param {number} u pointer, tile-normalised (0..1, left → right as the viewer sees it)
|
|
37
|
-
* @param {number} v pointer, tile-normalised (0..1, top → bottom)
|
|
38
|
-
* @param {number} height
|
|
51
|
+
* @param {number} u pointer hotspot, tile-normalised (0..1, left → right as the viewer sees it)
|
|
52
|
+
* @param {number} v pointer hotspot, tile-normalised (0..1, top → bottom)
|
|
53
|
+
* @param {number} height arrow height (fraction of the tile height); the footprint covers the
|
|
54
|
+
* arrow's body, which hangs down-right of the hotspot
|
|
39
55
|
* @param {number} aspect the tile's width / height, for a round footprint
|
|
40
56
|
* @returns {number|null} output px, + = in front of the glass
|
|
41
57
|
*/
|
|
@@ -49,7 +65,8 @@ export function pairFootprintDisparity(luma, geo, u, v, height, aspect) {
|
|
|
49
65
|
const { rL, rR } = geo;
|
|
50
66
|
const k = geo.outW / rL.sw; // output px per source px
|
|
51
67
|
let best = null;
|
|
52
|
-
|
|
68
|
+
// The ring is centred on the arrow's body, not its tip: that is what content must not overlap.
|
|
69
|
+
for (const [pu, pv] of cursorFootprint(u + (0.3 * height) / aspect, v + 0.5 * height, height, aspect)) {
|
|
53
70
|
if (!(pu >= 0 && pu <= 1 && pv >= 0 && pv <= 1)) continue;
|
|
54
71
|
const su = geo.mirror ? 1 - pu : pu;
|
|
55
72
|
const x = Math.round((rL.sx + su * rL.sw) * scale - b / 2);
|
|
@@ -64,42 +81,39 @@ export function pairFootprintDisparity(luma, geo, u, v, height, aspect) {
|
|
|
64
81
|
return best;
|
|
65
82
|
}
|
|
66
83
|
|
|
67
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* The arrow's target disparity: in front of content that comes OUT of the screen (its disparity
|
|
86
|
+
* plus the margin, clamped), else the glass. Never behind the glass — the cursor does not go inside
|
|
87
|
+
* the display.
|
|
88
|
+
*/
|
|
68
89
|
export function pairCursorTarget(contentPx, outW) {
|
|
69
|
-
if (contentPx === null || !Number.isFinite(contentPx)) return 0;
|
|
70
|
-
|
|
71
|
-
return Math.max(-lim, Math.min(lim, contentPx + PAIR_CURSOR_MARGIN * outW));
|
|
90
|
+
if (contentPx === null || !Number.isFinite(contentPx) || !(contentPx > 0)) return 0;
|
|
91
|
+
return Math.min(PAIR_CURSOR_CLAMP * outW, contentPx + PAIR_CURSOR_MARGIN * outW);
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
/**
|
|
75
|
-
* Draw the
|
|
76
|
-
*
|
|
77
|
-
* its own half, so
|
|
95
|
+
* Draw the arrow into both halves of a woven buffer: the left eye's copy `disparityPx / 2` right of
|
|
96
|
+
* the pointer, the right eye's as far left (crossed = in front). White with a dark outline, like the
|
|
97
|
+
* OS arrow. Each copy is clipped to its own half, so an arrow at the edge never bleeds into the other eye.
|
|
98
|
+
* @param {number} sizePx arrow height in output px
|
|
78
99
|
*/
|
|
79
|
-
export function
|
|
80
|
-
const size = height * outH;
|
|
100
|
+
export function drawPairArrow(g, outW, outH, u, v, disparityPx, sizePx) {
|
|
81
101
|
const cy = v * outH;
|
|
82
|
-
const rgba = (c) => `rgba(${Math.round(c[0] * 255)},${Math.round(c[1] * 255)},${Math.round(c[2] * 255)},${c[3]})`;
|
|
83
102
|
for (const eye of [0, 1]) {
|
|
84
103
|
const cx = eye * outW + u * outW + (eye === 0 ? disparityPx / 2 : -disparityPx / 2);
|
|
85
104
|
g.save();
|
|
86
105
|
g.beginPath();
|
|
87
106
|
g.rect(eye * outW, 0, outW, outH);
|
|
88
107
|
g.clip();
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
g.moveTo(cx + x0 * size, cy + y0 * size);
|
|
99
|
-
g.lineTo(cx + x1 * size, cy + y1 * size);
|
|
100
|
-
}
|
|
101
|
-
g.stroke();
|
|
102
|
-
}
|
|
108
|
+
g.beginPath();
|
|
109
|
+
PAIR_CURSOR_ARROW.forEach(([x, y], k) => (k ? g.lineTo : g.moveTo).call(g, cx + x * sizePx, cy + y * sizePx));
|
|
110
|
+
g.closePath();
|
|
111
|
+
g.fillStyle = '#fff';
|
|
112
|
+
g.fill();
|
|
113
|
+
g.lineWidth = Math.max(1, sizePx / 19);
|
|
114
|
+
g.lineJoin = 'miter';
|
|
115
|
+
g.strokeStyle = '#000';
|
|
116
|
+
g.stroke();
|
|
103
117
|
g.restore();
|
|
104
118
|
}
|
|
105
119
|
}
|
|
@@ -113,12 +127,13 @@ export class PairCursor {
|
|
|
113
127
|
/** @param {HTMLElement} canvas the tile — the pointer counts over its box */
|
|
114
128
|
constructor(canvas, { height, pointerScope = 'window' } = {}) {
|
|
115
129
|
this.pointer = new CursorPointer(canvas, { scope: pointerScope });
|
|
116
|
-
|
|
130
|
+
// A fraction of the tile height when given; by default the OS arrow's size (CSS px → tile).
|
|
131
|
+
this.height = height > 0 ? height : null;
|
|
117
132
|
this.filter = {};
|
|
118
133
|
this._luma = null;
|
|
119
134
|
this._uv = null;
|
|
120
135
|
this._content = null;
|
|
121
|
-
/** The last frame's placement, for diagnostics: `{ contentPx, targetPx, disparityPx }` or null. */
|
|
136
|
+
/** The last frame's placement, for diagnostics: `{ contentPx, targetPx, disparityPx, drawn }` or null. */
|
|
122
137
|
this.last = null;
|
|
123
138
|
}
|
|
124
139
|
|
|
@@ -127,6 +142,8 @@ export class PairCursor {
|
|
|
127
142
|
}
|
|
128
143
|
|
|
129
144
|
/**
|
|
145
|
+
* Draws the arrow only while content in front of the glass is under it (the OS cursor is hidden
|
|
146
|
+
* exactly then); otherwise leaves the buffer alone and the OS arrow shows, on the plane.
|
|
130
147
|
* @param {CanvasRenderingContext2D} g the woven buffer (2·outW × outH), already painted
|
|
131
148
|
* @param {object|null} luma the latest grayscale source copy (pairFootprintDisparity)
|
|
132
149
|
* @param {{rL: object, rR: object, outW: number, outH: number, mirror?: boolean}} geo
|
|
@@ -136,21 +153,29 @@ export class PairCursor {
|
|
|
136
153
|
const uv = this.pointer.uv;
|
|
137
154
|
if (!uv) return this.idle();
|
|
138
155
|
const [u, v] = uv;
|
|
156
|
+
const hf = this.height ?? this._osArrowFraction();
|
|
139
157
|
// Match only when there is something new to match: a new frame copy or a moved pointer.
|
|
140
158
|
if (luma !== this._luma || !this._uv || this._uv[0] !== u || this._uv[1] !== v) {
|
|
141
|
-
this._content = pairFootprintDisparity(luma, geo, u, v,
|
|
159
|
+
this._content = pairFootprintDisparity(luma, geo, u, v, hf, geo.outW / geo.outH);
|
|
142
160
|
this._luma = luma;
|
|
143
161
|
this._uv = uv;
|
|
144
162
|
}
|
|
145
163
|
const target = pairCursorTarget(this._content, geo.outW);
|
|
146
164
|
// The shared filter rises fast toward SMALLER values (nearer, in its convention): negate.
|
|
147
165
|
const disparity = -cursorFilterStep(this.filter, CURSOR_DEFAULT_TUNING, -target, nowSec);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.
|
|
166
|
+
const drawn = disparity >= PAIR_CURSOR_SHOW_PX;
|
|
167
|
+
if (drawn) drawPairArrow(g, geo.outW, geo.outH, u, v, disparity, hf * geo.outH);
|
|
168
|
+
this.pointer.hideCss(drawn); // the OS arrow IS the cursor on the plane
|
|
169
|
+
this.last = { contentPx: this._content, targetPx: target, disparityPx: drawn ? disparity : 0, drawn };
|
|
151
170
|
return this.last;
|
|
152
171
|
}
|
|
153
172
|
|
|
173
|
+
/** The OS arrow's height as a fraction of the tile's on-page height. */
|
|
174
|
+
_osArrowFraction() {
|
|
175
|
+
const r = this.pointer.canvas.getBoundingClientRect?.();
|
|
176
|
+
return r && r.height > 0 ? Math.min(0.5, PAIR_CURSOR_CSS_PX / r.height) : PAIR_CURSOR_HEIGHT;
|
|
177
|
+
}
|
|
178
|
+
|
|
154
179
|
/** Not drawing this frame (pointer away, tile not woven): the CSS cursor comes back. */
|
|
155
180
|
idle() {
|
|
156
181
|
this.pointer.hideCss(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.40.
|
|
3
|
+
"version": "1.40.1",
|
|
4
4
|
"description": "Turn any HTML <canvas> into a 3D window on a spatial display, inside an ordinary web page, for the DisplayXR Browser. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|