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.
@@ -0,0 +1,68 @@
1
+ import { type ClipDef } from './clip';
2
+ /** A sampled pose: "target/channel" → value. Plain object; order-insensitive. */
3
+ export type Pose = Record<string, number>;
4
+ /** Compose a pose key from a track's target + channel. */
5
+ export declare const poseKey: (target: string, channel: string) => string;
6
+ /**
7
+ * Sample a whole clip into a Pose at NORMALIZED phase `phase` in [0,1) — the
8
+ * blend-space entry point. `phase * duration` is the clip-local time (so two
9
+ * clips of different length stay in step). Pure; allocates one fresh Pose.
10
+ */
11
+ export declare function samplePose(def: ClipDef, phase: number): Pose;
12
+ /**
13
+ * Linear blend of two poses: `out[k] = lerp(a[k], b[k], t)`. Keys present in only
14
+ * one pose pass through unblended (the other side has no opinion). When `out` is
15
+ * supplied it is reused (cleared then filled) — the allocation-free path
16
+ * ClipPlayer takes during a crossfade. Returns `out`.
17
+ */
18
+ export declare function mixPose(a: Pose, b: Pose, t: number, out?: Pose): Pose;
19
+ /** One entry of a blend space: a clip pinned at a parameter position. */
20
+ export interface BlendSample {
21
+ clip: ClipDef;
22
+ /** Parameter position of this sample (e.g. speed). Blend1D reads `.x` only. */
23
+ x: number;
24
+ /** Second parameter (Blend2D only). */
25
+ y?: number;
26
+ }
27
+ /**
28
+ * A 1-D blend space (e.g. idle→walk→run along speed). Samples are sorted by `x`
29
+ * on construction; `sample(param, phase)` finds the bracketing pair and mixes
30
+ * them by the normalized distance, all clips sampled at the SAME phase (foot-lock).
31
+ * Below the first / above the last sample it clamps to the end clip.
32
+ */
33
+ export declare class Blend1D {
34
+ readonly samples: BlendSample[];
35
+ constructor(samples: BlendSample[]);
36
+ /** The clip weights at parameter `param` (sums to 1; empty space → {}). */
37
+ weights(param: number): {
38
+ clip: ClipDef;
39
+ weight: number;
40
+ }[];
41
+ /** Blend the neighbor clips at `param`, all sampled at normalized `phase`. */
42
+ sample(param: number, phase: number): Pose;
43
+ }
44
+ /**
45
+ * A 2-D blend space (e.g. a locomotion pad: forward/back × left/right). No
46
+ * triangulation library — we pick the NEAREST THREE samples to the query point
47
+ * and blend them barycentrically; if the point is outside their triangle the
48
+ * barycentric weights are clamped to >= 0 and renormalized (a documented
49
+ * approximation: no Delaunay, so a sparse/concave set can pick a non-ideal trio).
50
+ */
51
+ export declare class Blend2D {
52
+ readonly samples: Required<BlendSample>[];
53
+ constructor(samples: BlendSample[]);
54
+ /** Clip weights at (px,py): nearest-3 barycentric, clamped + renormalized. */
55
+ weights(px: number, py: number): {
56
+ clip: ClipDef;
57
+ weight: number;
58
+ }[];
59
+ private barycentric;
60
+ /** Blend the nearest-3 clips at (px,py), all sampled at normalized `phase`. */
61
+ sample(px: number, py: number, phase: number): Pose;
62
+ }
63
+ /**
64
+ * Validate a blend space's samples (levelIssues idiom). Catches empty spaces,
65
+ * duplicate parameter positions (a degenerate bracket / triangle), and non-finite
66
+ * coordinates. `dims` selects which axes must be finite.
67
+ */
68
+ export declare function blendIssues(samples: BlendSample[], dims?: 1 | 2): string[];
@@ -0,0 +1,87 @@
1
+ import { EASINGS } from '../scene/tween';
2
+ /** Which transform channel a track drives. */
3
+ export type ClipChannel = 'x' | 'y' | 'rotation' | 'scaleX' | 'scaleY' | 'opacity';
4
+ export declare const CLIP_CHANNELS: readonly ClipChannel[];
5
+ /** How the playhead behaves past `duration`. */
6
+ export type ClipLoop = 'loop' | 'once' | 'pingpong';
7
+ /** One keyframe: value `v` at time `t` (seconds), with the ease used to reach it FROM the previous key. */
8
+ export interface Keyframe {
9
+ t: number;
10
+ v: number;
11
+ /** EASINGS name applied over the segment ENDING at this key. Default 'linear'. */
12
+ ease?: keyof typeof EASINGS;
13
+ }
14
+ /** One track: a channel of one target node, driven by an ordered keyframe list. */
15
+ export interface TrackDef {
16
+ /** '/'-separated path from the rig root to the target node (matched by Node.name). */
17
+ target: string;
18
+ channel: ClipChannel;
19
+ /** Keyframes, ascending in `t`. At least one. */
20
+ keys: Keyframe[];
21
+ }
22
+ /** A discrete event fired as the playhead crosses `t` (half-open (t0,t1] window). */
23
+ export interface ClipEvent {
24
+ t: number;
25
+ name: string;
26
+ }
27
+ /** A full clip: duration, loop mode, tracks, optional events. Plain data. */
28
+ export interface ClipDef {
29
+ /** Clip length in seconds (> 0). */
30
+ duration: number;
31
+ loop: ClipLoop;
32
+ tracks: TrackDef[];
33
+ events?: ClipEvent[];
34
+ /** RESERVED — root motion is not implemented (see file header). */
35
+ rootMotion?: never;
36
+ }
37
+ /**
38
+ * Map a raw elapsed time to a clip-local time in [0, duration], honoring the
39
+ * loop mode. `once` clamps at both ends; `loop` wraps; `pingpong` folds so the
40
+ * playhead ping-pongs 0→d→0. Pure and total (guards duration <= 0 → 0).
41
+ */
42
+ export declare function clipTime(def: ClipDef, elapsed: number): number;
43
+ /** True once a `once` clip has run past its end (used by ClipPlayer to fire `finished`). */
44
+ export declare function clipFinished(def: ClipDef, elapsed: number): boolean;
45
+ /**
46
+ * Binary-search the keyframe segment containing local time `t` and return the
47
+ * eased-interpolated value. Before the first key → first value; after the last →
48
+ * last value (clips are sampled at a clamped clipTime, so this only bites at the
49
+ * exact ends). Allocation-free: no array building, no closures.
50
+ */
51
+ export declare function sampleTrack(keys: Keyframe[], t: number): number;
52
+ /**
53
+ * A sampled pose channel: which target/channel and the value. `sampleClip`
54
+ * returns one per track. Kept flat (not a nested object) so ClipPlayer can bind
55
+ * each to a resolved node once and apply in O(tracks) with no lookups.
56
+ */
57
+ export interface SampledChannel {
58
+ target: string;
59
+ channel: ClipChannel;
60
+ value: number;
61
+ }
62
+ /**
63
+ * PURE sample of the whole clip at raw `elapsed` seconds → one SampledChannel per
64
+ * track. Never mutates `def`. Two calls with the same args are deep-equal. This
65
+ * is the reference sampler; ClipPlayer uses a prebound, allocation-free variant
66
+ * of the same math for the hot path.
67
+ */
68
+ export declare function sampleClip(def: ClipDef, elapsed: number): SampledChannel[];
69
+ /**
70
+ * Events crossed as the playhead advances from raw `prev` to raw `next`, in the
71
+ * order they fire. FIXED SEMANTICS:
72
+ * - half-open window `(t0, t1]` in clip-local time (an event exactly at the new
73
+ * time fires; one exactly at the old time does NOT re-fire);
74
+ * - `loop` wrap emits the TAIL segment (prevLocal, duration] then the HEAD
75
+ * segment (0, nextLocal];
76
+ * - `once` never re-fires an event once the playhead is at/after duration;
77
+ * - order within a segment is ascending by `t`, tail before head on a wrap.
78
+ * Pure; returns event names in fire order.
79
+ */
80
+ export declare function clipEvents(def: ClipDef, prev: number, next: number): string[];
81
+ /**
82
+ * Validate a clip's DATA, in the levelIssues/tuningIssues idiom: return ALL
83
+ * problems as human-readable strings (empty = clean), never throw. Optionally
84
+ * pass the set of valid target paths (`knownTargets`) to catch tracks that point
85
+ * at a node the rig doesn't contain.
86
+ */
87
+ export declare function clipIssues(def: ClipDef, knownTargets?: readonly string[]): string[];
@@ -0,0 +1,40 @@
1
+ import type { Vec2 } from '../core/math';
2
+ /** Result of a 2-bone solve: absolute WORLD-space angles (radians) for each joint. */
3
+ export interface TwoBoneResult {
4
+ /** Absolute angle of bone 0 (root→mid). */
5
+ angle0: number;
6
+ /** Absolute angle of bone 1 (mid→end). */
7
+ angle1: number;
8
+ /** True if the target was in range; false → chain clamped to full extension toward it. */
9
+ reachable: boolean;
10
+ }
11
+ /**
12
+ * Analytic two-bone IK. `root` is the base joint (world), `l0`/`l1` the bone
13
+ * lengths, `target` the desired end-effector position (world). `bendDir` is +1
14
+ * or −1 to choose which side the middle joint bends to (elbow up/down). Returns
15
+ * the two ABSOLUTE bone angles (add to nothing — they are world angles; convert
16
+ * to local by subtracting the parent's world angle at the call site).
17
+ */
18
+ export declare function solveTwoBoneIK(root: Vec2, l0: number, l1: number, target: Vec2, bendDir?: 1 | -1): TwoBoneResult;
19
+ export interface FabrikResult {
20
+ /** Joint positions (world), joints[0] === root, joints[n] === end effector. */
21
+ joints: Vec2[];
22
+ /** Absolute bone angles (radians), one per bone (length joints.length - 1). */
23
+ angles: number[];
24
+ reachable: boolean;
25
+ /** Iterations actually run before convergence (or the cap). */
26
+ iterations: number;
27
+ }
28
+ /**
29
+ * FABRIK N-bone solver. `root` anchors joint 0; `lengths` are the bone lengths
30
+ * (n bones → n+1 joints); `target` is the desired end position. Alternates a
31
+ * backward reach (pull the end to the target, propagate inward) and a forward
32
+ * reach (pin the root, propagate outward) until the end is within `tolerance` of
33
+ * the target or `maxIter` is hit. When the target is unreachable the chain is
34
+ * laid out straight toward it (reachable=false). Pure; deterministic (dmath).
35
+ */
36
+ export declare function solveFabrik(root: Vec2, lengths: number[], target: Vec2, opts?: {
37
+ maxIter?: number;
38
+ tolerance?: number;
39
+ initial?: Vec2[];
40
+ }): FabrikResult;
@@ -0,0 +1,64 @@
1
+ import { Node, type NodeConfig } from '../scene/node';
2
+ import type { ClipChannel, ClipDef, Keyframe } from './clip';
3
+ export interface Bone2DConfig extends NodeConfig {
4
+ /** Bone reach in local px (root→tip along +x at rotation 0). Default 0. */
5
+ length?: number;
6
+ }
7
+ /**
8
+ * A rig joint: a Node with a `length`. Rotation is the joint angle; children
9
+ * inherit the transform, so a chain of Bone2Ds nested tip-to-root forms a limb.
10
+ * Registered (`Bone2D`) so a serialized rig round-trips — `length` persists.
11
+ */
12
+ export declare class Bone2D extends Node {
13
+ readonly type = "Bone2D";
14
+ /** Reach in local px along +x. Used by IK, skinning, and the debug overlay. */
15
+ length: number;
16
+ constructor(config?: Bone2DConfig);
17
+ /** Tip position in LOCAL space (root is the node origin; tip is `length` along +x). */
18
+ get tip(): {
19
+ x: number;
20
+ y: number;
21
+ };
22
+ protected serializeProps(): Record<string, unknown>;
23
+ applyProps(props: Record<string, unknown>): void;
24
+ }
25
+ /**
26
+ * An index of a rig subtree: '/'-separated name path → node. Paths are relative
27
+ * to the rig root passed to `buildSkeleton` (the root itself is '' and its direct
28
+ * children are just their names). Ordered iteration only (a plain Map, built by a
29
+ * deterministic DFS).
30
+ */
31
+ export declare class Skeleton {
32
+ readonly root: Node;
33
+ readonly byPath: Map<string, Node>;
34
+ constructor(root: Node);
35
+ private index;
36
+ /** Resolve a track/IK target path to a node (null if the rig lacks it). */
37
+ resolve(path: string): Node | null;
38
+ /** All target paths in the rig — feed to `clipIssues(def, skeleton.targets())`. */
39
+ targets(): string[];
40
+ /** Only the Bone2D joints, by path (for IK / skinning). */
41
+ bones(): Bone2D[];
42
+ }
43
+ /** Build a skeleton index from a rig root. */
44
+ export declare function buildSkeleton(root: Node): Skeleton;
45
+ /**
46
+ * A track prebound to its target node: the resolved Node, the channel, and the
47
+ * clip's keyframes captured by reference. ClipPlayer holds an array of these and
48
+ * applies each with a single write in the hot path — no path lookup, no find().
49
+ */
50
+ export interface BoundTrack {
51
+ node: Node;
52
+ channel: ClipChannel;
53
+ keys: Keyframe[];
54
+ /** The track's path, kept for diagnostics / rebind. */
55
+ target: string;
56
+ }
57
+ /**
58
+ * Resolve every track of a clip against a skeleton ONCE. Tracks whose target the
59
+ * rig doesn't contain are dropped (surfaced separately by clipIssues) so the hot
60
+ * path never guards for a null node. Returns bound tracks in clip order.
61
+ */
62
+ export declare function resolveTracks(def: ClipDef, skeleton: Skeleton): BoundTrack[];
63
+ /** Apply one channel's value to a node's transform (the single hot-path write). */
64
+ export declare function applyChannel(node: Node, channel: ClipChannel, value: number): void;