913 lines
28 KiB
HTML
913 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>智慧猫头鹰课堂助手</title>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<style>
|
|
body {
|
|
font-family: "Microsoft YaHei", sans-serif;
|
|
background: linear-gradient(
|
|
135deg,
|
|
#fef9e7 0%,
|
|
#f8f4e6 50%,
|
|
#e8f5e8 100%
|
|
);
|
|
margin: 0;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.container {
|
|
width: 100%;
|
|
max-width: 900px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 20px;
|
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
|
padding: 40px;
|
|
margin-top: 20px;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
h1 {
|
|
color: #8b4513;
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
font-size: 2.2em;
|
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.owl-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
margin-bottom: 30px;
|
|
position: relative;
|
|
}
|
|
|
|
.owl-stage {
|
|
width: 400px;
|
|
height: 400px;
|
|
border-radius: 20px;
|
|
border: 3px solid #d2691e;
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
|
margin-bottom: 20px;
|
|
transition: transform 0.3s ease;
|
|
background: linear-gradient(135deg, #f0e68c 0%, #daa520 100%);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.owl-stage:hover {
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.owl-stage.speaking {
|
|
animation: owlGlow 2s infinite;
|
|
}
|
|
|
|
#three-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 17px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
@keyframes owlGlow {
|
|
0% {
|
|
box-shadow: 0 0 0 0 rgba(210, 105, 30, 0.7);
|
|
}
|
|
70% {
|
|
box-shadow: 0 0 0 20px rgba(210, 105, 30, 0);
|
|
}
|
|
100% {
|
|
box-shadow: 0 0 0 0 rgba(210, 105, 30, 0);
|
|
}
|
|
}
|
|
|
|
.speech-bubble {
|
|
position: relative;
|
|
background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%);
|
|
color: white;
|
|
border-radius: 15px;
|
|
padding: 20px 25px;
|
|
max-width: 85%;
|
|
text-align: center;
|
|
margin-bottom: 25px;
|
|
opacity: 0;
|
|
transition: all 0.5s ease;
|
|
font-size: 16px;
|
|
line-height: 1.5;
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.speech-bubble:after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 50%;
|
|
margin-left: -12px;
|
|
border-width: 12px;
|
|
border-style: solid;
|
|
border-color: #ff6b35 transparent transparent transparent;
|
|
}
|
|
|
|
.speech-bubble.visible {
|
|
opacity: 1;
|
|
transform: translateY(-5px);
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 20px;
|
|
margin-bottom: 25px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
button {
|
|
background: linear-gradient(135deg, #cd853f 0%, #8b4513 100%);
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 25px;
|
|
border-radius: 25px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
button:disabled {
|
|
background: linear-gradient(135deg, #d3d3d3 0%, #a9a9a9 100%);
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.status {
|
|
text-align: center;
|
|
margin-top: 20px;
|
|
font-size: 18px;
|
|
color: #8b4513;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.input-container {
|
|
width: 100%;
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
textarea {
|
|
width: 100%;
|
|
height: 120px;
|
|
padding: 15px;
|
|
border: 2px solid #daa520;
|
|
border-radius: 12px;
|
|
font-family: inherit;
|
|
font-size: 16px;
|
|
resize: vertical;
|
|
transition: border-color 0.3s ease;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
textarea:focus {
|
|
outline: none;
|
|
border-color: #ff8c42;
|
|
box-shadow: 0 0 10px rgba(255, 140, 66, 0.3);
|
|
}
|
|
|
|
.features {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-top: 35px;
|
|
}
|
|
|
|
.feature-card {
|
|
background: linear-gradient(135deg, #fff8dc 0%, #f5deb3 100%);
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
|
transition: transform 0.3s ease;
|
|
border: 2px solid #daa520;
|
|
}
|
|
|
|
.feature-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.feature-icon {
|
|
font-size: 32px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.feature-card h3 {
|
|
color: #8b4513;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.feature-card p {
|
|
color: #696969;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.wisdom-quote {
|
|
text-align: center;
|
|
font-style: italic;
|
|
color: #8b4513;
|
|
margin-top: 30px;
|
|
padding: 15px;
|
|
background: rgba(218, 165, 32, 0.1);
|
|
border-radius: 10px;
|
|
border-left: 4px solid #daa520;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🦉 智慧猫头鹰课堂助手</h1>
|
|
|
|
<div class="owl-container">
|
|
<div id="owlStage" class="owl-stage">
|
|
<div id="three-container"></div>
|
|
</div>
|
|
<div id="speechBubble" class="speech-bubble">
|
|
你好!我是智慧猫头鹰,很高兴成为你的课堂助手!有什么知识想要探索吗?🌟
|
|
</div>
|
|
</div>
|
|
|
|
<div class="input-container">
|
|
<textarea id="speechText" placeholder="输入你希望猫头鹰讲解的内容...">
|
|
欢迎来到智慧的课堂!我是你的猫头鹰老师。今天我们将一起探索知识的森林,学习新的技能。猫头鹰象征着智慧和洞察力,让我们用敏锐的眼光去发现学习的乐趣吧!</textarea
|
|
>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<button id="speakBtn">🎓 开始讲解</button>
|
|
<button id="stopBtn" disabled>⏹️ 停止讲解</button>
|
|
<button id="expressionBtn">😊 切换表情</button>
|
|
</div>
|
|
|
|
<div id="status" class="status">智慧待机中...</div>
|
|
|
|
<div class="features">
|
|
<div class="feature-card">
|
|
<div class="feature-icon">📖</div>
|
|
<h3>知识讲解</h3>
|
|
<p>用生动有趣的方式为学生讲解各种知识点</p>
|
|
</div>
|
|
<div class="feature-card">
|
|
<div class="feature-icon">🤔</div>
|
|
<h3>启发思考</h3>
|
|
<p>通过提问和引导帮助学生深入思考问题</p>
|
|
</div>
|
|
<div class="feature-card">
|
|
<div class="feature-icon">🌳</div>
|
|
<h3>知识树</h3>
|
|
<p>构建完整的知识体系,让学习更有条理</p>
|
|
</div>
|
|
<div class="feature-card">
|
|
<div class="feature-icon">✨</div>
|
|
<h3>智慧启迪</h3>
|
|
<p>分享学习方法和人生智慧,启迪心灵</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="wisdom-quote">
|
|
💡 "知识是智慧的翅膀,让我们一起在学习的天空中翱翔!"
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const owlStage = document.getElementById("owlStage");
|
|
const speechBubble = document.getElementById("speechBubble");
|
|
const speechText = document.getElementById("speechText");
|
|
const speakBtn = document.getElementById("speakBtn");
|
|
const stopBtn = document.getElementById("stopBtn");
|
|
const expressionBtn = document.getElementById("expressionBtn");
|
|
const status = document.getElementById("status");
|
|
|
|
let speechSynthesis = window.speechSynthesis;
|
|
let currentUtterance = null;
|
|
|
|
// Three.js 猫头鹰
|
|
let scene, camera, renderer, owl;
|
|
let isSpeaking = false;
|
|
let animationId;
|
|
let currentExpression = "normal";
|
|
|
|
// 初始化Three.js场景
|
|
function initThreeJS() {
|
|
const container = document.getElementById("three-container");
|
|
|
|
// 创建场景
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0xf0e68c);
|
|
|
|
// 创建相机
|
|
camera = new THREE.OrthographicCamera(-200, 200, 200, -200, 0.1, 1000);
|
|
camera.position.z = 100;
|
|
|
|
// 创建渲染器
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(400, 400);
|
|
renderer.setClearColor(0xf0e68c);
|
|
container.appendChild(renderer.domElement);
|
|
|
|
// 创建猫头鹰
|
|
createOwl();
|
|
|
|
// 开始动画循环
|
|
animate();
|
|
}
|
|
|
|
// 创建猫头鹰角色
|
|
function createOwl() {
|
|
owl = new THREE.Group();
|
|
|
|
// 树枝
|
|
const branchGeometry = new THREE.CylinderGeometry(4, 6, 120, 8);
|
|
const branchMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0x8b4513,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
const branch = new THREE.Mesh(branchGeometry, branchMaterial);
|
|
branch.rotation.z = Math.PI / 2;
|
|
branch.position.y = -120;
|
|
owl.add(branch);
|
|
|
|
// 叶子
|
|
const leafPositions = [
|
|
{ x: -80, y: -110, rotation: 0.3 },
|
|
{ x: -40, y: -115, rotation: -0.2 },
|
|
{ x: 20, y: -112, rotation: 0.1 },
|
|
{ x: 60, y: -118, rotation: -0.4 },
|
|
{ x: 90, y: -108, rotation: 0.2 },
|
|
];
|
|
|
|
leafPositions.forEach((pos) => {
|
|
const leafGeometry = new THREE.CircleGeometry(12, 8);
|
|
const leafMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0x32cd32,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
const leaf = new THREE.Mesh(leafGeometry, leafMaterial);
|
|
leaf.position.set(pos.x, pos.y, 1);
|
|
leaf.rotation.z = pos.rotation;
|
|
leaf.scale.set(1, 1.8, 1);
|
|
owl.add(leaf);
|
|
});
|
|
|
|
// 身体 - 椭圆形
|
|
const bodyGeometry = new THREE.CircleGeometry(80, 32);
|
|
const bodyMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xd2691e,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
|
body.position.y = -40;
|
|
body.scale.set(1, 1.3, 1);
|
|
owl.add(body);
|
|
|
|
// 肚子 - 浅色
|
|
const bellyGeometry = new THREE.CircleGeometry(55, 32);
|
|
const bellyMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xfff8dc,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
const belly = new THREE.Mesh(bellyGeometry, bellyMaterial);
|
|
belly.position.set(0, -45, 1);
|
|
belly.scale.set(1, 1.2, 1);
|
|
owl.add(belly);
|
|
|
|
// 羽毛纹理 - 身体上的小圆点
|
|
for (let i = 0; i < 20; i++) {
|
|
const featherGeometry = new THREE.CircleGeometry(3, 8);
|
|
const featherMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xcd853f,
|
|
side: THREE.DoubleSide,
|
|
transparent: true,
|
|
opacity: 0.6,
|
|
});
|
|
const feather = new THREE.Mesh(featherGeometry, featherMaterial);
|
|
feather.position.set(
|
|
(Math.random() - 0.5) * 80,
|
|
-60 + (Math.random() - 0.5) * 40,
|
|
2
|
|
);
|
|
owl.add(feather);
|
|
}
|
|
|
|
// 头部 - 圆形
|
|
const headGeometry = new THREE.CircleGeometry(70, 32);
|
|
const headMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xd2691e,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
const head = new THREE.Mesh(headGeometry, headMaterial);
|
|
head.position.y = 30;
|
|
owl.add(head);
|
|
|
|
// 脸部 - 浅色心形区域
|
|
const faceGeometry = new THREE.CircleGeometry(50, 32);
|
|
const faceMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xfff8dc,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
const face = new THREE.Mesh(faceGeometry, faceMaterial);
|
|
face.position.set(0, 25, 1);
|
|
face.scale.set(1, 1.1, 1);
|
|
owl.add(face);
|
|
|
|
// 眼睛外圈 - 大圆
|
|
const eyeOuterGeometry = new THREE.CircleGeometry(35, 32);
|
|
const eyeOuterMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xffffff,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
|
|
const leftEyeOuter = new THREE.Mesh(eyeOuterGeometry, eyeOuterMaterial);
|
|
leftEyeOuter.position.set(-25, 40, 2);
|
|
owl.add(leftEyeOuter);
|
|
|
|
const rightEyeOuter = new THREE.Mesh(
|
|
eyeOuterGeometry,
|
|
eyeOuterMaterial
|
|
);
|
|
rightEyeOuter.position.set(25, 40, 2);
|
|
owl.add(rightEyeOuter);
|
|
|
|
// 眼睛内圈
|
|
const eyeInnerGeometry = new THREE.CircleGeometry(28, 32);
|
|
const eyeInnerMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0x000000,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
|
|
const leftEyeInner = new THREE.Mesh(eyeInnerGeometry, eyeInnerMaterial);
|
|
leftEyeInner.position.set(-25, 40, 3);
|
|
owl.add(leftEyeInner);
|
|
|
|
const rightEyeInner = new THREE.Mesh(
|
|
eyeInnerGeometry,
|
|
eyeInnerMaterial
|
|
);
|
|
rightEyeInner.position.set(25, 40, 3);
|
|
owl.add(rightEyeInner);
|
|
|
|
// 瞳孔
|
|
const pupilGeometry = new THREE.CircleGeometry(20, 16);
|
|
const pupilMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0x8b4513,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
|
|
const leftPupil = new THREE.Mesh(pupilGeometry, pupilMaterial);
|
|
leftPupil.position.set(-25, 40, 4);
|
|
owl.add(leftPupil);
|
|
|
|
const rightPupil = new THREE.Mesh(pupilGeometry, pupilMaterial);
|
|
rightPupil.position.set(25, 40, 4);
|
|
owl.add(rightPupil);
|
|
|
|
// 眼睛高光
|
|
const highlightGeometry = new THREE.CircleGeometry(8, 8);
|
|
const highlightMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xffffff,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
|
|
const leftHighlight = new THREE.Mesh(
|
|
highlightGeometry,
|
|
highlightMaterial
|
|
);
|
|
leftHighlight.position.set(-20, 45, 5);
|
|
owl.add(leftHighlight);
|
|
|
|
const rightHighlight = new THREE.Mesh(
|
|
highlightGeometry,
|
|
highlightMaterial
|
|
);
|
|
rightHighlight.position.set(20, 45, 5);
|
|
owl.add(rightHighlight);
|
|
|
|
// 小高光
|
|
const smallHighlightGeometry = new THREE.CircleGeometry(4, 8);
|
|
const leftSmallHighlight = new THREE.Mesh(
|
|
smallHighlightGeometry,
|
|
highlightMaterial
|
|
);
|
|
leftSmallHighlight.position.set(-30, 35, 5);
|
|
owl.add(leftSmallHighlight);
|
|
|
|
const rightSmallHighlight = new THREE.Mesh(
|
|
smallHighlightGeometry,
|
|
highlightMaterial
|
|
);
|
|
rightSmallHighlight.position.set(30, 35, 5);
|
|
owl.add(rightSmallHighlight);
|
|
|
|
// 眉毛/头部羽毛装饰
|
|
const eyebrowGeometry = new THREE.CircleGeometry(15, 8);
|
|
const eyebrowMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xcd853f,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
|
|
const leftEyebrow = new THREE.Mesh(eyebrowGeometry, eyebrowMaterial);
|
|
leftEyebrow.position.set(-30, 65, 2);
|
|
leftEyebrow.scale.set(1.5, 0.6, 1);
|
|
leftEyebrow.rotation.z = 0.3;
|
|
owl.add(leftEyebrow);
|
|
|
|
const rightEyebrow = new THREE.Mesh(eyebrowGeometry, eyebrowMaterial);
|
|
rightEyebrow.position.set(30, 65, 2);
|
|
rightEyebrow.scale.set(1.5, 0.6, 1);
|
|
rightEyebrow.rotation.z = -0.3;
|
|
owl.add(rightEyebrow);
|
|
|
|
// 喙
|
|
const beakGeometry = new THREE.CircleGeometry(8, 8);
|
|
const beakMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xff8c00,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
const beak = new THREE.Mesh(beakGeometry, beakMaterial);
|
|
beak.position.set(0, 15, 3);
|
|
beak.scale.set(1, 0.8, 1);
|
|
owl.add(beak);
|
|
|
|
// 翅膀
|
|
const wingGeometry = new THREE.CircleGeometry(45, 16);
|
|
const wingMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xcd853f,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
|
|
const leftWing = new THREE.Mesh(wingGeometry, wingMaterial);
|
|
leftWing.position.set(-85, -30, 0);
|
|
leftWing.scale.set(0.8, 1.4, 1);
|
|
leftWing.rotation.z = 0.3;
|
|
owl.add(leftWing);
|
|
|
|
const rightWing = new THREE.Mesh(wingGeometry, wingMaterial);
|
|
rightWing.position.set(85, -30, 0);
|
|
rightWing.scale.set(0.8, 1.4, 1);
|
|
rightWing.rotation.z = -0.3;
|
|
owl.add(rightWing);
|
|
|
|
// 翅膀羽毛纹理
|
|
for (let i = 0; i < 10; i++) {
|
|
const wingFeatherGeometry = new THREE.CircleGeometry(4, 8);
|
|
const wingFeatherMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xd2691e,
|
|
side: THREE.DoubleSide,
|
|
transparent: true,
|
|
opacity: 0.7,
|
|
});
|
|
|
|
const leftWingFeather = new THREE.Mesh(
|
|
wingFeatherGeometry,
|
|
wingFeatherMaterial
|
|
);
|
|
leftWingFeather.position.set(
|
|
-85 + (Math.random() - 0.5) * 40,
|
|
-30 + (Math.random() - 0.5) * 60,
|
|
1
|
|
);
|
|
owl.add(leftWingFeather);
|
|
|
|
const rightWingFeather = new THREE.Mesh(
|
|
wingFeatherGeometry,
|
|
wingFeatherMaterial
|
|
);
|
|
rightWingFeather.position.set(
|
|
85 + (Math.random() - 0.5) * 40,
|
|
-30 + (Math.random() - 0.5) * 60,
|
|
1
|
|
);
|
|
owl.add(rightWingFeather);
|
|
}
|
|
|
|
// 爪子
|
|
const clawGeometry = new THREE.CircleGeometry(6, 8);
|
|
const clawMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xff8c00,
|
|
side: THREE.DoubleSide,
|
|
});
|
|
|
|
// 左爪
|
|
const leftClaw1 = new THREE.Mesh(clawGeometry, clawMaterial);
|
|
leftClaw1.position.set(-25, -110, 2);
|
|
leftClaw1.scale.set(0.6, 1.5, 1);
|
|
owl.add(leftClaw1);
|
|
|
|
const leftClaw2 = new THREE.Mesh(clawGeometry, clawMaterial);
|
|
leftClaw2.position.set(-15, -112, 2);
|
|
leftClaw2.scale.set(0.6, 1.5, 1);
|
|
owl.add(leftClaw2);
|
|
|
|
// 右爪
|
|
const rightClaw1 = new THREE.Mesh(clawGeometry, clawMaterial);
|
|
rightClaw1.position.set(15, -112, 2);
|
|
rightClaw1.scale.set(0.6, 1.5, 1);
|
|
owl.add(rightClaw1);
|
|
|
|
const rightClaw2 = new THREE.Mesh(clawGeometry, clawMaterial);
|
|
rightClaw2.position.set(25, -110, 2);
|
|
rightClaw2.scale.set(0.6, 1.5, 1);
|
|
owl.add(rightClaw2);
|
|
|
|
// 存储可动画的部分
|
|
owl.userData = {
|
|
head: head,
|
|
body: body,
|
|
leftEyeOuter: leftEyeOuter,
|
|
rightEyeOuter: rightEyeOuter,
|
|
leftEyeInner: leftEyeInner,
|
|
rightEyeInner: rightEyeInner,
|
|
leftPupil: leftPupil,
|
|
rightPupil: rightPupil,
|
|
leftHighlight: leftHighlight,
|
|
rightHighlight: rightHighlight,
|
|
leftSmallHighlight: leftSmallHighlight,
|
|
rightSmallHighlight: rightSmallHighlight,
|
|
beak: beak,
|
|
leftWing: leftWing,
|
|
rightWing: rightWing,
|
|
blinkTimer: 0,
|
|
speakTimer: 0,
|
|
idleTimer: 0,
|
|
};
|
|
|
|
scene.add(owl);
|
|
}
|
|
|
|
// 动画循环
|
|
function animate() {
|
|
animationId = requestAnimationFrame(animate);
|
|
|
|
if (owl && owl.userData) {
|
|
const userData = 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 (isSpeaking) {
|
|
userData.speakTimer += 0.2;
|
|
userData.beak.scale.y = 0.8 + Math.sin(userData.speakTimer) * 0.3;
|
|
userData.head.position.y =
|
|
30 + 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 = 30;
|
|
userData.leftWing.rotation.z = 0.3;
|
|
userData.rightWing.rotation.z = -0.3;
|
|
}
|
|
|
|
// 闲置动画 - 轻微摆动
|
|
userData.idleTimer += 0.008;
|
|
owl.rotation.z = Math.sin(userData.idleTimer) * 0.02;
|
|
|
|
// 头部轻微转动
|
|
userData.head.rotation.z = Math.sin(userData.idleTimer * 0.7) * 0.05;
|
|
}
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
// 开始说话动画
|
|
function startSpeaking() {
|
|
isSpeaking = true;
|
|
owlStage.classList.add("speaking");
|
|
}
|
|
|
|
// 停止说话动画
|
|
function stopSpeaking() {
|
|
isSpeaking = false;
|
|
owlStage.classList.remove("speaking");
|
|
}
|
|
|
|
// 切换表情
|
|
function changeExpression() {
|
|
if (!owl || !owl.userData) return;
|
|
|
|
const expressions = ["normal", "happy", "thinking", "surprised"];
|
|
const currentIndex = expressions.indexOf(currentExpression);
|
|
currentExpression =
|
|
expressions[(currentIndex + 1) % expressions.length];
|
|
|
|
const userData = owl.userData;
|
|
|
|
switch (currentExpression) {
|
|
case "happy":
|
|
// 开心表情 - 眯眼
|
|
userData.leftEyeOuter.scale.y = 0.7;
|
|
userData.rightEyeOuter.scale.y = 0.7;
|
|
userData.beak.scale.set(1.2, 1, 1);
|
|
status.textContent = "😊 开心模式";
|
|
break;
|
|
case "thinking":
|
|
// 思考表情 - 头部倾斜
|
|
userData.head.rotation.z = 0.2;
|
|
userData.leftPupil.position.x = -20;
|
|
userData.rightPupil.position.x = 30;
|
|
status.textContent = "🤔 思考模式";
|
|
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);
|
|
status.textContent = "😮 惊讶模式";
|
|
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.leftPupil.position.x = -25;
|
|
userData.rightPupil.position.x = 25;
|
|
userData.beak.scale.set(1, 0.8, 1);
|
|
status.textContent = "😐 正常模式";
|
|
}
|
|
}
|
|
|
|
// 初始显示欢迎消息
|
|
setTimeout(() => {
|
|
speechBubble.classList.add("visible");
|
|
}, 800);
|
|
|
|
// 事件监听器
|
|
speakBtn.addEventListener("click", () => {
|
|
const text = speechText.value.trim();
|
|
if (!text) {
|
|
status.textContent = "请输入要讲解的内容";
|
|
return;
|
|
}
|
|
|
|
if (speechSynthesis.speaking) {
|
|
speechSynthesis.cancel();
|
|
}
|
|
|
|
currentUtterance = new SpeechSynthesisUtterance(text);
|
|
currentUtterance.lang = "zh-CN";
|
|
currentUtterance.rate = 0.9;
|
|
currentUtterance.pitch = 1.1;
|
|
|
|
currentUtterance.onstart = () => {
|
|
startSpeaking();
|
|
speechBubble.textContent = text;
|
|
speechBubble.classList.add("visible");
|
|
status.textContent = "📚 正在智慧讲解中...";
|
|
speakBtn.disabled = true;
|
|
stopBtn.disabled = false;
|
|
};
|
|
|
|
currentUtterance.onend = () => {
|
|
stopSpeaking();
|
|
status.textContent = "✨ 讲解完成!";
|
|
speakBtn.disabled = false;
|
|
stopBtn.disabled = true;
|
|
};
|
|
|
|
currentUtterance.onerror = (event) => {
|
|
stopSpeaking();
|
|
status.textContent = `❌ 讲解出错: ${event.error}`;
|
|
speakBtn.disabled = false;
|
|
stopBtn.disabled = true;
|
|
};
|
|
|
|
speechSynthesis.speak(currentUtterance);
|
|
});
|
|
|
|
stopBtn.addEventListener("click", () => {
|
|
if (speechSynthesis.speaking) {
|
|
speechSynthesis.cancel();
|
|
stopSpeaking();
|
|
status.textContent = "⏹️ 讲解已停止";
|
|
speakBtn.disabled = false;
|
|
stopBtn.disabled = true;
|
|
}
|
|
});
|
|
|
|
expressionBtn.addEventListener("click", changeExpression);
|
|
|
|
// 鼠标交互
|
|
function addMouseInteractions() {
|
|
const container = document.getElementById("three-container");
|
|
|
|
container.addEventListener("mouseenter", () => {
|
|
if (owl && owl.userData) {
|
|
container.addEventListener("mousemove", followMouse);
|
|
}
|
|
});
|
|
|
|
container.addEventListener("mouseleave", () => {
|
|
if (owl && owl.userData) {
|
|
container.removeEventListener("mousemove", followMouse);
|
|
resetEyes();
|
|
}
|
|
});
|
|
|
|
container.addEventListener("click", () => {
|
|
if (owl && owl.userData) {
|
|
blink();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 眼睛跟随鼠标
|
|
function followMouse(event) {
|
|
if (!owl || !owl.userData) return;
|
|
|
|
const rect = event.target.getBoundingClientRect();
|
|
const x = (event.clientX - rect.left - 200) / 200;
|
|
const y = (event.clientY - rect.top - 200) / 200;
|
|
|
|
const userData = owl.userData;
|
|
const maxOffset = 6;
|
|
|
|
userData.leftPupil.position.x = -25 + x * maxOffset;
|
|
userData.leftPupil.position.y = 40 - y * maxOffset;
|
|
userData.rightPupil.position.x = 25 + x * maxOffset;
|
|
userData.rightPupil.position.y = 40 - y * maxOffset;
|
|
|
|
userData.leftHighlight.position.x = -20 + x * maxOffset * 0.8;
|
|
userData.leftHighlight.position.y = 45 - y * maxOffset * 0.8;
|
|
userData.rightHighlight.position.x = 20 + x * maxOffset * 0.8;
|
|
userData.rightHighlight.position.y = 45 - y * maxOffset * 0.8;
|
|
}
|
|
|
|
// 重置眼睛
|
|
function resetEyes() {
|
|
if (!owl || !owl.userData) return;
|
|
|
|
const userData = owl.userData;
|
|
userData.leftPupil.position.set(-25, 40, 4);
|
|
userData.rightPupil.position.set(25, 40, 4);
|
|
userData.leftHighlight.position.set(-20, 45, 5);
|
|
userData.rightHighlight.position.set(20, 45, 5);
|
|
}
|
|
|
|
// 眨眼
|
|
function blink() {
|
|
if (!owl || !owl.userData) return;
|
|
|
|
const userData = 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);
|
|
}
|
|
|
|
// 初始化
|
|
window.addEventListener("load", () => {
|
|
initThreeJS();
|
|
addMouseInteractions();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|