// BlendShape Animation SDK - Engine Agnostic class BlendShapeAnimator { constructor(config = {}) { this.morphTargetAdapter = null; this.animationFrames = []; this.animationShapeNames = []; this.isPlaying = false; this.currentFrameIndex = 0; this.currentSentenceIndex = -1; this.animationStartTime = 0; this.idleAnimations = {}; this.blendShapeScale = config.blendShapeScale || 1.0; this.dataFps = config.dataFps || 30; this.isStreaming = false; this.streamingComplete = true; this.streamingWaitStart = null; this.streamingStallMs = 0; this.sentenceTexts = []; // 句子文本列表 // 空闲动画参数 this.blinkParams = config.blinkParams || { intervalMin: 2000, intervalMax: 5000, duration: 150, speed: 100 }; this.eyeLookParams = config.eyeLookParams || { intervalMin: 2000, intervalMax: 6000, durationMin: 1000, durationMax: 2500, speed: 250 }; this.expressionParams = config.expressionParams || { intervalMin: 3000, intervalMax: 8000, speed: 400 }; this.enabledExpressions = new Set(); this.expressionDurations = {}; // 播放动画时禁用的 blendshape 列表(由空闲动画控制) this.disabledShapesInAnimation = config.disabledShapesInAnimation || [ 'eyeblinkleft', 'eyeblinkright', // 眨眼 'browdownleft', 'browdownright', // 眉毛下 'browinnerup', // 眉毛内上 'browouterupleft', 'browouterupright' // 眉毛外上 ]; // 状态标志 this.isBlinkEnabled = false; this.isEyeLookEnabled = false; this.isExpressionEnabled = false; // 定时器 this.blinkInterval = null; this.eyeLookInterval = null; // 回调 this.onStatusChange = config.onStatusChange || (() => {}); // 启动空闲动画循环 this._updateIdleAnimations(); } // 设置形态键适配器 setMorphTargetAdapter(adapter) { this.morphTargetAdapter = adapter; } // 加载动画帧数据 loadAnimationFrames(frames) { this.animationFrames = frames || []; this.animationShapeNames = this._collectAnimationShapeNames(this.animationFrames); } appendAnimationFrames(frames) { if (!Array.isArray(frames) || frames.length === 0) { return; } this.animationFrames.push(...frames); const newNames = this._collectAnimationShapeNames(frames); if (newNames.length === 0) { return; } const existingNames = new Set(this.animationShapeNames); newNames.forEach(name => { if (!existingNames.has(name)) { existingNames.add(name); this.animationShapeNames.push(name); } }); } startStreaming() { console.log('Starting streaming mode'); this.isStreaming = true; this.streamingComplete = false; this.streamingWaitStart = null; this.streamingStallMs = 0; } endStreaming() { console.log('Ending streaming mode'); this.streamingComplete = true; this.isStreaming = false; this.streamingWaitStart = null; this.streamingStallMs = 0; } // 播放动画 playAnimation() { if (this.animationFrames.length === 0) { this.onStatusChange('error', '请先加载动画数据'); return; } if (!this.morphTargetAdapter) { this.onStatusChange('error', '未设置形态键适配器'); return; } // 停止并立即重置眼球移动 if (this.isEyeLookEnabled) { this._stopRandomEyeLook(); this._immediateResetEyeLook(); } // 停止并立即重置随机表情 if (this.isExpressionEnabled && window.ExpressionLibrary) { window.ExpressionLibrary.randomPlayer.stop(); this._immediateResetExpressions(); } // 注意:不停止眨眼,让眨眼继续运行 this.stopAnimation(false); this.isPlaying = true; this.currentFrameIndex = 0; this.animationStartTime = performance.now(); this.streamingWaitStart = null; this.streamingStallMs = 0; this._animateFrame(); this.onStatusChange('info', '播放中...'); } // 停止动画 stopAnimation(resumeExpressions = true) { this.isPlaying = false; this._resetAnimationInfluences(); // 恢复眼球移动 if (resumeExpressions && this.isEyeLookEnabled) { this._startRandomEyeLook(); } // 恢复随机表情 if (resumeExpressions && this.isExpressionEnabled && window.ExpressionLibrary) { window.ExpressionLibrary.randomPlayer.start(); } this.onStatusChange('info', '已停止'); } _collectAnimationShapeNames(frames) { const names = new Set(); frames.forEach(frame => { const blendShapes = frame?.blendShapes; if (!blendShapes) return; Object.keys(blendShapes).forEach(name => names.add(name)); }); return Array.from(names); } _resetAnimationInfluences() { if (!this.morphTargetAdapter || this.animationShapeNames.length === 0) { return; } this.animationShapeNames.forEach(name => { this.morphTargetAdapter.setInfluence(name, 0); }); } // 内部动画帧处理 _animateFrame() { if (!this.isPlaying) return; const now = performance.now(); if (this.streamingWaitStart !== null && this.animationFrames.length > this.currentFrameIndex + 1) { this.streamingStallMs += now - this.streamingWaitStart; this.streamingWaitStart = null; } const frameDuration = 1000 / this.dataFps; const elapsed = now - this.animationStartTime - this.streamingStallMs; const exactFrame = elapsed / frameDuration; const targetFrameIndex = Math.floor(exactFrame); if (targetFrameIndex >= this.animationFrames.length) { if (this.isStreaming && !this.streamingComplete) { if (this.streamingWaitStart === null) { this.streamingWaitStart = now; console.log(`Waiting for more frames... (current: ${this.animationFrames.length}, target: ${targetFrameIndex})`); } const waitTime = now - this.streamingWaitStart; if (waitTime > 30000) { console.warn('Streaming timeout after 30s, stopping animation'); this.stopAnimation(); return; } if (waitTime > 1000 && Math.floor(waitTime / 1000) !== Math.floor((waitTime - 16) / 1000)) { console.log(`Still waiting... ${Math.floor(waitTime / 1000)}s`); } requestAnimationFrame(() => this._animateFrame()); return; } console.log(`Animation complete. Total frames: ${this.animationFrames.length}`); this.stopAnimation(); return; } const currentFrame = this.animationFrames[targetFrameIndex]; const nextFrame = this.animationFrames[Math.min(targetFrameIndex + 1, this.animationFrames.length - 1)]; const currentBlendShapes = currentFrame?.blendShapes || {}; const nextBlendShapes = nextFrame?.blendShapes || {}; const frameProgress = exactFrame - targetFrameIndex; const smoothProgress = this._easeOutQuad(frameProgress); const shapeNames = this.animationShapeNames.length > 0 ? this.animationShapeNames : Object.keys(currentBlendShapes); for (const key of shapeNames) { // 跳过禁用列表中的 blendshape,让空闲动画继续控制它们 if (this.disabledShapesInAnimation.includes(key.toLowerCase())) { continue; } const currentValue = currentBlendShapes[key] || 0; const nextValue = nextBlendShapes[key] || 0; const interpolatedValue = this._lerp(currentValue, nextValue, smoothProgress); const scaledValue = interpolatedValue * this.blendShapeScale; this.morphTargetAdapter.setInfluence(key, scaledValue); } this.currentFrameIndex = targetFrameIndex; // 更新当前句子显示 const sentenceIndex = currentFrame?.sentenceIndex ?? -1; if (sentenceIndex !== this.currentSentenceIndex) { this.currentSentenceIndex = sentenceIndex; this._updateCurrentSentenceDisplay(); } requestAnimationFrame(() => this._animateFrame()); } // 设置空闲动画 setIdleAnimation(name, target, duration = 200, easing = 'easeOutQuad') { this.idleAnimations[name] = { target: target, duration: duration, easing: easing, startTime: null, startValue: this.idleAnimations[name]?.target || 0 }; } // 更新空闲动画 _updateIdleAnimations() { const now = performance.now(); for (const name in this.idleAnimations) { const anim = this.idleAnimations[name]; if (!this.morphTargetAdapter) continue; if (!anim.startTime) { anim.startTime = now; anim.startValue = this.morphTargetAdapter.getInfluence(name) || 0; } const elapsed = now - anim.startTime; const duration = anim.duration || 200; let progress = Math.min(elapsed / duration, 1.0); progress = this._applyEasing(progress, anim.easing); const currentValue = this._lerp(anim.startValue, anim.target, progress); this.morphTargetAdapter.setInfluence(name, currentValue); if (progress >= 1.0) { if (anim.target === 0) { delete this.idleAnimations[name]; } else { anim.startTime = null; } } } requestAnimationFrame(() => this._updateIdleAnimations()); } // 眨眼控制 toggleBlink(enabled) { this.isBlinkEnabled = enabled; if (enabled) { this._startRandomBlink(); this.onStatusChange('success', '随机眨眼已开启'); } else { this._stopRandomBlink(); this.onStatusChange('info', '随机眨眼已关闭'); } } _startRandomBlink() { this._stopRandomBlink(); const scheduleNext = () => { const delay = this.blinkParams.intervalMin + Math.random() * (this.blinkParams.intervalMax - this.blinkParams.intervalMin); this.blinkInterval = setTimeout(() => { if (this.isBlinkEnabled) { this._doBlink(); const holdDuration = Math.max(0, this.blinkParams.duration || 0); this.blinkInterval = setTimeout(() => { if (this.isBlinkEnabled) { scheduleNext(); } }, holdDuration); } }, delay); }; scheduleNext(); } _stopRandomBlink() { if (this.blinkInterval) { clearTimeout(this.blinkInterval); this.blinkInterval = null; } } _doBlink() { const blinkShapes = ['eyeblinkleft', 'eyeblinkright']; blinkShapes.forEach(name => { this.setIdleAnimation(name, 1.0, this.blinkParams.speed, 'easeOutQuad'); }); setTimeout(() => { blinkShapes.forEach(name => { this.setIdleAnimation(name, 0, this.blinkParams.speed + 20, 'easeInOutQuad'); }); }, this.blinkParams.duration); } // 眼球移动控制 toggleEyeLook(enabled) { this.isEyeLookEnabled = enabled; if (enabled) { this._startRandomEyeLook(); this.onStatusChange('success', '眼球移动已开启'); } else { this._stopRandomEyeLook(); this._resetEyeLook(); this.onStatusChange('info', '眼球移动已关闭'); } } _startRandomEyeLook() { this._stopRandomEyeLook(); const scheduleNext = () => { const delay = this.eyeLookParams.intervalMin + Math.random() * (this.eyeLookParams.intervalMax - this.eyeLookParams.intervalMin); this.eyeLookInterval = setTimeout(() => { if (this.isEyeLookEnabled) { const holdDuration = this._doRandomEyeLook(); const waitDuration = Math.max(0, holdDuration || 0); this.eyeLookInterval = setTimeout(() => { if (this.isEyeLookEnabled) { scheduleNext(); } }, waitDuration); } }, delay); }; scheduleNext(); } _stopRandomEyeLook() { if (this.eyeLookInterval) { clearTimeout(this.eyeLookInterval); this.eyeLookInterval = null; } } _doRandomEyeLook() { const directions = [ { shapes: {} }, { shapes: { 'eyelookupleft': 0.3, 'eyelookupright': 0.3 } }, { shapes: { 'eyelookdownleft': 0.3, 'eyelookdownright': 0.3 } }, { shapes: { 'eyelookinleft': 0.4, 'eyelookoutright': 0.4 } }, { shapes: { 'eyelookoutleft': 0.4, 'eyelookinright': 0.4 } } ]; const direction = directions[Math.floor(Math.random() * directions.length)]; const eyeLookShapes = [ 'eyelookupleft', 'eyelookupright', 'eyelookdownleft', 'eyelookdownright', 'eyelookinleft', 'eyelookinright', 'eyelookoutleft', 'eyelookoutright' ]; eyeLookShapes.forEach(name => { this.setIdleAnimation(name, 0, this.eyeLookParams.speed * 0.8, 'easeInOutQuad'); }); for (const [name, value] of Object.entries(direction.shapes)) { this.setIdleAnimation(name, value, this.eyeLookParams.speed, 'easeInOutQuad'); } const holdDuration = this.eyeLookParams.durationMin + Math.random() * (this.eyeLookParams.durationMax - this.eyeLookParams.durationMin); setTimeout(() => { if (this.isEyeLookEnabled) { eyeLookShapes.forEach(name => { this.setIdleAnimation(name, 0, this.eyeLookParams.speed * 1.2, 'easeInOutQuad'); }); } }, holdDuration); return holdDuration; } _resetEyeLook() { const eyeLookShapes = [ 'eyelookupleft', 'eyelookupright', 'eyelookdownleft', 'eyelookdownright', 'eyelookinleft', 'eyelookinright', 'eyelookoutleft', 'eyelookoutright' ]; eyeLookShapes.forEach(name => { this.setIdleAnimation(name, 0, this.eyeLookParams.speed, 'easeInOutQuad'); }); } _immediateResetEyeLook() { const eyeLookShapes = [ 'eyelookupleft', 'eyelookupright', 'eyelookdownleft', 'eyelookdownright', 'eyelookinleft', 'eyelookinright', 'eyelookoutleft', 'eyelookoutright' ]; if (!this.morphTargetAdapter) return; eyeLookShapes.forEach(name => { this.morphTargetAdapter.setInfluence(name, 0); delete this.idleAnimations[name]; }); } _immediateResetExpressions() { if (!this.morphTargetAdapter || !window.ExpressionLibrary) return; // 获取所有表情的 blendshape 名称并立即重置 const expressions = window.ExpressionLibrary.expressions; for (const exprKey in expressions) { const expr = expressions[exprKey]; if (expr.blendShapes) { for (const shapeName in expr.blendShapes) { this.morphTargetAdapter.setInfluence(shapeName, 0); delete this.idleAnimations[shapeName]; } } } } // 随机表情控制 toggleRandomExpression(enabled) { if (!window.ExpressionLibrary) { this.onStatusChange('error', '表情库未加载'); return; } this.isExpressionEnabled = enabled; if (enabled) { window.ExpressionLibrary.randomPlayer.intervalMin = this.expressionParams.intervalMin; window.ExpressionLibrary.randomPlayer.intervalMax = this.expressionParams.intervalMax; if (!this.isPlaying) { window.ExpressionLibrary.randomPlayer.start(); } this.onStatusChange('success', '随机表情已开启'); } else { window.ExpressionLibrary.randomPlayer.stop(); this.onStatusChange('info', '随机表情已关闭'); } } // 缓动函数 _easeOutQuad(t) { return t * (2 - t); } _lerp(start, end, t) { return start + (end - start) * t; } _updateCurrentSentenceDisplay() { const sentenceDiv = document.getElementById('currentSentence'); const sentenceText = document.getElementById('sentenceText'); if (!sentenceDiv || !sentenceText) return; if (this.currentSentenceIndex >= 0 && this.currentSentenceIndex < this.sentenceTexts.length) { sentenceDiv.style.display = 'block'; sentenceText.textContent = this.sentenceTexts[this.currentSentenceIndex]; console.log(`[前端调试] 显示句子 ${this.currentSentenceIndex}: ${this.sentenceTexts[this.currentSentenceIndex]}`); } else { sentenceDiv.style.display = 'none'; } } _applyEasing(t, type) { switch(type) { case 'easeOutQuad': return t * (2 - t); case 'easeInOutQuad': return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; case 'easeInOutCubic': return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; case 'easeOutCubic': return 1 - Math.pow(1 - t, 3); case 'linear': default: return t; } } // 更新配置 updateConfig(key, value) { if (key === 'blendShapeScale') { this.blendShapeScale = value; } else if (this[key]) { this[key] = value; } } } // 导出到全局 if (typeof window !== 'undefined') { window.BlendShapeAnimator = BlendShapeAnimator; }