// BlendShape Animation SDK - Engine Agnostic class BlendShapeAnimator { constructor(config = {}) { this.morphTargetAdapter = null; this.animationFrames = []; this.isPlaying = false; this.currentFrameIndex = 0; this.animationStartTime = 0; this.idleAnimations = {}; this.blendShapeScale = config.blendShapeScale || 1.0; this.dataFps = config.dataFps || 30; // 空闲动画参数 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 = {}; // 状态标志 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; } // 播放动画 playAnimation() { if (this.animationFrames.length === 0) { this.onStatusChange('error', '请先加载动画数据'); return; } if (!this.morphTargetAdapter) { this.onStatusChange('error', '未设置形态键适配器'); return; } // 停止随机表情 if (this.isExpressionEnabled && window.ExpressionLibrary) { window.ExpressionLibrary.randomPlayer.stop(); } this.stopAnimation(); this.isPlaying = true; this.currentFrameIndex = 0; this.animationStartTime = performance.now(); this._animateFrame(); this.onStatusChange('info', '播放中...'); } // 停止动画 stopAnimation() { this.isPlaying = false; // 恢复随机表情 if (this.isExpressionEnabled && window.ExpressionLibrary) { window.ExpressionLibrary.randomPlayer.start(); } this.onStatusChange('info', '已停止'); } // 内部动画帧处理 _animateFrame() { if (!this.isPlaying) return; const now = performance.now(); const frameDuration = 1000 / this.dataFps; const elapsed = now - this.animationStartTime; const exactFrame = elapsed / frameDuration; const targetFrameIndex = Math.floor(exactFrame); if (targetFrameIndex >= this.animationFrames.length) { this.stopAnimation(); return; } const currentFrame = this.animationFrames[targetFrameIndex]; const nextFrame = this.animationFrames[Math.min(targetFrameIndex + 1, this.animationFrames.length - 1)]; const frameProgress = exactFrame - targetFrameIndex; const smoothProgress = this._easeOutQuad(frameProgress); for (const key in currentFrame.blendShapes) { const currentValue = currentFrame.blendShapes[key] || 0; const nextValue = nextFrame.blendShapes[key] || 0; const interpolatedValue = this._lerp(currentValue, nextValue, smoothProgress); const scaledValue = interpolatedValue * this.blendShapeScale; this.morphTargetAdapter.setInfluence(key, scaledValue); } this.currentFrameIndex = targetFrameIndex; 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(); scheduleNext(); } }, 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) { this._doRandomEyeLook(); scheduleNext(); } }, 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); } _resetEyeLook() { const eyeLookShapes = [ 'eyelookupleft', 'eyelookupright', 'eyelookdownleft', 'eyelookdownright', 'eyelookinleft', 'eyelookinright', 'eyelookoutleft', 'eyelookoutright' ]; eyeLookShapes.forEach(name => { this.setIdleAnimation(name, 0, this.eyeLookParams.speed, 'easeInOutQuad'); }); } // 随机表情控制 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; } _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; }