{"project":{"id":"yqUE4Hb","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"World Explorer","visible":true,"contributors":"","githubRepo":null,"forkedFrom":null,"isTemplate":false,"tags":"","files":{"folder":"","files":[{"name":"index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Untitled</title>\n  <link rel=\"stylesheet\" href=\"style.css\">\n<script type=\"importmap\">\n{\n  \"imports\": {\n    \"three\": \"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js\",\n    \"three/addons/\": \"https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/\"\n  }\n}\n</script>\n</head>\n<body>\n<div id=\"globe-container\">\n\t<div id=\"globe-canvas\"></div>\n\t<div class=\"location-label\" id=\"label-london\">London</div>\n\t<div class=\"location-label\" id=\"label-redcar\">Redcar</div>\n\t<div class=\"location-label\" id=\"label-guangzhou\">Guangzhou</div>\n\t<div class=\"location-label\" id=\"label-newyork\">New York</div>\n\t<div class=\"location-label\" id=\"label-tokyo\">Tokyo</div>\n\t<div class=\"location-label\" id=\"label-sydney\">Sydney</div>\n\t<div class=\"location-label\" id=\"label-paris\">Paris</div>\n\t<div class=\"location-label\" id=\"label-dubai\">Dubai</div>\n\t<div class=\"location-label\" id=\"label-cairo\">Cairo</div>\n\t<div class=\"location-label\" id=\"label-mumbai\">Mumbai</div>\n\t<div class=\"location-label\" id=\"label-nairobi\">Nairobi</div>\n\t<div class=\"location-label\" id=\"label-moscow\">Moscow</div>\n\t<div class=\"location-label\" id=\"label-saopaulo\">São Paulo</div>\n\t<div class=\"location-label\" id=\"label-losangeles\">Los Angeles</div>\n\t<div class=\"location-label\" id=\"label-buenosaires\">Buenos Aires</div>\n\t<div class=\"globe-title\">\n\t\t<h1>World Explorer</h1>\n\t\t<p>Interactive Globe</p>\n\t</div>\n\t<div class=\"globe-legend\">\n\t\t<div class=\"legend-item\">\n\t\t\t<span class=\"legend-dot\"></span>\n\t\t\t<span>Global Landmarks</span>\n\t\t</div>\n\t\t<div class=\"legend-item\">\n\t\t\t<span class=\"legend-dot legend-dot--arc\"></span>\n\t\t\t<span>Connected Routes</span>\n\t\t</div>\n\t</div>\n</div>\n  <script type=\"module\" src=\"main.js\"></script>\n</body>\n</html>"},{"name":"main.js","content":"import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js';\nimport {\n\tOrbitControls\n} from 'https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/controls/OrbitControls.js';\n\n// Scene setup\nconst container = document.getElementById('globe-canvas');\nconst scene = new THREE.Scene();\n\nconst camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);\ncamera.position.set(0, 0.5, 3.2);\n\nconst renderer = new THREE.WebGLRenderer({\n\tantialias: true,\n\talpha: true\n});\nrenderer.setSize(window.innerWidth, window.innerHeight);\nrenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nrenderer.toneMappingExposure = 1.2;\ncontainer.appendChild(renderer.domElement);\n\n// Controls\nconst controls = new OrbitControls(camera, renderer.domElement);\ncontrols.enableDamping = true;\ncontrols.dampingFactor = 0.05;\ncontrols.rotateSpeed = 0.5;\ncontrols.enableZoom = true;\ncontrols.minDistance = 2;\ncontrols.maxDistance = 6;\ncontrols.enablePan = false;\ncontrols.autoRotate = true;\ncontrols.autoRotateSpeed = 0.4;\n\n// Lighting\nconst ambientLight = new THREE.AmbientLight(0x556677, 2.0);\nscene.add(ambientLight);\n\nconst directionalLight = new THREE.DirectionalLight(0xfff4e6, 2.5);\ndirectionalLight.position.set(5, 3, 5);\nscene.add(directionalLight);\n\nconst rimLight = new THREE.DirectionalLight(0x3366ff, 0.6);\nrimLight.position.set(-5, -2, -5);\nscene.add(rimLight);\n\n// Texture loader\nconst textureLoader = new THREE.TextureLoader();\n\n// Globe\nconst globeRadius = 1;\nconst globeGeometry = new THREE.SphereGeometry(globeRadius, 64, 64);\n\nconst earthTexture = textureLoader.load('https://unpkg.com/three-globe@2.31.1/example/img/earth-blue-marble.jpg');\nconst bumpTexture = textureLoader.load('https://unpkg.com/three-globe@2.31.1/example/img/earth-topology.png');\n\nconst globeMaterial = new THREE.MeshPhongMaterial({\n\tmap: earthTexture,\n\tbumpMap: bumpTexture,\n\tbumpScale: 0.03,\n\tspecular: 0x333333,\n\tshininess: 15,\n});\n\nconst globe = new THREE.Mesh(globeGeometry, globeMaterial);\nscene.add(globe);\n\n// Atmosphere glow\nconst atmosphereGeometry = new THREE.SphereGeometry(globeRadius * 1.015, 64, 64);\nconst atmosphereMaterial = new THREE.ShaderMaterial({\n\tvertexShader: `\n    varying vec3 vNormal;\n    void main() {\n      vNormal = normalize(normalMatrix * normal);\n      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n    }\n  `,\n\tfragmentShader: `\n    varying vec3 vNormal;\n    void main() {\n      float intensity = pow(0.65 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 3.0);\n      gl_FragColor = vec4(0.3, 0.5, 1.0, intensity * 0.4);\n    }\n  `,\n\tblending: THREE.AdditiveBlending,\n\tside: THREE.BackSide,\n\ttransparent: true,\n});\nconst atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);\nscene.add(atmosphere);\n\n// Location data\nconst locations = [{\n\t\tname: 'London',\n\t\tlat: 51.5074,\n\t\tlng: -0.1278,\n\t\tlabelId: 'label-london'\n\t},\n\t{\n\t\tname: 'Redcar',\n\t\tlat: 54.6180,\n\t\tlng: -1.0686,\n\t\tlabelId: 'label-redcar'\n\t},\n\t{\n\t\tname: 'Guangzhou',\n\t\tlat: 23.1291,\n\t\tlng: 113.2644,\n\t\tlabelId: 'label-guangzhou'\n\t},\n\t{\n\t\tname: 'New York',\n\t\tlat: 40.7128,\n\t\tlng: -74.0060,\n\t\tlabelId: 'label-newyork'\n\t},\n\t{\n\t\tname: 'Tokyo',\n\t\tlat: 35.6762,\n\t\tlng: 139.6503,\n\t\tlabelId: 'label-tokyo'\n\t},\n\t{\n\t\tname: 'Sydney',\n\t\tlat: -33.8688,\n\t\tlng: 151.2093,\n\t\tlabelId: 'label-sydney'\n\t},\n\t{\n\t\tname: 'Paris',\n\t\tlat: 48.8566,\n\t\tlng: 2.3522,\n\t\tlabelId: 'label-paris'\n\t},\n\t{\n\t\tname: 'Dubai',\n\t\tlat: 25.2048,\n\t\tlng: 55.2708,\n\t\tlabelId: 'label-dubai'\n\t},\n\t{\n\t\tname: 'Cairo',\n\t\tlat: 30.0444,\n\t\tlng: 31.2357,\n\t\tlabelId: 'label-cairo'\n\t},\n\t{\n\t\tname: 'Mumbai',\n\t\tlat: 19.0760,\n\t\tlng: 72.8777,\n\t\tlabelId: 'label-mumbai'\n\t},\n\t{\n\t\tname: 'Nairobi',\n\t\tlat: -1.2921,\n\t\tlng: 36.8219,\n\t\tlabelId: 'label-nairobi'\n\t},\n\t{\n\t\tname: 'Moscow',\n\t\tlat: 55.7558,\n\t\tlng: 37.6173,\n\t\tlabelId: 'label-moscow'\n\t},\n\t{\n\t\tname: 'São Paulo',\n\t\tlat: -23.5505,\n\t\tlng: -46.6333,\n\t\tlabelId: 'label-saopaulo'\n\t},\n\t{\n\t\tname: 'Los Angeles',\n\t\tlat: 34.0522,\n\t\tlng: -118.2437,\n\t\tlabelId: 'label-losangeles'\n\t},\n\t{\n\t\tname: 'Buenos Aires',\n\t\tlat: -34.6037,\n\t\tlng: -58.3816,\n\t\tlabelId: 'label-buenosaires'\n\t},\n];\n\n// Convert lat/lng to 3D position\nfunction latLngToVector3(lat, lng, radius) {\n\tconst phi = (90 - lat) * (Math.PI / 180);\n\tconst theta = (lng + 180) * (Math.PI / 180);\n\tconst x = -radius * Math.sin(phi) * Math.cos(theta);\n\tconst y = radius * Math.cos(phi);\n\tconst z = radius * Math.sin(phi) * Math.sin(theta);\n\treturn new THREE.Vector3(x, y, z);\n}\n\n// Create pins\nconst pinGroup = new THREE.Group();\nconst pinMeshes = [];\n\nlocations.forEach((loc) => {\n\tconst pos = latLngToVector3(loc.lat, loc.lng, globeRadius);\n\tconst pinHeight = 0.12;\n\n\tconst stemGeometry = new THREE.CylinderGeometry(0.005, 0.005, pinHeight, 8);\n\tconst stemMaterial = new THREE.MeshPhongMaterial({\n\t\tcolor: 0xff6b3d,\n\t\temissive: 0xff3300,\n\t\temissiveIntensity: 0.5,\n\t});\n\tconst stem = new THREE.Mesh(stemGeometry, stemMaterial);\n\n\tconst headGeometry = new THREE.SphereGeometry(0.022, 16, 16);\n\tconst headMaterial = new THREE.MeshPhongMaterial({\n\t\tcolor: 0xff6b3d,\n\t\temissive: 0xff4400,\n\t\temissiveIntensity: 0.7,\n\t});\n\tconst head = new THREE.Mesh(headGeometry, headMaterial);\n\thead.position.y = pinHeight / 2;\n\n\tconst ringGeometry = new THREE.RingGeometry(0.015, 0.035, 32);\n\tconst ringMaterial = new THREE.MeshBasicMaterial({\n\t\tcolor: 0xff6b3d,\n\t\ttransparent: true,\n\t\topacity: 0.5,\n\t\tside: THREE.DoubleSide,\n\t});\n\tconst ring = new THREE.Mesh(ringGeometry, ringMaterial);\n\n\tconst pin = new THREE.Group();\n\tpin.add(stem);\n\tpin.add(head);\n\tpin.add(ring);\n\n\tpin.position.copy(pos);\n\tpin.lookAt(new THREE.Vector3(0, 0, 0));\n\tpin.rotateX(Math.PI / 2);\n\n\tpinGroup.add(pin);\n\tpinMeshes.push({\n\t\tmesh: pin,\n\t\tdata: loc,\n\t\tpos\n\t});\n});\n\nscene.add(pinGroup);\n\n// Pulse rings\nconst pulseRings = [];\nlocations.forEach((loc) => {\n\tconst pos = latLngToVector3(loc.lat, loc.lng, globeRadius * 1.003);\n\tconst ringGeo = new THREE.RingGeometry(0.01, 0.015, 32);\n\tconst ringMat = new THREE.MeshBasicMaterial({\n\t\tcolor: 0xff6b3d,\n\t\ttransparent: true,\n\t\topacity: 0.6,\n\t\tside: THREE.DoubleSide,\n\t});\n\tconst ring = new THREE.Mesh(ringGeo, ringMat);\n\tring.position.copy(pos);\n\tring.lookAt(new THREE.Vector3(0, 0, 0));\n\tscene.add(ring);\n\tpulseRings.push({\n\t\tmesh: ring,\n\t\tphase: Math.random() * Math.PI * 2\n\t});\n});\n\n// Stars background\nfunction createStars() {\n\tconst geometry = new THREE.BufferGeometry();\n\tconst vertices = [];\n\tfor (let i = 0; i < 3000; i++) {\n\t\tconst r = 50 + Math.random() * 50;\n\t\tconst theta = Math.random() * Math.PI * 2;\n\t\tconst phi = Math.acos(2 * Math.random() - 1);\n\t\tvertices.push(\n\t\t\tr * Math.sin(phi) * Math.cos(theta),\n\t\t\tr * Math.sin(phi) * Math.sin(theta),\n\t\t\tr * Math.cos(phi)\n\t\t);\n\t}\n\tgeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));\n\tconst material = new THREE.PointsMaterial({\n\t\tcolor: 0xffffff,\n\t\tsize: 0.15,\n\t\ttransparent: true,\n\t\topacity: 0.6\n\t});\n\treturn new THREE.Points(geometry, material);\n}\nscene.add(createStars());\n\n// Arc helper\nfunction createArc(start, end, color = 0xfbbf24, opacity = 0.55, lift = 1.35) {\n\tconst mid = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);\n\tmid.normalize().multiplyScalar(globeRadius * lift);\n\tconst curve = new THREE.QuadraticBezierCurve3(\n\t\tstart.clone().multiplyScalar(1.005),\n\t\tmid,\n\t\tend.clone().multiplyScalar(1.005)\n\t);\n\tconst points = curve.getPoints(64);\n\tconst geometry = new THREE.BufferGeometry().setFromPoints(points);\n\tconst material = new THREE.LineBasicMaterial({\n\t\tcolor,\n\t\ttransparent: true,\n\t\topacity\n\t});\n\treturn new THREE.Line(geometry, material);\n}\n\n// Arc route pairs [indexA, indexB]\nconst arcPairs = [\n\t[0, 1], // London — Redcar\n\t[0, 6], // London — Paris\n\t[0, 3], // London — New York\n\t[3, 13], // New York — Los Angeles\n\t[3, 12], // New York — São Paulo\n\t[12, 14], // São Paulo — Buenos Aires\n\t[6, 11], // Paris — Moscow\n\t[6, 8], // Paris — Cairo\n\t[8, 9], // Cairo — Mumbai\n\t[8, 10], // Cairo — Nairobi\n\t[9, 7], // Mumbai — Dubai\n\t[9, 2], // Mumbai — Guangzhou\n\t[2, 4], // Guangzhou — Tokyo\n\t[4, 5], // Tokyo — Sydney\n\t[11, 7], // Moscow — Dubai\n];\n\nconst arcGroup = new THREE.Group();\narcPairs.forEach(([a, b]) => {\n\tconst posA = latLngToVector3(locations[a].lat, locations[a].lng, globeRadius);\n\tconst posB = latLngToVector3(locations[b].lat, locations[b].lng, globeRadius);\n\tarcGroup.add(createArc(posA, posB));\n});\n\n// Labels\nconst labelElements = locations.map(loc => document.getElementById(loc.labelId));\n\nfunction updateLabels() {\n\tlocations.forEach((loc, i) => {\n\t\tconst pos3D = latLngToVector3(loc.lat, loc.lng, globeRadius * 1.14);\n\t\tconst worldPos = pos3D.clone();\n\t\trotatingGroup.localToWorld(worldPos);\n\t\tconst screenPos = worldPos.clone().project(camera);\n\n\t\tconst camDir = camera.position.clone().normalize();\n\t\tconst pinDir = worldPos.clone().normalize();\n\t\tconst dotProduct = pinDir.dot(camDir);\n\t\tconst label = labelElements[i];\n\n\t\tif (dotProduct > 0.15) {\n\t\t\tconst x = (screenPos.x * 0.5 + 0.5) * window.innerWidth;\n\t\t\tconst y = (-screenPos.y * 0.5 + 0.5) * window.innerHeight;\n\t\t\tlabel.style.left = x + 'px';\n\t\t\tlabel.style.top = y + 'px';\n\t\t\tlabel.classList.add('visible');\n\t\t} else {\n\t\t\tlabel.classList.remove('visible');\n\t\t}\n\t});\n}\n\n// Group everything that rotates\nconst rotatingGroup = new THREE.Group();\nrotatingGroup.add(globe);\nrotatingGroup.add(atmosphere);\nrotatingGroup.add(pinGroup);\nrotatingGroup.add(arcGroup);\npulseRings.forEach(pr => rotatingGroup.add(pr.mesh));\nscene.add(rotatingGroup);\n\nrotatingGroup.rotation.y = 0;\nrotatingGroup.rotation.x = -0.15;\n\n// Animation loop\nconst clock = new THREE.Clock();\n\nfunction animate() {\n\trequestAnimationFrame(animate);\n\tconst elapsed = clock.getElapsedTime();\n\n\tpulseRings.forEach((pr) => {\n\t\tconst scale = 1 + 1.5 * Math.sin(elapsed * 2 + pr.phase);\n\t\tpr.mesh.scale.set(scale, scale, scale);\n\t\tpr.mesh.material.opacity = 0.5 * Math.max(0, 1 - (scale - 1) / 1.5);\n\t});\n\n\tcontrols.update();\n\tupdateLabels();\n\trenderer.render(scene, camera);\n}\n\nanimate();\n\nwindow.addEventListener('resize', () => {\n\tcamera.aspect = window.innerWidth / window.innerHeight;\n\tcamera.updateProjectionMatrix();\n\trenderer.setSize(window.innerWidth, window.innerHeight);\n});"},{"name":"style.css","content":"@import url('https://fonts.googleapis.com/css2?family=Inter+Tight:wght@300;400;600;800&family=Outfit:wght@300;500;700&display=swap');\n\n:root {\n\tcolor-scheme: dark;\n\t--bg-deep: #0a0a0f;\n\t--bg-surface: #12121a;\n\t--accent-warm: #ff6b3d;\n\t--accent-gold: #fbbf24;\n\t--text-primary: #e8e6e1;\n\t--text-muted: #6b6a72;\n}\n\n* {\n\tbox-sizing: border-box;\n\tmargin: 0;\n\tpadding: 0;\n}\n\nbody {\n\tbackground: var(--bg-deep);\n\tcolor: var(--text-primary);\n\tfont-family: 'Outfit', sans-serif;\n\toverflow: hidden;\n\theight: 100vh;\n\twidth: 100vw;\n}\n\n#globe-container {\n\tposition: relative;\n\twidth: 100vw;\n\theight: 100vh;\n\tbackground: radial-gradient(ellipse at 40% 50%, #141428 0%, #0a0a0f 70%);\n}\n\n#globe-canvas {\n\twidth: 100%;\n\theight: 100%;\n}\n\ncanvas {\n\tdisplay: block;\n}\n\n.globe-title {\n\tposition: absolute;\n\ttop: 3rem;\n\tleft: 3rem;\n\tpointer-events: none;\n\tz-index: 10;\n}\n\n.globe-title h1 {\n\tfont-family: 'Inter Tight', sans-serif;\n\tfont-weight: 300;\n\tfont-size: clamp(2rem, 5vw, 3.5rem);\n\tletter-spacing: 0.08em;\n\tline-height: 1;\n\ttext-transform: uppercase;\n\tbackground: linear-gradient(90deg, #ffffff 0%, #a0a0b0 50%, var(--accent-gold) 100%);\n\t-webkit-background-clip: text;\n\t-webkit-text-fill-color: transparent;\n\tbackground-clip: text;\n}\n\n.globe-title p {\n\tfont-weight: 300;\n\tfont-size: 1rem;\n\tcolor: var(--text-muted);\n\tmargin-top: 0.5rem;\n\tletter-spacing: 0.15em;\n\ttext-transform: uppercase;\n}\n\n.globe-legend {\n\tposition: absolute;\n\tbottom: 3rem;\n\tleft: 3rem;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 0.75rem;\n\tz-index: 10;\n\tpointer-events: none;\n}\n\n.legend-item {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 0.6rem;\n\tfont-size: 0.9rem;\n\tfont-weight: 500;\n\tcolor: var(--text-muted);\n}\n\n.legend-dot {\n\twidth: 10px;\n\theight: 10px;\n\tborder-radius: 50%;\n\tbackground: var(--accent-warm);\n\tbox-shadow: 0 0 12px var(--accent-warm), 0 0 24px rgba(255, 107, 61, 0.3);\n}\n\n.location-label {\n\tposition: absolute;\n\tpointer-events: none;\n\tz-index: 20;\n\tfont-family: 'Outfit', sans-serif;\n\tfont-weight: 500;\n\tfont-size: 0.8rem;\n\tcolor: var(--accent-gold);\n\tbackground: rgba(10, 10, 15, 0.85);\n\tborder: 1px solid rgba(251, 191, 36, 0.3);\n\tpadding: 0.3rem 0.7rem;\n\tborder-radius: 4px;\n\twhite-space: nowrap;\n\topacity: 0;\n\ttransform: translate(-50%, -100%) translateY(-12px);\n\ttransition: opacity 0.3s ease;\n\tbackdrop-filter: blur(8px);\n}\n\n.location-label.visible {\n\topacity: 1;\n}\n\n.location-label::after {\n\tcontent: '';\n\tposition: absolute;\n\tbottom: -5px;\n\tleft: 50%;\n\ttransform: translateX(-50%) rotate(45deg);\n\twidth: 8px;\n\theight: 8px;\n\tbackground: rgba(10, 10, 15, 0.85);\n\tborder-right: 1px solid rgba(251, 191, 36, 0.3);\n\tborder-bottom: 1px solid rgba(251, 191, 36, 0.3);\n}"}],"folders":[]},"variants":null,"createdAt":"2026-02-07T12:24:33.254Z","updatedAt":"2026-03-12T21:27:25.606Z","hasThumbnail":true}}