UNPKG

@finsweet/3dglobes

Version:

3D interactive globe on a Webflow site without coding.

365 lines (295 loc) 11.5 kB
/** * * @param {url} param */ function FsGlobe() { const mainContainer = document.querySelector("[fs-3dglobe-element='container']"); const bgTexture = mainContainer.getAttribute('fs-3dglobe-img'); const defaultValue = { url: bgTexture || 'https://cdn.finsweet.com/files/globe/earthmap1k.jpg', }; const globeContainer = document.createElement('div'); globeContainer.className = 'fs-3dglobe-container'; mainContainer.appendChild(globeContainer); const canvas = document.createElement('canvas'); canvas.className = 'canvas-3dglobe-container'; globeContainer.appendChild(canvas); const userInfo = [].slice.call(document.querySelectorAll("[fs-3dglobe-element='tooltip']")); const marker = [].slice.call(document.querySelectorAll("[fs-3dglobe-element='pin']")); const renderer = new THREE.WebGLRenderer({ canvas, alpha: true }); const fov = 60; const aspect = 2; // the canvas default const near = 0.1; const far = 10; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); camera.position.z = 2.5; const controls = new THREE.OrbitControls(camera, canvas); controls.enableDamping = true; controls.enablePan = false; controls.minDistance = 1.2; controls.maxDistance = 4; controls.autoRotate = true; controls.autoRotateSpeed = 0.2; // // controls.enableDamping = true; // controls.campingFactor = 0.25; controls.enableZoom = false; controls.update(); const scene = new THREE.Scene(); // scene.background = new THREE.Color("#246"); renderer.setClearColor(0x000000, 0); let renderRequested = false; let animationFrame; const team = fetchDataFromCollection("[fs-3dglobe-element='list'] .w-dyn-item"); const loader = new THREE.TextureLoader(); const texture = loader.load(defaultValue.url, render); texture.needsUpdate = true; const geometry = new THREE.SphereBufferGeometry(1, 64, 32); const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide, }); const mesh = new THREE.Mesh(geometry, material); scene.add(mesh); material.map.needsUpdate = true; function loadCountryData() { const lonFudge = Math.PI * 1.5; const latFudge = Math.PI; // these helpers will make it easy to position the boxes // We can rotate the lon helper on its Y axis to the longitude const lonHelper = new THREE.Object3D(); // We rotate the latHelper on its X axis to the latitude const latHelper = new THREE.Object3D(); lonHelper.add(latHelper); // The position helper moves the object to the edge of the sphere const positionHelper = new THREE.Object3D(); positionHelper.position.z = 1; latHelper.add(positionHelper); const labelParentElem = document.createElement('div'); labelParentElem.className = 'fs-3dglobe-labels'; globeContainer.appendChild(labelParentElem); for (const [index, companyInfo] of team.entries()) { const { lat, lon, name, url } = companyInfo; // adjust the helpers to point to the latitude and longitude lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + lonFudge; latHelper.rotation.x = THREE.MathUtils.degToRad(lat) + latFudge; // get the position of the lat/lon positionHelper.updateWorldMatrix(true, false); const position = new THREE.Vector3(); positionHelper.getWorldPosition(position); companyInfo.position = position; // add an element for each country const elem = document.createElement('div'); const infoBox = document.createElement('div'); elem.className = 'map-container'; infoBox.innerHTML = userInfo[index].outerHTML || getInfoBox({ url, name, }); infoBox.className = 'fs-3dglobe-info-box'; elem.style.cursor = 'pointer'; const pin = document.createElement('div'); pin.className = 'fs-3dglobe-arrow-box'; if (!marker[index]) { const box = document.createElement('div'); box.className = 'fs-3dglobe-arrow_box'; const logo = document.createElement('img'); logo.className = 'logo_dot'; logo.position = 'relative'; logo.setAttribute('alt', name); logo.setAttribute('src', url); logo.style.cursor = 'pointer'; logo.style.width = '50px'; box.appendChild(logo); pin.appendChild(box); } else { pin.appendChild(marker[index]); } elem.appendChild(pin); elem.appendChild(infoBox); labelParentElem.appendChild(elem); companyInfo.elem = elem; } requestRenderIfNotRequested(); } loadCountryData(); const tempV = new THREE.Vector3(); const cameraToPoint = new THREE.Vector3(); const cameraPosition = new THREE.Vector3(); const normalMatrix = new THREE.Matrix3(); const settings = { minArea: 20, maxVisibleDot: -0.08, }; function updateLabels() { const large = settings.minArea * settings.minArea; // get a matrix that represents a relative orientation of the camera normalMatrix.getNormalMatrix(camera.matrixWorldInverse); // get the camera's position camera.getWorldPosition(cameraPosition); for (const companyInfo of team) { const { position, elem, area } = companyInfo; // large enough? if (area < large) { elem.style.opacity = '.009'; elem.style.display = 'none'; continue; } // Orient the position based on the camera's orientation. // Since the sphere is at the origin and the sphere is a unit sphere // this gives us a camera relative direction vector for the position. tempV.copy(position); tempV.applyMatrix3(normalMatrix); // compute the direction to this position from the camera cameraToPoint.copy(position); cameraToPoint.applyMatrix4(camera.matrixWorldInverse).normalize(); // get the dot product of camera relative direction to this position // on the globe with the direction from the camera to that point. // -1 = facing directly towards the camera // 0 = exactly on tangent of the sphere from the camera // > 0 = facing away const dot = tempV.dot(cameraToPoint); // if the orientation is not facing us hide it. if (dot > settings.maxVisibleDot) { elem.style.opacity = '.009'; elem.style.display = 'none'; continue; } // restore the element to its default display style elem.style.opacity = '1'; elem.style.display = ''; // get the normalized screen coordinate of that position // x and y will be in the -1 to +1 range with x = -1 being // on the left and y = -1 being on the bottom tempV.copy(position); tempV.project(camera); // convert the normalized position to CSS coordinates const x = (tempV.x * 0.5 + 0.5) * canvas.clientWidth; const y = (tempV.y * -0.5 + 0.5) * canvas.clientHeight; // move the elem to that position elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`; // // set the zIndex for sorting elem.style.zIndex = ((-tempV.z * 0.5 + 0.5) * 100000) | 0; } } function resizeRendererToDisplaySize(renderer) { const canvas = renderer.domElement; const width = canvas.clientWidth; const height = canvas.clientHeight; const needResize = canvas.width !== width || canvas.height !== height; if (needResize) { renderer.setSize(width, height, false); } return needResize; } function render() { renderRequested = undefined; animationFrame = requestAnimationFrame(render); if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); } controls.update(); updateLabels(); renderer.render(scene, camera); } render(); function requestRenderIfNotRequested() { if (!renderRequested) { cancelAnimationFrame(animationFrame); renderRequested = true; animationFrame = requestAnimationFrame(render); } } // USED TO PASS PAGE SCROLL AS PERCENTAGE var getScrollPercent = function () { var h = document.documentElement, b = document.body, st = 'scrollTop', sh = 'scrollHeight'; return ((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100; }; // window.addEventListener("scroll", updateCamera); // canvas.addEventListener("mousemove", onMousemove, false); controls.addEventListener('change', requestRenderIfNotRequested); window.addEventListener('resize', requestRenderIfNotRequested); } function getInfoBox({ url, name, location = 'N/A', role = 'N/A' }) { return ` <div style=" border: 1px solid #dadce0; border-radius: 8px; overflow: hidden;"> <div class="caption"> <img src="${url}" style="height: 200px; max-width:600px;" /> </div> <div style="padding:5px 10px"> <div> <strong>${name}</strong> </div> <div>Javascript, Node.js</div> <div>${location}</div> </div> </div> `; } function fetchDataFromCollection(collectionWrapper) { const collection = [].slice.call(document.querySelectorAll(collectionWrapper)); // const data = [].slice.call(collection.getElementsByTagName("embed")); // return data.map((elem) => { // return { // name: elem.getAttribute("name"), // lat: elem.getAttribute("lat"), // lon: elem.getAttribute("lon"), // url: elem.getAttribute("url"), // hovertext: elem.getAttribute("hovertext"), // }; // }); return collection.map((elem) => { return { name: (elem.querySelector("[fs-3dglobe-element='name'") || {}).textContent, lat: (elem.querySelector("[fs-3dglobe-element='lat'") || {}).textContent, lon: (elem.querySelector("[fs-3dglobe-element='lon'") || {}).textContent, url: (elem.querySelector("[fs-3dglobe-element='url'") || {}).textContent, }; }); } function LoadSvg(url, scene) { const loader = new THREE.SVGLoader(); loader.load( url, function (data) { let paths = data.paths; let group = new THREE.Group(); group.scale.multiplyScalar(0.011); group.position.x = -9; group.rotation.x = Math.PI; group.position.y = 5; group.position.z = -3; for (let i = 0; i < paths.length; i++) { let path = paths[i]; let material = new THREE.MeshBasicMaterial({ color: path.color, side: THREE.DoubleSide, depthWrite: false, }); let shapes = path.toShapes(true); for (let j = 0; j < shapes.length; j++) { let shape = shapes[j]; let geometry = new THREE.ShapeBufferGeometry(shape); let mesh = new THREE.Mesh(geometry, material); group.add(mesh); } } scene.add(group); }, function (xhr) { console.log((xhr.loaded / xhr.total) * 100 + '% loaded'); }, function (error) { console.log('An error happened'); } ); } (function () { FsGlobe(); })();