152152 .init-bar-fill .v1 { background : # f59e0b ; }
153153 .init-bar-fill .v2 { background : # 10b981 ; }
154154
155+ /* ── Emotion Control (V2 pane) ── */
156+ .emotion-bar {
157+ display : flex; align-items : center; gap : 6px ; padding : 6px 16px ;
158+ border-bottom : 1px solid rgba (255 , 255 , 255 , 0.04 ); flex-wrap : wrap;
159+ }
160+ .emotion-bar-label { font-size : 0.65rem ; color : # 10b981 ; font-weight : 600 ; white-space : nowrap; }
161+ .emo-slider-group { display : flex; align-items : center; gap : 3px ; }
162+ .emo-slider-group span { font-size : 0.6rem ; color : # 64748b ; width : 42px ; }
163+ .emo-slider-group input [type = range ] {
164+ width : 60px ; height : 3px ; -webkit-appearance : none; appearance : none;
165+ background : rgba (255 , 255 , 255 , 0.08 ); border-radius : 2px ; cursor : pointer;
166+ }
167+ .emo-slider-group input [type = range ]::-webkit-slider-thumb {
168+ -webkit-appearance : none; width : 10px ; height : 10px ; border-radius : 50% ; background : # 10b981 ;
169+ }
170+ .emo-slider-group input [type = range ]::-moz-range-thumb {
171+ width : 10px ; height : 10px ; border : none; border-radius : 50% ; background : # 10b981 ;
172+ }
173+ .emo-presets { display : flex; gap : 3px ; margin-left : 4px ; }
174+ .emo-presets button {
175+ padding : 2px 8px ; border-radius : 4px ; border : 1px solid rgba (255 , 255 , 255 , 0.08 );
176+ background : transparent; color : # 64748b ; font-size : 0.6rem ; cursor : pointer;
177+ }
178+ .emo-presets button : hover { color : # 10b981 ; border-color : rgba (16 , 185 , 129 , 0.3 ); }
179+ .emo-presets button .active { color : # 10b981 ; border-color : # 10b981 ; background : rgba (16 , 185 , 129 , 0.08 ); }
180+
155181 footer {
156182 padding : 10px 24px ; text-align : center; font-size : 0.65rem ; color : # 1e293b ;
157183 border-top : 1px solid rgba (255 , 255 , 255 , 0.04 );
@@ -209,6 +235,21 @@ <h1>Anima<span>Sync</span></h1>
209235 < span class ="pane-title v2 "> V2 — Emotion Model</ span >
210236 < span class ="pane-dim "> 52-dim ARKit</ span >
211237 </ div >
238+ < div class ="emotion-bar " id ="emotion-bar ">
239+ < span class ="emotion-bar-label "> Emotion</ span >
240+ < div class ="emo-slider-group "> < span > Neutral</ span > < input type ="range " class ="emo-v2 " data-idx ="0 " min ="0 " max ="100 " value ="0 "> </ div >
241+ < div class ="emo-slider-group "> < span > Joy</ span > < input type ="range " class ="emo-v2 " data-idx ="1 " min ="0 " max ="100 " value ="0 "> </ div >
242+ < div class ="emo-slider-group "> < span > Anger</ span > < input type ="range " class ="emo-v2 " data-idx ="2 " min ="0 " max ="100 " value ="0 "> </ div >
243+ < div class ="emo-slider-group "> < span > Sadness</ span > < input type ="range " class ="emo-v2 " data-idx ="3 " min ="0 " max ="100 " value ="0 "> </ div >
244+ < div class ="emo-slider-group "> < span > Surprise</ span > < input type ="range " class ="emo-v2 " data-idx ="4 " min ="0 " max ="100 " value ="0 "> </ div >
245+ < div class ="emo-presets ">
246+ < button data-emo ="0 "> Neutral</ button >
247+ < button data-emo ="1 "> Happy</ button >
248+ < button data-emo ="2 "> Angry</ button >
249+ < button data-emo ="3 "> Sad</ button >
250+ < button data-emo ="4 "> Surprised</ button >
251+ </ div >
252+ </ div >
212253 < div class ="canvas-wrap ">
213254 < canvas id ="canvas-v2 "> </ canvas >
214255 < div class ="vrm-drop " id ="vrm-drop-v2 ">
@@ -421,6 +462,49 @@ <h1>Anima<span>Sync</span></h1>
421462 for ( const name of names ) vrm . expressionManager . setValue ( name , 0 ) ;
422463}
423464
465+ // ================================================================
466+ // Emotion Control (V2 only)
467+ // ================================================================
468+ const emoSliders = document . querySelectorAll ( '.emo-v2' ) ;
469+ const emoPresetBtns = document . querySelectorAll ( '.emo-presets button' ) ;
470+ let _emoReInferTimer = null ;
471+ let fileResultV2 = null ; // reference for reInferWithEmotion
472+
473+ function applyV2Emotion ( ) {
474+ if ( ! lsV2 ?. setEmotion ) return ;
475+ const vec = Array . from ( emoSliders ) . map ( s => parseInt ( s . value ) / 100 ) ;
476+ try { lsV2 . setEmotion ( vec ) ; } catch ( e ) { console . warn ( 'setEmotion:' , e ) ; }
477+
478+ // File playback: debounced reInfer for V2 queue (mic uses setEmotion only)
479+ if ( ! micActive && lsV2 . reInferWithEmotion && fileResultV2 ) {
480+ clearTimeout ( _emoReInferTimer ) ;
481+ _emoReInferTimer = setTimeout ( async ( ) => {
482+ try {
483+ const newR2 = await lsV2 . reInferWithEmotion ( ) ;
484+ // Refill V2 queue from current playback position
485+ queueV2 . length = 0 ;
486+ for ( let i = 0 ; i < newR2 . frame_count ; i ++ ) {
487+ queueV2 . push ( { arkit : lsV2 . getFrame ( newR2 , i ) , vrm : lsV2 . getVrmFrame ?. ( newR2 , i ) } ) ;
488+ }
489+ fileResultV2 = newR2 ;
490+ } catch ( e ) { console . warn ( 'reInferWithEmotion:' , e . message ) ; }
491+ } , 300 ) ;
492+ }
493+ }
494+
495+ emoSliders . forEach ( s => s . addEventListener ( 'input' , ( ) => {
496+ emoPresetBtns . forEach ( b => b . classList . remove ( 'active' ) ) ;
497+ applyV2Emotion ( ) ;
498+ } ) ) ;
499+
500+ emoPresetBtns . forEach ( btn => btn . addEventListener ( 'click' , ( ) => {
501+ const idx = parseInt ( btn . dataset . emo ) ;
502+ emoPresetBtns . forEach ( b => b . classList . remove ( 'active' ) ) ;
503+ btn . classList . add ( 'active' ) ;
504+ emoSliders . forEach ( s => { s . value = parseInt ( s . dataset . idx ) === idx ? 100 : 0 ; } ) ;
505+ applyV2Emotion ( ) ;
506+ } ) ) ;
507+
424508// VRM drop handler (drops onto either pane, loads into both)
425509for ( const id of [ 'vrm-drop-v1' , 'vrm-drop-v2' ] ) {
426510 const el = document . getElementById ( id ) ;
@@ -528,9 +612,10 @@ <h1>Anima<span>Sync</span></h1>
528612 document . getElementById ( 's-v1-wasm' ) . textContent = ( e1 / 1000 ) . toFixed ( 2 ) + 's' ;
529613 document . getElementById ( 's-v1-rtf' ) . textContent = ( e1 / 1000 / dur1 ) . toFixed ( 2 ) + 'x' ;
530614
531- // Process V2
615+ // Process V2 (cache result for reInferWithEmotion)
532616 const t2 = performance . now ( ) ;
533617 const r2 = await lsV2 . processFile ( file ) ;
618+ fileResultV2 = r2 ;
534619 const e2 = performance . now ( ) - t2 ;
535620 const dur2 = Math . max ( r2 . frame_count / r2 . fps , 0.001 ) ;
536621 document . getElementById ( 's-v2-frames' ) . textContent = r2 . frame_count ;
@@ -553,7 +638,7 @@ <h1>Anima<span>Sync</span></h1>
553638 const audioBuf = await fileAudioCtx . decodeAudioData ( buf ) ;
554639 const src = fileAudioCtx . createBufferSource ( ) ;
555640 src . buffer = audioBuf ; src . connect ( fileAudioCtx . destination ) ; src . start ( ) ;
556- src . onended = ( ) => { transitionToIdle ( ) ; resetVRM ( vrmV1 ) ; resetVRM ( vrmV2 ) ; } ;
641+ src . onended = ( ) => { transitionToIdle ( ) ; resetVRM ( vrmV1 ) ; resetVRM ( vrmV2 ) ; fileResultV2 = null ; } ;
557642 } catch ( err ) {
558643 console . error ( 'Process error:' , err ) ;
559644 }
0 commit comments