hayao 0.4.0 → 0.4.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/dist/anim/blend.d.ts +68 -0
- package/dist/anim/clip.d.ts +87 -0
- package/dist/anim/ik.d.ts +40 -0
- package/dist/anim/skeleton.d.ts +64 -0
- package/dist/hayao.global.js +12 -12
- package/dist/index.d.ts +11 -1
- package/dist/index.js +1683 -516
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +12 -12
- package/dist/render/canvas2d-core.d.ts +4 -0
- package/dist/render/commands.d.ts +19 -0
- package/dist/render/lightRun.d.ts +35 -0
- package/dist/render/svgString.d.ts +8 -3
- package/dist/scene/clipPlayer.d.ts +58 -0
- package/dist/scene/ikTarget.d.ts +25 -0
- package/dist/scene/light.d.ts +80 -0
- package/dist/scene/shadow2d.d.ts +25 -0
- package/dist/scene/skeletonDebug.d.ts +28 -0
- package/dist/verify/gates.d.ts +25 -0
- package/docs/API.md +66 -8
- package/docs/CONVENTIONS.md +56 -0
- package/package.json +1 -1
|
@@ -4,6 +4,10 @@ type Ctx2D = CanvasRenderingContext2D;
|
|
|
4
4
|
* Paint a sorted display list onto an existing Canvas2D context at `scale`
|
|
5
5
|
* (device pixel ratio). The caller is responsible for sizing the canvas to
|
|
6
6
|
* `width * scale` × `height * scale` before calling.
|
|
7
|
+
*
|
|
8
|
+
* A lighting run (commands at `LAYER_LIGHT`) is composited through an offscreen
|
|
9
|
+
* buffer between the world and HUD passes: world → light buffer (multiply) →
|
|
10
|
+
* HUD. When no light run is present this is exactly the old single-pass paint.
|
|
7
11
|
*/
|
|
8
12
|
export declare function drawToCanvas2D(ctx: Ctx2D, commands: DrawCommand[], width: number, height: number, background: string, scale: number): void;
|
|
9
13
|
export {};
|
|
@@ -50,6 +50,14 @@ export interface Paint {
|
|
|
50
50
|
* Canvas2D `setLineDash` / SVG `stroke-dasharray`). Omit for solid lines.
|
|
51
51
|
*/
|
|
52
52
|
lineDash?: number[];
|
|
53
|
+
/**
|
|
54
|
+
* Blend mode for compositing this command over what is already painted — the
|
|
55
|
+
* Canvas2D/SVG-expressible intersection: `'multiply'` darkens (ambient base,
|
|
56
|
+
* shadow quads), `'screen'` brightens (a light pool). Omit for normal
|
|
57
|
+
* source-over. Consumed by the lighting run (see render/lightRun.ts); Canvas
|
|
58
|
+
* maps it to `globalCompositeOperation`, SVG to `mix-blend-mode`.
|
|
59
|
+
*/
|
|
60
|
+
blend?: 'multiply' | 'screen';
|
|
53
61
|
}
|
|
54
62
|
export type TextAlign = 'left' | 'center' | 'right';
|
|
55
63
|
interface Base extends Paint {
|
|
@@ -140,6 +148,17 @@ export interface ImageCommand extends Base {
|
|
|
140
148
|
h: number;
|
|
141
149
|
}
|
|
142
150
|
export type DrawCommand = RectCommand | CircleCommand | EllipseCommand | ArcCommand | PolyCommand | PathCommand | TextCommand | ImageCommand;
|
|
151
|
+
/**
|
|
152
|
+
* Render-layer conventions. Commands sort by layer FIRST (see `sortCommands`),
|
|
153
|
+
* so these are painter-order bands, not a fixed enum — a game may still use any
|
|
154
|
+
* fractional layer. `LAYER_LIGHT` sits between world and HUD so the lighting run
|
|
155
|
+
* multiplies over world content but never darkens the overlay pass.
|
|
156
|
+
*/
|
|
157
|
+
export declare const LAYER_WORLD = 0;
|
|
158
|
+
/** The lighting run: reserved band above the world, below the HUD. */
|
|
159
|
+
export declare const LAYER_LIGHT = 0.5;
|
|
160
|
+
/** Screen-space overlay pass (HUD, transitions) — set for `screenSpace` subtrees. */
|
|
161
|
+
export declare const LAYER_HUD = 1;
|
|
143
162
|
/** Stable painter's sort: by layer, then z, then original index (tree order) as tiebreak. */
|
|
144
163
|
export declare function sortCommands(cmds: DrawCommand[]): DrawCommand[];
|
|
145
164
|
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type CircleCommand, type DrawCommand, type PolyCommand, type RectCommand } from './commands';
|
|
2
|
+
/** The three painter bands the compositor needs: world, the light run, HUD/overlay. */
|
|
3
|
+
export interface LightRunSplit {
|
|
4
|
+
/** Everything below the light layer (the lit world). */
|
|
5
|
+
below: DrawCommand[];
|
|
6
|
+
/** The light run itself (layer === LAYER_LIGHT), in stream order. */
|
|
7
|
+
light: DrawCommand[];
|
|
8
|
+
/** Everything above the light layer (HUD/overlay — never darkened). */
|
|
9
|
+
above: DrawCommand[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Split a SORTED display list into below/light/above runs by layer. The caller
|
|
13
|
+
* passes commands already through `sortCommands`, so the light run is contiguous.
|
|
14
|
+
*/
|
|
15
|
+
export declare function splitByLightLayer(sorted: DrawCommand[]): LightRunSplit;
|
|
16
|
+
/** One parsed light: its pool circle plus the shadow polys that erase it. */
|
|
17
|
+
export interface ParsedLight {
|
|
18
|
+
circle: CircleCommand;
|
|
19
|
+
shadows: PolyCommand[];
|
|
20
|
+
}
|
|
21
|
+
/** A parsed light run: the ambient darkness base + the per-light pools/shadows. */
|
|
22
|
+
export interface ParsedLightRun {
|
|
23
|
+
ambient: RectCommand;
|
|
24
|
+
lights: ParsedLight[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse a light run (as produced by `splitByLightLayer().light`) into its
|
|
28
|
+
* structured form, or `null` if the stream does not match the expected order —
|
|
29
|
+
* in which case the backend must fall back to painting the run flat. An EMPTY
|
|
30
|
+
* run is unparseable (null): there is nothing to composite, so backends paint
|
|
31
|
+
* it flat (a no-op) and produce byte-identical output to a lit-layer-free frame.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseLightRun(run: DrawCommand[]): ParsedLightRun | null;
|
|
34
|
+
/** True when a command carries the light-layer tag (helper for tests/backends). */
|
|
35
|
+
export declare const isLightCommand: (c: DrawCommand) => boolean;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { type DrawCommand } from './commands';
|
|
2
2
|
/**
|
|
3
3
|
* Inner SVG markup for a display list (no wrapping <svg>). `idPrefix` salts the
|
|
4
|
-
* ids of any gradient/shadow <defs> so several inner markups can share one
|
|
5
|
-
* document (e.g. a filmstrip) without their `url(#…)` references colliding.
|
|
4
|
+
* ids of any gradient/shadow/mask <defs> so several inner markups can share one
|
|
5
|
+
* SVG document (e.g. a filmstrip) without their `url(#…)` references colliding.
|
|
6
|
+
*
|
|
7
|
+
* A lighting run (commands at `LAYER_LIGHT`) is emitted as the nested-blend group
|
|
8
|
+
* between the world and HUD passes. An EMPTY or unparseable run falls through the
|
|
9
|
+
* normal per-command path (honouring each command's `blend` via mix-blend-mode),
|
|
10
|
+
* so a lit-layer-free frame is byte-identical to before this feature.
|
|
6
11
|
*/
|
|
7
|
-
export declare function commandsToSVGInner(commands: DrawCommand[], idPrefix?: string): string;
|
|
12
|
+
export declare function commandsToSVGInner(commands: DrawCommand[], idPrefix?: string, viewW?: number, viewH?: number): string;
|
|
8
13
|
/** A complete, standalone SVG document string for a frame — a headless screenshot. */
|
|
9
14
|
export declare function renderToSVGString(commands: DrawCommand[], width: number, height: number, background?: string): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Node, type NodeConfig } from './node';
|
|
2
|
+
import { type ClipDef } from '../anim/clip';
|
|
3
|
+
export interface ClipPlayerConfig extends NodeConfig {
|
|
4
|
+
/** The rig root this player poses. Defaults to the player's own parent at ready. */
|
|
5
|
+
rig?: Node;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Plays named clips onto a rig. Add clips with `add(name, def)`, then `play(name,
|
|
9
|
+
* {fade})`. Reads: `time` (current playhead, s), `current` (playing clip name).
|
|
10
|
+
* Signals: `event` (payload = event name) and `finished` (a `once` clip ended).
|
|
11
|
+
*/
|
|
12
|
+
export declare class ClipPlayer extends Node {
|
|
13
|
+
readonly type = "ClipPlayer";
|
|
14
|
+
private rig;
|
|
15
|
+
private skeleton;
|
|
16
|
+
private clips;
|
|
17
|
+
private currentName;
|
|
18
|
+
private elapsed;
|
|
19
|
+
private prevName;
|
|
20
|
+
private prevElapsed;
|
|
21
|
+
private fadeDur;
|
|
22
|
+
private fadeT;
|
|
23
|
+
private poseA;
|
|
24
|
+
private poseB;
|
|
25
|
+
private poseMix;
|
|
26
|
+
constructor(config?: ClipPlayerConfig);
|
|
27
|
+
protected onReady(): void;
|
|
28
|
+
/** Register a clip under `name`. Rebinds its tracks against the current rig. */
|
|
29
|
+
add(name: string, def: ClipDef): this;
|
|
30
|
+
/** Re-resolve the rig + every clip's tracks (call after the rig subtree changes). */
|
|
31
|
+
rebind(rig?: Node): void;
|
|
32
|
+
private ensureSkeleton;
|
|
33
|
+
/**
|
|
34
|
+
* Play `name`, optionally crossfading over `fade` seconds from whatever is
|
|
35
|
+
* playing. Restarting the same clip with no fade rewinds it. A fade freezes the
|
|
36
|
+
* outgoing clip's playhead and blends it out with EASINGS.quadInOut.
|
|
37
|
+
*/
|
|
38
|
+
play(name: string, opts?: {
|
|
39
|
+
fade?: number;
|
|
40
|
+
}): void;
|
|
41
|
+
/** Stop playback (freezes the rig at its current pose). */
|
|
42
|
+
stop(): void;
|
|
43
|
+
/** Current playhead in seconds (raw elapsed, pre-loop-wrap). */
|
|
44
|
+
get time(): number;
|
|
45
|
+
/** The clip currently playing, or null. */
|
|
46
|
+
get current(): string | null;
|
|
47
|
+
/** Fired with the event name each time the playhead crosses a clip event. */
|
|
48
|
+
get event(): import("..").Signal<string>;
|
|
49
|
+
/** Fired when a `once` clip reaches its end. */
|
|
50
|
+
get finished(): import("..").Signal<string>;
|
|
51
|
+
protected onProcess(dt: number): void;
|
|
52
|
+
/** Direct steady-state apply: sample each bound track and write its channel. */
|
|
53
|
+
private applyDirect;
|
|
54
|
+
/** Sample an entry's bound tracks into a reusable Pose (clears then fills). */
|
|
55
|
+
private samplePose;
|
|
56
|
+
/** Write a mixed pose back onto the incoming entry's bound nodes. */
|
|
57
|
+
private applyPose;
|
|
58
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Node, type NodeConfig } from './node';
|
|
2
|
+
import { Bone2D } from '../anim/skeleton';
|
|
3
|
+
export interface IkTargetConfig extends NodeConfig {
|
|
4
|
+
/** The bone chain to solve, ROOT first. 2 bones → analytic solve; N → FABRIK. */
|
|
5
|
+
bones?: Bone2D[];
|
|
6
|
+
/** Elbow/knee bend side for the 2-bone solve (+1 or −1). Default 1. */
|
|
7
|
+
bendDir?: 1 | -1;
|
|
8
|
+
/** FABRIK iteration cap (N-bone only). Default 16. */
|
|
9
|
+
maxIter?: number;
|
|
10
|
+
}
|
|
11
|
+
/** A cosmetic node that drives a Bone2D chain to reach the node's own world position. */
|
|
12
|
+
export declare class IkTarget extends Node {
|
|
13
|
+
readonly type = "IkTarget";
|
|
14
|
+
bones: Bone2D[];
|
|
15
|
+
bendDir: 1 | -1;
|
|
16
|
+
maxIter: number;
|
|
17
|
+
/** True if the chain reached the target on the last solve (else it was clamped). */
|
|
18
|
+
reached: boolean;
|
|
19
|
+
constructor(config?: IkTargetConfig);
|
|
20
|
+
/** Repoint the solver at a new chain (root-first). */
|
|
21
|
+
setBones(bones: Bone2D[]): void;
|
|
22
|
+
protected onProcess(_dt: number): void;
|
|
23
|
+
/** Current joint positions (parent space) for FABRIK seeding — stable elbow side. */
|
|
24
|
+
private currentJoints;
|
|
25
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type Transform } from '../core/math';
|
|
2
|
+
import { type DrawCommand } from '../render/commands';
|
|
3
|
+
import { Node, type NodeConfig } from './node';
|
|
4
|
+
import { type Occluder } from './shadow2d';
|
|
5
|
+
export interface PointLightConfig extends NodeConfig {
|
|
6
|
+
/** Pool radius in design-space px. */
|
|
7
|
+
radius?: number;
|
|
8
|
+
/** Light color (hex). */
|
|
9
|
+
color?: string;
|
|
10
|
+
/** Peak intensity 0..1 (scales the pool's brightness). */
|
|
11
|
+
intensity?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Falloff exponent for the radial gradient's mid stop (>1 = tighter core,
|
|
14
|
+
* <1 = broader glow). Default 1.
|
|
15
|
+
*/
|
|
16
|
+
falloff?: number;
|
|
17
|
+
/** Flicker: sinusoidal + noise wobble of intensity. Off (amount 0) by default. */
|
|
18
|
+
flicker?: {
|
|
19
|
+
amount: number;
|
|
20
|
+
speed: number;
|
|
21
|
+
};
|
|
22
|
+
/** Private flicker-Rng seed (observer-side, not serialized). Default 43. */
|
|
23
|
+
seed?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A radial point light. Emits nothing itself (`draw` is empty) — its parent
|
|
27
|
+
* LightLayer reads its world position, radius, color, and `litIntensity` to
|
|
28
|
+
* build the run. `litIntensity` is cached each `onProcess` from the flicker
|
|
29
|
+
* stream; with flicker off it equals `intensity` exactly.
|
|
30
|
+
*/
|
|
31
|
+
export declare class PointLight extends Node {
|
|
32
|
+
readonly type: string;
|
|
33
|
+
radius: number;
|
|
34
|
+
color: string;
|
|
35
|
+
intensity: number;
|
|
36
|
+
falloff: number;
|
|
37
|
+
flicker: {
|
|
38
|
+
amount: number;
|
|
39
|
+
speed: number;
|
|
40
|
+
};
|
|
41
|
+
/** Current intensity after flicker — read by the LightLayer during draw. */
|
|
42
|
+
litIntensity: number;
|
|
43
|
+
private rng;
|
|
44
|
+
private phase;
|
|
45
|
+
constructor(config?: PointLightConfig);
|
|
46
|
+
protected onProcess(dt: number): void;
|
|
47
|
+
protected serializeProps(): Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
export interface LightLayerConfig extends NodeConfig {
|
|
50
|
+
/** Ambient darkness the pools lift out of. `color` multiplies the world; `level` blends toward white (1 = no darkening). */
|
|
51
|
+
ambient?: {
|
|
52
|
+
color?: string;
|
|
53
|
+
level?: number;
|
|
54
|
+
};
|
|
55
|
+
/** Soft shadows: add a half-opacity penumbra quad per segment (pure geometry, no blur). */
|
|
56
|
+
softShadows?: boolean;
|
|
57
|
+
/** Ambient-rect viewport size (screen space). Default 1280×720. */
|
|
58
|
+
width?: number;
|
|
59
|
+
height?: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The lighting container. Emits the whole light run from its direct PointLight
|
|
63
|
+
* children: an ambient darkness base, then per light a screen-blended pool plus
|
|
64
|
+
* its multiply shadow quads. Cosmetic; place at the tree origin in world space.
|
|
65
|
+
*/
|
|
66
|
+
export declare class LightLayer extends Node {
|
|
67
|
+
readonly type: string;
|
|
68
|
+
ambient: {
|
|
69
|
+
color: string;
|
|
70
|
+
level: number;
|
|
71
|
+
};
|
|
72
|
+
softShadows: boolean;
|
|
73
|
+
width: number;
|
|
74
|
+
height: number;
|
|
75
|
+
private occluders;
|
|
76
|
+
constructor(config?: LightLayerConfig);
|
|
77
|
+
/** Set the occluder edges (design-space segments) this layer casts shadows from. */
|
|
78
|
+
setOccluders(segs: Occluder[]): void;
|
|
79
|
+
protected draw(out: DrawCommand[], world: Transform): void;
|
|
80
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Vec2 } from '../core/math';
|
|
2
|
+
import { type ContourSegment } from '../art/autotile';
|
|
3
|
+
import { type TilemapData } from '../physics/tilemap';
|
|
4
|
+
/** An occluder edge in world (design-space) coordinates. */
|
|
5
|
+
export type Occluder = ContourSegment;
|
|
6
|
+
/**
|
|
7
|
+
* Occluder edges from a tilemap: the marching-squares contour of the SOLID
|
|
8
|
+
* cells, in design-space px. Wraps `marchingSquaresContours` over a boolean
|
|
9
|
+
* grid of `tileAt === TILE.SOLID`, scaled by the map's tileSize.
|
|
10
|
+
*/
|
|
11
|
+
export declare function occludersFromTilemap(map: TilemapData): Occluder[];
|
|
12
|
+
/**
|
|
13
|
+
* Cull occluders to those that can matter for a light: an endpoint within, or
|
|
14
|
+
* the segment straddling, the light's reach (radius). A cheap circle/AABB-ish
|
|
15
|
+
* test — keeps the O(lights × segments) shadow pass tight without changing the
|
|
16
|
+
* result (a segment fully outside the disc casts no shadow inside it).
|
|
17
|
+
*/
|
|
18
|
+
export declare function cullSegments(light: Vec2, radius: number, segs: readonly Occluder[]): Occluder[];
|
|
19
|
+
/**
|
|
20
|
+
* Shadow quads cast by `segs` from `light`, one flat `[ax,ay, bx,by, bx',by',
|
|
21
|
+
* ax',ay']` quad per segment (closed poly winding). Extrusion length is 2×radius
|
|
22
|
+
* so the far edge always clears the lit disc. A segment passing through the light
|
|
23
|
+
* (degenerate direction) is skipped. Culling is the caller's job (`cullSegments`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function shadowQuads(light: Vec2, radius: number, segs: readonly Occluder[]): number[][];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Transform } from '../core/math';
|
|
2
|
+
import type { DrawCommand } from '../render/commands';
|
|
3
|
+
import { Node, type NodeConfig } from './node';
|
|
4
|
+
export interface SkeletonDebugConfig extends NodeConfig {
|
|
5
|
+
/** The rig root to visualize. Defaults to the node's parent. */
|
|
6
|
+
rig?: Node;
|
|
7
|
+
/** Bone segment color. Default '#e0483b'. */
|
|
8
|
+
boneColor?: string;
|
|
9
|
+
/** Joint pivot color. Default '#f4d35e'. */
|
|
10
|
+
pivotColor?: string;
|
|
11
|
+
/** Joint circle radius, px. Default 3. */
|
|
12
|
+
pivotRadius?: number;
|
|
13
|
+
/** Bone stroke width, px. Default 2. */
|
|
14
|
+
strokeWidth?: number;
|
|
15
|
+
}
|
|
16
|
+
/** Draws a rig's bones + pivots as transient overlay commands. Cosmetic. */
|
|
17
|
+
export declare class SkeletonDebug extends Node {
|
|
18
|
+
readonly type = "SkeletonDebug";
|
|
19
|
+
rig: Node | null;
|
|
20
|
+
boneColor: string;
|
|
21
|
+
pivotColor: string;
|
|
22
|
+
pivotRadius: number;
|
|
23
|
+
strokeWidth: number;
|
|
24
|
+
constructor(config?: SkeletonDebugConfig);
|
|
25
|
+
protected onReady(): void;
|
|
26
|
+
protected draw(out: DrawCommand[], _world: Transform): void;
|
|
27
|
+
private emitFor;
|
|
28
|
+
}
|
package/dist/verify/gates.d.ts
CHANGED
|
@@ -92,6 +92,23 @@ export interface TelegraphFrame {
|
|
|
92
92
|
* Feed a per-frame timeline (one entry per sim frame) for a single threat.
|
|
93
93
|
*/
|
|
94
94
|
export declare function telegraphIssues(timeline: readonly TelegraphFrame[], minFrames: number, label?: string): string[];
|
|
95
|
+
export interface LightingOptions {
|
|
96
|
+
/**
|
|
97
|
+
* Minimum effective ambient level (0 = pitch black, 1 = fully lit) the avatar
|
|
98
|
+
* must sit in — either from the ambient base or a pool falling on it (default
|
|
99
|
+
* 0.35). Below this the avatar reads as a silhouette in shadow.
|
|
100
|
+
*/
|
|
101
|
+
minLitLevel?: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Lit-readability gate. Given the rendered display list (which must include the
|
|
105
|
+
* LightLayer's run) and the avatar's WORLD position + fill, compute the light
|
|
106
|
+
* level reaching the avatar: the ambient base, lifted toward 1 by any pool whose
|
|
107
|
+
* radius covers it (scaled by that pool's peak brightness). If the avatar ends up
|
|
108
|
+
* below `minLitLevel`, it is a silhouette lost in the dark — flagged. When the
|
|
109
|
+
* frame carries no parseable light run, there is nothing to darken: pass (empty).
|
|
110
|
+
*/
|
|
111
|
+
export declare function lightingIssues(commands: DrawCommand[], avatarFill: string, avatarWorld: Vec2, opts?: LightingOptions): string[];
|
|
95
112
|
export interface CameraOptions {
|
|
96
113
|
/** Fixed timestep between samples in seconds (default 1/60). */
|
|
97
114
|
dt?: number;
|
|
@@ -127,6 +144,12 @@ export interface FeelSpec {
|
|
|
127
144
|
};
|
|
128
145
|
/** True for scrolling games — enables the camera gate (needs sampled positions). */
|
|
129
146
|
scrolls?: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* True for games with a LightLayer — enables the lit-readability gate (needs a
|
|
149
|
+
* rendered frame carrying the light run + the avatar's world position). Uses
|
|
150
|
+
* `avatarFill` for the color.
|
|
151
|
+
*/
|
|
152
|
+
lit?: boolean;
|
|
130
153
|
}
|
|
131
154
|
/** Runtime inputs the audit/verify computes and hands to the aggregator. */
|
|
132
155
|
export interface FeelContext {
|
|
@@ -140,6 +163,8 @@ export interface FeelContext {
|
|
|
140
163
|
dt?: number;
|
|
141
164
|
/** Background fallback if the spec omits one. */
|
|
142
165
|
background?: string;
|
|
166
|
+
/** The avatar's WORLD position in the rendered frame — enables the lit-readability gate. */
|
|
167
|
+
avatarWorld?: Vec2;
|
|
143
168
|
}
|
|
144
169
|
export interface FeelReport {
|
|
145
170
|
ok: boolean;
|