@doki-land/live2d 0.0.23 → 0.0.24

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/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _doki_land_live2d_core from '@doki-land/live2d-core';
2
- import { Live2dStageAssets, ModelSource, AssetResolver, ModelAsset, LoadProgress, ModelSettings, InternalModel, Live2dActor, CreateActorOptions, ActorTransform, PlayMotionActorOptions, ActorHit, CreateLive2dStageOptions, Live2dStage, PointerTrackingPolicy, StagePointerEvent, Live2DSession, HitAreaDefinition } from '@doki-land/live2d-core';
3
- export { ActorHit, ActorInstance, ActorTransform, AssetResolver, CreateActorOptions, CreateLive2dStageOptions, DEFAULT_ACTOR_TRANSFORM, EventEmitter, FrameProfile, FrameSnapshot, InternalModel, Live2DSession, Live2dActor, Live2dStage, Live2dStageAssets, LoadProgress, LoadProgressStage, ModelAsset, ModelFormat, ModelInstance, ModelProgram, ModelSettings, ModelSource, PlayMotionActorOptions, PointerTrackingMode, PointerTrackingPolicy, SessionPhase, SessionState, StagePointerEvent, StageUpdateMode } from '@doki-land/live2d-core';
2
+ import { Live2dStageAssets, ModelSource, AssetResolver, ModelAsset, LoadProgress, ModelSettings, InternalModel, Live2dActor, CreateActorOptions, ActorTransform, PlayMotionActorOptions, ActorHit, CreateLive2dStageOptions, Live2dStage, PointerTrackingPolicy, StagePointerEvent, Live2dSession, HitAreaDefinition } from '@doki-land/live2d-core';
3
+ export { ActorHit, ActorInstance, ActorTransform, AssetResolver, CreateActorOptions, CreateLive2dStageOptions, DEFAULT_ACTOR_TRANSFORM, EventEmitter, FrameProfile, FrameSnapshot, InternalModel, Live2dActor, Live2dSession, Live2dStage, Live2dStageAssets, LoadProgress, LoadProgressStage, ModelAsset, ModelFormat, ModelInstance, ModelProgram, ModelSettings, ModelSource, PlayMotionActorOptions, PointerTrackingMode, PointerTrackingPolicy, SessionPhase, SessionState, StagePointerEvent, StageUpdateMode } from '@doki-land/live2d-core';
4
4
  export { DEFAULT_NPM_CDN, resolveModelSourceUrl, resolveNpmSpecifier } from '@doki-land/live2d-loader';
5
5
  import * as _doki_land_live2d_renderer from '@doki-land/live2d-renderer';
6
6
  import { ModelBackend, compileSharedModelCompile, TextureData, Renderer, ParameterBinding, DrawableMesh, RendererKind } from '@doki-land/live2d-renderer';
@@ -219,7 +219,9 @@ declare class ModelAssetRegistry implements Live2dStageAssets {
219
219
  #private;
220
220
  constructor(options: ModelAssetRegistryOptions);
221
221
  load(source: ModelSource, resolver?: AssetResolver): Promise<ModelAsset>;
222
- acquire(source: ModelSource, resolver: AssetResolver | undefined, onProgress?: (payload: LoadProgress) => void): Promise<ModelAssetLease>;
222
+ acquire(source: ModelSource, resolver: AssetResolver | undefined, onProgress?: (payload: LoadProgress) => void, options?: {
223
+ signal?: AbortSignal;
224
+ }): Promise<ModelAssetLease>;
223
225
  acquireExisting(asset: ModelAsset): ModelAssetLease;
224
226
  destroy(): void;
225
227
  }
@@ -250,7 +252,9 @@ declare class ActorModelSlot {
250
252
  /** Load-time stable id → binding map (binding.value updated on setParameter). */
251
253
  parameterMap(): ReadonlyMap<string, ParameterBinding>;
252
254
  resolveParameter(id: string): number | undefined;
253
- load(source: ModelSource, resolver?: AssetResolver): Promise<InternalModel>;
255
+ load(source: ModelSource, resolver?: AssetResolver, options?: {
256
+ signal?: AbortSignal;
257
+ }): Promise<InternalModel>;
254
258
  loadAsset(asset: ModelAsset): Promise<InternalModel>;
255
259
  setParameter(id: string, value: number): void;
256
260
  listParameters(): readonly ParameterBinding[];
@@ -285,6 +289,19 @@ declare class Live2dActorImpl implements Live2dActor {
285
289
  readonly id: string;
286
290
  readonly creationIndex: number;
287
291
  constructor(options: CreateActorOptions | undefined, shared: Live2dActorImplOptions);
292
+ /** Bridge MotionPlayer lifecycle into Stage/facade event buses. */
293
+ setMotionEventHandlers(handlers: {
294
+ onStart?: (payload: {
295
+ group: string;
296
+ index: number;
297
+ slot: string;
298
+ }) => void;
299
+ onFinish?: (payload: {
300
+ group: string;
301
+ index: number;
302
+ slot: string;
303
+ }) => void;
304
+ }): void;
288
305
  get model(): InternalModel | null;
289
306
  get visible(): boolean;
290
307
  set visible(value: boolean);
@@ -296,7 +313,9 @@ declare class Live2dActorImpl implements Live2dActor {
296
313
  set order(value: number);
297
314
  getTransform(): ActorTransform;
298
315
  setTransform(patch: Partial<ActorTransform>): void;
299
- load(source: Parameters<Live2dActor["load"]>[0], resolver?: Parameters<Live2dActor["load"]>[1]): Promise<InternalModel>;
316
+ load(source: Parameters<Live2dActor["load"]>[0], resolver?: Parameters<Live2dActor["load"]>[1], options?: {
317
+ signal?: AbortSignal;
318
+ }): Promise<InternalModel>;
300
319
  loadAsset(asset: ModelAsset): Promise<InternalModel>;
301
320
  setParameter(id: string, value: number): void;
302
321
  listParameters(): readonly _doki_land_live2d_renderer.ParameterBinding[];
@@ -362,13 +381,13 @@ declare class Live2dStageImpl implements Live2dStage {
362
381
  }
363
382
  declare function createLive2dStage(options?: CreateLive2dStageFullOptions): Live2dStage;
364
383
 
365
- interface CreateLive2DOptions {
384
+ interface CreateLive2dOptions {
366
385
  backends?: ModelBackend[];
367
386
  renderer?: Renderer;
368
387
  prefer?: RendererKind[];
369
388
  updateMode?: "auto" | "manual";
370
389
  }
371
- interface Live2DRuntime extends Live2DSession {
390
+ interface Live2dRuntime extends Live2dSession {
372
391
  readonly renderer: Renderer;
373
392
  readonly backends: readonly ModelBackend[];
374
393
  readonly stage: Live2dStageImpl;
@@ -399,9 +418,52 @@ interface Live2DRuntime extends Live2DSession {
399
418
  quality?: number;
400
419
  }): Promise<Blob>;
401
420
  }
421
+ /** @deprecated Use `CreateLive2dOptions`. */
422
+ type CreateLive2DOptions = CreateLive2dOptions;
423
+ /** @deprecated Use `Live2dRuntime`. */
424
+ type Live2DRuntime = Live2dRuntime;
402
425
 
403
426
  /** Wire moc backends and a renderer into one single-actor session. */
404
- declare function createLive2D(options?: CreateLive2DOptions): Live2DRuntime;
427
+ declare function createLive2d(options?: CreateLive2dOptions): Live2dRuntime;
428
+ /** @deprecated Use `createLive2d`. */
429
+ declare const createLive2D: typeof createLive2d;
430
+
431
+ /** One physics output destination (parameter id only for the thin gate). */
432
+ interface Physics3Output {
433
+ readonly destinationId: string;
434
+ }
435
+ /** One PhysicsSettings entry with output destinations. */
436
+ interface Physics3Setting {
437
+ readonly id: string;
438
+ readonly outputs: readonly Physics3Output[];
439
+ }
440
+ /**
441
+ * Parsed Cubism `physics3.json` (minimal).
442
+ * Full spring / pendulum simulation is intentionally out of scope for this gate.
443
+ */
444
+ interface Physics3Clip {
445
+ readonly settings: readonly Physics3Setting[];
446
+ /** Flattened destination parameter ids from all outputs. */
447
+ readonly outputParameterIds: readonly string[];
448
+ }
449
+
450
+ interface Physics3ApplyBinding {
451
+ readonly value: number;
452
+ }
453
+ /**
454
+ * Minimal Physics3 apply gate — **not** a full Cubism spring / pendulum solver.
455
+ *
456
+ * For each output destination parameter id that exists in `bindings`, performs a
457
+ * trivial identity step (write current value back). Exists so load → update can
458
+ * exercise the physics clip API; real physics evaluation is a later milestone.
459
+ */
460
+ declare function applyPhysics3(clip: Physics3Clip, _deltaTimeSeconds: number, bindings: ReadonlyMap<string, Physics3ApplyBinding>, setParameter: (id: string, value: number) => void): void;
461
+
462
+ /**
463
+ * Parse Cubism `physics3.json` enough to expose PhysicsSettings output parameter ids.
464
+ * Tolerates minimal fixtures (missing / empty PhysicsSettings → empty clip).
465
+ */
466
+ declare function parsePhysics3(json: unknown): Physics3Clip;
405
467
 
406
468
  /** Parsed Cubism `pose3.json` / legacy `pose.json` groups. */
407
469
  interface Pose3Clip {
@@ -447,16 +509,16 @@ declare function resolveHitAreaName(input: ResolveHitAreaInput): string;
447
509
  * `@doki-land/live2d` — public facade.
448
510
  *
449
511
  * Layout:
450
- * - `facade/` — `createLive2D()` default stage + actor entry
512
+ * - `facade/` — `createLive2d()` default stage + actor entry
451
513
  * - `motion/` — motion3 parse + playback
452
514
  * - `stage/` — multi-actor stage, assets, transforms
453
515
  * - `reexports/` — optional subpath `@doki-land/live2d/{core,loader,renderer}`
454
516
  *
455
517
  * ```ts
456
- * import { createLive2D } from "@doki-land/live2d";
518
+ * import { createLive2d } from "@doki-land/live2d";
457
519
  * ```
458
520
  */
459
521
 
460
522
  declare const LIVE2D_VERSION: "0.0.0";
461
523
 
462
- export { type CreateLive2DOptions, type CreateLive2dStageFullOptions, type Expression3Clip, type Expression3Parameter, type ExpressionBlendMode, LIVE2D_VERSION, type Live2DRuntime, type Motion3Clip, type MotionApplySample, MotionPlayer, MotionPriority, type PlayMotionOptions, type Pose3Clip, allocateActorId, applyExpression3Clip, applyPose3Activation, blendMotionLayers, createLive2D, createLive2dStage, evaluateCurve, evaluateMotion3, focusParameterUpdates, parseExpression3, parseMotion3, parsePose3, resolveHitAreaName };
524
+ export { type CreateLive2DOptions, type CreateLive2dOptions, type CreateLive2dStageFullOptions, type Expression3Clip, type Expression3Parameter, type ExpressionBlendMode, LIVE2D_VERSION, type Live2DRuntime, type Live2dRuntime, type Motion3Clip, type MotionApplySample, MotionPlayer, MotionPriority, type Physics3ApplyBinding, type Physics3Clip, type Physics3Output, type Physics3Setting, type PlayMotionOptions, type Pose3Clip, allocateActorId, applyExpression3Clip, applyPhysics3, applyPose3Activation, blendMotionLayers, createLive2D, createLive2d, createLive2dStage, evaluateCurve, evaluateMotion3, focusParameterUpdates, parseExpression3, parseMotion3, parsePhysics3, parsePose3, resolveHitAreaName };
package/dist/index.js CHANGED
@@ -573,6 +573,60 @@ function optionalNum(v) {
573
573
  return typeof v === "number" && Number.isFinite(v) ? v : void 0;
574
574
  }
575
575
 
576
+ // src/physics/apply-physics3.ts
577
+ function applyPhysics3(clip, _deltaTimeSeconds, bindings, setParameter) {
578
+ for (const id of clip.outputParameterIds) {
579
+ const binding = bindings.get(id);
580
+ if (!binding) continue;
581
+ setParameter(id, binding.value);
582
+ }
583
+ }
584
+
585
+ // src/physics/parse-physics3.ts
586
+ function parsePhysics3(json) {
587
+ if (!json || typeof json !== "object") {
588
+ throw new Error(
589
+ "@doki-land/live2d: physics3 json root must be an object"
590
+ );
591
+ }
592
+ const root = json;
593
+ const settingsRaw = root.PhysicsSettings;
594
+ if (settingsRaw === void 0 || settingsRaw === null) {
595
+ return { settings: [], outputParameterIds: [] };
596
+ }
597
+ if (!Array.isArray(settingsRaw)) {
598
+ throw new Error("@doki-land/live2d: PhysicsSettings must be an array");
599
+ }
600
+ const settings = [];
601
+ const outputParameterIds = [];
602
+ for (let si = 0; si < settingsRaw.length; si++) {
603
+ const entry = settingsRaw[si];
604
+ if (!entry || typeof entry !== "object") {
605
+ throw new Error(
606
+ `@doki-land/live2d: PhysicsSettings[${si}] must be an object`
607
+ );
608
+ }
609
+ const rec = entry;
610
+ const id = typeof rec.Id === "string" ? rec.Id : `PhysicsSetting${si}`;
611
+ const outputsRaw = rec.Output;
612
+ const outputs = [];
613
+ if (Array.isArray(outputsRaw)) {
614
+ for (let oi = 0; oi < outputsRaw.length; oi++) {
615
+ const out = outputsRaw[oi];
616
+ if (!out || typeof out !== "object") continue;
617
+ const dest = out.Destination;
618
+ if (!dest || typeof dest !== "object") continue;
619
+ const destId = dest.Id;
620
+ if (typeof destId !== "string" || !destId) continue;
621
+ outputs.push({ destinationId: destId });
622
+ outputParameterIds.push(destId);
623
+ }
624
+ }
625
+ settings.push({ id, outputs });
626
+ }
627
+ return { settings, outputParameterIds };
628
+ }
629
+
576
630
  // src/pose/apply-pose3.ts
577
631
  function applyPose3Activation(clip, activatedPartId, setPartOpacity) {
578
632
  for (const group of clip.groups) {
@@ -645,6 +699,7 @@ var ActorModelSlot = class {
645
699
  #expressionCache = /* @__PURE__ */ new Map();
646
700
  #activeExpression = null;
647
701
  #poseClip = null;
702
+ #physicsClip = null;
648
703
  constructor(options) {
649
704
  this.#assets = options.assets;
650
705
  this.#renderer = options.renderer;
@@ -745,6 +800,27 @@ var ActorModelSlot = class {
745
800
  this.#poseClip = null;
746
801
  }
747
802
  }
803
+ async #loadPhysicsClip() {
804
+ this.#physicsClip = null;
805
+ const physicsPath = this.#model?.settings.physics;
806
+ if (!physicsPath || !this.#lease) return;
807
+ try {
808
+ const json = await this.#lease.resolver.fetchJson(physicsPath);
809
+ this.#physicsClip = parsePhysics3(json);
810
+ } catch {
811
+ this.#physicsClip = null;
812
+ }
813
+ }
814
+ #applyPhysicsLayer(deltaTimeSeconds) {
815
+ if (!this.#physicsClip || !this.#model || !this.#backend) return;
816
+ applyPhysics3(
817
+ this.#physicsClip,
818
+ deltaTimeSeconds,
819
+ this.#paramById,
820
+ (id, value) => this.#backend?.setParameter?.(this.#model, id, value)
821
+ );
822
+ this.#syncParamCacheFromBackend();
823
+ }
748
824
  #rebuildParamCache() {
749
825
  this.#paramById.clear();
750
826
  this.#paramIndexById.clear();
@@ -774,24 +850,37 @@ var ActorModelSlot = class {
774
850
  }
775
851
  return this.#paramIndexById.get(id);
776
852
  }
777
- async load(source, resolver) {
853
+ async load(source, resolver, options) {
778
854
  const gen = ++this.#loadGeneration;
855
+ const signal = options?.signal;
856
+ if (signal?.aborted) {
857
+ throw new Error("@doki-land/live2d: load cancelled");
858
+ }
859
+ const onAbort = () => {
860
+ this.#loadGeneration += 1;
861
+ };
862
+ signal?.addEventListener("abort", onAbort, { once: true });
779
863
  this.#report({
780
864
  stage: "mounting",
781
865
  progress: 0.01,
782
866
  detail: "prepare draw pass"
783
867
  });
784
868
  const drawPass = this.ensureDrawPass();
785
- const lease = await this.#assets.acquire(
786
- source,
787
- resolver,
788
- (p) => this.#report(p)
789
- );
790
- if (gen !== this.#loadGeneration) {
791
- lease.release();
792
- throw new Error("@doki-land/live2d: load cancelled");
869
+ try {
870
+ const lease = await this.#assets.acquire(
871
+ source,
872
+ resolver,
873
+ (p) => this.#report(p),
874
+ { signal }
875
+ );
876
+ if (gen !== this.#loadGeneration) {
877
+ lease.release();
878
+ throw new Error("@doki-land/live2d: load cancelled");
879
+ }
880
+ return await this.#attachLease(lease, drawPass, gen);
881
+ } finally {
882
+ signal?.removeEventListener("abort", onAbort);
793
883
  }
794
- return await this.#attachLease(lease, drawPass, gen);
795
884
  }
796
885
  async loadAsset(asset) {
797
886
  const gen = ++this.#loadGeneration;
@@ -817,6 +906,7 @@ var ActorModelSlot = class {
817
906
  this.#motionPlayer.clear();
818
907
  this.#activeExpression = null;
819
908
  this.#poseClip = null;
909
+ this.#physicsClip = null;
820
910
  this.#releaseLease();
821
911
  const { model, backend } = await lease.createInstance(this.#renderer);
822
912
  if (gen !== this.#loadGeneration) {
@@ -833,6 +923,7 @@ var ActorModelSlot = class {
833
923
  this.#backend = backend;
834
924
  this.#rebuildParamCache();
835
925
  await this.#loadPoseClip();
926
+ await this.#loadPhysicsClip();
836
927
  this.#report({
837
928
  stage: "ready",
838
929
  progress: 1,
@@ -907,6 +998,7 @@ var ActorModelSlot = class {
907
998
  this.#applyMotionSamples(this.#motionPlayer.update(deltaTimeSeconds));
908
999
  this.#tickExpression(deltaTimeSeconds);
909
1000
  this.#applyExpressionLayer();
1001
+ this.#applyPhysicsLayer(deltaTimeSeconds);
910
1002
  this.#backend.updateModel(this.#model, deltaTimeSeconds);
911
1003
  return this.#backend.getDrawables(this.#model);
912
1004
  }
@@ -946,6 +1038,7 @@ var ActorModelSlot = class {
946
1038
  this.#motionPlayer.clear();
947
1039
  this.#activeExpression = null;
948
1040
  this.#poseClip = null;
1041
+ this.#physicsClip = null;
949
1042
  if (this.#model && this.#backend) {
950
1043
  this.#backend.destroyModel(this.#model);
951
1044
  }
@@ -1171,9 +1264,18 @@ var Live2dActorImpl = class {
1171
1264
  this.#order = options?.order ?? 0;
1172
1265
  this.#slot = new ActorModelSlot({
1173
1266
  assets: shared.assets,
1174
- renderer: shared.renderer
1267
+ renderer: shared.renderer,
1268
+ onMotionStart: (payload) => this.#onMotionStart?.(payload),
1269
+ onMotionFinish: (payload) => this.#onMotionFinish?.(payload)
1175
1270
  });
1176
1271
  }
1272
+ #onMotionStart = null;
1273
+ #onMotionFinish = null;
1274
+ /** Bridge MotionPlayer lifecycle into Stage/facade event buses. */
1275
+ setMotionEventHandlers(handlers) {
1276
+ this.#onMotionStart = handlers.onStart ?? null;
1277
+ this.#onMotionFinish = handlers.onFinish ?? null;
1278
+ }
1177
1279
  get model() {
1178
1280
  return this.#slot.model;
1179
1281
  }
@@ -1210,11 +1312,11 @@ var Live2dActorImpl = class {
1210
1312
  ...patch
1211
1313
  });
1212
1314
  }
1213
- async load(source, resolver) {
1315
+ async load(source, resolver, options) {
1214
1316
  if (this.#destroyed) {
1215
1317
  throw new Error("@doki-land/live2d: actor destroyed");
1216
1318
  }
1217
- return await this.#slot.load(source, resolver);
1319
+ return await this.#slot.load(source, resolver, options);
1218
1320
  }
1219
1321
  async loadAsset(asset) {
1220
1322
  if (this.#destroyed) {
@@ -1363,10 +1465,10 @@ function createSingleActorFacade(stage, actor, backends) {
1363
1465
  }
1364
1466
  );
1365
1467
  },
1366
- async loadModel(source, resolver) {
1468
+ async loadModel(source, resolver, options) {
1367
1469
  setPhase("loading");
1368
1470
  try {
1369
- const model = await actor.load(source, resolver);
1471
+ const model = await actor.load(source, resolver, options);
1370
1472
  lastError = null;
1371
1473
  setPhase("live");
1372
1474
  events.emit("ready", { modelId: model.id });
@@ -1479,6 +1581,10 @@ function createSingleActorFacade(stage, actor, backends) {
1479
1581
  events.clear();
1480
1582
  }
1481
1583
  };
1584
+ actor.setMotionEventHandlers({
1585
+ onStart: (payload) => events.emit("motion:start", payload),
1586
+ onFinish: (payload) => events.emit("motion:finish", payload)
1587
+ });
1482
1588
  return runtime;
1483
1589
  }
1484
1590
 
@@ -1608,8 +1714,13 @@ var ModelAssetRegistry = class {
1608
1714
  const entry = await this.#ensureEntry(source, resolver);
1609
1715
  return new ModelAssetHandle(entry);
1610
1716
  }
1611
- async acquire(source, resolver, onProgress) {
1612
- const entry = await this.#ensureEntry(source, resolver, onProgress);
1717
+ async acquire(source, resolver, onProgress, options) {
1718
+ const entry = await this.#ensureEntry(
1719
+ source,
1720
+ resolver,
1721
+ onProgress,
1722
+ options
1723
+ );
1613
1724
  entry.refCount += 1;
1614
1725
  return this.#leaseFromEntry(entry);
1615
1726
  }
@@ -1660,13 +1771,19 @@ var ModelAssetRegistry = class {
1660
1771
  }
1661
1772
  };
1662
1773
  }
1663
- async #ensureEntry(source, resolver, onProgress) {
1774
+ async #ensureEntry(source, resolver, onProgress, options) {
1664
1775
  const key = resolveModelAssetKey(source);
1665
1776
  const existing = this.#entries.get(key);
1666
1777
  if (existing) return existing;
1667
1778
  let pending = this.#inFlight.get(key);
1668
1779
  if (!pending) {
1669
- pending = this.#compileEntry(key, source, resolver, onProgress);
1780
+ pending = this.#compileEntry(
1781
+ key,
1782
+ source,
1783
+ resolver,
1784
+ onProgress,
1785
+ options
1786
+ );
1670
1787
  this.#inFlight.set(key, pending);
1671
1788
  }
1672
1789
  try {
@@ -1677,8 +1794,12 @@ var ModelAssetRegistry = class {
1677
1794
  this.#inFlight.delete(key);
1678
1795
  }
1679
1796
  }
1680
- async #compileEntry(key, source, resolver, onProgress) {
1797
+ async #compileEntry(key, source, resolver, onProgress, options) {
1681
1798
  const notify = (payload) => onProgress?.(payload);
1799
+ const signal = options?.signal;
1800
+ if (signal?.aborted) {
1801
+ throw new Error("@doki-land/live2d: load cancelled");
1802
+ }
1682
1803
  notify({
1683
1804
  stage: "resolve",
1684
1805
  progress: 0.02,
@@ -1707,19 +1828,26 @@ var ModelAssetRegistry = class {
1707
1828
  progress: 0.05,
1708
1829
  detail: fetchUrl
1709
1830
  });
1710
- json = await fetchModelJson(fetchUrl, (u) => {
1711
- const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
1712
- notify({
1713
- stage: "settings",
1714
- progress: lerp(0.05, 0.22, ratio),
1715
- detail: fetchUrl,
1716
- bytesLoaded: u.bytesLoaded,
1717
- bytesTotal: u.bytesTotal
1718
- });
1719
- });
1831
+ json = await fetchModelJson(
1832
+ fetchUrl,
1833
+ (u) => {
1834
+ const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
1835
+ notify({
1836
+ stage: "settings",
1837
+ progress: lerp(0.05, 0.22, ratio),
1838
+ detail: fetchUrl,
1839
+ bytesLoaded: u.bytesLoaded,
1840
+ bytesTotal: u.bytesTotal
1841
+ });
1842
+ },
1843
+ { signal }
1844
+ );
1720
1845
  baseUrl = fetchUrl;
1721
1846
  settingsUrl = fetchUrl;
1722
1847
  }
1848
+ if (signal?.aborted) {
1849
+ throw new Error("@doki-land/live2d: load cancelled");
1850
+ }
1723
1851
  const settings = normalizeModelSettings(json, settingsUrl);
1724
1852
  notify({
1725
1853
  stage: "moc",
@@ -1727,6 +1855,7 @@ var ModelAssetRegistry = class {
1727
1855
  detail: settings.moc
1728
1856
  });
1729
1857
  const assetResolver = resolver ?? createUrlAssetResolver(baseUrl, {
1858
+ signal,
1730
1859
  onBytesProgress: (assetKey, u) => {
1731
1860
  const isMoc = assetKey === settings.moc;
1732
1861
  const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
@@ -1756,6 +1885,9 @@ var ModelAssetRegistry = class {
1756
1885
  detail: `decode ${settings.format}`
1757
1886
  });
1758
1887
  const mocBytes = await assetResolver.fetchBytes(settings.moc);
1888
+ if (signal?.aborted) {
1889
+ throw new Error("@doki-land/live2d: load cancelled");
1890
+ }
1759
1891
  const sharedCompile = compileSharedModelCompile(settings, mocBytes);
1760
1892
  const textures = [];
1761
1893
  if (settings.textures.length > 0) {
@@ -1779,6 +1911,9 @@ var ModelAssetRegistry = class {
1779
1911
  })
1780
1912
  );
1781
1913
  }
1914
+ if (signal?.aborted) {
1915
+ throw new Error("@doki-land/live2d: load cancelled");
1916
+ }
1782
1917
  notify({
1783
1918
  stage: "ready",
1784
1919
  progress: 1,
@@ -2148,7 +2283,7 @@ function createLive2dStage(options) {
2148
2283
  }
2149
2284
 
2150
2285
  // src/facade/create-live2d.ts
2151
- function createLive2D(options = {}) {
2286
+ function createLive2d(options = {}) {
2152
2287
  const backends = options.backends ?? [
2153
2288
  createMoc2Backend2(),
2154
2289
  createMoc3Backend2()
@@ -2163,6 +2298,7 @@ function createLive2D(options = {}) {
2163
2298
  });
2164
2299
  return createSingleActorFacade(stage, actor, backends);
2165
2300
  }
2301
+ var createLive2D = createLive2d;
2166
2302
 
2167
2303
  // src/index.ts
2168
2304
  var LIVE2D_VERSION = "0.0.0";
@@ -2175,10 +2311,12 @@ export {
2175
2311
  MotionPriority,
2176
2312
  allocateActorId,
2177
2313
  applyExpression3Clip,
2314
+ applyPhysics3,
2178
2315
  applyPose3Activation,
2179
2316
  blendMotionLayers,
2180
2317
  createCanvas2DRenderer,
2181
2318
  createLive2D,
2319
+ createLive2d,
2182
2320
  createLive2dStage,
2183
2321
  createMoc2Backend3 as createMoc2Backend,
2184
2322
  createMoc3Backend3 as createMoc3Backend,
@@ -2195,6 +2333,7 @@ export {
2195
2333
  parseCpuProgram,
2196
2334
  parseExpression3,
2197
2335
  parseMotion3,
2336
+ parsePhysics3,
2198
2337
  parsePose3,
2199
2338
  resolveHitAreaName,
2200
2339
  resolveModelSourceUrl2 as resolveModelSourceUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doki-land/live2d",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Live2D in the browser — load moc2/moc3 models, Stage + multi-actor, motion; WebGPU/WebGL2/Canvas2D. Main entry for live2d.ts.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -59,9 +59,9 @@
59
59
  "test": "vitest run --passWithNoTests"
60
60
  },
61
61
  "dependencies": {
62
- "@doki-land/live2d-core": "0.0.23",
63
- "@doki-land/live2d-loader": "0.0.23",
64
- "@doki-land/live2d-renderer": "0.0.23"
62
+ "@doki-land/live2d-core": "0.0.24",
63
+ "@doki-land/live2d-loader": "0.0.24",
64
+ "@doki-land/live2d-renderer": "0.0.24"
65
65
  },
66
66
  "sideEffects": false
67
67
  }
@@ -5,14 +5,18 @@ import {
5
5
  } from "@doki-land/live2d-renderer";
6
6
  import { allocateActorId } from "../stage/actor.js";
7
7
  import {
8
- type CreateLive2DOptions,
8
+ type CreateLive2dOptions,
9
9
  createSingleActorFacade,
10
- type Live2DRuntime,
10
+ type Live2dRuntime,
11
11
  } from "../stage/single-facade.js";
12
12
  import { createLive2dStage } from "../stage/stage.js";
13
13
 
14
14
  export type {
15
+ CreateLive2dOptions,
16
+ Live2dRuntime,
17
+ /** @deprecated Use `CreateLive2dOptions`. */
15
18
  CreateLive2DOptions,
19
+ /** @deprecated Use `Live2dRuntime`. */
16
20
  Live2DRuntime,
17
21
  } from "../stage/single-facade.js";
18
22
  export {
@@ -21,7 +25,7 @@ export {
21
25
  } from "../stage/single-facade.js";
22
26
 
23
27
  /** Wire moc backends and a renderer into one single-actor session. */
24
- export function createLive2D(options: CreateLive2DOptions = {}): Live2DRuntime {
28
+ export function createLive2d(options: CreateLive2dOptions = {}): Live2dRuntime {
25
29
  const backends = options.backends ?? [
26
30
  createMoc2Backend(),
27
31
  createMoc3Backend(),
@@ -37,3 +41,6 @@ export function createLive2D(options: CreateLive2DOptions = {}): Live2DRuntime {
37
41
  }) as import("../stage/actor.js").Live2dActorImpl;
38
42
  return createSingleActorFacade(stage, actor, backends);
39
43
  }
44
+
45
+ /** @deprecated Use `createLive2d`. */
46
+ export const createLive2D = createLive2d;
package/src/index.ts CHANGED
@@ -2,13 +2,13 @@
2
2
  * `@doki-land/live2d` — public facade.
3
3
  *
4
4
  * Layout:
5
- * - `facade/` — `createLive2D()` default stage + actor entry
5
+ * - `facade/` — `createLive2d()` default stage + actor entry
6
6
  * - `motion/` — motion3 parse + playback
7
7
  * - `stage/` — multi-actor stage, assets, transforms
8
8
  * - `reexports/` — optional subpath `@doki-land/live2d/{core,loader,renderer}`
9
9
  *
10
10
  * ```ts
11
- * import { createLive2D } from "@doki-land/live2d";
11
+ * import { createLive2d } from "@doki-land/live2d";
12
12
  * ```
13
13
  */
14
14
 
@@ -22,8 +22,8 @@ export type {
22
22
  FrameProfile,
23
23
  FrameSnapshot,
24
24
  InternalModel,
25
- Live2DSession,
26
25
  Live2dActor,
26
+ Live2dSession,
27
27
  Live2dStage,
28
28
  Live2dStageAssets,
29
29
  LoadProgress,
@@ -74,8 +74,14 @@ export {
74
74
  parseExpression3,
75
75
  } from "./expression/index.js";
76
76
  export {
77
- type CreateLive2DOptions,
77
+ type CreateLive2dOptions,
78
+ createLive2d,
79
+ type Live2dRuntime,
80
+ /** @deprecated Use `createLive2d`. */
78
81
  createLive2D,
82
+ /** @deprecated Use `CreateLive2dOptions`. */
83
+ type CreateLive2DOptions,
84
+ /** @deprecated Use `Live2dRuntime`. */
79
85
  type Live2DRuntime,
80
86
  MotionPriority,
81
87
  type PlayMotionOptions,
@@ -89,6 +95,14 @@ export {
89
95
  MotionPlayer,
90
96
  parseMotion3,
91
97
  } from "./motion/index.js";
98
+ export {
99
+ applyPhysics3,
100
+ type Physics3ApplyBinding,
101
+ type Physics3Clip,
102
+ type Physics3Output,
103
+ type Physics3Setting,
104
+ parsePhysics3,
105
+ } from "./physics/index.js";
92
106
  export {
93
107
  applyPose3Activation,
94
108
  type Pose3Clip,
@@ -0,0 +1,26 @@
1
+ import type { Physics3Clip } from "./types.js";
2
+
3
+ export interface Physics3ApplyBinding {
4
+ readonly value: number;
5
+ }
6
+
7
+ /**
8
+ * Minimal Physics3 apply gate — **not** a full Cubism spring / pendulum solver.
9
+ *
10
+ * For each output destination parameter id that exists in `bindings`, performs a
11
+ * trivial identity step (write current value back). Exists so load → update can
12
+ * exercise the physics clip API; real physics evaluation is a later milestone.
13
+ */
14
+ export function applyPhysics3(
15
+ clip: Physics3Clip,
16
+ _deltaTimeSeconds: number,
17
+ bindings: ReadonlyMap<string, Physics3ApplyBinding>,
18
+ setParameter: (id: string, value: number) => void,
19
+ ): void {
20
+ for (const id of clip.outputParameterIds) {
21
+ const binding = bindings.get(id);
22
+ if (!binding) continue;
23
+ // Identity / zero-step: prove the output walk without changing values.
24
+ setParameter(id, binding.value);
25
+ }
26
+ }
@@ -0,0 +1,7 @@
1
+ export { applyPhysics3, type Physics3ApplyBinding } from "./apply-physics3.js";
2
+ export { parsePhysics3 } from "./parse-physics3.js";
3
+ export type {
4
+ Physics3Clip,
5
+ Physics3Output,
6
+ Physics3Setting,
7
+ } from "./types.js";
@@ -0,0 +1,52 @@
1
+ import type { Physics3Clip, Physics3Output, Physics3Setting } from "./types.js";
2
+
3
+ /**
4
+ * Parse Cubism `physics3.json` enough to expose PhysicsSettings output parameter ids.
5
+ * Tolerates minimal fixtures (missing / empty PhysicsSettings → empty clip).
6
+ */
7
+ export function parsePhysics3(json: unknown): Physics3Clip {
8
+ if (!json || typeof json !== "object") {
9
+ throw new Error(
10
+ "@doki-land/live2d: physics3 json root must be an object",
11
+ );
12
+ }
13
+ const root = json as Record<string, unknown>;
14
+ const settingsRaw = root.PhysicsSettings;
15
+ if (settingsRaw === undefined || settingsRaw === null) {
16
+ return { settings: [], outputParameterIds: [] };
17
+ }
18
+ if (!Array.isArray(settingsRaw)) {
19
+ throw new Error("@doki-land/live2d: PhysicsSettings must be an array");
20
+ }
21
+
22
+ const settings: Physics3Setting[] = [];
23
+ const outputParameterIds: string[] = [];
24
+
25
+ for (let si = 0; si < settingsRaw.length; si++) {
26
+ const entry = settingsRaw[si];
27
+ if (!entry || typeof entry !== "object") {
28
+ throw new Error(
29
+ `@doki-land/live2d: PhysicsSettings[${si}] must be an object`,
30
+ );
31
+ }
32
+ const rec = entry as Record<string, unknown>;
33
+ const id = typeof rec.Id === "string" ? rec.Id : `PhysicsSetting${si}`;
34
+ const outputsRaw = rec.Output;
35
+ const outputs: Physics3Output[] = [];
36
+ if (Array.isArray(outputsRaw)) {
37
+ for (let oi = 0; oi < outputsRaw.length; oi++) {
38
+ const out = outputsRaw[oi];
39
+ if (!out || typeof out !== "object") continue;
40
+ const dest = (out as Record<string, unknown>).Destination;
41
+ if (!dest || typeof dest !== "object") continue;
42
+ const destId = (dest as Record<string, unknown>).Id;
43
+ if (typeof destId !== "string" || !destId) continue;
44
+ outputs.push({ destinationId: destId });
45
+ outputParameterIds.push(destId);
46
+ }
47
+ }
48
+ settings.push({ id, outputs });
49
+ }
50
+
51
+ return { settings, outputParameterIds };
52
+ }
@@ -0,0 +1,20 @@
1
+ /** One physics output destination (parameter id only for the thin gate). */
2
+ export interface Physics3Output {
3
+ readonly destinationId: string;
4
+ }
5
+
6
+ /** One PhysicsSettings entry with output destinations. */
7
+ export interface Physics3Setting {
8
+ readonly id: string;
9
+ readonly outputs: readonly Physics3Output[];
10
+ }
11
+
12
+ /**
13
+ * Parsed Cubism `physics3.json` (minimal).
14
+ * Full spring / pendulum simulation is intentionally out of scope for this gate.
15
+ */
16
+ export interface Physics3Clip {
17
+ readonly settings: readonly Physics3Setting[];
18
+ /** Flattened destination parameter ids from all outputs. */
19
+ readonly outputParameterIds: readonly string[];
20
+ }
@@ -22,6 +22,11 @@ import {
22
22
  type PlayMotionOptions,
23
23
  parseMotion3,
24
24
  } from "../motion/index.js";
25
+ import {
26
+ applyPhysics3,
27
+ type Physics3Clip,
28
+ parsePhysics3,
29
+ } from "../physics/index.js";
25
30
  import {
26
31
  applyPose3Activation,
27
32
  type Pose3Clip,
@@ -72,6 +77,7 @@ export class ActorModelSlot {
72
77
  weight: number;
73
78
  } | null = null;
74
79
  #poseClip: Pose3Clip | null = null;
80
+ #physicsClip: Physics3Clip | null = null;
75
81
 
76
82
  constructor(options: ActorModelSlotOptions) {
77
83
  this.#assets = options.assets;
@@ -187,6 +193,30 @@ export class ActorModelSlot {
187
193
  }
188
194
  }
189
195
 
196
+ async #loadPhysicsClip(): Promise<void> {
197
+ this.#physicsClip = null;
198
+ const physicsPath = this.#model?.settings.physics;
199
+ if (!physicsPath || !this.#lease) return;
200
+ try {
201
+ const json = await this.#lease.resolver.fetchJson(physicsPath);
202
+ this.#physicsClip = parsePhysics3(json);
203
+ } catch {
204
+ this.#physicsClip = null;
205
+ }
206
+ }
207
+
208
+ #applyPhysicsLayer(deltaTimeSeconds: number): void {
209
+ if (!this.#physicsClip || !this.#model || !this.#backend) return;
210
+ applyPhysics3(
211
+ this.#physicsClip,
212
+ deltaTimeSeconds,
213
+ this.#paramById,
214
+ (id, value) =>
215
+ this.#backend?.setParameter?.(this.#model!, id, value),
216
+ );
217
+ this.#syncParamCacheFromBackend();
218
+ }
219
+
190
220
  #rebuildParamCache(): void {
191
221
  this.#paramById.clear();
192
222
  this.#paramIndexById.clear();
@@ -223,8 +253,17 @@ export class ActorModelSlot {
223
253
  async load(
224
254
  source: ModelSource,
225
255
  resolver?: AssetResolver,
256
+ options?: { signal?: AbortSignal },
226
257
  ): Promise<InternalModel> {
227
258
  const gen = ++this.#loadGeneration;
259
+ const signal = options?.signal;
260
+ if (signal?.aborted) {
261
+ throw new Error("@doki-land/live2d: load cancelled");
262
+ }
263
+ const onAbort = () => {
264
+ this.#loadGeneration += 1;
265
+ };
266
+ signal?.addEventListener("abort", onAbort, { once: true });
228
267
  this.#report({
229
268
  stage: "mounting",
230
269
  progress: 0.01,
@@ -232,15 +271,22 @@ export class ActorModelSlot {
232
271
  });
233
272
  const drawPass = this.ensureDrawPass();
234
273
 
235
- const lease = await this.#assets.acquire(source, resolver, (p) =>
236
- this.#report(p),
237
- );
238
- if (gen !== this.#loadGeneration) {
239
- lease.release();
240
- throw new Error("@doki-land/live2d: load cancelled");
241
- }
274
+ try {
275
+ const lease = await this.#assets.acquire(
276
+ source,
277
+ resolver,
278
+ (p) => this.#report(p),
279
+ { signal },
280
+ );
281
+ if (gen !== this.#loadGeneration) {
282
+ lease.release();
283
+ throw new Error("@doki-land/live2d: load cancelled");
284
+ }
242
285
 
243
- return await this.#attachLease(lease, drawPass, gen);
286
+ return await this.#attachLease(lease, drawPass, gen);
287
+ } finally {
288
+ signal?.removeEventListener("abort", onAbort);
289
+ }
244
290
  }
245
291
 
246
292
  async loadAsset(asset: ModelAsset): Promise<InternalModel> {
@@ -275,6 +321,7 @@ export class ActorModelSlot {
275
321
  this.#motionPlayer.clear();
276
322
  this.#activeExpression = null;
277
323
  this.#poseClip = null;
324
+ this.#physicsClip = null;
278
325
  this.#releaseLease();
279
326
 
280
327
  const { model, backend } = await lease.createInstance(this.#renderer);
@@ -294,6 +341,7 @@ export class ActorModelSlot {
294
341
  this.#backend = backend;
295
342
  this.#rebuildParamCache();
296
343
  await this.#loadPoseClip();
344
+ await this.#loadPhysicsClip();
297
345
  this.#report({
298
346
  stage: "ready",
299
347
  progress: 1,
@@ -395,6 +443,7 @@ export class ActorModelSlot {
395
443
  this.#applyMotionSamples(this.#motionPlayer.update(deltaTimeSeconds));
396
444
  this.#tickExpression(deltaTimeSeconds);
397
445
  this.#applyExpressionLayer();
446
+ this.#applyPhysicsLayer(deltaTimeSeconds);
398
447
  this.#backend.updateModel(this.#model, deltaTimeSeconds);
399
448
  return this.#backend.getDrawables(this.#model);
400
449
  }
@@ -446,6 +495,7 @@ export class ActorModelSlot {
446
495
  this.#motionPlayer.clear();
447
496
  this.#activeExpression = null;
448
497
  this.#poseClip = null;
498
+ this.#physicsClip = null;
449
499
  if (this.#model && this.#backend) {
450
500
  this.#backend.destroyModel(this.#model);
451
501
  }
@@ -53,9 +53,35 @@ export class Live2dActorImpl implements Live2dActor {
53
53
  this.#slot = new ActorModelSlot({
54
54
  assets: shared.assets,
55
55
  renderer: shared.renderer,
56
+ onMotionStart: (payload) => this.#onMotionStart?.(payload),
57
+ onMotionFinish: (payload) => this.#onMotionFinish?.(payload),
56
58
  });
57
59
  }
58
60
 
61
+ #onMotionStart:
62
+ | ((payload: { group: string; index: number; slot: string }) => void)
63
+ | null = null;
64
+ #onMotionFinish:
65
+ | ((payload: { group: string; index: number; slot: string }) => void)
66
+ | null = null;
67
+
68
+ /** Bridge MotionPlayer lifecycle into Stage/facade event buses. */
69
+ setMotionEventHandlers(handlers: {
70
+ onStart?: (payload: {
71
+ group: string;
72
+ index: number;
73
+ slot: string;
74
+ }) => void;
75
+ onFinish?: (payload: {
76
+ group: string;
77
+ index: number;
78
+ slot: string;
79
+ }) => void;
80
+ }): void {
81
+ this.#onMotionStart = handlers.onStart ?? null;
82
+ this.#onMotionFinish = handlers.onFinish ?? null;
83
+ }
84
+
59
85
  get model(): InternalModel | null {
60
86
  return this.#slot.model;
61
87
  }
@@ -106,11 +132,12 @@ export class Live2dActorImpl implements Live2dActor {
106
132
  async load(
107
133
  source: Parameters<Live2dActor["load"]>[0],
108
134
  resolver?: Parameters<Live2dActor["load"]>[1],
135
+ options?: { signal?: AbortSignal },
109
136
  ): Promise<InternalModel> {
110
137
  if (this.#destroyed) {
111
138
  throw new Error("@doki-land/live2d: actor destroyed");
112
139
  }
113
- return await this.#slot.load(source, resolver);
140
+ return await this.#slot.load(source, resolver, options);
114
141
  }
115
142
 
116
143
  async loadAsset(asset: ModelAsset): Promise<InternalModel> {
@@ -1,8 +1,12 @@
1
1
  export { allocateActorId, Live2dActorImpl } from "./actor.js";
2
2
  export { ActorModelSlot } from "./actor-model-slot.js";
3
3
  export {
4
- type CreateLive2DOptions,
4
+ type CreateLive2dOptions,
5
5
  createSingleActorFacade,
6
+ type Live2dRuntime,
7
+ /** @deprecated Use `CreateLive2dOptions`. */
8
+ type CreateLive2DOptions,
9
+ /** @deprecated Use `Live2dRuntime`. */
6
10
  type Live2DRuntime,
7
11
  } from "./single-facade.js";
8
12
  export { createLive2dStage, Live2dStageImpl } from "./stage.js";
@@ -103,8 +103,14 @@ export class ModelAssetRegistry implements Live2dStageAssets {
103
103
  source: ModelSource,
104
104
  resolver: AssetResolver | undefined,
105
105
  onProgress?: (payload: LoadProgress) => void,
106
+ options?: { signal?: AbortSignal },
106
107
  ): Promise<ModelAssetLease> {
107
- const entry = await this.#ensureEntry(source, resolver, onProgress);
108
+ const entry = await this.#ensureEntry(
109
+ source,
110
+ resolver,
111
+ onProgress,
112
+ options,
113
+ );
108
114
  entry.refCount += 1;
109
115
  return this.#leaseFromEntry(entry);
110
116
  }
@@ -166,6 +172,7 @@ export class ModelAssetRegistry implements Live2dStageAssets {
166
172
  source: ModelSource,
167
173
  resolver?: AssetResolver,
168
174
  onProgress?: (payload: LoadProgress) => void,
175
+ options?: { signal?: AbortSignal },
169
176
  ): Promise<SharedModelAssetEntry> {
170
177
  const key = resolveModelAssetKey(source);
171
178
  const existing = this.#entries.get(key);
@@ -173,7 +180,13 @@ export class ModelAssetRegistry implements Live2dStageAssets {
173
180
 
174
181
  let pending = this.#inFlight.get(key);
175
182
  if (!pending) {
176
- pending = this.#compileEntry(key, source, resolver, onProgress);
183
+ pending = this.#compileEntry(
184
+ key,
185
+ source,
186
+ resolver,
187
+ onProgress,
188
+ options,
189
+ );
177
190
  this.#inFlight.set(key, pending);
178
191
  }
179
192
  try {
@@ -190,8 +203,13 @@ export class ModelAssetRegistry implements Live2dStageAssets {
190
203
  source: ModelSource,
191
204
  resolver: AssetResolver | undefined,
192
205
  onProgress?: (payload: LoadProgress) => void,
206
+ options?: { signal?: AbortSignal },
193
207
  ): Promise<SharedModelAssetEntry> {
194
208
  const notify = (payload: LoadProgress) => onProgress?.(payload);
209
+ const signal = options?.signal;
210
+ if (signal?.aborted) {
211
+ throw new Error("@doki-land/live2d: load cancelled");
212
+ }
195
213
 
196
214
  notify({
197
215
  stage: "resolve",
@@ -231,23 +249,31 @@ export class ModelAssetRegistry implements Live2dStageAssets {
231
249
  progress: 0.05,
232
250
  detail: fetchUrl,
233
251
  });
234
- json = await fetchModelJson(fetchUrl, (u) => {
235
- const ratio =
236
- u.bytesTotal && u.bytesTotal > 0
237
- ? u.bytesLoaded / u.bytesTotal
238
- : 0;
239
- notify({
240
- stage: "settings",
241
- progress: lerp(0.05, 0.22, ratio),
242
- detail: fetchUrl,
243
- bytesLoaded: u.bytesLoaded,
244
- bytesTotal: u.bytesTotal,
245
- });
246
- });
252
+ json = await fetchModelJson(
253
+ fetchUrl,
254
+ (u) => {
255
+ const ratio =
256
+ u.bytesTotal && u.bytesTotal > 0
257
+ ? u.bytesLoaded / u.bytesTotal
258
+ : 0;
259
+ notify({
260
+ stage: "settings",
261
+ progress: lerp(0.05, 0.22, ratio),
262
+ detail: fetchUrl,
263
+ bytesLoaded: u.bytesLoaded,
264
+ bytesTotal: u.bytesTotal,
265
+ });
266
+ },
267
+ { signal },
268
+ );
247
269
  baseUrl = fetchUrl;
248
270
  settingsUrl = fetchUrl;
249
271
  }
250
272
 
273
+ if (signal?.aborted) {
274
+ throw new Error("@doki-land/live2d: load cancelled");
275
+ }
276
+
251
277
  const settings = normalizeModelSettings(json, settingsUrl);
252
278
  notify({
253
279
  stage: "moc",
@@ -258,6 +284,7 @@ export class ModelAssetRegistry implements Live2dStageAssets {
258
284
  const assetResolver =
259
285
  resolver ??
260
286
  createUrlAssetResolver(baseUrl, {
287
+ signal,
261
288
  onBytesProgress: (assetKey, u) => {
262
289
  const isMoc = assetKey === settings.moc;
263
290
  const ratio =
@@ -292,6 +319,9 @@ export class ModelAssetRegistry implements Live2dStageAssets {
292
319
  });
293
320
 
294
321
  const mocBytes = await assetResolver.fetchBytes(settings.moc);
322
+ if (signal?.aborted) {
323
+ throw new Error("@doki-land/live2d: load cancelled");
324
+ }
295
325
  const sharedCompile = compileSharedModelCompile(settings, mocBytes);
296
326
 
297
327
  const textures: TextureData[] = [];
@@ -317,6 +347,10 @@ export class ModelAssetRegistry implements Live2dStageAssets {
317
347
  );
318
348
  }
319
349
 
350
+ if (signal?.aborted) {
351
+ throw new Error("@doki-land/live2d: load cancelled");
352
+ }
353
+
320
354
  notify({
321
355
  stage: "ready",
322
356
  progress: 1,
@@ -1,6 +1,6 @@
1
1
  import type {
2
2
  FrameSnapshot,
3
- Live2DSession,
3
+ Live2dSession,
4
4
  LoadProgress,
5
5
  ModelSource,
6
6
  SessionPhase,
@@ -18,14 +18,14 @@ import { MotionPriority } from "../motion/index.js";
18
18
  import type { Live2dActorImpl } from "./actor.js";
19
19
  import type { Live2dStageImpl } from "./stage.js";
20
20
 
21
- export interface CreateLive2DOptions {
21
+ export interface CreateLive2dOptions {
22
22
  backends?: ModelBackend[];
23
23
  renderer?: Renderer;
24
24
  prefer?: RendererKind[];
25
25
  updateMode?: "auto" | "manual";
26
26
  }
27
27
 
28
- export interface Live2DRuntime extends Live2DSession {
28
+ export interface Live2dRuntime extends Live2dSession {
29
29
  readonly renderer: Renderer;
30
30
  readonly backends: readonly ModelBackend[];
31
31
  readonly stage: Live2dStageImpl;
@@ -73,7 +73,7 @@ export function createSingleActorFacade(
73
73
  stage: Live2dStageImpl,
74
74
  actor: Live2dActorImpl,
75
75
  backends: readonly ModelBackend[],
76
- ): Live2DRuntime {
76
+ ): Live2dRuntime {
77
77
  const events = new EventEmitter();
78
78
  let canvas: HTMLCanvasElement | null = null;
79
79
  let phase: SessionPhase = "idle";
@@ -92,7 +92,7 @@ export function createSingleActorFacade(
92
92
  generation,
93
93
  });
94
94
 
95
- const runtime: Live2DRuntime = {
95
+ const runtime: Live2dRuntime = {
96
96
  events,
97
97
  backends,
98
98
  renderer: stage.renderer,
@@ -117,10 +117,10 @@ export function createSingleActorFacade(
117
117
  },
118
118
  );
119
119
  },
120
- async loadModel(source: ModelSource, resolver) {
120
+ async loadModel(source: ModelSource, resolver, options) {
121
121
  setPhase("loading");
122
122
  try {
123
- const model = await actor.load(source, resolver);
123
+ const model = await actor.load(source, resolver, options);
124
124
  lastError = null;
125
125
  setPhase("live");
126
126
  events.emit("ready", { modelId: model.id });
@@ -234,7 +234,17 @@ export function createSingleActorFacade(
234
234
  },
235
235
  };
236
236
 
237
+ actor.setMotionEventHandlers({
238
+ onStart: (payload) => events.emit("motion:start", payload),
239
+ onFinish: (payload) => events.emit("motion:finish", payload),
240
+ });
241
+
237
242
  return runtime;
238
243
  }
239
244
 
240
245
  export { MotionPriority, type PlayMotionOptions };
246
+
247
+ /** @deprecated Use `CreateLive2dOptions`. */
248
+ export type CreateLive2DOptions = CreateLive2dOptions;
249
+ /** @deprecated Use `Live2dRuntime`. */
250
+ export type Live2DRuntime = Live2dRuntime;