@found-in-space/skykit 0.2.0-alpha.0 → 0.2.0-alpha.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/README.md CHANGED
@@ -20,17 +20,89 @@ strategies. SkyKit passes strategies through to provider sessions; it does not
20
20
  redefine planning, inspect strategy kinds, or hide loader registries behind
21
21
  string names.
22
22
 
23
- ## Create A Viewer
23
+ ## Paste into a static page or CMS
24
+
25
+ For the beginner path, use the auto-booting embed. Paste this into a static HTML
26
+ page or a CMS custom HTML block:
27
+
28
+ ```html
29
+ <div
30
+ data-skykit-browser
31
+ data-skykit-status="#skykit-status"
32
+ style="width: 100%; height: 70vh; min-height: 420px; background: #02040b"
33
+ ></div>
34
+
35
+ <pre id="skykit-status">Loading stars...</pre>
36
+
37
+ <script
38
+ type="module"
39
+ src="https://esm.sh/@found-in-space/skykit/embed?bundle"
40
+ ></script>
41
+ ```
42
+
43
+ The embed script finds every `[data-skykit-browser]` element and creates the
44
+ standard star browser there. It owns the normal beginner plumbing: Three.js
45
+ renderer and camera, the public star provider, the star-field renderer, streaming
46
+ stars, keyboard navigation, drag-to-look controls, resize handling, the animation
47
+ loop, and page-lifecycle cleanup.
48
+
49
+ Optional attributes keep small tweaks HTML-only:
50
+
51
+ ```html
52
+ <div
53
+ data-skykit-browser
54
+ data-skykit-status="#skykit-status"
55
+ data-skykit-magnitude="7"
56
+ data-skykit-speed="4"
57
+ data-skykit-exposure="2600"
58
+ style="width: 100%; height: 520px; background: #02040b"
59
+ ></div>
60
+ ```
61
+
62
+ The host dispatches `skykit-browser-ready` with `{ browser, viewer }` in
63
+ `event.detail` after startup, and `skykit-browser-error` if startup fails. The
64
+ embed does not install a global object, so pages can host multiple viewers.
65
+
66
+ Pin the CDN URL to a released SkyKit version when publishing long-lived pages,
67
+ for example
68
+ `https://esm.sh/@found-in-space/skykit@x.y.z/embed?bundle&deps=three@0.170.0`.
69
+
70
+ ## Create a browser from JavaScript
71
+
72
+ If your site has a module script, npm, or a bundler, call the helper directly:
73
+
74
+ ```html
75
+ <div id="viewer" style="width: 100vw; height: 100vh"></div>
76
+ <pre id="status">Loading stars...</pre>
77
+
78
+ <script type="module">
79
+ import { createSkykitBrowser } from '@found-in-space/skykit/browser';
80
+
81
+ await createSkykitBrowser({
82
+ host: '#viewer',
83
+ status: '#status',
84
+ });
85
+ </script>
86
+ ```
87
+
88
+ The helper still returns the pieces when a lesson wants to grow:
89
+
90
+ ```js
91
+ const sky = await createSkykitBrowser('#viewer');
92
+
93
+ sky.viewer.requestViewState({ observerPc: { x: 4, y: 0, z: -8 } });
94
+ sky.loop.stop();
95
+ await sky.dispose();
96
+ ```
97
+
98
+ Use the lower-level factories when a lesson is teaching composition or replacing
99
+ a part of the stack:
24
100
 
25
101
  ```js
26
102
  import {
27
- SKYKIT_ACTIONS,
28
- SKYKIT_DEFAULT_KEYBOARD_NAVIGATION_BINDINGS,
29
103
  createKeyboardNavigationPlugin,
30
- createSkykitDefaultKeyboardNavigationBindings,
31
104
  createSkyGrabPlugin,
32
105
  createSkykitAnimationLoop,
33
- createSkykitStatusPlugin,
34
106
  createSkykitViewer,
35
107
  createStreamingStarsPlugin,
36
108
  } from '@found-in-space/skykit';
@@ -41,11 +113,12 @@ import {
41
113
  import { createObserverShellStrategy } from '@found-in-space/star-trees';
42
114
  import { createThreeStarField } from '@found-in-space/three-star-field';
43
115
 
116
+ const host = document.querySelector('#viewer');
44
117
  const provider = createStarOctreeProviderService({ url: OCTREE_DEFAULT });
45
118
  const starField = createThreeStarField();
46
119
 
47
120
  const viewer = await createSkykitViewer({
48
- host: document.querySelector('#skykit'),
121
+ host,
49
122
  view: { coordinateUnitsPerParsec: 0.001 },
50
123
  plugins: [
51
124
  createStreamingStarsPlugin({
@@ -54,8 +127,7 @@ const viewer = await createSkykitViewer({
54
127
  session: { strategy: createObserverShellStrategy() },
55
128
  }),
56
129
  createKeyboardNavigationPlugin({ speedPcPerSec: 2 }),
57
- createSkyGrabPlugin({ target: document.querySelector('#skykit') }),
58
- createSkykitStatusPlugin({ target: document.querySelector('#status') }),
130
+ createSkyGrabPlugin({ target: host }),
59
131
  ],
60
132
  });
61
133
 
@@ -69,6 +141,12 @@ supplied, it is the complete key map. Multiple keys can still point to the same
69
141
  action:
70
142
 
71
143
  ```js
144
+ import {
145
+ SKYKIT_ACTIONS,
146
+ createKeyboardNavigationPlugin,
147
+ createSkykitDefaultKeyboardNavigationBindings,
148
+ } from '@found-in-space/skykit';
149
+
72
150
  createKeyboardNavigationPlugin({
73
151
  rotationSpeedDegPerSec: 45,
74
152
  bindings: createSkykitDefaultKeyboardNavigationBindings({
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@found-in-space/skykit",
3
- "version": "0.2.0-alpha.0",
3
+ "version": "0.2.0-alpha.1",
4
4
  "description": "Slim composition and teaching layer for Found in Space packages",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Found-in-Space/skykit",
10
+ "directory": "packages/skykit"
11
+ },
7
12
  "publishConfig": {
8
13
  "access": "public"
9
14
  },
@@ -14,6 +19,14 @@
14
19
  "types": "./src/index.d.ts",
15
20
  "default": "./src/index.js"
16
21
  },
22
+ "./browser": {
23
+ "types": "./src/browser.d.ts",
24
+ "default": "./src/browser.js"
25
+ },
26
+ "./embed": {
27
+ "types": "./src/embed.d.ts",
28
+ "default": "./src/embed.js"
29
+ },
17
30
  "./parallax": {
18
31
  "types": "./src/parallax.d.ts",
19
32
  "default": "./src/parallax.js"
@@ -32,7 +45,9 @@
32
45
  "examples",
33
46
  "README.md"
34
47
  ],
35
- "sideEffects": false,
48
+ "sideEffects": [
49
+ "./src/embed.js"
50
+ ],
36
51
  "scripts": {
37
52
  "typecheck": "tsc -p tsconfig.json --noEmit",
38
53
  "test": "node --test"
@@ -0,0 +1,225 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import * as THREE from 'three';
4
+
5
+ import { createSkykitBrowser } from '../browser.js';
6
+
7
+ test('createSkykitBrowser wires the starter viewer and extra plugins', async () => {
8
+ await withFakeWindow(async (fakeWindow) => {
9
+ const host = createHost();
10
+ const status = { textContent: '' };
11
+ const renderer = createRenderer();
12
+ const provider = createProvider();
13
+ const starField = createStarField();
14
+ const extraPartCalls = [];
15
+
16
+ const browser = await createSkykitBrowser({
17
+ host,
18
+ status,
19
+ renderer,
20
+ camera: new THREE.PerspectiveCamera(),
21
+ provider,
22
+ starField,
23
+ autoResize: false,
24
+ autoDispose: false,
25
+ autoStart: false,
26
+ maxDevicePixelRatio: 1.5,
27
+ plugins: [
28
+ (context) => context.addPart({
29
+ id: 'extra-part',
30
+ attach() { extraPartCalls.push('attach'); },
31
+ start() { extraPartCalls.push('start'); },
32
+ }),
33
+ ],
34
+ });
35
+
36
+ assert.equal(host.children[0], renderer.domElement);
37
+ assert.equal(host.style.touchAction, 'none');
38
+ assert.equal(renderer.clearColor.color, 0x02040b);
39
+ assert.deepEqual(renderer.size, { width: 640, height: 360, updateStyle: true });
40
+ assert.equal(renderer.pixelRatio, 1.5);
41
+ assert.equal(provider.sessions.length, 1);
42
+ assert.equal(provider.sessions[0].subscribers.size, 1);
43
+ assert.equal(provider.sessions[0].updateViewCalls.length, 2);
44
+ assert.deepEqual(extraPartCalls, ['attach', 'start']);
45
+ assert.match(status.textContent, /"starsLoaded": 0/);
46
+
47
+ browser.resize();
48
+ assert.equal(fakeWindow.addedEvents.length, 0);
49
+
50
+ await browser.dispose();
51
+ assert.equal(provider.disposed, false, 'caller-owned provider should not be disposed');
52
+ assert.equal(provider.sessions[0].disposed, true);
53
+ assert.equal(starField.disposed, true);
54
+ assert.equal(renderer.disposed, false, 'caller-owned renderer should not be disposed');
55
+ assert.equal(host.children.length, 0);
56
+ });
57
+ });
58
+
59
+ test('createSkykitBrowser registers resize and page-lifecycle cleanup by default', async () => {
60
+ await withFakeWindow(async (fakeWindow) => {
61
+ const host = createHost();
62
+ const renderer = createRenderer();
63
+ const provider = createProvider();
64
+ const starField = createStarField();
65
+
66
+ const browser = await createSkykitBrowser({
67
+ host,
68
+ status: false,
69
+ renderer,
70
+ provider,
71
+ starField,
72
+ autoStart: false,
73
+ });
74
+
75
+ assert.deepEqual(fakeWindow.addedEvents.map((entry) => entry.type), [
76
+ 'resize',
77
+ 'pagehide',
78
+ 'beforeunload',
79
+ ]);
80
+
81
+ await browser.dispose();
82
+
83
+ assert.deepEqual(fakeWindow.removedEvents.map((entry) => entry.type), [
84
+ 'resize',
85
+ 'pagehide',
86
+ 'beforeunload',
87
+ ]);
88
+ });
89
+ });
90
+
91
+ async function withFakeWindow(callback) {
92
+ const previousWindow = globalThis.window;
93
+ const fakeWindow = {
94
+ devicePixelRatio: 2,
95
+ addedEvents: [],
96
+ removedEvents: [],
97
+ addEventListener(type, listener, options) {
98
+ this.addedEvents.push({ type, listener, options });
99
+ },
100
+ removeEventListener(type, listener) {
101
+ this.removedEvents.push({ type, listener });
102
+ },
103
+ };
104
+
105
+ Object.defineProperty(globalThis, 'window', {
106
+ configurable: true,
107
+ value: fakeWindow,
108
+ });
109
+
110
+ try {
111
+ await callback(fakeWindow);
112
+ } finally {
113
+ if (previousWindow === undefined) {
114
+ delete globalThis.window;
115
+ } else {
116
+ Object.defineProperty(globalThis, 'window', {
117
+ configurable: true,
118
+ value: previousWindow,
119
+ });
120
+ }
121
+ }
122
+ }
123
+
124
+ function createHost() {
125
+ return {
126
+ children: [],
127
+ clientWidth: 640,
128
+ clientHeight: 360,
129
+ style: {},
130
+ appendChild(node) {
131
+ this.children.push(node);
132
+ },
133
+ removeChild(node) {
134
+ const index = this.children.indexOf(node);
135
+ if (index >= 0) this.children.splice(index, 1);
136
+ },
137
+ };
138
+ }
139
+
140
+ function createRenderer() {
141
+ return {
142
+ domElement: { nodeName: 'CANVAS' },
143
+ clearColor: null,
144
+ size: null,
145
+ pixelRatio: null,
146
+ disposed: false,
147
+ setClearColor(color, alpha) {
148
+ this.clearColor = { color, alpha };
149
+ },
150
+ setSize(width, height, updateStyle) {
151
+ this.size = { width, height, updateStyle };
152
+ },
153
+ setPixelRatio(value) {
154
+ this.pixelRatio = value;
155
+ },
156
+ render() {},
157
+ dispose() {
158
+ this.disposed = true;
159
+ },
160
+ };
161
+ }
162
+
163
+ function createProvider() {
164
+ return {
165
+ sessions: [],
166
+ disposed: false,
167
+ createSession(options) {
168
+ const session = createSession(options);
169
+ this.sessions.push(session);
170
+ return session;
171
+ },
172
+ dispose() {
173
+ this.disposed = true;
174
+ },
175
+ };
176
+ }
177
+
178
+ function createSession(options) {
179
+ return {
180
+ id: 'test-session',
181
+ options,
182
+ subscribers: new Set(),
183
+ updateViewCalls: [],
184
+ disposed: false,
185
+ subscribe(callback) {
186
+ this.subscribers.add(callback);
187
+ return () => {
188
+ this.subscribers.delete(callback);
189
+ };
190
+ },
191
+ updateView(view, options) {
192
+ this.updateViewCalls.push({ view, options });
193
+ },
194
+ getSnapshot() {
195
+ return {
196
+ id: this.id,
197
+ updateViewCalls: this.updateViewCalls.length,
198
+ };
199
+ },
200
+ async dispose() {
201
+ this.disposed = true;
202
+ },
203
+ };
204
+ }
205
+
206
+ function createStarField() {
207
+ return {
208
+ object3d: new THREE.Group(),
209
+ disposed: false,
210
+ view: null,
211
+ apply() {},
212
+ setView(view) {
213
+ this.view = view;
214
+ },
215
+ getSnapshot() {
216
+ return {
217
+ starCount: 0,
218
+ hasView: Boolean(this.view),
219
+ };
220
+ },
221
+ dispose() {
222
+ this.disposed = true;
223
+ },
224
+ };
225
+ }
@@ -0,0 +1,72 @@
1
+ import type { StarCellStrategy } from '@found-in-space/star-trees';
2
+ import type {
3
+ StarOctreeProviderService,
4
+ StarOctreeSessionOptions,
5
+ } from '@found-in-space/star-octree-provider';
6
+ import type { ThreeStarField } from '@found-in-space/three-star-field';
7
+ import type * as THREE from 'three';
8
+
9
+ import type {
10
+ SkykitAnimationLoop,
11
+ SkykitAnimationLoopOptions,
12
+ SkykitDragLookOptions,
13
+ SkykitKeyboardNavigationOptions,
14
+ SkykitPluginInput,
15
+ SkykitViewState,
16
+ SkykitViewer,
17
+ } from './index.js';
18
+
19
+ export type SkykitBrowserHost = string | {
20
+ appendChild?: (node: unknown) => void;
21
+ removeChild?: (node: unknown) => void;
22
+ clientWidth?: number;
23
+ clientHeight?: number;
24
+ style?: { touchAction?: string };
25
+ };
26
+
27
+ export type SkykitBrowserStatusTarget = string | { textContent?: string | null };
28
+
29
+ export interface SkykitBrowserOptions {
30
+ host?: SkykitBrowserHost;
31
+ status?: boolean | SkykitBrowserStatusTarget | null;
32
+ renderer?: THREE.WebGLRenderer;
33
+ camera?: THREE.PerspectiveCamera;
34
+ provider?: StarOctreeProviderService;
35
+ starField?: ThreeStarField;
36
+ octreeUrl?: string;
37
+ strategy?: StarCellStrategy;
38
+ session?: StarOctreeSessionOptions;
39
+ keyboard?: false | SkykitKeyboardNavigationOptions;
40
+ grab?: false | SkykitDragLookOptions;
41
+ plugins?: Iterable<SkykitPluginInput>;
42
+ view?: Partial<SkykitViewState>;
43
+ loop?: SkykitAnimationLoopOptions;
44
+ limitingMagnitude?: number;
45
+ exposure?: number;
46
+ coordinateUnitsPerParsec?: number;
47
+ speedPcPerSec?: number;
48
+ fovDeg?: number;
49
+ near?: number;
50
+ far?: number;
51
+ antialias?: boolean;
52
+ background?: THREE.ColorRepresentation;
53
+ maxDevicePixelRatio?: number;
54
+ autoStart?: boolean;
55
+ autoResize?: boolean;
56
+ autoDispose?: boolean;
57
+ disableTouchAction?: boolean;
58
+ }
59
+
60
+ export interface SkykitBrowser {
61
+ viewer: SkykitViewer;
62
+ renderer: THREE.WebGLRenderer;
63
+ camera: THREE.PerspectiveCamera;
64
+ provider: StarOctreeProviderService;
65
+ starField: ThreeStarField;
66
+ loop: SkykitAnimationLoop;
67
+ resize(): void;
68
+ dispose(): Promise<void>;
69
+ }
70
+
71
+ export declare function createSkykitBrowser(host: SkykitBrowserHost): Promise<SkykitBrowser>;
72
+ export declare function createSkykitBrowser(options?: SkykitBrowserOptions): Promise<SkykitBrowser>;
package/src/browser.js ADDED
@@ -0,0 +1,167 @@
1
+ import * as THREE from 'three';
2
+
3
+ import {
4
+ OCTREE_DEFAULT,
5
+ createStarOctreeProviderService,
6
+ } from '@found-in-space/star-octree-provider';
7
+ import { createObserverShellStrategy } from '@found-in-space/star-trees';
8
+ import { createThreeStarField } from '@found-in-space/three-star-field';
9
+
10
+ import { createSkykitAnimationLoop } from './animation-loop.js';
11
+ import {
12
+ createKeyboardNavigationPlugin,
13
+ createSkyGrabPlugin,
14
+ createSkykitStatusPlugin,
15
+ createStreamingStarsPlugin,
16
+ } from './plugins.js';
17
+ import { createSkykitViewer } from './viewer.js';
18
+
19
+ const DEFAULT_LIMITING_MAGNITUDE = 6.5;
20
+ const DEFAULT_EXPOSURE = 2400;
21
+ const DEFAULT_UNITS_PER_PARSEC = 0.001;
22
+ const DEFAULT_MAX_DEVICE_PIXEL_RATIO = 2;
23
+
24
+ /**
25
+ * Create the default browser star viewer used by the starter lessons.
26
+ *
27
+ * @param {import('./browser.d.ts').SkykitBrowserOptions | import('./browser.d.ts').SkykitBrowserHost} [input]
28
+ * @returns {Promise<import('./browser.d.ts').SkykitBrowser>}
29
+ */
30
+ export async function createSkykitBrowser(input = {}) {
31
+ const options = normalizeOptions(input);
32
+ const host = resolveTarget(options.host ?? '#viewer', 'SkyKit browser host');
33
+ const statusInput = options.status === true ? '#status' : options.status;
34
+ const statusTarget = statusInput === false || statusInput == null
35
+ ? null
36
+ : resolveTarget(statusInput, 'SkyKit status target');
37
+ const limitingMagnitude = positive(options.limitingMagnitude, DEFAULT_LIMITING_MAGNITUDE);
38
+ const renderer = options.renderer ?? new THREE.WebGLRenderer({
39
+ antialias: options.antialias !== false,
40
+ });
41
+ const camera = options.camera ?? new THREE.PerspectiveCamera(
42
+ positive(options.fovDeg, 58),
43
+ 1,
44
+ positive(options.near, 0.0001),
45
+ positive(options.far, 1000),
46
+ );
47
+ const provider = options.provider ?? createStarOctreeProviderService({
48
+ url: options.octreeUrl ?? OCTREE_DEFAULT,
49
+ });
50
+ const starField = options.starField ?? createThreeStarField({
51
+ limitingMagnitude,
52
+ exposure: positive(options.exposure, DEFAULT_EXPOSURE),
53
+ });
54
+
55
+ renderer.setClearColor?.(options.background ?? 0x02040b, 1);
56
+ if (host.style && options.disableTouchAction !== false) host.style.touchAction = 'none';
57
+
58
+ const viewer = await createSkykitViewer({
59
+ host,
60
+ renderer,
61
+ camera,
62
+ view: {
63
+ observerPc: { x: 0, y: 0, z: 0 },
64
+ coordinateUnitsPerParsec: positive(options.coordinateUnitsPerParsec, DEFAULT_UNITS_PER_PARSEC),
65
+ limitingMagnitude,
66
+ ...(options.view ?? {}),
67
+ },
68
+ plugins: [
69
+ createStreamingStarsPlugin({
70
+ id: 'stars',
71
+ provider,
72
+ renderer: starField,
73
+ session: {
74
+ strategy: options.strategy ?? createObserverShellStrategy(),
75
+ ...(options.session ?? {}),
76
+ },
77
+ }),
78
+ ...(options.keyboard === false ? [] : [
79
+ createKeyboardNavigationPlugin({
80
+ speedPcPerSec: positive(options.speedPcPerSec, 2),
81
+ ...(options.keyboard ?? {}),
82
+ }),
83
+ ]),
84
+ ...(options.grab === false ? [] : [
85
+ createSkyGrabPlugin({
86
+ target: host,
87
+ sensitivityRadiansPerPixel: 0.00075,
88
+ ...(options.grab ?? {}),
89
+ }),
90
+ ]),
91
+ ...(statusTarget ? [createStatusPlugin(statusTarget)] : []),
92
+ ...(options.plugins ?? []),
93
+ ],
94
+ });
95
+
96
+ const loop = createSkykitAnimationLoop(viewer, options.loop);
97
+ let disposed = false;
98
+
99
+ const resize = () => {
100
+ viewer.resize({
101
+ devicePixelRatio: Math.min(
102
+ window.devicePixelRatio || 1,
103
+ positive(options.maxDevicePixelRatio, DEFAULT_MAX_DEVICE_PIXEL_RATIO),
104
+ ),
105
+ });
106
+ };
107
+ const dispose = async () => {
108
+ if (disposed) return;
109
+ disposed = true;
110
+ window.removeEventListener('resize', resize);
111
+ window.removeEventListener('pagehide', disposeSoon);
112
+ window.removeEventListener('beforeunload', disposeSoon);
113
+ loop.dispose();
114
+ await viewer.dispose();
115
+ if (!options.provider) await provider.dispose?.();
116
+ if (!options.renderer) renderer.dispose?.();
117
+ };
118
+ const disposeSoon = () => { void dispose(); };
119
+
120
+ if (options.autoResize !== false) window.addEventListener('resize', resize);
121
+ if (options.autoDispose !== false) {
122
+ window.addEventListener('pagehide', disposeSoon, { once: true });
123
+ window.addEventListener('beforeunload', disposeSoon, { once: true });
124
+ }
125
+ resize();
126
+ if (options.autoStart !== false) loop.start();
127
+
128
+ return { viewer, renderer, camera, provider, starField, loop, resize, dispose };
129
+ }
130
+
131
+ function createStatusPlugin(target) {
132
+ return createSkykitStatusPlugin({
133
+ intervalSeconds: 0.5,
134
+ render({ viewer }) {
135
+ const stars = viewer.parts.find((part) => part.id === 'stars')?.snapshot;
136
+ target.textContent = JSON.stringify({
137
+ observerPc: viewer.view.observerPc,
138
+ starsLoaded: stars?.renderer?.starCount ?? 0,
139
+ stream: stars?.status ?? 'starting',
140
+ }, null, 2);
141
+ },
142
+ });
143
+ }
144
+
145
+ function normalizeOptions(input) {
146
+ if (typeof input === 'string' || isElementLike(input)) return { host: input };
147
+ return input ?? {};
148
+ }
149
+
150
+ function resolveTarget(input, label) {
151
+ if (typeof input !== 'string') {
152
+ if (input) return input;
153
+ throw new Error(`${label} is missing.`);
154
+ }
155
+ const target = document.querySelector(input);
156
+ if (!target) throw new Error(`${label} not found: ${input}`);
157
+ return target;
158
+ }
159
+
160
+ function isElementLike(value) {
161
+ return Boolean(value && typeof value === 'object' && 'appendChild' in value);
162
+ }
163
+
164
+ function positive(value, fallback) {
165
+ const number = Number(value);
166
+ return Number.isFinite(number) && number > 0 ? number : fallback;
167
+ }
package/src/embed.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { createSkykitBrowser } from './browser.js';
package/src/embed.js ADDED
@@ -0,0 +1,69 @@
1
+ import { createSkykitBrowser } from './browser.js';
2
+
3
+ const DEFAULT_SELECTOR = '[data-skykit-browser]';
4
+ const started = new WeakSet();
5
+
6
+ if (typeof document !== 'undefined') {
7
+ ready(() => {
8
+ for (const host of document.querySelectorAll(DEFAULT_SELECTOR)) {
9
+ if (started.has(host)) continue;
10
+ started.add(host);
11
+ void createSkykitBrowser(readOptions(host))
12
+ .then((browser) => {
13
+ reportReady(host, browser);
14
+ })
15
+ .catch((error) => {
16
+ reportError(host, error);
17
+ });
18
+ }
19
+ });
20
+ }
21
+
22
+ /** @param {Element} host */
23
+ function readOptions(host) {
24
+ const data = host instanceof HTMLElement ? host.dataset : {};
25
+ return {
26
+ host,
27
+ ...(data.skykitStatus ? { status: data.skykitStatus } : {}),
28
+ ...(data.skykitMagnitude ? { limitingMagnitude: Number(data.skykitMagnitude) } : {}),
29
+ ...(data.skykitSpeed ? { speedPcPerSec: Number(data.skykitSpeed) } : {}),
30
+ ...(data.skykitExposure ? { exposure: Number(data.skykitExposure) } : {}),
31
+ };
32
+ }
33
+
34
+ /**
35
+ * @param {Element} host
36
+ * @param {import('./browser.d.ts').SkykitBrowser} browser
37
+ */
38
+ function reportReady(host, browser) {
39
+ host.dispatchEvent(new CustomEvent('skykit-browser-ready', {
40
+ detail: { browser, viewer: browser.viewer },
41
+ bubbles: true,
42
+ }));
43
+ }
44
+
45
+ /**
46
+ * @param {Element} host
47
+ * @param {unknown} error
48
+ */
49
+ function reportError(host, error) {
50
+ host.dispatchEvent(new CustomEvent('skykit-browser-error', {
51
+ detail: { error },
52
+ bubbles: true,
53
+ }));
54
+ const data = host instanceof HTMLElement ? host.dataset : {};
55
+ if (!data.skykitStatus) return;
56
+ const status = document.querySelector(data.skykitStatus);
57
+ if (status) status.textContent = error instanceof Error ? error.stack ?? error.message : String(error);
58
+ }
59
+
60
+ /** @param {() => void} callback */
61
+ function ready(callback) {
62
+ if (document.readyState === 'loading') {
63
+ document.addEventListener('DOMContentLoaded', callback, { once: true });
64
+ return;
65
+ }
66
+ callback();
67
+ }
68
+
69
+ export { createSkykitBrowser } from './browser.js';