@ikijs/engine 0.2.0 → 0.3.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/README.md CHANGED
@@ -38,15 +38,16 @@ clamped to the declared range; unknown ids and non-finite values are ignored.
38
38
 
39
39
  ## API
40
40
 
41
- | Export | What it is |
42
- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
43
- | `IkiPlayer` | The renderer: `load` / `start` / `stop` / `setParameter` / `getParameter` / `getParameters` / `destroy` |
44
- | `IkiLoadResult` | `{ failedTextures, superseded }` returned by `load()` |
45
- | `ParameterStore` | The clamped parameter map the player drives |
46
- | `IdleMotion` | Auto-blink / breath / gaze-drift driver |
47
- | `PhysicsMotion` | Spring-mass-damper secondary motion (`model.physics`) |
48
- | `HairChainMotion` | Multi-segment angular chain with gravity (`model.physicsChains`) |
49
- | `translate` `rotate` `scale` `multiply` `toMat3` | The 2D affine helpers the engine itself uses |
41
+ | Export | What it is |
42
+ | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
43
+ | `IkiPlayer` | The renderer: `load` / `start` / `stop` / `setParameter` / `getParameter` / `getParameters` / `destroy` |
44
+ | `IkiLoadResult` | `{ failedTextures, superseded }` returned by `load()` |
45
+ | `ParameterStore` | The clamped parameter map the player drives |
46
+ | `IkiMotion` | The three drivers below, built from a model and stepped as one; `drivenParameterIds` lists what they write |
47
+ | `IdleMotion` | Auto-blink / breath / gaze-drift driver |
48
+ | `PhysicsMotion` | Spring-mass-damper secondary motion (`model.physics`) |
49
+ | `HairChainMotion` | Multi-segment angular chain with gravity (`model.physicsChains`) |
50
+ | `translate` `rotate` `scale` `multiply` `toMat3` | The 2D affine helpers the engine itself uses |
50
51
 
51
52
  ## Motion drivers
52
53
 
@@ -56,37 +57,30 @@ per frame, and each writes through a sink you supply. That keeps them testable
56
57
  and lets a host override or omit any of them.
57
58
 
58
59
  ```ts
59
- import { HairChainMotion, IdleMotion, PhysicsMotion } from "@ikijs/engine";
60
+ import { IkiMotion } from "@ikijs/engine";
60
61
 
61
62
  // The drivers read the live pose and write the next one, both through the
62
63
  // player — no host-side copy of the parameter state to keep in sync.
63
- const drive = (id: string, value: number) => player.setParameter(id, value);
64
- const read = (id: string) => player.getParameter(id);
65
-
66
- const idle = new IdleMotion(drive);
67
- const physics = new PhysicsMotion(
68
- model.physics ?? [],
69
- model.parameters,
70
- read,
71
- drive,
72
- );
73
- const chains = new HairChainMotion(
74
- model.physicsChains ?? [],
75
- model.parameters,
76
- model.deformers ?? [],
77
- read,
78
- drive,
64
+ const motion = new IkiMotion(
65
+ model,
66
+ (id) => player.getParameter(id),
67
+ (id, value) => player.setParameter(id, value),
79
68
  );
80
69
 
81
70
  const tick = (now: number) => {
82
- idle.update(now);
83
- physics.update(now); // reads what idle just wrote
84
- chains.update(now);
71
+ motion.update(now); // idle, then physics (lags what idle just wrote), then chains
85
72
  requestAnimationFrame(tick);
86
73
  };
87
74
  requestAnimationFrame(tick);
88
75
  ```
89
76
 
77
+ To stop, stop calling `update()` — the drivers leave the pose where it was,
78
+ and `motion.drivenParameterIds` lists every parameter they wrote for a host
79
+ that wants to restore it. The three drivers are also exported individually
80
+ (`IdleMotion`, `PhysicsMotion`, `HairChainMotion`): the two physics drivers
81
+ take the same `read`/`sink` pair, `IdleMotion` only the `sink`, and each is
82
+ stepped with `update(nowMs)` in that order.
83
+
90
84
  Both physics drivers integrate on a fixed 1/60 s sub-step with a clamped frame
91
85
  delta, so a backgrounded tab or a long hitch cannot snap the rig.
92
86
 
package/dist/index.d.mts CHANGED
@@ -189,7 +189,7 @@ interface IdleMotionOptions {
189
189
  rng?: () => number;
190
190
  }
191
191
  /**
192
- * Pure-logic idle-animation driver. Animates the seven "life" parameters
192
+ * Pure-logic idle-animation driver. Animates the eight "life" parameters
193
193
  * (eyes, breath, gaze, head sway) on an internal clock so tab-backgrounding
194
194
  * or irregular frame delivery can't produce teleports or snap-close blinks.
195
195
  *
@@ -201,6 +201,13 @@ interface IdleMotionOptions {
201
201
  * The host is responsible for scheduling; this class has no timers or rAF.
202
202
  */
203
203
  declare class IdleMotion {
204
+ /**
205
+ * Every parameter id {@link update} and `emitRestingPose` write, in write
206
+ * order. Published so a host (or IkiMotion) can restore them after it
207
+ * stops driving — keep in step with those two methods; the test suite
208
+ * pins this list to the actual emissions.
209
+ */
210
+ readonly drivenParameterIds: readonly string[];
204
211
  private readonly sink;
205
212
  private readonly rng;
206
213
  private clockMs;
@@ -262,6 +269,8 @@ declare class IdleMotion {
262
269
  * updated params on its own render loop (drivers and rendering are decoupled).
263
270
  */
264
271
  declare class PhysicsMotion {
272
+ /** The output parameter of every rig, in rig order — what `emit` writes. */
273
+ readonly drivenParameterIds: readonly string[];
265
274
  private readonly rigs;
266
275
  private readonly read;
267
276
  private readonly sink;
@@ -317,6 +326,8 @@ declare class PhysicsMotion {
317
326
  * The host schedules updates; this class has no timers, rAF, DOM, or Date.now.
318
327
  */
319
328
  declare class HairChainMotion {
329
+ /** Every segment's output, chain by chain — what `emitSegment` writes. */
330
+ readonly drivenParameterIds: readonly string[];
320
331
  private readonly chainData;
321
332
  private readonly params;
322
333
  private readonly deformers;
@@ -372,4 +383,49 @@ declare class HairChainMotion {
372
383
  private emitSegment;
373
384
  }
374
385
 
375
- export { type Affine, HairChainMotion, IdleMotion, type IdleMotionOptions, type IkiLoadResult, IkiPlayer, ParameterStore, PhysicsMotion, multiply, rotate, scale, toMat3, translate };
386
+ /**
387
+ * Bundles the three motion drivers — {@link IdleMotion}, {@link PhysicsMotion},
388
+ * {@link HairChainMotion} — into the one loop every host otherwise hand-builds:
389
+ * construct all three from the model, step them in the order physics and
390
+ * chains depend on, and know what they wrote.
391
+ *
392
+ * Usage:
393
+ * const motion = new IkiMotion(
394
+ * model,
395
+ * (id) => player.getParameter(id),
396
+ * (id, value) => player.setParameter(id, value),
397
+ * );
398
+ * // inside your rAF loop:
399
+ * motion.update(performance.now());
400
+ *
401
+ * The host schedules; this class has no timers, rAF, DOM, or Date.now.
402
+ *
403
+ * Stopping is the host's too: stop calling update(). The drivers leave the
404
+ * pose where it was — a host that wants it back writes its own resting values
405
+ * to `drivenParameterIds`.
406
+ */
407
+ declare class IkiMotion {
408
+ /**
409
+ * Idle ids, then rig outputs, then chain-segment outputs, deduplicated and
410
+ * insertion-ordered. May name ids the model lacks (the player silently
411
+ * drops writes to unknown ids) — intersect with the model's parameters if
412
+ * you mirror into your own store.
413
+ */
414
+ readonly drivenParameterIds: readonly string[];
415
+ private readonly idle;
416
+ private readonly physics;
417
+ private readonly chains;
418
+ constructor(model: IkiModel, read: (id: string) => number, sink: (id: string, value: number) => void);
419
+ /**
420
+ * Advance idle, then physics, then chains to the same wall-clock timestamp
421
+ * (milliseconds). Order is load-bearing: `PhysicsMotion` reads its input
422
+ * parameters (typically `ParamAngleX/Z`) through `read`, and the head sway
423
+ * idle wrote THIS frame is what the springs must lag behind; `HairChainMotion`
424
+ * then resolves its anchor deformer's world rotation from that same
425
+ * just-written pose (which may include a physics output). One timestamp for
426
+ * all three keeps their dt in lockstep.
427
+ */
428
+ update(nowMs: number): void;
429
+ }
430
+
431
+ export { type Affine, HairChainMotion, IdleMotion, type IdleMotionOptions, type IkiLoadResult, IkiMotion, IkiPlayer, ParameterStore, PhysicsMotion, multiply, rotate, scale, toMat3, translate };
package/dist/index.d.ts CHANGED
@@ -189,7 +189,7 @@ interface IdleMotionOptions {
189
189
  rng?: () => number;
190
190
  }
191
191
  /**
192
- * Pure-logic idle-animation driver. Animates the seven "life" parameters
192
+ * Pure-logic idle-animation driver. Animates the eight "life" parameters
193
193
  * (eyes, breath, gaze, head sway) on an internal clock so tab-backgrounding
194
194
  * or irregular frame delivery can't produce teleports or snap-close blinks.
195
195
  *
@@ -201,6 +201,13 @@ interface IdleMotionOptions {
201
201
  * The host is responsible for scheduling; this class has no timers or rAF.
202
202
  */
203
203
  declare class IdleMotion {
204
+ /**
205
+ * Every parameter id {@link update} and `emitRestingPose` write, in write
206
+ * order. Published so a host (or IkiMotion) can restore them after it
207
+ * stops driving — keep in step with those two methods; the test suite
208
+ * pins this list to the actual emissions.
209
+ */
210
+ readonly drivenParameterIds: readonly string[];
204
211
  private readonly sink;
205
212
  private readonly rng;
206
213
  private clockMs;
@@ -262,6 +269,8 @@ declare class IdleMotion {
262
269
  * updated params on its own render loop (drivers and rendering are decoupled).
263
270
  */
264
271
  declare class PhysicsMotion {
272
+ /** The output parameter of every rig, in rig order — what `emit` writes. */
273
+ readonly drivenParameterIds: readonly string[];
265
274
  private readonly rigs;
266
275
  private readonly read;
267
276
  private readonly sink;
@@ -317,6 +326,8 @@ declare class PhysicsMotion {
317
326
  * The host schedules updates; this class has no timers, rAF, DOM, or Date.now.
318
327
  */
319
328
  declare class HairChainMotion {
329
+ /** Every segment's output, chain by chain — what `emitSegment` writes. */
330
+ readonly drivenParameterIds: readonly string[];
320
331
  private readonly chainData;
321
332
  private readonly params;
322
333
  private readonly deformers;
@@ -372,4 +383,49 @@ declare class HairChainMotion {
372
383
  private emitSegment;
373
384
  }
374
385
 
375
- export { type Affine, HairChainMotion, IdleMotion, type IdleMotionOptions, type IkiLoadResult, IkiPlayer, ParameterStore, PhysicsMotion, multiply, rotate, scale, toMat3, translate };
386
+ /**
387
+ * Bundles the three motion drivers — {@link IdleMotion}, {@link PhysicsMotion},
388
+ * {@link HairChainMotion} — into the one loop every host otherwise hand-builds:
389
+ * construct all three from the model, step them in the order physics and
390
+ * chains depend on, and know what they wrote.
391
+ *
392
+ * Usage:
393
+ * const motion = new IkiMotion(
394
+ * model,
395
+ * (id) => player.getParameter(id),
396
+ * (id, value) => player.setParameter(id, value),
397
+ * );
398
+ * // inside your rAF loop:
399
+ * motion.update(performance.now());
400
+ *
401
+ * The host schedules; this class has no timers, rAF, DOM, or Date.now.
402
+ *
403
+ * Stopping is the host's too: stop calling update(). The drivers leave the
404
+ * pose where it was — a host that wants it back writes its own resting values
405
+ * to `drivenParameterIds`.
406
+ */
407
+ declare class IkiMotion {
408
+ /**
409
+ * Idle ids, then rig outputs, then chain-segment outputs, deduplicated and
410
+ * insertion-ordered. May name ids the model lacks (the player silently
411
+ * drops writes to unknown ids) — intersect with the model's parameters if
412
+ * you mirror into your own store.
413
+ */
414
+ readonly drivenParameterIds: readonly string[];
415
+ private readonly idle;
416
+ private readonly physics;
417
+ private readonly chains;
418
+ constructor(model: IkiModel, read: (id: string) => number, sink: (id: string, value: number) => void);
419
+ /**
420
+ * Advance idle, then physics, then chains to the same wall-clock timestamp
421
+ * (milliseconds). Order is load-bearing: `PhysicsMotion` reads its input
422
+ * parameters (typically `ParamAngleX/Z`) through `read`, and the head sway
423
+ * idle wrote THIS frame is what the springs must lag behind; `HairChainMotion`
424
+ * then resolves its anchor deformer's world rotation from that same
425
+ * just-written pose (which may include a physics output). One timestamp for
426
+ * all three keeps their dt in lockstep.
427
+ */
428
+ update(nowMs: number): void;
429
+ }
430
+
431
+ export { type Affine, HairChainMotion, IdleMotion, type IdleMotionOptions, type IkiLoadResult, IkiMotion, IkiPlayer, ParameterStore, PhysicsMotion, multiply, rotate, scale, toMat3, translate };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  HairChainMotion: () => HairChainMotion,
24
24
  IdleMotion: () => IdleMotion,
25
+ IkiMotion: () => IkiMotion,
25
26
  IkiPlayer: () => IkiPlayer,
26
27
  ParameterStore: () => ParameterStore,
27
28
  PhysicsMotion: () => PhysicsMotion,
@@ -1098,6 +1099,22 @@ function randomInDisk(rng, radius) {
1098
1099
  return [r * Math.cos(theta), r * Math.sin(theta)];
1099
1100
  }
1100
1101
  var IdleMotion = class {
1102
+ /**
1103
+ * Every parameter id {@link update} and `emitRestingPose` write, in write
1104
+ * order. Published so a host (or IkiMotion) can restore them after it
1105
+ * stops driving — keep in step with those two methods; the test suite
1106
+ * pins this list to the actual emissions.
1107
+ */
1108
+ drivenParameterIds = [
1109
+ import_format.StandardParameter.EyeOpenLeft,
1110
+ import_format.StandardParameter.EyeOpenRight,
1111
+ import_format.StandardParameter.Breath,
1112
+ import_format.StandardParameter.EyeballX,
1113
+ import_format.StandardParameter.EyeballY,
1114
+ import_format.StandardParameter.AngleX,
1115
+ import_format.StandardParameter.AngleY,
1116
+ import_format.StandardParameter.AngleZ
1117
+ ];
1101
1118
  sink;
1102
1119
  rng;
1103
1120
  // Internal clock: advances by clamped dt, NOT by raw wall-clock jumps.
@@ -1230,6 +1247,8 @@ function signedNormalized(value, param) {
1230
1247
  return clamp((value - rest) / den, -1, 1);
1231
1248
  }
1232
1249
  var PhysicsMotion = class {
1250
+ /** The output parameter of every rig, in rig order — what `emit` writes. */
1251
+ drivenParameterIds;
1233
1252
  rigs;
1234
1253
  read;
1235
1254
  sink;
@@ -1243,6 +1262,7 @@ var PhysicsMotion = class {
1243
1262
  this.sink = sink;
1244
1263
  this.params = new Map(params.map((p) => [p.id, p]));
1245
1264
  this.state = rigs.map(() => ({ x: 0, v: 0 }));
1265
+ this.drivenParameterIds = rigs.map((rig) => rig.output.parameter);
1246
1266
  }
1247
1267
  /**
1248
1268
  * Advance every rig to the given wall-clock timestamp (milliseconds).
@@ -1313,6 +1333,8 @@ var PhysicsMotion = class {
1313
1333
  var DEG2RAD = Math.PI / 180;
1314
1334
  var RAD2DEG = 180 / Math.PI;
1315
1335
  var HairChainMotion = class {
1336
+ /** Every segment's output, chain by chain — what `emitSegment` writes. */
1337
+ drivenParameterIds;
1316
1338
  chainData;
1317
1339
  params;
1318
1340
  deformers;
@@ -1333,6 +1355,9 @@ var HairChainMotion = class {
1333
1355
  ),
1334
1356
  state: chain.segments.map(() => ({ angle: 0, angularVelocity: 0 }))
1335
1357
  }));
1358
+ this.drivenParameterIds = chains.flatMap(
1359
+ (c) => c.segments.map((s) => s.output.parameter)
1360
+ );
1336
1361
  }
1337
1362
  /**
1338
1363
  * Advance every chain to the given wall-clock timestamp (milliseconds).
@@ -1445,10 +1470,59 @@ var HairChainMotion = class {
1445
1470
  );
1446
1471
  }
1447
1472
  };
1473
+
1474
+ // src/iki-motion.ts
1475
+ var IkiMotion = class {
1476
+ /**
1477
+ * Idle ids, then rig outputs, then chain-segment outputs, deduplicated and
1478
+ * insertion-ordered. May name ids the model lacks (the player silently
1479
+ * drops writes to unknown ids) — intersect with the model's parameters if
1480
+ * you mirror into your own store.
1481
+ */
1482
+ drivenParameterIds;
1483
+ idle;
1484
+ physics;
1485
+ chains;
1486
+ constructor(model, read, sink) {
1487
+ const rigs = model.physics ?? [];
1488
+ const chains = model.physicsChains ?? [];
1489
+ this.idle = new IdleMotion(sink);
1490
+ this.physics = new PhysicsMotion(rigs, model.parameters, read, sink);
1491
+ this.chains = new HairChainMotion(
1492
+ chains,
1493
+ model.parameters,
1494
+ model.deformers ?? [],
1495
+ read,
1496
+ sink
1497
+ );
1498
+ this.drivenParameterIds = [
1499
+ .../* @__PURE__ */ new Set([
1500
+ ...this.idle.drivenParameterIds,
1501
+ ...this.physics.drivenParameterIds,
1502
+ ...this.chains.drivenParameterIds
1503
+ ])
1504
+ ];
1505
+ }
1506
+ /**
1507
+ * Advance idle, then physics, then chains to the same wall-clock timestamp
1508
+ * (milliseconds). Order is load-bearing: `PhysicsMotion` reads its input
1509
+ * parameters (typically `ParamAngleX/Z`) through `read`, and the head sway
1510
+ * idle wrote THIS frame is what the springs must lag behind; `HairChainMotion`
1511
+ * then resolves its anchor deformer's world rotation from that same
1512
+ * just-written pose (which may include a physics output). One timestamp for
1513
+ * all three keeps their dt in lockstep.
1514
+ */
1515
+ update(nowMs) {
1516
+ this.idle.update(nowMs);
1517
+ this.physics.update(nowMs);
1518
+ this.chains.update(nowMs);
1519
+ }
1520
+ };
1448
1521
  // Annotate the CommonJS export names for ESM import in node:
1449
1522
  0 && (module.exports = {
1450
1523
  HairChainMotion,
1451
1524
  IdleMotion,
1525
+ IkiMotion,
1452
1526
  IkiPlayer,
1453
1527
  ParameterStore,
1454
1528
  PhysicsMotion,