1 2// Debounce function to limit the rate at which a function can fire. 3function debounce(func, wait, immediate) { 4 let timeout; 5 return function executedFunction(...args) { 6 const later = () => { 7 timeout = null; 8 if (!immediate) func.apply(this, args); 9 }; 10 const callNow = immediate && !timeout; 11 clearTimeout(timeout); 12 timeout = setTimeout(later, wait); 13 if (callNow) func.apply(this, args); 14 }; 15}; 16 17 18class ThreeJSGlobe { 19 constructor() { 20 this.world = null; 21 this.dayNightMaterial = null; 22 this.dayNightCleanup = null; 23 this.dayNightEnabled = true; 24 this.enableDayNightShader = true; // set false to keep always-day texture 25 this.sunAnimationId = null; 26 this.solarPromise = null; 27 this.animateSunFn = null; 28 this.updateRotationUniform = null; 29 this.globeDefaults = { 30 globeImageUrl: '//unpkg.com/three-globe/example/img/earth-blue-marble.jpg', 31 bumpImageUrl: '//unpkg.com/three-globe/example/img/earth-topology.png' 32 }; 33 this.init(); 34 this.setupScrollRotation(); 35 } 36 37 init() { 38 // Initialize the Globe with a more dynamic arc style 39 var OPACITY = 0.9; 40 this.world = new Globe(document.getElementById('globe')) 41 .globeImageUrl(this.globeDefaults.globeImageUrl) 42 .bumpImageUrl(this.globeDefaults.bumpImageUrl) 43 .pointOfView({ lat: 27.9659, lng: -82.8001, altitude: 2 }) 44 .arcStartLat(d => d.startLat) 45 .arcStartLng(d => d.startLng) 46 .arcEndLat(d => d.endLat) 47 .arcEndLng(d => d.endLng) 48 .arcLabel(() => '') 49 .arcDashLength(0.25) 50 .arcDashGap(1) 51 .arcDashInitialGap(() => Math.random()) 52 .arcDashAnimateTime(3000) 53 .arcAltitudeAutoScale(0.6) 54 .arcStroke(0.35) 55 .arcColor(() => [`rgba(0, 255, 0, ${OPACITY})`, `rgba(255, 0, 0, ${OPACITY})`]) 56 .arcsTransitionDuration(0) 57 .pointColor(() => 'orange') 58 .pointAltitude(0) 59 .pointRadius(0.04) 60 .pointsMerge(true); 61 62 const startCity = { name: 'Flag', lat: 27.9659, lng: -82.8001 }; 63 64 let endCities = [ 65 { name: 'New York', lat: 40.7128, lng: -74.0060 }, 66 { name: 'Washington, DC', lat: 38.9072, lng: -77.0369 }, 67 { name: 'Twin Cities', lat: 44.9778, lng: -93.2650 }, 68 { name: 'Milan', lat: 45.4642, lng: 9.1900 }, 69 { name: 'Stuttgart', lat: 48.7758, lng: 9.1829 }, 70 { name: 'Tokyo', lat: 35.6762, lng: 139.6503 }, 71 { name: 'Kaohsiung', lat: 22.6273, lng: 120.3014 }, 72 { name: 'Eastern Cape', lat: -32.2968, lng: 26.4194 }, 73 ]; 74 75 // Add the startCity to the endCities list for labels 76 const allCities = [startCity, ...endCities]; 77 78 // Ensure unique labels by filtering duplicates based on city name 79 const uniqueCities = allCities.filter((city, index, self) => 80 index === self.findIndex(c => c.name === city.name) 81 ); 82 83 // multiply the cities 84 const multiply = 4; // send multiple concurrent arcs from Florida for higher cadence 85 endCities = endCities.reduce((acc, city) => acc.concat(Array(multiply).fill(city)), []); 86 87 // Generate arcs from startCity to endCities 88 const arcsData = endCities.map(endCity => ({ 89 startLat: startCity.lat, 90 startLng: startCity.lng, 91 endLat: endCity.lat, 92 endLng: endCity.lng, 93 fromName: 'Flag', 94 toName: endCity.name 95 })); 96 97 // Add arcs data to the globe 98 this.world.arcsData(arcsData); 99 100 // Add points so we can render the arc anchors 101 this.world.pointsData(uniqueCities); 102 103 // Add labels to cities 104 this.world 105 .labelsData(uniqueCities) // Use the list of all cities 106 .labelLat(d => d.lat) // Latitude for label 107 .labelLng(d => d.lng) // Longitude for label 108 .labelText(() => '') // Hide label text 109 .labelSize(() => 0.8) // Fixed label size (can adjust if needed) 110 .labelDotRadius(() => 0.5) // Add small dot for the label 111 .labelColor(() => 'rgba(255, 165, 0, 0.75)') // Label color 112 .labelResolution(2); // Resolution for text rendering 113 114 this.world.pauseAnimation(); 115 116 this.initObserver(); 117 118 // Disable Orbit Controls 119 const controls = this.world.controls();
120 if (controls) { 121 controls.enabled = false; 122 } 123 124 // Initialize optional day/night shader based on flag 125 this.dayNightEnabled = this.enableDayNightShader; 126 if (this.enableDayNightShader) { 127 this.initDayNightShader(); 128 } 129 130 window.addEventListener('resize', debounce(this.onWindowResize.bind(this), 250)); 131 } 132 133 134 onWindowResize() { 135 // Update renderer size 136 this.world.renderer().setSize(window.innerWidth, window.innerHeight); 137 138 // Update camera aspect ratio and projection matrix 139 this.world.camera().aspect = window.innerWidth / window.innerHeight; 140 this.world.camera().updateProjectionMatrix(); 141 } 142 143 initObserver() { 144 const observerOptions = { 145 root: null, // Use the viewport as the root 146 rootMargin: '0px 0px 300px 0px', 147 threshold: 0.1 // Trigger when 10% of the element is visible 148 }; 149 150 const observer = new IntersectionObserver((entries, obs) => { 151 entries.forEach(entry => { 152 const element = entry.target; 153 if (entry.isIntersecting) { 154 this.startAnimation(); 155 } else { 156 // Element is not intersecting; stop the animation if it exists 157 this.stopAnimation(); 158 } 159 }); 160 }, observerOptions); 161 162 const canvasElement = document.getElementById('globe'); 163 observer.observe(canvasElement); 164 } 165 166 startAnimation() { 167 this.world.resumeAnimation(); 168 } 169 170 stopAnimation() { 171 this.world.pauseAnimation(); 172 } 173 174 175 setupScrollRotation() { 176 var self = this; 177 178 const globeScene = this.world.scene(); 179 180 // Create the .year-in-review-2026-fireworks-bottom-gradient element 181 const gradientElement = document.createElement('div'); 182 gradientElement.className = 'year-in-review-2026-fireworks-bottom-gradient'; 183 184 185 // Append the gradient element to the globe scene container 186 const globeContainer = document.querySelector('.year-in-review-2026-globe-inner .scene-container'); 187 if (globeContainer) { 188 globeContainer.appendChild(gradientElement); 189 } 190 191 // Initial rotation and zoom levels 192 const initialRotation = Math.PI * 2.05; 193 const endRotation = Math.PI * 1.7; 194 // const endRotation = -Math.PI / 5; 195 const initialZoom = 0.5; // Initial zoom level 196 const zoomedInAltitude = 2.5; // End altitude 197 198 // Set initial rotation 199 // globeScene.rotation.y = initialRotation; 200 201 const initialPosition = { lat: 27.9659, lng: -82.8001, altitude: initialZoom }; 202 203 // this.world.pointOfView({ lat: 27.9659, lng: -82.8001, altitude: initialZoom }) // aim at continental US centroid 204 this.world.pointOfView(initialPosition) // aim at continental US centroid 205 206 const latPointOfView = 0; 207 const lngPointOfView = -82.8001 ; 208 209 const endPosition = { lat: latPointOfView, lng: lngPointOfView, altitude: zoomedInAltitude }; 210 211 // Create the ScrollTrigger 212 ScrollTrigger.create({ 213 trigger: ".year-in-review-2026-globe-inner", // Wrapper element 214 start: "top top", // Start animation when wrapper's top reaches the bottom of the viewport 215 endTrigger: ".year-in-review-2026--febc-end-trigger", 216 end: "bottom bottom-=20%", // Duration of the pin effect (scroll distance) 217 pin: true, 218 pinSpacing: false, 219 scrub: true, 220 }); 221 222 const canvasElement = document.getElementById('globe'); 223 224 const world = this.world; 225 var timeline = gsap.timeline({ 226 scrollTrigger: { 227 trigger: ".year-in-review-2026-globe-wrapper", // Wrapper element 228 start: "top top", // Start animation when wrapper's top reaches the bottom of the viewport 229 end: "+=200%", // Duration of the pin effect (scroll distance) 230 scrub: true, 231 onUpdate: (self) => { 232 const progress = self.progress;
232 // Progress is a value between 0 and 1 233 // const currentRotation = gsap.utils.mapRange(0, 1, initialRotation, endRotation, progress); 234 const currentRotation = gsap.utils.mapRange(0, 1, initialRotation, endRotation, progress); 235 const currentLat = gsap.utils.mapRange(0, 1, initialPosition.lat, endPosition.lat, progress); 236 const currentLng = gsap.utils.mapRange(0, 1, initialPosition.lng, endPosition.lng, progress); 237 const currentZoom = gsap.utils.mapRange(0, 1, initialPosition.altitude, endPosition.altitude, progress); 238 globeScene.rotation.y = currentRotation; 239 // console.log('currentRotation',currentRotation); 240 // world.pointOfView({ altitude: currentZoom }, 0); 241 world.pointOfView({ lat: currentLat, lng: currentLng, altitude: currentZoom }, 0); 242 }, 243 }, 244 }); 245 246 timeline.fromTo('.year-in-review-2026-globe-inner', { autoAlpha: 0 }, { duration: 0.2, autoAlpha: 1 }); 247 } 248 249 async initDayNightShader() { 250 if (!window.THREE) { 251 return; // three.js not available in this context 252 } 253 254 const { TextureLoader, ShaderMaterial, Vector2 } = THREE; 255 const dayNightShader = { 256 vertexShader: ` 257 varying vec3 vNormal; 258 varying vec2 vUv; 259 void main() { 260 vNormal = normalize(normalMatrix * normal); 261 vUv = uv; 262 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 263 } 264 `, 265 fragmentShader: ` 266 #define PI 3.141592653589793 267 uniform sampler2D dayTexture; 268 uniform sampler2D nightTexture; 269 uniform vec2 sunPosition; 270 uniform vec2 globeRotation; 271 varying vec3 vNormal; 272 varying vec2 vUv; 273 274 float toRad(in float a) { 275 return a * PI / 180.0; 276 } 277 278 vec3 Polar2Cartesian(in vec2 c) { // [lng, lat] 279 float theta = toRad(90.0 - c.x); 280 float phi = toRad(90.0 - c.y); 281 return vec3( 282 sin(phi) * cos(theta), 283 cos(phi), 284 sin(phi) * sin(theta) 285 ); 286 } 287 288 void main() { 289 float invLon = toRad(globeRotation.x); 290 float invLat = -toRad(globeRotation.y); 291 mat3 rotX = mat3( 292 1, 0, 0, 293 0, cos(invLat), -sin(invLat), 294 0, sin(invLat), cos(invLat) 295 ); 296 mat3 rotY = mat3( 297 cos(invLon), 0, sin(invLon), 298 0, 1, 0, 299 -sin(invLon), 0, cos(invLon) 300 ); 301 vec3 rotatedSunDirection = rotX * rotY * Polar2Cartesian(sunPosition); 302 float intensity = dot(normalize(vNormal), normalize(rotatedSunDirection)); 303 vec4 dayColor = texture2D(dayTexture, vUv); 304 vec4 nightColor = texture2D(nightTexture, vUv); 305 float blendFactor = smoothstep(-0.1, 0.1, intensity); 306 gl_FragColor = mix(nightColor, dayColor, blendFactor); 307 } 308 ` 309 }; 310 311 if (!this.solarPromise) { 312 this.solarPromise = import('https://esm.sh/solar-calculator'); 313 } 314 315 const [solar, dayTexture, nightTexture] = await Promise.all([ 316 this.solarPromise, 317 new TextureLoader().loadAsync('//cdn.jsdelivr.net/npm/three-globe/example/img/earth-day.jpg'), 318 new TextureLoader().loadAsync('//cdn.jsdelivr.net/npm/three-globe/example/img/earth-night.jpg') 319 ]); 320 321 const material = new ShaderMaterial({ 322 uniforms: { 323 dayTexture: { value: dayTexture }, 324 nightTexture: { value: nightTexture }, 325 sunPosition: { value: new Vector2() }, 326 globeRotation: { value: new Vector2() } 327 }, 328 vertexShader: dayNightShader.vertexShader, 329 fragmentShader: dayNightShader.fragmentShader 330 }); 331 332 this.dayNightMaterial = material; 333 334 const sunPosAt = dt => { 335 const day = new Date(+dt).setUTCHours(0, 0, 0, 0); 336 const t = solar.century(dt); 337 const longitude = (day - dt) / 864e5 * 360 - 180;
338 return [longitude - solar.equationOfTime(t) / 4, solar.declination(t)]; 339 }; 340 341 const VELOCITY = 1; // minutes per frame 342 let dt = +new Date(); 343 344 const animateSun = () => { 345 dt += VELOCITY * 60 * 1000; 346 material.uniforms.sunPosition.value.set(...sunPosAt(dt)); 347 this.sunAnimationId = requestAnimationFrame(animateSun); 348 }; 349 this.animateSunFn = animateSun; 350 351 const updateRotationUniform = ({ lng, lat }) => { 352 material.uniforms.globeRotation.value.set(lng, lat); 353 }; 354 this.updateRotationUniform = updateRotationUniform; 355 356 // Apply shader material and wire up animation + rotation updates 357 this.world 358 .globeMaterial(material) 359 .onZoom(updateRotationUniform); 360 361 animateSun(); 362 363 this.dayNightCleanup = () => { 364 if (this.sunAnimationId) { 365 cancelAnimationFrame(this.sunAnimationId); 366 this.sunAnimationId = null; 367 } 368 this.world.globeMaterial(null); 369 this.world 370 .globeImageUrl(this.globeDefaults.globeImageUrl) 371 .bumpImageUrl(this.globeDefaults.bumpImageUrl) 372 .backgroundImageUrl(null) 373 .onZoom(null); 374 }; 375 } 376 377 async toggleDayNight(forceState) { 378 const nextState = forceState !== undefined ? forceState : !this.dayNightEnabled; 379 this.dayNightEnabled = nextState; 380 381 if (!nextState) { 382 if (this.dayNightCleanup) { 383 this.dayNightCleanup(); 384 } 385 return; 386 } 387 388 if (!this.dayNightMaterial) { 389 await this.initDayNightShader(); 390 } else { 391 this.world 392 .globeMaterial(this.dayNightMaterial) 393 .onZoom(this.updateRotationUniform || null); 394 if (this.animateSunFn) { 395 this.animateSunFn(); 396 } 397 } 398 } 399 400}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.