@displayxr/inline3d 1.44.0 → 1.45.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.
@@ -29,13 +29,16 @@
29
29
 
30
30
  import {
31
31
  CursorPointer,
32
+ CursorShapeSelector,
32
33
  cursorArrowFootprint,
33
- cursorArrowInset,
34
+ cursorShapePolys,
35
+ cursorShapeRaster,
36
+ cursorSoftenPixels,
37
+ drawCursorShape2D,
34
38
  cursorDefaultHeight,
35
39
  cursorFilterStep,
36
40
  cursorShouldDraw,
37
41
  cursorSoften,
38
- cursorSoftArrowRaster,
39
42
  cursorTarget,
40
43
  resolveCursorSoften,
41
44
  CURSOR_ARROW,
@@ -218,42 +221,38 @@ export function pairCursorTarget(contentPx, pxPerBaseline, tuning = CURSOR_DEFAU
218
221
  return -cursorTarget(tuning, true, -contentPx / pxPerBaseline) * pxPerBaseline || 0;
219
222
  }
220
223
 
221
- /** The arrow's fill, inset by one output px (at least 1/19 of its height), cached per size. */
222
- const _fillBySize = new Map();
223
- function arrowFill(sizePx) {
224
+ /** The shape's outline + fill inset by one output px (at least 1/19 of its height), cached per size. */
225
+ const _polysBySize = new Map();
226
+ function shapePolys(name, sizePx) {
224
227
  const d = sizePx > 0 ? Math.max(1 / 19, 1 / sizePx) : 1 / 19;
225
- const key = Math.round(d * 1e4);
226
- let f = _fillBySize.get(key);
228
+ const key = `${name}:${Math.round(d * 1e4)}`;
229
+ let f = _polysBySize.get(key);
227
230
  if (!f) {
228
- if (_fillBySize.size > 32) _fillBySize.clear();
229
- _fillBySize.set(key, (f = cursorArrowInset(d)));
231
+ if (_polysBySize.size > 64) _polysBySize.clear();
232
+ _polysBySize.set(key, (f = cursorShapePolys(name, d)));
230
233
  }
231
234
  return f;
232
235
  }
233
236
 
234
237
  /**
235
- * Draw the arrow into both halves of a woven buffer: the left eye's copy `disparityPx / 2` right of
236
- * the pointer, the right eye's as far left (crossed = in front). The shared arrow: its dark outline
237
- * polygon, then the white fill inset by a px; the tip on the hotspot. Each copy is clipped to its
238
- * own half, so an arrow at the edge never bleeds into the other eye.
239
- * @param {number} sizePx arrow height in output px
238
+ * Draw the cursor into both halves of a woven buffer: the left eye's copy `disparityPx / 2` right of
239
+ * the pointer, the right eye's as far left (crossed = in front). The shared sprite (the arrow by
240
+ * default, or `shape`): its dark outline, then the white fill inset by a px; the hotspot on the
241
+ * pointer. Each copy is clipped to its own half, so a sprite at the edge never bleeds into the
242
+ * other eye.
243
+ * @param {number} sizePx sprite height (one arrow height) in output px
244
+ * @param {string} [shape='arrow'] a CURSOR_SHAPES name
240
245
  */
241
- export function drawPairArrow(g, outW, outH, u, v, disparityPx, sizePx) {
246
+ export function drawPairArrow(g, outW, outH, u, v, disparityPx, sizePx, shape = 'arrow') {
242
247
  const cy = v * outH;
243
- const fill = arrowFill(sizePx);
248
+ const polys = shapePolys(shape, sizePx);
244
249
  for (const eye of [0, 1]) {
245
250
  const cx = eye * outW + u * outW + (eye === 0 ? disparityPx / 2 : -disparityPx / 2);
246
251
  g.save();
247
252
  g.beginPath();
248
253
  g.rect(eye * outW, 0, outW, outH);
249
254
  g.clip();
250
- for (const [poly, style] of [[CURSOR_ARROW, '#000'], [fill, '#fff']]) {
251
- g.beginPath();
252
- poly.forEach(([x, y], k) => (k ? g.lineTo : g.moveTo).call(g, cx + x * sizePx, cy + y * sizePx));
253
- g.closePath();
254
- g.fillStyle = style;
255
- g.fill();
256
- }
255
+ drawCursorShape2D(g, shape, cx, cy, sizePx, polys);
257
256
  g.restore();
258
257
  }
259
258
  }
@@ -282,13 +281,12 @@ function scratchCanvas(w, h) {
282
281
  * arrow with `radius` (arrow heights) and `alpha` (opacity). Returns false (nothing drawn) when no
283
282
  * scratch canvas is available, so the caller can fall back to the crisp arrow.
284
283
  */
285
- export function drawPairArrowSoft(g, outW, outH, u, v, disparityPx, sizePx, radius, alpha) {
284
+ export function drawPairArrowSoft(g, outW, outH, u, v, disparityPx, sizePx, radius, alpha, shape = 'arrow') {
286
285
  const cy = v * outH;
287
- const fill = arrowFill(sizePx);
288
- const polys = [CURSOR_ARROW, fill];
286
+ const polys = shapePolys(shape, sizePx);
289
287
  const rasters = [0, 1].map((eye) => {
290
288
  const cx = eye * outW + u * outW + (eye === 0 ? disparityPx / 2 : -disparityPx / 2);
291
- return cursorSoftArrowRaster(cx, cy, sizePx, radius, alpha, [1, 1, 1, 1], [0, 0, 0, 1], polys);
289
+ return cursorShapeRaster(shape, cx, cy, sizePx, radius, alpha, [1, 1, 1, 1], [0, 0, 0, 1], polys);
292
290
  });
293
291
  const sg = scratchCanvas(Math.max(rasters[0].width, rasters[1].width), Math.max(rasters[0].height, rasters[1].height));
294
292
  if (!sg || typeof sg.putImageData !== 'function' || typeof ImageData !== 'function') return false;
@@ -305,6 +303,34 @@ export function drawPairArrowSoft(g, outW, outH, u, v, disparityPx, sizePx, radi
305
303
  return true;
306
304
  }
307
305
 
306
+ /**
307
+ * A url() image cursor into both halves: at its CSS size (one sprite unit = `sizePx` output px per
308
+ * 19 CSS px), its hotspot on the pointer, softened in the PIXELS (cursorSoftenPixels: `radiusPx`
309
+ * blur, `alpha` opacity) — never a CSS filter or opacity. Returns false when no scratch canvas.
310
+ */
311
+ export function drawPairImage(g, outW, outH, u, v, disparityPx, sizePx, sprite, radiusPx, alpha) {
312
+ const k = sizePx / 19; // output px per CSS px
313
+ const w = Math.max(1, Math.round(sprite.width * k)), h = Math.max(1, Math.round(sprite.height * k));
314
+ const sg = scratchCanvas(w + 2 * (Math.ceil(radiusPx) + 2), h + 2 * (Math.ceil(radiusPx) + 2));
315
+ if (!sg || typeof sg.getImageData !== 'function' || typeof ImageData !== 'function') return false;
316
+ sg.clearRect(0, 0, sg.canvas.width, sg.canvas.height);
317
+ sg.drawImage(sprite.entry.image, 0, 0, w, h);
318
+ const soft = cursorSoftenPixels(sg.getImageData(0, 0, w, h).data, w, h, radiusPx, alpha);
319
+ sg.clearRect(0, 0, sg.canvas.width, sg.canvas.height);
320
+ sg.putImageData(new ImageData(soft.data, soft.width, soft.height), 0, 0);
321
+ const cy = v * outH - sprite.hotspot[1] * k - soft.pad;
322
+ for (const eye of [0, 1]) {
323
+ const cx = eye * outW + u * outW + (eye === 0 ? disparityPx / 2 : -disparityPx / 2) - sprite.hotspot[0] * k - soft.pad;
324
+ g.save();
325
+ g.beginPath();
326
+ g.rect(eye * outW, 0, outW, outH);
327
+ g.clip();
328
+ g.drawImage(sg.canvas, 0, 0, soft.width, soft.height, Math.round(cx), Math.round(cy), soft.width, soft.height);
329
+ g.restore();
330
+ }
331
+ return true;
332
+ }
333
+
308
334
  /**
309
335
  * One cursor over one woven pair. The owner calls `draw()` after painting the pair each frame
310
336
  * (`idle()` on any frame it does not paint a woven pair), and hands it the latest grayscale copy
@@ -312,8 +338,10 @@ export function drawPairArrowSoft(g, outW, outH, u, v, disparityPx, sizePx, radi
312
338
  */
313
339
  export class PairCursor {
314
340
  /** @param {HTMLElement} canvas the tile — the pointer counts over its box */
315
- constructor(canvas, { height, pointerScope = 'window', soften } = {}) {
341
+ constructor(canvas, { height, pointerScope = 'window', soften, shape = 'auto', shapeFor = null } = {}) {
316
342
  this.pointer = new CursorPointer(canvas, { scope: pointerScope });
343
+ /** Which sprite (CSS mirror / fixed / shapeFor); `.current` = the one drawn last. */
344
+ this.shapes = new CursorShapeSelector(this.pointer, { shape, shapeFor });
317
345
  /** Crosstalk softening parameters (resolveCursorSoften). */
318
346
  this.soften = resolveCursorSoften(soften, '@displayxr/inline3d pair cursor');
319
347
  // A fraction of the tile height when given; by default the OS arrow's size (CSS px → tile).
@@ -365,6 +393,7 @@ export class PairCursor {
365
393
  const drawn = cursorShouldDraw(d, pxPerBaseline);
366
394
  const disparity = -d * pxPerBaseline; // output px, + = in front
367
395
  let soften = null;
396
+ let shape = null;
368
397
  if (drawn) {
369
398
  // Softening in PANEL px: one output px of an eye spans (tile CSS width × dpr) / outW of them.
370
399
  // The ghost offset of a pair IS its disparity (the two halves are the two views).
@@ -373,13 +402,17 @@ export class PairCursor {
373
402
  const k = r && r.width > 0 ? (r.width * dpr) / geo.outW : 1;
374
403
  const spritePx = r && r.height > 0 ? hf * r.height * dpr : sizePx;
375
404
  const s = cursorSoften(d, disparity * k, spritePx, this.soften);
405
+ const sprite = this.shapes.pick({ u, v, point: null }, nowSec * 1000);
406
+ shape = this.shapes.current;
376
407
  soften = { blurPx: s.blurPx, alpha: s.alpha, ghostPx: Math.abs(disparity * k), spritePx, radius: spritePx > 0 ? s.blurPx / spritePx : 0, soft: s.blurPx > 0 || s.alpha < 1 };
377
- if (!soften.soft || !drawPairArrowSoft(g, geo.outW, geo.outH, u, v, disparity, sizePx, soften.radius, s.alpha)) {
378
- drawPairArrow(g, geo.outW, geo.outH, u, v, disparity, sizePx);
379
- }
408
+ if (sprite.kind === 'image') {
409
+ drawPairImage(g, geo.outW, geo.outH, u, v, disparity, sizePx, sprite, soften.soft ? soften.radius * sizePx : 0, soften.soft ? s.alpha : 1);
410
+ } else if (sprite.kind === 'shape' && (!soften.soft || !drawPairArrowSoft(g, geo.outW, geo.outH, u, v, disparity, sizePx, soften.radius, s.alpha, shape))) {
411
+ drawPairArrow(g, geo.outW, geo.outH, u, v, disparity, sizePx, shape);
412
+ } // 'none': the page hides its cursor; draw nothing (the OS cursor stays hidden)
380
413
  }
381
414
  this.pointer.hideCss(drawn); // the OS arrow IS the cursor on the plane
382
- this.last = { contentPx: this._content, targetPx: -target * pxPerBaseline || 0, disparityPx: drawn ? disparity : 0, filteredPx: disparity || 0, drawn, pxPerBaseline, soften };
415
+ this.last = { contentPx: this._content, targetPx: -target * pxPerBaseline || 0, disparityPx: drawn ? disparity : 0, filteredPx: disparity || 0, drawn, pxPerBaseline, soften, shape };
383
416
  return this.last;
384
417
  }
385
418
 
@@ -414,5 +447,6 @@ export class PairCursor {
414
447
 
415
448
  dispose() {
416
449
  this.pointer.dispose();
450
+ this.shapes.dispose();
417
451
  }
418
452
  }