@laplace.live/pixijs-live2d 0.3.0 → 0.5.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/dist/index.d.ts CHANGED
@@ -3240,6 +3240,14 @@ export declare enum MotionPreloadStrategy {
3240
3240
  /** No preload. */
3241
3241
  NONE = "NONE"
3242
3242
  }
3243
+ /**
3244
+ * What plays when nothing else does: a motion to loop, `'idle-group'` for a random motion of
3245
+ * the idle group (the default), or `null` to hold still.
3246
+ */
3247
+ export type IdleMotion = {
3248
+ group: string;
3249
+ index: number;
3250
+ } | 'idle-group' | null;
3243
3251
  /**
3244
3252
  * Handles the motion playback for Live2D models.
3245
3253
  * Responsible for loading, playing, and managing motion states and audio.
@@ -3322,6 +3330,17 @@ export declare abstract class MotionManager<Motion = unknown, MotionSpec = unkno
3322
3330
  * @type {boolean}
3323
3331
  */
3324
3332
  destroyed: boolean;
3333
+ /** The idle selection; see {@link setIdleMotion}. */
3334
+ idleMotion: IdleMotion;
3335
+ /**
3336
+ * Choose what plays when nothing else does. A running idle gives way at once and a loading one
3337
+ * is dropped; a higher-priority motion in flight hands over to the new selection when it ends.
3338
+ */
3339
+ setIdleMotion(motion: IdleMotion): void;
3340
+ /** Stop the idle so the next update starts the selection. Cubism eases its parameters home too. */
3341
+ protected releaseIdle(): void;
3342
+ /** Start the idle selection at IDLE priority; a chosen motion loops regardless of its own flag. */
3343
+ protected startIdleMotion(): Promise<boolean>;
3325
3344
  /**
3326
3345
  * Reference to the parent InternalModel.
3327
3346
  * @type {InternalModel}
@@ -3698,6 +3717,8 @@ export declare const VOLUME = 0.5;
3698
3717
  * Manages all the sounds.
3699
3718
  */
3700
3719
  export declare class SoundManager {
3720
+ /** Select the speaker used by current and future motion audio and speak() calls. */
3721
+ static setOutputDevice(deviceId: string): Promise<void>;
3701
3722
  /**
3702
3723
  * Audio elements playing or pending to play. Finished audios will be removed automatically.
3703
3724
  */
@@ -6915,6 +6936,10 @@ export declare class CubismMotionManager extends MotionManager<CubismMotion, Mot
6915
6936
  expressionManager?: CubismExpressionManager;
6916
6937
  eyeBlinkIds: string[];
6917
6938
  lipSyncIds: string[];
6939
+ /** What each motion's curves write, read once when it is created. */
6940
+ private readonly meta;
6941
+ /** Eases the parameters a released idle animated back to rest; see {@link releaseIdle}. */
6942
+ private release;
6918
6943
  constructor(parent: CubismInternalModel);
6919
6944
  protected init(options?: MotionManagerOptions): void;
6920
6945
  isFinished(): boolean;
@@ -6924,6 +6949,15 @@ export declare class CubismMotionManager extends MotionManager<CubismMotion, Mot
6924
6949
  getMotionFile(definition: Motion): string;
6925
6950
  protected getMotionName(definition: Motion): string;
6926
6951
  protected getSoundFile(definition: Motion): string | undefined;
6952
+ /** Whether the playing motion animates the eyes itself, so the engine's auto-blink must yield to it. */
6953
+ currentMotionBlinks(): boolean;
6954
+ /** The parameters the playing motion writes, for a host driver handing them over to it; null when none plays. */
6955
+ currentMotionParameters(): ReadonlySet<string> | null;
6956
+ private currentMotion;
6957
+ private currentMeta;
6958
+ protected releaseIdle(): void;
6959
+ /** One frame of the ease-to-rest, ahead of the motions so a new idle's fade-in blends from it. */
6960
+ private updateRelease;
6927
6961
  protected updateParameters(model: CubismModel, now: DOMHighResTimeStamp): boolean;
6928
6962
  destroy(): void;
6929
6963
  motionLastFrame(group: string, index: number, { expression }?: {
package/dist/index.js CHANGED
@@ -3567,6 +3567,10 @@ var MotionState = class {
3567
3567
  */
3568
3568
  start(motion, group, index, priority) {
3569
3569
  if (priority === 1) {
3570
+ if (group !== this.reservedIdleGroup || index !== this.reservedIdleIndex) {
3571
+ logger.log(this.tag, "Cannot start idle motion because it is no longer reserved.", this.dump(group, index));
3572
+ return false;
3573
+ }
3570
3574
  this.setReservedIdle(void 0, void 0);
3571
3575
  if (this.currentPriority !== 0) {
3572
3576
  logger.log(this.tag, "Cannot start idle motion because another motion is playing.", this.dump(group, index));
@@ -3663,17 +3667,33 @@ const VOLUME = .5;
3663
3667
  const SOUND_ALIAS_PREFIX = "live2d-sound-";
3664
3668
  let soundId = 0;
3665
3669
  let pixiSoundPromise;
3666
- const configuredLibraries = /* @__PURE__ */ new WeakSet();
3670
+ const configuredLibraries = /* @__PURE__ */ new Set();
3671
+ let outputDeviceId = "";
3672
+ let outputError = null;
3673
+ /** Device changes and first-use routing run in order, so a library never settles on a stale device. */
3674
+ let routing = Promise.resolve();
3675
+ function queueRouting(task) {
3676
+ const run = routing.then(task);
3677
+ routing = run.catch(() => void 0);
3678
+ return run;
3679
+ }
3680
+ async function routeContext(context, deviceId) {
3681
+ const routable = context;
3682
+ if (routable.setSinkId) await routable.setSinkId(deviceId);
3683
+ else if (deviceId) throw new Error("Audio output selection is not supported");
3684
+ }
3667
3685
  function configureSoundLibrary(library) {
3668
- if (!configuredLibraries.has(library)) {
3686
+ return queueRouting(async () => {
3687
+ if (configuredLibraries.has(library)) return library;
3669
3688
  try {
3670
3689
  library.disableAutoPause = true;
3671
3690
  } catch (e) {
3672
3691
  logger.warn(TAG$2, "Failed to disable @pixi/sound auto pause.", e);
3673
3692
  }
3693
+ if (outputDeviceId) await routeContext(library.context.audioContext, outputDeviceId);
3674
3694
  configuredLibraries.add(library);
3675
- }
3676
- return library;
3695
+ return library;
3696
+ });
3677
3697
  }
3678
3698
  function getGlobalSoundLibrary() {
3679
3699
  return typeof PIXI !== "undefined" ? PIXI?.sound : void 0;
@@ -3681,11 +3701,12 @@ function getGlobalSoundLibrary() {
3681
3701
  async function resolveSoundLibrary() {
3682
3702
  const globalLibrary = getGlobalSoundLibrary();
3683
3703
  if (globalLibrary) return configureSoundLibrary(globalLibrary);
3684
- pixiSoundPromise ??= import("@pixi/sound").then(({ sound }) => configureSoundLibrary(sound)).catch((e) => {
3704
+ pixiSoundPromise ??= import("@pixi/sound").then(({ sound }) => sound).catch((e) => {
3685
3705
  logger.warn(TAG$2, "@pixi/sound is not available. Load pixi-sound.js before using motion sounds, speak(), or lip sync.", e);
3686
3706
  return null;
3687
3707
  });
3688
- return pixiSoundPromise;
3708
+ const library = await pixiSoundPromise;
3709
+ return library ? configureSoundLibrary(library) : null;
3689
3710
  }
3690
3711
  function removeSound(library, alias) {
3691
3712
  try {
@@ -3710,6 +3731,25 @@ function getAudioBuffer(audio) {
3710
3731
  * Manages all the sounds.
3711
3732
  */
3712
3733
  var SoundManager = class SoundManager {
3734
+ /** Select the speaker used by current and future motion audio and speak() calls. */
3735
+ static setOutputDevice(deviceId) {
3736
+ return queueRouting(async () => {
3737
+ const changed = [];
3738
+ try {
3739
+ for (const library of configuredLibraries) {
3740
+ const context = library.context.audioContext;
3741
+ await routeContext(context, deviceId);
3742
+ changed.push(context);
3743
+ }
3744
+ outputDeviceId = deviceId;
3745
+ outputError = null;
3746
+ } catch (error) {
3747
+ outputError = error;
3748
+ await Promise.allSettled(changed.map((context) => routeContext(context, outputDeviceId)));
3749
+ throw error;
3750
+ }
3751
+ });
3752
+ }
3713
3753
  /**
3714
3754
  * Audio elements playing or pending to play. Finished audios will be removed automatically.
3715
3755
  */
@@ -3740,6 +3780,7 @@ var SoundManager = class SoundManager {
3740
3780
  let library = null;
3741
3781
  let alias;
3742
3782
  try {
3783
+ if (outputError) throw outputError;
3743
3784
  library = await resolveSoundLibrary();
3744
3785
  if (!library) throw new Error("@pixi/sound is not available");
3745
3786
  alias = `${SOUND_ALIAS_PREFIX}${soundId++}`;
@@ -3768,6 +3809,7 @@ var SoundManager = class SoundManager {
3768
3809
  }
3769
3810
  });
3770
3811
  });
3812
+ if (outputError) throw outputError;
3771
3813
  SoundManager.aliases.set(audio, {
3772
3814
  alias: soundAlias,
3773
3815
  library: soundLibrary
@@ -3787,6 +3829,10 @@ var SoundManager = class SoundManager {
3787
3829
  * @param onFinish - Callback invoked when the playback has finished.
3788
3830
  */
3789
3831
  static play(audio, onFinish) {
3832
+ if (outputError) {
3833
+ SoundManager.dispose(audio);
3834
+ return;
3835
+ }
3790
3836
  audio.play({
3791
3837
  singleInstance: true,
3792
3838
  complete: () => {
@@ -3870,6 +3916,10 @@ let MotionPreloadStrategy = /* @__PURE__ */ function(MotionPreloadStrategy) {
3870
3916
  MotionPreloadStrategy["NONE"] = "NONE";
3871
3917
  return MotionPreloadStrategy;
3872
3918
  }({});
3919
+ function sameIdleMotion(a, b) {
3920
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return a === b;
3921
+ return a.group === b.group && a.index === b.index;
3922
+ }
3873
3923
  /**
3874
3924
  * Handles the motion playback for Live2D models.
3875
3925
  * Responsible for loading, playing, and managing motion states and audio.
@@ -3924,6 +3974,29 @@ var MotionManager = class extends EventEmitter {
3924
3974
  * @type {boolean}
3925
3975
  */
3926
3976
  destroyed = false;
3977
+ /** The idle selection; see {@link setIdleMotion}. */
3978
+ idleMotion = "idle-group";
3979
+ /**
3980
+ * Choose what plays when nothing else does. A running idle gives way at once and a loading one
3981
+ * is dropped; a higher-priority motion in flight hands over to the new selection when it ends.
3982
+ */
3983
+ setIdleMotion(motion) {
3984
+ if (sameIdleMotion(this.idleMotion, motion)) return;
3985
+ this.idleMotion = motion;
3986
+ this.state.setReservedIdle(void 0, void 0);
3987
+ if (this.state.currentPriority === 1) this.releaseIdle();
3988
+ }
3989
+ /** Stop the idle so the next update starts the selection. Cubism eases its parameters home too. */
3990
+ releaseIdle() {
3991
+ this.stopAllMotions();
3992
+ }
3993
+ /** Start the idle selection at IDLE priority; a chosen motion loops regardless of its own flag. */
3994
+ startIdleMotion() {
3995
+ const idle = this.idleMotion;
3996
+ if (idle === null) return Promise.resolve(false);
3997
+ if (idle === "idle-group") return this.startRandomMotion(this.groups.idle, 1);
3998
+ return this.startMotion(idle.group, idle.index, 1, { loop: true });
3999
+ }
3927
4000
  /**
3928
4001
  * Reference to the parent InternalModel.
3929
4002
  * @type {InternalModel}
@@ -4222,7 +4295,7 @@ var MotionManager = class extends EventEmitter {
4222
4295
  }
4223
4296
  if (this.state.shouldOverrideExpression()) this.expressionManager?.restoreExpression();
4224
4297
  this.state.complete();
4225
- if (this.state.shouldRequestIdleMotion()) this.startRandomMotion(this.groups.idle, 1);
4298
+ if (this.idleMotion !== null && this.state.shouldRequestIdleMotion()) this.startIdleMotion();
4226
4299
  }
4227
4300
  return this.updateParameters(model, now);
4228
4301
  }
@@ -14651,6 +14724,10 @@ var CubismMotionManager = class extends MotionManager {
14651
14724
  expressionManager;
14652
14725
  eyeBlinkIds;
14653
14726
  lipSyncIds = ["ParamMouthOpenY"];
14727
+ /** What each motion's curves write, read once when it is created. */
14728
+ meta = /* @__PURE__ */ new WeakMap();
14729
+ /** Eases the parameters a released idle animated back to rest; see {@link releaseIdle}. */
14730
+ release = null;
14654
14731
  constructor(parent) {
14655
14732
  super(parent);
14656
14733
  this.definitions = parent.settings.motions ?? {};
@@ -14680,6 +14757,7 @@ var CubismMotionManager = class extends MotionManager {
14680
14757
  });
14681
14758
  motion._motionData.curves = filtered;
14682
14759
  motion._motionData.curveCount = filtered.length;
14760
+ this.meta.set(motion, motionMeta(motion, this.eyeBlinkIds));
14683
14761
  }
14684
14762
  this.queueManager.stopAllMotions();
14685
14763
  return this.queueManager.startMotion(motion, false);
@@ -14692,6 +14770,7 @@ var CubismMotionManager = class extends MotionManager {
14692
14770
  const motion = CubismMotion.create(data, data.byteLength, void 0, void 0, shouldCheckMotionConsistency);
14693
14771
  const json = new CubismMotionJson(data, data.byteLength);
14694
14772
  motion.setLoop(json.isMotionLoop());
14773
+ this.meta.set(motion, motionMeta(motion, this.eyeBlinkIds));
14695
14774
  const defaultFadingDuration = (group === this.groups.idle ? config.idleMotionFadingDuration : config.motionFadingDuration) / 1e3;
14696
14775
  if (json.getMotionFadeInTime() === void 0) motion.setFadeInTime(definition.FadeInTime > 0 ? definition.FadeInTime : defaultFadingDuration);
14697
14776
  if (json.getMotionFadeOutTime() === void 0) motion.setFadeOutTime(definition.FadeOutTime > 0 ? definition.FadeOutTime : defaultFadingDuration);
@@ -14707,7 +14786,57 @@ var CubismMotionManager = class extends MotionManager {
14707
14786
  getSoundFile(definition) {
14708
14787
  return definition.Sound;
14709
14788
  }
14789
+ /** Whether the playing motion animates the eyes itself, so the engine's auto-blink must yield to it. */
14790
+ currentMotionBlinks() {
14791
+ return this.currentMeta()?.blinks ?? false;
14792
+ }
14793
+ /** The parameters the playing motion writes, for a host driver handing them over to it; null when none plays. */
14794
+ currentMotionParameters() {
14795
+ return this.currentMeta()?.animated ?? null;
14796
+ }
14797
+ currentMotion() {
14798
+ const { currentGroup, currentIndex } = this.state;
14799
+ if (currentGroup === void 0 || currentIndex === void 0) return null;
14800
+ return this.motionGroups[currentGroup]?.[currentIndex] ?? null;
14801
+ }
14802
+ currentMeta() {
14803
+ const motion = this.currentMotion();
14804
+ return motion ? this.meta.get(motion) ?? null : null;
14805
+ }
14806
+ releaseIdle() {
14807
+ const motion = this.currentMotion();
14808
+ super.releaseIdle();
14809
+ if (motion) this.release = {
14810
+ ids: this.meta.get(motion)?.parameterIds ?? [],
14811
+ seconds: motion.getFadeOutTime(),
14812
+ ramp: null
14813
+ };
14814
+ }
14815
+ /** One frame of the ease-to-rest, ahead of the motions so a new idle's fade-in blends from it. */
14816
+ updateRelease(model, now) {
14817
+ const r = this.release;
14818
+ if (!r) return;
14819
+ if (r.ramp === null) {
14820
+ const count = model.getParameterCount();
14821
+ const indices = r.ids.map((id) => model.getParameterIndex(id)).filter((index) => index < count);
14822
+ r.ramp = {
14823
+ indices,
14824
+ from: indices.map((index) => model.getParameterValueByIndex(index)),
14825
+ startedAt: now
14826
+ };
14827
+ }
14828
+ const { indices, from, startedAt } = r.ramp;
14829
+ const t = r.seconds > 0 ? Math.min(1, (now - startedAt) / r.seconds) : 1;
14830
+ const w = CubismMath.getEasingSine(t);
14831
+ for (let i = 0; i < indices.length; i++) {
14832
+ const index = indices[i];
14833
+ const start = from[i];
14834
+ model.setParameterValueByIndex(index, start + (model.getParameterDefaultValue(index) - start) * w);
14835
+ }
14836
+ if (t >= 1) this.release = null;
14837
+ }
14710
14838
  updateParameters(model, now) {
14839
+ this.updateRelease(model, now);
14711
14840
  return this.queueManager.doUpdateMotion(model, now);
14712
14841
  }
14713
14842
  destroy() {
@@ -14728,6 +14857,23 @@ var CubismMotionManager = class extends MotionManager {
14728
14857
  return ids.map((id) => idManager.getId(id));
14729
14858
  }
14730
14859
  };
14860
+ function motionMeta(motion, eyeBlinkIds) {
14861
+ const parameterIds = [];
14862
+ const animated = /* @__PURE__ */ new Set();
14863
+ let blinks = false;
14864
+ for (const curve of motion._motionData.curves) {
14865
+ const id = getCurveId$1(curve);
14866
+ if (curve.type === 1) {
14867
+ parameterIds.push(curve.id);
14868
+ if (id !== void 0) animated.add(id);
14869
+ } else if (curve.type === 0 && id === "EyeBlink") blinks = true;
14870
+ }
14871
+ return {
14872
+ parameterIds,
14873
+ animated,
14874
+ blinks: blinks || eyeBlinkIds.some((id) => animated.has(id))
14875
+ };
14876
+ }
14731
14877
  function getCurveId$1(curve) {
14732
14878
  if (!curve || typeof curve !== "object") return;
14733
14879
  const id = curve.id;
@@ -15280,10 +15426,10 @@ var CubismInternalModel = class extends InternalModel {
15280
15426
  dt /= 1e3;
15281
15427
  now /= 1e3;
15282
15428
  const model = this.coreModel;
15283
- const motionUpdated = this.updateMotions(model, now);
15429
+ this.updateMotions(model, now);
15284
15430
  model.saveParameters();
15285
15431
  this.motionManager.expressionManager?.update(model, now);
15286
- if (!motionUpdated) this.eyeBlink?.updateParameters?.(model, dt);
15432
+ if (!this.motionManager.currentMotionBlinks()) this.eyeBlink?.updateParameters?.(model, dt);
15287
15433
  this.updateFocus();
15288
15434
  this.updateNaturalMovements(dt * 1e3, now * 1e3);
15289
15435
  if (this.lipSync && this.motionManager.currentAudio) {