Skip to content

Commit 5b4b96d

Browse files
jcgglclaude
andcommitted
feat: add V2 emotion control bar to comparison page
- Compact emotion sliders (5 channels) + preset buttons in V2 pane header - Real-time setEmotion() on slider input for mic streaming - Debounced reInferWithEmotion() (300ms) for file playback - V2 queue refilled with new emotion frames on re-inference Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 64d0e06 commit 5b4b96d

File tree

1 file changed

+87
-2
lines changed

1 file changed

+87
-2
lines changed

examples/vanilla-comparison/index.html

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,32 @@
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)
425509
for (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

Comments
 (0)