@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 +86 -8
- package/package.json +17 -2
- package/src/__tests__/skykit-browser.test.js +225 -0
- package/src/browser.d.ts +72 -0
- package/src/browser.js +167 -0
- package/src/embed.d.ts +1 -0
- package/src/embed.js +69 -0
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
|
-
##
|
|
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
|
|
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:
|
|
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.
|
|
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":
|
|
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
|
+
}
|
package/src/browser.d.ts
ADDED
|
@@ -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';
|