@laplace.live/persona-sdk 0.9.0 → 0.11.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/dist/client/client.js +3 -0
- package/dist/effects.d.ts +79 -0
- package/dist/effects.js +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/values/custom-effect.d.ts +44 -0
- package/dist/values/custom-effect.js +136 -0
- package/dist/values/effect-schema.d.ts +18 -7
- package/dist/values/effect-schema.js +111 -2
- package/dist/values/limits.d.ts +2 -0
- package/dist/values/limits.js +6 -1
- package/dist/wire/schemas.d.ts +0 -2
- package/dist/wire/schemas.js +0 -1
- package/dist/wire/types.d.ts +165 -6
- package/package.json +15 -1
package/dist/client/client.js
CHANGED
|
@@ -89,6 +89,9 @@ export class PersonaClient {
|
|
|
89
89
|
this.settleConnect?.(new Error('client closed'));
|
|
90
90
|
const ws = this.ws;
|
|
91
91
|
this.ws = null;
|
|
92
|
+
// Synchronously, not in the attempt's `.finally` a microtask later: a connect()
|
|
93
|
+
// in this same tick must dial instead of joining the attempt just settled.
|
|
94
|
+
this.connectPromise = null;
|
|
92
95
|
ws?.close();
|
|
93
96
|
this.setState('closed');
|
|
94
97
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type * as TSL from 'three/tsl';
|
|
2
|
+
import type { Color, Node, Vector2, Vector3, Vector4 } from 'three/webgpu';
|
|
3
|
+
export * from './values/custom-effect.ts';
|
|
4
|
+
/** A vec4-typed TSL node with its swizzles — what `sample` returns and `build` composes over. */
|
|
5
|
+
export type CustomEffectVec4 = ReturnType<typeof TSL.nodeObject<Node<'vec4'>>>;
|
|
6
|
+
/** A float-typed TSL node with its operators — what `viewZ` returns. */
|
|
7
|
+
export type CustomEffectFloat = ReturnType<typeof TSL.nodeObject<Node<'float'>>>;
|
|
8
|
+
/** A vec3-typed TSL node with its swizzles — what the camera probes return. */
|
|
9
|
+
export type CustomEffectVec3 = ReturnType<typeof TSL.nodeObject<Node<'vec3'>>>;
|
|
10
|
+
/** What an author's module receives. Not a security boundary — just the useful surface. */
|
|
11
|
+
export interface CustomEffectApi {
|
|
12
|
+
/** The whole `three/tsl` namespace: `vec4`, `uv`, `uniform`, `mix`, `Fn`, … */
|
|
13
|
+
tsl: typeof TSL;
|
|
14
|
+
three: {
|
|
15
|
+
Color: typeof Color;
|
|
16
|
+
Vector2: typeof Vector2;
|
|
17
|
+
Vector3: typeof Vector3;
|
|
18
|
+
Vector4: typeof Vector4;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Stock three display nodes an effect cannot build for itself. `afterImage`
|
|
22
|
+
* is the seam for anything temporal: correct frame feedback needs two render
|
|
23
|
+
* targets ping-ponged plus a renderer reference, and `build()` hands authors
|
|
24
|
+
* neither. Upstream types return the bare class, hiding the TSL swizzles the
|
|
25
|
+
* runtime proxy carries; this signature types the echo as what authors use it
|
|
26
|
+
* as — a sampleable vec4 image.
|
|
27
|
+
*/
|
|
28
|
+
nodes: {
|
|
29
|
+
afterImage: (node: Node, damp?: Node | number) => CustomEffectVec4;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Build-time context for one custom effect. */
|
|
33
|
+
export interface CustomEffectBuildContext {
|
|
34
|
+
/** Sample the incoming frame at any UV — the seam that makes warps and blurs possible. */
|
|
35
|
+
sample: (at: unknown) => CustomEffectVec4;
|
|
36
|
+
/** Screen UV of the pixel being shaded. */
|
|
37
|
+
uv: typeof TSL.uv;
|
|
38
|
+
/**
|
|
39
|
+
* View-space Z of the 3D scene at this pixel — three's convention: 0 at the
|
|
40
|
+
* camera, more negative with distance, the far plane where nothing was drawn.
|
|
41
|
+
* The Live2D layer and 2D objects contribute no depth. Costs one depth read,
|
|
42
|
+
* and only if called.
|
|
43
|
+
*/
|
|
44
|
+
viewZ: () => CustomEffectFloat;
|
|
45
|
+
/**
|
|
46
|
+
* Normalized world-space direction of the scene camera's view ray through
|
|
47
|
+
* this pixel, fov and aspect folded in. Inside the post pass TSL's own
|
|
48
|
+
* `cameraPosition`/`cameraWorldMatrix` describe the fullscreen quad's
|
|
49
|
+
* camera — this is the real one. Anchor a field here instead of `uv()` and
|
|
50
|
+
* it holds still in the world while the camera orbits.
|
|
51
|
+
*/
|
|
52
|
+
worldRay: () => CustomEffectVec3;
|
|
53
|
+
/** The scene camera's world position in metres, as a per-frame uniform. */
|
|
54
|
+
cameraPosition: () => CustomEffectVec3;
|
|
55
|
+
/** Hand a build-time disposable (a blur render target) to the chain's transient list. */
|
|
56
|
+
track: (t: {
|
|
57
|
+
dispose?: () => void;
|
|
58
|
+
}) => void;
|
|
59
|
+
}
|
|
60
|
+
/** What an author's module returns. Everything but `build` is optional. */
|
|
61
|
+
export interface CustomEffectModule {
|
|
62
|
+
/** Uniform nodes keyed by manifest param name; param edits write straight into `.value`. */
|
|
63
|
+
uniforms?: Record<string, {
|
|
64
|
+
value: unknown;
|
|
65
|
+
}>;
|
|
66
|
+
/** Compose the effect over `input` — the frame so far, already sampleable — and return the result node. */
|
|
67
|
+
build: (input: Node, ctx: CustomEffectBuildContext) => Node;
|
|
68
|
+
/** Per-frame CPU state. `dt` and `elapsed` are seconds. */
|
|
69
|
+
update?: (dt: number, elapsed: number) => void;
|
|
70
|
+
dispose?: () => void;
|
|
71
|
+
}
|
|
72
|
+
/** The module's default export. Runs once per load — mint uniforms here so they survive rebuilds. */
|
|
73
|
+
export type CustomEffectFactory = (api: CustomEffectApi) => CustomEffectModule;
|
|
74
|
+
/** Why an effect is not rendering, for the panel's error row. */
|
|
75
|
+
export interface CustomEffectFault {
|
|
76
|
+
slug: string;
|
|
77
|
+
phase: 'load' | 'build' | 'update';
|
|
78
|
+
message: string;
|
|
79
|
+
}
|
package/dist/effects.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The custom-effect authoring contract: what an effect module receives, what it
|
|
2
|
+
// returns, and (re-exported) the manifest shape naming its params. Everything
|
|
3
|
+
// touching three is type-only — the desktop hands the runtime to the module's
|
|
4
|
+
// factory, because a module loaded from `persona://` can import nothing.
|
|
5
|
+
// Typechecking against this entry needs `@types/three` (optional peer).
|
|
6
|
+
export * from "./values/custom-effect.js";
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// and the values/registries every Persona app must agree on.
|
|
3
3
|
export * from "./client/address.js";
|
|
4
4
|
export * from "./client/client.js";
|
|
5
|
+
export * from "./values/custom-effect.js";
|
|
5
6
|
export * from "./values/effect-schema.js";
|
|
6
7
|
export * from "./values/guards.js";
|
|
7
8
|
export * from "./values/labels.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** How wide a manifest may open a slider; keeps a typo'd range from making one unusable. */
|
|
2
|
+
export declare const CUSTOM_EFFECT_PARAM_LIMIT = 1000000;
|
|
3
|
+
/** Enabled custom effects one scene may compose — each costs a full-frame RTT. Disabled entries are uncapped. */
|
|
4
|
+
export declare const CUSTOM_EFFECTS_MAX = 8;
|
|
5
|
+
/** Params per effect. Past this the panel section stops being navigable. */
|
|
6
|
+
export declare const CUSTOM_EFFECT_PARAM_MAX = 32;
|
|
7
|
+
export type CustomEffectParamKind = 'number' | 'boolean' | 'color';
|
|
8
|
+
/** One author-declared control. `kind` picks the panel widget and the healing rule. */
|
|
9
|
+
export interface CustomEffectParam {
|
|
10
|
+
kind: CustomEffectParamKind;
|
|
11
|
+
/** Slider/field label. Author-supplied, so never translated. */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Numbers: the clamp range and readout. Booleans and colors ignore these. */
|
|
14
|
+
default: number | boolean | string;
|
|
15
|
+
min?: number;
|
|
16
|
+
max?: number;
|
|
17
|
+
step?: number;
|
|
18
|
+
digits?: number;
|
|
19
|
+
unit?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A validated `manifest.json`. */
|
|
22
|
+
export interface CustomEffectManifest {
|
|
23
|
+
name: string;
|
|
24
|
+
/** Author-declared, shown in the panel's detail row. */
|
|
25
|
+
version?: string;
|
|
26
|
+
author?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
params: Record<string, CustomEffectParam>;
|
|
29
|
+
}
|
|
30
|
+
export declare function isCustomEffectSlug(v: unknown): v is string;
|
|
31
|
+
/**
|
|
32
|
+
* Validate a parsed `manifest.json`. Null when it carries no usable name —
|
|
33
|
+
* everything else degrades (a bad param is dropped, not fatal), because a
|
|
34
|
+
* half-typed manifest should still show the author what already works.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseCustomEffectManifest(raw: unknown): CustomEffectManifest | null;
|
|
37
|
+
/** Every declared param at its default — the params half of a fresh scene entry. */
|
|
38
|
+
export declare function defaultCustomEffectParams(manifest: CustomEffectManifest): Record<string, number | boolean | string>;
|
|
39
|
+
/**
|
|
40
|
+
* Clamp saved params to the manifest that is installed now. Unknown keys are
|
|
41
|
+
* kept: an author mid-edit who renames a param back should not find the value
|
|
42
|
+
* gone, and a param costs nothing until the module reads it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function healCustomEffectParams(raw: unknown, manifest: CustomEffectManifest | null): Record<string, number | boolean | string>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// User-authored effects: the manifest contract and its validator.
|
|
2
|
+
//
|
|
3
|
+
// An effect folder holds `manifest.json` (plain data — this file's shape) and
|
|
4
|
+
// `effect.js` (a TSL builder the stage worker evaluates). The split is what lets
|
|
5
|
+
// main heal a scene's saved params without ever running author code: only the
|
|
6
|
+
// worker imports the module, and it is the sole place third-party JS runs.
|
|
7
|
+
//
|
|
8
|
+
// Params mirror EffectParamSpec so the panel renders both kinds of effect with
|
|
9
|
+
// the same sliders — but these arrive at runtime from disk, so everything here
|
|
10
|
+
// validates rather than trusting the type.
|
|
11
|
+
import { finiteOr, isFiniteNumber, isRecord, nonEmptyString } from "./guards.js";
|
|
12
|
+
import { hexColorOr } from "./limits.js";
|
|
13
|
+
/** How wide a manifest may open a slider; keeps a typo'd range from making one unusable. */
|
|
14
|
+
export const CUSTOM_EFFECT_PARAM_LIMIT = 1e6;
|
|
15
|
+
/** Enabled custom effects one scene may compose — each costs a full-frame RTT. Disabled entries are uncapped. */
|
|
16
|
+
export const CUSTOM_EFFECTS_MAX = 8;
|
|
17
|
+
/** Params per effect. Past this the panel section stops being navigable. */
|
|
18
|
+
export const CUSTOM_EFFECT_PARAM_MAX = 32;
|
|
19
|
+
/** Folder-name rule: lowercase, dash-separated. Also the on-disk path segment, so no dots or slashes. */
|
|
20
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
21
|
+
export function isCustomEffectSlug(v) {
|
|
22
|
+
return typeof v === 'string' && SLUG_RE.test(v);
|
|
23
|
+
}
|
|
24
|
+
function bounded(v) {
|
|
25
|
+
return Math.min(CUSTOM_EFFECT_PARAM_LIMIT, Math.max(-CUSTOM_EFFECT_PARAM_LIMIT, v));
|
|
26
|
+
}
|
|
27
|
+
function parseParam(raw) {
|
|
28
|
+
if (!isRecord(raw))
|
|
29
|
+
return null;
|
|
30
|
+
const label = nonEmptyString(raw.label);
|
|
31
|
+
if (label === null)
|
|
32
|
+
return null;
|
|
33
|
+
if (raw.kind === 'boolean')
|
|
34
|
+
return { kind: 'boolean', label, default: raw.default === true };
|
|
35
|
+
if (raw.kind === 'color')
|
|
36
|
+
return { kind: 'color', label, default: hexColorOr(raw.default, '#ffffff') };
|
|
37
|
+
// Numbers are the default kind: an omitted or unknown `kind` is far more often
|
|
38
|
+
// a slider the author forgot to tag than a control they meant to drop.
|
|
39
|
+
// Both ends clamp into ±LIMIT before ordering, or a range wholly past the
|
|
40
|
+
// limit would keep one end beyond it.
|
|
41
|
+
const lo = bounded(finiteOr(raw.min, 0));
|
|
42
|
+
const hi = bounded(finiteOr(raw.max, 1));
|
|
43
|
+
// An inverted or empty range would leave a slider that cannot move; widen to the default's own value.
|
|
44
|
+
const min = Math.min(lo, hi);
|
|
45
|
+
const max = Math.max(lo, hi);
|
|
46
|
+
const def = Math.min(max, Math.max(min, finiteOr(raw.default, min)));
|
|
47
|
+
const step = Math.min(max - min || 1, Math.abs(finiteOr(raw.step, 0.01)) || 0.01);
|
|
48
|
+
return {
|
|
49
|
+
kind: 'number',
|
|
50
|
+
label,
|
|
51
|
+
default: def,
|
|
52
|
+
min,
|
|
53
|
+
max,
|
|
54
|
+
step,
|
|
55
|
+
...(isFiniteNumber(raw.digits) && { digits: Math.min(6, Math.max(0, Math.trunc(raw.digits))) }),
|
|
56
|
+
...(typeof raw.unit === 'string' && raw.unit !== '' && { unit: raw.unit.slice(0, 8) }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Validate a parsed `manifest.json`. Null when it carries no usable name —
|
|
61
|
+
* everything else degrades (a bad param is dropped, not fatal), because a
|
|
62
|
+
* half-typed manifest should still show the author what already works.
|
|
63
|
+
*/
|
|
64
|
+
export function parseCustomEffectManifest(raw) {
|
|
65
|
+
if (!isRecord(raw))
|
|
66
|
+
return null;
|
|
67
|
+
const name = nonEmptyString(raw.name);
|
|
68
|
+
if (name === null)
|
|
69
|
+
return null;
|
|
70
|
+
const params = {};
|
|
71
|
+
if (isRecord(raw.params)) {
|
|
72
|
+
for (const [key, value] of Object.entries(raw.params)) {
|
|
73
|
+
if (Object.keys(params).length >= CUSTOM_EFFECT_PARAM_MAX)
|
|
74
|
+
break;
|
|
75
|
+
// The key is a JS identifier on the uniforms object the module builds; a
|
|
76
|
+
// key it cannot name would silently never receive its value.
|
|
77
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(key))
|
|
78
|
+
continue;
|
|
79
|
+
const param = parseParam(value);
|
|
80
|
+
if (param)
|
|
81
|
+
params[key] = param;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
name: name.slice(0, 64),
|
|
86
|
+
params,
|
|
87
|
+
...(typeof raw.version === 'string' && raw.version !== '' && { version: raw.version.slice(0, 32) }),
|
|
88
|
+
...(typeof raw.author === 'string' && raw.author !== '' && { author: raw.author.slice(0, 64) }),
|
|
89
|
+
...(typeof raw.description === 'string' &&
|
|
90
|
+
raw.description !== '' && { description: raw.description.slice(0, 280) }),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Every declared param at its default — the params half of a fresh scene entry. */
|
|
94
|
+
export function defaultCustomEffectParams(manifest) {
|
|
95
|
+
const out = {};
|
|
96
|
+
for (const [key, param] of Object.entries(manifest.params))
|
|
97
|
+
out[key] = param.default;
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/** A primitive a saved param may hold; non-finite numbers are junk, not tuning. */
|
|
101
|
+
function isParamValue(v) {
|
|
102
|
+
return isFiniteNumber(v) || typeof v === 'boolean' || typeof v === 'string';
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Clamp saved params to the manifest that is installed now. Unknown keys are
|
|
106
|
+
* kept: an author mid-edit who renames a param back should not find the value
|
|
107
|
+
* gone, and a param costs nothing until the module reads it.
|
|
108
|
+
*/
|
|
109
|
+
export function healCustomEffectParams(raw, manifest) {
|
|
110
|
+
const src = isRecord(raw) ? raw : {};
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const [key, value] of Object.entries(src)) {
|
|
113
|
+
const spec = manifest?.params[key];
|
|
114
|
+
if (!spec) {
|
|
115
|
+
// No spec to clamp against — an unknown key, or no manifest at all (effect
|
|
116
|
+
// not installed on this machine); keeping primitives is what lets a
|
|
117
|
+
// reinstall restore the user's tuning.
|
|
118
|
+
if (isParamValue(value))
|
|
119
|
+
out[key] = value;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (spec.kind === 'boolean')
|
|
123
|
+
out[key] = typeof value === 'boolean' ? value : spec.default === true;
|
|
124
|
+
else if (spec.kind === 'color')
|
|
125
|
+
out[key] = hexColorOr(value, String(spec.default));
|
|
126
|
+
else {
|
|
127
|
+
const lo = spec.min ?? 0;
|
|
128
|
+
const hi = spec.max ?? 1;
|
|
129
|
+
out[key] = Math.min(hi, Math.max(lo, finiteOr(value, Number(spec.default))));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (manifest)
|
|
133
|
+
for (const [key, spec] of Object.entries(manifest.params))
|
|
134
|
+
out[key] ??= spec.default;
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SceneEffects } from '../wire/types.ts';
|
|
1
|
+
import type { SceneEffects, ToggleEffectKey } from '../wire/types.ts';
|
|
2
2
|
/** Slider + healing metadata for one numeric effect parameter. */
|
|
3
3
|
export interface EffectParamSpec {
|
|
4
4
|
default: number;
|
|
@@ -10,13 +10,17 @@ export interface EffectParamSpec {
|
|
|
10
10
|
/** Appended to the readout (`s`, `px`, …). */
|
|
11
11
|
unit?: string;
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} ? K : never;
|
|
18
|
-
}[keyof SceneEffects];
|
|
13
|
+
/** Healing metadata for one hex-color effect parameter (validated, not clamped). */
|
|
14
|
+
export interface EffectColorSpec {
|
|
15
|
+
default: string;
|
|
16
|
+
}
|
|
19
17
|
export declare const EFFECT_SPECS: Record<ToggleEffectKey, Readonly<Record<string, EffectParamSpec>>>;
|
|
18
|
+
/**
|
|
19
|
+
* Hex-color params, keyed like {@link EFFECT_SPECS} — generic walkers (healing,
|
|
20
|
+
* defaults, hotkey snapshots, the panel) visit both tables. Only effects with a
|
|
21
|
+
* string param appear.
|
|
22
|
+
*/
|
|
23
|
+
export declare const EFFECT_COLOR_SPECS: Partial<Record<ToggleEffectKey, Readonly<Record<string, EffectColorSpec>>>>;
|
|
20
24
|
export declare const TOGGLE_EFFECT_KEYS: readonly ToggleEffectKey[];
|
|
21
25
|
/**
|
|
22
26
|
* Registry effects that render as scene geometry inside the scene pass rather
|
|
@@ -26,5 +30,12 @@ export declare const TOGGLE_EFFECT_KEYS: readonly ToggleEffectKey[];
|
|
|
26
30
|
export declare const SCENE_SPACE_EFFECT_KEYS: readonly ToggleEffectKey[];
|
|
27
31
|
/** Registry effects the post chain composes — {@link TOGGLE_EFFECT_KEYS} minus the scene-space ones. */
|
|
28
32
|
export declare const POST_EFFECT_KEYS: readonly ToggleEffectKey[];
|
|
33
|
+
/** The canonical `effectLayers`: registry order, junk dropped, every switched-on effect included. */
|
|
34
|
+
export declare function effectLayerKeys(listed: readonly unknown[], effects: Pick<SceneEffects, ToggleEffectKey>): ToggleEffectKey[];
|
|
35
|
+
/**
|
|
36
|
+
* Composition order, bottom to top, with user-authored effects as one `'custom'` group.
|
|
37
|
+
* Display-only: the desktop chain's `rebuild()` is the order that runs — a stage moved there moves here too.
|
|
38
|
+
*/
|
|
39
|
+
export declare const EFFECT_STACK_ORDER: readonly (ToggleEffectKey | 'custom')[];
|
|
29
40
|
/** Every registry effect at its defaults — the derived half of a fresh {@link SceneEffects}. */
|
|
30
41
|
export declare function defaultToggleEffects(): Pick<SceneEffects, ToggleEffectKey>;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Defaults, healing clamps, active checks, structural keys, and the panel's
|
|
3
3
|
// sliders are all derived from this table, so adding an effect is:
|
|
4
4
|
// 1. Type its values in `SceneEffects` (types.ts) + its interface.
|
|
5
|
-
// 2. Spec it here
|
|
5
|
+
// 2. Spec it here (hex-color params go in EFFECT_COLOR_SPECS); label it in
|
|
6
|
+
// the panel's effect-labels.ts (parity-checked).
|
|
6
7
|
// 3. Implement its stage under apps/desktop/src/renderer/vrm/webgpu/effects/.
|
|
7
8
|
// Scene-space effects (geometry, not a post stage — e.g. snow) also join
|
|
8
9
|
// SCENE_SPACE_EFFECT_KEYS below.
|
|
@@ -27,14 +28,31 @@ export const EFFECT_SPECS = {
|
|
|
27
28
|
// Threshold 0.8 catches highlights without hazing the whole avatar; radius is the lib default.
|
|
28
29
|
threshold: { default: 0.8, min: 0, max: 1, step: 0.01 },
|
|
29
30
|
radius: { default: 0.85, min: 0, max: 1, step: 0.01 },
|
|
31
|
+
// VTS bloom's anamorphic-flare half. Streak 0 keeps the passes out of the
|
|
32
|
+
// graph; the angle is VTS's horizontal/vertical toggle made continuous.
|
|
33
|
+
streak: { default: 0, min: 0, max: 3, step: 0.01 },
|
|
34
|
+
streakThreshold: { default: 0.8, min: 0, max: 1, step: 0.01 },
|
|
35
|
+
streakAngle: { default: 0, min: -90, max: 90, step: 1, unit: '°' },
|
|
36
|
+
darken: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
37
|
+
},
|
|
38
|
+
// Shoost's diffusion (its Kino bloom variant, sliders Radius/Intensity/
|
|
39
|
+
// Contrast→threshold): screen-composited, so its Opacity slider folds into
|
|
40
|
+
// intensity. Threshold 0.35 mists the midtones up — the stock Pro-Mist read.
|
|
41
|
+
diffusion: {
|
|
42
|
+
intensity: { default: 1, min: 0, max: 2, step: 0.01 },
|
|
43
|
+
radius: { default: 0.85, min: 0, max: 1, step: 0.01 },
|
|
44
|
+
threshold: { default: 0.35, min: 0, max: 1, step: 0.01 },
|
|
30
45
|
},
|
|
31
46
|
dof: {
|
|
32
47
|
bokehScale: { default: 2, min: 0, max: 8, step: 0.01 },
|
|
33
48
|
/** World metres of acceptably-sharp depth around the focus plane. */
|
|
34
49
|
focusRange: { default: 2, min: 0.1, max: 10, step: 0.01 },
|
|
35
50
|
},
|
|
51
|
+
// VTube Studio's chromatic_aberration surface: strength + blur edges, the
|
|
52
|
+
// latter continuous like rim.bothSides (VTS fades the same shader uniform).
|
|
36
53
|
chromaticAberration: {
|
|
37
54
|
strength: { default: 0.2, min: 0, max: 1, step: 0.01 },
|
|
55
|
+
blurEdges: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
38
56
|
},
|
|
39
57
|
grain: {
|
|
40
58
|
strength: { default: 0.3, min: 0, max: 1, step: 0.01 },
|
|
@@ -43,6 +61,36 @@ export const EFFECT_SPECS = {
|
|
|
43
61
|
darkness: { default: 0.5, min: 0, max: 1, step: 0.01 },
|
|
44
62
|
offset: { default: 0.5, min: 0, max: 1, step: 0.01 },
|
|
45
63
|
},
|
|
64
|
+
// Shoost's rim-light ranges where the control carried over (size, angle,
|
|
65
|
+
// opacity→intensity); the rest follows VTube Studio's backlight. New params
|
|
66
|
+
// default neutral (omni 0, limit 1, darken 0) so healed scenes keep their look.
|
|
67
|
+
rim: {
|
|
68
|
+
intensity: { default: 1, min: 0, max: 2, step: 0.01 },
|
|
69
|
+
omni: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
70
|
+
size: { default: 16, min: 1, max: 64, step: 1, unit: 'px' },
|
|
71
|
+
softness: { default: 0.6, min: 0, max: 1, step: 0.01 },
|
|
72
|
+
angle: { default: 45, min: 0, max: 360, step: 1, unit: '°' },
|
|
73
|
+
bothSides: { default: 0.5, min: 0, max: 1, step: 0.01 },
|
|
74
|
+
brightnessLimit: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
75
|
+
darken: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
76
|
+
},
|
|
77
|
+
// VTube Studio's backlight outline block: stripe count/speed/curve ranges are
|
|
78
|
+
// VTS's; size is ours (px, like rim) instead of VTS's normalized 0..1.
|
|
79
|
+
outline: {
|
|
80
|
+
size: { default: 8, min: 1, max: 64, step: 1, unit: 'px' },
|
|
81
|
+
opacity: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
82
|
+
stripes: { default: 0.5, min: 0, max: 1, step: 0.01 },
|
|
83
|
+
stripeMix: { default: 1, min: 0, max: 1, step: 0.01 },
|
|
84
|
+
stripeSpeed: { default: 0.2, min: -1, max: 1, step: 0.01 },
|
|
85
|
+
stripeCurve: { default: 0, min: 0, max: 1, step: 0.01 },
|
|
86
|
+
},
|
|
87
|
+
// VTube Studio's backlight shadow: an offset silhouette copy. VTS hides it at
|
|
88
|
+
// zero offset; ours defaults to a visible down-right cast so enabling shows it.
|
|
89
|
+
dropShadow: {
|
|
90
|
+
opacity: { default: 0.6, min: 0, max: 1, step: 0.01 },
|
|
91
|
+
offsetX: { default: 3, min: -20, max: 20, step: 0.5, digits: 1, unit: '%' },
|
|
92
|
+
offsetY: { default: 3, min: -20, max: 20, step: 0.5, digits: 1, unit: '%' },
|
|
93
|
+
},
|
|
46
94
|
pixelate: {
|
|
47
95
|
// Pixels per block; past ~64 the frame is a handful of tiles.
|
|
48
96
|
granularity: { default: 8, min: 2, max: 64, step: 1 },
|
|
@@ -74,6 +122,17 @@ export const EFFECT_SPECS = {
|
|
|
74
122
|
staticDrops: { default: 0.2, min: 0, max: 3, step: 0.1, digits: 1 },
|
|
75
123
|
glints: { default: 0.6, min: 0, max: 1, step: 0.05 },
|
|
76
124
|
},
|
|
125
|
+
// VTS's weather rain rebuilt in world space: velocity-aligned streaks that
|
|
126
|
+
// slant with wind and splash where they land. `amount` is the live instance count.
|
|
127
|
+
rain: {
|
|
128
|
+
amount: { default: 4000, min: 100, max: 10000, step: 100 },
|
|
129
|
+
speed: { default: 1, min: 0.2, max: 3, step: 0.1, digits: 1, unit: '×' },
|
|
130
|
+
wind: { default: 0, min: -6, max: 6, step: 0.1, digits: 1, unit: 'm/s' },
|
|
131
|
+
length: { default: 1, min: 0.2, max: 3, step: 0.05 },
|
|
132
|
+
size: { default: 1, min: 0.4, max: 3, step: 0.05 },
|
|
133
|
+
splash: { default: 0.7, min: 0, max: 1, step: 0.05 },
|
|
134
|
+
opacity: { default: 0.4, min: 0.05, max: 1, step: 0.05 },
|
|
135
|
+
},
|
|
77
136
|
// three's webgpu_compute_particles_snow, rescaled to the stage's metre-scale
|
|
78
137
|
// world. `amount` is the live instance count; buffers allocate the max once.
|
|
79
138
|
snow: {
|
|
@@ -86,6 +145,28 @@ export const EFFECT_SPECS = {
|
|
|
86
145
|
opacity: { default: 1, min: 0.1, max: 1, step: 0.05 },
|
|
87
146
|
},
|
|
88
147
|
};
|
|
148
|
+
/**
|
|
149
|
+
* Hex-color params, keyed like {@link EFFECT_SPECS} — generic walkers (healing,
|
|
150
|
+
* defaults, hotkey snapshots, the panel) visit both tables. Only effects with a
|
|
151
|
+
* string param appear.
|
|
152
|
+
*/
|
|
153
|
+
export const EFFECT_COLOR_SPECS = {
|
|
154
|
+
bloom: {
|
|
155
|
+
tint: { default: '#ffffff' },
|
|
156
|
+
// Beautify's anamorphic-flare tint default — the classic lens-streak blue.
|
|
157
|
+
streakTint: { default: '#8080ff' },
|
|
158
|
+
},
|
|
159
|
+
rim: {
|
|
160
|
+
color: { default: '#ffffff' },
|
|
161
|
+
},
|
|
162
|
+
outline: {
|
|
163
|
+
color: { default: '#ffffff' },
|
|
164
|
+
stripeColor: { default: '#000000' },
|
|
165
|
+
},
|
|
166
|
+
dropShadow: {
|
|
167
|
+
color: { default: '#000000' },
|
|
168
|
+
},
|
|
169
|
+
};
|
|
89
170
|
// Object.keys widens to string[]; the annotation above pins the keys to exactly ToggleEffectKey.
|
|
90
171
|
export const TOGGLE_EFFECT_KEYS = Object.keys(EFFECT_SPECS);
|
|
91
172
|
/**
|
|
@@ -93,9 +174,35 @@ export const TOGGLE_EFFECT_KEYS = Object.keys(EFFECT_SPECS);
|
|
|
93
174
|
* than composing into the post chain. A new scene-space effect must join this
|
|
94
175
|
* list, or its toggle needlessly forces the post path on and rebuilds the graph.
|
|
95
176
|
*/
|
|
96
|
-
export const SCENE_SPACE_EFFECT_KEYS = ['snow'];
|
|
177
|
+
export const SCENE_SPACE_EFFECT_KEYS = ['rain', 'snow'];
|
|
97
178
|
/** Registry effects the post chain composes — {@link TOGGLE_EFFECT_KEYS} minus the scene-space ones. */
|
|
98
179
|
export const POST_EFFECT_KEYS = TOGGLE_EFFECT_KEYS.filter(key => !SCENE_SPACE_EFFECT_KEYS.includes(key));
|
|
180
|
+
/** The canonical `effectLayers`: registry order, junk dropped, every switched-on effect included. */
|
|
181
|
+
export function effectLayerKeys(listed, effects) {
|
|
182
|
+
return TOGGLE_EFFECT_KEYS.filter(key => listed.includes(key) || effects[key].enabled);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Composition order, bottom to top, with user-authored effects as one `'custom'` group.
|
|
186
|
+
* Display-only: the desktop chain's `rebuild()` is the order that runs — a stage moved there moves here too.
|
|
187
|
+
*/
|
|
188
|
+
export const EFFECT_STACK_ORDER = [
|
|
189
|
+
'rain',
|
|
190
|
+
'snow',
|
|
191
|
+
'dof',
|
|
192
|
+
'rim',
|
|
193
|
+
'outline',
|
|
194
|
+
'dropShadow',
|
|
195
|
+
'chromaticAberration',
|
|
196
|
+
'bloom',
|
|
197
|
+
'diffusion',
|
|
198
|
+
'color',
|
|
199
|
+
'pixelate',
|
|
200
|
+
'glitch',
|
|
201
|
+
'droplets',
|
|
202
|
+
'custom',
|
|
203
|
+
'grain',
|
|
204
|
+
'vignette',
|
|
205
|
+
];
|
|
99
206
|
/** Every registry effect at its defaults — the derived half of a fresh {@link SceneEffects}. */
|
|
100
207
|
export function defaultToggleEffects() {
|
|
101
208
|
const out = {};
|
|
@@ -103,6 +210,8 @@ export function defaultToggleEffects() {
|
|
|
103
210
|
const fx = { enabled: false };
|
|
104
211
|
for (const [param, spec] of Object.entries(EFFECT_SPECS[key]))
|
|
105
212
|
fx[param] = spec.default;
|
|
213
|
+
for (const [param, spec] of Object.entries(EFFECT_COLOR_SPECS[key] ?? {}))
|
|
214
|
+
fx[param] = spec.default;
|
|
106
215
|
out[key] = fx;
|
|
107
216
|
}
|
|
108
217
|
// Runtime-built, invisible to TS; EFFECT_SPECS's satisfies check is what pins
|
package/dist/values/limits.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare const DEFAULT_LIVE2D_PLACEMENT: ScreenPlacement;
|
|
|
6
6
|
export declare const DEFAULT_VRM_PLACEMENT: VrmPlacement;
|
|
7
7
|
/** Scene colors heal to 6-digit hex. */
|
|
8
8
|
export declare const SCENE_COLOR_RE: RegExp;
|
|
9
|
+
/** The value when it is a 6-digit hex color, else `fallback` — validated, never clamped. */
|
|
10
|
+
export declare function hexColorOr(v: unknown, fallback: string): string;
|
|
9
11
|
/** Drop the alpha byte a picker's hex input can produce (`#rrggbbaa` → `#rrggbb`). */
|
|
10
12
|
export declare function opaqueHex(hex: string): string;
|
|
11
13
|
export declare const SCENE_LIGHT_INTENSITY_MAX = 2;
|
package/dist/values/limits.js
CHANGED
|
@@ -9,6 +9,10 @@ export const DEFAULT_LIVE2D_PLACEMENT = { x: 0, y: 0, scale: 1, rotation: 0 };
|
|
|
9
9
|
export const DEFAULT_VRM_PLACEMENT = { x: 0, y: 0, z: 0, rotX: 0, rotY: 0, rotZ: 0, scale: 1 };
|
|
10
10
|
/** Scene colors heal to 6-digit hex. */
|
|
11
11
|
export const SCENE_COLOR_RE = /^#[0-9a-f]{6}$/i;
|
|
12
|
+
/** The value when it is a 6-digit hex color, else `fallback` — validated, never clamped. */
|
|
13
|
+
export function hexColorOr(v, fallback) {
|
|
14
|
+
return typeof v === 'string' && SCENE_COLOR_RE.test(v) ? v : fallback;
|
|
15
|
+
}
|
|
12
16
|
/** Drop the alpha byte a picker's hex input can produce (`#rrggbbaa` → `#rrggbb`). */
|
|
13
17
|
export function opaqueHex(hex) {
|
|
14
18
|
return /^#[0-9a-f]{8}$/i.test(hex) ? hex.slice(0, 7) : hex;
|
|
@@ -64,7 +68,8 @@ export function defaultSceneLightOf(type) {
|
|
|
64
68
|
id: crypto.randomUUID(),
|
|
65
69
|
type,
|
|
66
70
|
color: '#ffffff',
|
|
67
|
-
|
|
71
|
+
// Ambient is a flat albedo floor with no shading — 1.0 alone washes the model out.
|
|
72
|
+
intensity: type === 'ambient' ? 0.4 : 1,
|
|
68
73
|
azimuth: DEFAULT_LIGHT_AZIMUTH_DEG,
|
|
69
74
|
elevation: DEFAULT_LIGHT_ELEVATION_DEG,
|
|
70
75
|
x: 0,
|
package/dist/wire/schemas.d.ts
CHANGED
|
@@ -29,7 +29,6 @@ export declare const SettingsPatchSchema: z.ZodObject<{
|
|
|
29
29
|
trayVisible: z.ZodOptional<z.ZodBoolean>;
|
|
30
30
|
}, z.core.$strip>>;
|
|
31
31
|
performance: z.ZodOptional<z.ZodObject<{
|
|
32
|
-
backgroundRendering: z.ZodOptional<z.ZodBoolean>;
|
|
33
32
|
showFps: z.ZodOptional<z.ZodBoolean>;
|
|
34
33
|
fpsLimit: z.ZodOptional<z.ZodNumber>;
|
|
35
34
|
selectionOutline: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -139,7 +138,6 @@ export declare const requestSchemas: {
|
|
|
139
138
|
trayVisible: z.ZodOptional<z.ZodBoolean>;
|
|
140
139
|
}, z.core.$strip>>;
|
|
141
140
|
performance: z.ZodOptional<z.ZodObject<{
|
|
142
|
-
backgroundRendering: z.ZodOptional<z.ZodBoolean>;
|
|
143
141
|
showFps: z.ZodOptional<z.ZodBoolean>;
|
|
144
142
|
fpsLimit: z.ZodOptional<z.ZodNumber>;
|
|
145
143
|
selectionOutline: z.ZodOptional<z.ZodBoolean>;
|
package/dist/wire/schemas.js
CHANGED
|
@@ -22,7 +22,6 @@ export const SettingsPatchSchema = z.object({
|
|
|
22
22
|
ui: z.object({ trayVisible: z.boolean().optional() }).optional(),
|
|
23
23
|
performance: z
|
|
24
24
|
.object({
|
|
25
|
-
backgroundRendering: z.boolean().optional(),
|
|
26
25
|
showFps: z.boolean().optional(),
|
|
27
26
|
fpsLimit: z.number().optional(),
|
|
28
27
|
selectionOutline: z.boolean().optional(),
|
package/dist/wire/types.d.ts
CHANGED
|
@@ -3,8 +3,11 @@ export type ModelFormat = 'live2d' | 'vrm';
|
|
|
3
3
|
export type ContentOrigin = 'bundled' | 'user';
|
|
4
4
|
/** How a registered file is labelled. Wider than an object's content kinds: `.hdr` is only ever an environment map. */
|
|
5
5
|
export type AssetKind = 'image' | 'video' | 'prop' | 'ibl' | 'lut' | 'animation';
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Every content kind the Inventory can list. `pngtuber` is schema-ready before any
|
|
8
|
+
* producer exists; `effect` rows are post-processing effects (built-in or installed), not files.
|
|
9
|
+
*/
|
|
10
|
+
export type InventoryKind = ModelFormat | 'pngtuber' | AssetKind | 'effect';
|
|
8
11
|
/**
|
|
9
12
|
* What every registry entry carries, model or asset alike. `kind` is always *what
|
|
10
13
|
* the thing is* and `origin` always *where it came from* — the two were once
|
|
@@ -280,12 +283,42 @@ export interface SceneFog {
|
|
|
280
283
|
}
|
|
281
284
|
/** Display transform applied after the scene renders. `none` keeps colors exactly as authored. */
|
|
282
285
|
export type SceneToneMapping = 'none' | 'neutral' | 'aces' | 'agx';
|
|
283
|
-
/**
|
|
286
|
+
/**
|
|
287
|
+
* Glow around bright pixels, with anamorphic streak flares on the side
|
|
288
|
+
* (VTube Studio's Beautify-based bloom). `threshold` is the luminance floor;
|
|
289
|
+
* `radius` widens the halo.
|
|
290
|
+
*/
|
|
284
291
|
export interface SceneBloom {
|
|
285
292
|
enabled: boolean;
|
|
286
293
|
intensity: number;
|
|
287
294
|
threshold: number;
|
|
288
295
|
radius: number;
|
|
296
|
+
/** Halo tint (hex); white is untinted. */
|
|
297
|
+
tint: string;
|
|
298
|
+
/** Anamorphic streak strength; 0 keeps the streak passes out of the graph. */
|
|
299
|
+
streak: number;
|
|
300
|
+
/** Luminance floor for the streak's own bright pass. */
|
|
301
|
+
streakThreshold: number;
|
|
302
|
+
/** Streak axis in degrees: 0 horizontal, ±90 vertical (VTS's vertical toggle, continuous). */
|
|
303
|
+
streakAngle: number;
|
|
304
|
+
/** Streak tint (hex); classic anamorphic flares are light blue. */
|
|
305
|
+
streakTint: string;
|
|
306
|
+
/** Darkens the avatar under the glow so highlights pop (VTS's model darken), 0..1. */
|
|
307
|
+
darken: number;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Cinematic soft-focus veil (Shoost's diffusion, a Kino bloom variant): the
|
|
311
|
+
* frame's own light blurred wide and screened back over itself, so highlights
|
|
312
|
+
* halo and nearby shadows lift without bloom's additive blowout.
|
|
313
|
+
*/
|
|
314
|
+
export interface SceneDiffusion {
|
|
315
|
+
enabled: boolean;
|
|
316
|
+
/** Veil strength; screen-composited, so 1 stays short of clipping. */
|
|
317
|
+
intensity: number;
|
|
318
|
+
/** Veil spread: 0 tight halos, 1 the widest wash. */
|
|
319
|
+
radius: number;
|
|
320
|
+
/** Luminance floor that feeds the veil: 0 mists the whole frame, higher keeps it to highlights. */
|
|
321
|
+
threshold: number;
|
|
289
322
|
}
|
|
290
323
|
/** Darkened frame corners. `offset` pushes the falloff outward. */
|
|
291
324
|
export interface SceneVignette {
|
|
@@ -301,10 +334,13 @@ export interface SceneColorGrade {
|
|
|
301
334
|
brightness: number;
|
|
302
335
|
contrast: number;
|
|
303
336
|
}
|
|
304
|
-
/**
|
|
337
|
+
/** Lens fringing that grows toward frame edges (Unity PPv2's curve, the one VTube Studio wraps). */
|
|
305
338
|
export interface SceneChromaticAberration {
|
|
306
339
|
enabled: boolean;
|
|
340
|
+
/** Fringe displacement 0..1; 1 smears ~10% of the frame at the corners. */
|
|
307
341
|
strength: number;
|
|
342
|
+
/** 0 = three crisp RGB ghosts, 1 = fully integrated spectral smear (VTS's "blur edges"). */
|
|
343
|
+
blurEdges: number;
|
|
308
344
|
}
|
|
309
345
|
/** Animated film grain. `strength` is blend opacity 0..1. */
|
|
310
346
|
export interface SceneFilmGrain {
|
|
@@ -378,6 +414,89 @@ export interface SceneDroplets {
|
|
|
378
414
|
/** Specular glint on drops where the frame is transparent, so rain reads over the desktop (0 to 1). */
|
|
379
415
|
glints: number;
|
|
380
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Screen-space rim light along the avatar silhouette (Shoost's layer rim light,
|
|
419
|
+
* VTube Studio's backlight): coverage edges facing `angle` catch the glow.
|
|
420
|
+
* Silhouette-driven — an opaque skybox leaves no edges to light.
|
|
421
|
+
*/
|
|
422
|
+
export interface SceneRim {
|
|
423
|
+
enabled: boolean;
|
|
424
|
+
/** Rim tint (hex). */
|
|
425
|
+
color: string;
|
|
426
|
+
/** Glow strength; past 1 overdrives into bloom territory. */
|
|
427
|
+
intensity: number;
|
|
428
|
+
/** Rim width in output pixels. */
|
|
429
|
+
size: number;
|
|
430
|
+
/** Edge falloff: 0 a crisp line, 1 a wide soft fade. */
|
|
431
|
+
softness: number;
|
|
432
|
+
/** Light direction in degrees, compass-style: 0 lights from above, 90 from the right. */
|
|
433
|
+
angle: number;
|
|
434
|
+
/** How much the edge opposite the light glows too (0 to 1). */
|
|
435
|
+
bothSides: number;
|
|
436
|
+
/** Uniform glow on every edge regardless of `angle` (VTS's main backlight strength), 0..1. */
|
|
437
|
+
omni: number;
|
|
438
|
+
/** Ceiling the glow lightens pixels toward: 1 screens to white, lower protects highlights. */
|
|
439
|
+
brightnessLimit: number;
|
|
440
|
+
/** Darkens the avatar under the glow for contrast (VTS's model darken), 0..1. */
|
|
441
|
+
darken: number;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Contour band hugging the avatar silhouette, with an optional animated stripe
|
|
445
|
+
* pattern scrolling through it (VTube Studio's backlight outline). Silhouette-
|
|
446
|
+
* driven like the rim light — it needs coverage edges to trace.
|
|
447
|
+
*/
|
|
448
|
+
export interface SceneOutline {
|
|
449
|
+
enabled: boolean;
|
|
450
|
+
/** Outline color (hex). */
|
|
451
|
+
color: string;
|
|
452
|
+
/** Stripe color (hex), painted over `color` where the stripe pattern lands. */
|
|
453
|
+
stripeColor: string;
|
|
454
|
+
/** Band thickness in output pixels. */
|
|
455
|
+
size: number;
|
|
456
|
+
/** Outline opacity 0..1. */
|
|
457
|
+
opacity: number;
|
|
458
|
+
/** Stripe density: 0 broad bands, 1 fine candy stripes. */
|
|
459
|
+
stripes: number;
|
|
460
|
+
/** Stripe visibility: 0 a solid outline, 1 full-strength stripes. */
|
|
461
|
+
stripeMix: number;
|
|
462
|
+
/** Stripe scroll speed; negative reverses the direction. */
|
|
463
|
+
stripeSpeed: number;
|
|
464
|
+
/** Bends the stripes into waves: 0 straight, 1 strongly curled. */
|
|
465
|
+
stripeCurve: number;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Hard-edged copy of the avatar silhouette cast behind it (VTube Studio's
|
|
469
|
+
* backlight shadow). Offsets are percent of the frame height.
|
|
470
|
+
*/
|
|
471
|
+
export interface SceneDropShadow {
|
|
472
|
+
enabled: boolean;
|
|
473
|
+
/** Shadow color (hex). */
|
|
474
|
+
color: string;
|
|
475
|
+
/** Shadow opacity 0..1. */
|
|
476
|
+
opacity: number;
|
|
477
|
+
/** Horizontal offset, percent of frame height; positive casts right. */
|
|
478
|
+
offsetX: number;
|
|
479
|
+
/** Vertical offset, percent of frame height; positive casts down. */
|
|
480
|
+
offsetY: number;
|
|
481
|
+
}
|
|
482
|
+
/** Compute-driven 3D rainfall: motion-blur streaks fall through the scene and splash on models, props, and the floor. */
|
|
483
|
+
export interface SceneRain {
|
|
484
|
+
enabled: boolean;
|
|
485
|
+
/** How many drops are alive at once. */
|
|
486
|
+
amount: number;
|
|
487
|
+
/** Fall clock multiplier. */
|
|
488
|
+
speed: number;
|
|
489
|
+
/** Sideways drift in metres per second; slants the streaks. */
|
|
490
|
+
wind: number;
|
|
491
|
+
/** Streak length multiplier over the motion-blur baseline. */
|
|
492
|
+
length: number;
|
|
493
|
+
/** Streak width multiplier. */
|
|
494
|
+
size: number;
|
|
495
|
+
/** Impact splash strength; 0 recycles drops with no splash. */
|
|
496
|
+
splash: number;
|
|
497
|
+
/** Streak opacity. */
|
|
498
|
+
opacity: number;
|
|
499
|
+
}
|
|
381
500
|
/** Compute-driven snowfall in the 3D scene: flakes drift down, settle on models and props, then melt. */
|
|
382
501
|
export interface SceneSnow {
|
|
383
502
|
enabled: boolean;
|
|
@@ -407,17 +526,46 @@ export interface SceneEffects {
|
|
|
407
526
|
/** Scene brightness multiplied in before the tone curve; 1 is neutral. Works in every mode, including `none`. */
|
|
408
527
|
exposure: number;
|
|
409
528
|
bloom: SceneBloom;
|
|
529
|
+
diffusion: SceneDiffusion;
|
|
410
530
|
vignette: SceneVignette;
|
|
411
531
|
color: SceneColorGrade;
|
|
412
532
|
chromaticAberration: SceneChromaticAberration;
|
|
413
533
|
grain: SceneFilmGrain;
|
|
414
534
|
lut: SceneLut;
|
|
415
535
|
dof: SceneDepthOfField;
|
|
536
|
+
rim: SceneRim;
|
|
537
|
+
outline: SceneOutline;
|
|
538
|
+
dropShadow: SceneDropShadow;
|
|
416
539
|
pixelate: ScenePixelate;
|
|
417
540
|
glitch: SceneGlitch;
|
|
418
541
|
droplets: SceneDroplets;
|
|
542
|
+
rain: SceneRain;
|
|
419
543
|
snow: SceneSnow;
|
|
420
544
|
}
|
|
545
|
+
/** Keys of {@link SceneEffects} that follow the `{ enabled } + params` pattern. */
|
|
546
|
+
export type ToggleEffectKey = {
|
|
547
|
+
[K in keyof SceneEffects]: SceneEffects[K] extends {
|
|
548
|
+
enabled: boolean;
|
|
549
|
+
} ? K : never;
|
|
550
|
+
}[keyof SceneEffects];
|
|
551
|
+
/**
|
|
552
|
+
* One user-authored effect's placement in a scene. Deliberately a sibling of
|
|
553
|
+
* {@link SceneEffects} rather than a key inside it: `ToggleEffectKey` is derived
|
|
554
|
+
* from that interface, and healing, defaults, hotkey snapshots and the panel all
|
|
555
|
+
* iterate it as a closed compile-time union. Runtime keys in there would erase
|
|
556
|
+
* that guarantee for the built-in effects too.
|
|
557
|
+
*/
|
|
558
|
+
export interface SceneCustomEffect {
|
|
559
|
+
/** Folder name under the config home's `effects/` dir — stable id and module location. */
|
|
560
|
+
slug: string;
|
|
561
|
+
enabled: boolean;
|
|
562
|
+
/**
|
|
563
|
+
* Author-declared values, keyed by the manifest's param names. Untyped by
|
|
564
|
+
* construction: the specs arrive from disk at runtime, so these are validated
|
|
565
|
+
* against the installed manifest rather than checked at compile time.
|
|
566
|
+
*/
|
|
567
|
+
params: Record<string, number | boolean | string>;
|
|
568
|
+
}
|
|
421
569
|
/**
|
|
422
570
|
* Image-based lighting for the 3D stage: an environment map, whether to show it
|
|
423
571
|
* behind the scene, the scene-global fog, and post-processing.
|
|
@@ -429,6 +577,19 @@ export interface SceneEnvironment {
|
|
|
429
577
|
showSkybox: boolean;
|
|
430
578
|
fog: SceneFog;
|
|
431
579
|
effects: SceneEffects;
|
|
580
|
+
/**
|
|
581
|
+
* Built-in effects added as layers, in registry order — one keeps its row and tuning while
|
|
582
|
+
* switched off. Healing lists every switched-on effect, so a patch that switches one on adds its layer.
|
|
583
|
+
*/
|
|
584
|
+
effectLayers: ToggleEffectKey[];
|
|
585
|
+
/**
|
|
586
|
+
* User-authored effects, in the order they compose. They run as a group at one
|
|
587
|
+
* fixed point in the post chain — after grading and the screen-space warps,
|
|
588
|
+
* before grain and vignette — so the built-in composition order stays fixed.
|
|
589
|
+
* An entry whose effect is not installed here is kept, not dropped: it would
|
|
590
|
+
* cost the user their tuning on a machine that simply lacks the folder.
|
|
591
|
+
*/
|
|
592
|
+
customEffects: SceneCustomEffect[];
|
|
432
593
|
}
|
|
433
594
|
export interface Scene {
|
|
434
595
|
id: string;
|
|
@@ -606,7 +767,6 @@ export interface Settings {
|
|
|
606
767
|
trayVisible: boolean;
|
|
607
768
|
};
|
|
608
769
|
performance: {
|
|
609
|
-
backgroundRendering: boolean;
|
|
610
770
|
showFps: boolean;
|
|
611
771
|
fpsLimit: number;
|
|
612
772
|
selectionOutline: boolean;
|
|
@@ -633,7 +793,6 @@ export interface SettingsPatch {
|
|
|
633
793
|
trayVisible?: boolean;
|
|
634
794
|
};
|
|
635
795
|
performance?: {
|
|
636
|
-
backgroundRendering?: boolean;
|
|
637
796
|
showFps?: boolean;
|
|
638
797
|
fpsLimit?: number;
|
|
639
798
|
selectionOutline?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@laplace.live/persona-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "TypeScript SDK and wire schema for the LAPLACE Persona plugin API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
".": {
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"import": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./effects": {
|
|
21
|
+
"types": "./dist/effects.d.ts",
|
|
22
|
+
"import": "./dist/effects.js"
|
|
19
23
|
}
|
|
20
24
|
},
|
|
21
25
|
"files": [
|
|
@@ -26,10 +30,20 @@
|
|
|
26
30
|
"provenance": true
|
|
27
31
|
},
|
|
28
32
|
"devDependencies": {
|
|
33
|
+
"@types/three": "^0.185.4",
|
|
29
34
|
"rimraf": "^6.1.3",
|
|
35
|
+
"three": "0.185.1",
|
|
30
36
|
"typescript": "~6.0.3",
|
|
31
37
|
"vitest": "^4.1.11"
|
|
32
38
|
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@types/three": "^0.185.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"@types/three": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
33
47
|
"engines": {
|
|
34
48
|
"node": ">=22"
|
|
35
49
|
},
|