Files
RGKT/rg-09112127/js/virtualTeacher.js
2025-10-10 19:44:14 +08:00

855 lines
26 KiB
JavaScript

/**
* 虚拟猫头鹰老师模块
* 提供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;
}