<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Globe (Revised)</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- UI remains largely the same -->
<div id="uiContainer">
<div class="control-box search-box">
<input type="text" id="searchInput" placeholder="Search for a place...">
<button id="searchButton">Search</button>
<p id="status">Initializing...</p> <!-- Start with Initializing -->
</div>
<div class="control-box layer-toggles">
<h4>Layers:</h4>
<label><input type="checkbox" id="toggleClouds" checked> Clouds</label><br>
<label><input type="checkbox" id="toggleAtmosphere" checked> Atmosphere</label><br>
<label><input type="checkbox" id="toggleStars" checked> Stars</label><br>
<label><input type="checkbox" id="toggleMarkers" checked> Markers</label><br>
<label><input type="checkbox" id="toggleAutorotate"> Auto-Rotate</label>
</div>
<div id="markerInfoBox" class="control-box marker-info" style="display: none;">
<h4>Marker Info</h4>
<p id="markerName">Name: -</p>
<p id="markerCoords">Coords: -</p>
<button id="closeMarkerInfo">Close</button>
<button id="removeMarker">Remove</button>
</div>
</div>
<div id="globeContainer"></div>
<!-- Libraries (Ensure these are accessible) -->
<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.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
<!-- Post-processing scripts removed for now -->
<script src="script.js"></script>
</body>
</html>
/* Use the same style.css as provided in the previous "Advanced" example */
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #000;
color: #fff;
font-size: 14px;
}
#globeContainer {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 1;
cursor: grab;
}
#globeContainer:active {
cursor: grabbing;
}
#uiContainer {
position: absolute;
top: 0;
left: 0;
padding: 10px;
z-index: 2;
display: flex;
flex-direction: column;
gap: 10px;
}
.control-box {
background-color: rgba(0, 0, 0, 0.65);
padding: 12px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
max-width: 250px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}
.search-box input {
padding: 8px;
margin-right: 5px;
border: 1px solid #555;
background-color: #333;
color: #fff;
border-radius: 4px;
width: calc(100% - 80px); /* Adjust based on button width */
}
.search-box button {
padding: 8px 12px;
cursor: pointer;
background-color: #007bff;
border: none;
color: white;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.search-box button:hover {
background-color: #0056b3;
}
#status {
margin-top: 8px;
font-size: 0.9em;
min-height: 1.2em;
color: #ccc;
}
.layer-toggles h4, .marker-info h4 {
margin-top: 0;
margin-bottom: 8px;
border-bottom: 1px solid #555;
padding-bottom: 5px;
}
.layer-toggles label {
display: inline-block; /* Changed from block */
margin-bottom: 5px;
margin-right: 10px; /* Added spacing */
cursor: pointer;
}
.layer-toggles input[type="checkbox"] {
margin-right: 5px;
vertical-align: middle;
}
.marker-info p {
margin: 5px 0;
word-wrap: break-word;
}
.marker-info button {
margin-top: 10px;
padding: 5px 10px;
cursor: pointer;
margin-right: 5px;
border-radius: 4px;
border: none;
}
#closeMarkerInfo {
background-color: #6c757d;
color: white;
}
#removeMarker {
background-color: #dc3545;
color: white;
}
#closeMarkerInfo:hover { background-color: #5a6268; }
#removeMarker:hover { background-color: #c82333; }
/* Simple loading indicator */
#status:contains("Loading")::after,
#status:contains("Searching")::after,
#status:contains("Initializing")::after {
content: '...';
display: inline-block;
animation: ellipsis 1.2s infinite;
width: 1.5em; /* Reserve space */
text-align: left;
}
@keyframes ellipsis {
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
}
// --- Strict mode and Library Check ---
'use strict';
if (typeof THREE === 'undefined' || typeof TWEEN === 'undefined' || typeof THREE.OrbitControls === 'undefined') {
console.error("FATAL: Required libraries (Three.js, Tween.js, OrbitControls) not loaded!");
alert("Error: Could not load 3D libraries. Please check console (F12) and ensure all script tags in HTML are correct and accessible.");
document.getElementById('status').innerText = "Error: Failed to load 3D components.";
throw new Error("Missing critical libraries");
}
console.log("Libraries loaded successfully.");
// --- Constants ---
const GLOBE_RADIUS = 5;
const CAMERA_START_DISTANCE = 15;
const CLOUD_ALTITUDE = 0.03;
const ATMOSPHERE_ALTITUDE = 0.4;
const MARKER_ALTITUDE = 0.05;
const MARKER_HEIGHT = 0.2;
const MARKER_RADIAL_SEGMENTS = 16;
const MARKER_COLOR = 0xff4400; // Bright orange-red
const STAR_RADIUS = 90;
const ROTATION_SPEED = 0.0003;
const CLOUD_ROTATION_SPEED = 0.0004;
const PULSE_SPEED = 1.5;
// --- DOM Elements ---
const globeContainer = document.getElementById('globeContainer');
const searchInput = document.getElementById('searchInput');
const searchButton = document.getElementById('searchButton');
const statusElement = document.getElementById('status');
const toggleClouds = document.getElementById('toggleClouds');
const toggleAtmosphere = document.getElementById('toggleAtmosphere');
const toggleStars = document.getElementById('toggleStars'); // Added
const toggleMarkers = document.getElementById('toggleMarkers');
const toggleAutorotate = document.getElementById('toggleAutorotate');
const markerInfoBox = document.getElementById('markerInfoBox');
const markerNameElement = document.getElementById('markerName');
const markerCoordsElement = document.getElementById('markerCoords');
const closeMarkerInfoButton = document.getElementById('closeMarkerInfo');
const removeMarkerButton = document.getElementById('removeMarker');
// --- Three.js Variables ---
let scene, camera, renderer, controls;
let globe, clouds, atmosphere, starField;
let pointLight, ambientLight;
let markersGroup;
let activeMarker = null;
let isAutoRotating = false;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let animationFrameId = null; // To manage animation loop
// --- Shaders (Atmosphere - Kept the definition, but ensure it compiles) ---
const vertexShader = `
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vNormal = normalize( normalMatrix * normal );
vPosition = (modelViewMatrix * vec4( position, 1.0 )).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`;
const fragmentShader = `
uniform vec3 uColor;
uniform float uOpacity;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vec3 viewDirection = normalize( -vPosition );
float intensity = pow( 1.0 - dot( vNormal, viewDirection ), 2.5 );
gl_FragColor = vec4( uColor, intensity * uOpacity );
}`;
// --- Initialization ---
function init() {
console.log("init() called");
statusElement.innerText = "Initializing Scene...";
// Scene
scene = new THREE.Scene();
console.log("Scene created");
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = CAMERA_START_DISTANCE;
console.log("Camera created");
// Renderer
try {
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); // alpha: false might be slightly faster if no transparency needed behind canvas
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x000000, 1); // Explicitly black
globeContainer.appendChild(renderer.domElement);
console.log("Renderer created and attached to DOM");
} catch (error) {
console.error("FATAL: Failed to create WebGL Renderer:", error);
statusElement.innerText = "Error: WebGL not supported or enabled.";
alert("Error: Could not initialize WebGL. Your browser might not support it, or it might be disabled.");
return; // Stop initialization if renderer fails
}
// Controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = GLOBE_RADIUS + 0.5; // Keep slightly further away
controls.maxDistance = 50;
controls.autoRotate = false;
console.log("OrbitControls created");
// Lighting
ambientLight = new THREE.AmbientLight(0x777777); // Slightly brighter ambient
scene.add(ambientLight);
pointLight = new THREE.PointLight(0xffffff, 1.0, 400);
pointLight.position.set(50, 30, 50); // Adjust light position
scene.add(pointLight);
console.log("Lighting added");
// Starfield (Create first, add later if texture loads)
createStarfield();
// Markers Group
markersGroup = new THREE.Group();
scene.add(markersGroup);
console.log("Markers group added");
// --- Central Globe Creation ---
createGlobeAndAssets(); // Start loading process
// Event Listeners
setupEventListeners();
console.log("Event listeners setup");
// Start animation loop ONLY after globe is potentially ready
// The call is moved inside createGlobeAndAssets's callbacks
}
// --- Asset Creation ---
function createGlobeAndAssets() {
statusElement.innerText = "Loading Earth texture...";
const textureLoader = new THREE.TextureLoader();
const earthTextureUrl = 'https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73909/world.topo.bathy.200412.3x5400x2700.jpg';
const cloudTextureUrl = 'https://raw.githubusercontent.com/turban/webgl-earth/master/images/fair_clouds_4k.png';
let globeMeshCreated = false; // Flag to ensure animation starts only once
// 1. Globe Geometry (defined once)
const globeGeometry = new THREE.SphereGeometry(GLOBE_RADIUS, 64, 64);
console.log("Globe geometry created");
// 2. Globe Material and Mesh (async loading)
textureLoader.load(
earthTextureUrl,
(texture) => { // onLoad
console.log("Earth texture loaded successfully!");
statusElement.innerText = "Creating Globe...";
const globeMaterial = new THREE.MeshStandardMaterial({
map: texture,
metalness: 0.1,
roughness: 0.8,
name: "GlobeMaterial" // For debugging
});
globe = new THREE.Mesh(globeGeometry, globeMaterial);
globe.name = "Globe";
scene.add(globe);
console.log("Globe mesh with texture added to scene.");
globeMeshCreated = true;
loadOptionalAssets(textureLoader, cloudTextureUrl); // Load clouds etc. *after* globe
startAnimationLoop(); // Start animation now
},
(xhr) => { // onProgress
const percentComplete = (xhr.loaded / xhr.total) * 100;
statusElement.innerText = `Loading Earth: ${Math.round(percentComplete)}%`;
},
(error) => { // onError
console.error('FATAL: Error loading Earth texture:', error);
statusElement.innerText = "Error loading Earth texture. Using fallback.";
const fallbackMaterial = new THREE.MeshStandardMaterial({
color: 0x2266ff, // Bright blue fallback
metalness: 0.1,
roughness: 0.8,
name: "FallbackMaterial"
});
globe = new THREE.Mesh(globeGeometry, fallbackMaterial);
globe.name = "Globe (Fallback)";
scene.add(globe);
console.warn("Globe mesh with FALLBACK material added to scene.");
globeMeshCreated = true;
loadOptionalAssets(textureLoader, cloudTextureUrl); // Still load other assets
startAnimationLoop(); // Start animation now
}
);
// Create Atmosphere (add it regardless of texture loading)
createAtmosphere();
}
function loadOptionalAssets(textureLoader, cloudTextureUrl) {
console.log("Loading optional assets (Clouds, Stars texture)...");
// Add Starfield to scene (geometry was created earlier)
if (starField) {
textureLoader.load(
//'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/starfield.jpg',
'https://star-map.herokuapp.com/map', // Alternative star map source
(texture) => {
console.log("Starfield texture loaded.");
starField.material.map = texture;
starField.material.needsUpdate = true;
scene.add(starField); // Add only after texture loaded
toggleStars.checked = true; // Ensure checkbox matches
starField.visible = true;
},
undefined,
(error) => {
console.error("Error loading starfield texture:", error);
toggleStars.checked = false; // Uncheck if loading fails
toggleStars.disabled = true;
if(starField) scene.remove(starField); // Remove mesh if texture fails
starField = null;
}
);
}
// Clouds (only if globe exists)
if (!globe) return; // Safety check
statusElement.innerText = "Loading cloud texture...";
const cloudGeometry = new THREE.SphereGeometry(GLOBE_RADIUS + CLOUD_ALTITUDE, 64, 64);
textureLoader.load(cloudTextureUrl,
(texture) => { // onLoad
console.log("Cloud texture loaded.");
statusElement.innerText = "Creating clouds...";
const cloudMaterial = new THREE.MeshStandardMaterial({
map: texture,
alphaMap: texture,
transparent: true,
opacity: 0.7,
depthWrite: false,
});
clouds = new THREE.Mesh(cloudGeometry, cloudMaterial);
clouds.name = "Clouds";
scene.add(clouds);
console.log("Clouds mesh added to scene.");
toggleClouds.checked = true; // Ensure checkbox matches
clouds.visible = true;
statusElement.innerText = "Globe loaded."; // Update status finally
},
undefined, // onProgress (optional)
(error) => { // onError
console.error('Error loading Cloud texture:', error);
statusElement.innerText = "Warning: Could not load clouds.";
toggleClouds.checked = false; // Uncheck if loading fails
toggleClouds.disabled = true;
}
);
}
function createStarfield() {
// Create geometry/material but don't load texture/add to scene yet
try {
const starGeometry = new THREE.SphereGeometry(STAR_RADIUS, 64, 64);
const starMaterial = new THREE.MeshBasicMaterial({
// Map will be loaded in loadOptionalAssets
side: THREE.BackSide,
depthWrite: false,
color: 0xaaaaaa, // Placeholder color until texture loads
name: "StarMaterial"
});
starField = new THREE.Mesh(starGeometry, starMaterial);
starField.name = "StarField";
starField.visible = false; // Initially hidden
console.log("Starfield geometry/material created.");
} catch(error) {
console.error("Error creating starfield geometry/material:", error);
starField = null;
}
}
function createAtmosphere() {
// Atmosphere (can be added immediately as it doesn't rely on textures)
try {
const atmosphereGeometry = new THREE.SphereGeometry(GLOBE_RADIUS + ATMOSPHERE_ALTITUDE, 64, 64);
const atmosphereMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: {
uColor: { value: new THREE.Color(0x87ceeb) },
uOpacity: { value: 0.6 }
},
transparent: true,
blending: THREE.AdditiveBlending,
side: THREE.BackSide,
depthWrite: false,
name: "AtmosphereMaterial"
});
atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
atmosphere.name = "Atmosphere";
scene.add(atmosphere);
console.log("Atmosphere mesh added to scene.");
} catch (error) {
console.error("Error creating atmosphere shader/mesh:", error);
statusElement.innerText = "Warning: Atmosphere effect failed.";
toggleAtmosphere.checked = false;
toggleAtmosphere.disabled = true;
}
}
// --- Marker Functions (Mostly unchanged) ---
function latLonToVector3(lat, lon, radius = GLOBE_RADIUS) {
const phi = (90 - lat) * (Math.PI / 180);
const theta = (lon + 180) * (Math.PI / 180);
const x = -(radius * Math.sin(phi) * Math.cos(theta));
const y = radius * Math.cos(phi);
const z = radius * Math.sin(phi) * Math.sin(theta);
return new THREE.Vector3(x, y, z);
}
function vector3ToLatLon(vector) {
const normalizedVector = vector.clone().normalize();
const lat = 90 - (Math.acos(normalizedVector.y) * 180 / Math.PI);
let lon = ((Math.atan2(normalizedVector.z, -normalizedVector.x) * 180 / Math.PI)) - 180;
if (lon < -180) lon += 360;
if (lon > 180) lon -= 360;
return { lat, lon };
}
function addMarker(lat, lon, name = "Selected Location") {
if (!markersGroup) return null; // Safety check
const markerPosition = latLonToVector3(lat, lon, GLOBE_RADIUS + MARKER_ALTITUDE);
const markerBasePosition = latLonToVector3(lat, lon, GLOBE_RADIUS);
const markerGeometry = new THREE.ConeGeometry(0.05, MARKER_HEIGHT, MARKER_RADIAL_SEGMENTS);
markerGeometry.translate(0, MARKER_HEIGHT / 2, 0);
const markerMaterial = new THREE.MeshBasicMaterial({ color: MARKER_COLOR });
const markerMesh = new THREE.Mesh(markerGeometry, markerMaterial);
markerMesh.position.copy(markerPosition);
markerMesh.lookAt(markerBasePosition.multiplyScalar(1.1)); // Point outwards
markerMesh.rotateX(Math.PI / 2);
markerMesh.userData = {
id: THREE.MathUtils.generateUUID(),
name: name, lat: lat, lon: lon, isMarker: true,
baseScale: 1.0, pulseTime: Math.random() * PULSE_SPEED
};
markersGroup.add(markerMesh);
console.log(`Marker added: ${name} (${lat.toFixed(4)}, ${lon.toFixed(4)})`);
return markerMesh;
}
function showMarkerInfo(marker) {
if (!marker || !marker.userData) return;
activeMarker = marker;
markerNameElement.textContent = `Name: ${marker.userData.name}`;
markerCoordsElement.textContent = `Coords: ${marker.userData.lat.toFixed(4)}, ${marker.userData.lon.toFixed(4)}`;
markerInfoBox.style.display = 'block';
}
function hideMarkerInfo() {
activeMarker = null;
markerInfoBox.style.display = 'none';
}
function removeActiveMarker() {
if (activeMarker && markersGroup) {
const markerToRemove = activeMarker; // Keep reference
hideMarkerInfo(); // Hide box first, setting activeMarker to null
markersGroup.remove(markerToRemove);
// Optional: Dispose geometry/material if they are unique and not reused
// markerToRemove.geometry.dispose();
// markerToRemove.material.dispose();
console.log(`Marker removed: ${markerToRemove.userData.name}`);
}
}
// --- Interaction & Geocoding (Mostly unchanged, added User-Agent reminder) ---
function onGlobeClick(event) {
if (!globe || !raycaster || !camera || !markersGroup) return; // Safety checks
// Use offsetX/offsetY for position relative to the container
const rect = globeContainer.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 1. Check markers
const markerIntersects = raycaster.intersectObjects(markersGroup.children);
if (markerIntersects.length > 0 && markerIntersects[0].object.userData.isMarker) {
const clickedMarker = markerIntersects[0].object;
showMarkerInfo(clickedMarker);
animateCameraTo(clickedMarker.userData.lat, clickedMarker.userData.lon);
return;
}
// 2. Check globe
const globeIntersects = raycaster.intersectObject(globe); // Use the main globe mesh variable
if (globeIntersects.length > 0) {
hideMarkerInfo();
const intersectPoint = globeIntersects[0].point;
const coords = vector3ToLatLon(intersectPoint);
addMarker(coords.lat, coords.lon, `Lat: ${coords.lat.toFixed(2)}, Lon: ${coords.lon.toFixed(2)}`);
// Optional: reverseGeocode(coords.lat, coords.lon, newMarker); // Call if needed
} else {
hideMarkerInfo(); // Clicked outside everything
}
}
function animateCameraTo(lat, lon, duration = 1500) {
if (!controls || !camera) return;
const targetSurfacePos = latLonToVector3(lat, lon, GLOBE_RADIUS);
const targetCamDistance = Math.max(controls.minDistance, camera.position.length());
const targetCameraPos = latLonToVector3(lat, lon, GLOBE_RADIUS + targetCamDistance - GLOBE_RADIUS );
TWEEN.removeAll(); // Stop existing tweens
new TWEEN.Tween(camera.position)
.to({ x: targetCameraPos.x, y: targetCameraPos.y, z: targetCameraPos.z }, duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.start();
new TWEEN.Tween(controls.target)
.to({ x: targetSurfacePos.x, y: targetSurfacePos.y, z: targetSurfacePos.z }, duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onComplete(() => controls.update()) // Ensure controls know the new target
.start();
}
async function searchPlace(query) {
if (!query) return;
statusElement.innerText = `Searching for "${query}"...`;
hideMarkerInfo();
// ***** IMPORTANT: REPLACE with YOUR User-Agent! *****
const userAgent = 'InteractiveGlobeDemo/1.2 (your-contact-email@example.com)'; // CHANGE THIS
// *****************************************************
if (userAgent.includes('your-contact-email@example.com')) {
console.warn("Please update the User-Agent string in script.js for Nominatim API calls.");
statusElement.innerText = "Search disabled: Update User-Agent in code.";
alert("Developer: Please set a unique User-Agent in script.js before using the search feature (required by Nominatim policy).");
return;
}
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=1&addressdetails=1`;
try {
const response = await fetch(url, { headers: { 'User-Agent': userAgent } });
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
const data = await response.json();
if (data && data.length > 0) {
const place = data[0];
const lat = parseFloat(place.lat);
const lon = parseFloat(place.lon);
const displayName = place.display_name;
statusElement.innerText = `Found: ${displayName.substring(0, 50)}...`;
const markerName = place.address?.name || place.address?.city || place.address?.state || query;
const newMarker = addMarker(lat, lon, markerName);
animateCameraTo(lat, lon);
if (newMarker) showMarkerInfo(newMarker);
} else {
statusElement.innerText = `Could not find "${query}".`;
}
} catch (error) {
statusElement.innerText = `Search failed. See console.`;
console.error("Error during geocoding search:", error);
}
}
// --- Event Listener Setup ---
function setupEventListeners() {
window.addEventListener('resize', onWindowResize, false);
// Click listener is sensitive, use mouseup to avoid firing after drag
let isDragging = false;
globeContainer.addEventListener('mousedown', () => { isDragging = false; }, false);
globeContainer.addEventListener('mousemove', () => { isDragging = true; }, false);
globeContainer.addEventListener('mouseup', (event) => {
if (!isDragging) {
onGlobeClick(event);
}
isDragging = false;
}, false);
searchButton.addEventListener('click', () => searchPlace(searchInput.value));
searchInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') searchPlace(searchInput.value);
});
// Layer Toggles
toggleClouds.addEventListener('change', () => { if (clouds) clouds.visible = toggleClouds.checked; });
toggleAtmosphere.addEventListener('change', () => { if (atmosphere) atmosphere.visible = toggleAtmosphere.checked; });
toggleStars.addEventListener('change', () => { if (starField) starField.visible = toggleStars.checked; });
toggleMarkers.addEventListener('change', () => { if (markersGroup) markersGroup.visible = toggleMarkers.checked; });
toggleAutorotate.addEventListener('change', () => {
isAutoRotating = toggleAutorotate.checked;
controls.autoRotate = isAutoRotating;
});
// Marker Info Box Buttons
closeMarkerInfoButton.addEventListener('click', hideMarkerInfo);
removeMarkerButton.addEventListener('click', removeActiveMarker);
}
// --- Window Resize ---
function onWindowResize() {
if (!camera || !renderer) return;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
console.log("Window resized");
}
// --- Animation Loop ---
function startAnimationLoop() {
if (animationFrameId === null) { // Prevent multiple loops
console.log("Starting animation loop.");
animate();
} else {
console.log("Animation loop already running.");
}
}
function animate(time) {
// Ensure loop continues
animationFrameId = requestAnimationFrame(animate);
// Required updates
TWEEN.update(time);
controls.update(); // Updates damping, auto-rotate
const dt = time * 0.001; // Delta time in seconds (useful but optional here)
// Optional rotations (if not using controls.autoRotate)
// if (globe && !isAutoRotating) { globe.rotation.y += ROTATION_SPEED; }
if (clouds && clouds.visible) {
clouds.rotation.y += CLOUD_ROTATION_SPEED;
}
// Marker pulse
if (markersGroup && markersGroup.visible) {
markersGroup.children.forEach(marker => {
if (marker.userData.isMarker) {
marker.userData.pulseTime += dt || (1/60); // Use delta or estimate
const pulseScale = marker.userData.baseScale + Math.sin(marker.userData.pulseTime * PULSE_SPEED) * 0.1;
marker.scale.set(pulseScale, pulseScale, pulseScale);
}
});
}
// Render the scene
if (renderer && scene && camera) {
renderer.render(scene, camera);
} else {
console.error("Render call skipped: Renderer, Scene or Camera not ready.");
// Optionally stop the loop if core components missing: cancelAnimationFrame(animationFrameId); animationFrameId = null;
}
}
// --- Start ---
try {
init();
} catch (error) {
console.error("Error during initialization:", error);
// Display a user-friendly message if init fails catastrophically
document.body.innerHTML = `<div style="padding: 20px; color: red; background: black; font-family: sans-serif;">
<h2>Initialization Failed</h2>
<p>Could not start the 3D globe. Please check the developer console (F12) for errors.</p>
<p>Possible issues: WebGL not supported, network error loading resources, or a script error.</p>
<p>Error details: ${error.message}</p>
</div>`;
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.