|
352 | 352 | } |
353 | 353 | .btn-danger:hover { background: rgba(239,68,68,0.2); } |
354 | 354 |
|
| 355 | + /* ── Emotion Control ── */ |
| 356 | + .emotion-panel { display: flex; flex-direction: column; gap: 6px; } |
| 357 | + .emotion-row { |
| 358 | + display: flex; align-items: center; gap: 8px; font-size: 0.78rem; |
| 359 | + } |
| 360 | + .emotion-label { width: 80px; color: #94a3b8; font-size: 0.72rem; } |
| 361 | + .emotion-slider { |
| 362 | + flex: 1; height: 4px; -webkit-appearance: none; appearance: none; |
| 363 | + background: rgba(255,255,255,0.08); border-radius: 2px; outline: none; |
| 364 | + cursor: pointer; |
| 365 | + } |
| 366 | + .emotion-slider::-webkit-slider-thumb { |
| 367 | + -webkit-appearance: none; width: 14px; height: 14px; |
| 368 | + border-radius: 50%; background: #4cc9f0; cursor: pointer; |
| 369 | + } |
| 370 | + .emotion-slider::-moz-range-thumb { |
| 371 | + width: 14px; height: 14px; border: none; |
| 372 | + border-radius: 50%; background: #4cc9f0; cursor: pointer; |
| 373 | + } |
| 374 | + .emotion-val { width: 32px; text-align: right; color: #e2e8f0; font-size: 0.72rem; font-variant-numeric: tabular-nums; } |
| 375 | + .emotion-presets { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; } |
| 376 | + .emotion-presets button { |
| 377 | + padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); |
| 378 | + background: rgba(255,255,255,0.04); color: #94a3b8; |
| 379 | + font-size: 0.7rem; cursor: pointer; transition: all 0.15s; |
| 380 | + } |
| 381 | + .emotion-presets button:hover { background: rgba(76,201,240,0.12); color: #4cc9f0; border-color: rgba(76,201,240,0.3); } |
| 382 | + .emotion-presets button.active { background: rgba(76,201,240,0.15); color: #4cc9f0; border-color: #4cc9f0; } |
| 383 | + |
355 | 384 | /* ── Buttons ── */ |
356 | 385 | .btn { |
357 | 386 | display: inline-flex; align-items: center; gap: 8px; |
@@ -816,6 +845,30 @@ <h2 class="step-title">Add Real-time Microphone</h2> |
816 | 845 |
|
817 | 846 | <div class="demo-divider"></div> |
818 | 847 |
|
| 848 | + <!-- Emotion Control (V2 only) --> |
| 849 | + <div class="demo-step-group" id="emotion-group" style="display:none"> |
| 850 | + <div class="demo-step-label"> |
| 851 | + <span style="font-size:0.78rem;color:#4cc9f0">Emotion Control</span> |
| 852 | + <span style="font-size:0.65rem;color:#64748b;margin-left:6px">V2 only</span> |
| 853 | + </div> |
| 854 | + <div class="emotion-panel" id="emotion-panel"> |
| 855 | + <div class="emotion-row"><span class="emotion-label">Neutral</span><input type="range" class="emotion-slider" id="emo-neutral" min="0" max="100" value="0"><span class="emotion-val" id="emo-neutral-val">0%</span></div> |
| 856 | + <div class="emotion-row"><span class="emotion-label">Joy</span><input type="range" class="emotion-slider" id="emo-joy" min="0" max="100" value="0"><span class="emotion-val" id="emo-joy-val">0%</span></div> |
| 857 | + <div class="emotion-row"><span class="emotion-label">Anger</span><input type="range" class="emotion-slider" id="emo-anger" min="0" max="100" value="0"><span class="emotion-val" id="emo-anger-val">0%</span></div> |
| 858 | + <div class="emotion-row"><span class="emotion-label">Sadness</span><input type="range" class="emotion-slider" id="emo-sadness" min="0" max="100" value="0"><span class="emotion-val" id="emo-sadness-val">0%</span></div> |
| 859 | + <div class="emotion-row"><span class="emotion-label">Surprise</span><input type="range" class="emotion-slider" id="emo-surprise" min="0" max="100" value="0"><span class="emotion-val" id="emo-surprise-val">0%</span></div> |
| 860 | + </div> |
| 861 | + <div class="emotion-presets"> |
| 862 | + <button data-preset="neutral">Neutral</button> |
| 863 | + <button data-preset="joy">Happy</button> |
| 864 | + <button data-preset="anger">Angry</button> |
| 865 | + <button data-preset="sadness">Sad</button> |
| 866 | + <button data-preset="surprise">Surprised</button> |
| 867 | + </div> |
| 868 | + </div> |
| 869 | + |
| 870 | + <div class="demo-divider" id="emotion-divider" style="display:none"></div> |
| 871 | + |
819 | 872 | <!-- Step 6: Mic --> |
820 | 873 | <div class="demo-step-group" id="demo-group-6"> |
821 | 874 | <div class="demo-step-label"> |
@@ -1097,6 +1150,11 @@ <h2 class="step-title">Add Real-time Microphone</h2> |
1097 | 1150 | completeStep(3); |
1098 | 1151 | setStatus(`${label} engine ready. Upload a VRM to continue.`); |
1099 | 1152 |
|
| 1153 | + // Show/hide emotion panel based on engine |
| 1154 | + showEmotionPanel(selectedEngine === 'v2'); |
| 1155 | + resetEmotionUI(); |
| 1156 | + if (selectedEngine === 'v2') updateEmotionVector(); |
| 1157 | + |
1100 | 1158 | // Reload VRM to rebind VRMA bone animations from new engine |
1101 | 1159 | if (vrm) { |
1102 | 1160 | const vrmScene = vrm.scene; |
@@ -1140,6 +1198,56 @@ <h2 class="step-title">Add Real-time Microphone</h2> |
1140 | 1198 | } |
1141 | 1199 | }); |
1142 | 1200 |
|
| 1201 | +// ════════════════════════════════════════ |
| 1202 | +// Emotion Control (V2 only) |
| 1203 | +// ════════════════════════════════════════ |
| 1204 | +const emotionGroup = $('emotion-group'); |
| 1205 | +const emotionDivider = $('emotion-divider'); |
| 1206 | +const EMOTION_KEYS = ['neutral', 'joy', 'anger', 'sadness', 'surprise']; |
| 1207 | +const emotionSliders = EMOTION_KEYS.map(k => $(`emo-${k}`)); |
| 1208 | +const emotionVals = EMOTION_KEYS.map(k => $(`emo-${k}-val`)); |
| 1209 | + |
| 1210 | +function updateEmotionVector() { |
| 1211 | + const vec = emotionSliders.map(s => parseInt(s.value) / 100); |
| 1212 | + if (lipsync?.setEmotion) { |
| 1213 | + try { lipsync.setEmotion(vec); } catch (e) { console.warn('setEmotion:', e.message); } |
| 1214 | + } |
| 1215 | +} |
| 1216 | + |
| 1217 | +function showEmotionPanel(show) { |
| 1218 | + emotionGroup.style.display = show ? '' : 'none'; |
| 1219 | + emotionDivider.style.display = show ? '' : 'none'; |
| 1220 | +} |
| 1221 | + |
| 1222 | +function resetEmotionUI() { |
| 1223 | + emotionSliders.forEach(s => { s.value = 0; }); |
| 1224 | + emotionVals.forEach(v => { v.textContent = '0%'; }); |
| 1225 | + document.querySelectorAll('.emotion-presets button').forEach(b => b.classList.remove('active')); |
| 1226 | +} |
| 1227 | + |
| 1228 | +// Slider input events |
| 1229 | +emotionSliders.forEach((slider, i) => { |
| 1230 | + slider.addEventListener('input', () => { |
| 1231 | + emotionVals[i].textContent = slider.value + '%'; |
| 1232 | + document.querySelectorAll('.emotion-presets button').forEach(b => b.classList.remove('active')); |
| 1233 | + updateEmotionVector(); |
| 1234 | + }); |
| 1235 | +}); |
| 1236 | + |
| 1237 | +// Preset buttons |
| 1238 | +document.querySelectorAll('.emotion-presets button').forEach(btn => { |
| 1239 | + btn.addEventListener('click', () => { |
| 1240 | + const preset = btn.dataset.preset; |
| 1241 | + document.querySelectorAll('.emotion-presets button').forEach(b => b.classList.remove('active')); |
| 1242 | + btn.classList.add('active'); |
| 1243 | + emotionSliders.forEach((s, i) => { |
| 1244 | + s.value = EMOTION_KEYS[i] === preset ? 100 : 0; |
| 1245 | + emotionVals[i].textContent = s.value + '%'; |
| 1246 | + }); |
| 1247 | + updateEmotionVector(); |
| 1248 | + }); |
| 1249 | +}); |
| 1250 | + |
1143 | 1251 | // ════════════════════════════════════════ |
1144 | 1252 | // VRM Upload (button + drop + drag) |
1145 | 1253 | // ════════════════════════════════════════ |
|
0 commit comments