@aether-stack-dev/client-sdk 1.2.5 → 1.3.1

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/react.js CHANGED
@@ -7,6 +7,16 @@ var jsxRuntime = require('react/jsx-runtime');
7
7
  var THREE = require('three');
8
8
  var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
9
9
  var threeVrm = require('@pixiv/three-vrm');
10
+ var threeVrmAnimation = require('@pixiv/three-vrm-animation');
11
+ var EffectComposer_js = require('three/examples/jsm/postprocessing/EffectComposer.js');
12
+ var RenderPass_js = require('three/examples/jsm/postprocessing/RenderPass.js');
13
+ var UnrealBloomPass_js = require('three/examples/jsm/postprocessing/UnrealBloomPass.js');
14
+ var SSAOPass_js = require('three/examples/jsm/postprocessing/SSAOPass.js');
15
+ var BokehPass_js = require('three/examples/jsm/postprocessing/BokehPass.js');
16
+ var OutputPass_js = require('three/examples/jsm/postprocessing/OutputPass.js');
17
+ var HDRLoader_js = require('three/examples/jsm/loaders/HDRLoader.js');
18
+ var EXRLoader_js = require('three/examples/jsm/loaders/EXRLoader.js');
19
+ var KTX2Loader_js = require('three/examples/jsm/loaders/KTX2Loader.js');
10
20
 
11
21
  function _interopNamespaceDefault(e) {
12
22
  var n = Object.create(null);
@@ -49,8 +59,18 @@ const CRITICAL_BLENDSHAPES = [
49
59
  'jawOpen', 'eyeBlinkLeft', 'eyeBlinkRight',
50
60
  ];
51
61
  const DEFAULT_BLENDSHAPE_MAP = Object.fromEntries(ARKIT_BLENDSHAPES.map(name => [name, name]));
52
- ({
53
- ...DEFAULT_BLENDSHAPE_MAP});
62
+ const VROID_BLENDSHAPE_MAP = {
63
+ ...DEFAULT_BLENDSHAPE_MAP,
64
+ jawOpen: 'Fcl_MTH_A',
65
+ mouthFunnel: 'Fcl_MTH_U',
66
+ mouthSmileLeft: 'Fcl_MTH_Fun',
67
+ mouthSmileRight: 'Fcl_MTH_Fun',
68
+ eyeBlinkLeft: 'Fcl_EYE_Close_L',
69
+ eyeBlinkRight: 'Fcl_EYE_Close_R',
70
+ browDownLeft: 'Fcl_BRW_Angry_L',
71
+ browDownRight: 'Fcl_BRW_Angry_R',
72
+ browInnerUp: 'Fcl_BRW_Surprised',
73
+ };
54
74
 
55
75
  const IDX_JAW_OPEN = ARKIT_BLENDSHAPES.indexOf('jawOpen');
56
76
  const IDX_MOUTH_FUNNEL = ARKIT_BLENDSHAPES.indexOf('mouthFunnel');
@@ -818,7 +838,6 @@ function useAStackCSR(options) {
818
838
  };
819
839
  }
820
840
 
821
- const DEFAULT_MAX_MODEL_SIZE = 30 * 1024 * 1024; // 30MB
822
841
  const HIGH_VERTEX_THRESHOLD = 100000;
823
842
  function buildCompatibilityReport(vrm) {
824
843
  const foundNames = new Set();
@@ -855,7 +874,9 @@ function buildCompatibilityReport(vrm) {
855
874
  const supported = [];
856
875
  const missing = [];
857
876
  for (const name of ARKIT_BLENDSHAPES) {
858
- if (foundNames.has(name) || foundNames.has(name.toLowerCase())) {
877
+ const vroidMapped = VROID_BLENDSHAPE_MAP[name];
878
+ if (foundNames.has(name) || foundNames.has(name.toLowerCase())
879
+ || (vroidMapped && (foundNames.has(vroidMapped) || foundNames.has(vroidMapped.toLowerCase())))) {
859
880
  supported.push(name);
860
881
  }
861
882
  else {
@@ -878,45 +899,205 @@ function buildCompatibilityReport(vrm) {
878
899
  modelStats: { vertexCount, textureCount, morphTargetCount },
879
900
  };
880
901
  }
881
- function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models/avatar.vrm', backgroundColor = 0x1a1a2e, blendshapeMap, onModelLoad, maxModelSize = DEFAULT_MAX_MODEL_SIZE, }) {
902
+
903
+ function createPostProcessing(renderer, scene, camera, config, width, height) {
904
+ const composer = new EffectComposer_js.EffectComposer(renderer);
905
+ composer.addPass(new RenderPass_js.RenderPass(scene, camera));
906
+ if (config.bloom !== undefined && config.bloom > 0) {
907
+ composer.addPass(new UnrealBloomPass_js.UnrealBloomPass(new THREE__namespace.Vector2(width, height), config.bloom * 0.8, 0.4, 0.85));
908
+ }
909
+ if (config.ao !== undefined && config.ao > 0) {
910
+ const ssao = new SSAOPass_js.SSAOPass(scene, camera, width, height);
911
+ ssao.kernelRadius = 8 * config.ao;
912
+ ssao.minDistance = 0.005;
913
+ ssao.maxDistance = 0.1;
914
+ composer.addPass(ssao);
915
+ }
916
+ if (config.dof) {
917
+ composer.addPass(new BokehPass_js.BokehPass(scene, camera, { focus: 1.4, aperture: 0.002, maxblur: 0.005 }));
918
+ }
919
+ composer.addPass(new OutputPass_js.OutputPass());
920
+ return {
921
+ composer,
922
+ dispose: () => { composer.renderTarget1.dispose(); composer.renderTarget2.dispose(); },
923
+ setSize: (w, h) => composer.setSize(w, h),
924
+ };
925
+ }
926
+ const FPS_SAMPLE_SIZE = 60;
927
+ const FPS_THRESHOLD = 30;
928
+ class FpsMonitor {
929
+ constructor() {
930
+ this.samples = [];
931
+ this.lastTime = 0;
932
+ this.disabled = false;
933
+ }
934
+ tick(time) {
935
+ if (this.lastTime > 0) {
936
+ const delta = time - this.lastTime;
937
+ if (delta > 0)
938
+ this.samples.push(1000 / delta);
939
+ if (this.samples.length > FPS_SAMPLE_SIZE)
940
+ this.samples.shift();
941
+ }
942
+ this.lastTime = time;
943
+ if (this.samples.length >= FPS_SAMPLE_SIZE) {
944
+ const avg = this.samples.reduce((a, b) => a + b, 0) / this.samples.length;
945
+ if (avg < FPS_THRESHOLD) {
946
+ this.disabled = true;
947
+ console.warn(`[VRM] Post-processing auto-disabled (avg FPS: ${avg.toFixed(0)})`);
948
+ }
949
+ }
950
+ }
951
+ reset() {
952
+ this.samples = [];
953
+ this.lastTime = 0;
954
+ this.disabled = false;
955
+ }
956
+ }
957
+
958
+ let ktx2Loader = null;
959
+ function getKTX2Loader(renderer) {
960
+ if (!ktx2Loader) {
961
+ ktx2Loader = new KTX2Loader_js.KTX2Loader();
962
+ ktx2Loader.setTranscoderPath('/basis/');
963
+ ktx2Loader.detectSupport(renderer);
964
+ }
965
+ return ktx2Loader;
966
+ }
967
+ function loadEnvironment(renderer, url) {
968
+ const cleanUrl = url.split('?')[0].split('#')[0].toLowerCase();
969
+ // For blob URLs, the filename is passed as a hash fragment (e.g. blob:...#file.ktx2)
970
+ const hash = (url.split('#')[1] || '').toLowerCase();
971
+ const hashExt = hash.includes('.') ? '.' + hash.split('.').pop() : '';
972
+ const pmrem = new THREE__namespace.PMREMGenerator(renderer);
973
+ pmrem.compileEquirectangularShader();
974
+ const finalize = (texture) => {
975
+ const envMap = pmrem.fromEquirectangular(texture).texture;
976
+ texture.dispose();
977
+ pmrem.dispose();
978
+ return { envMap, dispose: () => envMap.dispose() };
979
+ };
980
+ if (cleanUrl.endsWith('.ktx2') || hashExt === '.ktx2') {
981
+ return new Promise((resolve, reject) => {
982
+ getKTX2Loader(renderer).load(url, (compressedTex) => {
983
+ // Compressed textures can't be flipped by WebGL on upload, so the
984
+ // top-down KTX2 data is inverted vs what PMREM expects. Render to
985
+ // an intermediate target with flipped v to decompress + correct.
986
+ const w = compressedTex.image.width, h = compressedTex.image.height;
987
+ const rt = new THREE__namespace.WebGLRenderTarget(w, h, { type: THREE__namespace.HalfFloatType });
988
+ const cam = new THREE__namespace.OrthographicCamera(-1, 1, 1, -1, 0, 1);
989
+ const mat = new THREE__namespace.ShaderMaterial({
990
+ uniforms: { tMap: { value: compressedTex } },
991
+ vertexShader: 'varying vec2 vUv; void main(){ vUv=uv; gl_Position=vec4(position,1.); }',
992
+ fragmentShader: 'uniform sampler2D tMap; varying vec2 vUv; void main(){ gl_FragColor=texture2D(tMap,vec2(vUv.x,1.-vUv.y)); }',
993
+ });
994
+ const quad = new THREE__namespace.Mesh(new THREE__namespace.PlaneGeometry(2, 2), mat);
995
+ const sc = new THREE__namespace.Scene();
996
+ sc.add(quad);
997
+ const prevRT = renderer.getRenderTarget();
998
+ renderer.setRenderTarget(rt);
999
+ renderer.render(sc, cam);
1000
+ renderer.setRenderTarget(prevRT);
1001
+ mat.dispose();
1002
+ quad.geometry.dispose();
1003
+ compressedTex.dispose();
1004
+ rt.texture.mapping = THREE__namespace.EquirectangularReflectionMapping;
1005
+ const envMap = pmrem.fromEquirectangular(rt.texture).texture;
1006
+ rt.dispose();
1007
+ pmrem.dispose();
1008
+ resolve({ envMap, dispose: () => envMap.dispose() });
1009
+ }, undefined, reject);
1010
+ });
1011
+ }
1012
+ return new Promise((resolve, reject) => {
1013
+ const onLoad = (texture) => resolve(finalize(texture));
1014
+ if (cleanUrl.endsWith('.exr') || hashExt === '.exr') {
1015
+ new EXRLoader_js.EXRLoader().load(url, onLoad, undefined, reject);
1016
+ }
1017
+ else {
1018
+ new HDRLoader_js.HDRLoader().load(url, onLoad, undefined, reject);
1019
+ }
1020
+ });
1021
+ }
1022
+
1023
+ const DEFAULT_MAX_MODEL_SIZE = 30 * 1024 * 1024; // 30MB
1024
+ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = 'https://xwqtyabmavpacejuypyp.supabase.co/storage/v1/object/public/vrm-templates/5705015963733407866.vrm', backgroundColor = 0xffffff, blendshapeMap, expressionOverrides, onModelLoad, maxModelSize = DEFAULT_MAX_MODEL_SIZE, cameraPosition, cameraTarget, cameraFov, lightIntensity, idleAnimationUrl, animationSpeed = 1, animationWeight = 1, animationCrossfade = 0.5, postProcessing, environmentUrl, environmentIntensity, environmentBlur, environmentZoom, onEnvironmentLoad, features, orbitAngle, orbitElevation, avatarRotation, onOrbitChange, cursorFollow = true, preserveDrawingBuffer = false, }) {
882
1025
  const containerRef = react.useRef(null);
883
1026
  const rendererRef = react.useRef(null);
884
1027
  const sceneRef = react.useRef(null);
885
1028
  const cameraRef = react.useRef(null);
886
1029
  const vrmRef = react.useRef(null);
1030
+ const lightsRef = react.useRef(null);
1031
+ const onModelLoadRef = react.useRef(onModelLoad);
1032
+ onModelLoadRef.current = onModelLoad;
1033
+ const animSpeedRef = react.useRef(animationSpeed);
1034
+ animSpeedRef.current = animationSpeed;
1035
+ const animWeightRef = react.useRef(animationWeight);
1036
+ animWeightRef.current = animationWeight;
1037
+ const animCrossfadeRef = react.useRef(animationCrossfade);
1038
+ animCrossfadeRef.current = animationCrossfade;
1039
+ const exprOverridesRef = react.useRef(expressionOverrides);
1040
+ exprOverridesRef.current = expressionOverrides;
1041
+ const featuresRef = react.useRef(features);
1042
+ featuresRef.current = features;
1043
+ const envZoomRef = react.useRef(environmentZoom);
1044
+ envZoomRef.current = environmentZoom;
1045
+ const onEnvLoadRef = react.useRef(onEnvironmentLoad);
1046
+ onEnvLoadRef.current = onEnvironmentLoad;
887
1047
  const [loading, setLoading] = react.useState(true);
888
1048
  const [error, setError] = react.useState(null);
889
1049
  const animationFrameRef = react.useRef(0);
890
1050
  const animationTimeRef = react.useRef(0);
1051
+ const mixerRef = react.useRef(null);
1052
+ const clipActionRef = react.useRef(null);
1053
+ const composerRef = react.useRef(null);
1054
+ const envMapRef = react.useRef(null);
1055
+ const bgSceneRef = react.useRef(new THREE__namespace.Scene());
1056
+ react.useRef(null);
1057
+ const fpsMonitorRef = react.useRef(new FpsMonitor());
1058
+ const orbitRef = react.useRef({ angle: 0, targetAngle: 0, elevation: 0, targetElevation: 0, zoomOffset: 0, dragging: false, lastX: 0, lastY: 0 });
1059
+ const orbitAnglePropRef = react.useRef(orbitAngle);
1060
+ orbitAnglePropRef.current = orbitAngle;
1061
+ const onOrbitChangeRef = react.useRef(onOrbitChange);
1062
+ onOrbitChangeRef.current = onOrbitChange;
1063
+ const baseCamRef = react.useRef({ distance: 1.2, height: 1.4, targetY: 1.3 });
1064
+ const idleRef = react.useRef({ blinkNext: 2 + Math.random() * 4, blinkPhase: 0, blinkStart: 0,
1065
+ happy: 0, happyTarget: 0, browInner: 0, browTarget: 0, cheek: 0, cheekTarget: 0, microNext: 3,
1066
+ eyeX: 0, eyeY: 0, eyeTargetX: 0, eyeTargetY: 0, eyeNext: 1 });
1067
+ const isA2FActiveRef = react.useRef(false);
1068
+ const idleMorphsRef = react.useRef([]);
1069
+ const springSettleFramesRef = react.useRef(0);
1070
+ const cursorFollowRef = react.useRef(cursorFollow);
1071
+ cursorFollowRef.current = cursorFollow;
1072
+ const cursorRef = react.useRef({ x: 0, y: 0, active: false });
891
1073
  const applyBlendshapes = react.useCallback((vrm, shapes) => {
892
1074
  const arkitMap = {};
893
1075
  ARKIT_BLENDSHAPES.forEach((name, i) => {
894
1076
  arkitMap[name] = shapes[i] || 0;
895
1077
  });
896
- const mouthOpen = arkitMap.jawOpen || 0;
1078
+ const mouthOpen = arkitMap.jawOpen || 0, funnel = arkitMap.mouthFunnel || 0;
897
1079
  const smile = ((arkitMap.mouthSmileLeft || 0) + (arkitMap.mouthSmileRight || 0)) / 2;
898
- const funnel = arkitMap.mouthFunnel || 0;
899
- const blinkLeft = arkitMap.eyeBlinkLeft || 0;
900
- const blinkRight = arkitMap.eyeBlinkRight || 0;
1080
+ const blinkLeft = arkitMap.eyeBlinkLeft || 0, blinkRight = arkitMap.eyeBlinkRight || 0;
901
1081
  if (vrm.expressionManager) {
902
1082
  vrm.expressionManager.expressions.forEach(exp => {
903
1083
  vrm.expressionManager?.setValue(exp.expressionName, 0);
904
1084
  });
905
- const trySetExpression = (name, value) => {
1085
+ const trySet = (name, value) => {
906
1086
  try {
907
1087
  vrm.expressionManager?.setValue(name, Math.min(Math.max(value, 0), 1));
908
1088
  }
909
- catch {
910
- // Expression doesn't exist
911
- }
1089
+ catch { }
912
1090
  };
913
- trySetExpression('aa', mouthOpen * 1.5);
914
- trySetExpression('happy', smile * 2);
915
- trySetExpression('blink', (blinkLeft + blinkRight) / 2);
916
- trySetExpression('blinkLeft', blinkLeft);
917
- trySetExpression('blinkRight', blinkRight);
918
- trySetExpression('ou', funnel);
919
- vrm.expressionManager.update();
1091
+ trySet('aa', mouthOpen * 1.5);
1092
+ trySet('happy', smile * 2);
1093
+ trySet('blink', (blinkLeft + blinkRight) / 2);
1094
+ trySet('blinkLeft', blinkLeft);
1095
+ trySet('blinkRight', blinkRight);
1096
+ trySet('ou', funnel);
1097
+ try {
1098
+ vrm.expressionManager.update();
1099
+ }
1100
+ catch { }
920
1101
  }
921
1102
  vrm.scene.traverse((obj) => {
922
1103
  if (obj.isMesh) {
@@ -944,7 +1125,8 @@ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models
944
1125
  renderer = new THREE__namespace.WebGLRenderer({
945
1126
  antialias: true,
946
1127
  alpha: true,
947
- powerPreference: 'high-performance'
1128
+ powerPreference: 'high-performance',
1129
+ preserveDrawingBuffer,
948
1130
  });
949
1131
  }
950
1132
  catch {
@@ -958,20 +1140,21 @@ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models
958
1140
  container.appendChild(renderer.domElement);
959
1141
  rendererRef.current = renderer;
960
1142
  const scene = new THREE__namespace.Scene();
961
- scene.background = new THREE__namespace.Color(backgroundColor);
1143
+ scene.background = backgroundColor === null ? null : new THREE__namespace.Color(backgroundColor ?? 0xffffff);
962
1144
  sceneRef.current = scene;
963
1145
  const camera = new THREE__namespace.PerspectiveCamera(30, width / height, 0.1, 20);
964
1146
  camera.position.set(0, 1.4, 1.2);
965
1147
  camera.lookAt(0, 1.3, 0);
966
1148
  cameraRef.current = camera;
967
- const ambientLight = new THREE__namespace.AmbientLight(0xffffff, 0.6);
1149
+ const ambientLight = new THREE__namespace.AmbientLight(0xffffff, 1.0);
968
1150
  scene.add(ambientLight);
969
- const directionalLight = new THREE__namespace.DirectionalLight(0xffffff, 0.8);
1151
+ const directionalLight = new THREE__namespace.DirectionalLight(0xffffff, 1.2);
970
1152
  directionalLight.position.set(1, 1, 1);
971
1153
  scene.add(directionalLight);
972
- const backLight = new THREE__namespace.DirectionalLight(0x4a9eff, 0.3);
1154
+ const backLight = new THREE__namespace.DirectionalLight(0x4a9eff, 0.4);
973
1155
  backLight.position.set(-1, 1, -1);
974
1156
  scene.add(backLight);
1157
+ lightsRef.current = { ambient: ambientLight, directional: directionalLight, back: backLight };
975
1158
  let cancelled = false;
976
1159
  // Size guard: fire HEAD check in parallel — if model is too large, cancel before render
977
1160
  if (maxModelSize > 0) {
@@ -986,7 +1169,7 @@ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models
986
1169
  const errMsg = `Model too large (${sizeMB}MB, limit ${limitMB}MB)`;
987
1170
  setError(errMsg);
988
1171
  setLoading(false);
989
- onModelLoad?.({
1172
+ onModelLoadRef.current?.({
990
1173
  supported: 0, missing: [...ARKIT_BLENDSHAPES], warnings: [errMsg],
991
1174
  modelStats: { vertexCount: 0, textureCount: 0, morphTargetCount: 0 },
992
1175
  });
@@ -1002,12 +1185,49 @@ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models
1002
1185
  if (vrm) {
1003
1186
  scene.add(vrm.scene);
1004
1187
  vrmRef.current = vrm;
1188
+ const mc = [];
1189
+ vrm.scene.traverse((o) => {
1190
+ const mesh = o;
1191
+ if (!mesh.isMesh)
1192
+ return;
1193
+ const d = mesh.morphTargetDictionary, inf = mesh.morphTargetInfluences;
1194
+ if (!d || !inf)
1195
+ return;
1196
+ const e = { inf };
1197
+ if (d['browInnerUp'] !== undefined)
1198
+ e.brow = d['browInnerUp'];
1199
+ if (d['cheekSquintLeft'] !== undefined)
1200
+ e.cheekL = d['cheekSquintLeft'];
1201
+ if (d['cheekSquintRight'] !== undefined)
1202
+ e.cheekR = d['cheekSquintRight'];
1203
+ if (e.brow !== undefined || e.cheekL !== undefined)
1204
+ mc.push(e);
1205
+ });
1206
+ idleMorphsRef.current = mc;
1207
+ // Apply idle pose immediately so the model never renders in T-pose
1208
+ const humanoid = vrm.humanoid;
1209
+ if (humanoid) {
1210
+ const b = (n) => humanoid.getNormalizedBoneNode(n);
1211
+ const lArm = b(threeVrm.VRMHumanBoneName.LeftUpperArm), rArm = b(threeVrm.VRMHumanBoneName.RightUpperArm);
1212
+ if (lArm)
1213
+ lArm.rotation.z = -1;
1214
+ if (rArm)
1215
+ rArm.rotation.z = 1.0;
1216
+ }
1217
+ if (vrm.lookAt) {
1218
+ const proxy = new threeVrmAnimation.VRMLookAtQuaternionProxy(vrm.lookAt);
1219
+ proxy.name = 'VRMLookAtQuaternionProxy';
1220
+ vrm.scene.add(proxy);
1221
+ }
1222
+ vrm.update(0);
1223
+ vrm.springBoneManager?.reset();
1224
+ springSettleFramesRef.current = 30;
1005
1225
  setLoading(false);
1006
1226
  const report = buildCompatibilityReport(vrm);
1007
1227
  if (report.warnings.length > 0) {
1008
1228
  console.warn('[VRM] Compatibility warnings:', report.warnings);
1009
1229
  }
1010
- onModelLoad?.(report);
1230
+ onModelLoadRef.current?.(report);
1011
1231
  }
1012
1232
  }, () => { }, (err) => {
1013
1233
  if (cancelled)
@@ -1021,79 +1241,501 @@ function VRMAvatar({ blendshapes, width = 400, height = 400, modelUrl = '/models
1021
1241
  setError(detail);
1022
1242
  setLoading(false);
1023
1243
  });
1024
- const clock = new THREE__namespace.Clock();
1244
+ const timer = new THREE__namespace.Timer();
1025
1245
  const animate = () => {
1026
1246
  animationFrameRef.current = requestAnimationFrame(animate);
1027
- const delta = clock.getDelta();
1247
+ timer.update();
1248
+ const delta = timer.getDelta();
1028
1249
  animationTimeRef.current += delta;
1029
1250
  if (vrmRef.current) {
1030
- vrmRef.current.update(delta);
1251
+ try {
1252
+ vrmRef.current.update(delta);
1253
+ }
1254
+ catch { }
1255
+ if (featuresRef.current?.springBones === false || springSettleFramesRef.current > 0) {
1256
+ vrmRef.current.springBoneManager?.reset();
1257
+ if (springSettleFramesRef.current > 0)
1258
+ springSettleFramesRef.current--;
1259
+ }
1260
+ if (mixerRef.current)
1261
+ mixerRef.current.update(delta);
1031
1262
  const humanoid = vrmRef.current.humanoid;
1032
- if (humanoid) {
1033
- const time = animationTimeRef.current;
1034
- const breathCycle = time * 1.2;
1035
- const breathIntensity = 0.03;
1036
- const idleSway = time * 0.5;
1037
- const swayIntensity = 0.02;
1038
- const headBob = time * 0.7;
1039
- const headBobIntensity = 0.015;
1040
- const spineBone = humanoid.getNormalizedBoneNode(threeVrm.VRMHumanBoneName.Spine);
1041
- if (spineBone) {
1042
- spineBone.rotation.x = Math.sin(breathCycle) * breathIntensity;
1043
- spineBone.rotation.z = Math.sin(idleSway) * swayIntensity * 0.3;
1263
+ if (featuresRef.current?.idleAnimation !== false && humanoid && !clipActionRef.current) {
1264
+ const t = animationTimeRef.current, br = t * 1.2, sw = t * 0.5, hb = t * 0.7;
1265
+ const b = (n) => humanoid.getNormalizedBoneNode(n);
1266
+ const spine = b(threeVrm.VRMHumanBoneName.Spine);
1267
+ if (spine) {
1268
+ spine.rotation.x = Math.sin(br) * 0.03;
1269
+ spine.rotation.z = Math.sin(sw) * 0.006;
1270
+ }
1271
+ const chest = b(threeVrm.VRMHumanBoneName.Chest);
1272
+ if (chest) {
1273
+ chest.rotation.x = Math.sin(br + 0.3) * 0.015;
1274
+ chest.rotation.z = Math.sin(sw + 0.5) * 0.004;
1044
1275
  }
1045
- const chestBone = humanoid.getNormalizedBoneNode(threeVrm.VRMHumanBoneName.Chest);
1046
- if (chestBone) {
1047
- chestBone.rotation.x = Math.sin(breathCycle + 0.3) * breathIntensity * 0.5;
1048
- chestBone.rotation.z = Math.sin(idleSway + 0.5) * swayIntensity * 0.2;
1276
+ const neck = b(threeVrm.VRMHumanBoneName.Neck);
1277
+ if (neck) {
1278
+ neck.rotation.x = Math.sin(hb) * 0.015;
1279
+ neck.rotation.y = Math.sin(sw * 0.7) * 0.01;
1049
1280
  }
1050
- const neckBone = humanoid.getNormalizedBoneNode(threeVrm.VRMHumanBoneName.Neck);
1051
- if (neckBone) {
1052
- neckBone.rotation.x = Math.sin(headBob) * headBobIntensity;
1053
- neckBone.rotation.y = Math.sin(idleSway * 0.7) * swayIntensity * 0.5;
1281
+ const head = b(threeVrm.VRMHumanBoneName.Head);
1282
+ if (head) {
1283
+ head.rotation.x = Math.sin(hb + 0.5) * 0.006;
1284
+ head.rotation.y = Math.sin(sw * 0.6 + 0.8) * 0.006;
1054
1285
  }
1055
- const headBone = humanoid.getNormalizedBoneNode(threeVrm.VRMHumanBoneName.Head);
1056
- if (headBone) {
1057
- headBone.rotation.x = Math.sin(headBob + 0.5) * headBobIntensity * 0.4;
1058
- headBone.rotation.y = Math.sin(idleSway * 0.6 + 0.8) * swayIntensity * 0.3;
1286
+ const lSh = b(threeVrm.VRMHumanBoneName.LeftShoulder), rSh = b(threeVrm.VRMHumanBoneName.RightShoulder);
1287
+ if (lSh)
1288
+ lSh.rotation.z = Math.sin(br) * 0.006;
1289
+ if (rSh)
1290
+ rSh.rotation.z = -Math.sin(br) * 0.006;
1291
+ const lArm = b(threeVrm.VRMHumanBoneName.LeftUpperArm), rArm = b(threeVrm.VRMHumanBoneName.RightUpperArm);
1292
+ if (lArm)
1293
+ lArm.rotation.z = -1 + Math.sin(sw * 0.4) * 0.02;
1294
+ if (rArm)
1295
+ rArm.rotation.z = 1.0 + Math.sin(sw * 0.4 + 0.5) * 0.02;
1296
+ }
1297
+ if (featuresRef.current?.microExpressions !== false && vrmRef.current.expressionManager && !isA2FActiveRef.current &&
1298
+ !Object.keys(exprOverridesRef.current ?? {}).length) {
1299
+ const idle = idleRef.current, t = animationTimeRef.current;
1300
+ let bv = 0;
1301
+ if (idle.blinkPhase === 0 && t >= idle.blinkNext) {
1302
+ idle.blinkPhase = 1;
1303
+ idle.blinkStart = t;
1059
1304
  }
1060
- const leftShoulder = humanoid.getNormalizedBoneNode(threeVrm.VRMHumanBoneName.LeftShoulder);
1061
- const rightShoulder = humanoid.getNormalizedBoneNode(threeVrm.VRMHumanBoneName.RightShoulder);
1062
- if (leftShoulder) {
1063
- leftShoulder.rotation.z = Math.sin(breathCycle) * breathIntensity * 0.2;
1305
+ if (idle.blinkPhase === 1) {
1306
+ bv = Math.min((t - idle.blinkStart) / 0.15, 1);
1307
+ if (bv >= 1) {
1308
+ idle.blinkPhase = 2;
1309
+ idle.blinkStart = t;
1310
+ }
1064
1311
  }
1065
- if (rightShoulder) {
1066
- rightShoulder.rotation.z = -Math.sin(breathCycle) * breathIntensity * 0.2;
1312
+ else if (idle.blinkPhase === 2) {
1313
+ bv = 1 - Math.min((t - idle.blinkStart) / 0.15, 1);
1314
+ if (bv <= 0) {
1315
+ idle.blinkPhase = 0;
1316
+ idle.blinkNext = t + 2 + Math.random() * 4;
1317
+ }
1067
1318
  }
1068
- const leftUpperArm = humanoid.getNormalizedBoneNode(threeVrm.VRMHumanBoneName.LeftUpperArm);
1069
- const rightUpperArm = humanoid.getNormalizedBoneNode(threeVrm.VRMHumanBoneName.RightUpperArm);
1070
- if (leftUpperArm) {
1071
- leftUpperArm.rotation.z = -1 + Math.sin(idleSway * 0.4) * 0.02;
1319
+ if (t >= idle.microNext) {
1320
+ idle.happyTarget = Math.random() * 0.1;
1321
+ idle.browTarget = Math.random() * 0.08;
1322
+ idle.cheekTarget = Math.random() * 0.05;
1323
+ idle.microNext = t + 3 + Math.random() * 5;
1072
1324
  }
1073
- if (rightUpperArm) {
1074
- rightUpperArm.rotation.z = 1.0 + Math.sin(idleSway * 0.4 + 0.5) * 0.02;
1325
+ idle.happy += (idle.happyTarget - idle.happy) * 0.02;
1326
+ idle.browInner += (idle.browTarget - idle.browInner) * 0.02;
1327
+ idle.cheek += (idle.cheekTarget - idle.cheek) * 0.02;
1328
+ const cursor = cursorRef.current;
1329
+ if (cursorFollowRef.current && cursor.active) {
1330
+ idle.eyeTargetX = cursor.x * 0.26; // ±15° horizontal (0.26 rad ≈ 15°)
1331
+ idle.eyeTargetY = cursor.y * 0.17; // ±10° vertical (0.17 rad ≈ 10°)
1332
+ }
1333
+ else if (t >= idle.eyeNext) {
1334
+ idle.eyeTargetX = (Math.random() - 0.5) * 0.06;
1335
+ idle.eyeTargetY = (Math.random() - 0.5) * 0.04;
1336
+ idle.eyeNext = t + 1.5 + Math.random() * 3;
1337
+ }
1338
+ idle.eyeX += (idle.eyeTargetX - idle.eyeX) * 0.06;
1339
+ idle.eyeY += (idle.eyeTargetY - idle.eyeY) * 0.06;
1340
+ const em = vrmRef.current.expressionManager;
1341
+ try {
1342
+ em.setValue('blink', Math.max(bv, 0));
1343
+ }
1344
+ catch { }
1345
+ try {
1346
+ em.setValue('happy', Math.max(idle.happy, 0));
1347
+ }
1348
+ catch { }
1349
+ try {
1350
+ em.update();
1351
+ }
1352
+ catch { }
1353
+ for (const m of idleMorphsRef.current) {
1354
+ if (m.brow !== undefined)
1355
+ m.inf[m.brow] = idle.browInner;
1356
+ if (m.cheekL !== undefined)
1357
+ m.inf[m.cheekL] = idle.cheek;
1358
+ if (m.cheekR !== undefined)
1359
+ m.inf[m.cheekR] = idle.cheek;
1360
+ }
1361
+ if (humanoid) {
1362
+ for (const bn of [threeVrm.VRMHumanBoneName.LeftEye, threeVrm.VRMHumanBoneName.RightEye]) {
1363
+ const eye = humanoid.getNormalizedBoneNode(bn);
1364
+ if (eye) {
1365
+ eye.rotation.x = idle.eyeY;
1366
+ eye.rotation.y = idle.eyeX;
1367
+ }
1368
+ }
1075
1369
  }
1076
1370
  }
1077
1371
  }
1078
- renderer.render(scene, camera);
1372
+ // Smooth lerp for spherical orbit (azimuth + elevation)
1373
+ const orbit = orbitRef.current;
1374
+ const elevDiff = orbit.targetElevation - orbit.elevation;
1375
+ const angleDiff = orbit.targetAngle - orbit.angle;
1376
+ if (Math.abs(elevDiff) > 0.0005 || Math.abs(angleDiff) > 0.0005) {
1377
+ orbit.elevation += elevDiff * 0.1;
1378
+ orbit.angle += angleDiff * 0.1;
1379
+ const base = baseCamRef.current;
1380
+ const d = Math.max(0.3, base.distance + orbit.zoomOffset);
1381
+ const cosEl = Math.cos(orbit.elevation);
1382
+ const sinEl = Math.sin(orbit.elevation);
1383
+ camera.position.set(Math.sin(orbit.angle) * cosEl * d, base.height + sinEl * d, Math.cos(orbit.angle) * cosEl * d);
1384
+ camera.lookAt(0, base.targetY, 0);
1385
+ }
1386
+ const pipeline = composerRef.current;
1387
+ const ez = envZoomRef.current;
1388
+ const hasBgZoom = ez && ez !== 1 && scene.background && envMapRef.current;
1389
+ if (hasBgZoom && !pipeline) {
1390
+ const baseFov = camera.fov;
1391
+ const bgScene = bgSceneRef.current;
1392
+ bgScene.backgroundBlurriness = scene.backgroundBlurriness;
1393
+ bgScene.backgroundIntensity = scene.backgroundIntensity ?? 1;
1394
+ camera.fov = baseFov / ez;
1395
+ camera.updateProjectionMatrix();
1396
+ renderer.render(bgScene, camera);
1397
+ camera.fov = baseFov;
1398
+ camera.updateProjectionMatrix();
1399
+ const savedBg = scene.background;
1400
+ scene.background = null;
1401
+ renderer.autoClear = false;
1402
+ renderer.clearDepth();
1403
+ renderer.render(scene, camera);
1404
+ renderer.autoClear = true;
1405
+ scene.background = savedBg;
1406
+ }
1407
+ else if (pipeline && !fpsMonitorRef.current.disabled) {
1408
+ pipeline.composer.render();
1409
+ fpsMonitorRef.current.tick(performance.now());
1410
+ }
1411
+ else {
1412
+ renderer.render(scene, camera);
1413
+ }
1079
1414
  };
1080
1415
  animate();
1081
1416
  return () => {
1082
1417
  cancelled = true;
1083
1418
  cancelAnimationFrame(animationFrameRef.current);
1419
+ clipActionRef.current = null;
1420
+ mixerRef.current = null;
1421
+ composerRef.current?.dispose();
1422
+ composerRef.current = null;
1423
+ envMapRef.current?.dispose();
1424
+ envMapRef.current = null;
1084
1425
  renderer.dispose();
1085
1426
  if (container && renderer.domElement) {
1086
1427
  container.removeChild(renderer.domElement);
1087
1428
  }
1088
1429
  };
1089
- }, [width, height, modelUrl, backgroundColor, maxModelSize, onModelLoad]);
1430
+ }, [width, height, modelUrl, maxModelSize, preserveDrawingBuffer]);
1431
+ react.useEffect(() => {
1432
+ composerRef.current?.dispose();
1433
+ composerRef.current = null;
1434
+ fpsMonitorRef.current.reset();
1435
+ const renderer = rendererRef.current, scene = sceneRef.current, camera = cameraRef.current;
1436
+ const pp = features?.postProcessing ?? postProcessing;
1437
+ if (!renderer || !scene || !camera || !pp)
1438
+ return;
1439
+ const hasAnyEffect = (pp.bloom && pp.bloom > 0) || (pp.ao && pp.ao > 0) || pp.dof;
1440
+ if (!hasAnyEffect)
1441
+ return;
1442
+ composerRef.current = createPostProcessing(renderer, scene, camera, pp, width, height);
1443
+ return () => { composerRef.current?.dispose(); composerRef.current = null; };
1444
+ }, [postProcessing, features, width, height]);
1445
+ // Load/unload .vrma animation clip — crossfade between clips
1446
+ react.useEffect(() => {
1447
+ const vrm = vrmRef.current;
1448
+ const fade = animCrossfadeRef.current;
1449
+ if (!idleAnimationUrl) {
1450
+ const old = clipActionRef.current;
1451
+ if (old) {
1452
+ old.fadeOut(fade);
1453
+ setTimeout(() => {
1454
+ old.stop();
1455
+ if (clipActionRef.current === old)
1456
+ clipActionRef.current = null;
1457
+ if (mixerRef.current && !clipActionRef.current) {
1458
+ mixerRef.current.stopAllAction();
1459
+ mixerRef.current = null;
1460
+ }
1461
+ }, fade * 1000);
1462
+ }
1463
+ return;
1464
+ }
1465
+ if (!vrm)
1466
+ return;
1467
+ let cancelled = false;
1468
+ const loader = new GLTFLoader_js.GLTFLoader();
1469
+ loader.register((parser) => new threeVrmAnimation.VRMAnimationLoaderPlugin(parser));
1470
+ const origWarn = console.warn;
1471
+ console.warn = (...args) => {
1472
+ if (typeof args[0] === 'string' && args[0].includes('specVersion'))
1473
+ return;
1474
+ origWarn.apply(console, args);
1475
+ };
1476
+ loader.load(idleAnimationUrl, (gltf) => {
1477
+ console.warn = origWarn;
1478
+ if (cancelled || !vrmRef.current)
1479
+ return;
1480
+ const animations = gltf.userData.vrmAnimations;
1481
+ if (!animations || animations.length === 0)
1482
+ return;
1483
+ const mixer = mixerRef.current ?? new THREE__namespace.AnimationMixer(vrmRef.current.scene);
1484
+ mixerRef.current = mixer;
1485
+ const clip = threeVrmAnimation.createVRMAnimationClip(animations[0], vrmRef.current);
1486
+ const newAction = mixer.clipAction(clip);
1487
+ newAction.setLoop(THREE__namespace.LoopPingPong, Infinity);
1488
+ newAction.timeScale = animSpeedRef.current;
1489
+ newAction.setEffectiveWeight(animWeightRef.current);
1490
+ const oldAction = clipActionRef.current;
1491
+ const cf = animCrossfadeRef.current;
1492
+ if (oldAction) {
1493
+ newAction.reset().play();
1494
+ oldAction.crossFadeTo(newAction, cf, true);
1495
+ setTimeout(() => { oldAction.stop(); mixer.uncacheAction(oldAction.getClip()); }, cf * 1000);
1496
+ }
1497
+ else {
1498
+ newAction.reset().fadeIn(cf).play();
1499
+ }
1500
+ clipActionRef.current = newAction;
1501
+ }, () => { }, (err) => {
1502
+ console.warn = origWarn;
1503
+ if (cancelled)
1504
+ return;
1505
+ console.warn('[VRM] Failed to load animation clip:', err);
1506
+ });
1507
+ return () => { cancelled = true; };
1508
+ }, [idleAnimationUrl, loading]);
1509
+ // Update animation speed/weight in real time
1510
+ react.useEffect(() => {
1511
+ if (clipActionRef.current)
1512
+ clipActionRef.current.timeScale = animationSpeed;
1513
+ }, [animationSpeed]);
1514
+ react.useEffect(() => {
1515
+ if (clipActionRef.current)
1516
+ clipActionRef.current.setEffectiveWeight(animationWeight);
1517
+ }, [animationWeight]);
1518
+ // Update background in-place (skip when HDRI active)
1519
+ react.useEffect(() => {
1520
+ if (sceneRef.current && !environmentUrl) {
1521
+ sceneRef.current.background = backgroundColor === null ? null : new THREE__namespace.Color(backgroundColor ?? 0xffffff);
1522
+ }
1523
+ }, [backgroundColor, environmentUrl]);
1524
+ // HDRI environment map loading — keep old envMap visible until new one is ready
1525
+ react.useEffect(() => {
1526
+ const scene = sceneRef.current, renderer = rendererRef.current;
1527
+ if (!scene || !renderer || !environmentUrl) {
1528
+ envMapRef.current?.dispose();
1529
+ envMapRef.current = null;
1530
+ if (scene) {
1531
+ scene.environment = null;
1532
+ scene.background = null;
1533
+ scene.backgroundBlurriness = 0;
1534
+ scene.environmentIntensity = 1;
1535
+ }
1536
+ return;
1537
+ }
1538
+ let cancelled = false;
1539
+ const prev = envMapRef.current;
1540
+ loadEnvironment(renderer, environmentUrl).then(r => {
1541
+ if (cancelled) {
1542
+ r.dispose();
1543
+ return;
1544
+ }
1545
+ prev?.dispose();
1546
+ scene.environment = r.envMap;
1547
+ scene.background = r.envMap;
1548
+ scene.backgroundBlurriness = environmentBlur ?? 0;
1549
+ scene.environmentIntensity = environmentIntensity ?? 1;
1550
+ scene.backgroundIntensity = environmentIntensity ?? 1;
1551
+ bgSceneRef.current.background = r.envMap;
1552
+ envMapRef.current = { dispose: r.dispose, envMap: r.envMap };
1553
+ onEnvLoadRef.current?.();
1554
+ }).catch(e => console.warn('[VRM] HDRI load failed:', e));
1555
+ return () => { cancelled = true; };
1556
+ }, [environmentUrl]);
1557
+ // Update HDRI intensity/blur without reloading
1558
+ react.useEffect(() => {
1559
+ if (!sceneRef.current || !envMapRef.current)
1560
+ return;
1561
+ sceneRef.current.backgroundBlurriness = environmentBlur ?? 0;
1562
+ sceneRef.current.environmentIntensity = environmentIntensity ?? 1;
1563
+ sceneRef.current.backgroundIntensity = environmentIntensity ?? 1;
1564
+ }, [environmentIntensity, environmentBlur]);
1565
+ // Update camera base values and apply orbit
1566
+ react.useEffect(() => {
1567
+ const pos = cameraPosition ?? [0, 1.4, 1.2];
1568
+ const target = cameraTarget ?? [0, 1.3, 0];
1569
+ baseCamRef.current = { distance: pos[2], height: pos[1], targetY: target[1] };
1570
+ if (orbitAnglePropRef.current === undefined) {
1571
+ orbitRef.current.angle = 0;
1572
+ orbitRef.current.targetAngle = 0;
1573
+ }
1574
+ orbitRef.current.zoomOffset = 0;
1575
+ if (orbitElevation === undefined) {
1576
+ orbitRef.current.elevation = 0;
1577
+ orbitRef.current.targetElevation = 0;
1578
+ }
1579
+ const camera = cameraRef.current;
1580
+ if (!camera)
1581
+ return;
1582
+ const d = baseCamRef.current.distance;
1583
+ camera.position.set(0, baseCamRef.current.height, d);
1584
+ camera.lookAt(0, baseCamRef.current.targetY, 0);
1585
+ if (cameraFov !== undefined && camera.fov !== cameraFov) {
1586
+ camera.fov = cameraFov;
1587
+ camera.updateProjectionMatrix();
1588
+ }
1589
+ }, [cameraPosition, cameraTarget, cameraFov]);
1590
+ // Prop-driven orbit angle
1591
+ react.useEffect(() => {
1592
+ if (orbitAngle !== undefined) {
1593
+ orbitRef.current.targetAngle = orbitAngle;
1594
+ }
1595
+ }, [orbitAngle]);
1596
+ // Prop-driven orbit elevation
1597
+ react.useEffect(() => {
1598
+ if (orbitElevation !== undefined) {
1599
+ orbitRef.current.targetElevation = orbitElevation;
1600
+ }
1601
+ }, [orbitElevation]);
1602
+ // Prop-driven avatar rotation
1603
+ react.useEffect(() => {
1604
+ if (avatarRotation !== undefined && vrmRef.current) {
1605
+ vrmRef.current.scene.rotation.y = avatarRotation;
1606
+ }
1607
+ }, [avatarRotation, loading]);
1608
+ // Mouse orbit (horizontal only) + scroll zoom
1609
+ react.useEffect(() => {
1610
+ const container = containerRef.current;
1611
+ if (!container)
1612
+ return;
1613
+ const applyCameraOrbit = () => {
1614
+ const camera = cameraRef.current;
1615
+ if (!camera)
1616
+ return;
1617
+ const base = baseCamRef.current;
1618
+ const orbit = orbitRef.current;
1619
+ const d = Math.max(0.3, base.distance + orbit.zoomOffset);
1620
+ const cosEl = Math.cos(orbit.elevation);
1621
+ const sinEl = Math.sin(orbit.elevation);
1622
+ camera.position.set(Math.sin(orbit.angle) * cosEl * d, base.height + sinEl * d, Math.cos(orbit.angle) * cosEl * d);
1623
+ camera.lookAt(0, base.targetY, 0);
1624
+ };
1625
+ const onWheel = (e) => {
1626
+ e.preventDefault();
1627
+ orbitRef.current.zoomOffset += e.deltaY * 0.002;
1628
+ applyCameraOrbit();
1629
+ };
1630
+ const onMouseDown = (e) => {
1631
+ orbitRef.current.dragging = true;
1632
+ orbitRef.current.lastX = e.clientX;
1633
+ orbitRef.current.lastY = e.clientY;
1634
+ };
1635
+ const onMouseMove = (e) => {
1636
+ if (!orbitRef.current.dragging)
1637
+ return;
1638
+ const dx = e.clientX - orbitRef.current.lastX;
1639
+ const dy = e.clientY - orbitRef.current.lastY;
1640
+ orbitRef.current.lastX = e.clientX;
1641
+ orbitRef.current.lastY = e.clientY;
1642
+ orbitRef.current.angle -= dx * 0.005;
1643
+ orbitRef.current.targetAngle = orbitRef.current.angle;
1644
+ orbitRef.current.elevation = Math.max(-Math.PI / 3, Math.min(Math.PI / 3, orbitRef.current.elevation + dy * 0.005));
1645
+ orbitRef.current.targetElevation = orbitRef.current.elevation;
1646
+ applyCameraOrbit();
1647
+ onOrbitChangeRef.current?.(orbitRef.current.angle, orbitRef.current.elevation);
1648
+ };
1649
+ const onMouseUp = () => {
1650
+ orbitRef.current.dragging = false;
1651
+ };
1652
+ const onKeyDown = (e) => {
1653
+ const o = orbitRef.current;
1654
+ if (e.key === 'ArrowUp') {
1655
+ e.preventDefault();
1656
+ o.targetElevation = Math.min(o.targetElevation + 0.1, Math.PI / 3);
1657
+ }
1658
+ else if (e.key === 'ArrowDown') {
1659
+ e.preventDefault();
1660
+ o.targetElevation = Math.max(o.targetElevation - 0.1, -Math.PI / 3);
1661
+ }
1662
+ else if (e.key === 'ArrowLeft') {
1663
+ e.preventDefault();
1664
+ o.targetAngle += 0.15;
1665
+ }
1666
+ else if (e.key === 'ArrowRight') {
1667
+ e.preventDefault();
1668
+ o.targetAngle -= 0.15;
1669
+ }
1670
+ onOrbitChangeRef.current?.(o.targetAngle, o.targetElevation);
1671
+ };
1672
+ const onCursorMove = (e) => {
1673
+ const rect = container.getBoundingClientRect();
1674
+ cursorRef.current.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
1675
+ cursorRef.current.y = ((e.clientY - rect.top) / rect.height) * 2 - 1;
1676
+ cursorRef.current.active = true;
1677
+ };
1678
+ const onCursorLeave = () => { cursorRef.current.active = false; };
1679
+ container.addEventListener('wheel', onWheel, { passive: false });
1680
+ container.addEventListener('mousedown', onMouseDown);
1681
+ container.addEventListener('mousemove', onCursorMove);
1682
+ container.addEventListener('mouseleave', onCursorLeave);
1683
+ window.addEventListener('mousemove', onMouseMove);
1684
+ window.addEventListener('mouseup', onMouseUp);
1685
+ window.addEventListener('keydown', onKeyDown);
1686
+ return () => {
1687
+ container.removeEventListener('wheel', onWheel);
1688
+ container.removeEventListener('mousedown', onMouseDown);
1689
+ container.removeEventListener('mousemove', onCursorMove);
1690
+ container.removeEventListener('mouseleave', onCursorLeave);
1691
+ window.removeEventListener('mousemove', onMouseMove);
1692
+ window.removeEventListener('mouseup', onMouseUp);
1693
+ window.removeEventListener('keydown', onKeyDown);
1694
+ };
1695
+ }, []);
1696
+ // Update lighting in-place
1697
+ react.useEffect(() => {
1698
+ if (!lightsRef.current)
1699
+ return;
1700
+ const intensity = lightIntensity ?? 1;
1701
+ lightsRef.current.ambient.intensity = 1.0 * intensity;
1702
+ lightsRef.current.directional.intensity = 1.2 * intensity;
1703
+ lightsRef.current.back.intensity = 0.4 * intensity;
1704
+ }, [lightIntensity]);
1090
1705
  react.useEffect(() => {
1706
+ isA2FActiveRef.current = blendshapes.some(v => v !== 0);
1091
1707
  if (vrmRef.current && blendshapes.length > 0) {
1092
1708
  applyBlendshapes(vrmRef.current, blendshapes);
1093
1709
  }
1094
1710
  }, [blendshapes, applyBlendshapes]);
1711
+ react.useEffect(() => {
1712
+ const vrm = vrmRef.current;
1713
+ if (!vrm?.expressionManager)
1714
+ return;
1715
+ const hasLiveData = blendshapes.some(v => v !== 0);
1716
+ if (hasLiveData)
1717
+ return;
1718
+ if (features?.expressionPresets === false)
1719
+ return;
1720
+ vrm.expressionManager.expressions.forEach(exp => {
1721
+ vrm.expressionManager.setValue(exp.expressionName, 0);
1722
+ });
1723
+ if (expressionOverrides) {
1724
+ for (const [name, value] of Object.entries(expressionOverrides)) {
1725
+ try {
1726
+ vrm.expressionManager.setValue(name, Math.min(Math.max(value, 0), 1));
1727
+ }
1728
+ catch { /* expression doesn't exist on this model */ }
1729
+ }
1730
+ }
1731
+ try {
1732
+ vrm.expressionManager.update();
1733
+ }
1734
+ catch { }
1735
+ }, [expressionOverrides, blendshapes]);
1095
1736
  return (jsxRuntime.jsxs("div", { className: "relative", style: { width, height }, children: [jsxRuntime.jsx("div", { ref: containerRef, className: "rounded-lg overflow-hidden" }), loading && (jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-gray-800 rounded-lg", children: jsxRuntime.jsx("div", { className: "text-white text-sm", children: "Loading VRM avatar..." }) })), error && (jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-red-900/50 rounded-lg", children: jsxRuntime.jsx("div", { className: "text-white text-sm", children: error }) }))] }));
1096
1737
  }
1097
1738
 
1098
1739
  exports.VRMAvatar = VRMAvatar;
1740
+ exports.VROID_BLENDSHAPE_MAP = VROID_BLENDSHAPE_MAP;
1099
1741
  exports.useAStackCSR = useAStackCSR;