PageSourceSearch

https://www.scientologi.no/js-static/year-in-review-2026/threejs.js?_=

js scientologi.no collected 2026-09-25 08:33:36 UTC 15,379 bytes, 400 lines download raw bytes

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.