@memori.ai/memori-react 7.6.0 → 7.7.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/components/Avatar/Avatar.js +2 -2
  3. package/dist/components/Avatar/Avatar.js.map +1 -1
  4. package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.d.ts +4 -3
  5. package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js +10 -6
  6. package/dist/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js.map +1 -1
  7. package/dist/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.d.ts +11 -17
  8. package/dist/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js +128 -104
  9. package/dist/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js.map +1 -1
  10. package/dist/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.d.ts +1 -4
  11. package/dist/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js +2 -4
  12. package/dist/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js.map +1 -1
  13. package/dist/components/Avatar/AvatarView/index.d.ts +5 -3
  14. package/dist/components/Avatar/AvatarView/index.js +2 -2
  15. package/dist/components/Avatar/AvatarView/index.js.map +1 -1
  16. package/dist/components/MemoriWidget/MemoriWidget.js +117 -118
  17. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  18. package/dist/components/layouts/HiddenChat.js +3 -4
  19. package/dist/components/layouts/HiddenChat.js.map +1 -1
  20. package/dist/components/layouts/ZoomedFullBody.d.ts +2 -2
  21. package/dist/components/layouts/ZoomedFullBody.js +11 -2
  22. package/dist/components/layouts/ZoomedFullBody.js.map +1 -1
  23. package/dist/components/layouts/hidden-chat.css +23 -23
  24. package/dist/components/layouts/zoomed-full-body.css +16 -0
  25. package/dist/context/visemeContext.d.ts +8 -15
  26. package/dist/context/visemeContext.js +64 -166
  27. package/dist/context/visemeContext.js.map +1 -1
  28. package/dist/helpers/translations.js +10 -2
  29. package/dist/helpers/translations.js.map +1 -1
  30. package/dist/helpers/utils.js +5 -6
  31. package/dist/helpers/utils.js.map +1 -1
  32. package/dist/styles.css +1 -0
  33. package/esm/components/Avatar/Avatar.js +2 -2
  34. package/esm/components/Avatar/Avatar.js.map +1 -1
  35. package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.d.ts +4 -3
  36. package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js +10 -6
  37. package/esm/components/Avatar/AvatarView/AvatarComponent/avatarComponent.js.map +1 -1
  38. package/esm/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.d.ts +11 -17
  39. package/esm/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js +131 -107
  40. package/esm/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.js.map +1 -1
  41. package/esm/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.d.ts +1 -4
  42. package/esm/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js +2 -4
  43. package/esm/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.js.map +1 -1
  44. package/esm/components/Avatar/AvatarView/index.d.ts +5 -3
  45. package/esm/components/Avatar/AvatarView/index.js +2 -2
  46. package/esm/components/Avatar/AvatarView/index.js.map +1 -1
  47. package/esm/components/MemoriWidget/MemoriWidget.js +117 -118
  48. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  49. package/esm/components/layouts/HiddenChat.js +3 -4
  50. package/esm/components/layouts/HiddenChat.js.map +1 -1
  51. package/esm/components/layouts/ZoomedFullBody.d.ts +2 -2
  52. package/esm/components/layouts/ZoomedFullBody.js +11 -2
  53. package/esm/components/layouts/ZoomedFullBody.js.map +1 -1
  54. package/esm/components/layouts/hidden-chat.css +23 -23
  55. package/esm/components/layouts/zoomed-full-body.css +16 -0
  56. package/esm/context/visemeContext.d.ts +8 -15
  57. package/esm/context/visemeContext.js +65 -167
  58. package/esm/context/visemeContext.js.map +1 -1
  59. package/esm/helpers/translations.js +10 -2
  60. package/esm/helpers/translations.js.map +1 -1
  61. package/esm/helpers/utils.js +5 -6
  62. package/esm/helpers/utils.js.map +1 -1
  63. package/esm/styles.css +1 -0
  64. package/package.json +1 -1
  65. package/src/components/Avatar/Avatar.stories.tsx +7 -5
  66. package/src/components/Avatar/Avatar.tsx +3 -5
  67. package/src/components/Avatar/AvatarView/AvatarComponent/avatarComponent.tsx +20 -19
  68. package/src/components/Avatar/AvatarView/AvatarComponent/components/fullbodyAvatar.tsx +206 -140
  69. package/src/components/Avatar/AvatarView/AvatarComponent/components/halfbodyAvatar.tsx +1 -7
  70. package/src/components/Avatar/AvatarView/AvatarView.stories.tsx +36 -24
  71. package/src/components/Avatar/AvatarView/index.tsx +3 -8
  72. package/src/components/MemoriWidget/MemoriWidget.tsx +140 -160
  73. package/src/components/layouts/HiddenChat.tsx +13 -14
  74. package/src/components/layouts/ZoomedFullBody.tsx +38 -29
  75. package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +12 -12
  76. package/src/components/layouts/__snapshots__/ZoomedFullBody.test.tsx.snap +25 -21
  77. package/src/components/layouts/hidden-chat.css +23 -23
  78. package/src/components/layouts/zoomed-full-body.css +16 -0
  79. package/src/context/visemeContext.tsx +90 -260
  80. package/src/helpers/translations.ts +11 -8
  81. package/src/helpers/utils.ts +9 -8
  82. package/src/styles.css +1 -0
@@ -14,9 +14,7 @@ interface Props {
14
14
  speaking: boolean;
15
15
  isZoomed: boolean;
16
16
  chatEmission: any;
17
- setMeshRef: any;
18
- clearVisemes: () => void;
19
- setEmotion: (emotion: string) => void;
17
+ updateCurrentViseme: (currentTime: number) => { name: string; weight: number } | null;
20
18
  }
21
19
 
22
20
  interface BaseAction {
@@ -50,9 +48,8 @@ const baseActions: Record<string, BaseAction> = {
50
48
  Loading3: { weight: 0 },
51
49
  };
52
50
 
51
+
53
52
  export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
54
- setMeshRef,
55
- clearVisemes,
56
53
  chatEmission,
57
54
  showControls,
58
55
  animation,
@@ -64,7 +61,7 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
64
61
  halfBody,
65
62
  loading,
66
63
  isZoomed,
67
- setEmotion,
64
+ updateCurrentViseme,
68
65
  }) => {
69
66
  const [currentBaseAction, setCurrentBaseAction] = useState({
70
67
  action: animation || 'Idle1',
@@ -77,6 +74,9 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
77
74
  const [morphTargetDictionary, setMorphTargetDictionary] = useState<{
78
75
  [key: string]: number;
79
76
  }>({});
77
+ const [emotionMorphTargets, setEmotionMorphTargets] = useState<{
78
+ [key: string]: number;
79
+ }>({});
80
80
 
81
81
  const [timeScale, setTimeScale] = useState(0.8);
82
82
 
@@ -93,7 +93,7 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
93
93
 
94
94
  //remove the last character from the action
95
95
  const newEmotion = action.slice(0, -1);
96
- setEmotion(newEmotion);
96
+ // setEmotion(newEmotion);
97
97
 
98
98
  const defaultEmotions = Object.keys(emotionMap).reduce((acc, key) => {
99
99
  acc[key] = 0;
@@ -105,9 +105,8 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
105
105
  const emotionValues =
106
106
  emotion === 'default' ? defaultEmotions : emotionMap[emotion];
107
107
 
108
- setMorphTargetInfluences(prevInfluences => ({
109
- ...prevInfluences,
110
- ...defaultEmotions,
108
+ setEmotionMorphTargets(prevEmotions => ({
109
+ ...prevEmotions,
111
110
  ...emotionValues,
112
111
  }));
113
112
  }, []);
@@ -162,6 +161,11 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
162
161
  const emotion = `${outputContent}${randomNumber}`;
163
162
 
164
163
  onBaseActionChange(emotion);
164
+ } else {
165
+ //Set a random idle animation
166
+ const randomNumber = Math.floor(Math.random() * 5) + 1;
167
+ const animation = `Idle${randomNumber === 3 ? 4 : randomNumber}`;
168
+ onBaseActionChange(animation);
165
169
  }
166
170
  }, [chatEmission]);
167
171
 
@@ -174,6 +178,7 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
174
178
  }
175
179
  }, [loading]);
176
180
 
181
+
177
182
  return (
178
183
  <>
179
184
  {showControls && (
@@ -191,13 +196,11 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
191
196
  {halfBody ? (
192
197
  <HalfBodyAvatar
193
198
  url={url}
194
- setMeshRef={setMeshRef}
195
- setMorphTargetInfluences={setMorphTargetInfluences}
196
199
  headMovement={headMovement}
197
200
  speaking={speaking}
198
201
  eyeBlink={eyeBlink}
199
202
  morphTargetInfluences={morphTargetInfluences}
200
- clearVisemes={clearVisemes}
203
+ setMorphTargetInfluences={setMorphTargetInfluences}
201
204
  setMorphTargetDictionary={setMorphTargetDictionary}
202
205
  />
203
206
  ) : (
@@ -205,16 +208,14 @@ export const AvatarView: React.FC<Props & { halfBody: boolean }> = ({
205
208
  url={url}
206
209
  sex={sex}
207
210
  eyeBlink={eyeBlink}
208
- speaking={speaking}
209
211
  currentBaseAction={currentBaseAction}
210
212
  timeScale={timeScale}
211
- setMorphTargetInfluences={setMorphTargetInfluences}
212
- setMorphTargetDictionary={setMorphTargetDictionary}
213
213
  morphTargetInfluences={morphTargetInfluences}
214
- morphTargetDictionary={morphTargetDictionary}
215
214
  isZoomed={isZoomed}
216
- setMeshRef={setMeshRef}
217
- clearVisemes={clearVisemes}
215
+ updateCurrentViseme={updateCurrentViseme}
216
+ setMorphTargetDictionary={setMorphTargetDictionary}
217
+ setMorphTargetInfluences={setMorphTargetInfluences}
218
+ emotionMorphTargets={emotionMorphTargets}
218
219
  />
219
220
  )}
220
221
  </>
@@ -1,21 +1,17 @@
1
- import React, { useEffect, useRef, useState, useCallback } from 'react';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  Vector3,
4
4
  Euler,
5
5
  AnimationMixer,
6
6
  SkinnedMesh,
7
7
  Object3D,
8
+ MathUtils,
8
9
  AnimationAction,
10
+ LoopOnce,
9
11
  } from 'three';
10
12
  import { useAnimations, useGLTF } from '@react-three/drei';
11
- import { useGraph, dispose, useFrame } from '@react-three/fiber';
13
+ import { useGraph, useFrame } from '@react-three/fiber';
12
14
  import { correctMaterials, isSkinnedMesh } from '../../../../../helpers/utils';
13
- import { useAvatarBlink } from '../../utils/useEyeBlink';
14
- import { useViseme } from '../../../../../context/visemeContext';
15
-
16
- const lerp = (start: number, end: number, alpha: number): number => {
17
- return start * (1 - alpha) + end * alpha;
18
- };
19
15
 
20
16
  interface FullbodyAvatarProps {
21
17
  url: string;
@@ -26,16 +22,21 @@ interface FullbodyAvatarProps {
26
22
  weight: number;
27
23
  };
28
24
  timeScale: number;
29
- loading?: boolean;
30
- speaking?: boolean;
31
25
  isZoomed?: boolean;
32
- setMorphTargetInfluences: (influences: { [key: string]: number }) => void;
33
- setMorphTargetDictionary: (dictionary: { [key: string]: number }) => void;
34
- morphTargetInfluences: { [key: string]: number };
35
- morphTargetDictionary: { [key: string]: number };
36
- setMeshRef: any;
37
26
  eyeBlink?: boolean;
38
- clearVisemes: () => void;
27
+ updateCurrentViseme: (
28
+ currentTime: number
29
+ ) => { name: string; weight: number } | null;
30
+ smoothMorphTarget?: boolean;
31
+ morphTargetSmoothing?: number;
32
+ morphTargetInfluences: Record<string, number>;
33
+ setMorphTargetDictionary: (
34
+ morphTargetDictionary: Record<string, number>
35
+ ) => void;
36
+ setMorphTargetInfluences: (
37
+ morphTargetInfluences: Record<string, number>
38
+ ) => void;
39
+ emotionMorphTargets: Record<string, number>;
39
40
  }
40
41
 
41
42
  const AVATAR_POSITION = new Vector3(0, -1, 0);
@@ -43,11 +44,19 @@ const AVATAR_ROTATION = new Euler(0.175, 0, 0);
43
44
  const AVATAR_POSITION_ZOOMED = new Vector3(0, -1.45, 0);
44
45
 
45
46
  const ANIMATION_URLS = {
46
- MALE: 'https://assets.memori.ai/api/v2/asset/1c350a21-97d8-4add-82cc-9dc10767a26b.glb',
47
+ MALE: 'https://assets.memori.ai/api/v2/asset/2c5e88a4-cf62-408b-9ef0-518b099dfcb2.glb',
47
48
  FEMALE:
48
- 'https://assets.memori.ai/api/v2/asset/c2b07166-de10-4c66-918b-7b7cd380cca7.glb',
49
+ 'https://assets.memori.ai/api/v2/asset/0e49aa5d-f757-4292-a170-d843c2839a41.glb',
50
+ };
51
+
52
+ // Blink configuration
53
+ const BLINK_CONFIG = {
54
+ minInterval: 1000,
55
+ maxInterval: 5000,
56
+ blinkDuration: 150,
49
57
  };
50
- const ANIMATION_DURATION = 3000; // Duration in milliseconds for non-idle animations
58
+
59
+ const EMOTION_TRANSITION_SPEED = 0.1; // Adjust this value to control emotion transition speed
51
60
 
52
61
  export default function FullbodyAvatar({
53
62
  url,
@@ -56,85 +65,65 @@ export default function FullbodyAvatar({
56
65
  currentBaseAction,
57
66
  timeScale,
58
67
  isZoomed,
59
- setMorphTargetInfluences,
60
- setMorphTargetDictionary,
61
- morphTargetInfluences,
62
68
  eyeBlink,
63
- setMeshRef,
64
- clearVisemes,
69
+ morphTargetSmoothing = 0.5,
70
+ updateCurrentViseme,
71
+ setMorphTargetDictionary,
72
+ setMorphTargetInfluences,
73
+ emotionMorphTargets,
65
74
  }: FullbodyAvatarProps) {
66
75
  const { scene } = useGLTF(url);
67
76
  const { animations } = useGLTF(ANIMATION_URLS[sex]);
68
77
  const { nodes, materials } = useGraph(scene);
69
78
  const { actions } = useAnimations(animations, scene);
70
- const [mixer] = useState(() => new AnimationMixer(scene));
71
79
 
72
- const avatarMeshRef = useRef<SkinnedMesh>();
80
+ const mixer = useRef(new AnimationMixer(scene));
81
+ const headMeshRef = useRef<SkinnedMesh>();
73
82
  const currentActionRef = useRef<AnimationAction | null>(null);
74
- const isTransitioningRef = useRef(false);
75
-
76
- // Blink animation
77
- useAvatarBlink({
78
- enabled: eyeBlink || false,
79
- setMorphTargetInfluences,
80
- config: {
81
- minInterval: 1500,
82
- maxInterval: 4000,
83
- blinkDuration: 120,
84
- },
85
- });
83
+ const [isTransitioningToIdle, setIsTransitioningToIdle] = useState(false);
86
84
 
87
- // Idle animation when emotion animation is finished
88
- const transitionToIdle = useCallback(() => {
89
- if (!actions || isTransitioningRef.current) return;
85
+ // Blink state
86
+ const lastBlinkTime = useRef(0);
87
+ const nextBlinkTime = useRef(0);
88
+ const isBlinking = useRef(false);
89
+ const blinkStartTime = useRef(0);
90
90
 
91
- isTransitioningRef.current = true;
91
+ // Morph targets
92
+ const currentEmotionRef = useRef<Record<string, number>>({});
93
+ const previousEmotionKeysRef = useRef<Set<string>>(new Set());
92
94
 
93
- const finishCurrentAnimation = () => {
94
- if (currentActionRef.current && !currentActionRef.current.paused) {
95
- const remainingTime = (currentActionRef.current.getClip().duration - currentActionRef.current.time) * 1000;
96
- setTimeout(() => {
97
- startIdleAnimation();
98
- }, remainingTime);
99
- } else {
100
- startIdleAnimation();
101
- }
102
- };
103
-
104
- const startIdleAnimation = () => {
105
- const idleAnimations = Object.keys(actions).filter(key =>
106
- key.startsWith('Idle')
107
- );
108
- const randomIdle =
109
- idleAnimations[Math.floor(Math.random() * idleAnimations.length)];
95
+ useEffect(() => {
96
+ correctMaterials(materials);
110
97
 
111
- const idleAction = actions[randomIdle];
112
- const fadeOutDuration = 0.5;
113
- const fadeInDuration = 0.5;
98
+ scene.traverse((object: Object3D) => {
99
+ if (object instanceof SkinnedMesh) {
100
+ if (object.name === 'GBNL__Head' || object.name === 'Wolf3D_Avatar') {
101
+ headMeshRef.current = object;
102
+ if (object.morphTargetDictionary && object.morphTargetInfluences) {
103
+ setMorphTargetDictionary(object.morphTargetDictionary);
114
104
 
115
- if (currentActionRef.current) {
116
- currentActionRef.current.fadeOut(fadeOutDuration);
105
+ const initialInfluences = Object.keys(
106
+ object.morphTargetDictionary
107
+ ).reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
108
+ setMorphTargetInfluences(initialInfluences);
109
+ }
110
+ }
117
111
  }
112
+ });
118
113
 
119
- idleAction?.reset().fadeIn(fadeInDuration).play();
120
- currentActionRef.current = idleAction;
114
+ onLoaded?.();
121
115
 
122
- setTimeout(() => {
123
- isTransitioningRef.current = false;
124
- }, (fadeOutDuration + fadeInDuration) * 1000);
116
+ return () => {
117
+ Object.values(materials).forEach(material => material.dispose());
118
+ Object.values(nodes)
119
+ .filter(isSkinnedMesh)
120
+ .forEach(mesh => mesh.geometry.dispose());
125
121
  };
122
+ }, [materials, nodes, url, onLoaded, scene]);
126
123
 
127
- if (currentActionRef.current && !currentActionRef.current.getClip().name.startsWith('Idle')) {
128
- finishCurrentAnimation();
129
- } else {
130
- startIdleAnimation();
131
- }
132
- }, [actions]);
133
-
134
- // Base animation
124
+ // Handle base animation changes
135
125
  useEffect(() => {
136
- if (!actions || !currentBaseAction.action || isTransitioningRef.current)
137
- return;
126
+ if (!actions || !currentBaseAction.action) return;
138
127
 
139
128
  const newAction = actions[currentBaseAction.action];
140
129
  if (!newAction) {
@@ -147,79 +136,156 @@ export default function FullbodyAvatar({
147
136
  const fadeOutDuration = 0.8;
148
137
  const fadeInDuration = 0.8;
149
138
 
150
- if (!currentBaseAction.action.startsWith('Idle')) {
151
- setTimeout(() => {
152
- transitionToIdle();
153
- }, ANIMATION_DURATION);
154
- }
155
-
156
139
  if (currentActionRef.current) {
157
140
  currentActionRef.current.fadeOut(fadeOutDuration);
158
141
  }
159
-
160
- newAction.timeScale = timeScale;
142
+
143
+ console.log(newAction);
161
144
  newAction.reset().fadeIn(fadeInDuration).play();
162
145
  currentActionRef.current = newAction;
163
- }, [currentBaseAction, timeScale, actions, transitionToIdle]);
164
146
 
165
- // Set up the mesh reference and morph target influences
166
- useEffect(() => {
167
- correctMaterials(materials);
147
+ // Set the time scale for the new action
148
+ newAction.timeScale = timeScale;
168
149
 
169
- scene.traverse((object: Object3D) => {
170
- if (
171
- object instanceof SkinnedMesh &&
172
- (object.name === 'GBNL__Head' || object.name === 'Wolf3D_Avatar')
173
- ) {
174
- avatarMeshRef.current = object;
175
- setMeshRef(object);
176
-
177
- if (object.morphTargetDictionary && object.morphTargetInfluences) {
178
- setMorphTargetDictionary(object.morphTargetDictionary);
179
-
180
- const initialInfluences = Object.keys(
181
- object.morphTargetDictionary
182
- ).reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
183
- setMorphTargetInfluences(initialInfluences);
150
+ // If it's an emotion animation, set it to play once and then transition to idle
151
+ if (
152
+ currentBaseAction.action.startsWith('Gioia') ||
153
+ currentBaseAction.action.startsWith('Rabbia') ||
154
+ currentBaseAction.action.startsWith('Sorpresa') ||
155
+ currentBaseAction.action.startsWith('Timore') ||
156
+ currentBaseAction.action.startsWith('Tristezza')
157
+ ) {
158
+ newAction.setLoop(LoopOnce, 1);
159
+ newAction.clampWhenFinished = true;
160
+ setIsTransitioningToIdle(true);
161
+ }
162
+ }, [actions, currentBaseAction, timeScale]);
163
+
164
+ useFrame(state => {
165
+ if (
166
+ headMeshRef.current &&
167
+ headMeshRef.current.morphTargetDictionary &&
168
+ headMeshRef.current.morphTargetInfluences
169
+ ) {
170
+ const currentTime = state.clock.getElapsedTime() * 1000; // Convert to milliseconds
171
+
172
+ // Handle blinking
173
+ let blinkValue = 0;
174
+ if (eyeBlink) {
175
+ if (currentTime >= nextBlinkTime.current && !isBlinking.current) {
176
+ isBlinking.current = true;
177
+ blinkStartTime.current = currentTime;
178
+ lastBlinkTime.current = currentTime;
179
+ nextBlinkTime.current =
180
+ currentTime +
181
+ Math.random() *
182
+ (BLINK_CONFIG.maxInterval - BLINK_CONFIG.minInterval) +
183
+ BLINK_CONFIG.minInterval;
184
+ }
185
+
186
+ if (isBlinking.current) {
187
+ const blinkProgress =
188
+ (currentTime - blinkStartTime.current) / BLINK_CONFIG.blinkDuration;
189
+ if (blinkProgress <= 0.5) {
190
+ // Eyes closing
191
+ blinkValue = blinkProgress * 2;
192
+ } else if (blinkProgress <= 1) {
193
+ // Eyes opening
194
+ blinkValue = 2 - blinkProgress * 2;
195
+ } else {
196
+ // Blink finished
197
+ isBlinking.current = false;
198
+ blinkValue = 0;
199
+ }
184
200
  }
185
201
  }
186
- });
187
202
 
188
- onLoaded?.();
203
+ const currentViseme = updateCurrentViseme(currentTime / 1000);
189
204
 
190
- return () => {
191
- Object.values(materials).forEach(dispose);
192
- Object.values(nodes).filter(isSkinnedMesh).forEach(dispose);
193
- clearVisemes();
194
- };
195
- }, [
196
- materials,
197
- nodes,
198
- url,
199
- onLoaded,
200
- setMorphTargetDictionary,
201
- setMorphTargetInfluences,
202
- setMeshRef,
203
- clearVisemes,
204
- ]);
205
-
206
- // Update morph target influences
207
- useFrame((_, delta) => {
208
- if (avatarMeshRef.current && avatarMeshRef.current.morphTargetDictionary) {
209
- updateMorphTargetInfluences();
210
- }
211
- mixer.update(delta * 0.001);
212
-
213
- function updateMorphTargetInfluences() {
214
- Object.entries(morphTargetInfluences).forEach(([key, value]) => {
215
- const index = avatarMeshRef.current!.morphTargetDictionary![key];
216
- if (typeof index === 'number' &&
217
- avatarMeshRef.current!.morphTargetInfluences) {
218
- const currentValue = avatarMeshRef.current!.morphTargetInfluences[index];
219
- const smoothValue = lerp(currentValue, value, 0.1);
220
- avatarMeshRef.current!.morphTargetInfluences[index] = smoothValue;
205
+ // Create a set of current emotion keys
206
+ const currentEmotionKeys = new Set(Object.keys(emotionMorphTargets));
207
+
208
+ // Reset old emotion morph targets
209
+ previousEmotionKeysRef.current.forEach(key => {
210
+ if (!currentEmotionKeys.has(key)) {
211
+ const index = headMeshRef.current!.morphTargetDictionary![key];
212
+ if (typeof index === 'number') {
213
+ currentEmotionRef.current[key] = 0;
214
+ if (headMeshRef.current && headMeshRef.current.morphTargetInfluences) {
215
+ headMeshRef.current.morphTargetInfluences[index] = 0;
216
+ }
217
+ }
221
218
  }
222
219
  });
220
+
221
+ // Update morph targets
222
+ Object.entries(headMeshRef.current.morphTargetDictionary).forEach(
223
+ ([key, index]) => {
224
+ if (typeof index === 'number') {
225
+ let targetValue = 0;
226
+
227
+ // Handle emotions (base layer)
228
+ if (Object.prototype.hasOwnProperty.call(emotionMorphTargets, key)) {
229
+ const targetEmotionValue = emotionMorphTargets[key];
230
+ const currentEmotionValue = currentEmotionRef.current[key] || 0;
231
+ const newEmotionValue = MathUtils.lerp(
232
+ currentEmotionValue,
233
+ targetEmotionValue * 2,
234
+ EMOTION_TRANSITION_SPEED
235
+ );
236
+ currentEmotionRef.current[key] = newEmotionValue;
237
+ targetValue += newEmotionValue;
238
+ }
239
+
240
+ // Handle visemes (additive layer)
241
+ if (currentViseme && key === currentViseme.name) {
242
+ targetValue += currentViseme.weight * 1.2; // Amplify the effect
243
+ }
244
+
245
+ // Handle blinking (additive layer, only for 'eyesClosed')
246
+ if (key === 'eyesClosed' && eyeBlink) {
247
+ targetValue += blinkValue;
248
+ }
249
+
250
+ // Clamp the final value between 0 and 1
251
+ targetValue = MathUtils.clamp(targetValue, 0, 1);
252
+
253
+ // Apply smoothing
254
+ if (headMeshRef.current && headMeshRef.current.morphTargetInfluences) {
255
+ headMeshRef.current.morphTargetInfluences[index] = MathUtils.lerp(
256
+ headMeshRef.current.morphTargetInfluences[index],
257
+ targetValue,
258
+ morphTargetSmoothing
259
+ );
260
+ }
261
+ }
262
+ }
263
+ );
264
+
265
+ // Update the set of previous emotion keys for the next frame
266
+ previousEmotionKeysRef.current = currentEmotionKeys;
267
+
268
+ // Handle transition from emotion animation to idle
269
+ if (isTransitioningToIdle && currentActionRef.current) {
270
+ if (
271
+ currentActionRef.current.time >=
272
+ currentActionRef.current.getClip().duration
273
+ ) {
274
+ // Transition to the idle animation
275
+ const idleNumber = Math.floor(Math.random() * 5) + 1; // Randomly choose 1, 2, 3, 4 or 5
276
+ const idleAction = actions[`Idle${idleNumber == 3 ? 4 : idleNumber}`];
277
+
278
+ if (idleAction) {
279
+ currentActionRef.current.fadeOut(0.5);
280
+ idleAction.reset().fadeIn(0.5).play();
281
+ currentActionRef.current = idleAction;
282
+ setIsTransitioningToIdle(false);
283
+ }
284
+ }
285
+ }
286
+
287
+ // Update the animation mixer
288
+ mixer.current.update(0.01); // Fixed delta time for consistent animation speed
223
289
  }
224
290
  });
225
291
 
@@ -231,4 +297,4 @@ export default function FullbodyAvatar({
231
297
  <primitive object={scene} />
232
298
  </group>
233
299
  );
234
- }
300
+ }
@@ -14,8 +14,6 @@ interface HalfBodyAvatarProps {
14
14
  headMovement?: boolean;
15
15
  speaking?: boolean;
16
16
  onLoaded?: () => void;
17
- setMeshRef: (mesh: Object3D) => void;
18
- clearVisemes: () => void;
19
17
  setMorphTargetDictionary: (morphTargetDictionary: any) => void;
20
18
  eyeBlink?: boolean;
21
19
  morphTargetInfluences: any;
@@ -31,9 +29,7 @@ export default function HalfBodyAvatar({
31
29
  setMorphTargetDictionary,
32
30
  headMovement,
33
31
  eyeBlink,
34
- setMeshRef,
35
32
  onLoaded,
36
- clearVisemes,
37
33
  morphTargetInfluences,
38
34
  }: HalfBodyAvatarProps) {
39
35
  const { scene } = useGLTF(url);
@@ -58,7 +54,6 @@ export default function HalfBodyAvatar({
58
54
  // Set mesh reference for the first SkinnedMesh found
59
55
  const firstSkinnedMesh = Object.values(nodes).find(isSkinnedMesh) as SkinnedMesh;
60
56
  if (firstSkinnedMesh) {
61
- setMeshRef(firstSkinnedMesh);
62
57
  avatarMeshRef.current = firstSkinnedMesh;
63
58
  if (firstSkinnedMesh.morphTargetDictionary && firstSkinnedMesh.morphTargetInfluences) {
64
59
  setMorphTargetDictionary(firstSkinnedMesh.morphTargetDictionary);
@@ -77,12 +72,11 @@ export default function HalfBodyAvatar({
77
72
  const disposeObjects = () => {
78
73
  Object.values(materials).forEach(dispose);
79
74
  Object.values(nodes).filter(isSkinnedMesh).forEach(dispose);
80
- clearVisemes();
81
75
  };
82
76
 
83
77
  disposeObjects();
84
78
  };
85
- }, [materials, nodes, url, onLoaded, clearVisemes]);
79
+ }, [materials, nodes, url, onLoaded]);
86
80
 
87
81
  const skinnedMeshes = useMemo(
88
82
  () => Object.values(nodes).filter(isSkinnedMesh),