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/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/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> |