@ikijs/engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,353 @@
1
+ import { IkiModel, IkiParameter, IkiPhysics, IkiPhysicsChain, IkiDeformer } from '@ikijs/format';
2
+
3
+ /**
4
+ * Outcome of {@link IkiPlayer.load}: the indices into `model.textures` that
5
+ * failed to decode or upload (empty = every declared texture loaded). The model
6
+ * is still swapped in and rendered; parts using a failed texture are skipped.
7
+ * A host can inspect this to detect and report a partial load.
8
+ */
9
+ interface IkiLoadResult {
10
+ failedTextures: number[];
11
+ /**
12
+ * True when a newer `load()` (or `destroy()`) superseded this call before it
13
+ * adopted anything — the model was NOT loaded and `failedTextures` is empty
14
+ * because nothing was attempted, not because everything succeeded. Without
15
+ * this flag a caller awaiting the losing promise cannot tell the two apart.
16
+ */
17
+ superseded: boolean;
18
+ }
19
+ /**
20
+ * Drives a single `.iki` model on a WebGL2 canvas.
21
+ *
22
+ * v1 scope: parts are solid-color or atlas-sampled textured quads or meshes,
23
+ * transformed each frame by their base transform plus the sum of their parameter
24
+ * bindings. `load()` is async — it decodes and uploads textures before swapping
25
+ * the model in. Mesh parts additionally carry per-vertex UV and optional warp
26
+ * keyforms, interpolated each frame on the CPU into a dynamic vertex buffer.
27
+ */
28
+ declare class IkiPlayer {
29
+ private readonly canvas;
30
+ private readonly gl;
31
+ private readonly program;
32
+ private readonly quad;
33
+ private readonly uMatrix;
34
+ private readonly uColor;
35
+ private readonly uUseTexture;
36
+ private readonly uTex;
37
+ private readonly uUvOffset;
38
+ private readonly uUvScale;
39
+ private readonly uUseMeshUv;
40
+ private readonly uAlphaCutoff;
41
+ private readonly aPos;
42
+ private readonly aUv;
43
+ /** True when the context granted a stencil buffer; clipping needs it. */
44
+ private readonly stencilAvailable;
45
+ private model?;
46
+ private parts;
47
+ private params;
48
+ private rafId?;
49
+ /** Uploaded textures, index-aligned with `model.textures`; `null` = unusable. */
50
+ private textures;
51
+ /** Bumped by every `load` and by `destroy`; lets a stale async load bail. */
52
+ private loadGeneration;
53
+ private destroyed;
54
+ /**
55
+ * Engine-internal mesh buffers, keyed by the part's INDEX in `this.parts`
56
+ * (NOT by part id — duplicate ids must not swap buffers).
57
+ */
58
+ private partMeshes;
59
+ /**
60
+ * Clip groups resolved once per `load()`: consumer part index (into `this.parts`)
61
+ * → its mask part indices. A part absent from this map is unclipped.
62
+ */
63
+ private partClipGroups;
64
+ constructor(canvas: HTMLCanvasElement);
65
+ /**
66
+ * Load a model and reset parameters to their defaults. All textures are
67
+ * decoded and uploaded before the model is swapped in — the swap is atomic,
68
+ * so you never see a partially-textured frame. `start()` may be called any
69
+ * time, but nothing renders until the first `load()` resolves. For an
70
+ * embedded `data:` atlas this is near-instant.
71
+ *
72
+ * Individual texture decode/upload failures are non-fatal: they are logged
73
+ * via `console.error`, the affected parts are skipped, and `load()` still
74
+ * resolves — the returned {@link IkiLoadResult} lists the indices of any
75
+ * textures that failed, so a host can detect and report a partial load. The
76
+ * model is assumed already validated by `@ikijs/format`.
77
+ *
78
+ * Mesh buffer allocation failure IS fatal (unlike per-texture skip) because
79
+ * textures have an `IkiLoadResult.failedTextures` reporting surface and mesh
80
+ * buffers have none — there is no partial-mesh concept in the format.
81
+ */
82
+ load(model: IkiModel): Promise<IkiLoadResult>;
83
+ /**
84
+ * Start the render loop. Safe to call more than once, and a no-op after
85
+ * {@link destroy} — the program and buffers the loop draws with are gone, so
86
+ * restarting would only spray GL errors.
87
+ */
88
+ start(): void;
89
+ stop(): void;
90
+ /**
91
+ * Set a parameter value (clamped to its range). Unknown ids and non-finite
92
+ * values are ignored — see {@link ParameterStore.set}.
93
+ */
94
+ setParameter(id: string, value: number): void;
95
+ /**
96
+ * Current value of a parameter, or 0 for an unknown id.
97
+ *
98
+ * Hosts need this to avoid shadowing the engine's state: the motion drivers
99
+ * read the live pose to compute the next one, and without a read accessor
100
+ * every host has to keep its own mirror of what it last wrote — and keep that
101
+ * mirror's clamping in step with {@link ParameterStore} by hand.
102
+ */
103
+ getParameter(id: string): number;
104
+ /** The model's parameter descriptors, for building UI or host wiring. */
105
+ getParameters(): IkiParameter[];
106
+ destroy(): void;
107
+ private renderFrame;
108
+ /**
109
+ * Draw a clipped part: stencil the union of its masks' alpha coverage, then
110
+ * draw the part only where the stencil was written. The mask parts also draw
111
+ * normally in their own `order` slot — this is an EXTRA, color-free pass over
112
+ * the same per-frame deformed geometry. All stencil/colorMask state the pass
113
+ * touches is restored before returning so later parts are unaffected.
114
+ */
115
+ private drawClipped;
116
+ /**
117
+ * Draw a single part with its full per-part material + geometry state. Shared
118
+ * by the normal pass, the stencil mask-write pass, and the masked consumer
119
+ * draw — so every path prepares the SAME complete uniform/texture/VBO state
120
+ * (the caller only sets stencil/colorMask/u_alphaCutoff around it).
121
+ */
122
+ private drawPart;
123
+ /** Resolve a part's effective transform from its base plus active bindings. */
124
+ private evaluate;
125
+ }
126
+
127
+ /**
128
+ * Holds the live value of every model parameter, clamped to its declared
129
+ * range. This is the single surface a host drives (lip-sync, gaze, blink) and
130
+ * the engine reads each frame to evaluate bindings.
131
+ */
132
+ declare class ParameterStore {
133
+ private readonly params;
134
+ private readonly values;
135
+ /**
136
+ * Resting value per id: the declared default clamped into range, resolved
137
+ * ONCE here so `reset()` is a straight copy and a malformed descriptor is
138
+ * reported once rather than on every reset.
139
+ */
140
+ private readonly defaults;
141
+ constructor(parameters: IkiParameter[]);
142
+ /**
143
+ * Set a parameter's value, clamped to its range. Unknown ids are ignored, as
144
+ * are non-finite values: this is the boundary a host drives with live signals,
145
+ * and `clamp` cannot filter NaN (`Math.max(min, Math.min(max, NaN))` is NaN),
146
+ * so one bad lip-sync/gaze frame would otherwise poison every binding that
147
+ * reads the parameter. A dropped write holds the last good pose.
148
+ */
149
+ set(id: string, value: number): void;
150
+ /** Current value, or 0 if the id is unknown. */
151
+ get(id: string): number;
152
+ /** Position of a parameter within its range, 0..1. */
153
+ normalized(id: string): number;
154
+ /** Reset every parameter to its resting value (see `defaults`). */
155
+ reset(): void;
156
+ list(): IkiParameter[];
157
+ }
158
+
159
+ type Affine = [number, number, number, number, number, number];
160
+ declare function translate(tx: number, ty: number): Affine;
161
+ declare function scale(sx: number, sy: number): Affine;
162
+ declare function rotate(degrees: number): Affine;
163
+ declare function multiply(a: Affine, b: Affine): Affine;
164
+ /** Expand a 2D affine into a column-major mat3 for `uniformMatrix3fv`. */
165
+ declare function toMat3(a: Affine): Float32Array;
166
+
167
+ interface IdleMotionOptions {
168
+ /** Inject a deterministic rng for testing. Defaults to Math.random. */
169
+ rng?: () => number;
170
+ }
171
+ /**
172
+ * Pure-logic idle-animation driver. Animates the seven "life" parameters
173
+ * (eyes, breath, gaze, head sway) on an internal clock so tab-backgrounding
174
+ * or irregular frame delivery can't produce teleports or snap-close blinks.
175
+ *
176
+ * Usage:
177
+ * const idle = new IdleMotion(player.setParameter.bind(player));
178
+ * // inside your rAF loop:
179
+ * idle.update(performance.now());
180
+ *
181
+ * The host is responsible for scheduling; this class has no timers or rAF.
182
+ */
183
+ declare class IdleMotion {
184
+ private readonly sink;
185
+ private readonly rng;
186
+ private clockMs;
187
+ private prevNowMs;
188
+ private nextBlinkAtMs;
189
+ private blinkStartMs;
190
+ private gazeCurrentX;
191
+ private gazeCurrentY;
192
+ private gazeTargetX;
193
+ private gazeTargetY;
194
+ private nextGazeRetargetMs;
195
+ constructor(sink: (id: string, value: number) => void, options?: IdleMotionOptions);
196
+ /**
197
+ * Advance the idle animation to the given wall-clock timestamp (milliseconds).
198
+ *
199
+ * On the first call: record prevNowMs, emit the resting pose, and return —
200
+ * no animation advance happens so there is no jump from time 0.
201
+ *
202
+ * On subsequent calls: compute dt = clamp(nowMs - prevNowMs, 0, MAX_DT_MS)
203
+ * and advance the internal clock by dt. A non-monotonic nowMs produces a
204
+ * negative raw delta that the clamp floors to 0 — no rewind.
205
+ */
206
+ update(nowMs: number): void;
207
+ private emitRestingPose;
208
+ /** Returns the current eye-open value (0..1) and advances blink state. */
209
+ private advanceBlink;
210
+ private advanceBreath;
211
+ /** Horizontal head sway in degrees, pure function of the internal clock. */
212
+ private swayX;
213
+ /** Vertical head sway in degrees, pure function of the internal clock. */
214
+ private swayY;
215
+ /** Ease gaze current toward target; pick a new target on the internal clock. */
216
+ private advanceGaze;
217
+ }
218
+
219
+ /**
220
+ * Host-agnostic 1D spring-mass-damper secondary-motion driver — the physics
221
+ * peer of {@link IdleMotion}. For each rig it reads the input parameter,
222
+ * signed-normalizes it around the input's default × `weight` to form a spring
223
+ * target, integrates a lagging spring position with semi-implicit (symplectic)
224
+ * Euler on a fixed 1/60s sub-step accumulator, and writes
225
+ * `outputDefault + x * scale` onto the output parameter — so the output lags
226
+ * and overshoots the input (hair/accessory sway).
227
+ *
228
+ * Usage:
229
+ * const physics = new PhysicsMotion(
230
+ * model.physics ?? [],
231
+ * model.parameters,
232
+ * (id) => currentValue(id),
233
+ * player.setParameter.bind(player),
234
+ * );
235
+ * // inside your rAF loop, right AFTER idle.update(now):
236
+ * physics.update(performance.now());
237
+ *
238
+ * The host schedules updates; this class has no timers, rAF, DOM, or Date.now.
239
+ * Writes go through the sink, exactly like IdleMotion; the player renders the
240
+ * updated params on its own render loop (drivers and rendering are decoupled).
241
+ */
242
+ declare class PhysicsMotion {
243
+ private readonly rigs;
244
+ private readonly read;
245
+ private readonly sink;
246
+ private readonly params;
247
+ private readonly state;
248
+ private readonly clock;
249
+ constructor(rigs: IkiPhysics[], params: IkiParameter[], read: (id: string) => number, sink: (id: string, value: number) => void);
250
+ /**
251
+ * Advance every rig to the given wall-clock timestamp (milliseconds).
252
+ *
253
+ * First call: seed each spring to rest AT its current target (so a model
254
+ * loaded with a nonzero input does not kick), emit the resting output, and
255
+ * return without integrating — mirroring IdleMotion's first-frame behavior.
256
+ *
257
+ * Subsequent calls: {@link FixedStepClock} folds the clamped frame delta into
258
+ * its accumulator and returns how many {@link FIXED_DT_S} sub-steps are due;
259
+ * the spring advances that many semi-implicit Euler steps, then each rig emits
260
+ * its output once. The clock's dt clamp and sub-step cap plus the symplectic
261
+ * integrator are what keep it stable across hitches.
262
+ */
263
+ update(nowMs: number): void;
264
+ /** Spring target = signed-normalized input value × weight. */
265
+ private targetFor;
266
+ /** One semi-implicit (symplectic) Euler sub-step of FIXED_DT_S seconds. */
267
+ private step;
268
+ /** Write outputDefault + x * scale onto the output param via the sink. */
269
+ private emit;
270
+ }
271
+
272
+ /**
273
+ * Host-agnostic multi-segment angular-pendulum-chain secondary-motion driver.
274
+ * Peer of {@link PhysicsMotion} and {@link IdleMotion}.
275
+ *
276
+ * Each chain anchors to a matrix deformer in the model hierarchy. The driver
277
+ * self-computes the anchor's world rotation via `resolveDeformerWorlds` (a
278
+ * private `ParameterStore` is filled from `read` ONCE per frame) and integrates
279
+ * a per-segment angular pendulum with semi-implicit Euler on a fixed 1/60s
280
+ * sub-step accumulator. Each segment's angular displacement θ (in radians
281
+ * internally) is emitted in DEGREES on its output parameter, so `rotate = 0`
282
+ * when the chain is at its authored rest pose.
283
+ *
284
+ * Usage:
285
+ * const chains = new HairChainMotion(
286
+ * model.physicsChains ?? [],
287
+ * model.parameters,
288
+ * model.deformers ?? [],
289
+ * (id) => currentValue(id),
290
+ * player.setParameter.bind(player),
291
+ * );
292
+ * // inside your rAF loop, right AFTER physics.update(now):
293
+ * chains.update(performance.now());
294
+ *
295
+ * The host schedules updates; this class has no timers, rAF, DOM, or Date.now.
296
+ */
297
+ declare class HairChainMotion {
298
+ private readonly chainData;
299
+ private readonly params;
300
+ private readonly deformers;
301
+ private readonly store;
302
+ private readonly read;
303
+ private readonly sink;
304
+ private readonly clock;
305
+ constructor(chains: IkiPhysicsChain[], params: IkiParameter[], deformers: IkiDeformer[], read: (id: string) => number, sink: (id: string, value: number) => void);
306
+ /**
307
+ * Advance every chain to the given wall-clock timestamp (milliseconds).
308
+ *
309
+ * First call: seed every segment to θ=0/ω=0 (rest), emit the rest output
310
+ * (outDefault + 0), and return without integrating — mirrors PhysicsMotion's
311
+ * first-frame behavior so a model loaded in motion does not kick.
312
+ *
313
+ * Subsequent calls: dt = clamp(nowMs - prevNowMs, 0, MAX_DT_MS) → seconds into
314
+ * accumulator. The per-frame world snapshot (anchor world angles) is taken ONCE
315
+ * per update() — NOT per chain — so all chains share a consistent frame snapshot.
316
+ * Fixed FIXED_DT_S sub-steps are run root→tip, capped at MAX_SUBSTEPS; leftover
317
+ * time is carried to the next frame. Segments emit after substeps (even on zero
318
+ * substeps) with a non-finite guard.
319
+ */
320
+ update(nowMs: number): void;
321
+ /**
322
+ * Extract world rotation (radians) from the anchor's Affine tuple.
323
+ * Affine = [a,b,c,d,e,f]; rotation column = (a,b) → atan2(b,a).
324
+ *
325
+ * If the anchor id is absent from the map, THROWS an internal Error — the
326
+ * format validator guarantees the anchor exists, so absence is an invariant
327
+ * break (mirrors resolveDeformerWorlds' throw on an unresolved parent,
328
+ * deform.ts:141).
329
+ */
330
+ private anchorWorldAngleRad;
331
+ /**
332
+ * One fixed sub-step of FIXED_DT_S seconds for all segments in a chain.
333
+ *
334
+ * Segments are integrated ROOT→TIP so each segment can read its upstream
335
+ * neighbor's current-substep state when computing the world angle Φ_i.
336
+ * (The chain is causal root-to-tip; reversing the order would use stale θ
337
+ * values from the previous substep for Φ_i computation.)
338
+ *
339
+ * Per-segment semi-implicit (symplectic) Euler:
340
+ * Φ_i = anchorWorldAngleRad + Σ_{j≤i}(restAngle_j + θ_j)
341
+ * α_i = (−stiffness_i·θ_i − strength·sin(Φ_i − gravityAngle_rad) − damping_i·ω_i) / mass_i
342
+ * ω_i += α_i · FIXED_DT_S (velocity updated FIRST = semi-implicit)
343
+ * θ_i += ω_i · FIXED_DT_S (position updated from NEW velocity)
344
+ *
345
+ * The spring term is −stiffness·θ (restoring θ→0); restAngle does NOT appear
346
+ * in the spring term, only in Φ_i for the gravity torque.
347
+ */
348
+ private stepChain;
349
+ /** Emit outDefault + (θ_i · RAD2DEG) · scale for one segment. */
350
+ private emitSegment;
351
+ }
352
+
353
+ export { type Affine, HairChainMotion, IdleMotion, type IdleMotionOptions, type IkiLoadResult, IkiPlayer, ParameterStore, PhysicsMotion, multiply, rotate, scale, toMat3, translate };