├── .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 | ![alt text](https://github.com/jessehhydee/threejs-globe/blob/main/img/app_screen_shot.png?raw=true) 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(); --------------------------------------------------------------------------------