├── .gitignore
├── README.md
├── img
├── app_screen_shot.png
├── favicon.png
└── world_alpha_mini.jpg
├── index.css
├── index.html
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # threejs-globe
2 |
3 | This is inspired by Github & Stripes webgl globes.
4 |
5 | The dots clustered together resembling continents are achieved by reading an image of the world.
6 | Getting the image data for each pixel and iterating over each pixel.
7 | If the pixels r,g,b values exceed 100, display dot.
8 | The position of the dot is worked out by determining the lat and long position of the pixel.
9 |
10 | Each dot within the canvas independently changes colour to give off a twinkling effect.
11 | This is achieved by shaders.
12 |
13 | If the globe is clicked and dragged, the globe rotates in the direction of the drag.
14 | Along with this functionality, each dot independently extrudes off the globe creating a scattered effect.
15 | This is achieved by shaders.
16 |
17 | To view, checkout: https://hydeit.co/globe/
18 |
19 | 
20 |
21 |
--------------------------------------------------------------------------------
/img/app_screen_shot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jessehhydee/threejs-globe/aac9d261cc00d034b04e3b6a108b371677ffa349/img/app_screen_shot.png
--------------------------------------------------------------------------------
/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jessehhydee/threejs-globe/aac9d261cc00d034b04e3b6a108b371677ffa349/img/favicon.png
--------------------------------------------------------------------------------
/img/world_alpha_mini.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jessehhydee/threejs-globe/aac9d261cc00d034b04e3b6a108b371677ffa349/img/world_alpha_mini.jpg
--------------------------------------------------------------------------------
/index.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 0px;
3 | padding: 0px;
4 | overflow-x: hidden;
5 | }
6 |
7 | .container {
8 | width: 100vw;
9 | height: 100vh;
10 | margin: 0px;
11 | padding: 0px;
12 | background: #0f2027;
13 | background: -webkit-linear-gradient(to top, #0f2027, #203a43, #2c5364);
14 | background: linear-gradient(to top, #0f2027, #203a43, #2c5364);
15 | color: rgb(49, 98, 127);
16 | }
17 |
18 | .canvas {
19 | width: 100%;
20 | height: 100%;
21 | position: absolute;
22 | top: 0px;
23 | left: 0px;
24 | }
25 |
26 | .source_btn {
27 | position: absolute;
28 | bottom: 30px;
29 | right: 30px;
30 | z-index: 10;
31 | width: fit-content;
32 | height: fit-content;
33 | border-radius: 50px;
34 | display: flex;
35 | border: 1px solid #FFFFFF;
36 | cursor: pointer;
37 | background: inherit;
38 | padding: 10px;
39 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Globe
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
3 |
4 | const vertex = `
5 | #ifdef GL_ES
6 | precision mediump float;
7 | #endif
8 |
9 | uniform float u_time;
10 | uniform float u_maxExtrusion;
11 |
12 | void main() {
13 |
14 | vec3 newPosition = position;
15 | if(u_maxExtrusion > 1.0) newPosition.xyz = newPosition.xyz * u_maxExtrusion + sin(u_time);
16 | else newPosition.xyz = newPosition.xyz * u_maxExtrusion;
17 |
18 | gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
19 |
20 | }
21 | `;
22 | const fragment = `
23 | #ifdef GL_ES
24 | precision mediump float;
25 | #endif
26 |
27 | uniform float u_time;
28 |
29 | vec3 colorA = vec3(0.196, 0.631, 0.886);
30 | vec3 colorB = vec3(0.192, 0.384, 0.498);
31 |
32 | void main() {
33 |
34 | vec3 color = vec3(0.0);
35 | float pct = abs(sin(u_time));
36 | color = mix(colorA, colorB, pct);
37 |
38 | gl_FragColor = vec4(color, 1.0);
39 |
40 | }
41 | `;
42 |
43 | const container = document.querySelector('.container');
44 | const canvas = document.querySelector('.canvas');
45 |
46 | let
47 | sizes,
48 | scene,
49 | camera,
50 | renderer,
51 | controls,
52 | raycaster,
53 | mouse,
54 | isIntersecting,
55 | twinkleTime,
56 | materials,
57 | material,
58 | baseMesh,
59 | minMouseDownFlag,
60 | mouseDown,
61 | grabbing;
62 |
63 | const setScene = () => {
64 |
65 | sizes = {
66 | width: container.offsetWidth,
67 | height: container.offsetHeight
68 | };
69 |
70 | scene = new THREE.Scene();
71 |
72 | camera = new THREE.PerspectiveCamera(
73 | 30,
74 | sizes.width / sizes.height,
75 | 1,
76 | 1000
77 | );
78 | camera.position.z = 100;
79 |
80 | renderer = new THREE.WebGLRenderer({
81 | canvas: canvas,
82 | antialias: false,
83 | alpha: true
84 | });
85 | renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
86 |
87 | const pointLight = new THREE.PointLight(0x081b26, 17, 200);
88 | pointLight.position.set(-50, 0, 60);
89 | scene.add(pointLight);
90 | scene.add(new THREE.HemisphereLight(0xffffbb, 0x080820, 1.5));
91 |
92 | raycaster = new THREE.Raycaster();
93 | mouse = new THREE.Vector2();
94 | isIntersecting = false;
95 | minMouseDownFlag = false;
96 | mouseDown = false;
97 | grabbing = false;
98 |
99 | setControls();
100 | setBaseSphere();
101 | setShaderMaterial();
102 | setMap();
103 | resize();
104 | listenTo();
105 | render();
106 |
107 | }
108 |
109 | const setControls = () => {
110 |
111 | controls = new OrbitControls(camera, renderer.domElement);
112 | controls.autoRotate = true;
113 | controls.autoRotateSpeed = 1.2;
114 | controls.enableDamping = true;
115 | controls.enableRotate = true;
116 | controls.enablePan = false;
117 | controls.enableZoom = false;
118 | controls.minPolarAngle = (Math.PI / 2) - 0.5;
119 | controls.maxPolarAngle = (Math.PI / 2) + 0.5;
120 |
121 | };
122 |
123 | const setBaseSphere = () => {
124 |
125 | const baseSphere = new THREE.SphereGeometry(19.5, 35, 35);
126 | const baseMaterial = new THREE.MeshStandardMaterial({
127 | color: 0x0b2636,
128 | transparent: true,
129 | opacity: 0.9
130 | });
131 | baseMesh = new THREE.Mesh(baseSphere, baseMaterial);
132 | scene.add(baseMesh);
133 |
134 | }
135 |
136 | const setShaderMaterial = () => {
137 |
138 | twinkleTime = 0.03;
139 | materials = [];
140 | material = new THREE.ShaderMaterial({
141 | side: THREE.DoubleSide,
142 | uniforms: {
143 | u_time: { value: 1.0 },
144 | u_maxExtrusion: { value: 1.0 }
145 | },
146 | vertexShader: vertex,
147 | fragmentShader: fragment,
148 | });
149 |
150 | }
151 |
152 | const setMap = () => {
153 |
154 | let activeLatLon = {};
155 | const dotSphereRadius = 20;
156 |
157 | const readImageData = (imageData) => {
158 |
159 | for(
160 | let i = 0, lon = -180, lat = 90;
161 | i < imageData.length;
162 | i += 4, lon++
163 | ) {
164 |
165 | if(!activeLatLon[lat]) activeLatLon[lat] = [];
166 |
167 | const red = imageData[i];
168 | const green = imageData[i + 1];
169 | const blue = imageData[i + 2];
170 |
171 | if(red < 80 && green < 80 && blue < 80)
172 | activeLatLon[lat].push(lon);
173 |
174 | if(lon === 180) {
175 | lon = -180;
176 | lat--;
177 | }
178 |
179 | }
180 |
181 | }
182 |
183 | const visibilityForCoordinate = (lon, lat) => {
184 |
185 | let visible = false;
186 |
187 | if(!activeLatLon[lat].length) return visible;
188 |
189 | const closest = activeLatLon[lat].reduce((prev, curr) => {
190 | return (Math.abs(curr - lon) < Math.abs(prev - lon) ? curr : prev);
191 | });
192 |
193 | if(Math.abs(lon - closest) < 0.5) visible = true;
194 |
195 | return visible;
196 |
197 | }
198 |
199 | const calcPosFromLatLonRad = (lon, lat) => {
200 |
201 | var phi = (90 - lat) * (Math.PI / 180);
202 | var theta = (lon + 180) * (Math.PI / 180);
203 |
204 | const x = -(dotSphereRadius * Math.sin(phi) * Math.cos(theta));
205 | const z = (dotSphereRadius * Math.sin(phi) * Math.sin(theta));
206 | const y = (dotSphereRadius * Math.cos(phi));
207 |
208 | return new THREE.Vector3(x, y, z);
209 |
210 | }
211 |
212 | const createMaterial = (timeValue) => {
213 |
214 | const mat = material.clone();
215 | mat.uniforms.u_time.value = timeValue * Math.sin(Math.random());
216 | materials.push(mat);
217 | return mat;
218 |
219 | }
220 |
221 | const setDots = () => {
222 |
223 | const dotDensity = 2.5;
224 | let vector = new THREE.Vector3();
225 |
226 | for (let lat = 90, i = 0; lat > -90; lat--, i++) {
227 |
228 | const radius =
229 | Math.cos(Math.abs(lat) * (Math.PI / 180)) * dotSphereRadius;
230 | const circumference = radius * Math.PI * 2;
231 | const dotsForLat = circumference * dotDensity;
232 |
233 | for (let x = 0; x < dotsForLat; x++) {
234 |
235 | const long = -180 + x * 360 / dotsForLat;
236 |
237 | if (!visibilityForCoordinate(long, lat)) continue;
238 |
239 | vector = calcPosFromLatLonRad(long, lat);
240 |
241 | const dotGeometry = new THREE.CircleGeometry(0.1, 5);
242 | dotGeometry.lookAt(vector);
243 | dotGeometry.translate(vector.x, vector.y, vector.z);
244 |
245 | const m = createMaterial(i);
246 | const mesh = new THREE.Mesh(dotGeometry, m);
247 |
248 | scene.add(mesh);
249 |
250 | }
251 |
252 | }
253 |
254 | }
255 |
256 | const image = new Image;
257 | image.onload = () => {
258 |
259 | image.needsUpdate = true;
260 |
261 | const imageCanvas = document.createElement('canvas');
262 | imageCanvas.width = image.width;
263 | imageCanvas.height = image.height;
264 |
265 | const context = imageCanvas.getContext('2d');
266 | context.drawImage(image, 0, 0);
267 |
268 | const imageData = context.getImageData(
269 | 0,
270 | 0,
271 | imageCanvas.width,
272 | imageCanvas.height
273 | );
274 | readImageData(imageData.data);
275 |
276 | setDots();
277 |
278 | }
279 |
280 | image.src = 'img/world_alpha_mini.jpg';
281 |
282 | }
283 |
284 | const resize = () => {
285 |
286 | sizes = {
287 | width: container.offsetWidth,
288 | height: container.offsetHeight
289 | };
290 |
291 | if(window.innerWidth > 700) camera.position.z = 100;
292 | else camera.position.z = 140;
293 |
294 | camera.aspect = sizes.width / sizes.height;
295 | camera.updateProjectionMatrix();
296 |
297 | renderer.setSize(sizes.width, sizes.height);
298 |
299 | }
300 |
301 | const mousemove = (event) => {
302 |
303 | isIntersecting = false;
304 |
305 | mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
306 | mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
307 |
308 | raycaster.setFromCamera(mouse, camera);
309 |
310 | const intersects = raycaster.intersectObject(baseMesh);
311 | if(intersects[0]) {
312 | isIntersecting = true;
313 | if(!grabbing) document.body.style.cursor = 'pointer';
314 | }
315 | else {
316 | if(!grabbing) document.body.style.cursor = 'default';
317 | }
318 |
319 | }
320 |
321 | const mousedown = () => {
322 |
323 | if(!isIntersecting) return;
324 |
325 | materials.forEach(el => {
326 | gsap.to(
327 | el.uniforms.u_maxExtrusion,
328 | {
329 | value: 1.07
330 | }
331 | );
332 | });
333 |
334 | mouseDown = true;
335 | minMouseDownFlag = false;
336 |
337 | setTimeout(() => {
338 | minMouseDownFlag = true;
339 | if(!mouseDown) mouseup();
340 | }, 500);
341 |
342 | document.body.style.cursor = 'grabbing';
343 | grabbing = true;
344 |
345 | }
346 |
347 | const mouseup = () => {
348 |
349 | mouseDown = false;
350 | if(!minMouseDownFlag) return;
351 |
352 | materials.forEach(el => {
353 | gsap.to(
354 | el.uniforms.u_maxExtrusion,
355 | {
356 | value: 1.0,
357 | duration: 0.15
358 | }
359 | );
360 | });
361 |
362 | grabbing = false;
363 | if(isIntersecting) document.body.style.cursor = 'pointer';
364 | else document.body.style.cursor = 'default';
365 |
366 | }
367 |
368 | const listenTo = () => {
369 |
370 | window.addEventListener('resize', resize.bind(this));
371 | window.addEventListener('mousemove', mousemove.bind(this));
372 | window.addEventListener('mousedown', mousedown.bind(this));
373 | window.addEventListener('mouseup', mouseup.bind(this));
374 |
375 | }
376 |
377 | const render = () => {
378 |
379 | materials.forEach(el => {
380 | el.uniforms.u_time.value += twinkleTime;
381 | });
382 |
383 | controls.update();
384 | renderer.render(scene, camera);
385 | requestAnimationFrame(render.bind(this))
386 |
387 | }
388 |
389 | setScene();
--------------------------------------------------------------------------------