@displayxr/inline3d 1.13.1 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -1
- package/README.md +4 -3
- package/js/inline3d-playcanvas-engine.js +2 -0
- package/js/inline3d-splat-effects.js +675 -6
- package/js/inline3d-splat-live.js +343 -0
- package/js/inline3d-splat-playcanvas.js +469 -33
- package/js/inline3d-splat.js +5 -0
- package/js/inline3d-viewer.js +25 -1
- package/package.json +2 -1
- package/splat.d.ts +136 -9
package/js/inline3d-splat.js
CHANGED
|
@@ -369,6 +369,10 @@ export function addSplat(wall, canvas, src, opts = {}) {
|
|
|
369
369
|
"only; with the Spark backend, remove() this handle and addSplat() the new asset.",
|
|
370
370
|
);
|
|
371
371
|
},
|
|
372
|
+
/** Not on this backend: prepareSource() feeds setSource(), a PlayCanvas-backend feature. */
|
|
373
|
+
prepareSource() {
|
|
374
|
+
throw new Error("@displayxr/inline3d/splat: prepareSource() is implemented on the PlayCanvas backend only.");
|
|
375
|
+
},
|
|
372
376
|
/** Not on this backend (splat effects are PlayCanvas-only in this version). */
|
|
373
377
|
playEffect() {
|
|
374
378
|
throw effectsNotOnSpark('playEffect()');
|
|
@@ -706,6 +710,7 @@ function addSplatDeferred(wall, canvas, src, opts) {
|
|
|
706
710
|
// A swap requested before the first asset has landed runs once it has (the adapter's own
|
|
707
711
|
// setSource replaces this stub on the same object by then).
|
|
708
712
|
setSource: (...args) => out.ready.then(() => out.setSource(...args)),
|
|
713
|
+
prepareSource: (...args) => out.ready.then(() => out.prepareSource(...args)),
|
|
709
714
|
// Effects before the adapter has loaded: validated NOW (a bad call throws at its own line),
|
|
710
715
|
// then run once the first asset is on screen.
|
|
711
716
|
playEffect: (name, o) => {
|
package/js/inline3d-viewer.js
CHANGED
|
@@ -152,7 +152,30 @@ function swap(a, i, j) {
|
|
|
152
152
|
* Set 0 to get the old percentile-only box back.
|
|
153
153
|
* @returns {{center:number[], extent:number[]}|null} null if there is nothing to measure.
|
|
154
154
|
*/
|
|
155
|
-
export function boundsFromPositions(xyz,
|
|
155
|
+
export function boundsFromPositions(xyz, opts) {
|
|
156
|
+
const it = boundsSteps(xyz, opts);
|
|
157
|
+
let r = it.next();
|
|
158
|
+
while (!r.done) r = it.next();
|
|
159
|
+
return r.value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* boundsFromPositions, in steps: the same code, the same numbers bit for bit, but a caller can
|
|
164
|
+
* give the main thread back between the per-axis selections and the window pass. On a 1.18M
|
|
165
|
+
* gaussian cloud under 4× CPU throttling the one-shot version was a single 60-80 ms task.
|
|
166
|
+
* `await boundsFromPositionsAsync(xyz, opts, yielder)` runs it with `yielder()` between steps.
|
|
167
|
+
*/
|
|
168
|
+
export async function boundsFromPositionsAsync(xyz, opts, yielder) {
|
|
169
|
+
const it = boundsSteps(xyz, opts);
|
|
170
|
+
let r = it.next();
|
|
171
|
+
while (!r.done) {
|
|
172
|
+
await yielder();
|
|
173
|
+
r = it.next();
|
|
174
|
+
}
|
|
175
|
+
return r.value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function* boundsSteps(xyz, { lo = 0.05, hi = 0.95, expand = 2.5 } = {}) {
|
|
156
179
|
const n = Math.floor(xyz.length / 3);
|
|
157
180
|
if (n < 1) return null;
|
|
158
181
|
// Below a few hundred points the percentiles are noise — just use the true box.
|
|
@@ -171,6 +194,7 @@ export function boundsFromPositions(xyz, { lo = 0.05, hi = 0.95, expand = 2.5 }
|
|
|
171
194
|
const hiV = selectKth(axisVals, n, kHi);
|
|
172
195
|
center[axis] = 0.5 * (loV + hiV);
|
|
173
196
|
extent[axis] = Math.max(hiV - loV, 1e-6);
|
|
197
|
+
yield;
|
|
174
198
|
}
|
|
175
199
|
// Untrimmed already IS the true box, and expand 0 asks for the old behaviour.
|
|
176
200
|
if (!trim || expand <= 0) return { center, extent };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@displayxr/inline3d",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Turn any HTML <canvas> into a glasses-free-3D window on a DisplayXR display, inside an ordinary web page. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"js/inline3d-splat-playcanvas.js",
|
|
52
52
|
"js/inline3d-splat-shared.js",
|
|
53
53
|
"js/inline3d-splat-effects.js",
|
|
54
|
+
"js/inline3d-splat-live.js",
|
|
54
55
|
"js/inline3d-model.js",
|
|
55
56
|
"js/inline3d-model-entry.js",
|
|
56
57
|
"js/inline3d-model-playcanvas.js",
|
package/splat.d.ts
CHANGED
|
@@ -336,7 +336,66 @@ export interface SplatOptions {
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
/** The transition effects `reveal` accepts. */
|
|
339
|
-
export type SplatRevealName = 'inflate' | 'sweep' | 'dissolve' | 'fade';
|
|
339
|
+
export type SplatRevealName = 'inflate' | 'sweep' | 'dissolve' | 'fade' | SplatParticleRevealName;
|
|
340
|
+
|
|
341
|
+
/** The particle reveals: every gaussian is a particle with its own start time (docs/splat-effects.md §Particle reveals). */
|
|
342
|
+
export type SplatParticleRevealName = 'assemble' | 'dissolve-in' | 'converge' | 'shimmer';
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Options every particle reveal takes (plus its timing). All sizes are in PICTURE units, so the
|
|
346
|
+
* look is the same on a 2 cm object and a 40 m street.
|
|
347
|
+
*/
|
|
348
|
+
export interface SplatParticleOptions {
|
|
349
|
+
/**
|
|
350
|
+
* What staggers the particles: `'radial'` (distance in the picture from the origin), `'depth'`
|
|
351
|
+
* (near first), `'noise'` (fbm patches), `'random'`, or `'layers'` (a SHARP photo's grid order:
|
|
352
|
+
* layer 0, the visible surface, then layer 1, the disocclusion fill, each outward from the
|
|
353
|
+
* origin — reads `splat.index`, so it forces `scope: 'entity'`).
|
|
354
|
+
*/
|
|
355
|
+
order?: 'radial' | 'depth' | 'noise' | 'random' | 'layers';
|
|
356
|
+
/** Fraction of the duration spent launching particles, 0..0.95. */
|
|
357
|
+
stagger?: number;
|
|
358
|
+
/** How much of each particle's key is random, 0..1. */
|
|
359
|
+
jitter?: number;
|
|
360
|
+
/** A particle in flight is a dot this size (a fraction of the view width). */
|
|
361
|
+
dotSize?: number;
|
|
362
|
+
/** A particle grows back to its own splat over the last (1 − grow) of its flight. */
|
|
363
|
+
grow?: number;
|
|
364
|
+
/** In-flight alpha multiplier, 0..1. */
|
|
365
|
+
flightAlpha?: number;
|
|
366
|
+
/** In-flight tint, added as `color · glow`. */
|
|
367
|
+
color?: [number, number, number];
|
|
368
|
+
glow?: number;
|
|
369
|
+
/** Size of the noise patches, per half-view-width. */
|
|
370
|
+
noiseScale?: number;
|
|
371
|
+
/**
|
|
372
|
+
* Comfort cap: no particle is ever nearer the eyes than its home depth by more than this much
|
|
373
|
+
* extra disparity, a fraction of the eye view's width (default 0.004; 0 = never nearer than
|
|
374
|
+
* home; max 0.05). In 2D a nominal 64 mm-at-1.7 m separation is assumed.
|
|
375
|
+
*/
|
|
376
|
+
maxDisparity?: number;
|
|
377
|
+
/** Fade a particle over the first `vanish` of its flight (0 = off, the reveals' default; the particle transitions use ~0.3–0.45). */
|
|
378
|
+
vanish?: number;
|
|
379
|
+
/** Share of the gaussians drawn while in flight, 0..1 (1 = all, the reveals' default; the swarm uses 0.2). */
|
|
380
|
+
density?: number;
|
|
381
|
+
/** `order: 'layers'`: gaussians per layer (default 768² = 589 824, SHARP). */
|
|
382
|
+
layerSize?: number;
|
|
383
|
+
/** `assemble`: how far out the swarm starts (half-view-widths), its spiral (rad), its curl turbulence, how coherent its start field is (0..1), and how much of it reaches back in depth (0..1). */
|
|
384
|
+
spread?: number;
|
|
385
|
+
swirl?: number;
|
|
386
|
+
turbulence?: number;
|
|
387
|
+
coherence?: number;
|
|
388
|
+
depth?: number;
|
|
389
|
+
/** `dissolve-in`: the wind's lift and the random drift (half-view-widths). */
|
|
390
|
+
lift?: number;
|
|
391
|
+
drift?: number;
|
|
392
|
+
/** `converge`: the spiral (rad) and the launch ball's radius (half-view-widths). */
|
|
393
|
+
spin?: number;
|
|
394
|
+
burst?: number;
|
|
395
|
+
/** `shimmer`: twinkle rate (rad/s) and sparkle strength. */
|
|
396
|
+
twinkle?: number;
|
|
397
|
+
sparkle?: number;
|
|
398
|
+
}
|
|
340
399
|
|
|
341
400
|
/** Named easings; a function `(x) => y` on [0, 1] also works. */
|
|
342
401
|
export type SplatEasing =
|
|
@@ -397,7 +456,7 @@ export interface SplatCustomEffect extends SplatEffectTiming {
|
|
|
397
456
|
hold?: boolean;
|
|
398
457
|
}
|
|
399
458
|
|
|
400
|
-
export type SplatEffectName = 'inflate' | 'deflate' | 'sweep' | 'dissolve' | 'fade' | 'pulse' | 'grade' | 'clip' | 'custom' | `custom:${string}`;
|
|
459
|
+
export type SplatEffectName = 'inflate' | 'deflate' | 'sweep' | 'dissolve' | 'fade' | SplatParticleRevealName | 'pulse' | 'grade' | 'clip' | 'custom' | `custom:${string}`;
|
|
401
460
|
|
|
402
461
|
/** `setEffect('grade', …)`. */
|
|
403
462
|
export interface SplatGradeParams {
|
|
@@ -442,8 +501,15 @@ export interface SplatSourceOptions {
|
|
|
442
501
|
* inflates out of its own; default 2200 ms), `'wavefront'` (a soft front crosses left → right
|
|
443
502
|
* with a thin depth ridge riding it; default 2000 ms, ease-in-out; falls back to the crossfade
|
|
444
503
|
* in a hidden tab). For photo slideshows: `crossfade` or `wavefront`.
|
|
445
|
-
|
|
446
|
-
|
|
504
|
+
*
|
|
505
|
+
* The particle transitions (docs/splat-effects.md §Particle transitions): `'swarm'` (the old
|
|
506
|
+
* photo disperses into a curl-noise swarm while the new one assembles out of one), `'burst'`
|
|
507
|
+
* (the old one collapses into its focus, the new one bursts out of its own), `'shimmer-cross'`
|
|
508
|
+
* (nothing moves: twinkling points out, twinkling points in) and `'dust'` (drifting dust out,
|
|
509
|
+
* gathering dust in). 2.6–2.8 s, linear shared clock (each particle eases itself). The old
|
|
510
|
+
* photo is live, in 2D too; a hidden tab gets the crossfade.
|
|
511
|
+
*/
|
|
512
|
+
transition?: 'cut' | 'crossfade' | 'flip' | 'wavefront' | SplatParticleTransitionName;
|
|
447
513
|
durationMs?: number;
|
|
448
514
|
easing?: SplatEasing;
|
|
449
515
|
/** `cut`/`crossfade` only: reveal the INCOMING asset (entity scope) while the old one fades. */
|
|
@@ -454,6 +520,55 @@ export interface SplatSourceOptions {
|
|
|
454
520
|
ridge?: number;
|
|
455
521
|
/** `wavefront`: the ridge's disparity cap, a fraction of the eye view's width (default 0.004, max 0.05). */
|
|
456
522
|
ridgeMaxDisparity?: number;
|
|
523
|
+
/**
|
|
524
|
+
* `crossfade` / `wavefront`: how the OUTGOING photo is shown during the window.
|
|
525
|
+
* `'live'` (the default in a woven 3D session): it stays resident and keeps rendering every frame
|
|
526
|
+
* through the same eye views (head motion included) into its own target, blended per eye with
|
|
527
|
+
* the live incoming one — about 2× draw for the window, both assets resident. `'frozen'` (the
|
|
528
|
+
* default in 2D): its last frame, frozen into a texture (1.12.1); one draw, but no head parallax
|
|
529
|
+
* on the outgoing photo — on a tracked panel it reads as tracking pausing. Engine builds without
|
|
530
|
+
* the single-camera RenderView path always use `'frozen'`.
|
|
531
|
+
*/
|
|
532
|
+
outgoing?: 'live' | 'frozen';
|
|
533
|
+
/** Particle transitions: particle options for BOTH photos (the shared ones of the particle reveals). */
|
|
534
|
+
order?: SplatParticleOptions['order'];
|
|
535
|
+
stagger?: number;
|
|
536
|
+
jitter?: number;
|
|
537
|
+
maxDisparity?: number;
|
|
538
|
+
dotSize?: number;
|
|
539
|
+
noiseScale?: number;
|
|
540
|
+
layerSize?: number;
|
|
541
|
+
origin?: SplatEffectOrigin;
|
|
542
|
+
/** Particle transitions: how much of the clock the two photos' spans share, 0..1 (0 = one after the other). */
|
|
543
|
+
overlap?: number;
|
|
544
|
+
/** Particle transitions: option overrides for the OUTGOING photo's effect. */
|
|
545
|
+
outgoingFx?: SplatParticleOptions & Record<string, unknown>;
|
|
546
|
+
/** Particle transitions: option overrides for the INCOMING photo's effect. */
|
|
547
|
+
incomingFx?: SplatParticleOptions & Record<string, unknown>;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* `prepareSource`'s options: the `setSource` options the prepared asset will be swapped in with.
|
|
552
|
+
* For a particle transition, its shaders are compiled and linked NOW, in the dwell — otherwise the
|
|
553
|
+
* first transition of each kind in a page blocks its first frame on the link (tens of ms, hundreds
|
|
554
|
+
* the first time a machine sees the variant). Validated like setSource's.
|
|
555
|
+
*/
|
|
556
|
+
export type SplatPrepareOptions = SplatSourceOptions;
|
|
557
|
+
|
|
558
|
+
/** setSource's particle transitions (docs/splat-effects.md §Particle transitions). */
|
|
559
|
+
export type SplatParticleTransitionName = 'swarm' | 'burst' | 'shimmer-cross' | 'dust';
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* What `prepareSource()` resolves to: an opaque, single-use handle for `setSource`. The asset is
|
|
563
|
+
* fully resident (GPU textures + the engine's centre array) until used or disposed.
|
|
564
|
+
*/
|
|
565
|
+
export interface SplatPreparedSource {
|
|
566
|
+
/** The asset's own count (every splat of a flat source). */
|
|
567
|
+
readonly numSplats: number;
|
|
568
|
+
/** `'ready'` until `setSource` uses it (`'used'`) or `dispose()` drops it (`'disposed'`). */
|
|
569
|
+
readonly state: 'ready' | 'used' | 'disposed';
|
|
570
|
+
/** Release the prepared asset (no-op once used or disposed). */
|
|
571
|
+
dispose(): void;
|
|
457
572
|
}
|
|
458
573
|
|
|
459
574
|
/** What `onBeforeFrame` receives. */
|
|
@@ -592,20 +707,32 @@ export interface SplatHandle {
|
|
|
592
707
|
/**
|
|
593
708
|
* Swap the asset (URL or bytes) in place. PlayCanvas backend only — throws on Spark.
|
|
594
709
|
*
|
|
595
|
-
* The new file loads BEHIND the current one
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
710
|
+
* The new file loads BEHIND the current one (or pass a `prepareSource()` result: no load at all
|
|
711
|
+
* on this path); then the two crossfade over `fadeMs` (0 = a cut) and the old one is released.
|
|
712
|
+
* The crossfade blends the two IMAGES linearly, per eye, so the mix is exactly `t` whatever the
|
|
713
|
+
* two photos' depth order. The old image is LIVE in a woven 3D session (it keeps rendering with
|
|
714
|
+
* head motion; `outgoing: 'frozen'` restores the 1.12.1 frozen last frame, the 2D default). The
|
|
715
|
+
* end state is exactly a plain swap. The rig waterfall re-runs for the new file (rig, lens, focus and
|
|
599
716
|
* frame update; `onFocusChange` fires). The pose (yaw/pitch/zoom/depth) is kept unless
|
|
600
717
|
* `resetPose`. A newer call supersedes an older one still loading. Resolves once the fade has
|
|
601
718
|
* finished; rejects if the new asset cannot be loaded (the current one stays on screen).
|
|
602
719
|
*/
|
|
603
|
-
setSource(src: string | Blob | ArrayBuffer | Uint8Array, opts?: SplatSourceOptions): Promise<SplatHandle>;
|
|
720
|
+
setSource(src: string | Blob | ArrayBuffer | Uint8Array | SplatPreparedSource, opts?: SplatSourceOptions): Promise<SplatHandle>;
|
|
721
|
+
/**
|
|
722
|
+
* PlayCanvas backend only — throws on Spark. Fetch, decode and upload `src` now, in the
|
|
723
|
+
* background (the SDK's own passes in idle periods; the engine's end-of-load unpack runs when
|
|
724
|
+
* it must), without rendering it. `setSource(prepared, opts)` then starts on the next frame with
|
|
725
|
+
* no load on the transition path. Single use; `dispose()` it if the page changes its mind.
|
|
726
|
+
* Memory: two full assets resident until the swap (≈ +70 MB of GPU textures per 1.18M-gaussian
|
|
727
|
+
* SOG). `remove()` disposes any still unused.
|
|
728
|
+
*/
|
|
729
|
+
prepareSource(src: string | Blob | ArrayBuffer | Uint8Array, opts?: SplatPrepareOptions): Promise<SplatPreparedSource>;
|
|
604
730
|
/**
|
|
605
731
|
* `engine: 'playcanvas'` only (throws on Spark). Play a transition/pulse/custom effect;
|
|
606
732
|
* validated at the call, run once the first asset is on screen. Resolves `{ finished }` —
|
|
607
733
|
* false when stopped or replaced. docs/splat-effects.md.
|
|
608
734
|
*/
|
|
735
|
+
playEffect(name: SplatParticleRevealName, opts?: Omit<SplatEffectTiming, 'direction'> & SplatParticleOptions): Promise<{ finished: boolean }>;
|
|
609
736
|
playEffect(
|
|
610
737
|
name: Exclude<SplatEffectName, 'grade' | 'clip'>,
|
|
611
738
|
opts?: SplatEffectTiming & Record<string, unknown>,
|