@doki-land/live2d 0.0.11 → 0.0.13

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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { EventEmitter as EventEmitter2 } from "@doki-land/live2d-core";
2
+ import { DEFAULT_ACTOR_TRANSFORM as DEFAULT_ACTOR_TRANSFORM2, EventEmitter as EventEmitter2 } from "@doki-land/live2d-core";
3
3
  import {
4
4
  DEFAULT_NPM_CDN,
5
5
  resolveModelSourceUrl as resolveModelSourceUrl2,
@@ -7,10 +7,10 @@ import {
7
7
  } from "@doki-land/live2d-loader";
8
8
  import {
9
9
  createCanvas2DRenderer,
10
- createMoc2Backend as createMoc2Backend2,
11
- createMoc3Backend as createMoc3Backend2,
10
+ createMoc2Backend as createMoc2Backend3,
11
+ createMoc3Backend as createMoc3Backend3,
12
12
  createQuadProgram,
13
- createRenderer as createRenderer2,
13
+ createRenderer as createRenderer3,
14
14
  createWebGl2Renderer,
15
15
  createWebGpuRenderer,
16
16
  decodeMoc3,
@@ -21,19 +21,51 @@ import {
21
21
  } from "@doki-land/live2d-renderer";
22
22
 
23
23
  // src/create-live2d.ts
24
- import { EventEmitter, modelSourceUrl } from "@doki-land/live2d-core";
24
+ import {
25
+ createMoc2Backend as createMoc2Backend2,
26
+ createMoc3Backend as createMoc3Backend2,
27
+ createRenderer as createRenderer2
28
+ } from "@doki-land/live2d-renderer";
29
+
30
+ // src/focus.ts
31
+ function focusParameterUpdates(parameters, dragX, dragY) {
32
+ const byId = new Map(parameters.map((p) => [p.id, p]));
33
+ const x = clampUnit(dragX);
34
+ const y = clampUnit(dragY);
35
+ const out = [];
36
+ const set = (id, normalized) => {
37
+ const binding = byId.get(id);
38
+ if (!binding) return;
39
+ out.push({ id, value: valueFromNormalized(binding, normalized) });
40
+ };
41
+ set("PARAM_ANGLE_X", x);
42
+ set("PARAM_ANGLE_Y", y);
43
+ set("PARAM_ANGLE_Z", clampUnit(x * y * -1));
44
+ set("PARAM_BODY_ANGLE_X", x);
45
+ set("PARAM_BODY_ANGLE_Y", y);
46
+ set("PARAM_EYE_BALL_X", x);
47
+ set("PARAM_EYE_BALL_Y", y);
48
+ return out;
49
+ }
50
+ function clampUnit(n) {
51
+ if (n > 1) return 1;
52
+ if (n < -1) return -1;
53
+ return n;
54
+ }
55
+ function valueFromNormalized(binding, normalized) {
56
+ const n = clampUnit(normalized);
57
+ return n >= 0 ? binding.defaultValue + (binding.max - binding.defaultValue) * n : binding.defaultValue + (binding.defaultValue - binding.min) * n;
58
+ }
59
+
60
+ // src/stage/actor-model-slot.ts
61
+ import { modelSourceUrl } from "@doki-land/live2d-core";
25
62
  import {
26
63
  createUrlAssetResolver,
27
64
  fetchModelJson,
28
65
  normalizeModelSettings,
29
66
  resolveModelSourceUrl
30
67
  } from "@doki-land/live2d-loader";
31
- import {
32
- createMoc2Backend,
33
- createMoc3Backend,
34
- createRenderer,
35
- selectModelBackend
36
- } from "@doki-land/live2d-renderer";
68
+ import { selectModelBackend } from "@doki-land/live2d-renderer";
37
69
 
38
70
  // src/load-textures.ts
39
71
  function guessMime(path) {
@@ -578,116 +610,577 @@ function optionalNum(v) {
578
610
  return typeof v === "number" && Number.isFinite(v) ? v : void 0;
579
611
  }
580
612
 
581
- // src/create-live2d.ts
613
+ // src/stage/actor-model-slot.ts
582
614
  function lerp(a, b, t) {
583
615
  return a + (b - a) * Math.min(1, Math.max(0, t));
584
616
  }
585
- function nowMs() {
586
- return typeof performance !== "undefined" ? performance.now() : Date.now();
587
- }
588
- function createLive2D(options = {}) {
589
- const backends = options.backends ?? [
590
- createMoc2Backend(),
591
- createMoc3Backend()
592
- ];
593
- const renderer = options.renderer ?? createRenderer({ prefer: options.prefer });
594
- const events = new EventEmitter();
595
- let canvas = null;
596
- let model = null;
597
- let activeBackend = null;
598
- let drawPass = null;
599
- let loadedTextures = [];
600
- let initPromise = null;
601
- let phase = "idle";
602
- let lastError = null;
603
- let generation = 0;
604
- let loadGeneration = 0;
605
- let fpsSmooth = 0;
606
- let activeResolver = null;
607
- const motionCache = /* @__PURE__ */ new Map();
608
- const motionPlayer = new MotionPlayer({
609
- onStart: ({ group, index, slot }) => events.emit("motion:start", { group, index, slot }),
610
- onFinish: ({ group, index, slot }) => events.emit("motion:finish", { group, index, slot })
611
- });
612
- const applyMotionSamples = (samples) => {
613
- if (!model || !activeBackend) return;
617
+ var ActorModelSlot = class {
618
+ #backends;
619
+ #renderer;
620
+ #onProgress;
621
+ #motionPlayer;
622
+ #motionCache = /* @__PURE__ */ new Map();
623
+ #drawPass = null;
624
+ #model = null;
625
+ #backend = null;
626
+ #textures = [];
627
+ #resolver = null;
628
+ #loadGeneration = 0;
629
+ constructor(options) {
630
+ this.#backends = options.backends;
631
+ this.#renderer = options.renderer;
632
+ this.#onProgress = options.onProgress;
633
+ this.#motionPlayer = new MotionPlayer({
634
+ onStart: options.onMotionStart,
635
+ onFinish: options.onMotionFinish
636
+ });
637
+ }
638
+ get model() {
639
+ return this.#model;
640
+ }
641
+ get drawPass() {
642
+ return this.#drawPass;
643
+ }
644
+ ensureDrawPass() {
645
+ if (!this.#drawPass) {
646
+ this.#drawPass = this.#renderer.createModelDrawPass();
647
+ }
648
+ return this.#drawPass;
649
+ }
650
+ #report(payload) {
651
+ this.#onProgress?.(payload);
652
+ }
653
+ #clearTextures() {
654
+ if (this.#textures.length > 0) {
655
+ releaseTextureData(this.#textures);
656
+ this.#textures = [];
657
+ }
658
+ this.#drawPass?.setTextures([]);
659
+ }
660
+ #applyMotionSamples(samples) {
661
+ if (!this.#model || !this.#backend) return;
614
662
  for (const s of samples) {
615
663
  if (s.weight <= 0) continue;
616
664
  if (s.target === "PartOpacity") {
617
- if (!activeBackend.setPartOpacity) continue;
665
+ if (!this.#backend.setPartOpacity) continue;
618
666
  if (s.weight >= 1) {
619
- activeBackend.setPartOpacity(model, s.id, s.value);
667
+ this.#backend.setPartOpacity(this.#model, s.id, s.value);
620
668
  } else {
621
669
  const cur2 = 1;
622
- activeBackend.setPartOpacity(
623
- model,
670
+ this.#backend.setPartOpacity(
671
+ this.#model,
624
672
  s.id,
625
673
  cur2 + (s.value - cur2) * s.weight
626
674
  );
627
675
  }
628
676
  continue;
629
677
  }
630
- if (s.target !== "Parameter" || !activeBackend.setParameter)
678
+ if (s.target !== "Parameter" || !this.#backend.setParameter)
631
679
  continue;
632
680
  if (s.weight >= 1) {
633
- activeBackend.setParameter(model, s.id, s.value);
681
+ this.#backend.setParameter(this.#model, s.id, s.value);
634
682
  continue;
635
683
  }
636
- const cur = activeBackend.listParameters?.(model).find((p) => p.id === s.id)?.value ?? s.value;
637
- activeBackend.setParameter(
638
- model,
684
+ const cur = this.#backend.listParameters?.(this.#model).find((p) => p.id === s.id)?.value ?? s.value;
685
+ this.#backend.setParameter(
686
+ this.#model,
639
687
  s.id,
640
688
  cur + (s.value - cur) * s.weight
641
689
  );
642
690
  }
643
- };
644
- const clearTextures = () => {
645
- if (loadedTextures.length > 0) {
646
- releaseTextureData(loadedTextures);
647
- loadedTextures = [];
691
+ }
692
+ async load(source, resolver) {
693
+ const gen = ++this.#loadGeneration;
694
+ this.#report({
695
+ stage: "mounting",
696
+ progress: 0.01,
697
+ detail: "prepare draw pass"
698
+ });
699
+ const drawPass = this.ensureDrawPass();
700
+ this.#report({
701
+ stage: "resolve",
702
+ progress: 0.02,
703
+ detail: "resolve source"
704
+ });
705
+ let json;
706
+ let baseUrl;
707
+ let settingsUrl;
708
+ if (typeof source === "object" && source.kind === "json") {
709
+ json = source.json;
710
+ baseUrl = source.baseUrl;
711
+ settingsUrl = source.baseUrl;
712
+ this.#report({
713
+ stage: "settings",
714
+ progress: 0.2,
715
+ detail: "inline settings"
716
+ });
717
+ } else {
718
+ const raw = typeof source === "string" ? source : source.kind === "npm" ? modelSourceUrl(source) : source.url;
719
+ const cdnBase = typeof source === "object" && source.kind === "npm" ? source.cdnBase : void 0;
720
+ const fetchUrl = resolveModelSourceUrl(raw, {
721
+ npmCdnBase: cdnBase
722
+ });
723
+ this.#report({
724
+ stage: "settings",
725
+ progress: 0.05,
726
+ detail: fetchUrl
727
+ });
728
+ json = await fetchModelJson(fetchUrl, (u) => {
729
+ const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
730
+ this.#report({
731
+ stage: "settings",
732
+ progress: lerp(0.05, 0.22, ratio),
733
+ detail: fetchUrl,
734
+ bytesLoaded: u.bytesLoaded,
735
+ bytesTotal: u.bytesTotal
736
+ });
737
+ });
738
+ baseUrl = fetchUrl;
739
+ settingsUrl = fetchUrl;
740
+ }
741
+ if (gen !== this.#loadGeneration) {
742
+ throw new Error("@doki-land/live2d: load cancelled");
648
743
  }
649
- drawPass?.setTextures([]);
744
+ const settings = normalizeModelSettings(json, settingsUrl);
745
+ this.#report({
746
+ stage: "moc",
747
+ progress: 0.25,
748
+ detail: settings.moc
749
+ });
750
+ const assetResolver = resolver ?? createUrlAssetResolver(baseUrl, {
751
+ onBytesProgress: (key, u) => {
752
+ const isMoc = key === settings.moc;
753
+ const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
754
+ if (isMoc) {
755
+ this.#report({
756
+ stage: "moc",
757
+ progress: lerp(0.25, 0.8, ratio),
758
+ detail: key,
759
+ bytesLoaded: u.bytesLoaded,
760
+ bytesTotal: u.bytesTotal
761
+ });
762
+ } else {
763
+ this.#report({
764
+ stage: "textures",
765
+ progress: lerp(0.8, 0.9, ratio),
766
+ detail: key,
767
+ bytesLoaded: u.bytesLoaded,
768
+ bytesTotal: u.bytesTotal
769
+ });
770
+ }
771
+ }
772
+ });
773
+ this.#resolver = assetResolver;
774
+ this.#motionPlayer.clear();
775
+ this.#motionCache.clear();
776
+ const backend = selectModelBackend([...this.#backends], json);
777
+ this.#report({
778
+ stage: "decode",
779
+ progress: 0.85,
780
+ detail: `decode ${settings.format}`
781
+ });
782
+ const next = await backend.createModel(settings, {
783
+ renderer: this.#renderer,
784
+ resolver: assetResolver
785
+ });
786
+ if (gen !== this.#loadGeneration) {
787
+ backend.destroyModel(next);
788
+ throw new Error("@doki-land/live2d: load cancelled");
789
+ }
790
+ this.#clearTextures();
791
+ if (settings.textures.length > 0) {
792
+ this.#report({
793
+ stage: "textures",
794
+ progress: 0.88,
795
+ detail: `${settings.textures.length} textures`
796
+ });
797
+ const textures = await loadTextureData(
798
+ assetResolver,
799
+ settings.textures,
800
+ {
801
+ onProgress: (u) => {
802
+ const ratio = u.total > 0 ? (u.index + 1) / u.total : 1;
803
+ this.#report({
804
+ stage: "textures",
805
+ progress: lerp(0.88, 0.96, ratio),
806
+ detail: u.key,
807
+ bytesLoaded: u.bytesLoaded,
808
+ bytesTotal: u.bytesTotal
809
+ });
810
+ }
811
+ }
812
+ );
813
+ if (gen !== this.#loadGeneration) {
814
+ releaseTextureData(textures);
815
+ backend.destroyModel(next);
816
+ throw new Error("@doki-land/live2d: load cancelled");
817
+ }
818
+ this.#textures = textures;
819
+ drawPass.setTextures(textures);
820
+ }
821
+ if (this.#model && this.#backend) {
822
+ this.#backend.destroyModel(this.#model);
823
+ }
824
+ this.#model = next;
825
+ this.#backend = backend;
826
+ this.#report({
827
+ stage: "ready",
828
+ progress: 1,
829
+ detail: next.id
830
+ });
831
+ return next;
832
+ }
833
+ setParameter(id, value) {
834
+ if (!this.#model || !this.#backend?.setParameter) return;
835
+ this.#backend.setParameter(this.#model, id, value);
836
+ }
837
+ listParameters() {
838
+ if (!this.#model || !this.#backend?.listParameters) return [];
839
+ return this.#backend.listParameters(this.#model);
840
+ }
841
+ listMotionGroups() {
842
+ return this.#model?.settings.motionGroups ?? {};
843
+ }
844
+ async playMotion(group, index = 0, options = {}) {
845
+ if (!this.#model || !this.#resolver) return false;
846
+ const list = this.#model.settings.motionGroups[group];
847
+ const def = list?.[index];
848
+ if (!def) return false;
849
+ let clip = this.#motionCache.get(def.file);
850
+ if (!clip) {
851
+ const json = await this.#resolver.fetchJson(def.file);
852
+ clip = parseMotion3(json);
853
+ this.#motionCache.set(def.file, clip);
854
+ }
855
+ const fadeInTime = options.fadeInTime ?? def.fadeInTime ?? clip.fadeInTime;
856
+ const fadeOutTime = options.fadeOutTime ?? def.fadeOutTime ?? clip.fadeOutTime;
857
+ return this.#motionPlayer.start(group, index, clip, {
858
+ priority: options.priority ?? MotionPriority.normal,
859
+ slot: options.slot,
860
+ queue: options.queue,
861
+ loop: options.loop,
862
+ fadeInTime,
863
+ fadeOutTime
864
+ });
865
+ }
866
+ stopMotion(opts) {
867
+ this.#motionPlayer.stop(opts?.fade !== false, opts?.slot);
868
+ }
869
+ listPlayingMotions() {
870
+ return this.#motionPlayer.listPlaying();
871
+ }
872
+ update(deltaTimeSeconds) {
873
+ if (!this.#model || !this.#backend || !this.#drawPass) return null;
874
+ this.#applyMotionSamples(this.#motionPlayer.update(deltaTimeSeconds));
875
+ this.#backend.updateModel(this.#model, deltaTimeSeconds);
876
+ return this.#backend.getDrawables(this.#model);
877
+ }
878
+ hitTestModelCoords(modelX, modelY) {
879
+ if (!this.#model || !this.#backend) return null;
880
+ const drawables = this.#backend.getDrawables(this.#model);
881
+ for (let n = drawables.length - 1; n >= 0; n -= 1) {
882
+ const d = drawables[n];
883
+ if (!d.visible || d.opacity <= 0) continue;
884
+ const p = d.vertexPositions;
885
+ const idx = d.indices;
886
+ for (let i = 0; i + 2 < idx.length; i += 3) {
887
+ const a = idx[i] * 2, b = idx[i + 1] * 2, c = idx[i + 2] * 2;
888
+ const ax = p[a], ay = p[a + 1];
889
+ const bx = p[b], by = p[b + 1];
890
+ const cx = p[c], cy = p[c + 1];
891
+ const s = (ax - cx) * (modelY - cy) - (ay - cy) * (modelX - cx);
892
+ const s1 = (bx - ax) * (modelY - ay) - (by - ay) * (modelX - ax);
893
+ const s2 = (cx - bx) * (modelY - by) - (cy - by) * (modelX - bx);
894
+ if (s >= 0 && s1 >= 0 && s2 >= 0 || s <= 0 && s1 <= 0 && s2 <= 0) {
895
+ const hitArea = this.#model.settings.hitAreas.find(
896
+ (h) => h.id === `D_${d.index}` || h.id === `${d.index}`
897
+ );
898
+ return hitArea?.name ?? `drawable:${d.index}`;
899
+ }
900
+ }
901
+ }
902
+ return null;
903
+ }
904
+ destroy() {
905
+ this.#loadGeneration += 1;
906
+ this.#motionPlayer.clear();
907
+ this.#motionCache.clear();
908
+ this.#resolver = null;
909
+ if (this.#model && this.#backend) {
910
+ this.#backend.destroyModel(this.#model);
911
+ }
912
+ this.#model = null;
913
+ this.#backend = null;
914
+ this.#clearTextures();
915
+ this.#drawPass?.destroy();
916
+ this.#drawPass = null;
917
+ }
918
+ };
919
+
920
+ // src/stage/transform.ts
921
+ import { DEFAULT_ACTOR_TRANSFORM } from "@doki-land/live2d-core";
922
+ function resolveActorTransform(patch) {
923
+ const base = DEFAULT_ACTOR_TRANSFORM;
924
+ const scale = patch?.scale ?? base.scale ?? 1;
925
+ return {
926
+ x: patch?.x ?? base.x,
927
+ y: patch?.y ?? base.y,
928
+ scale,
929
+ scaleX: patch?.scaleX ?? patch?.scale ?? scale,
930
+ scaleY: patch?.scaleY ?? patch?.scale ?? scale,
931
+ rotation: patch?.rotation ?? base.rotation ?? 0,
932
+ anchorX: patch?.anchorX ?? base.anchorX ?? 0.5,
933
+ anchorY: patch?.anchorY ?? base.anchorY ?? 1
934
+ };
935
+ }
936
+ function modelNdcToStage(modelX, modelY, transform) {
937
+ const scaleX = transform.scaleX ?? transform.scale ?? 1;
938
+ const scaleY = transform.scaleY ?? transform.scale ?? 1;
939
+ const anchorX = transform.anchorX ?? 0.5;
940
+ const anchorY = transform.anchorY ?? 1;
941
+ const rotation = transform.rotation ?? 0;
942
+ const anchorModelX = -1 + anchorX * 2;
943
+ const anchorModelY = 1 - anchorY * 2;
944
+ let localX = (modelX - anchorModelX) * scaleX * 0.5;
945
+ let localY = (anchorModelY - modelY) * scaleY * 0.5;
946
+ if (rotation !== 0) {
947
+ const c = Math.cos(rotation);
948
+ const s = Math.sin(rotation);
949
+ const rx = localX * c - localY * s;
950
+ const ry = localX * s + localY * c;
951
+ localX = rx;
952
+ localY = ry;
953
+ }
954
+ return {
955
+ stageX: transform.x + localX,
956
+ stageY: transform.y + localY
957
+ };
958
+ }
959
+ function stageToModelNdc(stageX, stageY, transform) {
960
+ const scaleX = transform.scaleX ?? transform.scale ?? 1;
961
+ const scaleY = transform.scaleY ?? transform.scale ?? 1;
962
+ const anchorX = transform.anchorX ?? 0.5;
963
+ const anchorY = transform.anchorY ?? 1;
964
+ const anchorModelX = -1 + anchorX * 2;
965
+ const anchorModelY = 1 - anchorY * 2;
966
+ const localX = (stageX - transform.x) / (scaleX * 0.5);
967
+ const localY = (stageY - transform.y) / (scaleY * 0.5);
968
+ return {
969
+ modelX: localX + anchorModelX,
970
+ modelY: anchorModelY - localY
971
+ };
972
+ }
973
+ function clientToStage(clientX, clientY, canvas) {
974
+ const rect = canvas.getBoundingClientRect();
975
+ const x = rect.width > 0 ? (clientX - rect.left) / rect.width : 0;
976
+ const y = rect.height > 0 ? (clientY - rect.top) / rect.height : 0;
977
+ return {
978
+ stageX: Math.min(1, Math.max(0, x)),
979
+ stageY: Math.min(1, Math.max(0, y))
650
980
  };
981
+ }
982
+ function stageFocusDrag(stageX, stageY, transform) {
983
+ const dx = stageX - transform.x;
984
+ const dy = transform.y - stageY;
985
+ const dragX = Math.min(1, Math.max(-1, dx * 2));
986
+ const dragY = Math.min(1, Math.max(-1, dy * 2));
987
+ return { dragX, dragY };
988
+ }
989
+ function layerOrderIndex(layer, definedLayers) {
990
+ const idx = definedLayers.indexOf(layer);
991
+ return idx >= 0 ? idx : definedLayers.length;
992
+ }
993
+ function compareActorsForDraw(a, b, definedLayers) {
994
+ const la = layerOrderIndex(a.layer, definedLayers);
995
+ const lb = layerOrderIndex(b.layer, definedLayers);
996
+ return la - lb || a.order - b.order || a.creationIndex - b.creationIndex;
997
+ }
998
+ function compareActorsForHit(a, b, definedLayers) {
999
+ return compareActorsForDraw(b, a, definedLayers);
1000
+ }
1001
+ function transformDrawablesForStage(drawables, transform, opacity) {
1002
+ const alpha = Math.min(1, Math.max(0, opacity));
1003
+ return drawables.map((d) => {
1004
+ if (!d.visible || alpha <= 0) return { ...d, visible: false };
1005
+ const pos = new Float32Array(d.vertexPositions.length);
1006
+ for (let i = 0; i < pos.length; i += 2) {
1007
+ const { stageX, stageY } = modelNdcToStage(
1008
+ d.vertexPositions[i],
1009
+ d.vertexPositions[i + 1],
1010
+ transform
1011
+ );
1012
+ pos[i] = stageX * 2 - 1;
1013
+ pos[i + 1] = 1 - stageY * 2;
1014
+ }
1015
+ return {
1016
+ ...d,
1017
+ vertexPositions: pos,
1018
+ opacity: d.opacity * alpha
1019
+ };
1020
+ });
1021
+ }
1022
+
1023
+ // src/stage/actor.ts
1024
+ var nextActorId = 0;
1025
+ var Live2dActorImpl = class {
1026
+ id;
1027
+ creationIndex;
1028
+ #transform;
1029
+ #visible = true;
1030
+ #opacity = 1;
1031
+ #layer = "characters";
1032
+ #order = 0;
1033
+ #destroyed = false;
1034
+ #lastDrawables = null;
1035
+ #slot;
1036
+ constructor(options, shared) {
1037
+ this.id = options?.id ?? shared.id;
1038
+ this.creationIndex = shared.creationIndex;
1039
+ this.#transform = resolveActorTransform(options?.transform);
1040
+ this.#visible = options?.visible ?? true;
1041
+ this.#opacity = options?.opacity ?? 1;
1042
+ this.#layer = options?.layer ?? "characters";
1043
+ this.#order = options?.order ?? 0;
1044
+ this.#slot = new ActorModelSlot({
1045
+ backends: shared.backends,
1046
+ renderer: shared.renderer
1047
+ });
1048
+ }
1049
+ get model() {
1050
+ return this.#slot.model;
1051
+ }
1052
+ get visible() {
1053
+ return this.#visible;
1054
+ }
1055
+ set visible(value) {
1056
+ this.#visible = value;
1057
+ }
1058
+ get opacity() {
1059
+ return this.#opacity;
1060
+ }
1061
+ set opacity(value) {
1062
+ this.#opacity = Math.min(1, Math.max(0, value));
1063
+ }
1064
+ get layer() {
1065
+ return this.#layer;
1066
+ }
1067
+ set layer(value) {
1068
+ this.#layer = value;
1069
+ }
1070
+ get order() {
1071
+ return this.#order;
1072
+ }
1073
+ set order(value) {
1074
+ this.#order = value;
1075
+ }
1076
+ getTransform() {
1077
+ return { ...this.#transform };
1078
+ }
1079
+ setTransform(patch) {
1080
+ this.#transform = resolveActorTransform({
1081
+ ...this.#transform,
1082
+ ...patch
1083
+ });
1084
+ }
1085
+ async load(source, resolver) {
1086
+ if (this.#destroyed) {
1087
+ throw new Error("@doki-land/live2d: actor destroyed");
1088
+ }
1089
+ return await this.#slot.load(source, resolver);
1090
+ }
1091
+ setParameter(id, value) {
1092
+ this.#slot.setParameter(id, value);
1093
+ }
1094
+ lookAt(stageX, stageY) {
1095
+ const { dragX, dragY } = stageFocusDrag(
1096
+ stageX,
1097
+ stageY,
1098
+ this.#transform
1099
+ );
1100
+ for (const u of focusParameterUpdates(
1101
+ this.#slot.listParameters(),
1102
+ dragX,
1103
+ dragY
1104
+ )) {
1105
+ this.#slot.setParameter(u.id, u.value);
1106
+ }
1107
+ }
1108
+ /** Internal: evaluate motion/physics and cache drawables for render. */
1109
+ update(deltaTimeSeconds) {
1110
+ if (!this.#visible || this.#opacity <= 0) {
1111
+ this.#lastDrawables = null;
1112
+ return null;
1113
+ }
1114
+ this.#lastDrawables = this.#slot.update(deltaTimeSeconds);
1115
+ return this.#lastDrawables;
1116
+ }
1117
+ get lastDrawables() {
1118
+ return this.#lastDrawables;
1119
+ }
1120
+ get slot() {
1121
+ return this.#slot;
1122
+ }
1123
+ hitTestStage(stageX, stageY) {
1124
+ if (!this.#visible || this.#opacity <= 0 || !this.#slot.model)
1125
+ return null;
1126
+ const { modelX, modelY } = stageToModelNdc(
1127
+ stageX,
1128
+ stageY,
1129
+ this.#transform
1130
+ );
1131
+ const area = this.#slot.hitTestModelCoords(modelX, modelY);
1132
+ if (!area) return null;
1133
+ const drawableMatch = /^drawable:(\d+)$/.exec(area);
1134
+ return {
1135
+ actorId: this.id,
1136
+ area,
1137
+ drawableIndex: drawableMatch ? Number(drawableMatch[1]) : -1,
1138
+ stageX,
1139
+ stageY,
1140
+ localX: modelX,
1141
+ localY: modelY
1142
+ };
1143
+ }
1144
+ destroy() {
1145
+ if (this.#destroyed) return;
1146
+ this.#destroyed = true;
1147
+ this.#slot.destroy();
1148
+ }
1149
+ };
1150
+ function allocateActorId(prefix = "actor") {
1151
+ nextActorId += 1;
1152
+ return `${prefix}-${nextActorId}`;
1153
+ }
1154
+
1155
+ // src/stage/single-facade.ts
1156
+ import { EventEmitter } from "@doki-land/live2d-core";
1157
+ function nowMs() {
1158
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1159
+ }
1160
+ function createSingleActorFacade(stage, actor, backends) {
1161
+ const events = new EventEmitter();
1162
+ let canvas = null;
1163
+ let phase = "idle";
1164
+ let lastError = null;
1165
+ let generation = 0;
1166
+ let fpsSmooth = 0;
651
1167
  const setPhase = (next) => {
652
1168
  phase = next;
653
1169
  events.emit("phase", { phase, generation });
654
1170
  };
655
- const report = (payload) => {
656
- events.emit("progress", payload);
657
- };
658
1171
  const state = () => ({
659
1172
  phase,
660
1173
  lastError,
661
1174
  generation
662
1175
  });
663
- const ensureInitialized = async () => {
664
- if (!canvas) {
665
- throw new Error(
666
- "@doki-land/live2d: call mount(canvas) before loadModel"
667
- );
668
- }
669
- if (!initPromise) {
670
- setPhase("mounting");
671
- const gen = generation;
672
- initPromise = renderer.initialize(canvas).then(() => {
673
- if (gen !== generation) return;
674
- drawPass = renderer.createModelDrawPass();
675
- setPhase("ready");
676
- }).catch((err) => {
677
- lastError = err;
678
- setPhase("error");
679
- events.emit("error", { error: err });
680
- throw err;
681
- });
682
- }
683
- await initPromise;
684
- };
685
1176
  const runtime = {
686
1177
  events,
687
1178
  backends,
688
- renderer,
1179
+ renderer: stage.renderer,
1180
+ stage,
1181
+ actor,
689
1182
  get model() {
690
- return model;
1183
+ return actor.model;
691
1184
  },
692
1185
  get state() {
693
1186
  return state();
@@ -695,159 +1188,22 @@ function createLive2D(options = {}) {
695
1188
  mount(target) {
696
1189
  generation += 1;
697
1190
  canvas = target;
698
- initPromise = null;
699
- clearTextures();
700
- drawPass?.destroy();
701
- drawPass = null;
702
- setPhase("idle");
703
- void ensureInitialized();
1191
+ setPhase("mounting");
1192
+ void stage.mount(target).then(
1193
+ () => setPhase(actor.model ? "live" : "ready"),
1194
+ (err) => {
1195
+ lastError = err;
1196
+ setPhase("error");
1197
+ events.emit("error", { error: err });
1198
+ }
1199
+ );
704
1200
  },
705
1201
  async loadModel(source, resolver) {
706
- const gen = ++loadGeneration;
707
1202
  setPhase("loading");
708
- report({
709
- stage: "mounting",
710
- progress: 0.01,
711
- detail: "initialize renderer"
712
- });
713
- await ensureInitialized();
714
- if (gen !== loadGeneration) {
715
- throw new Error("@doki-land/live2d: load cancelled");
716
- }
717
- report({
718
- stage: "resolve",
719
- progress: 0.02,
720
- detail: "resolve source"
721
- });
722
1203
  try {
723
- let json;
724
- let baseUrl;
725
- let settingsUrl;
726
- if (typeof source === "object" && source.kind === "json") {
727
- json = source.json;
728
- baseUrl = source.baseUrl;
729
- settingsUrl = source.baseUrl;
730
- report({
731
- stage: "settings",
732
- progress: 0.2,
733
- detail: "inline settings"
734
- });
735
- } else {
736
- const raw = typeof source === "string" ? source : source.kind === "npm" ? modelSourceUrl(source) : source.url;
737
- const cdnBase = typeof source === "object" && source.kind === "npm" ? source.cdnBase : void 0;
738
- const fetchUrl = resolveModelSourceUrl(raw, {
739
- npmCdnBase: cdnBase
740
- });
741
- report({
742
- stage: "settings",
743
- progress: 0.05,
744
- detail: fetchUrl
745
- });
746
- json = await fetchModelJson(fetchUrl, (u) => {
747
- const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
748
- report({
749
- stage: "settings",
750
- progress: lerp(0.05, 0.22, ratio),
751
- detail: fetchUrl,
752
- bytesLoaded: u.bytesLoaded,
753
- bytesTotal: u.bytesTotal
754
- });
755
- });
756
- baseUrl = fetchUrl;
757
- settingsUrl = fetchUrl;
758
- }
759
- if (gen !== loadGeneration) {
760
- throw new Error("@doki-land/live2d: load cancelled");
761
- }
762
- const settings = normalizeModelSettings(json, settingsUrl);
763
- report({
764
- stage: "moc",
765
- progress: 0.25,
766
- detail: settings.moc
767
- });
768
- const assetResolver = resolver ?? createUrlAssetResolver(baseUrl, {
769
- onBytesProgress: (key, u) => {
770
- const isMoc = key === settings.moc;
771
- const ratio = u.bytesTotal && u.bytesTotal > 0 ? u.bytesLoaded / u.bytesTotal : 0;
772
- if (isMoc) {
773
- report({
774
- stage: "moc",
775
- progress: lerp(0.25, 0.8, ratio),
776
- detail: key,
777
- bytesLoaded: u.bytesLoaded,
778
- bytesTotal: u.bytesTotal
779
- });
780
- } else {
781
- report({
782
- stage: "textures",
783
- progress: lerp(0.8, 0.9, ratio),
784
- detail: key,
785
- bytesLoaded: u.bytesLoaded,
786
- bytesTotal: u.bytesTotal
787
- });
788
- }
789
- }
790
- });
791
- activeResolver = assetResolver;
792
- motionPlayer.clear();
793
- motionCache.clear();
794
- const backend = selectModelBackend(backends, json);
795
- report({
796
- stage: "decode",
797
- progress: 0.85,
798
- detail: `decode ${settings.format}`
799
- });
800
- const next = await backend.createModel(settings, {
801
- renderer,
802
- resolver: assetResolver
803
- });
804
- if (gen !== loadGeneration) {
805
- backend.destroyModel(next);
806
- throw new Error("@doki-land/live2d: load cancelled");
807
- }
808
- clearTextures();
809
- if (settings.textures.length > 0 && drawPass) {
810
- report({
811
- stage: "textures",
812
- progress: 0.88,
813
- detail: `${settings.textures.length} textures`
814
- });
815
- const textures = await loadTextureData(
816
- assetResolver,
817
- settings.textures,
818
- {
819
- onProgress: (u) => {
820
- const ratio = u.total > 0 ? (u.index + 1) / u.total : 1;
821
- report({
822
- stage: "textures",
823
- progress: lerp(0.88, 0.96, ratio),
824
- detail: u.key,
825
- bytesLoaded: u.bytesLoaded,
826
- bytesTotal: u.bytesTotal
827
- });
828
- }
829
- }
830
- );
831
- if (gen !== loadGeneration) {
832
- releaseTextureData(textures);
833
- backend.destroyModel(next);
834
- throw new Error("@doki-land/live2d: load cancelled");
835
- }
836
- loadedTextures = textures;
837
- drawPass.setTextures(textures);
838
- }
839
- if (model && activeBackend) {
840
- activeBackend.destroyModel(model);
841
- }
842
- model = next;
843
- activeBackend = backend;
1204
+ const model = await actor.load(source, resolver);
844
1205
  lastError = null;
845
1206
  setPhase("live");
846
- report({
847
- stage: "ready",
848
- progress: 1,
849
- detail: model.id
850
- });
851
1207
  events.emit("ready", { modelId: model.id });
852
1208
  return model;
853
1209
  } catch (err) {
@@ -858,70 +1214,32 @@ function createLive2D(options = {}) {
858
1214
  }
859
1215
  },
860
1216
  captureFrame() {
861
- if (!model || !activeBackend?.captureFrame) return null;
862
- return activeBackend.captureFrame(model);
1217
+ return null;
863
1218
  },
864
1219
  setParameter(id, value) {
865
- if (!model || !activeBackend?.setParameter) return;
866
- activeBackend.setParameter(model, id, value);
1220
+ actor.setParameter(id, value);
867
1221
  },
868
1222
  hitTest(x, y) {
869
- if (!model || !activeBackend) return null;
870
- const drawables = activeBackend.getDrawables(model);
871
- for (let n = drawables.length - 1; n >= 0; n -= 1) {
872
- const d = drawables[n];
873
- if (!d.visible || d.opacity <= 0) continue;
874
- const p = d.vertexPositions;
875
- const idx = d.indices;
876
- for (let i = 0; i + 2 < idx.length; i += 3) {
877
- const a = idx[i] * 2, b = idx[i + 1] * 2, c = idx[i + 2] * 2;
878
- const ax = p[a], ay = p[a + 1];
879
- const bx = p[b], by = p[b + 1];
880
- const cx = p[c], cy = p[c + 1];
881
- const s = (ax - cx) * (y - cy) - (ay - cy) * (x - cx);
882
- const s1 = (bx - ax) * (y - ay) - (by - ay) * (x - ax);
883
- const s2 = (cx - bx) * (y - by) - (cy - by) * (x - bx);
884
- if (s >= 0 && s1 >= 0 && s2 >= 0 || s <= 0 && s1 <= 0 && s2 <= 0) {
885
- return `drawable:${d.index}`;
886
- }
887
- }
888
- }
889
- return null;
1223
+ const stageX = (x + 1) / 2;
1224
+ const stageY = (1 - y) / 2;
1225
+ const hit = stage.hitTest(stageX, stageY);
1226
+ if (!hit || hit.actorId !== actor.id) return null;
1227
+ return hit.area;
890
1228
  },
891
1229
  listParameters() {
892
- if (!model || !activeBackend?.listParameters) return [];
893
- return activeBackend.listParameters(model);
1230
+ return actor.slot.listParameters();
894
1231
  },
895
1232
  listMotionGroups() {
896
- return model?.settings.motionGroups ?? {};
1233
+ return actor.slot.listMotionGroups();
897
1234
  },
898
- async playMotion(group, index = 0, options2 = {}) {
899
- if (!model || !activeResolver) return false;
900
- const list = model.settings.motionGroups[group];
901
- const def = list?.[index];
902
- if (!def) return false;
903
- let clip = motionCache.get(def.file);
904
- if (!clip) {
905
- const json = await activeResolver.fetchJson(def.file);
906
- clip = parseMotion3(json);
907
- motionCache.set(def.file, clip);
908
- }
909
- const fadeInTime = options2.fadeInTime ?? def.fadeInTime ?? clip.fadeInTime;
910
- const fadeOutTime = options2.fadeOutTime ?? def.fadeOutTime ?? clip.fadeOutTime;
911
- return motionPlayer.start(group, index, clip, {
912
- priority: options2.priority ?? MotionPriority.normal,
913
- slot: options2.slot,
914
- queue: options2.queue,
915
- loop: options2.loop,
916
- fadeInTime,
917
- fadeOutTime
918
- });
1235
+ playMotion(group, index, options) {
1236
+ return actor.slot.playMotion(group, index, options);
919
1237
  },
920
1238
  stopMotion(opts) {
921
- motionPlayer.stop(opts?.fade !== false, opts?.slot);
1239
+ actor.slot.stopMotion(opts);
922
1240
  },
923
1241
  listPlayingMotions() {
924
- return motionPlayer.listPlaying();
1242
+ return actor.slot.listPlayingMotions();
925
1243
  },
926
1244
  async capturePng(opts = {}) {
927
1245
  if (!canvas) {
@@ -929,7 +1247,7 @@ function createLive2D(options = {}) {
929
1247
  "@doki-land/live2d: mount(canvas) before capturePng"
930
1248
  );
931
1249
  }
932
- if (phase === "live" && model && activeBackend && drawPass) {
1250
+ if (phase === "live") {
933
1251
  runtime.update(0);
934
1252
  }
935
1253
  const mime = opts.mimeType ?? "image/png";
@@ -950,55 +1268,35 @@ function createLive2D(options = {}) {
950
1268
  });
951
1269
  },
952
1270
  update(deltaTimeSeconds) {
953
- if (!model || !activeBackend || !drawPass) return;
954
- if (phase !== "live") return;
1271
+ if (phase !== "live" && phase !== "ready") return;
955
1272
  const t0 = nowMs();
956
- applyMotionSamples(motionPlayer.update(deltaTimeSeconds));
957
- activeBackend.updateModel(model, deltaTimeSeconds);
958
- const drawables = activeBackend.getDrawables(model);
1273
+ stage.update(deltaTimeSeconds);
1274
+ stage.render();
959
1275
  const t1 = nowMs();
960
- renderer.beginFrame();
961
- drawPass.draw(drawables, new Float32Array(16));
962
- renderer.endFrame();
963
- const t2 = nowMs();
1276
+ const drawables = actor.lastDrawables ?? [];
964
1277
  let vertexCount = 0;
965
1278
  let indexCount = 0;
966
1279
  for (const d of drawables) {
967
1280
  vertexCount += d.vertexPositions.length / 2;
968
1281
  indexCount += d.indices.length;
969
1282
  }
970
- const frameMs = t2 - t0;
971
- const evaluateMs = t1 - t0;
972
- const drawMs = t2 - t1;
1283
+ const frameMs = t1 - t0;
973
1284
  const fps = deltaTimeSeconds > 0 ? 1 / deltaTimeSeconds : 0;
974
1285
  fpsSmooth = fpsSmooth <= 0 ? fps : fpsSmooth * 0.85 + fps * 0.15;
975
1286
  events.emit("profile", {
976
1287
  fps,
977
1288
  fpsSmooth,
978
1289
  frameMs,
979
- evaluateMs,
980
- drawMs,
1290
+ evaluateMs: frameMs,
1291
+ drawMs: 0,
981
1292
  drawableCount: drawables.length,
982
1293
  vertexCount,
983
1294
  indexCount
984
1295
  });
985
1296
  },
986
1297
  destroy() {
987
- loadGeneration += 1;
988
1298
  generation += 1;
989
- motionPlayer.clear();
990
- motionCache.clear();
991
- activeResolver = null;
992
- if (model && activeBackend) {
993
- activeBackend.destroyModel(model);
994
- }
995
- model = null;
996
- activeBackend = null;
997
- clearTextures();
998
- drawPass?.destroy();
999
- drawPass = null;
1000
- initPromise = null;
1001
- renderer.destroy();
1299
+ stage.destroy();
1002
1300
  canvas = null;
1003
1301
  setPhase("destroyed");
1004
1302
  events.clear();
@@ -1007,39 +1305,339 @@ function createLive2D(options = {}) {
1007
1305
  return runtime;
1008
1306
  }
1009
1307
 
1010
- // src/focus.ts
1011
- function focusParameterUpdates(parameters, dragX, dragY) {
1012
- const byId = new Map(parameters.map((p) => [p.id, p]));
1013
- const x = clampUnit(dragX);
1014
- const y = clampUnit(dragY);
1015
- const out = [];
1016
- const set = (id, normalized) => {
1017
- const binding = byId.get(id);
1018
- if (!binding) return;
1019
- out.push({ id, value: valueFromNormalized(binding, normalized) });
1020
- };
1021
- set("PARAM_ANGLE_X", x);
1022
- set("PARAM_ANGLE_Y", y);
1023
- set("PARAM_ANGLE_Z", clampUnit(x * y * -1));
1024
- set("PARAM_BODY_ANGLE_X", x);
1025
- set("PARAM_BODY_ANGLE_Y", y);
1026
- set("PARAM_EYE_BALL_X", x);
1027
- set("PARAM_EYE_BALL_Y", y);
1028
- return out;
1308
+ // src/stage/stage.ts
1309
+ import {
1310
+ createMoc2Backend,
1311
+ createMoc3Backend,
1312
+ createRenderer
1313
+ } from "@doki-land/live2d-renderer";
1314
+ var Live2dStageImpl = class {
1315
+ #backends;
1316
+ #renderer;
1317
+ #updateMode;
1318
+ #actors = /* @__PURE__ */ new Map();
1319
+ #definedLayers = [
1320
+ "background",
1321
+ "characters-back",
1322
+ "characters",
1323
+ "characters-front",
1324
+ "effects"
1325
+ ];
1326
+ #pointerListeners = /* @__PURE__ */ new Map([
1327
+ ["pointerdown", /* @__PURE__ */ new Set()],
1328
+ ["pointermove", /* @__PURE__ */ new Set()],
1329
+ ["pointerup", /* @__PURE__ */ new Set()]
1330
+ ]);
1331
+ #canvas = null;
1332
+ #initPromise = null;
1333
+ #rafId = null;
1334
+ #running = false;
1335
+ #paused = false;
1336
+ #lastFrameMs = 0;
1337
+ #creationCounter = 0;
1338
+ #focusedActorId = null;
1339
+ #lastPointer = null;
1340
+ #pointerTracking = { mode: "focused" };
1341
+ #boundPointerDown;
1342
+ #boundPointerMove;
1343
+ #boundPointerUp;
1344
+ #destroyed = false;
1345
+ constructor(options = {}) {
1346
+ this.#backends = options.backends ?? [
1347
+ createMoc2Backend(),
1348
+ createMoc3Backend()
1349
+ ];
1350
+ this.#renderer = options.renderer ?? createRenderer({ prefer: options.prefer });
1351
+ this.#updateMode = options.updateMode ?? "auto";
1352
+ }
1353
+ get actors() {
1354
+ return [...this.#actors.values()];
1355
+ }
1356
+ get pointerTracking() {
1357
+ return this.#pointerTracking;
1358
+ }
1359
+ set pointerTracking(policy) {
1360
+ this.#pointerTracking = policy;
1361
+ }
1362
+ get renderer() {
1363
+ return this.#renderer;
1364
+ }
1365
+ async mount(canvas) {
1366
+ if (this.#destroyed) {
1367
+ throw new Error("@doki-land/live2d: stage destroyed");
1368
+ }
1369
+ this.#canvas = canvas;
1370
+ this.#initPromise = this.#renderer.initialize(canvas);
1371
+ await this.#initPromise;
1372
+ this.#attachPointerListeners(canvas);
1373
+ }
1374
+ createActor(options) {
1375
+ if (this.#destroyed) {
1376
+ throw new Error("@doki-land/live2d: stage destroyed");
1377
+ }
1378
+ const id = options?.id ?? allocateActorId();
1379
+ if (this.#actors.has(id)) {
1380
+ throw new Error(`@doki-land/live2d: duplicate actor id "${id}"`);
1381
+ }
1382
+ const creationIndex = this.#creationCounter++;
1383
+ const actor = new Live2dActorImpl(options, {
1384
+ id,
1385
+ creationIndex,
1386
+ backends: this.#backends,
1387
+ renderer: this.#renderer
1388
+ });
1389
+ this.#actors.set(id, actor);
1390
+ if (!this.#focusedActorId) {
1391
+ this.#focusedActorId = id;
1392
+ }
1393
+ return actor;
1394
+ }
1395
+ removeActor(actorOrId) {
1396
+ const id = typeof actorOrId === "string" ? actorOrId : actorOrId.id;
1397
+ const actor = this.#actors.get(id);
1398
+ if (!actor) return;
1399
+ actor.destroy();
1400
+ this.#actors.delete(id);
1401
+ if (this.#focusedActorId === id) {
1402
+ this.#focusedActorId = this.#actors.keys().next().value ?? null;
1403
+ }
1404
+ }
1405
+ defineLayers(layers) {
1406
+ this.#definedLayers.length = 0;
1407
+ this.#definedLayers.push(...layers);
1408
+ }
1409
+ update(deltaTimeSeconds) {
1410
+ if (this.#destroyed) return;
1411
+ for (const actor of this.#actors.values()) {
1412
+ actor.update(deltaTimeSeconds);
1413
+ }
1414
+ this.#applyPointerTracking();
1415
+ }
1416
+ render() {
1417
+ if (this.#destroyed || !this.#canvas) return;
1418
+ const sorted = [...this.#actors.values()].sort(
1419
+ (a, b) => compareActorsForDraw(a, b, this.#definedLayers)
1420
+ );
1421
+ this.#renderer.beginFrame();
1422
+ for (const actor of sorted) {
1423
+ if (!actor.visible || actor.opacity <= 0) continue;
1424
+ const drawables = actor.lastDrawables;
1425
+ const pass = actor.slot.drawPass;
1426
+ if (!drawables || !pass) continue;
1427
+ const placed = transformDrawablesForStage(
1428
+ drawables,
1429
+ actor.getTransform(),
1430
+ actor.opacity
1431
+ );
1432
+ pass.draw(placed, new Float32Array(16));
1433
+ }
1434
+ this.#renderer.endFrame();
1435
+ }
1436
+ start() {
1437
+ if (this.#updateMode === "manual") {
1438
+ throw new Error(
1439
+ "@doki-land/live2d: start() is not available when updateMode is manual"
1440
+ );
1441
+ }
1442
+ if (this.#running) return;
1443
+ this.#running = true;
1444
+ this.#paused = false;
1445
+ this.#lastFrameMs = nowMs2();
1446
+ const tick = () => {
1447
+ if (!this.#running) return;
1448
+ if (!this.#paused) {
1449
+ const t = nowMs2();
1450
+ const dt = Math.min(0.1, (t - this.#lastFrameMs) / 1e3);
1451
+ this.#lastFrameMs = t;
1452
+ this.update(dt);
1453
+ this.render();
1454
+ }
1455
+ this.#rafId = requestAnimationFrame(tick);
1456
+ };
1457
+ this.#rafId = requestAnimationFrame(tick);
1458
+ }
1459
+ pause() {
1460
+ this.#paused = true;
1461
+ }
1462
+ resume() {
1463
+ this.#paused = false;
1464
+ this.#lastFrameMs = nowMs2();
1465
+ }
1466
+ stop() {
1467
+ this.#running = false;
1468
+ this.#paused = false;
1469
+ if (this.#rafId !== null) {
1470
+ cancelAnimationFrame(this.#rafId);
1471
+ this.#rafId = null;
1472
+ }
1473
+ }
1474
+ hitTest(stageX, stageY) {
1475
+ const hits = this.hitTestAll(stageX, stageY);
1476
+ return hits[0] ?? null;
1477
+ }
1478
+ hitTestAll(stageX, stageY) {
1479
+ const sorted = [...this.#actors.values()].sort(
1480
+ (a, b) => compareActorsForHit(a, b, this.#definedLayers)
1481
+ );
1482
+ const hits = [];
1483
+ for (const actor of sorted) {
1484
+ const partial = actor.hitTestStage(stageX, stageY);
1485
+ if (!partial) continue;
1486
+ hits.push({ ...partial, actor });
1487
+ }
1488
+ return hits;
1489
+ }
1490
+ addEventListener(type, listener) {
1491
+ this.#pointerListeners.get(type)?.add(listener);
1492
+ }
1493
+ removeEventListener(type, listener) {
1494
+ this.#pointerListeners.get(type)?.delete(listener);
1495
+ }
1496
+ destroy() {
1497
+ if (this.#destroyed) return;
1498
+ this.#destroyed = true;
1499
+ this.stop();
1500
+ this.#detachPointerListeners();
1501
+ for (const actor of this.#actors.values()) {
1502
+ actor.destroy();
1503
+ }
1504
+ this.#actors.clear();
1505
+ this.#renderer.destroy();
1506
+ this.#canvas = null;
1507
+ this.#initPromise = null;
1508
+ for (const set of this.#pointerListeners.values()) {
1509
+ set.clear();
1510
+ }
1511
+ }
1512
+ #attachPointerListeners(canvas) {
1513
+ this.#detachPointerListeners();
1514
+ this.#boundPointerDown = (e) => this.#onPointer("pointerdown", e);
1515
+ this.#boundPointerMove = (e) => this.#onPointer("pointermove", e);
1516
+ this.#boundPointerUp = (e) => this.#onPointer("pointerup", e);
1517
+ canvas.addEventListener("pointerdown", this.#boundPointerDown);
1518
+ canvas.addEventListener("pointermove", this.#boundPointerMove);
1519
+ canvas.addEventListener("pointerup", this.#boundPointerUp);
1520
+ }
1521
+ #detachPointerListeners() {
1522
+ if (!this.#canvas) return;
1523
+ if (this.#boundPointerDown) {
1524
+ this.#canvas.removeEventListener(
1525
+ "pointerdown",
1526
+ this.#boundPointerDown
1527
+ );
1528
+ }
1529
+ if (this.#boundPointerMove) {
1530
+ this.#canvas.removeEventListener(
1531
+ "pointermove",
1532
+ this.#boundPointerMove
1533
+ );
1534
+ }
1535
+ if (this.#boundPointerUp) {
1536
+ this.#canvas.removeEventListener("pointerup", this.#boundPointerUp);
1537
+ }
1538
+ this.#boundPointerDown = void 0;
1539
+ this.#boundPointerMove = void 0;
1540
+ this.#boundPointerUp = void 0;
1541
+ }
1542
+ #onPointer(type, event) {
1543
+ if (!this.#canvas) return;
1544
+ const { stageX, stageY } = clientToStage(
1545
+ event.clientX,
1546
+ event.clientY,
1547
+ this.#canvas
1548
+ );
1549
+ this.#lastPointer = { stageX, stageY };
1550
+ const hit = this.hitTest(stageX, stageY);
1551
+ if (type === "pointerdown" && hit) {
1552
+ this.#focusedActorId = hit.actorId;
1553
+ }
1554
+ const payload = {
1555
+ actor: hit?.actor ?? null,
1556
+ actorId: hit?.actorId ?? null,
1557
+ area: hit?.area ?? null,
1558
+ stageX,
1559
+ stageY,
1560
+ clientX: event.clientX,
1561
+ clientY: event.clientY,
1562
+ hit
1563
+ };
1564
+ for (const listener of this.#pointerListeners.get(type) ?? []) {
1565
+ listener(payload);
1566
+ }
1567
+ }
1568
+ #applyPointerTracking() {
1569
+ if (!this.#canvas || !this.#lastPointer) return;
1570
+ const { stageX, stageY } = this.#lastPointer;
1571
+ const mode = this.#pointerTracking.mode;
1572
+ if (mode === "none") return;
1573
+ if (mode === "all") {
1574
+ for (const actor of this.#actors.values()) {
1575
+ actor.lookAt(stageX, stageY);
1576
+ }
1577
+ return;
1578
+ }
1579
+ if (mode === "custom" && this.#pointerTracking.targetActorId) {
1580
+ const actor = this.#actors.get(this.#pointerTracking.targetActorId);
1581
+ actor?.lookAt(stageX, stageY);
1582
+ return;
1583
+ }
1584
+ if (mode === "hovered") {
1585
+ for (const actor of this.#actors.values()) {
1586
+ if (actor.hitTestStage(stageX, stageY)) {
1587
+ actor.lookAt(stageX, stageY);
1588
+ }
1589
+ }
1590
+ return;
1591
+ }
1592
+ if (mode === "nearest") {
1593
+ let best = null;
1594
+ let bestDist = Number.POSITIVE_INFINITY;
1595
+ for (const actor of this.#actors.values()) {
1596
+ const t = actor.getTransform();
1597
+ const dx = t.x - stageX;
1598
+ const dy = t.y - stageY;
1599
+ const dist = dx * dx + dy * dy;
1600
+ if (dist < bestDist) {
1601
+ bestDist = dist;
1602
+ best = actor;
1603
+ }
1604
+ }
1605
+ best?.lookAt(stageX, stageY);
1606
+ return;
1607
+ }
1608
+ if (mode === "focused" && this.#focusedActorId) {
1609
+ this.#actors.get(this.#focusedActorId)?.lookAt(stageX, stageY);
1610
+ }
1611
+ }
1612
+ };
1613
+ function nowMs2() {
1614
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
1029
1615
  }
1030
- function clampUnit(n) {
1031
- if (n > 1) return 1;
1032
- if (n < -1) return -1;
1033
- return n;
1616
+ function createLive2dStage(options) {
1617
+ return new Live2dStageImpl(options);
1034
1618
  }
1035
- function valueFromNormalized(binding, normalized) {
1036
- const n = clampUnit(normalized);
1037
- return n >= 0 ? binding.defaultValue + (binding.max - binding.defaultValue) * n : binding.defaultValue + (binding.defaultValue - binding.min) * n;
1619
+
1620
+ // src/create-live2d.ts
1621
+ function createLive2D(options = {}) {
1622
+ const backends = options.backends ?? [
1623
+ createMoc2Backend2(),
1624
+ createMoc3Backend2()
1625
+ ];
1626
+ const stage = createLive2dStage({
1627
+ backends,
1628
+ renderer: options.renderer ?? createRenderer2({ prefer: options.prefer }),
1629
+ updateMode: options.updateMode ?? "manual"
1630
+ });
1631
+ const actor = stage.createActor({
1632
+ id: allocateActorId("default")
1633
+ });
1634
+ return createSingleActorFacade(stage, actor, backends);
1038
1635
  }
1039
1636
 
1040
1637
  // src/index.ts
1041
1638
  var LIVE2D_VERSION = "0.0.0";
1042
1639
  export {
1640
+ DEFAULT_ACTOR_TRANSFORM2 as DEFAULT_ACTOR_TRANSFORM,
1043
1641
  DEFAULT_NPM_CDN,
1044
1642
  EventEmitter2 as EventEmitter,
1045
1643
  LIVE2D_VERSION,
@@ -1048,10 +1646,11 @@ export {
1048
1646
  blendMotionLayers,
1049
1647
  createCanvas2DRenderer,
1050
1648
  createLive2D,
1051
- createMoc2Backend2 as createMoc2Backend,
1052
- createMoc3Backend2 as createMoc3Backend,
1649
+ createLive2dStage,
1650
+ createMoc2Backend3 as createMoc2Backend,
1651
+ createMoc3Backend3 as createMoc3Backend,
1053
1652
  createQuadProgram,
1054
- createRenderer2 as createRenderer,
1653
+ createRenderer3 as createRenderer,
1055
1654
  createWebGl2Renderer,
1056
1655
  createWebGpuRenderer,
1057
1656
  decodeMoc3,