space-oven-sim / index.html
Freefall's picture
Add 3 files
e6bf914 verified
<!DOCTYPE html>
<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>