/** * 虚拟猫头鹰老师模块 * 提供3D猫头鹰角色、语音朗读、状态管理等功能 */ class VirtualTeacher { constructor(containerId = 'virtual-teacher-container') { this.containerId = containerId; this.container = null; this.scene = null; this.camera = null; this.renderer = null; this.owl = null; this.isSpeaking = false; this.animationId = null; this.currentExpression = "normal"; this.speechSynthesis = window.speechSynthesis; this.currentUtterance = null; this.isInitialized = false; this.speechQueue = []; this.isPlaying = false; // 状态管理 this.gameStats = { correct: 0, incorrect: 0, total: 0, streak: 0, maxStreak: 0 }; // 表情和状态映射 this.expressions = { normal: { name: "正常", emoji: "😐" }, happy: { name: "开心", emoji: "😊" }, excited: { name: "兴奋", emoji: "🤩" }, thinking: { name: "思考", emoji: "🤔" }, surprised: { name: "惊讶", ememoji: "😮" }, encouraging: { name: "鼓励", emoji: "💪" }, proud: { name: "骄傲", emoji: "🦉" } }; } /** * 初始化虚拟老师 */ async init() { if (this.isInitialized) return; try { // 创建容器 this.createContainer(); // 初始化Three.js await this.initThreeJS(); // 添加交互 this.addInteractions(); this.isInitialized = true; console.log('🦉 虚拟猫头鹰老师初始化完成'); // 显示欢迎消息 this.showWelcomeMessage(); } catch (error) { console.error('虚拟老师初始化失败:', error); } } /** * 创建容器 */ createContainer() { // 检查是否已存在容器 let container = document.getElementById(this.containerId); if (!container) { container = document.createElement('div'); container.id = this.containerId; container.className = 'virtual-teacher-container'; document.body.appendChild(container); } this.container = container; // 添加样式 this.addStyles(); } /** * 添加样式 */ addStyles() { if (document.getElementById('virtual-teacher-styles')) return; const style = document.createElement('style'); style.id = 'virtual-teacher-styles'; style.textContent = ` .virtual-teacher-container { position: fixed; top: 20px; right: 20px; width: 200px; height: 200px; z-index: 1000; border-radius: 15px; border: 3px solid #d2691e; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); background: linear-gradient(135deg, #f0e68c 0%, #daa520 100%); overflow: hidden; transition: transform 0.3s ease; } .virtual-teacher-container:hover { transform: scale(1.05); } .virtual-teacher-container.speaking { animation: teacherGlow 2s infinite; } @keyframes teacherGlow { 0% { box-shadow: 0 0 0 0 rgba(210, 105, 30, 0.7); } 70% { box-shadow: 0 0 0 15px rgba(210, 105, 30, 0); } 100% { box-shadow: 0 0 0 0 rgba(210, 105, 30, 0); } } .teacher-speech-bubble { position: absolute; bottom: -80px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%); color: white; border-radius: 15px; padding: 10px 15px; max-width: 250px; text-align: center; font-size: 12px; line-height: 1.4; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); opacity: 0; transition: all 0.5s ease; z-index: 1001; } .teacher-speech-bubble:after { content: ""; position: absolute; top: -8px; left: 50%; margin-left: -8px; border-width: 8px; border-style: solid; border-color: transparent transparent #ff6b35 transparent; } .teacher-speech-bubble.visible { opacity: 1; transform: translateX(-50%) translateY(-5px); } .teacher-status { position: absolute; top: -30px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.7); color: white; padding: 5px 10px; border-radius: 10px; font-size: 11px; white-space: nowrap; } `; document.head.appendChild(style); } /** * 初始化Three.js场景 */ async initThreeJS() { // 检查Three.js是否已加载 if (typeof THREE === 'undefined') { await this.loadThreeJS(); } // 创建场景 this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0xf0e68c); // 创建相机 this.camera = new THREE.OrthographicCamera(-100, 100, 100, -100, 0.1, 1000); this.camera.position.z = 100; // 创建渲染器 this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(200, 200); this.renderer.setClearColor(0xf0e68c); this.container.appendChild(this.renderer.domElement); // 创建猫头鹰 this.createOwl(); // 创建状态显示 this.createStatusDisplay(); // 创建语音气泡 this.createSpeechBubble(); // 开始动画循环 this.animate(); } /** * 加载Three.js库 */ loadThreeJS() { return new Promise((resolve, reject) => { if (typeof THREE !== 'undefined') { resolve(); return; } const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js'; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } /** * 创建猫头鹰角色 */ createOwl() { this.owl = new THREE.Group(); // 身体 const bodyGeometry = new THREE.CircleGeometry(40, 32); const bodyMaterial = new THREE.MeshBasicMaterial({ color: 0xd2691e, side: THREE.DoubleSide, }); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.position.y = -20; body.scale.set(1, 1.3, 1); this.owl.add(body); // 肚子 const bellyGeometry = new THREE.CircleGeometry(28, 32); const bellyMaterial = new THREE.MeshBasicMaterial({ color: 0xfff8dc, side: THREE.DoubleSide, }); const belly = new THREE.Mesh(bellyGeometry, bellyMaterial); belly.position.set(0, -22, 1); belly.scale.set(1, 1.2, 1); this.owl.add(belly); // 头部 const headGeometry = new THREE.CircleGeometry(35, 32); const headMaterial = new THREE.MeshBasicMaterial({ color: 0xd2691e, side: THREE.DoubleSide, }); const head = new THREE.Mesh(headGeometry, headMaterial); head.position.y = 15; this.owl.add(head); // 脸部 const faceGeometry = new THREE.CircleGeometry(25, 32); const faceMaterial = new THREE.MeshBasicMaterial({ color: 0xfff8dc, side: THREE.DoubleSide, }); const face = new THREE.Mesh(faceGeometry, faceMaterial); face.position.set(0, 12, 1); face.scale.set(1, 1.1, 1); this.owl.add(face); // 眼睛外圈 const eyeOuterGeometry = new THREE.CircleGeometry(18, 32); const eyeOuterMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide, }); const leftEyeOuter = new THREE.Mesh(eyeOuterGeometry, eyeOuterMaterial); leftEyeOuter.position.set(-12, 20, 2); this.owl.add(leftEyeOuter); const rightEyeOuter = new THREE.Mesh(eyeOuterGeometry, eyeOuterMaterial); rightEyeOuter.position.set(12, 20, 2); this.owl.add(rightEyeOuter); // 眼睛内圈 const eyeInnerGeometry = new THREE.CircleGeometry(14, 32); const eyeInnerMaterial = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.DoubleSide, }); const leftEyeInner = new THREE.Mesh(eyeInnerGeometry, eyeInnerMaterial); leftEyeInner.position.set(-12, 20, 3); this.owl.add(leftEyeInner); const rightEyeInner = new THREE.Mesh(eyeInnerGeometry, eyeInnerMaterial); rightEyeInner.position.set(12, 20, 3); this.owl.add(rightEyeInner); // 瞳孔 const pupilGeometry = new THREE.CircleGeometry(10, 16); const pupilMaterial = new THREE.MeshBasicMaterial({ color: 0x8b4513, side: THREE.DoubleSide, }); const leftPupil = new THREE.Mesh(pupilGeometry, pupilMaterial); leftPupil.position.set(-12, 20, 4); this.owl.add(leftPupil); const rightPupil = new THREE.Mesh(pupilGeometry, pupilMaterial); rightPupil.position.set(12, 20, 4); this.owl.add(rightPupil); // 眼睛高光 const highlightGeometry = new THREE.CircleGeometry(4, 8); const highlightMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide, }); const leftHighlight = new THREE.Mesh(highlightGeometry, highlightMaterial); leftHighlight.position.set(-10, 22, 5); this.owl.add(leftHighlight); const rightHighlight = new THREE.Mesh(highlightGeometry, highlightMaterial); rightHighlight.position.set(10, 22, 5); this.owl.add(rightHighlight); // 喙 const beakGeometry = new THREE.CircleGeometry(4, 8); const beakMaterial = new THREE.MeshBasicMaterial({ color: 0xff8c00, side: THREE.DoubleSide, }); const beak = new THREE.Mesh(beakGeometry, beakMaterial); beak.position.set(0, 8, 3); beak.scale.set(1, 0.8, 1); this.owl.add(beak); // 翅膀 const wingGeometry = new THREE.CircleGeometry(22, 16); const wingMaterial = new THREE.MeshBasicMaterial({ color: 0xcd853f, side: THREE.DoubleSide, }); const leftWing = new THREE.Mesh(wingGeometry, wingMaterial); leftWing.position.set(-42, -15, 0); leftWing.scale.set(0.8, 1.4, 1); leftWing.rotation.z = 0.3; this.owl.add(leftWing); const rightWing = new THREE.Mesh(wingGeometry, wingMaterial); rightWing.position.set(42, -15, 0); rightWing.scale.set(0.8, 1.4, 1); rightWing.rotation.z = -0.3; this.owl.add(rightWing); // 存储可动画的部分 this.owl.userData = { head: head, body: body, leftEyeOuter: leftEyeOuter, rightEyeOuter: rightEyeOuter, leftEyeInner: leftEyeInner, rightEyeInner: rightEyeInner, leftPupil: leftPupil, rightPupil: rightPupil, leftHighlight: leftHighlight, rightHighlight: rightHighlight, beak: beak, leftWing: leftWing, rightWing: rightWing, blinkTimer: 0, speakTimer: 0, idleTimer: 0, }; this.scene.add(this.owl); } /** * 创建状态显示 */ createStatusDisplay() { const statusDiv = document.createElement('div'); statusDiv.className = 'teacher-status'; statusDiv.id = 'teacher-status'; statusDiv.textContent = '🦉 老师待机中...'; this.container.appendChild(statusDiv); } /** * 创建语音气泡 */ createSpeechBubble() { const bubbleDiv = document.createElement('div'); bubbleDiv.className = 'teacher-speech-bubble'; bubbleDiv.id = 'teacher-speech-bubble'; bubbleDiv.textContent = '你好!我是你的猫头鹰老师!'; this.container.appendChild(bubbleDiv); } /** * 动画循环 */ animate() { this.animationId = requestAnimationFrame(() => this.animate()); if (this.owl && this.owl.userData) { const userData = this.owl.userData; // 眨眼动画 userData.blinkTimer += 0.016; if (userData.blinkTimer > 4) { userData.leftEyeOuter.scale.y = 0.1; userData.rightEyeOuter.scale.y = 0.1; userData.leftEyeInner.scale.y = 0.1; userData.rightEyeInner.scale.y = 0.1; if (userData.blinkTimer > 4.2) { userData.leftEyeOuter.scale.y = 1; userData.rightEyeOuter.scale.y = 1; userData.leftEyeInner.scale.y = 1; userData.rightEyeInner.scale.y = 1; userData.blinkTimer = 0; } } // 说话动画 if (this.isSpeaking) { userData.speakTimer += 0.2; userData.beak.scale.y = 0.8 + Math.sin(userData.speakTimer) * 0.3; userData.head.position.y = 15 + Math.sin(userData.speakTimer * 0.7) * 2; // 说话时翅膀轻微扇动 userData.leftWing.rotation.z = 0.3 + Math.sin(userData.speakTimer * 0.5) * 0.1; userData.rightWing.rotation.z = -0.3 - Math.sin(userData.speakTimer * 0.5) * 0.1; } else { userData.beak.scale.y = 0.8; userData.head.position.y = 15; userData.leftWing.rotation.z = 0.3; userData.rightWing.rotation.z = -0.3; } // 闲置动画 userData.idleTimer += 0.008; this.owl.rotation.z = Math.sin(userData.idleTimer) * 0.02; userData.head.rotation.z = Math.sin(userData.idleTimer * 0.7) * 0.05; } this.renderer.render(this.scene, this.camera); } /** * 添加交互 */ addInteractions() { this.container.addEventListener('click', () => { this.blink(); }); this.container.addEventListener('mouseenter', () => { this.container.style.transform = 'scale(1.05)'; }); this.container.addEventListener('mouseleave', () => { this.container.style.transform = 'scale(1)'; }); } /** * 显示欢迎消息 */ showWelcomeMessage() { const messages = [ '你好!我是你的猫头鹰老师!', '准备好开始学习了吗?', '让我们一起探索知识的奥秘!', '智慧就在我们身边!' ]; const randomMessage = messages[Math.floor(Math.random() * messages.length)]; this.speak(randomMessage); } /** * 语音朗读 */ speak(text, options = {}) { if (!text) return; // 立即停止所有当前语音 this.stopAllSpeech(); // 显示文字气泡 this.showSpeechBubble(text); // 创建语音 this.currentUtterance = new SpeechSynthesisUtterance(text); this.currentUtterance.lang = options.lang || 'zh-CN'; this.currentUtterance.rate = options.rate || 0.9; this.currentUtterance.pitch = options.pitch || 1.1; this.currentUtterance.volume = options.volume || 0.8; // 语音事件 this.currentUtterance.onstart = () => { this.isPlaying = true; // 设置全局状态,通知无障碍语音播报 window.virtualTeacherPlaying = true; this.startSpeaking(); this.updateStatus('📚 正在讲解...'); }; this.currentUtterance.onend = () => { this.isPlaying = false; // 清除全局状态 window.virtualTeacherPlaying = false; this.stopSpeaking(); this.updateStatus('✨ 讲解完成'); setTimeout(() => { this.hideSpeechBubble(); }, 2000); }; this.currentUtterance.onerror = (event) => { this.isPlaying = false; // 清除全局状态 window.virtualTeacherPlaying = false; this.stopSpeaking(); this.updateStatus(`❌ 语音出错: ${event.error}`); console.error('语音合成错误:', event); }; // 开始语音 this.speechSynthesis.speak(this.currentUtterance); } /** * 停止所有语音播放 */ stopAllSpeech() { // 停止当前语音合成 if (this.speechSynthesis.speaking) { this.speechSynthesis.cancel(); } // 清空语音队列 this.speechQueue = []; // 重置播放状态 this.isPlaying = false; // 清除全局状态 window.virtualTeacherPlaying = false; // 停止说话动画 this.stopSpeaking(); // 隐藏语音气泡 this.hideSpeechBubble(); // 重置状态 this.updateStatus('🦉 老师待机中...'); } /** * 显示文字气泡 */ showSpeechBubble(text) { const bubble = document.getElementById('teacher-speech-bubble'); if (bubble) { bubble.textContent = text; bubble.classList.add('visible'); } } /** * 隐藏文字气泡 */ hideSpeechBubble() { const bubble = document.getElementById('teacher-speech-bubble'); if (bubble) { bubble.classList.remove('visible'); } } /** * 开始说话动画 */ startSpeaking() { this.isSpeaking = true; this.container.classList.add('speaking'); } /** * 停止说话动画 */ stopSpeaking() { this.isSpeaking = false; this.container.classList.remove('speaking'); } /** * 更新状态显示 */ updateStatus(text) { const status = document.getElementById('teacher-status'); if (status) { status.textContent = text; } } /** * 眨眼动画 */ blink() { if (!this.owl || !this.owl.userData) return; const userData = this.owl.userData; userData.leftEyeOuter.scale.y = 0.1; userData.rightEyeOuter.scale.y = 0.1; userData.leftEyeInner.scale.y = 0.1; userData.rightEyeInner.scale.y = 0.1; setTimeout(() => { userData.leftEyeOuter.scale.y = 1; userData.rightEyeOuter.scale.y = 1; userData.leftEyeInner.scale.y = 1; userData.rightEyeInner.scale.y = 1; }, 200); } /** * 改变表情 */ changeExpression(expression) { if (!this.owl || !this.owl.userData) return; const userData = this.owl.userData; this.currentExpression = expression; switch (expression) { case "happy": userData.leftEyeOuter.scale.y = 0.7; userData.rightEyeOuter.scale.y = 0.7; userData.beak.scale.set(1.2, 1, 1); this.updateStatus("😊 开心模式"); break; case "excited": userData.leftEyeOuter.scale.set(1.2, 1.2, 1); userData.rightEyeOuter.scale.set(1.2, 1.2, 1); userData.beak.scale.set(1.3, 1.1, 1); this.updateStatus("🤩 兴奋模式"); break; case "thinking": userData.head.rotation.z = 0.2; userData.leftPupil.position.x = -10; userData.rightPupil.position.x = 14; this.updateStatus("🤔 思考模式"); break; case "surprised": userData.leftEyeOuter.scale.set(1.3, 1.3, 1); userData.rightEyeOuter.scale.set(1.3, 1.3, 1); userData.beak.scale.set(0.8, 1.2, 1); this.updateStatus("😮 惊讶模式"); break; case "encouraging": userData.leftEyeOuter.scale.y = 0.8; userData.rightEyeOuter.scale.y = 0.8; userData.beak.scale.set(1.1, 0.9, 1); this.updateStatus("💪 鼓励模式"); break; case "proud": userData.head.position.y = 18; userData.beak.scale.set(1.2, 1.2, 1); this.updateStatus("🦉 骄傲模式"); break; default: // normal userData.leftEyeOuter.scale.set(1, 1, 1); userData.rightEyeOuter.scale.set(1, 1, 1); userData.leftEyeInner.scale.set(1, 1, 1); userData.rightEyeInner.scale.set(1, 1, 1); userData.head.rotation.z = 0; userData.head.position.y = 15; userData.leftPupil.position.x = -12; userData.rightPupil.position.x = 12; userData.beak.scale.set(1, 0.8, 1); this.updateStatus("😐 正常模式"); } } /** * 记录正确答案 */ recordCorrect() { this.gameStats.correct++; this.gameStats.total++; this.gameStats.streak++; if (this.gameStats.streak > this.gameStats.maxStreak) { this.gameStats.maxStreak = this.gameStats.streak; } // 根据连续正确次数选择表情和语音 if (this.gameStats.streak >= 5) { this.changeExpression("excited"); this.speak("太棒了!你已经连续答对" + this.gameStats.streak + "题了!继续保持!"); } else if (this.gameStats.streak >= 3) { this.changeExpression("happy"); this.speak("很好!连续答对了" + this.gameStats.streak + "题!"); } else { this.changeExpression("happy"); const messages = [ "正确!做得很好!", "太棒了!你答对了!", "很好!继续加油!", "答对了!你真聪明!" ]; this.speak(messages[Math.floor(Math.random() * messages.length)]); } console.log('🦉 老师记录正确答案:', this.gameStats); } /** * 记录错误答案 */ recordIncorrect() { this.gameStats.incorrect++; this.gameStats.total++; this.gameStats.streak = 0; // 重置连续正确次数 // 根据错误情况选择表情和语音 if (this.gameStats.incorrect <= 2) { this.changeExpression("encouraging"); const messages = [ "没关系,再试一次!", "不要灰心,继续努力!", "错了也没关系,学习就是这样!", "加油!你可以的!" ]; this.speak(messages[Math.floor(Math.random() * messages.length)]); } else { this.changeExpression("thinking"); this.speak("让我们重新思考一下这个问题,不要放弃!"); } console.log('🦉 老师记录错误答案:', this.gameStats); } /** * 游戏开始 */ gameStart(gameName) { this.changeExpression("normal"); this.speak("游戏开始!" + gameName + ",准备好了吗?"); console.log('🦉 老师:游戏开始 -', gameName); } /** * 游戏结束 */ gameEnd() { const accuracy = this.gameStats.total > 0 ? Math.round((this.gameStats.correct / this.gameStats.total) * 100) : 0; if (accuracy >= 80) { this.changeExpression("proud"); this.speak("游戏结束!你的正确率是" + accuracy + "%,表现非常优秀!"); } else if (accuracy >= 60) { this.changeExpression("happy"); this.speak("游戏结束!你的正确率是" + accuracy + "%,还不错!"); } else { this.changeExpression("encouraging"); this.speak("游戏结束!你的正确率是" + accuracy + "%,继续练习会更好的!"); } console.log('🦉 老师:游戏结束,正确率:', accuracy + '%'); } /** * 重置游戏统计 */ resetStats() { this.gameStats = { correct: 0, incorrect: 0, total: 0, streak: 0, maxStreak: 0 }; console.log('🦉 老师:重置游戏统计'); } /** * 朗读游戏介绍 */ readGameIntroduction(introText) { if (introText) { this.changeExpression("normal"); this.speak(introText); } } /** * 朗读题目 */ readQuestion(questionText) { if (questionText) { this.changeExpression("thinking"); this.speak("题目是:" + questionText); } } /** * 销毁虚拟老师 */ destroy() { if (this.animationId) { cancelAnimationFrame(this.animationId); } if (this.speechSynthesis.speaking) { this.speechSynthesis.cancel(); } if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); } this.isInitialized = false; console.log('🦉 虚拟老师已销毁'); } } // 创建全局实例 window.virtualTeacher = new VirtualTeacher(); // 导出类 if (typeof module !== 'undefined' && module.exports) { module.exports = VirtualTeacher; }