Building a Living Model of the Earth
Human beings have always sought to model their world, and that impulse is central to how and why we build software. But making a model of the entire earth is the ultimate cartographic challenge: globemakers have to lay triangular gores on a sphere without overlap, while mapmakers toil with trade-offs between preserving shape and preserving size. When we set out to build the new Stripe.com landing experience, we wanted a visual metaphor that captured both the global scale of the internet economy and the reality that so much remains untapped—a 3D model inviting exploration and communicating nuanced detail.
A sphere seemed the right answer over a flat map for three reasons. It occupies less than 20% of the screen area of a 2D map, it preserves relative size, shape and orientation of land masses more truthfully, and spinning it in an interactive scene is inherently more satisfying than dragging a map. After weighing the option of hiring GlobeKit, we chose to build it ourselves, committing to a project that would help us grow as developers and designers in the process.
Choosing Our Tools
WebGL and its GLSL shaders can feel like sorcery, with the simplest triangle requiring 50+ lines of code. None of us were 3D artists, and we quickly realized writing our own engine was out of the question. We turned to Three.js, a user-friendly library that abstracts away much of WebGL’s complexity behind documented APIs. Since Three.js is GPU-accelerated, it can process continuous visual change without overloading the CPU even on mid-range consumer hardware. Finding the boundary between an animated, living globe and a crashed browser became our biggest engineering constraint—but only one of several we faced.
Three Layers, One Globe
One glance suggests a single surface, but the globe composes three distinct layers. The base is a semi-transparent sphere with roughly 50 segments on each axis, representing the oceans. Resting on that is a sphere textured with tens of thousands of glimmering dots that define the continents. Finally, arcs of color travel from one country to another—from a pulsing disc in a region where Stripe processes payments to another country where businesses use the service—wrapping themselves around these two spheres. The arcs are data: when Stripe expands to a new country, a new disc and route are born.
Throughout development, we encountered challenges that each demanded novel solutions. Any team aiming to build their own interactive globe—or any complex 3D web object—benefits from understanding these struggles.
Filling the Surface with Dots
The outermost sphere has a simple job: define continents with a grid of dots. But the dots needed to meet two design requirements. They had to be evenly spaced from pole to pole, and each one had to be individually animatable. We tried several approaches before settling on the right one.
We first considered a static image of evenly spaced dots. This is easy to create, but the dots visually fuse together near the poles where the circumference of each latitudinal row shrinks. Moreover, a bitmap texture gives no ability to animate individual dots without an overly complex shader. We improved this slightly by generating a texture of nearly 80,000 unevenly spaced dots, with wider spacing on rows near the top and bottom to avoid clumping. This looked better, but we still couldn't animate the dots—and converting an SVG drawn sphere into textured triangles discouraged us further.
The most natural route was to generate dots as real geometry in three-dimensional space. Using a sine function to distribute dots by latitude, we created rows that had anywhere from zero to 500 dots, plotted each, and rotated them via lookAt so they faced the sphere's center. But the dot counts jumped awkwardly between rows with harsh breaks in the longitudinal columns, producing an unnatural effect.
The answer came from nature in the form of a sunflower pattern. Like its seed geometry, the dots form continuously spiraling spirals around the sphere instead of straight rows. Using the setFromSphericalCoords method, it gave us both uniform coverage and the individual animation capabilities we needed. To generate 60,000 tiny "hexagons" (which render as circles), we looped over a count, calculating the polar phi and azimuthal theta angles for each, and positioning them around a sphere of radius 600.
- Equally spaced dot image: simple but pins dots at the poles and can't support individual animation.
- Unevenly spaced dot image: gave nearly uniform spacing visually but remained a static bitmap.
- Generated sunflower pattern: achieved the design ideal with true geometry, perfect spacing, and per-dot animation.
Generated dots also came with another benefit: we could check each dot's position against the countryIds to identify which country border it fell within, laying the groundwork for animating specific parts of the world.
// Create 60000 tiny dots and spiral them around the sphere.
const DOT_COUNT = 60000;
// A hexagon with a radius of 2 pixels looks like a circle
const dotGeometry = new THREE.CircleGeometry(2, 5);
// The XYZ coordinate of each dot
const positions = [];
// A random identifier for each dot
const rndId = [];
// The country border each dot falls within
const countryIds = [];
const vector = new THREE.Vector3();
for (let i = DOT_COUNT; i >= 0; i--) {
const phi = Math.acos(-1 + (2 * i) / DOT_COUNT);
const theta = Math.sqrt(DOT_COUNT * Math.PI) * phi;
// Pass the angle between this dot an the Y-axis (phi)
// Pass this dot’s angle around the y axis (theta)
// Scale each position by 600 (the radius of the globe)
vector.setFromSphericalCoords(600, phi, theta);
dotGeometry.lookAt(vector);
// Move the dot to the newly calculated position
dotGeometry.translate(vector.x, vector.y, vector.z);
}
Grouping thousands of dots by country
An earlier version of the interactive globe grouped dots by country to show where Stripe operates. That feature was eventually turned off for the landing page, but the technique used to implement it is worth examining. The goal was twofold: render dots only inside the borders of countries where Stripe is live, and treat those dots as a single group that could be animated together.
One teammate, fresh off a gaming project involving shaders, suggested encoding a PNG with a unique color for each live country. Using canvas's built-in getImageData, the team could read the color of every pixel in the image, match that color against an array of country colors, and tag each dot with a countryId before the coordinates were passed to the shader. With that ID in hand, any country's dots could be isolated and animated as a unit—changing color, opacity, or position in z-space.
The obvious worry was the math required to animate 60,000 individual dots 60 times per second. The earth's surface is mostly water, though. By generating geometry only for countries where Stripe is live, the dot count dropped from 60,000 to roughly 20,000, and far less data needed to be pushed to the vertex shader. That freed rendering budget for other animations.
The implementation assigned a hex color to each ISO country code in a COUNTRY_MAPPING array. After loading the color-coded image, the code sampled each pixel and, if no color data existed, skipped the dot entirely. For pixels with color data, vertex faces were created and each vertex received the matching countryId. A helper converted RGB values back to hex and looked up the country ID in the mapping.
Each country where Stripe is live is given a unique color for identification.
~
Connecting the globe with animation
With dots in place and grouped by country, the next task was animating the globe so visitors could see how business connects across regions. The team wanted the globe to spin, dots to twinkle, and arcs to bend between countries to represent transaction patterns—all while letting users rotate the globe themselves.
A new teammate, who had previously engineered the scrolling for the Pencil by 53 site, contributed animations for undulating, aurora-like lights. He also made the globe rotate on page load and respond to user scroll. The team handled the twinkling dots and arcs with a custom fragment shader, guided heavily by thebookofshaders.com, while the remaining motion ran in vanilla JavaScript. requestAnimationFrame drives the arcs, globe spin, and color shifts.
Arcs were drawn as curved tubes with a 0.5px radius and 8 sides. Each curve was broken into 44 segments, with roughly 3,000 vertices total. The animation works by calling setDrawRange to reveal only the first vertex, then increasing that range over 2.5 seconds until the full arc is visible. Each arc interpolates between two geographic coordinates, using D3 to follow the great-circle route and adding control points at a height proportional to half the distance between endpoints.
~
Keeping the globe at 60fps
Performance requirements were set early: all animation and scrolling had to run at 60fps to match common display refresh rates. If the team couldn't hit that target, the fallback was a static image. Thanks to GPU-accelerated WebGL, that fallback was never needed.
Mobile support was initially ruled out—scrolling plus 3D animation seemed too demanding. As the team learned more about GPU capabilities, expectations rose. Most WebGL functionality works on mobile without modification. Minor adjustments were needed, though: during scroll, animations pause and events are debounced with Lodash to keep the spin smooth.
A few days before launch, testing on laptops without dedicated GPUs revealed trouble powering a fullscreen globe on 5K displays. The team cycled through every possible bottleneck: simplifying geometry, stopping animations, disabling lights and shaders. Nothing fixed it. On a whim, they disabled the antialias parameter on the WebGL renderer. That single change solved the high-resolution display problem and improved animation and scroll smoothness on every device, even those already hitting 60fps.
Removing antialiasing might seem like it would cause visible pixelation. In practice, it only affects geometry edges. Image textures, gradients, and lighting remain smooth. The arcs show minimal pixelation, but the performance gain justified the tradeoff.
Scroll-based rotation was handled by throttling the scroll handler to run at most every 16 milliseconds. A small constant—SCROLL_EPSILON of 0.0016—determined how much the globe rotated per pixel of scroll. Once the user scrolled past the globe's position on the page, all animation stopped and the globe element was translated off-screen.
~
Building the whole product, from day one
Countries are human-defined divisions of the earth's surface, and the same principle applies to organizations: how teams are structured determines how work gets done. The globe project benefited from a close working relationship between designers and engineers who respected both pixels and code. That rapport avoided the usual pipeline friction where design pressure meets engineering compromise at the last minute.
Rather than assembling modular components into a final product, the team built a functional prototype with a sphere on screen as early as possible—even in its ugliest form. This approach let them evaluate the globe's real behavior, separate functionality from polish, and resist the urge to cut quality just to make it operational. Improvements unfolded gradually through iteration, with the globe appearing in mockups, keynotes, websites, and a brief appearance in the Stripe Dashboard since its first version in 2019.
Time itself measures the earth's rotation—60 units per minute, 60 minutes per hour. As the product covers more of the globe's surface, the team plans to keep smoothing rough edges, connecting distant dots, and keeping the world spinning at 60 frames per second.



