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.
@@ -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 SVG
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
+ }
@@ -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;