in the code below it is a microscope simulation for education so i need you to modify the microscope cmponent to make it realistic because in the below simulation there are problems like
- the objective lenses are the same size so fix them they should represent different because they have different magnification poert
- the course foucu nob and the stage y and x controllers these components are just hanging on air with out attaching to the body so change their place to the rightfull place and attach to the body
- make the main body handler of themicroscope more relistic because this looks skinny and not look like a real microscope body or handle
- the specimen slide doesnt contain any component so let it contain teh onion specimen and for the user when viewing via themicroscope create svg or component that tricks users they are seeing different view for all or for the 4 magnifiers and also make it realistic when specimen or stage move so does the object when magnified
- make the design and color of the 4objective lenses realistic and same color as the body
- so user can see the onion with 4 lenses and different view for all as they function and can change the positions as well
- also for mobole users they might not get keyboard so change the logic to let the components be changed and moved by user grabbing them
- when magnification is changed the objective lenses rotate and change smoothly as well
- the current vieing objective lense is above the specimen stage initially at the center above the stage
al handles and component must be attached to the main body handles
- the objective 4 lenses must be attached to the nose piece this occurs in my code but not surround it they must attach on the nose pies bottom surfase not on its side
- change the surrounding theam to brigght smooth hospital theam
- create a realistic viwing of componnets when magnified a movable colorfull 3d copoents to trick the user so learn what is inside an onlion and view
-remember only html5 no frame work
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive 3D Microscope - Onion Cell Experiment</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: url('lab-background.jpg') center/cover no-repeat, linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
overflow: hidden;
color: #ecf0f1;
height: 100vh;
}
canvas {
display: block;
cursor: grab;
}
canvas:active {
cursor: grabbing;
}
/* Loading Screen */
.loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 0.8s ease;
}
.loading.hidden {
opacity: 0;
pointer-events: none;
}
/* Controls Panel */
.controls-panel {
position: fixed;
right: 10px;
top: 10px;
width: 280px;
max-height: calc(100vh - 20px);
background: rgba(26, 26, 46, 0.98);
backdrop-filter: blur(20px);
border-radius: 12px;
padding: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.15);
overflow-y: auto;
z-index: 1000;
transition: all 0.3s ease;
transform: translateX(0);
}
.controls-panel.minimized {
width: 60px;
padding: 8px;
transform: translateX(0);
}
.controls-panel.minimized .panel-content {
display: none;
}
.controls-panel.minimized .toggle-btn {
transform: rotate(180deg);
}
.toggle-btn {
position: absolute;
top: 15px;
right: 15px;
background: rgba(52, 152, 219, 0.2);
border: 1px solid rgba(52, 152, 219, 0.5);
color: #3498db;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.3s ease;
z-index: 1001;
}
.toggle-btn:hover {
background: rgba(52, 152, 219, 0.4);
transform: scale(1.1);
}
.panel-header {
text-align: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.panel-title {
color: #3498db;
font-size: 20px;
font-weight: 700;
margin-bottom: 5px;
}
.panel-subtitle {
color: #95a5a6;
font-size: 14px;
}
.control-group {
margin-bottom: 25px;
}
.control-group h3 {
color: #3498db;
margin-bottom: 15px;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.control-group h3::before {
content: '';
width: 4px;
height: 16px;
background: linear-gradient(135deg, #3498db, #2980b9);
border-radius: 2px;
}
.control-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding: 12px 15px;
background: rgba(255,255,255,0.05);
border-radius: 10px;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.control-item:hover {
background: rgba(255,255,255,0.1);
border-color: rgba(52, 152, 219, 0.3);
transform: translateY(-1px);
}
.control-label {
font-size: 14px;
color: #ecf0f1;
font-weight: 500;
}
.control-value {
font-size: 14px;
color: #3498db;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.instruction-step {
padding: 15px;
margin: 10px 0;
background: rgba(52, 152, 219, 0.1);
border-left: 4px solid #3498db;
border-radius: 0 10px 10px 0;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.instruction-step::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(52, 152, 219, 0.1), transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
.instruction-step:hover::before {
opacity: 1;
}
.instruction-step:hover {
background: rgba(52, 152, 219, 0.2);
transform: translateX(5px);
}
.instruction-step.active {
background: rgba(52, 152, 219, 0.3);
border-left-color: #2980b9;
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.2);
}
.instruction-step.completed {
background: rgba(39, 174, 96, 0.2);
border-left-color: #27ae60;
color: #2ecc71;
}
/* Camera Controls */
.camera-controls {
position: fixed;
bottom: 10px;
left: 10px;
background: rgba(26, 26, 46, 0.98);
backdrop-filter: blur(20px);
border-radius: 8px;
padding: 10px;
border: 1px solid rgba(255,255,255,0.15);
z-index: 1000;
transition: all 0.3s ease;
}
.camera-controls.minimized {
width: 50px;
height: 50px;
padding: 5px;
border-radius: 50%;
}
.camera-controls.minimized h4,
.camera-controls.minimized .camera-button:not(.toggle-cam) {
display: none;
}
.progress-tracker {
position: fixed;
top: 10px;
left: 10px;
background: rgba(26, 26, 46, 0.98);
backdrop-filter: blur(20px);
border-radius: 12px;
padding: 15px;
border: 1px solid rgba(255,255,255,0.15);
z-index: 1000;
min-width: 250px;
}
.progress-step {
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 8px;
border-radius: 6px;
transition: all 0.3s ease;
}
.progress-step.completed {
background: rgba(39, 174, 96, 0.2);
border-left: 3px solid #27ae60;
}
.progress-step.active {
background: rgba(52, 152, 219, 0.2);
border-left: 3px solid #3498db;
animation: pulse 2s infinite;
}
.progress-step.pending {
background: rgba(149, 165, 166, 0.1);
border-left: 3px solid #95a5a6;
}
.step-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(52, 152, 219, 0.3);
color: #3498db;
font-size: 12px;
font-weight: bold;
margin-right: 10px;
}
.step-number.completed {
background: #27ae60;
color: white;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.camera-controls h4 {
color: #3498db;
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
}
.camera-button {
background: rgba(52, 152, 219, 0.2);
border: 1px solid #3498db;
color: #3498db;
padding: 8px 12px;
margin: 4px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.camera-button:hover {
background: rgba(52, 152, 219, 0.3);
transform: translateY(-1px);
}
.camera-button.active {
background: #3498db;
color: white;
}
/* Eyepiece View */
.eyepiece-view {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 450px;
height: 450px;
background: radial-gradient(circle, #000 0%, #1a1a1a 70%, #333 100%);
border-radius: 50%;
border: 12px solid #2c3e50;
display: none;
z-index: 2000;
box-shadow:
0 0 60px rgba(0,0,0,0.8),
inset 0 0 30px rgba(0,0,0,0.5);
}
.eyepiece-view.active {
display: block;
animation: zoomIn 0.4s ease-out;
}
@keyframes zoomIn {
from {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
}
to {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
}
.field-of-view {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 350px;
height: 350px;
background: #f8f9fa;
border-radius: 50%;
overflow: hidden;
border: 3px solid #34495e;
box-shadow: inset 0 0 20px rgba(0,0,0,0.3);
}
.specimen-view {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200%;
height: 200%;
background: url('onion-cells.jpg') center/cover no-repeat;
opacity: 0.8;
transition: all 0.3s ease;
}
.specimen-view.focused {
opacity: 1;
filter: brightness(110%) contrast(120%);
}
.specimen-view.blurred {
opacity: 0.3;
filter: blur(8px);
}
.crosshairs {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 120px;
height: 120px;
pointer-events: none;
}
.crosshairs::before,
.crosshairs::after {
content: '';
position: absolute;
background: rgba(255,255,255,0.6);
box-shadow: 0 0 2px rgba(0,0,0,0.5);
}
.crosshairs::before {
top: 50%;
left: 0;
right: 0;
height: 1px;
transform: translateY(-50%);
}
.crosshairs::after {
left: 50%;
top: 0;
bottom: 0;
width: 1px;
transform: translateX(-50%);
}
/* Status Bar */
.status-bar {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(26, 26, 46, 0.95);
backdrop-filter: blur(15px);
border-radius: 12px;
padding: 12px 16px;
border: 1px solid rgba(255,255,255,0.1);
font-size: 14px;
color: #3498db;
z-index: 1000;
}
/* Responsive Design */
@media (max-width: 1024px) {
.controls-panel {
width: 240px;
font-size: 12px;
right: 5px;
top: 5px;
}
.progress-tracker {
left: 5px;
top: 5px;
min-width: 200px;
font-size: 12px;
}
.camera-controls {
bottom: 5px;
left: 5px;
}
.eyepiece-view {
width: 350px;
height: 350px;
}
.field-of-view {
width: 280px;
height: 280px;
}
}
@media (max-width: 768px) {
.controls-panel {
width: 200px;
padding: 10px;
}
.controls-panel.minimized {
width: 50px;
}
.progress-tracker {
min-width: 180px;
padding: 10px;
}
.camera-controls {
padding: 8px;
}
.eyepiece-view {
width: 280px;
height: 280px;
}
.field-of-view {
width: 220px;
height: 220px;
}
}
@media (max-width: 480px) {
.controls-panel {
width: 180px;
font-size: 11px;
}
.progress-tracker {
min-width: 160px;
font-size: 11px;
}
.eyepiece-view {
width: 250px;
height: 250px;
}
.field-of-view {
width: 200px;
height: 200px;
}
}
/* Tooltips */
.tooltip {
position: absolute;
background: rgba(0,0,0,0.9);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
pointer-events: none;
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
}
.tooltip.visible {
opacity: 1;
}
/* Performance indicator */
.performance-stats {
position: fixed;
top: 20px;
left: 20px;
background: rgba(0,0,0,0.7);
color: #00ff00;
font-family: 'Courier New', monospace;
font-size: 12px;
padding: 10px;
border-radius: 4px;
display: none;
}
.performance-stats.visible {
display: block;
}
/* Highlight effects */
.highlight-glow {
animation: highlightPulse 2s infinite;
}
@keyframes highlightPulse {
0%, 100% {
filter: drop-shadow(0 0 10px rgba(52, 152, 219, 0.6));
}
50% {
filter: drop-shadow(0 0 20px rgba(52, 152, 219, 1));
}
}
</style>
</head>
<body>
<!-- Loading Screen -->
<div class="loading" id="loading">
<div class="spinner"></div>
<div class="loading-text">
Loading 3D Microscope Simulation...<br>
<small>Preparing realistic components and lighting</small>
</div>
</div>
<!-- Performance Stats (hidden by default) -->
<div class="performance-stats" id="performance-stats">
FPS: <span id="fps">0</span><br>
Triangles: <span id="triangles">0</span><br>
Draw Calls: <span id="draw-calls">0</span>
</div>
<!-- Progress Tracker -->
<div class="progress-tracker" id="progress-tracker">
<h4 style="color: #3498db; margin-bottom: 15px; font-size: 14px;">Experiment Progress</h4>
<div class="progress-step active" data-step="0">
<span class="step-number">1</span>
<span>Prepare specimen slide</span>
</div>
<div class="progress-step pending" data-step="1">
<span class="step-number">2</span>
<span>Place slide on stage</span>
</div>
<div class="progress-step pending" data-step="2">
<span class="step-number">3</span>
<span>Adjust coarse focus</span>
</div>
<div class="progress-step pending" data-step="3">
<span class="step-number">4</span>
<span>Fine focus adjustment</span>
</div>
<div class="progress-step pending" data-step="4">
<span class="step-number">5</span>
<span>Change magnification</span>
</div>
<div class="progress-step pending" data-step="5">
<span class="step-number">6</span>
<span>Observe cells</span>
</div>
</div>
<!-- Controls Panel -->
<div class="controls-panel" id="controls-panel">
<button class="toggle-btn" id="toggle-controls">▶</button>
<div class="panel-content">
<div class="panel-header">
<div class="panel-title">Microscope Controls</div>
<div class="panel-subtitle">Interactive Settings</div>
</div>
<div class="control-group">
<h3>Current Settings</h3>
<div class="control-item">
<span class="control-label">Magnification</span>
<span class="control-value" id="current-mag">4x</span>
</div>
<div class="control-item">
<span class="control-label">Focus</span>
<span class="control-value" id="current-focus">0.0</span>
</div>
<div class="control-item">
<span class="control-label">Brightness</span>
<span class="control-value" id="current-brightness">75%</span>
</div>
<div class="control-item">
<span class="control-label">Stage Position</span>
<span class="control-value" id="stage-position">0, 0</span>
</div>
</div>
<div class="control-group">
<h3>Quick Actions</h3>
<div class="control-item" style="cursor: pointer;" onclick="resetExperiment()">
<span class="control-label">🔄 Reset Experiment</span>
</div>
<div class="control-item" style="cursor: pointer;" onclick="toggleEyepiece()">
<span class="control-label">👁️ Toggle Eyepiece</span>
</div>
<div class="control-item" style="cursor: pointer;" onclick="autoFocus()">
<span class="control-label">🎯 Auto Focus</span>
</div>
</div>
<div class="control-group">
<h3>Keyboard Shortcuts</h3>
<div class="control-item">
<span class="control-label">Arrow Keys</span>
<span class="control-value">Move Stage</span>
</div>
<div class="control-item">
<span class="control-label">+/-</span>
<span class="control-value">Focus</span>
</div>
<div class="control-item">
<span class="control-label">1-4</span>
<span class="control-value">Objectives</span>
</div>
<div class="control-item">
<span class="control-label">Space</span>
<span class="control-value">Eyepiece</span>
</div>
<div class="control-item">
<span class="control-label">R</span>
<span class="control-value">Reset View</span>
</div>
</div>
</div>
</div>
<!-- Camera Controls -->
<div class="camera-controls" id="camera-controls">
<button class="camera-button toggle-cam" onclick="toggleCameraControls()">📷</button>
<div class="camera-content">
<h4>View Controls</h4>
<button class="camera-button active" id="orbit-btn">🔄 Orbit</button>
<button class="camera-button" id="pan-btn">✋ Pan</button>
<button class="camera-button" id="zoom-btn">🔍 Zoom</button>
<button class="camera-button" id="reset-btn">🏠 Reset</button>
</div>
</div>
<!-- Status Bar -->
<div class="status-bar" id="status-bar">
Click and drag to orbit • Scroll to zoom • Right-click to pan
</div>
<!-- Eyepiece View -->
<div class="eyepiece-view" id="eyepiece-view">
<div class="field-of-view">
<div class="specimen-view" id="specimen-view"></div>
<div class="crosshairs"></div>
</div>
</div>
<!-- Tooltip -->
<div class="tooltip" id="tooltip"></div>
<!-- Three.js Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
class Realistic3DMicroscope {
constructor() {
// Initialize state
this.state = {
magnification: 4,
focus: 0,
brightness: 75,
stagePosition: { x: 0, y: 0 },
eyepieceActive: false,
currentStep: 0,
isInteracting: false,
cameraMode: 'orbit'
};
// Magnification settings
this.magnifications = {
4: { scale: 1, blur: 0, color: '#e74c3c' },
10: { scale: 2.5, blur: 0, color: '#f39c12' },
40: { scale: 10, blur: 0, color: '#27ae60' },
100: { scale: 25, blur: 0, color: '#8e44ad' }
};
// Performance monitoring
this.clock = new THREE.Clock();
this.frameCount = 0;
this.lastTime = 0;
this.init();
}
async init() {
await this.setupScene();
await this.createMicroscope();
this.setupLighting();
this.setupControls();
this.setupEventListeners();
this.setupPerformanceMonitoring();
this.animate();
// Hide loading screen
setTimeout(() => {
document.getElementById('loading').classList.add('hidden');
this.updateStatusBar('3D Microscope loaded successfully!');
this.initializeExperiment();
}, 2000);
}
async setupScene() {
// Scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x0f1419);
this.scene.fog = new THREE.Fog(0x0f1419, 50, 200);
// Camera
this.camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.camera.position.set(15, 12, 20);
this.camera.lookAt(0, 5, 0);
// Renderer
this.renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: "high-performance"
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1.2;
this.renderer.outputEncoding = THREE.sRGBEncoding;
document.body.appendChild(this.renderer.domElement);
// Raycaster for interaction
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
// Groups for organization
this.microscopeGroup = new THREE.Group();
this.scene.add(this.microscopeGroup);
}
async createMicroscope() {
await this.createBase();
await this.createArm();
await this.createStage();
await this.createTube();
await this.createEyepiece();
await this.createNosepiece();
await this.createObjectives();
await this.createFocusKnobs();
await this.createIlluminator();
await this.createEnvironment();
}
async createBase() {
// Main base
const baseGeometry = new THREE.CylinderGeometry(6, 6.5, 1.5, 32);
const baseMaterial = new THREE.MeshPhysicalMaterial({
color: 0x2c3e50,
metalness: 0.8,
roughness: 0.2,
clearcoat: 1.0,
clearcoatRoughness: 0.1
});
this.base = new THREE.Mesh(baseGeometry, baseMaterial);
this.base.position.y = 0.75;
this.base.castShadow = true;
this.base.receiveShadow = true;
this.microscopeGroup.add(this.base);
// Base details
const detailGeometry = new THREE.CylinderGeometry(5.5, 5.5, 0.3, 32);
const detailMaterial = new THREE.MeshPhysicalMaterial({
color: 0x3498db,
metalness: 0.9,
roughness: 0.1,
emissive: 0x1e5a96,
emissiveIntensity: 0.1
});
const detail = new THREE.Mesh(detailGeometry, detailMaterial);
detail.position.y = 1.3;
this.microscopeGroup.add(detail);
// Brand marking
const loader = new THREE.FontLoader();
// Note: In a real implementation, you'd load a font file
// For this demo, we'll add a simple geometric brand mark
const brandGeometry = new THREE.CylinderGeometry(1, 1, 0.1, 8);
const brandMaterial = new THREE.MeshPhysicalMaterial({
color: 0x34495e,
metalness: 0.7,
roughness: 0.3
});
const brand = new THREE.Mesh(brandGeometry, brandMaterial);
brand.position.set(0, 1.45, 0);
brand.rotation.y = Math.PI / 8;
this.microscopeGroup.add(brand);
}
async createArm() {
// Create arm using curve
const points = [
new THREE.Vector3(0, 1.5, 0),
new THREE.Vector3(-2, 4, 0),
new THREE.Vector3(-2.5, 8, 0),
new THREE.Vector3(-1, 12, 0),
new THREE.Vector3(0, 14, 0)
];
const curve = new THREE.CatmullRomCurve3(points);
const tubeGeometry = new THREE.TubeGeometry(curve, 50, 0.8, 16, false);
const armMaterial = new THREE.MeshPhysicalMaterial({
color: 0x34495e,
metalness: 0.8,
roughness: 0.3,
clearcoat: 0.5
});
this.arm = new THREE.Mesh(tubeGeometry, armMaterial);
this.arm.castShadow = true;
this.microscopeGroup.add(this.arm);
}
async createStage() {
// Main stage platform
const stageGeometry = new THREE.BoxGeometry(8, 0.5, 6);
const stageMaterial = new THREE.MeshPhysicalMaterial({
color: 0xecf0f1,
metalness: 0.1,
roughness: 0.4,
clearcoat: 0.8
});
this.stage = new THREE.Mesh(stageGeometry, stageMaterial);
this.stage.position.set(0, 8, 0);
this.stage.castShadow = true;
this.stage.receiveShadow = true;
this.stage.userData = { type: 'stage' };
this.microscopeGroup.add(this.stage);
// Stage clips
for (let i = 0; i < 2; i++) {
const clipGeometry = new THREE.BoxGeometry(0.3, 0.3, 1.5);
const clipMaterial = new THREE.MeshPhysicalMaterial({
color: 0x7f8c8d,
metalness: 0.9,
roughness: 0.1
});
const clip = new THREE.Mesh(clipGeometry, clipMaterial);
clip.position.set(i === 0 ? -3.5 : 3.5, 8.4, 0);
this.microscopeGroup.add(clip);
}
// Realistic glass slide
const slideGeometry = new THREE.BoxGeometry(3, 0.08, 1);
const slideMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
metalness: 0,
roughness: 0.05,
transmission: 0.98,
transparent: true,
opacity: 0.1,
thickness: 0.5,
ior: 1.5,
clearcoat: 1.0,
clearcoatRoughness: 0.0
});
this.slide = new THREE.Mesh(slideGeometry, slideMaterial);
this.slide.position.set(0, 8.3, 0);
this.slide.userData = { type: 'slide' };
this.slide.castShadow = true;
this.slide.receiveShadow = true;
this.microscopeGroup.add(this.slide);
// Glass cover slip
const coverGeometry = new THREE.BoxGeometry(1.5, 0.02, 0.8);
const coverMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
metalness: 0,
roughness: 0,
transmission: 0.99,
transparent: true,
opacity: 0.05,
ior: 1.52
});
const coverSlip = new THREE.Mesh(coverGeometry, coverMaterial);
coverSlip.position.set(0, 8.36, 0);
this.microscopeGroup.add(coverSlip);
// Specimen on slide (geometry first)
const specimenGeometry = new THREE.CircleGeometry(0.3, 32);
// Load real onion sample texture
const textureLoader = new THREE.TextureLoader();
textureLoader.load('onion-sample.jpg', (texture) => {
const specimenMaterial = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.8
});
this.specimen = new THREE.Mesh(specimenGeometry, specimenMaterial);
this.specimen.position.set(0, 8.31, 0);
this.specimen.rotation.x = -Math.PI / 2;
this.microscopeGroup.add(this.specimen);
});
// Stage control knobs
this.createStageKnobs();
}
createStageKnobs() {
this.stageKnobs = [];
const knobPositions = [
{ x: 6, y: 8, z: 1.5, axis: 'x' },
{ x: 6, y: 8, z: -1.5, axis: 'y' }
];
knobPositions.forEach((pos, index) => {
const knobGroup = new THREE.Group();
// Knob body
const knobGeometry = new THREE.CylinderGeometry(0.8, 0.8, 0.4, 32);
const knobMaterial = new THREE.MeshPhysicalMaterial({
color: 0x34495e,
metalness: 0.8,
roughness: 0.2
});
const knob = new THREE.Mesh(knobGeometry, knobMaterial);
knobGroup.add(knob);
// Knob grip lines
for (let i = 0; i < 8; i++) {
const lineGeometry = new THREE.BoxGeometry(0.05, 0.3, 1.6);
const lineMaterial = new THREE.MeshBasicMaterial({ color: 0x2c3e50 });
const line = new THREE.Mesh(lineGeometry, lineMaterial);
line.rotation.y = (i / 8) * Math.PI * 2;
knobGroup.add(line);
}
knobGroup.position.set(pos.x, pos.y, pos.z);
knobGroup.rotation.z = Math.PI / 2;
knobGroup.userData = {
type: 'stageKnob',
axis: pos.axis,
originalRotation: knobGroup.rotation.clone()
};
this.stageKnobs.push(knobGroup);
this.microscopeGroup.add(knobGroup);
});
}
async createTube() {
// Main tube
const tubeGeometry = new THREE.CylinderGeometry(1.2, 1.2, 8, 32);
const tubeMaterial = new THREE.MeshPhysicalMaterial({
color: 0x34495e,
metalness: 0.8,
roughness: 0.3
});
this.tube = new THREE.Mesh(tubeGeometry, tubeMaterial);
this.tube.position.set(0, 18, 0);
this.tube.castShadow = true;
this.microscopeGroup.add(this.tube);
}
async createEyepiece() {
const eyepieceGroup = new THREE.Group();
// Eyepiece body
const bodyGeometry = new THREE.CylinderGeometry(1.5, 1.2, 3, 32);
const bodyMaterial = new THREE.MeshPhysicalMaterial({
color: 0x2c3e50,
metalness: 0.8,
roughness: 0.2
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
eyepieceGroup.add(body);
// Eyepiece lens
const lensGeometry = new THREE.CylinderGeometry(1.2, 1.2, 0.1, 32);
const lensMaterial = new THREE.MeshPhysicalMaterial({
color: 0x3498db,
metalness: 0,
roughness: 0,
transmission: 0.9,
transparent: true
});
const lens = new THREE.Mesh(lensGeometry, lensMaterial);
lens.position.y = 1.4;
eyepieceGroup.add(lens);
eyepieceGroup.position.set(0, 24, 0);
eyepieceGroup.userData = { type: 'eyepiece' };
this.eyepiece = eyepieceGroup;
this.microscopeGroup.add(eyepieceGroup);
}
async createNosepiece() {
// Nosepiece turret
const nosepieceGeometry = new THREE.CylinderGeometry(2, 2, 1.5, 6);
const nosepieceMaterial = new THREE.MeshPhysicalMaterial({
color: 0x34495e,
metalness: 0.8,
roughness: 0.2
});
this.nosepiece = new THREE.Mesh(nosepieceGeometry, nosepieceMaterial);
this.nosepiece.position.set(0, 13, 0);
this.nosepiece.castShadow = true;
this.nosepiece.userData = {
type: 'nosepiece',
rotation: 0
};
this.microscopeGroup.add(this.nosepiece);
}
async createObjectives() {
this.objectives = [];
const magnifications = [4, 10, 40, 100];
const colors = [0xe74c3c, 0xf39c12, 0x27ae60, 0x8e44ad];
magnifications.forEach((mag, index) => {
const objectiveGroup = new THREE.Group();
// Objective body
const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.6, 3, 16);
const bodyMaterial = new THREE.MeshPhysicalMaterial({
color: colors[index],
metalness: 0.8,
roughness: 0.3
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
objectiveGroup.add(body);
// Objective lens
const lensGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 16);
const lensMaterial = new THREE.MeshPhysicalMaterial({
color: 0x87ceeb,
metalness: 0,
roughness: 0,
transmission: 0.8,
transparent: true
});
const lens = new THREE.Mesh(lensGeometry, lensMaterial);
lens.position.y = -1.4;
objectiveGroup.add(lens);
// Position around nosepiece
const angle = (index / magnifications.length) * Math.PI * 2;
const radius = 2.5;
objectiveGroup.position.set(
Math.cos(angle) * radius,
13,
Math.sin(angle) * radius
);
objectiveGroup.userData = {
type: 'objective',
magnification: mag,
active: mag === 4,
originalPosition: objectiveGroup.position.clone(),
angle: angle
};
this.objectives.push(objectiveGroup);
this.microscopeGroup.add(objectiveGroup);
});
// Set initial active objective
this.setActiveObjective(4);
}
async createFocusKnobs() {
this.focusKnobs = [];
const knobData = [
{ type: 'coarse', size: 2, position: { x: 4, y: 16, z: 0 } },
{ type: 'fine', size: 1.5, position: { x: 6.5, y: 16, z: 0 } }
];
knobData.forEach(data => {
const knobGroup = new THREE.Group();
// Main knob
const knobGeometry = new THREE.CylinderGeometry(data.size, data.size, 0.8, 32);
const knobMaterial = new THREE.MeshPhysicalMaterial({
color: 0x34495e,
metalness: 0.8,
roughness: 0.2
});
const knob = new THREE.Mesh(knobGeometry, knobMaterial);
knobGroup.add(knob);
// Knob details
for (let i = 0; i < 12; i++) {
const detailGeometry = new THREE.BoxGeometry(0.1, 0.6, data.size * 2);
const detailMaterial = new THREE.MeshBasicMaterial({ color: 0x2c3e50 });
const detail = new THREE.Mesh(detailGeometry, detailMaterial);
detail.rotation.y = (i / 12) * Math.PI * 2;
knobGroup.add(detail);
}
knobGroup.position.set(data.position.x, data.position.y, data.position.z);
knobGroup.rotation.z = Math.PI / 2;
knobGroup.userData = {
type: 'focusKnob',
focusType: data.type,
originalRotation: knobGroup.rotation.clone()
};
this.focusKnobs.push(knobGroup);
this.microscopeGroup.add(knobGroup);
});
}
async createIlluminator() {
const illuminatorGroup = new THREE.Group();
// Illuminator body
const bodyGeometry = new THREE.CylinderGeometry(2, 2, 1.5, 32);
const bodyMaterial = new THREE.MeshPhysicalMaterial({
color: 0xf39c12,
metalness: 0.7,
roughness: 0.3,
emissive: 0xf39c12,
emissiveIntensity: 0.1
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
illuminatorGroup.add(body);
// Light source
this.illuminatorLight = new THREE.SpotLight(0xffffff, 1, 50, Math.PI / 6, 0.5);
this.illuminatorLight.position.set(0, 2, 0);
this.illuminatorLight.target.position.set(0, 8, 0);
this.illuminatorLight.castShadow = true;
this.illuminatorLight.shadow.mapSize.width = 1024;
this.illuminatorLight.shadow.mapSize.height = 1024;
illuminatorGroup.add(this.illuminatorLight);
illuminatorGroup.add(this.illuminatorLight.target);
illuminatorGroup.position.set(0, 4, 0);
illuminatorGroup.userData = { type: 'illuminator' };
this.illuminator = illuminatorGroup;
this.microscopeGroup.add(illuminatorGroup);
}
async createEnvironment() {
// Lab table
const tableGeometry = new THREE.BoxGeometry(50, 2, 30);
const tableMaterial = new THREE.MeshPhysicalMaterial({
color: 0x8b4513,
metalness: 0,
roughness: 0.8
});
const table = new THREE.Mesh(tableGeometry, tableMaterial);
table.position.set(0, -1, 0);
table.receiveShadow = true;
this.scene.add(table);
// Lab equipment (decorative)
this.createLabEquipment();
}
createLabEquipment() {
// Beakers
for (let i = 0; i < 3; i++) {
const beakerGeometry = new THREE.CylinderGeometry(1, 1.2, 3, 16);
const beakerMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
metalness: 0,
roughness: 0,
transmission: 0.9,
transparent: true
});
const beaker = new THREE.Mesh(beakerGeometry, beakerMaterial);
beaker.position.set(-15 + i * 5, 1.5, -10);
beaker.castShadow = true;
this.scene.add(beaker);
}
// Books
for (let i = 0; i < 5; i++) {
const bookGeometry = new THREE.BoxGeometry(1.5, 0.3, 2);
const bookMaterial = new THREE.MeshPhysicalMaterial({
color: new THREE.Color().setHSL(Math.random(), 0.7, 0.5),
metalness: 0,
roughness: 0.8
});
const book = new THREE.Mesh(bookGeometry, bookMaterial);
book.position.set(10 + i * 0.1, i * 0.3, 8);
book.rotation.y = Math.random() * 0.2 - 0.1;
book.castShadow = true;
this.scene.add(book);
}
}
setupLighting() {
// Ambient light
const ambientLight = new THREE.AmbientLight(0x404040, 0.3);
this.scene.add(ambientLight);
// Main directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(20, 30, 20);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 100;
directionalLight.shadow.camera.left = -30;
directionalLight.shadow.camera.right = 30;
directionalLight.shadow.camera.top = 30;
directionalLight.shadow.camera.bottom = -30;
this.scene.add(directionalLight);
// Fill light
const fillLight = new THREE.DirectionalLight(0x87ceeb, 0.3);
fillLight.position.set(-20, 10, -20);
this.scene.add(fillLight);
// Rim light
const rimLight = new THREE.DirectionalLight(0xffffff, 0.2);
rimLight.position.set(0, 10, -30);
this.scene.add(rimLight);
}
setupControls() {
// Camera controls
this.cameraTarget = new THREE.Vector3(0, 12, 0);
this.cameraDistance = 25;
this.cameraTheta = Math.PI / 6;
this.cameraPhi = Math.PI / 3;
this.updateCameraPosition();
}
updateCameraPosition() {
const x = this.cameraDistance * Math.sin(this.cameraPhi) * Math.cos(this.cameraTheta);
const y = this.cameraDistance * Math.cos(this.cameraPhi) + this.cameraTarget.y;
const z = this.cameraDistance * Math.sin(this.cameraPhi) * Math.sin(this.cameraTheta);
this.camera.position.set(x, y, z);
this.camera.lookAt(this.cameraTarget);
}
initializeExperiment() {
this.experimentState = {
currentStep: 0,
steps: [
{ name: 'Prepare specimen slide', completed: false, target: 'slide' },
{ name: 'Place slide on stage', completed: false, target: 'stage' },
{ name: 'Adjust coarse focus', completed: false, target: 'coarse-focus' },
{ name: 'Fine focus adjustment', completed: false, target: 'fine-focus' },
{ name: 'Change magnification', completed: false, target: 'objectives' },
{ name: 'Observe cells', completed: false, target: 'eyepiece' }
]
};
this.updateProgressTracker();
}
updateProgressTracker() {
const steps = document.querySelectorAll('.progress-step');
steps.forEach((step, index) => {
const stepNumber = step.querySelector('.step-number');
if (index < this.experimentState.currentStep) {
step.className = 'progress-step completed';
stepNumber.textContent = '✓';
stepNumber.className = 'step-number completed';
} else if (index === this.experimentState.currentStep) {
step.className = 'progress-step active';
stepNumber.textContent = index + 1;
stepNumber.className = 'step-number';
} else {
step.className = 'progress-step pending';
stepNumber.textContent = index + 1;
stepNumber.className = 'step-number';
}
});
}
completeCurrentStep() {
if (this.experimentState.currentStep < this.experimentState.steps.length) {
this.experimentState.steps[this.experimentState.currentStep].completed = true;
this.experimentState.currentStep++;
this.updateProgressTracker();
if (this.experimentState.currentStep >= this.experimentState.steps.length) {
this.updateStatusBar('Experiment completed! Great job!');
setTimeout(() => {
alert('🎉 Congratulations! You have successfully completed the onion cell observation experiment!');
}, 1000);
} else {
const nextStep = this.experimentState.steps[this.experimentState.currentStep];
this.updateStatusBar(`Next: ${nextStep.name}`);
}
}
}
toggleControlsPanel() {
const panel = document.getElementById('controls-panel');
const toggleBtn = document.getElementById('toggle-controls');
panel.classList.toggle('minimized');
toggleBtn.textContent = panel.classList.contains('minimized') ? '▶' : '◀';
}
updateSpecimenView() {
const specimenView = document.getElementById('specimen-view');
if (!specimenView) return;
// Update specimen view based on focus and magnification
const focusQuality = Math.max(0, 1 - Math.abs(this.state.focus) * 0.1);
const magnificationScale = 1 + (this.state.magnification - 4) * 0.1;
specimenView.style.transform = `translate(-50%, -50%) scale(${magnificationScale})`;
if (focusQuality > 0.8) {
specimenView.className = 'specimen-view focused';
} else if (focusQuality < 0.3) {
specimenView.className = 'specimen-view blurred';
} else {
specimenView.className = 'specimen-view';
}
}
setupEventListeners() {
// Mouse events
this.renderer.domElement.addEventListener('mousedown', this.onMouseDown.bind(this));
this.renderer.domElement.addEventListener('mousemove', this.onMouseMove.bind(this));
this.renderer.domElement.addEventListener('mouseup', this.onMouseUp.bind(this));
this.renderer.domElement.addEventListener('wheel', this.onMouseWheel.bind(this));
this.renderer.domElement.addEventListener('contextmenu', e => e.preventDefault());
// Keyboard events
document.addEventListener('keydown', this.onKeyDown.bind(this));
document.addEventListener('keyup', this.onKeyUp.bind(this));
// UI events
document.getElementById('toggle-controls').addEventListener('click', this.toggleControlsPanel.bind(this));
document.getElementById('orbit-btn').addEventListener('click', () => this.setCameraMode('orbit'));
document.getElementById('pan-btn').addEventListener('click', () => this.setCameraMode('pan'));
document.getElementById('zoom-btn').addEventListener('click', () => this.setCameraMode('zoom'));
document.getElementById('reset-btn').addEventListener('click', () => this.resetCamera());
// Window resize
window.addEventListener('resize', this.onWindowResize.bind(this));
// Touch events for mobile
this.setupTouchEvents();
}
setupTouchEvents() {
let touchStart = null;
let touchCurrent = null;
this.renderer.domElement.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
});
this.renderer.domElement.addEventListener('touchmove', (e) => {
e.preventDefault();
if (e.touches.length === 1 && touchStart) {
touchCurrent = { x: e.touches[0].clientX, y: e.touches[0].clientY };
const deltaX = touchCurrent.x - touchStart.x;
const deltaY = touchCurrent.y - touchStart.y;
this.cameraTheta -= deltaX * 0.01;
this.cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, this.cameraPhi + deltaY * 0.01));
this.updateCameraPosition();
touchStart = touchCurrent;
}
});
this.renderer.domElement.addEventListener('touchend', () => {
touchStart = null;
touchCurrent = null;
});
}
onMouseDown(event) {
this.mouseDown = true;
this.mouseButton = event.button;
this.lastMouseX = event.clientX;
this.lastMouseY = event.clientY;
// Check for object interaction
this.checkObjectInteraction(event);
}
onMouseMove(event) {
// Update mouse position for raycasting
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
if (this.mouseDown) {
const deltaX = event.clientX - this.lastMouseX;
const deltaY = event.clientY - this.lastMouseY;
if (this.state.cameraMode === 'orbit' && this.mouseButton === 0) {
// Orbit
this.cameraTheta -= deltaX * 0.01;
this.cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, this.cameraPhi + deltaY * 0.01));
this.updateCameraPosition();
} else if (this.state.cameraMode === 'pan' || this.mouseButton === 2) {
// Pan
const panSpeed = 0.02;
const right = new THREE.Vector3().crossVectors(this.camera.up,
new THREE.Vector3().subVectors(this.camera.position, this.cameraTarget).normalize());
const up = new THREE.Vector3().crossVectors(
new THREE.Vector3().subVectors(this.camera.position, this.cameraTarget).normalize(), right);
this.cameraTarget.addScaledVector(right, -deltaX * panSpeed);
this.cameraTarget.addScaledVector(up, deltaY * panSpeed);
this.updateCameraPosition();
}
this.lastMouseX = event.clientX;
this.lastMouseY = event.clientY;
}
// Update tooltip
this.updateTooltip(event);
}
onMouseUp(event) {
this.mouseDown = false;
this.state.isInteracting = false;
}
onMouseWheel(event) {
event.preventDefault();
const zoomSpeed = 0.1;
this.cameraDistance = Math.max(10, Math.min(50, this.cameraDistance + event.deltaY * zoomSpeed));
this.updateCameraPosition();
}
checkObjectInteraction(event) {
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects(this.microscopeGroup.children, true);
if (intersects.length > 0) {
const object = intersects[0].object;
let userData = object.userData;
// Check parent userData if object doesn't have it
if (!userData.type && object.parent) {
userData = object.parent.userData;
}
this.handleObjectClick(userData, intersects[0].point);
}
}
handleObjectClick(userData, point) {
switch (userData.type) {
case 'objective':
this.changeObjective(userData.magnification);
break;
case 'eyepiece':
this.toggleEyepiece();
break;
case 'stageKnob':
this.state.isInteracting = true;
break;
case 'focusKnob':
this.state.isInteracting = true;
break;
case 'nosepiece':
this.rotateNosepiece();
break;
}
}
onKeyDown(event) {
switch (event.code) {
case 'ArrowLeft':
this.moveStage(-0.1, 0);
break;
case 'ArrowRight':
this.moveStage(0.1, 0);
break;
case 'ArrowUp':
this.moveStage(0, 0.1);
break;
case 'ArrowDown':
this.moveStage(0, -0.1);
break;
case 'Equal':
case 'NumpadAdd':
this.adjustFocus(1, 'fine');
break;
case 'Minus':
case 'NumpadSubtract':
this.adjustFocus(-1, 'fine');
break;
case 'Digit1':
this.changeObjective(4);
break;
case 'Digit2':
this.changeObjective(10);
break;
case 'Digit3':
this.changeObjective(40);
break;
case 'Digit4':
this.changeObjective(100);
break;
case 'Space':
event.preventDefault();
this.toggleEyepiece();
break;
case 'KeyR':
this.resetCamera();
break;
case 'Escape':
if (this.state.eyepieceActive) {
this.toggleEyepiece();
}
break;
}
}
onKeyUp(event) {
// Handle key releases if needed
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
setupPerformanceMonitoring() {
this.stats = {
fps: 0,
frameCount: 0,
lastTime: performance.now()
};
}
updatePerformanceStats() {
this.stats.frameCount++;
const now = performance.now();
if (now >= this.stats.lastTime + 1000) {
this.stats.fps = Math.round((this.stats.frameCount * 1000) / (now - this.stats.lastTime));
this.stats.frameCount = 0;
this.stats.lastTime = now;
// Update performance display if visible
const fpsElement = document.getElementById('fps');
const trianglesElement = document.getElementById('triangles');
const drawCallsElement = document.getElementById('draw-calls');
if (fpsElement) fpsElement.textContent = this.stats.fps;
if (trianglesElement) trianglesElement.textContent = this.renderer.info.render.triangles;
if (drawCallsElement) drawCallsElement.textContent = this.renderer.info.render.calls;
}
}
// Microscope functionality methods
changeObjective(magnification) {
this.state.magnification = magnification;
this.setActiveObjective(magnification);
this.updateUI();
this.updateSpecimenView();
this.updateStatusBar(`Magnification changed to ${magnification}x`);
// Check if this completes step 5
if (this.experimentState.currentStep === 4) {
this.completeCurrentStep();
}
}
setActiveObjective(magnification) {
this.objectives.forEach(obj => {
obj.userData.active = obj.userData.magnification === magnification;
// Add visual feedback for active objective
if (obj.userData.active) {
obj.children[0].material.emissive.setHex(0x333333);
obj.children[0].material.emissiveIntensity = 0.2;
} else {
obj.children[0].material.emissive.setHex(0x000000);
obj.children[0].material.emissiveIntensity = 0;
}
});
}
moveStage(deltaX, deltaY) {
this.state.stagePosition.x += deltaX;
this.state.stagePosition.y += deltaY;
// Constrain movement
this.state.stagePosition.x = Math.max(-2, Math.min(2, this.state.stagePosition.x));
this.state.stagePosition.y = Math.max(-2, Math.min(2, this.state.stagePosition.y));
// Move the stage visually
if (this.stage) {
this.stage.position.x = this.state.stagePosition.x;
this.stage.position.z = this.state.stagePosition.y;
}
// Move slide and specimen with stage
if (this.slide) {
this.slide.position.x = this.state.stagePosition.x;
this.slide.position.z = this.state.stagePosition.y;
}
if (this.specimen) {
this.specimen.position.x = this.state.stagePosition.x;
this.specimen.position.z = this.state.stagePosition.y;
}
this.updateUI();
this.updateStatusBar(`Stage position: ${this.state.stagePosition.x.toFixed(1)}, ${this.state.stagePosition.y.toFixed(1)}`);
// Check if this completes step 2
if (this.experimentState.currentStep === 1) {
this.completeCurrentStep();
}
}
adjustFocus(amount, type) {
if (type === 'coarse') {
this.state.focus += amount * 5;
} else {
this.state.focus += amount;
}
this.state.focus = Math.max(-50, Math.min(50, this.state.focus));
this.updateUI();
this.updateSpecimenView();
this.updateStatusBar(`${type} focus: ${this.state.focus.toFixed(1)}`);
// Check if this completes step 3 or 4
if (type === 'coarse' && this.experimentState.currentStep === 2) {
this.completeCurrentStep();
} else if (type === 'fine' && this.experimentState.currentStep === 3) {
this.completeCurrentStep();
}
}
adjustBrightness(amount) {
this.state.brightness += amount;
this.state.brightness = Math.max(0, Math.min(100, this.state.brightness));
// Update illuminator intensity
if (this.illuminatorLight) {
this.illuminatorLight.intensity = this.state.brightness / 100;
}
this.updateUI();
this.updateStatusBar(`Brightness: ${this.state.brightness}%`);
}
toggleEyepiece() {
this.state.eyepieceActive = !this.state.eyepieceActive;
const eyepieceView = document.getElementById('eyepiece-view');
if (this.state.eyepieceActive) {
eyepieceView.classList.add('active');
this.updateStatusBar('Viewing through eyepiece - Use ESC to exit');
// Check if this completes step 6
if (this.experimentState.currentStep === 5) {
this.completeCurrentStep();
}
} else {
eyepieceView.classList.remove('active');
this.updateStatusBar('Eyepiece view closed');
}
}
rotateNosepiece() {
// Find current active objective
const currentIndex = this.objectives.findIndex(obj => obj.userData.active);
const nextIndex = (currentIndex + 1) % this.objectives.length;
const nextMagnification = this.objectives[nextIndex].userData.magnification;
// Rotate nosepiece visually
if (this.nosepiece) {
this.nosepiece.rotation.y += Math.PI / 2;
}
this.changeObjective(nextMagnification);
}
setCameraMode(mode) {
this.state.cameraMode = mode;
// Update button states
document.querySelectorAll('.camera-button').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById(`${mode}-btn`).classList.add('active');
this.updateStatusBar(`Camera mode: ${mode}`);
}
resetCamera() {
this.cameraTarget = new THREE.Vector3(0, 12, 0);
this.cameraDistance = 25;
this.cameraTheta = Math.PI / 6;
this.cameraPhi = Math.PI / 3;
this.updateCameraPosition();
this.updateStatusBar('Camera reset to default position');
}
updateUI() {
// Update control panel values
document.getElementById('current-mag').textContent = `${this.state.magnification}x`;
document.getElementById('current-focus').textContent = this.state.focus.toFixed(1);
document.getElementById('current-brightness').textContent = `${this.state.brightness}%`;
document.getElementById('stage-position').textContent =
`${this.state.stagePosition.x.toFixed(1)}, ${this.state.stagePosition.y.toFixed(1)}`;
}
updateStatusBar(message) {
const statusBar = document.getElementById('status-bar');
if (statusBar) {
statusBar.textContent = message;
}
}
updateTooltip(event) {
const tooltip = document.getElementById('tooltip');
if (!tooltip) return;
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects(this.microscopeGroup.children, true);
if (intersects.length > 0) {
const object = intersects[0].object;
let userData = object.userData;
// Check parent userData if object doesn't have it
if (!userData.type && object.parent) {
userData = object.parent.userData;
}
let tooltipText = '';
switch (userData.type) {
case 'objective':
tooltipText = `${userData.magnification}x Objective - Click to change magnification`;
break;
case 'eyepiece':
tooltipText = 'Eyepiece - Click to view specimen (Space key)';
break;
case 'stage':
tooltipText = 'Stage - Use arrow keys to move';
break;
case 'slide':
tooltipText = 'Specimen slide - Contains onion cells';
break;
case 'focusKnob':
tooltipText = `${userData.focusType} focus knob - Use +/- keys`;
break;
case 'stageKnob':
tooltipText = `Stage ${userData.axis}-axis control`;
break;
case 'nosepiece':
tooltipText = 'Nosepiece - Click to rotate objectives';
break;
case 'illuminator':
tooltipText = 'Light source - Illuminates the specimen';
break;
default:
tooltipText = '';
}
if (tooltipText) {
tooltip.textContent = tooltipText;
tooltip.style.left = `${event.clientX + 10}px`;
tooltip.style.top = `${event.clientY - 10}px`;
tooltip.classList.add('visible');
} else {
tooltip.classList.remove('visible');
}
} else {
tooltip.classList.remove('visible');
}
}
animate() {
requestAnimationFrame(this.animate.bind(this));
// Update performance stats
this.updatePerformanceStats();
// Render the scene
this.renderer.render(this.scene, this.camera);
}
}
// Global functions for UI interactions
function resetExperiment() {
if (window.microscope) {
window.microscope.experimentState.currentStep = 0;
window.microscope.updateProgressTracker();
window.microscope.updateStatusBar('Experiment reset - Start with step 1');
}
}
function toggleEyepiece() {
if (window.microscope) {
window.microscope.toggleEyepiece();
}
}
function autoFocus() {
if (window.microscope) {
window.microscope.state.focus = 0;
window.microscope.updateUI();
window.microscope.updateSpecimenView();
window.microscope.updateStatusBar('Auto focus complete');
}
}
function toggleCameraControls() {
const controls = document.getElementById('camera-controls');
controls.classList.toggle('minimized');
}
// Initialize the microscope when the page loads
window.addEventListener('load', () => {
window.microscope = new Realistic3DMicroscope();
});
</script>
</body>
</html>