react-ai-avatar 0.1.0 → 0.1.2
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/README.md +22 -9
- package/dist/lib/GlbArkitAvatar-CIHx_Kop.cjs +1 -0
- package/dist/lib/GlbArkitAvatar-CjfIOQ2P.js +233 -0
- package/dist/lib/VrmAvatar-Dw0gf27E.js +245 -0
- package/dist/lib/VrmAvatar-g_9JGG0m.cjs +1 -0
- package/dist/lib/components/AvatarCaption.d.ts +36 -0
- package/dist/lib/index.cjs +4 -2
- package/dist/lib/index.js +446 -400
- package/dist/lib/lib/captionText.d.ts +33 -0
- package/dist/lib/lib/index.d.ts +4 -0
- package/dist/lib/react-ai-avatar.css +1 -1
- package/dist/lib/vrm.cjs +1 -1
- package/dist/lib/vrm.js +1 -1
- package/package.json +2 -14
- package/dist/lib/GlbArkitAvatar-CcPWCsQV.cjs +0 -1
- package/dist/lib/GlbArkitAvatar-Dm9STiyR.js +0 -232
- package/dist/lib/VrmAvatar-CehRzj0J.js +0 -224
- package/dist/lib/VrmAvatar-D_jr2TOG.cjs +0 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/banner.png" alt="react-ai-avatar — A face for your AI" width="100%" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# react-ai-avatar
|
|
2
6
|
|
|
3
7
|
> A presentational React avatar for realtime LLM voice UIs — **you bring the connection, it brings the face.**
|
|
@@ -24,6 +28,7 @@ One thing, done well, embeddable in a few lines, no backend, MIT. The library ha
|
|
|
24
28
|
- 🦺 **Graceful degradation** — `analyser={null}` while `state="speaking"`? The mouth animates with a synthetic speech-like pattern instead of freezing. Perfect for demos and non-WebRTC apps.
|
|
25
29
|
- ⌨️ **Text-streaming LLMs too** — no audio? Drive the mouth from *token cadence* with `createSpeechActivity()`. A text-only assistant (OpenAI-style `/chat/completions` or `/responses` with `stream: true`) gets a face that visibly tracks the stream — busy while tokens arrive, settling on pauses.
|
|
26
30
|
- 🧠 **A visible `thinking` state** — pulsing thought bubble + upward gaze. Your users *see* the LLM thinking, not just a color change.
|
|
31
|
+
- 🛠️ **A `working` state for tool use** — the fifth state, for agentic UIs. While the LLM runs a tool, the face goes amber and the state pill reads `Working: {tool}` (pass the tool name via the `tool` prop). Your users see *which* tool is running, not just a spinner.
|
|
27
32
|
- 🎨 **Own-design avatar catalog** — `geometric`, `memoji`, `pixelart`, `doodle`: four MIT, CC0-safe SVG presets. No third-party assets, no attribution headaches.
|
|
28
33
|
- 🎲 **DiceBear avatars (`dicebear`)** — generate deterministic [DiceBear](https://www.dicebear.com) avatars client-side, from a curated **CC0-only** style set (still no attribution). Animated with an audio-reactive bounce.
|
|
29
34
|
- 🔌 **Bring your own SVG (`byos`)** — any SVG implementing the small layer contract gets the full animation runtime for free. Your avatar, your license.
|
|
@@ -52,7 +57,7 @@ import 'react-ai-avatar/style.css';
|
|
|
52
57
|
|
|
53
58
|
export default function App() {
|
|
54
59
|
// You resolve this in your app (Gemini, OpenAI Realtime, WebRTC, anything)
|
|
55
|
-
const aiState = 'speaking'; // 'idle' | 'listening' | 'thinking' | 'speaking'
|
|
60
|
+
const aiState = 'speaking'; // 'idle' | 'listening' | 'thinking' | 'speaking' | 'working'
|
|
56
61
|
|
|
57
62
|
return <RealtimeAvatar state={aiState} />;
|
|
58
63
|
}
|
|
@@ -71,7 +76,7 @@ Every default is overridable. Opt into as much as you need:
|
|
|
71
76
|
size={300} // default 280
|
|
72
77
|
variant="geometric" // 'geometric' | 'memoji' | 'pixelart' | 'doodle' | 'dicebear' | 'vrm' | 'glb' | 'byos'
|
|
73
78
|
customization={{ skinColor: '#f5c7a9', hairColor: '#2c2c2c', glasses: true, headphones: true }}
|
|
74
|
-
stateColors={{ idle: '#4b5563', listening: '#3b82f6', thinking: '#8b5cf6', speaking: '#10b981' }}
|
|
79
|
+
stateColors={{ idle: '#4b5563', listening: '#3b82f6', thinking: '#8b5cf6', speaking: '#10b981', working: '#f59e0b' }}
|
|
75
80
|
/>
|
|
76
81
|
```
|
|
77
82
|
|
|
@@ -155,7 +160,8 @@ Optional data attributes: `data-base-x`/`data-base-y` (pupil rest position), `da
|
|
|
155
160
|
|
|
156
161
|
### `<RealtimeAvatar />`
|
|
157
162
|
|
|
158
|
-
- `state` (`'idle' | 'listening' | 'thinking' | 'speaking'`) — required. You resolve it; it is never inferred.
|
|
163
|
+
- `state` (`'idle' | 'listening' | 'thinking' | 'speaking' | 'working'`) — required. You resolve it; it is never inferred. `working` is the tool-use state for agentic UIs (amber).
|
|
164
|
+
- `tool` (`string`) — optional. The name of the tool currently running. While `state="working"`, the state pill reads `Working: {tool}` instead of the generic label.
|
|
159
165
|
- `analyser` (`AnalyserNode | null`) — optional. Drives the mouth from audio. Omitted or `null`, speaking falls back to the synthetic pattern.
|
|
160
166
|
- `streamingText` (`string`) — optional. Declarative mouth driver: pass the accumulated assistant text (e.g. from `useChat`) and the avatar diffs its growth to drive the mouth. Takes precedence over `analyser`. See [Text-streaming LLMs](#text-streaming-llms-no-audio).
|
|
161
167
|
- `speechActivity` (`SpeechActivitySource`) — optional. Imperative token-rate mouth driver, from `createSpeechActivity()`. Takes precedence over both `streamingText` and `analyser` when set.
|
|
@@ -166,8 +172,8 @@ Optional data attributes: `data-base-x`/`data-base-y` (pupil rest position), `da
|
|
|
166
172
|
- `glbUrl` (`string`) — CORS-enabled `.glb` URL with ARKit blendshapes, for `variant="glb"`.
|
|
167
173
|
- `dicebearCollection` (`string`) — DiceBear style id (curated CC0 set), for `variant="dicebear"`.
|
|
168
174
|
- `dicebearSeed` (`string`) — deterministic DiceBear seed, for `variant="dicebear"`.
|
|
169
|
-
- `subtitle` / `thought` (`string`) — optional movie-style
|
|
170
|
-
- `showGlow` / `showStatePill` / `showThought` / `showSubtitle` (`boolean`) — HUD satellites, each `true` by default. Set any to `false` to hide it individually: the reactive glow, the state pill, the thought bubble, and the subtitle respectively.
|
|
175
|
+
- `subtitle` / `thought` (`string`) — optional movie-style caption and a thought bubble. Pass raw text or markdown: both are flattened to spoken prose and rolled to a trailing window internally, so a long streamed reply never overflows or shows raw `**`/tables. For a long assistant reply, keep the full markdown in your chat transcript and pass the same text here for the short live caption.
|
|
176
|
+
- `showGlow` / `showStatePill` / `showThought` / `showSubtitle` (`boolean`) — HUD satellites, each `true` by default. Set any to `false` to hide it individually: the reactive glow, the state pill, the thought bubble, and the subtitle respectively. The built-in subtitle/thought float `absolute` around the face (needs open canvas); inside a constrained card, set `showSubtitle={false}` / `showThought={false}` and render `<AvatarCaption>` / `<AvatarThought>` in your own layout slot instead.
|
|
171
177
|
- `maxMouthOpening`, `mouseTrackingIntensity`, `blinkIntervalMin/Max`, `blinkDuration` — animation tuning.
|
|
172
178
|
- `stateColors`, `stateLabels` — theming; labels are announced via `aria-live`.
|
|
173
179
|
- `customization` — preset colors and accessories (skin, hair, clothing, glasses, headphones…).
|
|
@@ -183,7 +189,10 @@ Everything the runtime uses is exported, so you can compose your own:
|
|
|
183
189
|
- `useStreamingTextActivity(text)` — declarative wrapper: diffs accumulated streaming text into a `SpeechActivitySource` for you (what the `streamingText` prop uses).
|
|
184
190
|
- `useReducedMotion()` — SSR-safe `prefers-reduced-motion` hook.
|
|
185
191
|
- `GeometricAvatar`, `MemojiAvatar`, `PixelArtAvatar`, `DoodleAvatar` — the raw presets.
|
|
192
|
+
- `SquirrelAvatar` — a full branded character (red-squirrel dev face) built on the `#rra-*` contract; the worked `byos` example, shipped so the demos render it from one source. See [`examples/08-character-avatar-squirrel.tsx`](examples/08-character-avatar-squirrel.tsx).
|
|
186
193
|
- `AudioVisualizer` — Siri-style waveform telemetry strip.
|
|
194
|
+
- `AvatarCaption` / `AvatarThought` — host-placed caption + thought widgets. In-flow (not `absolute`), so they fit your own layout slot without overflow; both flatten markdown to spoken prose and roll a trailing window.
|
|
195
|
+
- `toPlainText(md)` / `tailWindow(text, { maxChars })` — the pure text helpers behind those widgets, for building your own caption.
|
|
187
196
|
|
|
188
197
|
## Getting an `AnalyserNode`
|
|
189
198
|
|
|
@@ -280,7 +289,7 @@ function TextAvatar() {
|
|
|
280
289
|
|
|
281
290
|
`createSpeechActivity(options?)` accepts `chargePerChar`, `decayMs` and `maxChargePerPush` to tune how wide / how fast the mouth reacts. The returned source has `push(chunk)`, `end()`, `reset()` (drop energy on an interrupted turn) and `sample()`. When `speechActivity` is provided it takes precedence over both `streamingText` and `analyser`. (`streamingText` is just this, with the diffing done for you — under the hood it's the exported `useStreamingTextActivity` hook.)
|
|
282
291
|
|
|
283
|
-
>
|
|
292
|
+
> [`examples/03-streaming-text-imperative.tsx`](examples/03-streaming-text-imperative.tsx) shows this end-to-end against an OpenAI-compatible endpoint. The browser only ever talks to your own `/api/chat`; a tiny reference relay that proxies to the provider (so the key never reaches the client) lives in [`examples/server/proxy.ts`](examples/server/proxy.ts).
|
|
284
293
|
|
|
285
294
|
## Positioning
|
|
286
295
|
|
|
@@ -294,17 +303,21 @@ The closest reference is [TalkingHead](https://github.com/met4citizen/TalkingHea
|
|
|
294
303
|
| Makes visible | the voice | the *thinking* |
|
|
295
304
|
| Setup | avatar platform + Blender + rig | `npm i` + one component |
|
|
296
305
|
|
|
297
|
-
##
|
|
306
|
+
## Development
|
|
298
307
|
|
|
299
|
-
|
|
308
|
+
This repo is the library only — no app or backend. The runnable, hosted demos
|
|
309
|
+
live on the project's docs site (built separately, client-side mock, no API key).
|
|
300
310
|
|
|
301
311
|
```bash
|
|
302
312
|
npm install
|
|
303
|
-
npm run dev # starts the demo at :3000 (MOCK_REALTIME=true needs no API key)
|
|
304
313
|
npm test # vitest: engine, layer contract, SSR, parsers
|
|
314
|
+
npm run lint # tsc --noEmit
|
|
305
315
|
npm run build:lib # builds the publishable package into dist/lib
|
|
306
316
|
```
|
|
307
317
|
|
|
318
|
+
Copy-pasteable integration examples — including a reference relay server for
|
|
319
|
+
real voice/text providers — live in [`examples/`](examples/).
|
|
320
|
+
|
|
308
321
|
## License
|
|
309
322
|
|
|
310
323
|
MIT — for the library, the runtime and all built-in presets. Use it commercially, fork it, reskin it. SVGs you bring via `byos` keep whatever license they had.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react/jsx-runtime"),n=require("react"),Y=require("@react-three/fiber"),he=require("three/addons/loaders/GLTFLoader.js"),me=require("three/addons/utils/SkeletonUtils.js"),de=require("@react-three/drei"),ge=require("three"),ae=require("./useReducedMotion-BRDEmRNI.cjs");function pe(f){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(f){for(const a in f)if(a!=="default"){const R=Object.getOwnPropertyDescriptor(f,a);Object.defineProperty(t,a,R.get?R:{enumerable:!0,get:()=>f[a]})}}return t.default=f,Object.freeze(t)}const W=pe(ge);class xe extends n.Component{constructor(t){super(t),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(t){this.props.onError((t==null?void 0:t.message)||String(t))}render(){return this.state.hasError?null:this.props.children}}function be(f){const t=new Map,a=c=>c.toLowerCase().replace(/[^a-z]/g,"");f.traverse(c=>{const h=c,m=h.morphTargetDictionary;if(!(!m||!h.morphTargetInfluences))for(const i of Object.keys(m)){const g=m[i],r=a(i);t.has(r)||t.set(r,[]),t.get(r).push({mesh:h,index:g})}});const R=c=>{const h=a(c);if(t.has(h))return t.get(h);const m=[];for(const[i,g]of t)i.endsWith(h)&&m.push(...g);return m},j=new Map;return{has(c){return this.entries(c).length>0},entries(c){return j.has(c)||j.set(c,R(c)),j.get(c)},set(c,h){const m=this.entries(c),i=Math.max(0,Math.min(1,h));for(const{mesh:g,index:r}of m)g.morphTargetInfluences[r]=i},names:Array.from(t.keys())}}function ke({url:f,state:t,analyser:a,maxMouthOpening:R=30,mouseTrackingIntensity:j=1,blinkIntervalMin:c=2e3,blinkIntervalMax:h=6e3,blinkDuration:m=100,reducedMotion:i=!1,onLoaded:g}){const r=Y.useLoader(he.GLTFLoader,f),{camera:l,controls:q}=Y.useThree(),p=n.useMemo(()=>me.clone(r.scene),[r.scene]),e=n.useMemo(()=>be(p),[p]);n.useEffect(()=>{var B;g(!0),console.log("[GLB] Model loaded. ARKit blendshapes found:",e.names),p.traverse(G=>{G.frustumCulled=!1}),p.updateMatrixWorld(!0);const L=new W.Box3().setFromObject(p),b=new W.Vector3,o=new W.Vector3;L.getSize(b),L.getCenter(o);const k=L.max.y-b.y*.08,Z=Math.max(.45,b.y*.42);l.position.set(o.x,k,L.max.z+Z),l.lookAt(o.x,k,o.z),l.updateProjectionMatrix();const w=q;w!=null&&w.target&&(w.target.set(o.x,k,o.z),(B=w.update)==null||B.call(w))},[p,e,l,q,g]);const S=n.useRef(0),O=n.useRef(0),v=n.useRef(0),z=n.useRef(0),T=n.useRef(0),A=n.useRef(0),d=n.useRef(0),u=n.useRef(0),D=n.useRef(t),$=n.useRef(0),I=n.useRef(0),V=n.useRef(0),X=n.useRef(0),J=n.useRef(Math.random()*3+2),x=n.useRef(0),N=n.useRef(!1),K=n.useRef(null),ee=n.useRef(null);return Y.useFrame((L,b)=>{var oe,ue;const o=W.MathUtils.lerp,k=L.clock.elapsedTime;if(t!==D.current&&(t==="listening"&&!i&&(u.current=1),D.current==="speaking"&&t!=="speaking"&&!i&&(I.current=2),D.current=t),u.current=Math.max(0,u.current-b*2.5),I.current=Math.max(0,I.current-b),!i)if(!N.current)J.current-=b,J.current<=0&&(N.current=!0);else{const M=1e3/(m||100);x.current+=b*M*2,x.current>=1&&(x.current=1,N.current=!1,J.current=Math.random()*((h-c)/1e3)+c/1e3)}if(!N.current&&x.current>0){const M=1e3/(m||100);x.current-=b*M*2,x.current<0&&(x.current=0)}e.set("eyeBlinkLeft",x.current),e.set("eyeBlinkRight",x.current);const Z=((oe=L.pointer)==null?void 0:oe.x)??0,w=((ue=L.pointer)==null?void 0:ue.y)??0,B=i?0:j;let G=Z*B,Q=w*B;t==="thinking"?(G=-.5*(i?0:1),Q=.6*(i?0:1)):i||(G+=Math.sin(k*.23)*.12,Q+=Math.sin(k*.31+1.3)*.08),V.current=o(V.current,G,.1),X.current=o(X.current,Q,.1);const F=V.current,U=X.current;e.set("eyeLookOutRight",Math.max(0,F)),e.set("eyeLookInLeft",Math.max(0,F)),e.set("eyeLookOutLeft",Math.max(0,-F)),e.set("eyeLookInRight",Math.max(0,-F)),e.set("eyeLookUpLeft",Math.max(0,U)),e.set("eyeLookUpRight",Math.max(0,U)),e.set("eyeLookDownLeft",Math.max(0,-U)),e.set("eyeLookDownRight",Math.max(0,-U));let E=0,te=0,ne=0;t==="listening"?(E=.35,te=.25):t==="thinking"?ne=.45:t==="speaking"?E=.12:E=.05,I.current>0&&t!=="speaking"&&(E=Math.max(E,.55));const le=t==="thinking"&&!i?1:0;T.current=o(T.current,E,.1),A.current=o(A.current,te,.1),d.current=o(d.current,ne,.1),$.current=o($.current,le,.08);const y=T.current,_=$.current;e.set("mouthSmileLeft",y),e.set("mouthSmileRight",y),e.set("browInnerUp",A.current),e.set("browDownLeft",d.current),e.set("browDownRight",d.current),e.set("cheekSquintLeft",y*.6),e.set("cheekSquintRight",y*.6),e.set("mouthDimpleLeft",y*.5),e.set("mouthDimpleRight",y*.5),e.set("eyeSquintLeft",Math.max(y*.35,_*.45)),e.set("eyeSquintRight",Math.max(y*.35,_*.45)),e.set("mouthPressLeft",_*.4),e.set("mouthPressRight",_*.4),e.set("eyeWideLeft",u.current*.7),e.set("eyeWideRight",u.current*.7),e.set("browOuterUpLeft",u.current*.6),e.set("browOuterUpRight",u.current*.6);let H=0,re=0,se=0,ce=0;if(t==="speaking"){(!K.current||ee.current!==a)&&(K.current=ae.createMouthEngine(a),ee.current=a);const M=K.current.read(),fe=R/30,P=Math.min(1,M.level*fe*1.7);M.shape==="o"?(re=P*.85,se=P*.6,H=P*.35):M.shape==="e"?(ce=P*.7,H=P*.3):M.shape==="a"&&(H=P*.9)}S.current=o(S.current,H,.25),O.current=o(O.current,re,.25),v.current=o(v.current,se,.25),z.current=o(z.current,ce,.25),e.set("jawOpen",S.current),e.set("mouthFunnel",O.current),e.set("mouthPucker",v.current),e.set("mouthStretchLeft",z.current),e.set("mouthStretchRight",z.current);const C=i||t==="speaking"||t==="thinking"?0:1,ie=Math.sin(k*.4)*.06*C;e.set("mouthLeft",Math.max(0,ie)),e.set("mouthRight",Math.max(0,-ie)),e.set("mouthRollLower",(Math.sin(k*.33)*.5+.5)*.07*C),e.set("mouthShrugUpper",(Math.sin(k*.7)*.5+.5)*.06*C)}),s.jsx("primitive",{object:p})}function we({state:f,analyser:t,size:a=300,className:R="",style:j,maxMouthOpening:c,blinkIntervalMin:h,blinkIntervalMax:m,blinkDuration:i,mouseTrackingIntensity:g,stateColors:r,glbUrl:l}){const[q,p]=n.useState(!1),[e,S]=n.useState(null),[O,v]=n.useState(!1),z=ae.useReducedMotion(),T=n.useCallback(d=>p(d),[]);n.useEffect(()=>{if(p(!1),S(null),v(!1),!l)return;let d=!1;return fetch(l).then(u=>{const D=u.headers.get("content-type")||"";if(!u.ok)throw new Error(`HTTP ${u.status} for ${l}`);if(D.includes("text/html"))throw new Error(`No .glb found at ${l} (server returned HTML).`);d||v(!0)}).catch(u=>{d||S((u==null?void 0:u.message)||String(u))}),()=>{d=!0}},[l]);const A={idle:(r==null?void 0:r.idle)??"#4b5563",listening:(r==null?void 0:r.listening)??"#3b82f6",thinking:(r==null?void 0:r.thinking)??"#8b5cf6",speaking:(r==null?void 0:r.speaking)??"#10b981",working:(r==null?void 0:r.working)??"#f59e0b"};return s.jsxs("div",{className:`relative flex items-center justify-center rounded-3xl overflow-hidden border border-zinc-800/40 bg-zinc-950/40 ${R}`,style:{width:a,height:a,...j},children:[s.jsxs(Y.Canvas,{camera:{position:[0,1.5,1],fov:35},shadows:!0,gl:{antialias:!0,alpha:!0,preserveDrawingBuffer:!0},style:{width:"100%",height:"100%",background:"transparent"},children:[s.jsx("ambientLight",{intensity:1.5}),s.jsx("spotLight",{position:[0,3,2],angle:.6,penumbra:1,intensity:3,color:A[f],castShadow:!0}),s.jsx("directionalLight",{position:[-2,2,-2],intensity:1.8,color:"#ffffff"}),s.jsx("directionalLight",{position:[2,2,2],intensity:2.2,color:"#ffffff"}),s.jsx(xe,{onError:d=>S(d),children:l&&O&&s.jsx(n.Suspense,{fallback:null,children:s.jsx(ke,{url:l,state:f,analyser:t,maxMouthOpening:c,mouseTrackingIntensity:g,blinkIntervalMin:h,blinkIntervalMax:m,blinkDuration:i,reducedMotion:z,onLoaded:T})})}),s.jsx(de.OrbitControls,{makeDefault:!0,enableZoom:!1,enablePan:!1,minPolarAngle:Math.PI/2.6,maxPolarAngle:Math.PI/1.7,minAzimuthAngle:-Math.PI/4,maxAzimuthAngle:Math.PI/4})]}),!l&&s.jsxs("div",{className:"absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/90 backdrop-blur-md z-20 p-4 text-center",children:[s.jsx("span",{className:"text-xs font-mono font-bold text-amber-400 uppercase tracking-wider mb-2",children:"Missing glbUrl"}),s.jsx("p",{className:"text-[10px] text-zinc-500 max-w-[200px] leading-relaxed",children:"Pass a CORS-enabled .glb URL (with ARKit blendshapes) via the glbUrl prop."})]}),l&&!q&&!e&&s.jsxs("div",{className:"absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/80 backdrop-blur-md z-20",children:[s.jsx("div",{className:"w-10 h-10 border-4 border-t-emerald-500 border-emerald-500/20 rounded-full animate-spin mb-3"}),s.jsx("span",{className:"text-[10px] font-mono font-bold tracking-widest text-zinc-400 animate-pulse",children:"LOADING GLB MODEL..."})]}),e&&s.jsxs("div",{className:"absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/90 backdrop-blur-md z-20 p-4 text-center",children:[s.jsx("span",{className:"text-xs font-mono font-bold text-red-400 uppercase tracking-wider mb-2",children:"Failed to load GLB"}),s.jsx("p",{className:"text-[10px] text-zinc-500 max-w-[200px] leading-relaxed break-all",children:e})]})]})}exports.GlbArkitAvatar=we;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { jsxs as O, jsx as a } from "react/jsx-runtime";
|
|
2
|
+
import de, { useState as ee, useCallback as pe, useEffect as he, Suspense as ge, useMemo as le, useRef as s } from "react";
|
|
3
|
+
import { Canvas as xe, useLoader as ke, useThree as be, useFrame as we } from "@react-three/fiber";
|
|
4
|
+
import { GLTFLoader as Le } from "three/addons/loaders/GLTFLoader.js";
|
|
5
|
+
import { clone as ye } from "three/addons/utils/SkeletonUtils.js";
|
|
6
|
+
import { OrbitControls as Me } from "@react-three/drei";
|
|
7
|
+
import * as H from "three";
|
|
8
|
+
import { u as Re, c as ze } from "./useReducedMotion-BDcEizfP.js";
|
|
9
|
+
class Se extends de.Component {
|
|
10
|
+
constructor(t) {
|
|
11
|
+
super(t), this.state = { hasError: !1 };
|
|
12
|
+
}
|
|
13
|
+
static getDerivedStateFromError() {
|
|
14
|
+
return { hasError: !0 };
|
|
15
|
+
}
|
|
16
|
+
componentDidCatch(t) {
|
|
17
|
+
this.props.onError((t == null ? void 0 : t.message) || String(t));
|
|
18
|
+
}
|
|
19
|
+
render() {
|
|
20
|
+
return this.state.hasError ? null : this.props.children;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function ve(M) {
|
|
24
|
+
const t = /* @__PURE__ */ new Map(), p = (r) => r.toLowerCase().replace(/[^a-z]/g, "");
|
|
25
|
+
M.traverse((r) => {
|
|
26
|
+
const l = r, h = l.morphTargetDictionary;
|
|
27
|
+
if (!(!h || !l.morphTargetInfluences))
|
|
28
|
+
for (const c of Object.keys(h)) {
|
|
29
|
+
const f = h[c], n = p(c);
|
|
30
|
+
t.has(n) || t.set(n, []), t.get(n).push({ mesh: l, index: f });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const A = (r) => {
|
|
34
|
+
const l = p(r);
|
|
35
|
+
if (t.has(l)) return t.get(l);
|
|
36
|
+
const h = [];
|
|
37
|
+
for (const [c, f] of t)
|
|
38
|
+
c.endsWith(l) && h.push(...f);
|
|
39
|
+
return h;
|
|
40
|
+
}, R = /* @__PURE__ */ new Map();
|
|
41
|
+
return {
|
|
42
|
+
has(r) {
|
|
43
|
+
return this.entries(r).length > 0;
|
|
44
|
+
},
|
|
45
|
+
entries(r) {
|
|
46
|
+
return R.has(r) || R.set(r, A(r)), R.get(r);
|
|
47
|
+
},
|
|
48
|
+
set(r, l) {
|
|
49
|
+
const h = this.entries(r), c = Math.max(0, Math.min(1, l));
|
|
50
|
+
for (const { mesh: f, index: n } of h)
|
|
51
|
+
f.morphTargetInfluences[n] = c;
|
|
52
|
+
},
|
|
53
|
+
/** All normalized morph-target names found, for debugging. */
|
|
54
|
+
names: Array.from(t.keys())
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function Ee({
|
|
58
|
+
url: M,
|
|
59
|
+
state: t,
|
|
60
|
+
analyser: p,
|
|
61
|
+
maxMouthOpening: A = 30,
|
|
62
|
+
mouseTrackingIntensity: R = 1,
|
|
63
|
+
blinkIntervalMin: r = 2e3,
|
|
64
|
+
blinkIntervalMax: l = 6e3,
|
|
65
|
+
blinkDuration: h = 100,
|
|
66
|
+
reducedMotion: c = !1,
|
|
67
|
+
onLoaded: f
|
|
68
|
+
}) {
|
|
69
|
+
const n = ke(Le, M), { camera: u, controls: N } = be(), d = le(() => ye(n.scene), [n.scene]), e = le(() => ve(d), [d]);
|
|
70
|
+
he(() => {
|
|
71
|
+
var G;
|
|
72
|
+
f(!0), console.log("[GLB] Model loaded. ARKit blendshapes found:", e.names), d.traverse((I) => {
|
|
73
|
+
I.frustumCulled = !1;
|
|
74
|
+
}), d.updateMatrixWorld(!0);
|
|
75
|
+
const w = new H.Box3().setFromObject(d), x = new H.Vector3(), i = new H.Vector3();
|
|
76
|
+
w.getSize(x), w.getCenter(i);
|
|
77
|
+
const k = w.max.y - x.y * 0.08, Q = Math.max(0.45, x.y * 0.42);
|
|
78
|
+
u.position.set(i.x, k, w.max.z + Q), u.lookAt(i.x, k, i.z), u.updateProjectionMatrix();
|
|
79
|
+
const b = N;
|
|
80
|
+
b != null && b.target && (b.target.set(i.x, k, i.z), (G = b.update) == null || G.call(b));
|
|
81
|
+
}, [d, e, u, N, f]);
|
|
82
|
+
const z = s(0), B = s(0), S = s(0), v = s(0), D = s(0), T = s(0), m = s(0), o = s(0), j = s(t), V = s(0), F = s(0), X = s(0), J = s(0), K = s(Math.random() * 3 + 2), g = s(0), q = s(!1), Z = s(null), te = s(null);
|
|
83
|
+
return we((w, x) => {
|
|
84
|
+
var ae, ue;
|
|
85
|
+
const i = H.MathUtils.lerp, k = w.clock.elapsedTime;
|
|
86
|
+
if (t !== j.current && (t === "listening" && !c && (o.current = 1), j.current === "speaking" && t !== "speaking" && !c && (F.current = 2), j.current = t), o.current = Math.max(0, o.current - x * 2.5), F.current = Math.max(0, F.current - x), !c)
|
|
87
|
+
if (!q.current)
|
|
88
|
+
K.current -= x, K.current <= 0 && (q.current = !0);
|
|
89
|
+
else {
|
|
90
|
+
const y = 1e3 / (h || 100);
|
|
91
|
+
g.current += x * y * 2, g.current >= 1 && (g.current = 1, q.current = !1, K.current = Math.random() * ((l - r) / 1e3) + r / 1e3);
|
|
92
|
+
}
|
|
93
|
+
if (!q.current && g.current > 0) {
|
|
94
|
+
const y = 1e3 / (h || 100);
|
|
95
|
+
g.current -= x * y * 2, g.current < 0 && (g.current = 0);
|
|
96
|
+
}
|
|
97
|
+
e.set("eyeBlinkLeft", g.current), e.set("eyeBlinkRight", g.current);
|
|
98
|
+
const Q = ((ae = w.pointer) == null ? void 0 : ae.x) ?? 0, b = ((ue = w.pointer) == null ? void 0 : ue.y) ?? 0, G = c ? 0 : R;
|
|
99
|
+
let I = Q * G, _ = b * G;
|
|
100
|
+
t === "thinking" ? (I = -0.5 * (c ? 0 : 1), _ = 0.6 * (c ? 0 : 1)) : c || (I += Math.sin(k * 0.23) * 0.12, _ += Math.sin(k * 0.31 + 1.3) * 0.08), X.current = i(X.current, I, 0.1), J.current = i(J.current, _, 0.1);
|
|
101
|
+
const U = X.current, W = J.current;
|
|
102
|
+
e.set("eyeLookOutRight", Math.max(0, U)), e.set("eyeLookInLeft", Math.max(0, U)), e.set("eyeLookOutLeft", Math.max(0, -U)), e.set("eyeLookInRight", Math.max(0, -U)), e.set("eyeLookUpLeft", Math.max(0, W)), e.set("eyeLookUpRight", Math.max(0, W)), e.set("eyeLookDownLeft", Math.max(0, -W)), e.set("eyeLookDownRight", Math.max(0, -W));
|
|
103
|
+
let E = 0, ne = 0, re = 0;
|
|
104
|
+
t === "listening" ? (E = 0.35, ne = 0.25) : t === "thinking" ? re = 0.45 : t === "speaking" ? E = 0.12 : E = 0.05, F.current > 0 && t !== "speaking" && (E = Math.max(E, 0.55));
|
|
105
|
+
const me = t === "thinking" && !c ? 1 : 0;
|
|
106
|
+
D.current = i(D.current, E, 0.1), T.current = i(T.current, ne, 0.1), m.current = i(m.current, re, 0.1), V.current = i(V.current, me, 0.08);
|
|
107
|
+
const L = D.current, Y = V.current;
|
|
108
|
+
e.set("mouthSmileLeft", L), e.set("mouthSmileRight", L), e.set("browInnerUp", T.current), e.set("browDownLeft", m.current), e.set("browDownRight", m.current), e.set("cheekSquintLeft", L * 0.6), e.set("cheekSquintRight", L * 0.6), e.set("mouthDimpleLeft", L * 0.5), e.set("mouthDimpleRight", L * 0.5), e.set("eyeSquintLeft", Math.max(L * 0.35, Y * 0.45)), e.set("eyeSquintRight", Math.max(L * 0.35, Y * 0.45)), e.set("mouthPressLeft", Y * 0.4), e.set("mouthPressRight", Y * 0.4), e.set("eyeWideLeft", o.current * 0.7), e.set("eyeWideRight", o.current * 0.7), e.set("browOuterUpLeft", o.current * 0.6), e.set("browOuterUpRight", o.current * 0.6);
|
|
109
|
+
let $ = 0, ce = 0, se = 0, ie = 0;
|
|
110
|
+
if (t === "speaking") {
|
|
111
|
+
(!Z.current || te.current !== p) && (Z.current = ze(p), te.current = p);
|
|
112
|
+
const y = Z.current.read(), fe = A / 30, P = Math.min(1, y.level * fe * 1.7);
|
|
113
|
+
y.shape === "o" ? (ce = P * 0.85, se = P * 0.6, $ = P * 0.35) : y.shape === "e" ? (ie = P * 0.7, $ = P * 0.3) : y.shape === "a" && ($ = P * 0.9);
|
|
114
|
+
}
|
|
115
|
+
z.current = i(z.current, $, 0.25), B.current = i(B.current, ce, 0.25), S.current = i(S.current, se, 0.25), v.current = i(v.current, ie, 0.25), e.set("jawOpen", z.current), e.set("mouthFunnel", B.current), e.set("mouthPucker", S.current), e.set("mouthStretchLeft", v.current), e.set("mouthStretchRight", v.current);
|
|
116
|
+
const C = c || t === "speaking" || t === "thinking" ? 0 : 1, oe = Math.sin(k * 0.4) * 0.06 * C;
|
|
117
|
+
e.set("mouthLeft", Math.max(0, oe)), e.set("mouthRight", Math.max(0, -oe)), e.set("mouthRollLower", (Math.sin(k * 0.33) * 0.5 + 0.5) * 0.07 * C), e.set("mouthShrugUpper", (Math.sin(k * 0.7) * 0.5 + 0.5) * 0.06 * C);
|
|
118
|
+
}), /* @__PURE__ */ a("primitive", { object: d });
|
|
119
|
+
}
|
|
120
|
+
function Ie({
|
|
121
|
+
state: M,
|
|
122
|
+
analyser: t,
|
|
123
|
+
size: p = 300,
|
|
124
|
+
className: A = "",
|
|
125
|
+
style: R,
|
|
126
|
+
maxMouthOpening: r,
|
|
127
|
+
blinkIntervalMin: l,
|
|
128
|
+
blinkIntervalMax: h,
|
|
129
|
+
blinkDuration: c,
|
|
130
|
+
mouseTrackingIntensity: f,
|
|
131
|
+
stateColors: n,
|
|
132
|
+
glbUrl: u
|
|
133
|
+
}) {
|
|
134
|
+
const [N, d] = ee(!1), [e, z] = ee(null), [B, S] = ee(!1), v = Re(), D = pe((m) => d(m), []);
|
|
135
|
+
he(() => {
|
|
136
|
+
if (d(!1), z(null), S(!1), !u) return;
|
|
137
|
+
let m = !1;
|
|
138
|
+
return fetch(u).then((o) => {
|
|
139
|
+
const j = o.headers.get("content-type") || "";
|
|
140
|
+
if (!o.ok) throw new Error(`HTTP ${o.status} for ${u}`);
|
|
141
|
+
if (j.includes("text/html"))
|
|
142
|
+
throw new Error(`No .glb found at ${u} (server returned HTML).`);
|
|
143
|
+
m || S(!0);
|
|
144
|
+
}).catch((o) => {
|
|
145
|
+
m || z((o == null ? void 0 : o.message) || String(o));
|
|
146
|
+
}), () => {
|
|
147
|
+
m = !0;
|
|
148
|
+
};
|
|
149
|
+
}, [u]);
|
|
150
|
+
const T = {
|
|
151
|
+
idle: (n == null ? void 0 : n.idle) ?? "#4b5563",
|
|
152
|
+
listening: (n == null ? void 0 : n.listening) ?? "#3b82f6",
|
|
153
|
+
thinking: (n == null ? void 0 : n.thinking) ?? "#8b5cf6",
|
|
154
|
+
speaking: (n == null ? void 0 : n.speaking) ?? "#10b981",
|
|
155
|
+
working: (n == null ? void 0 : n.working) ?? "#f59e0b"
|
|
156
|
+
};
|
|
157
|
+
return /* @__PURE__ */ O(
|
|
158
|
+
"div",
|
|
159
|
+
{
|
|
160
|
+
className: `relative flex items-center justify-center rounded-3xl overflow-hidden border border-zinc-800/40 bg-zinc-950/40 ${A}`,
|
|
161
|
+
style: { width: p, height: p, ...R },
|
|
162
|
+
children: [
|
|
163
|
+
/* @__PURE__ */ O(
|
|
164
|
+
xe,
|
|
165
|
+
{
|
|
166
|
+
camera: { position: [0, 1.5, 1], fov: 35 },
|
|
167
|
+
shadows: !0,
|
|
168
|
+
gl: { antialias: !0, alpha: !0, preserveDrawingBuffer: !0 },
|
|
169
|
+
style: { width: "100%", height: "100%", background: "transparent" },
|
|
170
|
+
children: [
|
|
171
|
+
/* @__PURE__ */ a("ambientLight", { intensity: 1.5 }),
|
|
172
|
+
/* @__PURE__ */ a(
|
|
173
|
+
"spotLight",
|
|
174
|
+
{
|
|
175
|
+
position: [0, 3, 2],
|
|
176
|
+
angle: 0.6,
|
|
177
|
+
penumbra: 1,
|
|
178
|
+
intensity: 3,
|
|
179
|
+
color: T[M],
|
|
180
|
+
castShadow: !0
|
|
181
|
+
}
|
|
182
|
+
),
|
|
183
|
+
/* @__PURE__ */ a("directionalLight", { position: [-2, 2, -2], intensity: 1.8, color: "#ffffff" }),
|
|
184
|
+
/* @__PURE__ */ a("directionalLight", { position: [2, 2, 2], intensity: 2.2, color: "#ffffff" }),
|
|
185
|
+
/* @__PURE__ */ a(Se, { onError: (m) => z(m), children: u && B && /* @__PURE__ */ a(ge, { fallback: null, children: /* @__PURE__ */ a(
|
|
186
|
+
Ee,
|
|
187
|
+
{
|
|
188
|
+
url: u,
|
|
189
|
+
state: M,
|
|
190
|
+
analyser: t,
|
|
191
|
+
maxMouthOpening: r,
|
|
192
|
+
mouseTrackingIntensity: f,
|
|
193
|
+
blinkIntervalMin: l,
|
|
194
|
+
blinkIntervalMax: h,
|
|
195
|
+
blinkDuration: c,
|
|
196
|
+
reducedMotion: v,
|
|
197
|
+
onLoaded: D
|
|
198
|
+
}
|
|
199
|
+
) }) }),
|
|
200
|
+
/* @__PURE__ */ a(
|
|
201
|
+
Me,
|
|
202
|
+
{
|
|
203
|
+
makeDefault: !0,
|
|
204
|
+
enableZoom: !1,
|
|
205
|
+
enablePan: !1,
|
|
206
|
+
minPolarAngle: Math.PI / 2.6,
|
|
207
|
+
maxPolarAngle: Math.PI / 1.7,
|
|
208
|
+
minAzimuthAngle: -Math.PI / 4,
|
|
209
|
+
maxAzimuthAngle: Math.PI / 4
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
),
|
|
215
|
+
!u && /* @__PURE__ */ O("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/90 backdrop-blur-md z-20 p-4 text-center", children: [
|
|
216
|
+
/* @__PURE__ */ a("span", { className: "text-xs font-mono font-bold text-amber-400 uppercase tracking-wider mb-2", children: "Missing glbUrl" }),
|
|
217
|
+
/* @__PURE__ */ a("p", { className: "text-[10px] text-zinc-500 max-w-[200px] leading-relaxed", children: "Pass a CORS-enabled .glb URL (with ARKit blendshapes) via the glbUrl prop." })
|
|
218
|
+
] }),
|
|
219
|
+
u && !N && !e && /* @__PURE__ */ O("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/80 backdrop-blur-md z-20", children: [
|
|
220
|
+
/* @__PURE__ */ a("div", { className: "w-10 h-10 border-4 border-t-emerald-500 border-emerald-500/20 rounded-full animate-spin mb-3" }),
|
|
221
|
+
/* @__PURE__ */ a("span", { className: "text-[10px] font-mono font-bold tracking-widest text-zinc-400 animate-pulse", children: "LOADING GLB MODEL..." })
|
|
222
|
+
] }),
|
|
223
|
+
e && /* @__PURE__ */ O("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/90 backdrop-blur-md z-20 p-4 text-center", children: [
|
|
224
|
+
/* @__PURE__ */ a("span", { className: "text-xs font-mono font-bold text-red-400 uppercase tracking-wider mb-2", children: "Failed to load GLB" }),
|
|
225
|
+
/* @__PURE__ */ a("p", { className: "text-[10px] text-zinc-500 max-w-[200px] leading-relaxed break-all", children: e })
|
|
226
|
+
] })
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
export {
|
|
232
|
+
Ie as GlbArkitAvatar
|
|
233
|
+
};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { jsxs as O, jsx as i } from "react/jsx-runtime";
|
|
2
|
+
import oe, { useState as H, useEffect as ie, useCallback as ne, Suspense as se, useRef as d } from "react";
|
|
3
|
+
import { Canvas as ce, useFrame as le } from "@react-three/fiber";
|
|
4
|
+
import { GLTFLoader as ue } from "three/addons/loaders/GLTFLoader.js";
|
|
5
|
+
import { VRMLoaderPlugin as de, VRMUtils as ae } from "@pixiv/three-vrm";
|
|
6
|
+
import { OrbitControls as pe } from "@react-three/drei";
|
|
7
|
+
import * as a from "three";
|
|
8
|
+
import { u as me, c as fe } from "./useReducedMotion-BDcEizfP.js";
|
|
9
|
+
class he extends oe.Component {
|
|
10
|
+
constructor(r) {
|
|
11
|
+
super(r), this.state = { hasError: !1 };
|
|
12
|
+
}
|
|
13
|
+
static getDerivedStateFromError() {
|
|
14
|
+
return { hasError: !0 };
|
|
15
|
+
}
|
|
16
|
+
componentDidCatch(r) {
|
|
17
|
+
this.props.onError((r == null ? void 0 : r.message) || String(r));
|
|
18
|
+
}
|
|
19
|
+
render() {
|
|
20
|
+
return this.state.hasError ? null : this.props.children;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function f(l, r, h) {
|
|
24
|
+
if (!l.expressionManager) return;
|
|
25
|
+
const E = l.expressionManager.expressionMap || {}, v = Object.keys(E);
|
|
26
|
+
let o = [];
|
|
27
|
+
switch (r) {
|
|
28
|
+
case "blink":
|
|
29
|
+
o = ["blink", "Blink", "BLINK"];
|
|
30
|
+
break;
|
|
31
|
+
case "aa":
|
|
32
|
+
o = ["aa", "Aa", "a", "A", "AA", "mouth_a", "mouth_A"];
|
|
33
|
+
break;
|
|
34
|
+
case "ee":
|
|
35
|
+
o = ["ee", "Ee", "e", "E", "EE", "mouth_e", "mouth_E"];
|
|
36
|
+
break;
|
|
37
|
+
case "oo":
|
|
38
|
+
o = ["oh", "Oh", "oo", "Oo", "o", "O", "OO", "mouth_o", "mouth_O"];
|
|
39
|
+
break;
|
|
40
|
+
case "happy":
|
|
41
|
+
o = ["happy", "Happy", "joy", "Joy", "JOY", "fun", "Fun", "FUN", "HAPPY"];
|
|
42
|
+
break;
|
|
43
|
+
case "sad":
|
|
44
|
+
o = ["sad", "Sad", "sorrow", "Sorrow", "SORROW", "sadness", "SAD"];
|
|
45
|
+
break;
|
|
46
|
+
case "relaxed":
|
|
47
|
+
o = ["relaxed", "Relaxed", "fun", "Fun", "FUN", "neutral", "Neutral", "RELAXED"];
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
const A = o.find((x) => E[x] !== void 0 || v.includes(x));
|
|
51
|
+
if (A)
|
|
52
|
+
l.expressionManager.setValue(A, h);
|
|
53
|
+
else
|
|
54
|
+
try {
|
|
55
|
+
l.expressionManager.setValue(o[0], h);
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function ge({
|
|
60
|
+
url: l,
|
|
61
|
+
state: r,
|
|
62
|
+
analyser: h,
|
|
63
|
+
maxMouthOpening: E = 30,
|
|
64
|
+
mouseTrackingIntensity: v = 1,
|
|
65
|
+
blinkIntervalMin: o = 2e3,
|
|
66
|
+
blinkIntervalMax: A = 6e3,
|
|
67
|
+
blinkDuration: x = 100,
|
|
68
|
+
reducedMotion: P = !1,
|
|
69
|
+
onLoaded: S,
|
|
70
|
+
onError: t
|
|
71
|
+
}) {
|
|
72
|
+
const [e, B] = H(null);
|
|
73
|
+
ie(() => {
|
|
74
|
+
let b = !1, M = null;
|
|
75
|
+
const c = new ue();
|
|
76
|
+
return c.register((n) => new de(n)), c.load(
|
|
77
|
+
l,
|
|
78
|
+
(n) => {
|
|
79
|
+
var D, L;
|
|
80
|
+
if (b) {
|
|
81
|
+
const y = n.userData.vrm;
|
|
82
|
+
y && ae.deepDispose(y.scene);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const k = n.userData.vrm;
|
|
86
|
+
if (!k) {
|
|
87
|
+
t(`No VRM found at ${l}.`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.log(
|
|
91
|
+
"[VRM] Model loaded successfully. Available expressions:",
|
|
92
|
+
Object.keys(((D = k.expressionManager) == null ? void 0 : D.expressionMap) || {})
|
|
93
|
+
), k.scene.traverse((y) => {
|
|
94
|
+
y.frustumCulled = !1;
|
|
95
|
+
});
|
|
96
|
+
const F = ((L = k.meta) == null ? void 0 : L.metaVersion) === "0";
|
|
97
|
+
k.scene.rotation.y = F ? Math.PI : 0, M = k, B(k), S(!0);
|
|
98
|
+
},
|
|
99
|
+
void 0,
|
|
100
|
+
(n) => {
|
|
101
|
+
b || t((n == null ? void 0 : n.message) || String(n));
|
|
102
|
+
}
|
|
103
|
+
), () => {
|
|
104
|
+
b = !0, M && ae.deepDispose(M.scene);
|
|
105
|
+
};
|
|
106
|
+
}, [l, S, t]);
|
|
107
|
+
const g = d(0), p = d(0), m = d(0), U = d(0), w = d(0), z = d(0), V = d(Math.random() * 3 + 2), s = d(0), j = d(!1), I = d(null), K = d(null);
|
|
108
|
+
return le((b, M) => {
|
|
109
|
+
var T, X, G, J, $, W, Z, q, Q;
|
|
110
|
+
if (!e) return;
|
|
111
|
+
e.update(M);
|
|
112
|
+
const c = (T = e.humanoid) == null ? void 0 : T.getNormalizedBoneNode("neck"), n = (X = e.humanoid) == null ? void 0 : X.getNormalizedBoneNode("head");
|
|
113
|
+
if (((G = e.meta) == null ? void 0 : G.metaVersion) === "0") {
|
|
114
|
+
const u = (J = e.humanoid) == null ? void 0 : J.getNormalizedBoneNode("leftUpperArm"), N = ($ = e.humanoid) == null ? void 0 : $.getNormalizedBoneNode("rightUpperArm");
|
|
115
|
+
u && (u.rotation.z = Math.PI / 2.6), N && (N.rotation.z = -Math.PI / 2.6);
|
|
116
|
+
}
|
|
117
|
+
const F = ((W = b.pointer) == null ? void 0 : W.x) ?? ((Z = b.mouse) == null ? void 0 : Z.x) ?? 0, D = ((q = b.pointer) == null ? void 0 : q.y) ?? ((Q = b.mouse) == null ? void 0 : Q.y) ?? 0, L = P ? 0 : v, y = F * 0.35 * L, Y = -D * 0.2 * L;
|
|
118
|
+
if (c && (c.rotation.y = a.MathUtils.lerp(c.rotation.y, y, 0.1), c.rotation.x = a.MathUtils.lerp(c.rotation.x, Y, 0.1)), n && (n.rotation.y = a.MathUtils.lerp(n.rotation.y, y * 0.2, 0.1), n.rotation.x = a.MathUtils.lerp(n.rotation.x, Y * 0.2, 0.1)), !P)
|
|
119
|
+
if (!j.current)
|
|
120
|
+
V.current -= M, V.current <= 0 && (j.current = !0);
|
|
121
|
+
else {
|
|
122
|
+
const u = 1e3 / (x || 100);
|
|
123
|
+
s.current += M * u * 2, s.current >= 1 && (s.current = 1, j.current = !1, V.current = Math.random() * ((A - o) / 1e3) + o / 1e3);
|
|
124
|
+
}
|
|
125
|
+
if (!j.current && s.current > 0) {
|
|
126
|
+
const u = 1e3 / (x || 100);
|
|
127
|
+
s.current -= M * u * 2, s.current < 0 && (s.current = 0);
|
|
128
|
+
}
|
|
129
|
+
if (e.expressionManager) {
|
|
130
|
+
f(e, "blink", s.current);
|
|
131
|
+
let u = 0, N = 0, C = 0;
|
|
132
|
+
if (r === "listening" ? (u = 0.35, N = 0.2) : r === "thinking" ? (C = 0.25, N = 0.15, c && (c.rotation.y = a.MathUtils.lerp(c.rotation.y, -0.18, 0.05), c.rotation.x = a.MathUtils.lerp(c.rotation.x, 0.12, 0.05))) : r === "speaking" ? u = 0.15 : u = 0.05, U.current = a.MathUtils.lerp(U.current, u, 0.1), w.current = a.MathUtils.lerp(w.current, N, 0.1), z.current = a.MathUtils.lerp(z.current, C, 0.1), f(e, "happy", U.current), f(e, "relaxed", w.current), f(e, "sad", z.current), r === "speaking") {
|
|
133
|
+
(!I.current || K.current !== h) && (I.current = fe(h), K.current = h);
|
|
134
|
+
const R = I.current.read(), _ = E / 30;
|
|
135
|
+
let ee = 0, te = 0, re = 0;
|
|
136
|
+
R.shape === "e" ? te = R.level * 0.8 * _ : R.shape === "o" ? re = R.level * 0.8 * _ : R.shape === "a" && (ee = R.level * 0.95 * _), g.current = a.MathUtils.lerp(g.current, ee, 0.25), p.current = a.MathUtils.lerp(p.current, te, 0.25), m.current = a.MathUtils.lerp(m.current, re, 0.25), f(e, "aa", g.current), f(e, "ee", p.current), f(e, "oo", m.current);
|
|
137
|
+
} else
|
|
138
|
+
g.current = a.MathUtils.lerp(g.current, 0, 0.2), p.current = a.MathUtils.lerp(p.current, 0, 0.2), m.current = a.MathUtils.lerp(m.current, 0, 0.2), f(e, "aa", g.current), f(e, "ee", p.current), f(e, "oo", m.current);
|
|
139
|
+
e.expressionManager.update();
|
|
140
|
+
}
|
|
141
|
+
}), e ? /* @__PURE__ */ i("primitive", { object: e.scene }) : null;
|
|
142
|
+
}
|
|
143
|
+
function Ne({
|
|
144
|
+
state: l,
|
|
145
|
+
analyser: r,
|
|
146
|
+
size: h = 300,
|
|
147
|
+
className: E = "",
|
|
148
|
+
style: v,
|
|
149
|
+
maxMouthOpening: o,
|
|
150
|
+
blinkIntervalMin: A,
|
|
151
|
+
blinkIntervalMax: x,
|
|
152
|
+
blinkDuration: P,
|
|
153
|
+
mouseTrackingIntensity: S,
|
|
154
|
+
stateColors: t,
|
|
155
|
+
vrmUrl: e
|
|
156
|
+
}) {
|
|
157
|
+
const [B, g] = H(!1), [p, m] = H(null), U = me();
|
|
158
|
+
ie(() => {
|
|
159
|
+
g(!1), m(null);
|
|
160
|
+
}, [e]);
|
|
161
|
+
const w = ne((s) => g(s), []), z = ne((s) => m(s), []), V = {
|
|
162
|
+
idle: (t == null ? void 0 : t.idle) ?? "#4b5563",
|
|
163
|
+
listening: (t == null ? void 0 : t.listening) ?? "#3b82f6",
|
|
164
|
+
thinking: (t == null ? void 0 : t.thinking) ?? "#8b5cf6",
|
|
165
|
+
speaking: (t == null ? void 0 : t.speaking) ?? "#10b981",
|
|
166
|
+
working: (t == null ? void 0 : t.working) ?? "#f59e0b"
|
|
167
|
+
};
|
|
168
|
+
return /* @__PURE__ */ O(
|
|
169
|
+
"div",
|
|
170
|
+
{
|
|
171
|
+
className: `relative flex items-center justify-center rounded-3xl overflow-hidden border border-zinc-800/40 bg-zinc-950/40 ${E}`,
|
|
172
|
+
style: { width: h, height: h, ...v },
|
|
173
|
+
children: [
|
|
174
|
+
/* @__PURE__ */ O(
|
|
175
|
+
ce,
|
|
176
|
+
{
|
|
177
|
+
camera: { position: [0, 1.43, 0.88], fov: 44 },
|
|
178
|
+
shadows: !0,
|
|
179
|
+
gl: { antialias: !0, alpha: !0, preserveDrawingBuffer: !0 },
|
|
180
|
+
style: { width: "100%", height: "100%", background: "transparent" },
|
|
181
|
+
children: [
|
|
182
|
+
/* @__PURE__ */ i("ambientLight", { intensity: 1.5 }),
|
|
183
|
+
/* @__PURE__ */ i(
|
|
184
|
+
"spotLight",
|
|
185
|
+
{
|
|
186
|
+
position: [0, 3, 2],
|
|
187
|
+
angle: 0.6,
|
|
188
|
+
penumbra: 1,
|
|
189
|
+
intensity: 3,
|
|
190
|
+
color: V[l],
|
|
191
|
+
castShadow: !0
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
/* @__PURE__ */ i("directionalLight", { position: [-2, 2, -2], intensity: 1.8, color: "#ffffff" }),
|
|
195
|
+
/* @__PURE__ */ i("directionalLight", { position: [2, 2, 2], intensity: 2.2, color: "#ffffff" }),
|
|
196
|
+
/* @__PURE__ */ i(he, { onError: (s) => m(s), children: e && /* @__PURE__ */ i(se, { fallback: null, children: /* @__PURE__ */ i(
|
|
197
|
+
ge,
|
|
198
|
+
{
|
|
199
|
+
url: e,
|
|
200
|
+
state: l,
|
|
201
|
+
analyser: r,
|
|
202
|
+
maxMouthOpening: o,
|
|
203
|
+
mouseTrackingIntensity: S,
|
|
204
|
+
blinkIntervalMin: A,
|
|
205
|
+
blinkIntervalMax: x,
|
|
206
|
+
blinkDuration: P,
|
|
207
|
+
reducedMotion: U,
|
|
208
|
+
onLoaded: w,
|
|
209
|
+
onError: z
|
|
210
|
+
}
|
|
211
|
+
) }) }),
|
|
212
|
+
/* @__PURE__ */ i(
|
|
213
|
+
pe,
|
|
214
|
+
{
|
|
215
|
+
enableZoom: !1,
|
|
216
|
+
enablePan: !1,
|
|
217
|
+
minPolarAngle: Math.PI / 2.6,
|
|
218
|
+
maxPolarAngle: Math.PI / 1.7,
|
|
219
|
+
minAzimuthAngle: -Math.PI / 4,
|
|
220
|
+
maxAzimuthAngle: Math.PI / 4,
|
|
221
|
+
target: [0, 1.38, 0]
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
),
|
|
227
|
+
!e && /* @__PURE__ */ O("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/90 backdrop-blur-md z-20 p-4 text-center", children: [
|
|
228
|
+
/* @__PURE__ */ i("span", { className: "text-xs font-mono font-bold text-amber-400 uppercase tracking-wider mb-2", children: "Missing vrmUrl" }),
|
|
229
|
+
/* @__PURE__ */ i("p", { className: "text-[10px] text-zinc-500 max-w-[200px] leading-relaxed", children: "Pass a CORS-enabled .vrm URL via the vrmUrl prop." })
|
|
230
|
+
] }),
|
|
231
|
+
e && !B && !p && /* @__PURE__ */ O("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/80 backdrop-blur-md z-20", children: [
|
|
232
|
+
/* @__PURE__ */ i("div", { className: "w-10 h-10 border-4 border-t-emerald-500 border-emerald-500/20 rounded-full animate-spin mb-3" }),
|
|
233
|
+
/* @__PURE__ */ i("span", { className: "text-[10px] font-mono font-bold tracking-widest text-zinc-400 animate-pulse", children: "LOADING NEURAL VRM MODEL..." })
|
|
234
|
+
] }),
|
|
235
|
+
p && /* @__PURE__ */ O("div", { className: "absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/90 backdrop-blur-md z-20 p-4 text-center", children: [
|
|
236
|
+
/* @__PURE__ */ i("span", { className: "text-xs font-mono font-bold text-red-400 uppercase tracking-wider mb-2", children: "Failed to load VRM" }),
|
|
237
|
+
/* @__PURE__ */ i("p", { className: "text-[10px] text-zinc-500 max-w-[200px] leading-relaxed break-all", children: p })
|
|
238
|
+
] })
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
export {
|
|
244
|
+
Ne as VrmAvatar
|
|
245
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react/jsx-runtime"),r=require("react"),re=require("@react-three/fiber"),ae=require("three/addons/loaders/GLTFLoader.js"),I=require("@pixiv/three-vrm"),se=require("@react-three/drei"),ie=require("three"),ne=require("./useReducedMotion-BRDEmRNI.cjs");function oe(s){const n=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(s){for(const c in s)if(c!=="default"){const x=Object.getOwnPropertyDescriptor(s,c);Object.defineProperty(n,c,x.get?x:{enumerable:!0,get:()=>s[c]})}}return n.default=s,Object.freeze(n)}const o=oe(ie);class ce extends r.Component{constructor(n){super(n),this.state={hasError:!1}}static getDerivedStateFromError(){return{hasError:!0}}componentDidCatch(n){this.props.onError((n==null?void 0:n.message)||String(n))}render(){return this.state.hasError?null:this.props.children}}function m(s,n,c){if(!s.expressionManager)return;const x=s.expressionManager.expressionMap||{},N=Object.keys(x);let l=[];switch(n){case"blink":l=["blink","Blink","BLINK"];break;case"aa":l=["aa","Aa","a","A","AA","mouth_a","mouth_A"];break;case"ee":l=["ee","Ee","e","E","EE","mouth_e","mouth_E"];break;case"oo":l=["oh","Oh","oo","Oo","o","O","OO","mouth_o","mouth_O"];break;case"happy":l=["happy","Happy","joy","Joy","JOY","fun","Fun","FUN","HAPPY"];break;case"sad":l=["sad","Sad","sorrow","Sorrow","SORROW","sadness","SAD"];break;case"relaxed":l=["relaxed","Relaxed","fun","Fun","FUN","neutral","Neutral","RELAXED"];break}const j=l.find(b=>x[b]!==void 0||N.includes(b));if(j)s.expressionManager.setValue(j,c);else try{s.expressionManager.setValue(l[0],c)}catch{}}function le({url:s,state:n,analyser:c,maxMouthOpening:x=30,mouseTrackingIntensity:N=1,blinkIntervalMin:l=2e3,blinkIntervalMax:j=6e3,blinkDuration:b=100,reducedMotion:z=!1,onLoaded:L,onError:a}){const[e,D]=r.useState(null);r.useEffect(()=>{let M=!1,y=null;const d=new ae.GLTFLoader;return d.register(i=>new I.VRMLoaderPlugin(i)),d.load(s,i=>{var S,w;if(M){const k=i.userData.vrm;k&&I.VRMUtils.deepDispose(k.scene);return}const R=i.userData.vrm;if(!R){a(`No VRM found at ${s}.`);return}console.log("[VRM] Model loaded successfully. Available expressions:",Object.keys(((S=R.expressionManager)==null?void 0:S.expressionMap)||{})),R.scene.traverse(k=>{k.frustumCulled=!1});const B=((w=R.meta)==null?void 0:w.metaVersion)==="0";R.scene.rotation.y=B?Math.PI:0,y=R,D(R),L(!0)},void 0,i=>{M||a((i==null?void 0:i.message)||String(i))}),()=>{M=!0,y&&I.VRMUtils.deepDispose(y.scene)}},[s,L,a]);const g=r.useRef(0),p=r.useRef(0),h=r.useRef(0),v=r.useRef(0),O=r.useRef(0),U=r.useRef(0),V=r.useRef(Math.random()*3+2),u=r.useRef(0),P=r.useRef(!1),_=r.useRef(null),q=r.useRef(null);return re.useFrame((M,y)=>{var H,K,Y,G,X,J,$,W,Z;if(!e)return;e.update(y);const d=(H=e.humanoid)==null?void 0:H.getNormalizedBoneNode("neck"),i=(K=e.humanoid)==null?void 0:K.getNormalizedBoneNode("head");if(((Y=e.meta)==null?void 0:Y.metaVersion)==="0"){const f=(G=e.humanoid)==null?void 0:G.getNormalizedBoneNode("leftUpperArm"),E=(X=e.humanoid)==null?void 0:X.getNormalizedBoneNode("rightUpperArm");f&&(f.rotation.z=Math.PI/2.6),E&&(E.rotation.z=-Math.PI/2.6)}const B=((J=M.pointer)==null?void 0:J.x)??(($=M.mouse)==null?void 0:$.x)??0,S=((W=M.pointer)==null?void 0:W.y)??((Z=M.mouse)==null?void 0:Z.y)??0,w=z?0:N,k=B*.35*w,T=-S*.2*w;if(d&&(d.rotation.y=o.MathUtils.lerp(d.rotation.y,k,.1),d.rotation.x=o.MathUtils.lerp(d.rotation.x,T,.1)),i&&(i.rotation.y=o.MathUtils.lerp(i.rotation.y,k*.2,.1),i.rotation.x=o.MathUtils.lerp(i.rotation.x,T*.2,.1)),!z)if(!P.current)V.current-=y,V.current<=0&&(P.current=!0);else{const f=1e3/(b||100);u.current+=y*f*2,u.current>=1&&(u.current=1,P.current=!1,V.current=Math.random()*((j-l)/1e3)+l/1e3)}if(!P.current&&u.current>0){const f=1e3/(b||100);u.current-=y*f*2,u.current<0&&(u.current=0)}if(e.expressionManager){m(e,"blink",u.current);let f=0,E=0,Q=0;if(n==="listening"?(f=.35,E=.2):n==="thinking"?(Q=.25,E=.15,d&&(d.rotation.y=o.MathUtils.lerp(d.rotation.y,-.18,.05),d.rotation.x=o.MathUtils.lerp(d.rotation.x,.12,.05))):n==="speaking"?f=.15:f=.05,v.current=o.MathUtils.lerp(v.current,f,.1),O.current=o.MathUtils.lerp(O.current,E,.1),U.current=o.MathUtils.lerp(U.current,Q,.1),m(e,"happy",v.current),m(e,"relaxed",O.current),m(e,"sad",U.current),n==="speaking"){(!_.current||q.current!==c)&&(_.current=ne.createMouthEngine(c),q.current=c);const A=_.current.read(),F=x/30;let C=0,ee=0,te=0;A.shape==="e"?ee=A.level*.8*F:A.shape==="o"?te=A.level*.8*F:A.shape==="a"&&(C=A.level*.95*F),g.current=o.MathUtils.lerp(g.current,C,.25),p.current=o.MathUtils.lerp(p.current,ee,.25),h.current=o.MathUtils.lerp(h.current,te,.25),m(e,"aa",g.current),m(e,"ee",p.current),m(e,"oo",h.current)}else g.current=o.MathUtils.lerp(g.current,0,.2),p.current=o.MathUtils.lerp(p.current,0,.2),h.current=o.MathUtils.lerp(h.current,0,.2),m(e,"aa",g.current),m(e,"ee",p.current),m(e,"oo",h.current);e.expressionManager.update()}}),e?t.jsx("primitive",{object:e.scene}):null}function ue({state:s,analyser:n,size:c=300,className:x="",style:N,maxMouthOpening:l,blinkIntervalMin:j,blinkIntervalMax:b,blinkDuration:z,mouseTrackingIntensity:L,stateColors:a,vrmUrl:e}){const[D,g]=r.useState(!1),[p,h]=r.useState(null),v=ne.useReducedMotion();r.useEffect(()=>{g(!1),h(null)},[e]);const O=r.useCallback(u=>g(u),[]),U=r.useCallback(u=>h(u),[]),V={idle:(a==null?void 0:a.idle)??"#4b5563",listening:(a==null?void 0:a.listening)??"#3b82f6",thinking:(a==null?void 0:a.thinking)??"#8b5cf6",speaking:(a==null?void 0:a.speaking)??"#10b981",working:(a==null?void 0:a.working)??"#f59e0b"};return t.jsxs("div",{className:`relative flex items-center justify-center rounded-3xl overflow-hidden border border-zinc-800/40 bg-zinc-950/40 ${x}`,style:{width:c,height:c,...N},children:[t.jsxs(re.Canvas,{camera:{position:[0,1.43,.88],fov:44},shadows:!0,gl:{antialias:!0,alpha:!0,preserveDrawingBuffer:!0},style:{width:"100%",height:"100%",background:"transparent"},children:[t.jsx("ambientLight",{intensity:1.5}),t.jsx("spotLight",{position:[0,3,2],angle:.6,penumbra:1,intensity:3,color:V[s],castShadow:!0}),t.jsx("directionalLight",{position:[-2,2,-2],intensity:1.8,color:"#ffffff"}),t.jsx("directionalLight",{position:[2,2,2],intensity:2.2,color:"#ffffff"}),t.jsx(ce,{onError:u=>h(u),children:e&&t.jsx(r.Suspense,{fallback:null,children:t.jsx(le,{url:e,state:s,analyser:n,maxMouthOpening:l,mouseTrackingIntensity:L,blinkIntervalMin:j,blinkIntervalMax:b,blinkDuration:z,reducedMotion:v,onLoaded:O,onError:U})})}),t.jsx(se.OrbitControls,{enableZoom:!1,enablePan:!1,minPolarAngle:Math.PI/2.6,maxPolarAngle:Math.PI/1.7,minAzimuthAngle:-Math.PI/4,maxAzimuthAngle:Math.PI/4,target:[0,1.38,0]})]}),!e&&t.jsxs("div",{className:"absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/90 backdrop-blur-md z-20 p-4 text-center",children:[t.jsx("span",{className:"text-xs font-mono font-bold text-amber-400 uppercase tracking-wider mb-2",children:"Missing vrmUrl"}),t.jsx("p",{className:"text-[10px] text-zinc-500 max-w-[200px] leading-relaxed",children:"Pass a CORS-enabled .vrm URL via the vrmUrl prop."})]}),e&&!D&&!p&&t.jsxs("div",{className:"absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/80 backdrop-blur-md z-20",children:[t.jsx("div",{className:"w-10 h-10 border-4 border-t-emerald-500 border-emerald-500/20 rounded-full animate-spin mb-3"}),t.jsx("span",{className:"text-[10px] font-mono font-bold tracking-widest text-zinc-400 animate-pulse",children:"LOADING NEURAL VRM MODEL..."})]}),p&&t.jsxs("div",{className:"absolute inset-0 flex flex-col items-center justify-center bg-zinc-950/90 backdrop-blur-md z-20 p-4 text-center",children:[t.jsx("span",{className:"text-xs font-mono font-bold text-red-400 uppercase tracking-wider mb-2",children:"Failed to load VRM"}),t.jsx("p",{className:"text-[10px] text-zinc-500 max-w-[200px] leading-relaxed break-all",children:p})]})]})}exports.VrmAvatar=ue;
|