@nakednous/host 0.0.8 → 0.0.10

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/README.md CHANGED
@@ -82,6 +82,7 @@ dependency first and an options object last, and registers with the context so
82
82
  | `host.poseTrack(opts)` · `host.cameraTrack(cam, opts)` | core tracks ticked while they play; `{ handles }` builds `TrackHandles`, one handle per keyframe. `add(depth)` — the transport panel's `+` — authors where the camera is looking: a pose track places the pose at the centre of the installed camera's frustum, `depth` `0`…`1` from the near plane to the far one, aimed along that camera (the view bag's matrices; never deduplicated), a camera track captures its camera state, a placement depth being nothing a camera keyframe could use. `remove()` — the transport panel's `−` — retracts the last keyframe, one per call; `remove(i)` is the core track's own |
83
83
  | `host.hid(opts)` · `host.gamepad(opts)` | rate streams — `lin[3]` / `ang[3]` in raw device units, `bind(helm)` feeds it each tick; `available`, `connected` |
84
84
  | `host.image(url)` · `host.video(opts)` · `host.raster(draw, w, h)` | texture sources — an `ImageBitmap`, a hidden `<video>` from a file or the camera with a `ready` promise, a Canvas2D drawing |
85
+ | `decodeRGBE(bytes, opts)` · `encodeRGBE(r, g, b, out)` | a Radiance `.hdr` file's RGBE bytes decoded to `{ data, width, height }` in linear light — three 8-bit mantissas and a shared exponent, `level · 2^(e − 136)` — and the inverse. A floating-point texture's pixels, which no browser image decoder gives: `loadImage` hands back 8-bit codes clamped at 1.0, so a tone-mapping pass would have nothing left to compress. Rows come back in the file's own order (`rowOrder: 'BOTTOM_UP'` for GL's, which is what `webgl.tree`'s `loadHDR` asks for); `maxPixels` refuses past 16 Mpixel rather than exhaust the tab. Pure, DOM-free, one allocation. Verified against three.js's `HDRLoader` and ImageMagick |
85
86
  | `host.mesh(url, opts)` | one mesh — the arrays shape `{ position, indices, normal?, tangent?, texcoord?, color?, joints?, weights? }` with its `bounds` `{ min, max, center, diag }` beside them — from an OBJ file, a one-part glTF 2.0 file (`.glb`, `.gltf`), or any format through `{ parse: (arrayBuffer) => arrays }`; what a bridge's buffer upload takes as it is, and `size / mesh.bounds.diag` fits any file to `size`. A file of several parts rejects, naming `model`. `{ format }` when the URL does not say. **`normals`**: left out, the file's normals are kept and a mesh without any gets smooth ones; `'smooth'` always recomputes them, summed per position, so a faceted file or a texture seam smooths too; `'flat'` always recomputes them on the flattened mesh (every triangle owning its vertices: more vertices, indices counting up); `false` computes nothing. The procedure, its limits and sources are tree's `mesh` module's; glTF viewers compute flat normals for a file without any — here that is `'flat'` |
86
87
  | `host.model(url, opts)` | a model with its structure — `{ parts: [{ name, node, skin, mesh, targets, color }], nodes: { names, parents, rest }, skins: [{ name, joints, inverseBind }], clips: [{ name, duration, channels }] }` — what a rig, morph targets or animation need. A part's `mesh` is as `host.mesh` returns it, in the part's own space; `targets` are morph deltas; nodes come parents first with a rest pose, and with `skins` and `clips` feed tree's `clipSample` · `poseBlend` · `poseWorld` · `jointPalette` as they are. Same options; `'flat'` flattens each part's morph targets with its mesh. An OBJ file is one white part under one identity node. host's own parsers: no textures, cameras, sparse accessors or compressed geometry |
87
88
  | `host.labels` · `host.hasLabels` | the label layer, created on first access — `set(id, text, x, y, z, opts)` at a world anchor, `setScreen(id, text, sx, sy, opts)` in canvas px, `{ frame: true }` for a label that lives one frame, `tick()`; `hasLabels` probes without creating |
@@ -92,7 +93,8 @@ dependency first and an options object last, and registers with the context so
92
93
  The module-level functions behind the methods are exported too (`createView`, `createPointer`,
93
94
  `createPlayers`, `createLoop`, `observeCanvas`, `Handle`, `PointerRouter`, `cameraHelm`,
94
95
  `poseHelm`, `helmBasis`, `poseTrack`, `cameraTrack`, `TrackHandles`, `createHid`,
95
- `createGamepad`, `decodeSpaceNavigator`, `loadImage`, `createVideo`, `raster`, `loadMesh`, `loadModel`, `createLabels`,
96
+ `createGamepad`, `decodeSpaceNavigator`, `loadImage`, `createVideo`, `raster`, `loadMesh`, `loadModel`,
97
+ `decodeRGBE`, `encodeRGBE`, `createLabels`,
96
98
  `createOrbit`), so an adapter can compose them on its own context.
97
99
 
98
100
  ---
package/dist/index.js CHANGED
@@ -186,10 +186,11 @@ function observeCanvas(canvas, onChange) {
186
186
  *
187
187
  * - pointerdown · pointermove · pointerup · pointercancel on the canvas,
188
188
  * keydown (Esc) on the window.
189
- * - Coordinates in logical canvas px through the element's bounding
190
- * rectangle — (clientX − rect.left) · (width / rect.width), the logical
191
- * size read off the view bag's vp — the numbers mapLocation(SCREEN) and vp
192
- * expect, so a CSS-scaled canvas maps correctly.
189
+ * - Coordinates in screen space — the surface's logical pixels, top-left, y
190
+ * down — through the element's bounding rectangle: (clientX − rect.left) ·
191
+ * (width / rect.width), the logical size read off the view bag's vp. These
192
+ * are the numbers mapLocation(SCREEN) and vp expect (a viewport of negative
193
+ * height), so a CSS-scaled canvas maps correctly.
193
194
  * - Per pointer: an entry { id, x, y, seq, down, up, cancel, owner }, kept
194
195
  * from its press until the frame after its release; `seq` counts moves,
195
196
  * so a consumer remembers the last seq it consumed and reads only new
@@ -787,7 +788,7 @@ class Handle {
787
788
  }
788
789
 
789
790
  // The proxy's world position and its grab radius in world units — a
790
- // constant grabPx screen size at the proxy's depth.
791
+ // constant grabPx canvas size at the proxy's depth.
791
792
  _proxyPrep() {
792
793
  const c = this._constraint, v = this._view, p = this._proxyPos;
793
794
  if (this._from && !this._grabbed) this._resolveFrame();
@@ -810,14 +811,14 @@ class Handle {
810
811
  return rayHitSphere(o[0], o[1], o[2], d[0], d[1], d[2], p[0], p[1], p[2], this._proxyRad);
811
812
  }
812
813
 
813
- /** Unproject a canvas pixel into the ray scratch; false on a stale view. */
814
+ /** Unproject a screen-space pixel into the ray scratch; false on a stale view. */
814
815
  _ray(x, y) {
815
816
  const v = this._view;
816
817
  if (v.stale) return false;
817
818
  return unproject(_rayO, _rayD, x, y, v, v.vp, v.ndcZMin) !== null;
818
819
  }
819
820
 
820
- /** Hit-test the proxy at a canvas pixel. */
821
+ /** Hit-test the proxy at a screen-space pixel. */
821
822
  _pickAt(x, y) {
822
823
  return this._ray(x, y) && this._proxyT(_rayO, _rayD) < Infinity;
823
824
  }
@@ -1207,7 +1208,7 @@ function cameraHelm(host, cam, opts) {
1207
1208
  const helm = new PoseHelm();
1208
1209
  _applyOpts(helm, opts);
1209
1210
  if (opts && opts.from != null) {
1210
- console.error('[host] cameraHelm: a camera helm is always body-fly and has no `from`. Ignoring it; bind the camera to a poseHelm for screen- or world-relative motion.');
1211
+ console.error('[host] cameraHelm: a camera helm is always body-fly and has no `from`. Ignoring it; bind the camera to a poseHelm for view- or world-relative motion.');
1211
1212
  }
1212
1213
  if (_isCameraState(cam)) helm.home(cameraToPose(_pose, cam));
1213
1214
  const player = {
@@ -1294,8 +1295,14 @@ function poseHelm(host, opts) {
1294
1295
  * poseTrack(host, opts) is a core PoseTrack whose activation hooks add and
1295
1296
  * remove its player. cameraTrack(host, cam, opts) is a core CameraTrack whose
1296
1297
  * player evaluates straight into the camera state `cam` — the state is the
1297
- * keyframe shape, so track.eval(cam) is the write — and once more on
1298
- * deactivate so the camera rests on the path; add() with no argument
1298
+ * keyframe shape, so track.eval(cam) is the write — while the track plays, and
1299
+ * once more on deactivate so the camera rests on the path. That bound is the
1300
+ * point: a stopped track's camera belongs to whoever else drives it — the
1301
+ * reader's orbit, the figure's own camera code — so a consumer that renders
1302
+ * from the state derives it instead, one evaluation per frame after the tick
1303
+ * that advances the transport, which carries a seek, a dragged keyframe and a
1304
+ * capture into the state as well and repeats the player's write harmlessly
1305
+ * while the track plays. add() with no argument
1299
1306
  * captures `cam`, add({ camera }) any camera-state object. { handles } on
1300
1307
  * either decorates the track with TrackHandles: one VIEW handle per
1301
1308
  * draggable keyframe field (pos / eye / center), an optional rot DIAL per
@@ -1401,7 +1408,17 @@ function poseTrack(host, opts) {
1401
1408
  }
1402
1409
 
1403
1410
  /**
1404
- * A core CameraTrack evaluating into the camera state `cam` while it plays.
1411
+ * A core CameraTrack evaluating into the camera state `cam` while it plays, and
1412
+ * once when it stops, so the camera rests on the path.
1413
+ *
1414
+ * The write is bounded by playback on purpose: while a track is stopped the
1415
+ * camera it names belongs to whoever else drives it — the reader's orbit, the
1416
+ * figure's own camera code — and a factory writing every frame would fight
1417
+ * them. A consumer that renders *from* the state therefore derives it: evaluate
1418
+ * the track once per frame, after the tick that advances the transport. A seek,
1419
+ * a dragged keyframe and a capture reach the state that way too, and the
1420
+ * evaluation repeats the player's own write harmlessly while the track plays.
1421
+ *
1405
1422
  * @param {object} host
1406
1423
  * @param {object} cam A camera state ({ eye, center, up, fov, halfHeight, near, far }).
1407
1424
  * @param {{ handles?:boolean|object }} [opts]
@@ -2491,7 +2508,7 @@ function raster(draw, w, h, opts) {
2491
2508
  * class — a CSS class beside the layer's own 'host-label', frame — a
2492
2509
  * transient label that lives for the frame it was set in: a gizmo re-sets
2493
2510
  * it every draw and tick() removes it once a frame passes without. A label
2494
- * whose screen depth leaves [0, 1] or whose anchor leaves the canvas is
2511
+ * whose window depth leaves [0, 1] or whose anchor leaves the canvas is
2495
2512
  * hidden, not clamped. Elements are reused by id; tick() writes transforms
2496
2513
  * only, text when the string changed.
2497
2514
  */
@@ -2684,7 +2701,7 @@ function createLabels(host, opts) {
2684
2701
  * what is still draining, so do home() and enabled = false. The default is
2685
2702
  * 0.2 s; 0 makes the orbit exact.
2686
2703
  *
2687
- * Screen y runs down; which world direction that is depends on the
2704
+ * Canvas y runs down; which world direction that is depends on the
2688
2705
  * projection installed in the view bag: a y-up projection (GL's, tree's
2689
2706
  * default) maps a downward drag to the eye's −up, a y-flipped one (p5's,
2690
2707
  * mat4Proj[5] < 0) to +up. The orbit reads that sign off the bag each
@@ -2799,7 +2816,7 @@ function createOrbit(host, cam, opts) {
2799
2816
  let moved = false;
2800
2817
  let az = 0, el = 0, px = 0, py = 0, ld = 0; // this frame's step
2801
2818
  const view = host.view;
2802
- const ys = view.mat4Proj[5] < 0 ? -1 : 1; // screen-down is the eye's −up under a y-up projection, +up under p5's flip
2819
+ const ys = view.mat4Proj[5] < 0 ? -1 : 1; // canvas-down is the eye's −up under a y-up projection, +up under p5's flip
2803
2820
  if (a !== null && b === null) {
2804
2821
  const t = tracked.get(a.id);
2805
2822
  if (t) {
@@ -2851,6 +2868,204 @@ function createOrbit(host, cam, opts) {
2851
2868
  return host.register(orbit);
2852
2869
  }
2853
2870
 
2871
+ /**
2872
+ * @file HDR — Radiance (.hdr) RGBE bytes decoded to the pixels a bridge uploads.
2873
+ * @module host/hdr
2874
+ * @license AGPL-3.0-only
2875
+ *
2876
+ * A floating-point texture needs radiance, and no image decoder in the browser
2877
+ * gives it any: `loadImage` hands back an `ImageBitmap` of 8-bit codes, clamped
2878
+ * at 1.0, so a tone-mapping pass would have nothing left to compress. Radiance's
2879
+ * RGBE is the one high-range format a few dozen lines decode.
2880
+ *
2881
+ * ```
2882
+ * const hdr = decodeRGBE(bytes) // { data, width, height } — radiance
2883
+ * const tex = texture(gl, hdr) // the bridge's upload, RGBA32F
2884
+ * ```
2885
+
2886
+ *
2887
+ * A pixel is three 8-bit mantissas and a shared exponent — `v · 2^(e − 136)` —
2888
+ * the format of Greg Ward's RADIANCE (Ward, *Real Pixels*, Graphics Gems II,
2889
+ * 1991). The reader follows `rgbe.c`'s structure and three.js's `HDRLoader`
2890
+ * (itself adapted from Bruce Walter's code), with the exact scale above rather
2891
+ * than that implementation's `v · 2^(e − 128) / 255`, and no `+ 0.5` — the
2892
+ * offset `rgbe.c`'s floating read carries, which lifts every pixel by half a
2893
+ * mantissa step (0.04 % of the mean on a Poly Haven HDRI) and which no file
2894
+ * declares. Rows keep the order the file states and are never flipped here: GL's
2895
+ * bottom-up space is the upload's concern (see texture.js).
2896
+ *
2897
+ * `loadHDR` — the fetch and the GL row order — is `webgl.tree`'s, and it
2898
+ * re-exports these two; a reader with no bridge to feed uses them directly. Rows
2899
+ * come back in the file's own order unless `rowOrder: 'BOTTOM_UP'` asks for GL's.
2900
+ * The decoder is trusted by round-tripping through the encoder and by agreeing
2901
+ * with three.js's `HDRLoader` and ImageMagick on real Radiance files.
2902
+ */
2903
+
2904
+
2905
+ // Radiance's own threshold: at or below it a float holds no light, so a pixel
2906
+ // that low is written as four zero bytes and read back as black.
2907
+ const _MIN_RADIANCE = 1e-32;
2908
+ // 16 Mpixel of RGBA32F is 256 MB of output: past this the tab is gone before
2909
+ // anything on screen says why, so the refusal comes from here.
2910
+ const _DEFAULT_MAX_PIXELS = 16 * 1024 * 1024;
2911
+
2912
+ const _fv = new DataView(new ArrayBuffer(4));
2913
+
2914
+ // The shared exponent as a float32: the bit pattern of 2^(e − 136) — mantissa
2915
+ // zero, biased exponent — read straight out of the view, no Math.pow per pixel.
2916
+ // 256 entries, made once at load, indexed by the exponent byte a pixel carries.
2917
+ const _SCALES = new Float32Array(256);
2918
+ for (let e = 0; e < 256; e++) {
2919
+ _fv.setUint32(0, e < 10 ? 0 : (e - 9) << 23, true); // e < 10 underflows to zero
2920
+ _SCALES[e] = _fv.getFloat32(0, true);
2921
+ }
2922
+
2923
+ /**
2924
+ * Encode three linear radiance samples and a shared exponent to RGBE bytes.
2925
+ * The inverse of a pixel in `decodeRGBE`: the largest component becomes a byte
2926
+ * level between 128 and 255 — `level = round(v / 2^(e − 136))` with e the
2927
+ * exponent that carries it — so all eight bits are spent and the components
2928
+ * keep their ratio. The level is an integer: 0.0 comes back exactly, and
2929
+ * everything else to within half a level, the 1/512 an 8-bit mantissa costs at
2930
+ * worst.
2931
+ * @param {number} r Red, ≥ 0.
2932
+ * @param {number} g Green.
2933
+ * @param {number} b Blue.
2934
+ * @param {Uint8Array} [out] Four bytes to write; a new one when absent.
2935
+ * @returns {Uint8Array} out
2936
+ */
2937
+ function encodeRGBE(r, g, b, out) {
2938
+ const o = out || new Uint8Array(4);
2939
+ const max = Math.max(r, g, b);
2940
+ if (!(max >= _MIN_RADIANCE)) { o[0] = o[1] = o[2] = o[3] = 0; return o; }
2941
+ // The exponent whose scale carries max as a byte level of 128 to 255 — the
2942
+ // finest step the format has for this magnitude. The float32's own exponent
2943
+ // sets the base, one scale step below the level 255 needs, and the search
2944
+ // steps up until max fits a byte. `_SCALES` is the decoder's own table, so
2945
+ // the two cannot drift apart.
2946
+ _fv.setFloat32(0, max, true);
2947
+ let e = Math.min(255, Math.max(1, ((_fv.getUint32(0, true) >>> 23) & 0xff) - 127 + 128));
2948
+ while (e < 255 && max / _SCALES[e] > 255) e++;
2949
+ o[0] = Math.min(255, Math.round(r / _SCALES[e]));
2950
+ o[1] = Math.min(255, Math.round(g / _SCALES[e]));
2951
+ o[2] = Math.min(255, Math.round(b / _SCALES[e]));
2952
+ o[3] = e;
2953
+ return o;
2954
+ }
2955
+
2956
+ // The header: a magic line, keyed lines and comments, a blank line, then the
2957
+ // resolution string (`-Y 512 +X 1024`). Anything before the resolution is
2958
+ // skipped — XMP and other writers put blocks there — so the walk stops at the
2959
+ // first line that states a dimension.
2960
+ function _headerAt(data) {
2961
+ let pos = 0, height = 0, width = 0, flipRows = false;
2962
+ const n = data.length;
2963
+ while (pos < n) {
2964
+ let end = pos;
2965
+ while (end < n && data[end] !== 10) end++;
2966
+ const line = String.fromCharCode.apply(null, data.subarray(pos, Math.min(end, pos + 400)));
2967
+ pos = end + 1;
2968
+ if (/^#\?/.test(line)) continue;
2969
+ const res = /^\s*([-+])([XY])\s+(\d+)\s+([-+])([XY])\s+(\d+)\s*$/.exec(line);
2970
+ if (res) {
2971
+ const first = res[2].toUpperCase(), second = res[5].toUpperCase();
2972
+ if (first === 'Y' && second === 'X') {
2973
+ height = +res[3]; width = +res[6];
2974
+ flipRows = res[1] === '+'; // +Y: row 0 at the bottom
2975
+ } else if (first === 'X' && second === 'Y') {
2976
+ width = +res[3]; height = +res[6];
2977
+ flipRows = res[1] === '+'; // +X: column 0 on the right
2978
+ } else {
2979
+ throw new Error('[host] hdr: the resolution names ' + first + ' then ' + second + '; -Y h +X w expected.');
2980
+ }
2981
+ return { width, height, flipRows, pos };
2982
+ }
2983
+ if (line === '') continue; // a writer may separate the keys from the resolution
2984
+ }
2985
+ throw new Error('[host] hdr: no resolution line — the file is not a Radiance image.');
2986
+ }
2987
+
2988
+ // One scanline of RGBE bytes into `row`: either the new-style RLE (4 planes,
2989
+ // runs of 128 bytes or fewer) or the flat tuples an old writer leaves.
2990
+ function _scanline(data, pos, row, w, off, rowBytes) {
2991
+ const end = data.length;
2992
+ if (w >= 8 && w < 32768 && pos + 3 < end && data[pos] === 2 && data[pos + 1] === 2 && (data[pos + 2] & 0x80) === 0) {
2993
+ const scanline = (data[pos + 2] << 8) | data[pos + 3];
2994
+ if (scanline !== w) throw new Error('[host] hdr: scanline width ' + scanline + ', image width ' + w + '.');
2995
+ pos += 4;
2996
+ for (let c = 0; c < 4; c++) {
2997
+ let x = 0;
2998
+ while (x < w) {
2999
+ if (pos >= end) throw new Error('[host] hdr: the file ends inside scanline ' + (off / rowBytes) + '.');
3000
+ const count = data[pos++];
3001
+ if (count > 128) { // a run of one byte
3002
+ if (pos >= end) throw new Error('[host] hdr: the file ends inside a run.');
3003
+ const v = data[pos++];
3004
+ for (let i = 0; i < count - 128; i++) row[(x + i) * 4 + c] = v;
3005
+ x += count - 128;
3006
+ } else { // a literal run
3007
+ if (count === 0 || pos + count > end) throw new Error('[host] hdr: bad run length in scanline ' + (off / rowBytes) + '.');
3008
+ for (let i = 0; i < count; i++) row[(x + i) * 4 + c] = data[pos + i];
3009
+ pos += count;
3010
+ x += count;
3011
+ }
3012
+ if (x > w) throw new Error('[host] hdr: a run overruns scanline ' + (off / rowBytes) + '.');
3013
+ }
3014
+ }
3015
+ return pos;
3016
+ }
3017
+ // flat: 4 bytes per pixel, in file order (the scanline is one plane-less array)
3018
+ if (pos + w * 4 > end) throw new Error('[host] hdr: the file ends inside scanline ' + (off / rowBytes) + '.');
3019
+ for (let i = 0; i < w * 4; i++) row[i] = data[pos + i];
3020
+ return pos + w * 4;
3021
+ }
3022
+
3023
+ /**
3024
+ * Decode Radiance RGBE bytes into the pixel source `texture` takes.
3025
+ * @details Every pixel becomes four float channels — R, G, B and 1 — and the
3026
+ * rows come back in the order the file states, top-down for the `-Y` every
3027
+ * writer emits. `loadHDR` is the one that turns them over for GL; a caller
3028
+ * here that wants the same passes `rowOrder: 'BOTTOM_UP'`. The decoder holds no
3029
+ * state, allocates its output once, and needs no DOM, so it runs under
3030
+ * node:test as well as in the browser.
3031
+ * @param {ArrayBuffer|Uint8Array} bytes The whole file.
3032
+ * @param {{ rowOrder?:string, maxPixels?:number }} [opts]
3033
+ * rowOrder: `FILE` (default) or `BOTTOM_UP` — the latter for GL's
3034
+ * texture space, row 0 at v = 0.
3035
+ * maxPixels: refuse past this many pixels (default 16 MPixel).
3036
+ * @returns {{ data:Float32Array, width:number, height:number }} Radiance in
3037
+ * linear light: what `texture` uploads as RGBA32F, and the range a
3038
+ * tone operator compresses.
3039
+ */
3040
+ function decodeRGBE(bytes, opts) {
3041
+ const o = opts || {};
3042
+ const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
3043
+ const head = _headerAt(data);
3044
+ const { width: w, height: h } = head;
3045
+ if (!(w > 0) || !(h > 0)) throw new Error('[host] hdr: the resolution states ' + w + ' × ' + h + '.');
3046
+ const max = o.maxPixels || _DEFAULT_MAX_PIXELS;
3047
+ if (w * h > max) {
3048
+ throw new Error('[host] hdr: ' + w + ' × ' + h + ' is past maxPixels (' + max +
3049
+ '); pass a bigger one, or decode at a lower resolution.');
3050
+ }
3051
+ const flip = o.rowOrder === 'BOTTOM_UP';
3052
+ const out = new Float32Array(w * h * 4);
3053
+ const row = new Uint8Array(w * 4);
3054
+ let pos = head.pos;
3055
+ for (let y = 0; y < h; y++) {
3056
+ pos = _scanline(data, pos, row, w, y * w * 4, w * 4);
3057
+ const at = (flip ? h - 1 - y : y) * w * 4;
3058
+ for (let x = 0; x < w; x++) {
3059
+ const p = x * 4, s = _SCALES[row[p + 3]], d = at + p;
3060
+ out[d] = row[p] * s;
3061
+ out[d + 1] = row[p + 1] * s;
3062
+ out[d + 2] = row[p + 2] * s;
3063
+ out[d + 3] = 1;
3064
+ }
3065
+ }
3066
+ return { data: out, width: w, height: h };
3067
+ }
3068
+
2854
3069
  /**
2855
3070
  * @file The host context — one per canvas: pointer, view bag, players, loop, observer.
2856
3071
  * @module host
@@ -3022,5 +3237,5 @@ function createHost(canvas, opts) {
3022
3237
  return host;
3023
3238
  }
3024
3239
 
3025
- export { GAMEPAD_MAP, HID_FILTERS, Handle, PointerRouter, TrackHandles, VIEW, cameraHelm, cameraTrack, createGamepad, createHid, createHost, createLabels, createLoop, createOrbit, createPlayers, createPointer, createVideo, createView, decodeSpaceNavigator, helmBasis, isConstraint, loadImage, loadMesh, loadModel, measureCanvas, observeCanvas, poseHelm, poseTrack, raster, validConstraint };
3240
+ export { GAMEPAD_MAP, HID_FILTERS, Handle, PointerRouter, TrackHandles, VIEW, cameraHelm, cameraTrack, createGamepad, createHid, createHost, createLabels, createLoop, createOrbit, createPlayers, createPointer, createVideo, createView, decodeRGBE, decodeSpaceNavigator, encodeRGBE, helmBasis, isConstraint, loadImage, loadMesh, loadModel, measureCanvas, observeCanvas, poseHelm, poseTrack, raster, validConstraint };
3026
3241
  //# sourceMappingURL=index.js.map