Public
Edited
Jul 14
Insert cell
Insert cell
Insert cell
route = [
[1.3521, -103.8198], // Singapore city centre
[51.509865, 0.118092], // London (Trafalgar Sq.)
[33.9425, 118.4081] // Los Angeles – LAX
]
Insert cell
Insert cell
Insert cell
async function createEarthFlightViz(container, route, progress, opts = {}) {
const {
globeRadius = 1,
dayTextureUrl = "https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg",
nightTextureUrl = night,
ribbonColors = { done: "#00ff00", todo: "#ff0000" },
clouds = false,
onDispose = () => {}
} = opts;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
container.clientWidth / container.clientHeight,
0.1,
1000
);
camera.position.z = globeRadius * 2;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
const pmrem = new THREE.PMREMGenerator(renderer);
pmrem.compileCubemapShader();
scene.environment = pmrem.fromScene(new RoomEnvironment()).texture;
const textureLoader = new THREE.TextureLoader();
const [dayTexture, nightTexture] = await Promise.all([
textureLoader.loadAsync(dayTextureUrl),
textureLoader.loadAsync(nightTextureUrl)
]);
dayTexture.colorSpace = THREE.SRGBColorSpace;
nightTexture.colorSpace = THREE.SRGBColorSpace;
const customUniforms = {
sunDirection: { value: new THREE.Vector3() }
};
const material = new THREE.MeshPhongMaterial({
map: dayTexture,
emissiveMap: nightTexture,
emissive: new THREE.Color(0xffffff),
emissiveIntensity: 1,
shininess: 10
});
material.onBeforeCompile = (shader) => {
shader.uniforms.sunDirection = customUniforms.sunDirection;
shader.vertexShader =
"varying vec3 vWorldNormal;\n" +
shader.vertexShader.replace(
"#include <begin_vertex>",
"#include <begin_vertex>\nvWorldNormal = normalize( ( modelMatrix * vec4( normal, 0.0 ) ).xyz );"
);
shader.fragmentShader =
"varying vec3 vWorldNormal;\nuniform vec3 sunDirection;\n" +
shader.fragmentShader.replace(
"#include <emissivemap_fragment>",
"#include <emissivemap_fragment>\nfloat ndot = dot( normalize(vWorldNormal), normalize(sunDirection) );\nfloat darkness = smoothstep(0.0, 0.1, -ndot);\nemissiveColor *= darkness;"
);
};
material.needsUpdate = true;
const globe = new THREE.Mesh(
new THREE.SphereGeometry(globeRadius, 64, 64),
material
);
scene.add(globe);
const light = new THREE.DirectionalLight(0xffffff, 1);
scene.add(light);
let cloudsMesh;
if (clouds) {
const cloudsTexture = await textureLoader.loadAsync(
"https://nsecluster.mit.edu/three.js-master/examples/textures/planets/earth_clouds_2048.png"
);
cloudsTexture.colorSpace = THREE.SRGBColorSpace;
cloudsMesh = new THREE.Mesh(
new THREE.SphereGeometry(globeRadius * 1.005, 64, 64),
new THREE.MeshPhongMaterial({
map: cloudsTexture,
transparent: true,
opacity: 0.8,
depthWrite: false
})
);
scene.add(cloudsMesh);
}
function latLonToVec3(lat, lon, rad = globeRadius) {
lat = (Math.PI * lat) / 180;
lon = (Math.PI * lon) / 180;
return new THREE.Vector3(
rad * Math.cos(lat) * Math.cos(lon),
rad * Math.sin(lat),
rad * Math.cos(lat) * Math.sin(lon)
);
}
function getGreatCirclePoints(start, end, numPoints = 200) {
const points = [];
const vStart = start.clone().normalize();
const vEnd = end.clone().normalize();
let dot = vStart.dot(vEnd);
dot = Math.max(Math.min(dot, 1), -1);
const theta = Math.acos(dot);
const sinTheta = Math.sin(theta);
for (let i = 0; i <= numPoints; i++) {
const t = i / numPoints;
if (sinTheta === 0) {
points.push(start.clone());
} else {
const a = Math.sin((1 - t) * theta) / sinTheta;
const b = Math.sin(t * theta) / sinTheta;
points.push(
vStart
.clone()
.multiplyScalar(a)
.add(vEnd.clone().multiplyScalar(b))
.multiplyScalar(globeRadius)
);
}
}
return points;
}
let controlPoints = [];
for (let i = 0; i < route.length - 1; i++) {
const start = latLonToVec3(route[i][0], route[i][1]);
const end = latLonToVec3(route[i + 1][0], route[i + 1][1]);
const legPoints = getGreatCirclePoints(start, end);
if (i > 0) legPoints.shift();
controlPoints.push(...legPoints);
}
const curve = new THREE.CatmullRomCurve3(
controlPoints,
false,
"catmullrom",
0.5
);
const tubularSegments = 1024;
const tubeRadius = globeRadius * 0.005;
const radialSegments = 8;
const geometry = new THREE.TubeGeometry(
curve,
tubularSegments,
tubeRadius,
radialSegments,
false
);
const positions = geometry.attributes.position;
const progArray = new Float32Array(positions.count);
const centers = [];
for (let i = 0; i <= tubularSegments; i++) {
centers.push(curve.getPoint(i / tubularSegments));
}
let totLen = 0;
const cumLens = new Array(centers.length).fill(0);
for (let i = 1; i < centers.length; i++) {
totLen += centers[i].distanceTo(centers[i - 1]);
cumLens[i] = totLen;
}
for (let i = 0; i <= tubularSegments; i++) {
const p = totLen > 0 ? cumLens[i] / totLen : 0;
for (let j = 0; j <= radialSegments; j++) {
progArray[i * (radialSegments + 1) + j] = p;
}
}
geometry.setAttribute("progress", new THREE.BufferAttribute(progArray, 1));
const ribbonMaterial = new THREE.ShaderMaterial({
uniforms: {
uProgress: { value: progress },
doneColor: { value: new THREE.Color(ribbonColors.done) },
todoColor: { value: new THREE.Color(ribbonColors.todo) }
},
vertexShader: `
attribute float progress;
varying float vProgress;
void main() {
vProgress = progress;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uProgress;
uniform vec3 doneColor;
uniform vec3 todoColor;
varying float vProgress;
void main() {
float factor = smoothstep(uProgress, uProgress + 0.005, vProgress);
gl_FragColor = vec4(mix(doneColor, todoColor, factor), 1.0);
}
`,
side: THREE.DoubleSide
});
const ribbon = new THREE.Mesh(geometry, ribbonMaterial);
scene.add(ribbon);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.enableZoom = true;
controls.autoRotateSpeed = 0.5;
let idleTimer;
controls.addEventListener("start", () => {
controls.autoRotate = false;
if (idleTimer) clearTimeout(idleTimer);
});
controls.addEventListener("end", () => {
idleTimer = setTimeout(() => {
controls.autoRotate = true;
}, 5000);
});
renderer.domElement.addEventListener("dblclick", () => {
controls.reset();
});
const clock = new THREE.Clock();
let animateFn = () => {
requestAnimationFrame(animateFn);
const delta = clock.getDelta();
const now = new Date();
const [decl, lon] = getSubsolarPoint(now);
const sunDir = latLonToVec3(decl, lon, 1);
light.position.copy(sunDir);
customUniforms.sunDirection.value.copy(sunDir);
const speed = (2 * Math.PI) / (23 * 3600 + 56 * 60);
globe.rotation.y += speed * delta;
if (cloudsMesh) cloudsMesh.rotation.y += speed * delta * 0.1;
controls.update();
renderer.render(scene, camera);
};
animateFn();
function getSubsolarPoint(date) {
const start = new Date(date.getFullYear(), 0, 0);
const diff =
date -
start +
(start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000;
const dayOfYear = Math.floor(diff / 86400000);
const decl = 23.45 * Math.sin((2 * Math.PI * (dayOfYear - 81)) / 365);
const utcHours =
date.getUTCHours() +
date.getUTCMinutes() / 60 +
date.getUTCSeconds() / 3600;
const lon = (12 - utcHours) * 15;
return [decl, lon];
}
const resizeListener = () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
};
window.addEventListener("resize", resizeListener);
return {
updateProgress: (p) => {
ribbonMaterial.uniforms.uProgress.value = Math.max(0, Math.min(1, p));
},
dispose: () => {
animateFn = () => {};
window.removeEventListener("resize", resizeListener);
dayTexture.dispose();
nightTexture.dispose();
if (clouds) cloudsMesh.material.map.dispose();
geometry.dispose();
ribbonMaterial.dispose();
material.dispose();
pmrem.dispose();
renderer.dispose();
container.removeChild(renderer.domElement);
onDispose();
}
};
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more