@starcried/starlite-core 0.1.0-alpha.3 → 0.1.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.en.md +45 -0
- package/README.md +3 -1
- package/dist/bubble.d.ts +6 -1
- package/dist/bubble.js +18 -3
- package/dist/environment-lighting.d.ts +9 -0
- package/dist/environment-lighting.js +11 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +2 -0
- package/dist/locale.d.ts +5 -0
- package/dist/locale.js +12 -0
- package/dist/quick-settings.d.ts +2 -0
- package/dist/quick-settings.js +6 -4
- package/package.json +3 -2
package/README.en.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @starcried/starlite-core
|
|
2
|
+
|
|
3
|
+
[简体中文](README.md) · English
|
|
4
|
+
|
|
5
|
+
Format-neutral playback, lifecycle, dialogue, and host controls for StarLite. This package imports neither Three.js nor Inochi2D WASM. Usually install a backend; it installs core automatically.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @starcried/starlite-core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Shared contracts
|
|
12
|
+
|
|
13
|
+
`ModelController` provides `pause`, `resume`, `getAnimations`, `playAnimation`, `stopAnimation`, `resetCamera`, and `destroy`. Playback resolves as `completed`, `interrupted`, or `stopped`; loops resolve when interrupted or stopped. `ModelMountSlot` cancels replaced loads and destroys late instances. Backend-specific bones, tracks, and parameters stay in their backend.
|
|
14
|
+
|
|
15
|
+
`DialogueScheduler` handles entrance, occasional interaction, and clicks. `DialogueBubble` provides plain text, ARIA announcements, placement, expiry, and cleanup. Both backends share these components and styling. A host can use bubbles without a model or motion; VRM dialogue can omit `animation` and specify only `lines`.
|
|
16
|
+
|
|
17
|
+
Optional controller methods include `setDisplaySize`, `setFrameRate`, `setPointerEnabled`, `setBubblesVisible`, and `setPerformanceVisible`. These do not remount the model; hiding bubbles does not stop scheduled motions.
|
|
18
|
+
|
|
19
|
+
`setDialogueContext(context | null)` temporarily replaces or merges a host library. Context contains `id`, `lines`, optional `animation`, `triggers`, `mode`, and `order`. Sequential click and occasional events share progress. Restoring `null` returns to the original library without remounting or entrance replay. Identical context updates preserve progress.
|
|
20
|
+
|
|
21
|
+
## Shared settings menu
|
|
22
|
+
|
|
23
|
+
`QuickSettingsMenu` receives `locale: "en" | "zh-CN"`, tabs, toggles, ranges, selects, and action callbacks. The host owns model hit testing and persistence. `open(x,y)` uses viewport coordinates; `update(options)` preserves selected tab and stable menu position; `close()` and `destroy()` clean up. Observe `starlite-menu-change` through `element` to synchronize a native input region.
|
|
24
|
+
|
|
25
|
+
`resolveLocale(hostLanguage)` maps Chinese tags to `zh-CN`, other nonempty tags to `en`, and absent input to `zh-CN`. Locale affects built-in menu labels and errors only; caller-provided labels and dialogue remain untouched.
|
|
26
|
+
|
|
27
|
+
## Motion-bound dialogue
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
dialogue: {
|
|
31
|
+
click: {
|
|
32
|
+
animations: ["Wave", "Nod"],
|
|
33
|
+
linesByAnimation: {
|
|
34
|
+
Wave: ["Hello!", "Nice to see you."],
|
|
35
|
+
Nod: ["I'm here.", "Go ahead."]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Each event chooses a motion, then a matching sentence. Names match exactly and are case-sensitive. Pools accept 1–20 unique names; `linesByAnimation` must cover the full pool and contain no unrelated names. Use `lines` for a shared sentence library.
|
|
42
|
+
|
|
43
|
+
[Configuration](https://github.com/StarCried/starlite/blob/main/docs/configuration.en.md) · [VRM backend](https://github.com/StarCried/starlite/blob/main/packages/vrm/README.en.md)
|
|
44
|
+
|
|
45
|
+
BSD-2-Clause. No model artwork is included.
|
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# @starcried/starlite-core
|
|
2
2
|
|
|
3
|
+
简体中文 · [English](README.en.md)
|
|
4
|
+
|
|
3
5
|
StarLite 的公共播放契约、生命周期和气泡组件。该包不导入 Three.js 或 Inochi2D WASM;宿主先选择后端,再加载对应运行时。
|
|
4
6
|
|
|
5
7
|
在 Node.js 22.12+ 项目中安装 alpha 版本:
|
|
6
8
|
|
|
7
9
|
```sh
|
|
8
|
-
npm install @starcried/starlite-core
|
|
10
|
+
npm install @starcried/starlite-core
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
通常直接安装所需后端,它会自动安装本包。
|
package/dist/bubble.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { DialogueAction } from "./dialogue.js";
|
|
2
|
+
/** Stable model top in host-local CSS pixels, independent of canvas padding/DPR. */
|
|
3
|
+
export interface ModelBubbleAnchor {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
}
|
|
2
7
|
/** Plain-text, accessible dialogue presentation shared by every renderer. */
|
|
3
8
|
export declare class DialogueBubble {
|
|
4
9
|
#private;
|
|
5
10
|
readonly element: HTMLDivElement;
|
|
6
|
-
constructor(host: HTMLElement);
|
|
11
|
+
constructor(host: HTMLElement, anchor?: () => ModelBubbleAnchor | undefined);
|
|
7
12
|
show(action: DialogueAction, timestamp: number, withoutFrames?: boolean): void;
|
|
8
13
|
update(timestamp: number): void;
|
|
9
14
|
hide(): void;
|
package/dist/bubble.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
export class DialogueBubble {
|
|
3
3
|
element;
|
|
4
4
|
#host;
|
|
5
|
+
#anchor;
|
|
5
6
|
#until = 0;
|
|
6
7
|
#timeout;
|
|
7
|
-
constructor(host) {
|
|
8
|
+
constructor(host, anchor) {
|
|
8
9
|
this.#host = host;
|
|
10
|
+
this.#anchor = anchor;
|
|
9
11
|
this.element = document.createElement("div");
|
|
10
12
|
this.element.className = "model-widget__bubble";
|
|
11
13
|
this.element.hidden = true;
|
|
@@ -47,8 +49,21 @@ export class DialogueBubble {
|
|
|
47
49
|
return;
|
|
48
50
|
const host = this.#host.getBoundingClientRect(), bubble = this.element.getBoundingClientRect();
|
|
49
51
|
const width = Math.max(1, document.documentElement.clientWidth || window.innerWidth);
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
+
const candidate = this.#anchor?.();
|
|
53
|
+
const anchor = candidate && Number.isFinite(candidate.x) && Number.isFinite(candidate.y) ? candidate : undefined;
|
|
54
|
+
const margin = 12, centered = host.left + (anchor?.x ?? host.width / 2) - bubble.width / 2;
|
|
55
|
+
const above = host.top + (anchor?.y ?? 0) >= bubble.height + margin * 2;
|
|
56
|
+
this.element.dataset.modelPlacement = above ? "above" : "below";
|
|
57
|
+
if (anchor) {
|
|
58
|
+
const height = Math.max(1, document.documentElement.clientHeight || window.innerHeight);
|
|
59
|
+
const preferredTop = above ? host.top + anchor.y - margin - bubble.height : host.top + host.height + margin;
|
|
60
|
+
const top = Math.max(margin, Math.min(Math.max(margin, height - bubble.height - margin), preferredTop));
|
|
61
|
+
Object.assign(this.element.style, { left: `${anchor.x}px`, top: `${top - host.top}px`, bottom: "auto" });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
for (const property of ["left", "top", "bottom"])
|
|
65
|
+
this.element.style.removeProperty(property);
|
|
66
|
+
}
|
|
52
67
|
const left = Math.max(margin, Math.min(Math.max(margin, width - bubble.width - margin), centered));
|
|
53
68
|
this.element.style.setProperty("--model-bubble-shift-x", `${Math.round(left - centered)}px`);
|
|
54
69
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A local RGB lighting hint. Hosts own colour acquisition and permission. */
|
|
2
|
+
export interface EnvironmentLighting {
|
|
3
|
+
color: string;
|
|
4
|
+
/** 0 restores the original lighting; 1 is the default, 2 exaggerates the effect. */
|
|
5
|
+
strength?: number;
|
|
6
|
+
/** Overall light-energy target .8–1.1, blended/amplified by strength. Omit to infer from RGB. */
|
|
7
|
+
brightness?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function validateEnvironmentLighting(input: unknown): asserts input is EnvironmentLighting;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function validateEnvironmentLighting(input) {
|
|
2
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
3
|
+
throw new Error("environmentLighting must be an object");
|
|
4
|
+
const value = input;
|
|
5
|
+
if (Object.keys(value).some(key => key !== "color" && key !== "strength" && key !== "brightness") || typeof value.color !== "string" || !/^#[0-9a-f]{6}$/iu.test(value.color))
|
|
6
|
+
throw new Error("environmentLighting.color must be #RRGGBB");
|
|
7
|
+
if (value.strength !== undefined && (typeof value.strength !== "number" || !Number.isFinite(value.strength) || value.strength < 0 || value.strength > 2))
|
|
8
|
+
throw new Error("environmentLighting.strength must be between 0 and 2");
|
|
9
|
+
if (value.brightness !== undefined && (typeof value.brightness !== "number" || !Number.isFinite(value.brightness) || value.brightness < .8 || value.brightness > 1.1))
|
|
10
|
+
throw new Error("environmentLighting.brightness must be between 0.8 and 1.1");
|
|
11
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { resolveLocale, validateLocale, type StarLiteLocale } from "./locale.js";
|
|
1
2
|
export type ModelBackend = "inochi2d" | "vrm";
|
|
2
3
|
export type WidgetStatus = "disabled" | "loading" | "running" | "paused" | "fallback" | "destroyed";
|
|
3
4
|
export type AnimationPlaybackResult = "completed" | "interrupted" | "stopped";
|
|
@@ -11,6 +12,9 @@ export interface ModelAnimationInfo {
|
|
|
11
12
|
}
|
|
12
13
|
import type { ModelHitMaskListener } from "./hit-mask.js";
|
|
13
14
|
import type { DialogueContext } from "./dialogue.js";
|
|
15
|
+
import type { EnvironmentLighting } from "./environment-lighting.js";
|
|
16
|
+
export { validateEnvironmentLighting, type EnvironmentLighting } from "./environment-lighting.js";
|
|
17
|
+
import type { ModelBubbleAnchor } from "./bubble.js";
|
|
14
18
|
export { RenderedHitMask, packAlphaMask, type ModelHitMask, type ModelHitMaskListener } from "./hit-mask.js";
|
|
15
19
|
export interface ModelController {
|
|
16
20
|
readonly status: WidgetStatus;
|
|
@@ -20,6 +24,8 @@ export interface ModelController {
|
|
|
20
24
|
playAnimation(name: string, options?: AnimationPlaybackOptions): Promise<AnimationPlaybackResult>;
|
|
21
25
|
stopAnimation(): void;
|
|
22
26
|
resetCamera(): void;
|
|
27
|
+
/** Stable reference-pose top, in host-local CSS pixels; no canvas padding/DPR. */
|
|
28
|
+
getBubbleAnchor?(): ModelBubbleAnchor | undefined;
|
|
23
29
|
observeInteractionMask(listener?: ModelHitMaskListener): void;
|
|
24
30
|
/** Refresh current alpha coverage after a host resize, including while paused. */
|
|
25
31
|
refreshInteractionMask?(): void;
|
|
@@ -38,6 +44,8 @@ export interface ModelController {
|
|
|
38
44
|
setBubblesVisible?(visible: boolean): void;
|
|
39
45
|
/** Replace bounded host dialogue without remounting or replaying entrance. */
|
|
40
46
|
setDialogueContext?(context: DialogueContext | null): void;
|
|
47
|
+
/** Optional ambient-light hint. Null restores the original lighting. */
|
|
48
|
+
setEnvironmentLighting?(lighting: EnvironmentLighting | null): void;
|
|
41
49
|
destroy(): void;
|
|
42
50
|
}
|
|
43
51
|
export interface MountOptions {
|
|
@@ -58,7 +66,7 @@ export declare function mountModelWidget<C extends {
|
|
|
58
66
|
/** Streaming limit applies to decoded response bytes, not just Content-Length. */
|
|
59
67
|
export declare function fetchBounded(url: string, maximumBytes: number, signal: AbortSignal): Promise<ArrayBuffer>;
|
|
60
68
|
export { DialogueScheduler, resolveDialogueSentences, resolveDialogueContext, type DialogueContext, type DialogueAction, type DialogueSchedule, type DialogueSceneConfig, type ResolvedDialogueScene } from "./dialogue.js";
|
|
61
|
-
export { DialogueBubble } from "./bubble.js";
|
|
69
|
+
export { DialogueBubble, type ModelBubbleAnchor } from "./bubble.js";
|
|
62
70
|
export { observePassThroughClicks } from "./pass-through.js";
|
|
63
71
|
export { PerformanceOverlay, FrameSamples, validatePerformanceConfig, type PerformanceConfig, type PerformanceFrameDetails } from "./performance.js";
|
|
64
72
|
export { QuickSettingsMenu, type QuickSettingsOptions, type QuickSettingItem, type QuickSettingCallback } from "./quick-settings.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { resolveLocale, validateLocale } from "./locale.js";
|
|
2
|
+
export { validateEnvironmentLighting } from "./environment-lighting.js";
|
|
1
3
|
export { RenderedHitMask, packAlphaMask } from "./hit-mask.js";
|
|
2
4
|
/** Own one mount, including asynchronous work which outlives a replacement. */
|
|
3
5
|
export class ModelMountSlot {
|
package/dist/locale.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Supported interface languages; user content is never translated. */
|
|
2
|
+
export type StarLiteLocale = "zh-CN" | "en";
|
|
3
|
+
/** Accept host language tags (en-US, zh-TW, ...); unsupported languages use English. */
|
|
4
|
+
export declare function resolveLocale(language?: unknown): StarLiteLocale;
|
|
5
|
+
export declare function validateLocale(value: unknown): void;
|
package/dist/locale.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Accept host language tags (en-US, zh-TW, ...); unsupported languages use English. */
|
|
2
|
+
export function resolveLocale(language) {
|
|
3
|
+
const value = Array.isArray(language) ? language[0] : language;
|
|
4
|
+
if (typeof value !== "string" || !value.trim())
|
|
5
|
+
return "zh-CN";
|
|
6
|
+
return /^zh(?:[-_]|$)/i.test(value.trim()) ? "zh-CN" : "en";
|
|
7
|
+
}
|
|
8
|
+
export function validateLocale(value) {
|
|
9
|
+
if (value !== undefined && value !== "zh-CN" && value !== "en") {
|
|
10
|
+
throw new Error("locale must be zh-CN or en");
|
|
11
|
+
}
|
|
12
|
+
}
|
package/dist/quick-settings.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type StarLiteLocale } from "./locale.js";
|
|
1
2
|
export type QuickSettingCallback<T> = (value: T) => void | Promise<void>;
|
|
2
3
|
interface QuickSettingBase {
|
|
3
4
|
id: string;
|
|
@@ -30,6 +31,7 @@ export type QuickSettingItem = (QuickSettingBase & {
|
|
|
30
31
|
onAction: () => void | Promise<void>;
|
|
31
32
|
});
|
|
32
33
|
export interface QuickSettingsOptions {
|
|
34
|
+
locale?: StarLiteLocale;
|
|
33
35
|
title?: string;
|
|
34
36
|
tabs: readonly {
|
|
35
37
|
id: string;
|
package/dist/quick-settings.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveLocale, validateLocale } from "./locale.js";
|
|
1
2
|
const STYLE = `
|
|
2
3
|
[data-starlite-menu]{position:fixed;box-sizing:border-box;width:300px;max-width:calc(100vw - 16px);max-height:calc(100vh - 16px);overflow:auto;padding:14px;border:1px solid #9ba9c740;border-radius:16px;background:#1c2130f5;color:#f0f3ff;box-shadow:0 14px 48px #0005;font:14px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;z-index:2147483647;pointer-events:auto;user-select:none;backdrop-filter:blur(16px);color-scheme:dark;overscroll-behavior:contain}
|
|
3
4
|
[data-starlite-menu][hidden]{display:none!important}
|
|
@@ -158,6 +159,7 @@ export class QuickSettingsMenu {
|
|
|
158
159
|
this.#selects.clear();
|
|
159
160
|
}
|
|
160
161
|
#validate(options) {
|
|
162
|
+
validateLocale(options.locale);
|
|
161
163
|
const ids = new Set();
|
|
162
164
|
if (!options.tabs.length)
|
|
163
165
|
throw new Error("quick settings requires a tab");
|
|
@@ -176,7 +178,7 @@ export class QuickSettingsMenu {
|
|
|
176
178
|
}
|
|
177
179
|
#item(id) { return this.#options.tabs.flatMap(tab => tab.items).find(item => item.id === id); }
|
|
178
180
|
#render() {
|
|
179
|
-
const structure = JSON.stringify([this.#activeTab, this.#options.title, this.#options.tabs.map(tab => [tab.id, tab.label, tab.items.map(item => [item.id, item.kind, item.label, item.description, item.kind === "select" ? item.options : item.kind === "range" ? [item.min, item.max, item.step] : null])])]);
|
|
181
|
+
const structure = JSON.stringify([this.#activeTab, this.#options.title, this.#options.locale, this.#options.tabs.map(tab => [tab.id, tab.label, tab.items.map(item => [item.id, item.kind, item.label, item.description, item.kind === "select" ? item.options : item.kind === "range" ? [item.min, item.max, item.step] : null])])]);
|
|
180
182
|
if (structure === this.#structure) {
|
|
181
183
|
this.#sync();
|
|
182
184
|
return;
|
|
@@ -196,12 +198,12 @@ export class QuickSettingsMenu {
|
|
|
196
198
|
close.type = "button";
|
|
197
199
|
close.className = "starlite-menu__close";
|
|
198
200
|
close.textContent = "×";
|
|
199
|
-
close.setAttribute("aria-label", "关闭菜单");
|
|
201
|
+
close.setAttribute("aria-label", resolveLocale(this.#options.locale) === "en" ? "Close menu" : "关闭菜单");
|
|
200
202
|
close.onclick = () => this.close();
|
|
201
203
|
header.append(title, close);
|
|
202
204
|
const tabs = document.createElement("div");
|
|
203
205
|
tabs.setAttribute("role", "tablist");
|
|
204
|
-
tabs.setAttribute("aria-label", "设置分类");
|
|
206
|
+
tabs.setAttribute("aria-label", resolveLocale(this.#options.locale) === "en" ? "Settings categories" : "设置分类");
|
|
205
207
|
for (const [index, tab] of this.#options.tabs.entries()) {
|
|
206
208
|
const button = document.createElement("button");
|
|
207
209
|
button.type = "button";
|
|
@@ -490,7 +492,7 @@ export class QuickSettingsMenu {
|
|
|
490
492
|
if (this.#destroyed)
|
|
491
493
|
return;
|
|
492
494
|
const status = this.element.querySelector("[role=status]");
|
|
493
|
-
status.textContent = "未能应用设置,请重试。";
|
|
495
|
+
status.textContent = resolveLocale(this.#options.locale) === "en" ? "Could not apply this setting. Please try again." : "未能应用设置,请重试。";
|
|
494
496
|
status.hidden = false;
|
|
495
497
|
}
|
|
496
498
|
#position() {
|