Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>3D Oven Temperature Visualization</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
| <style> | |
| #temperature-display { | |
| background: rgba(0, 0, 0, 0.7); | |
| border-radius: 16px; | |
| padding: 20px; | |
| color: white; | |
| font-family: 'Arial', sans-serif; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); | |
| } | |
| .sensor-label { | |
| font-size: 12px; | |
| color: white; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 4px 8px; | |
| border-radius: 12px; | |
| pointer-events: none; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| #controls { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| z-index: 100; | |
| display: flex; | |
| gap: 12px; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 12px 20px; | |
| border-radius: 16px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); | |
| } | |
| #loading { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| color: white; | |
| font-size: 24px; | |
| z-index: 1000; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 20px 30px; | |
| border-radius: 16px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .temperature-bar { | |
| height: 24px; | |
| background: linear-gradient(to right, #0000ff, #00ffff, #00ff00, #ffff00, #ff0000); | |
| border-radius: 12px; | |
| margin-top: 12px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .temperature-marker { | |
| position: absolute; | |
| top: -24px; | |
| transform: translateX(-50%); | |
| color: white; | |
| font-size: 12px; | |
| text-shadow: 0 0 4px rgba(0, 0, 0, 0.8); | |
| } | |
| .temperature-indicator { | |
| position: absolute; | |
| top: 0; | |
| height: 100%; | |
| width: 2px; | |
| background: white; | |
| transform: translateX(-50%); | |
| box-shadow: 0 0 4px rgba(0, 0, 0, 0.8); | |
| } | |
| #sensor-info { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 16px; | |
| border-radius: 16px; | |
| color: white; | |
| max-width: 300px; | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| #sensor-info.visible { | |
| opacity: 1; | |
| } | |
| #time-display { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| font-size: 48px; | |
| color: white; | |
| text-shadow: 0 0 10px rgba(0, 0, 0, 0.8); | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| pointer-events: none; | |
| } | |
| #time-display.visible { | |
| opacity: 0.7; | |
| } | |
| .btn { | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .btn i { | |
| font-size: 16px; | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| height: 6px; | |
| background: rgba(255, 255, 255, 0.2); | |
| border-radius: 3px; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: #3b82f6; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| background: #2563eb; | |
| transform: scale(1.1); | |
| } | |
| .sensor-hotspot { | |
| position: absolute; | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| background: rgba(255, 0, 0, 0.5); | |
| pointer-events: none; | |
| transform: translate(-50%, -50%); | |
| z-index: 10; | |
| } | |
| .tooltip { | |
| position: absolute; | |
| background: rgba(0, 0, 0, 0.8); | |
| color: white; | |
| padding: 6px 12px; | |
| border-radius: 6px; | |
| font-size: 12px; | |
| pointer-events: none; | |
| z-index: 100; | |
| white-space: nowrap; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white overflow-hidden"> | |
| <div id="loading"> | |
| <div class="flex flex-col items-center"> | |
| <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div> | |
| <div>Loading 3D visualization...</div> | |
| </div> | |
| </div> | |
| <div id="temperature-display" class="fixed top-4 left-4 z-50"> | |
| <h2 class="text-xl font-bold mb-3 flex items-center gap-2"> | |
| <i class="fas fa-fire"></i> Oven Temperature Monitoring | |
| </h2> | |
| <div class="flex justify-between mb-2"> | |
| <span id="current-time" class="flex items-center gap-1"> | |
| <i class="fas fa-clock"></i> Time: 0.0s | |
| </span> | |
| <span id="oven-temp" class="flex items-center gap-1"> | |
| <i class="fas fa-thermometer-half"></i> Oven: 0.0°C | |
| </span> | |
| </div> | |
| <div class="temperature-bar"> | |
| <div class="temperature-marker" style="left: 0%">0°C</div> | |
| <div class="temperature-marker" style="left: 20%">20°C</div> | |
| <div class="temperature-marker" style="left: 40%">40°C</div> | |
| <div class="temperature-marker" style="left: 60%">60°C</div> | |
| <div class="temperature-marker" style="left: 80%">80°C</div> | |
| <div class="temperature-marker" style="left: 100%">100°C</div> | |
| <div id="current-temp-indicator" class="temperature-indicator" style="left: 0%"></div> | |
| </div> | |
| </div> | |
| <div id="sensor-info"> | |
| <h3 class="font-bold text-lg mb-2">Sensor Details</h3> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <div>Sensor ID:</div> | |
| <div id="sensor-id" class="font-mono">-</div> | |
| <div>Position:</div> | |
| <div id="sensor-pos" class="font-mono">-</div> | |
| <div>Temperature:</div> | |
| <div id="sensor-temp" class="font-mono">- °C</div> | |
| <div>Status:</div> | |
| <div id="sensor-status" class="font-mono">-</div> | |
| </div> | |
| </div> | |
| <div id="time-display">0.0s</div> | |
| <div id="controls"> | |
| <button id="play-btn" class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded"> | |
| <i class="fas fa-play"></i> Play | |
| </button> | |
| <button id="pause-btn" class="btn bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded"> | |
| <i class="fas fa-pause"></i> Pause | |
| </button> | |
| <button id="reset-btn" class="btn bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded"> | |
| <i class="fas fa-undo"></i> Reset | |
| </button> | |
| <button id="speed-down" class="btn bg-gray-600 hover:bg-gray-700 text-white px-3 py-2 rounded"> | |
| <i class="fas fa-backward"></i> | |
| </button> | |
| <input id="time-slider" type="range" min="0" max="100" value="0" class="w-64"> | |
| <button id="speed-up" class="btn bg-gray-600 hover:bg-gray-700 text-white px-3 py-2 rounded"> | |
| <i class="fas fa-forward"></i> | |
| </button> | |
| <div id="speed-display" class="text-white px-2 flex items-center">1x</div> | |
| </div> | |
| <script> | |
| // Enhanced sample data with more realistic temperature variations | |
| const sampleData = (() => { | |
| const timePoints = Array.from({length: 101}, (_, i) => i); // 0 to 100 seconds | |
| const ovenTemps = timePoints.map(t => { | |
| // Simulate oven heating curve with some variation | |
| if (t < 20) return 20 + t * 1.5; // Initial rapid heating | |
| if (t < 60) return 50 + (t - 20) * 0.8; // Slower heating | |
| if (t < 80) return 82 + (t - 60) * 0.4; // Approaching target | |
| return 90 + (t - 80) * 0.2; // Final stabilization | |
| }); | |
| const data = { | |
| "Time_(s)": timePoints, | |
| "Oven_Mon": ovenTemps | |
| }; | |
| // Generate sensor data with realistic spatial variations | |
| for (let i = 1; i <= 15; i++) { | |
| const sensorId = i.toString(); | |
| const pos = sensorPositions[sensorId]; | |
| // Base temperature follows oven but with position-based variations | |
| data[sensorId] = ovenTemps.map((temp, idx) => { | |
| // Position factors (0-1) affecting temperature | |
| const heightFactor = pos[2] / 14; // Z position (bottom to top) | |
| const cornerFactor = (pos[0] === 2 || pos[0] === 12 || pos[1] === 2 || pos[1] === 12) ? 0.9 : 1; | |
| // Time-based variation | |
| const timeVar = Math.sin(idx / 10) * 0.5; | |
| // Calculate final temperature with variations | |
| return temp * (0.95 + heightFactor * 0.1) * cornerFactor + timeVar + (Math.random() - 0.5); | |
| }); | |
| } | |
| return data; | |
| })(); | |
| // Sensor positions (x, y, z in inches) | |
| const sensorPositions = { | |
| '1': [2, 2, 2], // left, front, bottom | |
| '2': [2, 12, 2], // left, back, bottom | |
| '3': [12, 12, 2], // right, back, bottom | |
| '4': [12, 2, 2], // right, front, bottom | |
| '5': [7, 7, 2], // center, center, bottom | |
| '6': [2, 2, 7], // left, front, middle | |
| '7': [2, 12, 7], // left, back, middle | |
| '8': [12, 12, 7], // right, back, middle | |
| '9': [12, 2, 7], // right, front, middle | |
| '10': [7, 7, 7], // center, center, middle | |
| '11': [2, 2, 12], // left, front, top | |
| '12': [2, 12, 12], // left, back, top | |
| '13': [12, 12, 12],// right, back, top | |
| '14': [12, 2, 12], // right, front, top | |
| '15': [7, 7, 12] // center, center, top | |
| }; | |
| // Initialize Three.js scene | |
| let scene, camera, renderer, controls; | |
| let oven, sensors = [], sensorLabels = []; | |
| let currentFrame = 0; | |
| let isPlaying = false; | |
| let animationId = null; | |
| let playbackSpeed = 1; | |
| let lastFrameTime = 0; | |
| const totalFrames = sampleData["Time_(s)"].length; | |
| let raycaster = new THREE.Raycaster(); | |
| let mouse = new THREE.Vector2(); | |
| let hoveredSensor = null; | |
| let tooltip = null; | |
| function init() { | |
| // Create scene | |
| scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x111111); | |
| scene.fog = new THREE.FogExp2(0x111111, 0.002); | |
| // Create camera | |
| camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(25, 25, 25); | |
| // Create renderer with better quality settings | |
| renderer = new THREE.WebGLRenderer({ | |
| antialias: true, | |
| powerPreference: "high-performance" | |
| }); | |
| renderer.setPixelRatio(window.devicePixelRatio); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| document.body.appendChild(renderer.domElement); | |
| // Add orbit controls with improved UX | |
| controls = new THREE.OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.25; | |
| controls.screenSpacePanning = false; | |
| controls.maxPolarAngle = Math.PI; | |
| controls.minDistance = 10; | |
| controls.maxDistance = 50; | |
| // Add enhanced lighting | |
| const ambientLight = new THREE.AmbientLight(0x404040, 0.5); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(1, 1, 1); | |
| directionalLight.castShadow = true; | |
| directionalLight.shadow.mapSize.width = 1024; | |
| directionalLight.shadow.mapSize.height = 1024; | |
| scene.add(directionalLight); | |
| const hemisphereLight = new THREE.HemisphereLight(0xffffbb, 0x080820, 0.3); | |
| scene.add(hemisphereLight); | |
| // Create oven structure with more details | |
| createOven(); | |
| // Create sensors with better visuals | |
| createSensors(); | |
| // Create tooltip element | |
| createTooltip(); | |
| // Handle window resize | |
| window.addEventListener('resize', onWindowResize); | |
| // Setup event listeners for controls | |
| setupControls(); | |
| // Setup mouse interaction | |
| setupMouseInteraction(); | |
| // Hide loading message | |
| document.getElementById('loading').style.display = 'none'; | |
| // Start animation loop | |
| animate(); | |
| } | |
| function createOven() { | |
| // Oven dimensions (14x14x14 inches) | |
| const ovenSize = 14; | |
| const wallThickness = 0.2; | |
| // Create oven frame (wireframe) | |
| const geometry = new THREE.BoxGeometry(ovenSize, ovenSize, ovenSize); | |
| const edges = new THREE.EdgesGeometry(geometry); | |
| const line = new THREE.LineSegments( | |
| edges, | |
| new THREE.LineBasicMaterial({ | |
| color: 0xffffff, | |
| transparent: true, | |
| opacity: 0.7 | |
| }) | |
| ); | |
| scene.add(line); | |
| // Create transparent oven walls | |
| const wallMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x333333, | |
| transparent: true, | |
| opacity: 0.2, | |
| side: THREE.DoubleSide | |
| }); | |
| // Create walls (excluding front) | |
| const walls = new THREE.Group(); | |
| // Back wall | |
| const backWall = new THREE.Mesh( | |
| new THREE.PlaneGeometry(ovenSize, ovenSize), | |
| wallMaterial | |
| ); | |
| backWall.position.set(7, 7, 14); | |
| backWall.rotation.y = Math.PI; | |
| walls.add(backWall); | |
| // Left wall | |
| const leftWall = new THREE.Mesh( | |
| new THREE.PlaneGeometry(ovenSize, ovenSize), | |
| wallMaterial | |
| ); | |
| leftWall.position.set(0, 7, 7); | |
| leftWall.rotation.y = Math.PI / 2; | |
| walls.add(leftWall); | |
| // Right wall | |
| const rightWall = new THREE.Mesh( | |
| new THREE.PlaneGeometry(ovenSize, ovenSize), | |
| wallMaterial | |
| ); | |
| rightWall.position.set(14, 7, 7); | |
| rightWall.rotation.y = -Math.PI / 2; | |
| walls.add(rightWall); | |
| // Top wall | |
| const topWall = new THREE.Mesh( | |
| new THREE.PlaneGeometry(ovenSize, ovenSize), | |
| wallMaterial | |
| ); | |
| topWall.position.set(7, 14, 7); | |
| topWall.rotation.x = -Math.PI / 2; | |
| walls.add(topWall); | |
| // Bottom wall | |
| const bottomWall = new THREE.Mesh( | |
| new THREE.PlaneGeometry(ovenSize, ovenSize), | |
| wallMaterial | |
| ); | |
| bottomWall.position.set(7, 0, 7); | |
| bottomWall.rotation.x = Math.PI / 2; | |
| walls.add(bottomWall); | |
| scene.add(walls); | |
| // Create door on front face | |
| const doorGeometry = new THREE.PlaneGeometry(12, 12); | |
| const doorMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0x555555, | |
| transparent: true, | |
| opacity: 0.3, | |
| side: THREE.DoubleSide, | |
| specular: 0x111111, | |
| shininess: 30 | |
| }); | |
| const door = new THREE.Mesh(doorGeometry, doorMaterial); | |
| door.position.set(7, 7, 0); | |
| door.rotation.y = Math.PI / 2; | |
| scene.add(door); | |
| // Add door frame | |
| const doorFrameGeometry = new THREE.BoxGeometry(12.2, 12.2, 0.5); | |
| const doorFrameEdges = new THREE.EdgesGeometry(doorFrameGeometry); | |
| const doorFrame = new THREE.LineSegments( | |
| doorFrameEdges, | |
| new THREE.LineBasicMaterial({ color: 0xaaaaaa }) | |
| ); | |
| doorFrame.position.set(7, 7, 0); | |
| doorFrame.rotation.y = Math.PI / 2; | |
| scene.add(doorFrame); | |
| // Add door handle | |
| const handleGeometry = new THREE.CylinderGeometry(0.3, 0.3, 2, 32); | |
| const handleMaterial = new THREE.MeshPhongMaterial({ | |
| color: 0xcccccc, | |
| specular: 0x111111, | |
| shininess: 30 | |
| }); | |
| const handle = new THREE.Mesh(handleGeometry, handleMaterial); | |
| handle.position.set(12, 7, 0); | |
| handle.rotation.z = Math.PI / 2; | |
| scene.add(handle); | |
| // Add "FRONT" label | |
| const frontLabel = createTextLabel("OVEN DOOR", 7, 7, -1, 0xffffff); | |
| scene.add(frontLabel); | |
| // Add grid helper to show floor | |
| const gridHelper = new THREE.GridHelper(20, 20, 0x555555, 0x333333); | |
| gridHelper.position.set(7, 0, 7); | |
| scene.add(gridHelper); | |
| } | |
| function createSensors() { | |
| // Create sensor spheres and labels | |
| for (let i = 1; i <= 15; i++) { | |
| const sensorId = i.toString(); | |
| const pos = sensorPositions[sensorId]; | |
| // Create sensor sphere with better material | |
| const geometry = new THREE.SphereGeometry(0.5, 24, 24); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: 0x00ff00, | |
| specular: 0x111111, | |
| shininess: 30, | |
| emissive: 0x000000, | |
| emissiveIntensity: 0 | |
| }); | |
| const sphere = new THREE.Mesh(geometry, material); | |
| sphere.position.set(pos[0], pos[1], pos[2]); | |
| sphere.castShadow = true; | |
| sphere.receiveShadow = true; | |
| sphere.userData.sensorId = sensorId; | |
| scene.add(sphere); | |
| sensors.push(sphere); | |
| // Create sensor label with better styling | |
| const label = createTextLabel(`S-${sensorId}`, pos[0], pos[1] + 1.2, pos[2], 0xffffff); | |
| scene.add(label); | |
| sensorLabels.push(label); | |
| } | |
| } | |
| function createTextLabel(text, x, y, z, color) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 256; | |
| canvas.height = 128; | |
| const context = canvas.getContext('2d'); | |
| // Draw rounded rectangle background | |
| context.beginPath(); | |
| context.roundRect(0, 0, canvas.width, canvas.height, 16); | |
| context.fillStyle = 'rgba(0, 0, 0, 0.7)'; | |
| context.fill(); | |
| // Draw border | |
| context.strokeStyle = 'rgba(255, 255, 255, 0.2)'; | |
| context.lineWidth = 2; | |
| context.stroke(); | |
| // Draw text | |
| context.font = 'Bold 24px Arial'; | |
| context.fillStyle = '#ffffff'; | |
| context.textAlign = 'center'; | |
| context.textBaseline = 'middle'; | |
| context.fillText(text, canvas.width / 2, canvas.height / 2); | |
| const texture = new THREE.CanvasTexture(canvas); | |
| const material = new THREE.SpriteMaterial({ | |
| map: texture, | |
| transparent: true | |
| }); | |
| const sprite = new THREE.Sprite(material); | |
| sprite.position.set(x, y, z); | |
| sprite.scale.set(5, 2.5, 1); | |
| sprite.userData.isLabel = true; | |
| return sprite; | |
| } | |
| function createTooltip() { | |
| tooltip = document.createElement('div'); | |
| tooltip.className = 'tooltip'; | |
| tooltip.style.display = 'none'; | |
| document.body.appendChild(tooltip); | |
| } | |
| function updateTemperatureColors(frame) { | |
| const ovenTemp = sampleData["Oven_Mon"][frame]; | |
| const normalizedOvenTemp = Math.min(Math.max((ovenTemp - 20) / 80, 0), 1); | |
| // Update temperature indicator position | |
| document.getElementById('current-temp-indicator').style.left = `${normalizedOvenTemp * 100}%`; | |
| // Update all sensors with temperature data for the current frame | |
| let maxTemp = 20; | |
| let minTemp = 20; | |
| for (let i = 1; i <= 15; i++) { | |
| const sensorId = i.toString(); | |
| const temp = sampleData[sensorId][frame]; | |
| // Update min/max for status calculation | |
| if (temp > maxTemp) maxTemp = temp; | |
| if (temp < minTemp) minTemp = temp; | |
| // Calculate color based on temperature (blue to red gradient) | |
| const normalizedTemp = Math.min(Math.max((temp - 20) / 80, 0), 1); | |
| const color = new THREE.Color(); | |
| color.setHSL((1 - normalizedTemp) * 0.7, 1, 0.5); | |
| // Update sensor color and emissive (glow effect for hot sensors) | |
| sensors[i-1].material.color = color; | |
| sensors[i-1].material.emissiveIntensity = normalizedTemp * 0.5; | |
| // Update sensor label position to always face camera | |
| const pos = sensorPositions[sensorId]; | |
| sensorLabels[i-1].position.set(pos[0], pos[1] + 1.2, pos[2]); | |
| sensorLabels[i-1].lookAt(camera.position); | |
| } | |
| // Update time and oven temperature display | |
| document.getElementById('current-time').textContent = `Time: ${sampleData["Time_(s)"][frame].toFixed(1)}s`; | |
| document.getElementById('oven-temp').textContent = `Oven: ${ovenTemp.toFixed(1)}°C`; | |
| // Briefly show large time display | |
| showTimeDisplay(sampleData["Time_(s)"][frame].toFixed(1) + 's'); | |
| // Update slider position | |
| document.getElementById('time-slider').value = (frame / (totalFrames - 1)) * 100; | |
| // Update hovered sensor info if any | |
| if (hoveredSensor) { | |
| updateSensorInfo(hoveredSensor.userData.sensorId, frame); | |
| } | |
| } | |
| function showTimeDisplay(text) { | |
| const display = document.getElementById('time-display'); | |
| display.textContent = text; | |
| display.classList.add('visible'); | |
| setTimeout(() => { | |
| display.classList.remove('visible'); | |
| }, 1000); | |
| } | |
| function updateSensorInfo(sensorId, frame) { | |
| const temp = sampleData[sensorId][frame]; | |
| const pos = sensorPositions[sensorId]; | |
| document.getElementById('sensor-id').textContent = sensorId; | |
| document.getElementById('sensor-pos').textContent = `${pos[0]}, ${pos[1]}, ${pos[2]}`; | |
| document.getElementById('sensor-temp').textContent = `${temp.toFixed(1)}°C`; | |
| // Determine status based on temperature | |
| let status = ''; | |
| let statusClass = ''; | |
| if (temp < 30) { | |
| status = 'Cool'; | |
| statusClass = 'text-blue-400'; | |
| } else if (temp < 60) { | |
| status = 'Warm'; | |
| statusClass = 'text-green-400'; | |
| } else if (temp < 80) { | |
| status = 'Hot'; | |
| statusClass = 'text-yellow-400'; | |
| } else { | |
| status = 'Very Hot'; | |
| statusClass = 'text-red-400'; | |
| } | |
| const statusElement = document.getElementById('sensor-status'); | |
| statusElement.textContent = status; | |
| statusElement.className = `font-mono ${statusClass}`; | |
| // Show sensor info panel | |
| document.getElementById('sensor-info').classList.add('visible'); | |
| } | |
| function setupControls() { | |
| // Play button | |
| document.getElementById('play-btn').addEventListener('click', () => { | |
| if (!isPlaying) { | |
| isPlaying = true; | |
| lastFrameTime = performance.now(); | |
| playAnimation(); | |
| } | |
| }); | |
| // Pause button | |
| document.getElementById('pause-btn').addEventListener('click', () => { | |
| isPlaying = false; | |
| if (animationId) { | |
| cancelAnimationFrame(animationId); | |
| animationId = null; | |
| } | |
| }); | |
| // Reset button | |
| document.getElementById('reset-btn').addEventListener('click', () => { | |
| isPlaying = false; | |
| if (animationId) { | |
| cancelAnimationFrame(animationId); | |
| animationId = null; | |
| } | |
| currentFrame = 0; | |
| updateTemperatureColors(currentFrame); | |
| }); | |
| // Speed down button | |
| document.getElementById('speed-down').addEventListener('click', () => { | |
| playbackSpeed = Math.max(0.25, playbackSpeed / 2); | |
| updateSpeedDisplay(); | |
| }); | |
| // Speed up button | |
| document.getElementById('speed-up').addEventListener('click', () => { | |
| playbackSpeed = Math.min(4, playbackSpeed * 2); | |
| updateSpeedDisplay(); | |
| }); | |
| // Time slider | |
| document.getElementById('time-slider').addEventListener('input', (e) => { | |
| const frame = Math.round((e.target.value / 100) * (totalFrames - 1)); | |
| if (frame !== currentFrame) { | |
| currentFrame = frame; | |
| updateTemperatureColors(currentFrame); | |
| } | |
| }); | |
| } | |
| function updateSpeedDisplay() { | |
| document.getElementById('speed-display').textContent = `${playbackSpeed}x`; | |
| } | |
| function setupMouseInteraction() { | |
| // Mouse move event for hover effects | |
| window.addEventListener('mousemove', (event) => { | |
| // Calculate mouse position in normalized device coordinates | |
| mouse.x = (event.clientX / window.innerWidth) * 2 - 1; | |
| mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; | |
| // Update the raycaster | |
| raycaster.setFromCamera(mouse, camera); | |
| // Calculate objects intersecting the raycaster | |
| const intersects = raycaster.intersectObjects(sensors); | |
| if (intersects.length > 0) { | |
| const object = intersects[0].object; | |
| // If we're hovering over a new sensor | |
| if (!hoveredSensor || hoveredSensor.userData.sensorId !== object.userData.sensorId) { | |
| hoveredSensor = object; | |
| // Update tooltip position and content | |
| const sensorId = object.userData.sensorId; | |
| const temp = sampleData[sensorId][currentFrame]; | |
| const pos = sensorPositions[sensorId]; | |
| // Convert 3D position to screen coordinates | |
| const vector = new THREE.Vector3(pos[0], pos[1], pos[2]); | |
| vector.project(camera); | |
| const x = (vector.x * 0.5 + 0.5) * window.innerWidth; | |
| const y = (vector.y * -0.5 + 0.5) * window.innerHeight; | |
| tooltip.style.display = 'block'; | |
| tooltip.style.left = `${x}px`; | |
| tooltip.style.top = `${y}px`; | |
| tooltip.textContent = `Sensor ${sensorId}: ${temp.toFixed(1)}°C`; | |
| // Update sensor info panel | |
| updateSensorInfo(sensorId, currentFrame); | |
| } | |
| } else { | |
| // No intersection, hide tooltip and sensor info | |
| if (hoveredSensor) { | |
| tooltip.style.display = 'none'; | |
| document.getElementById('sensor-info').classList.remove('visible'); | |
| hoveredSensor = null; | |
| } | |
| } | |
| }); | |
| // Click event for selecting sensors | |
| window.addEventListener('click', () => { | |
| if (hoveredSensor) { | |
| // You could add additional click behavior here | |
| } | |
| }); | |
| } | |
| function playAnimation() { | |
| const now = performance.now(); | |
| const delta = now - lastFrameTime; | |
| lastFrameTime = now; | |
| // Advance frames based on playback speed and time elapsed | |
| const framesToAdvance = (delta / 1000) * playbackSpeed * 5; // 5 = base speed factor | |
| if (currentFrame >= totalFrames - 1) { | |
| currentFrame = 0; | |
| } else { | |
| currentFrame = Math.min(totalFrames - 1, currentFrame + framesToAdvance); | |
| } | |
| updateTemperatureColors(Math.floor(currentFrame)); | |
| if (isPlaying && currentFrame < totalFrames - 1) { | |
| animationId = requestAnimationFrame(playAnimation); | |
| } else if (currentFrame >= totalFrames - 1) { | |
| isPlaying = false; | |
| } | |
| } | |
| function onWindowResize() { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| } | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| } | |
| // Start the application | |
| init(); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Freefall/space-oven-sim" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |