@found-in-space/skykit 0.2.0-dev.20260527.1 → 0.2.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/README.md +40 -35
- package/examples/xr-free-roam/xr-free-roam.js +31 -57
- package/package.json +11 -21
- package/src/__tests__/skykit-browser.test.js +184 -40
- package/src/__tests__/skykit-xr.test.js +56 -0
- package/src/__tests__/skykit.test.js +90 -503
- package/src/actions.js +0 -8
- package/src/browser.d.ts +2 -19
- package/src/browser.js +18 -31
- package/src/embed.js +22 -2
- package/src/hr-diagram.js +3 -1
- package/src/index.d.ts +10 -78
- package/src/index.js +6 -1
- package/src/plugins.js +0 -730
- package/src/utils.js +1 -0
- package/src/xr/plugins.js +74 -0
- package/src/xr.d.ts +18 -0
- package/src/xr.js +1 -0
- package/src/browser-journey.d.ts +0 -8
- package/src/browser-journey.js +0 -240
- package/src/story.d.ts +0 -57
- package/src/story.js +0 -396
package/src/story.js
DELETED
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
import { createSkykitBrowser } from './browser.js';
|
|
2
|
-
|
|
3
|
-
const DEFAULT_SELECTOR = '[data-skykit-story]';
|
|
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 createSkykitStory(host, readStoryOptions(host))
|
|
12
|
-
.then((story) => reportReady(host, story))
|
|
13
|
-
.catch((error) => reportError(host, error));
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Create an authored chapter tour around the beginner SkyKit browser.
|
|
20
|
-
*
|
|
21
|
-
* @param {import('./story.d.ts').SkykitStoryHost | import('./story.d.ts').SkykitStoryOptions} [input]
|
|
22
|
-
* @param {import('./story.d.ts').SkykitStoryOptions} [options]
|
|
23
|
-
* @returns {Promise<import('./story.d.ts').SkykitStory>}
|
|
24
|
-
*/
|
|
25
|
-
export async function createSkykitStory(input = {}, options = {}) {
|
|
26
|
-
const storyOptions = normalizeStoryOptions(input, options);
|
|
27
|
-
const host = resolveTarget(storyOptions.host ?? '#viewer', 'SkyKit story host');
|
|
28
|
-
const chapters = await resolveChapters(host, storyOptions);
|
|
29
|
-
const browser = await createSkykitBrowser({
|
|
30
|
-
...storyOptions,
|
|
31
|
-
host,
|
|
32
|
-
});
|
|
33
|
-
const controls = storyOptions.controls === false
|
|
34
|
-
? null
|
|
35
|
-
: createControls(host, storyOptions);
|
|
36
|
-
let currentIndex = clampIndex(resolveInitialIndex(chapters, storyOptions.initialChapter), chapters.length);
|
|
37
|
-
let disposed = false;
|
|
38
|
-
|
|
39
|
-
/** @type {import('./story.d.ts').SkykitStory} */
|
|
40
|
-
const storyApi = {
|
|
41
|
-
browser,
|
|
42
|
-
viewer: browser.viewer,
|
|
43
|
-
chapters,
|
|
44
|
-
get currentIndex() {
|
|
45
|
-
return currentIndex;
|
|
46
|
-
},
|
|
47
|
-
get currentChapter() {
|
|
48
|
-
return chapters[currentIndex] ?? null;
|
|
49
|
-
},
|
|
50
|
-
next,
|
|
51
|
-
previous,
|
|
52
|
-
goTo,
|
|
53
|
-
dispose,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
hideInlineChapters(host);
|
|
57
|
-
if (controls) bindControls(controls);
|
|
58
|
-
if (chapters.length > 0) {
|
|
59
|
-
goTo(currentIndex);
|
|
60
|
-
} else {
|
|
61
|
-
renderControls(controls, null, -1, 0);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return storyApi;
|
|
65
|
-
|
|
66
|
-
function next() {
|
|
67
|
-
return goTo(Math.min(currentIndex + 1, chapters.length - 1));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function previous() {
|
|
71
|
-
return goTo(Math.max(currentIndex - 1, 0));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @param {number | string} indexOrId
|
|
76
|
-
*/
|
|
77
|
-
function goTo(indexOrId) {
|
|
78
|
-
if (disposed || chapters.length === 0) return null;
|
|
79
|
-
const nextIndex = typeof indexOrId === 'string'
|
|
80
|
-
? chapters.findIndex((chapter) => chapter.id === indexOrId)
|
|
81
|
-
: indexOrId;
|
|
82
|
-
if (!Number.isInteger(nextIndex) || nextIndex < 0 || nextIndex >= chapters.length) {
|
|
83
|
-
throw new RangeError(`SkyKit story chapter not found: ${indexOrId}`);
|
|
84
|
-
}
|
|
85
|
-
currentIndex = nextIndex;
|
|
86
|
-
const chapter = chapters[currentIndex];
|
|
87
|
-
applyChapter(chapter);
|
|
88
|
-
renderControls(controls, chapter, currentIndex, chapters.length);
|
|
89
|
-
const detail = { story: storyApi, chapter, index: currentIndex };
|
|
90
|
-
host.dispatchEvent(new CustomEvent('skykit-story-chapter-change', {
|
|
91
|
-
detail,
|
|
92
|
-
bubbles: true,
|
|
93
|
-
}));
|
|
94
|
-
browser.viewer.emit({
|
|
95
|
-
type: 'story/chapter-change',
|
|
96
|
-
story: storyApi,
|
|
97
|
-
chapter,
|
|
98
|
-
index: currentIndex,
|
|
99
|
-
});
|
|
100
|
-
return chapter;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function dispose() {
|
|
104
|
-
if (disposed) return;
|
|
105
|
-
disposed = true;
|
|
106
|
-
controls?.root.remove();
|
|
107
|
-
await browser.dispose();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* @param {import('./story.d.ts').SkykitStoryChapter} chapter
|
|
112
|
-
*/
|
|
113
|
-
function applyChapter(chapter) {
|
|
114
|
-
const targetPc = chapter.targetPc ?? chapter.observerPc ?? null;
|
|
115
|
-
browser.viewer.requestViewState({
|
|
116
|
-
...(targetPc ? { observerPc: targetPc, targetPc } : {}),
|
|
117
|
-
...(chapter.view ?? {}),
|
|
118
|
-
}, 'story');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @param {ReturnType<typeof createControls>} storyControls
|
|
123
|
-
*/
|
|
124
|
-
function bindControls(storyControls) {
|
|
125
|
-
storyControls.previous.addEventListener('click', () => {
|
|
126
|
-
previous();
|
|
127
|
-
});
|
|
128
|
-
storyControls.next.addEventListener('click', () => {
|
|
129
|
-
next();
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* @param {Element} host
|
|
136
|
-
* @param {import('./story.d.ts').SkykitStoryOptions} options
|
|
137
|
-
* @returns {Promise<import('./story.d.ts').SkykitStoryChapter[]>}
|
|
138
|
-
*/
|
|
139
|
-
async function resolveChapters(host, options) {
|
|
140
|
-
if (options.chapters) return normalizeChapters(options.chapters);
|
|
141
|
-
if (options.src) {
|
|
142
|
-
const response = await fetch(options.src);
|
|
143
|
-
if (!response.ok) {
|
|
144
|
-
throw new Error(`SkyKit story could not load ${options.src}: ${response.status}`);
|
|
145
|
-
}
|
|
146
|
-
const json = await response.json();
|
|
147
|
-
const chapters = Array.isArray(json) ? json : json?.chapters;
|
|
148
|
-
return normalizeChapters(chapters ?? []);
|
|
149
|
-
}
|
|
150
|
-
return readInlineChapters(host);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* @param {Element} host
|
|
155
|
-
* @returns {import('./story.d.ts').SkykitStoryChapter[]}
|
|
156
|
-
*/
|
|
157
|
-
function readInlineChapters(host) {
|
|
158
|
-
return Array.from(host.querySelectorAll('[data-skykit-chapter]'))
|
|
159
|
-
.map((section, index) => {
|
|
160
|
-
const element = /** @type {HTMLElement} */ (section);
|
|
161
|
-
return normalizeChapter({
|
|
162
|
-
id: element.dataset.id ?? element.id ?? `chapter-${index + 1}`,
|
|
163
|
-
title: element.dataset.title ?? element.getAttribute('aria-label') ?? `Chapter ${index + 1}`,
|
|
164
|
-
body: element.textContent?.trim() ?? '',
|
|
165
|
-
bodyHtml: element.innerHTML.trim(),
|
|
166
|
-
targetPc: parseVector3(element.dataset.targetPc),
|
|
167
|
-
observerPc: parseVector3(element.dataset.observerPc),
|
|
168
|
-
}, index);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* @param {Iterable<import('./story.d.ts').SkykitStoryChapterInput>} chapters
|
|
174
|
-
*/
|
|
175
|
-
function normalizeChapters(chapters) {
|
|
176
|
-
return Array.from(chapters, normalizeChapter);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* @param {import('./story.d.ts').SkykitStoryChapterInput} input
|
|
181
|
-
* @param {number} index
|
|
182
|
-
* @returns {import('./story.d.ts').SkykitStoryChapter}
|
|
183
|
-
*/
|
|
184
|
-
function normalizeChapter(input, index) {
|
|
185
|
-
const chapter = input ?? {};
|
|
186
|
-
return {
|
|
187
|
-
id: String(chapter.id ?? `chapter-${index + 1}`),
|
|
188
|
-
title: String(chapter.title ?? `Chapter ${index + 1}`),
|
|
189
|
-
body: chapter.body == null ? '' : String(chapter.body),
|
|
190
|
-
bodyHtml: typeof chapter.bodyHtml === 'string' ? chapter.bodyHtml : null,
|
|
191
|
-
targetPc: normalizeOptionalVector3(chapter.targetPc),
|
|
192
|
-
observerPc: normalizeOptionalVector3(chapter.observerPc),
|
|
193
|
-
view: chapter.view ?? null,
|
|
194
|
-
annotations: Array.isArray(chapter.annotations) ? chapter.annotations : [],
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* @param {Element} host
|
|
200
|
-
*/
|
|
201
|
-
function hideInlineChapters(host) {
|
|
202
|
-
for (const section of host.querySelectorAll('[data-skykit-chapter]')) {
|
|
203
|
-
if (section instanceof HTMLElement) section.hidden = true;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* @param {Element} host
|
|
209
|
-
* @param {import('./story.d.ts').SkykitStoryOptions} options
|
|
210
|
-
*/
|
|
211
|
-
function createControls(host, options) {
|
|
212
|
-
const htmlHost = /** @type {HTMLElement} */ (host);
|
|
213
|
-
if (htmlHost.style && getComputedStyle(htmlHost).position === 'static') {
|
|
214
|
-
htmlHost.style.position = 'relative';
|
|
215
|
-
}
|
|
216
|
-
const root = document.createElement('div');
|
|
217
|
-
const title = document.createElement('h2');
|
|
218
|
-
const body = document.createElement('div');
|
|
219
|
-
const nav = document.createElement('div');
|
|
220
|
-
const previous = document.createElement('button');
|
|
221
|
-
const next = document.createElement('button');
|
|
222
|
-
|
|
223
|
-
root.dataset.skykitStoryControls = '';
|
|
224
|
-
title.dataset.skykitStoryTitle = '';
|
|
225
|
-
body.dataset.skykitStoryBody = '';
|
|
226
|
-
previous.type = 'button';
|
|
227
|
-
next.type = 'button';
|
|
228
|
-
previous.textContent = options.previousLabel ?? 'Back';
|
|
229
|
-
next.textContent = options.nextLabel ?? 'Next';
|
|
230
|
-
nav.append(previous, next);
|
|
231
|
-
root.append(title, body, nav);
|
|
232
|
-
applyDefaultControlStyles(root, title, body, nav, previous, next);
|
|
233
|
-
host.appendChild(root);
|
|
234
|
-
|
|
235
|
-
return { root, title, body, previous, next };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* @param {ReturnType<typeof createControls> | null} controls
|
|
240
|
-
* @param {import('./story.d.ts').SkykitStoryChapter | null} chapter
|
|
241
|
-
* @param {number} index
|
|
242
|
-
* @param {number} count
|
|
243
|
-
*/
|
|
244
|
-
function renderControls(controls, chapter, index, count) {
|
|
245
|
-
if (!controls) return;
|
|
246
|
-
controls.title.textContent = chapter?.title ?? 'SkyKit story';
|
|
247
|
-
if (chapter?.bodyHtml) {
|
|
248
|
-
controls.body.innerHTML = chapter.bodyHtml;
|
|
249
|
-
} else {
|
|
250
|
-
controls.body.textContent = chapter?.body ?? '';
|
|
251
|
-
}
|
|
252
|
-
controls.previous.disabled = index <= 0;
|
|
253
|
-
controls.next.disabled = index < 0 || index >= count - 1;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function applyDefaultControlStyles(root, title, body, nav, previous, next) {
|
|
257
|
-
Object.assign(root.style, {
|
|
258
|
-
position: 'absolute',
|
|
259
|
-
left: '16px',
|
|
260
|
-
right: '16px',
|
|
261
|
-
bottom: '16px',
|
|
262
|
-
zIndex: '2',
|
|
263
|
-
display: 'grid',
|
|
264
|
-
gap: '10px',
|
|
265
|
-
maxWidth: '34rem',
|
|
266
|
-
padding: '14px',
|
|
267
|
-
border: '1px solid rgba(242, 200, 121, 0.28)',
|
|
268
|
-
borderRadius: '8px',
|
|
269
|
-
background: 'rgba(2, 4, 11, 0.78)',
|
|
270
|
-
color: '#f6f1e8',
|
|
271
|
-
font: '14px system-ui, sans-serif',
|
|
272
|
-
pointerEvents: 'auto',
|
|
273
|
-
});
|
|
274
|
-
Object.assign(title.style, {
|
|
275
|
-
margin: '0',
|
|
276
|
-
color: '#f2c879',
|
|
277
|
-
fontSize: '16px',
|
|
278
|
-
lineHeight: '1.2',
|
|
279
|
-
});
|
|
280
|
-
Object.assign(body.style, {
|
|
281
|
-
color: '#d8deea',
|
|
282
|
-
lineHeight: '1.5',
|
|
283
|
-
});
|
|
284
|
-
Object.assign(nav.style, {
|
|
285
|
-
display: 'flex',
|
|
286
|
-
gap: '8px',
|
|
287
|
-
});
|
|
288
|
-
for (const button of [previous, next]) {
|
|
289
|
-
Object.assign(button.style, {
|
|
290
|
-
border: '1px solid rgba(242, 200, 121, 0.42)',
|
|
291
|
-
borderRadius: '999px',
|
|
292
|
-
background: 'rgba(242, 200, 121, 0.12)',
|
|
293
|
-
color: '#f6f1e8',
|
|
294
|
-
padding: '7px 12px',
|
|
295
|
-
cursor: 'pointer',
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* @param {import('./story.d.ts').SkykitStoryHost | import('./story.d.ts').SkykitStoryOptions} input
|
|
302
|
-
* @param {import('./story.d.ts').SkykitStoryOptions} options
|
|
303
|
-
*/
|
|
304
|
-
function normalizeStoryOptions(input, options) {
|
|
305
|
-
if (typeof input === 'string' || isElementLike(input)) {
|
|
306
|
-
return { ...options, host: input };
|
|
307
|
-
}
|
|
308
|
-
return { ...(input ?? {}), ...options };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* @param {Element} host
|
|
313
|
-
*/
|
|
314
|
-
function readStoryOptions(host) {
|
|
315
|
-
const data = host instanceof HTMLElement ? host.dataset : {};
|
|
316
|
-
return {
|
|
317
|
-
...(data.skykitStorySrc ? { src: data.skykitStorySrc } : {}),
|
|
318
|
-
...(data.skykitMagnitude ? { limitingMagnitude: Number(data.skykitMagnitude) } : {}),
|
|
319
|
-
...(data.skykitSpeed ? { speedPcPerSec: Number(data.skykitSpeed) } : {}),
|
|
320
|
-
...(data.skykitExposure ? { exposure: Number(data.skykitExposure) } : {}),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* @param {Element} host
|
|
326
|
-
* @param {import('./story.d.ts').SkykitStory} story
|
|
327
|
-
*/
|
|
328
|
-
function reportReady(host, story) {
|
|
329
|
-
host.dispatchEvent(new CustomEvent('skykit-story-ready', {
|
|
330
|
-
detail: { story, browser: story.browser, viewer: story.viewer },
|
|
331
|
-
bubbles: true,
|
|
332
|
-
}));
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* @param {Element} host
|
|
337
|
-
* @param {unknown} error
|
|
338
|
-
*/
|
|
339
|
-
function reportError(host, error) {
|
|
340
|
-
host.dispatchEvent(new CustomEvent('skykit-story-error', {
|
|
341
|
-
detail: { error },
|
|
342
|
-
bubbles: true,
|
|
343
|
-
}));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function ready(callback) {
|
|
347
|
-
if (document.readyState === 'loading') {
|
|
348
|
-
document.addEventListener('DOMContentLoaded', callback, { once: true });
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
callback();
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function resolveTarget(input, label) {
|
|
355
|
-
if (typeof input !== 'string') {
|
|
356
|
-
if (input) return input;
|
|
357
|
-
throw new Error(`${label} is missing.`);
|
|
358
|
-
}
|
|
359
|
-
const target = document.querySelector(input);
|
|
360
|
-
if (!target) throw new Error(`${label} not found: ${input}`);
|
|
361
|
-
return target;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function resolveInitialIndex(chapters, initialChapter) {
|
|
365
|
-
if (typeof initialChapter === 'string') {
|
|
366
|
-
return chapters.findIndex((chapter) => chapter.id === initialChapter);
|
|
367
|
-
}
|
|
368
|
-
return Number.isInteger(initialChapter) ? Number(initialChapter) : 0;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function clampIndex(index, count) {
|
|
372
|
-
if (count <= 0) return -1;
|
|
373
|
-
if (!Number.isInteger(index) || index < 0) return 0;
|
|
374
|
-
return Math.min(index, count - 1);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function parseVector3(value) {
|
|
378
|
-
if (!value) return null;
|
|
379
|
-
const parts = String(value).split(',').map((part) => Number(part.trim()));
|
|
380
|
-
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) return null;
|
|
381
|
-
return { x: parts[0], y: parts[1], z: parts[2] };
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function normalizeOptionalVector3(value) {
|
|
385
|
-
if (!value || typeof value !== 'object') return null;
|
|
386
|
-
const candidate = /** @type {{ x?: unknown; y?: unknown; z?: unknown }} */ (value);
|
|
387
|
-
const x = Number(candidate.x);
|
|
388
|
-
const y = Number(candidate.y);
|
|
389
|
-
const z = Number(candidate.z);
|
|
390
|
-
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return null;
|
|
391
|
-
return { x, y, z };
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function isElementLike(value) {
|
|
395
|
-
return Boolean(value && typeof value === 'object' && 'appendChild' in value);
|
|
396
|
-
}
|