If you're interested, ask here.
This is the third article in a series converting starplot from Python to JavaScript. We formalize the projection math previewed in article 01 and build two azimuthal projections: stereographic and orthographic.
A merchant arrives at the camp with something wrapped in oiled leather. He unfolds it: a brass disc, covered in engraved lines and rotating overlays. An astrolabe. The younger Tuareg turns it in his hands. The entire sky — pressed flat onto metal.
How? he asks.
The elder takes the instrument. It depends on what you are willing to sacrifice. He traces a finger along the engraved circles. The sphere cannot be flattened without something being lost. Distance, or area, or shape. The question is which lie you prefer.
The Projection Problem
Every star chart is a lie. A sphere cannot be flattened onto a plane without distortion — this is a mathematical certainty proved by Gauss's Theorema Egregium (1827). A map projection is a function that takes a point on a sphere (latitude, longitude) and returns a point on a flat surface (x, y):
f(φ, λ) → (x, y)
Every projection preserves some properties at the expense of others. The three properties that matter are:
- Conformal (shape-preserving): angles between curves are preserved. Small shapes look correct. The stereographic projection is conformal.
- Equal-area: the ratio of areas on the map matches the ratio on the sphere. The Mollweide projection (article 04) is equal-area.
- Equidistant: distances from one or two points are preserved. No projection can be equidistant from all points simultaneously.
No projection can be both conformal and equal-area. This impossibility forces every cartographer and astronomer to choose.
Ptolemy (c. 100–170 CE) described three projections in his Geographia: a simple conic, a modified conic, and a perspective projection. These were the first systematic attempts to solve the flattening problem, and variants of all three are still used today. The choice between projections has always been driven by purpose: navigators need angles (conformal), surveyors need areas (equal-area), and astronomers need both depending on the task.
We start by defining the types and interfaces that all projections in this series will share:
/** A point projected onto the 2D plane, with visibility flag */
export interface ProjectedPoint {
/** x coordinate on the projection plane */
x: number;
/** y coordinate on the projection plane */
y: number;
/** Whether the point is on the visible hemisphere */
visible: boolean;
}
/**
* A map projection function.
*
* Takes a point (lat, lon) in degrees and a center point (lat0, lon0)
* and returns projected (x, y) coordinates with a visibility flag.
*
* Conventions:
* - Input angles in degrees
* - Output x, y are unitless (typically in range [-2, 2])
* - visible = false means the point is on the far side of the sphere
* (for azimuthal projections) or outside the valid domain
*/
export type ProjectionFn = (
lat: number, lon: number,
lat0: number, lon0: number,
) => ProjectedPoint;
Stereographic Projection
The elder holds the astrolabe up against the sky. Imagine a lamp at the south pole of the celestial sphere, he says. Its light shines through every star and casts a shadow on a flat plate at the equator. That shadow is the astrolabe's map.
He points to a circle engraved on the brass. Every circle on the sphere becomes a circle on the plate. That is the magic of this projection. Shapes are preserved. Angles are true. But size is not: stars near the edge are stretched far larger than those near the center.
The stereographic projection maps the sphere onto a plane by projecting from the antipodal point. A light source at the opposite pole of the projection center casts every point through the sphere onto a tangent plane. The result is conformal: angles and local shapes are preserved. And it has a unique property among projections: circles on the sphere map to circles on the plane (or straight lines, which are circles of infinite radius).
The formulas, projecting from a center point (φ₀, λ₀):
k = 2 / (1 + sin φ₀ sin φ + cos φ₀ cos φ cos(λ - λ₀))
x = k cos φ sin(λ - λ₀)
y = k (cos φ₀ sin φ − sin φ₀ cos φ cos(λ - λ₀))
When the denominator of k approaches zero, the point is near the antipode of the center — the one point the stereographic projection sends to infinity.
import type { ProjectionFn, ProjectedPoint } from './03-projections.org?name=projection-types';
const DEG = Math.PI / 180;
/**
* Stereographic projection.
*
* Conformal (angle-preserving), maps circles to circles.
* Used in astrolabes since antiquity.
*
* The projection point is the antipode of the center:
* a light at the south pole projects onto a plane
* tangent to the north pole.
*/
export const stereographic: ProjectionFn = (
lat: number, lon: number,
lat0: number, lon0: number,
): ProjectedPoint => {
const φ = lat * DEG, λ = lon * DEG;
const φ0 = lat0 * DEG, λ0 = lon0 * DEG;
const dλ = λ - λ0;
const cosφ = Math.cos(φ), sinφ = Math.sin(φ);
const cosφ0 = Math.cos(φ0), sinφ0 = Math.sin(φ0);
const cosdλ = Math.cos(dλ);
const cosC = sinφ0 * sinφ + cosφ0 * cosφ * cosdλ;
// Point near the antipode — not projectable
if (cosC < -0.9) {
return { x: 0, y: 0, visible: false };
}
const k = 2 / (1 + cosC);
const x = k * cosφ * Math.sin(dλ);
const y = k * (cosφ0 * sinφ - sinφ0 * cosφ * cosdλ);
return { x, y, visible: true };
};
The stereographic projection was known to Hipparchus (c. 190–120 BCE), who may have used it for his star charts. It became the mathematical basis of the astrolabe, the most important astronomical instrument for over a millennium. Al-Biruni (973–1048) proved the circle-preserving property rigorously. Al-Zarqali (1029–1087) of Córdoba invented the universal astrolabe, which used a clever variant of the projection that worked at any latitude — a remarkable feat of applied mathematics. Mariam al-Astrulabi (10th century, Aleppo) was among the celebrated makers of these instruments.
Drag on the visualization below to rotate the center of projection. The grid lines (parallels and meridians) are projected from the sphere to the plane. Notice how shapes near the center are accurate, but features near the edge are enormously stretched — the price of conformality.
Orthographic Projection
The orthographic projection shows the sphere as it would look from infinite distance — a photograph of a globe. Unlike the stereographic, which uses a point light source at the antipode, the orthographic uses parallel rays from infinity. The result is a view that matches what your eyes expect from a sphere, but only shows one hemisphere at a time.
This is the same math we used in article 01's draw-sphere module — a preview of the projection math we formalize here. Now we wrap it in the ProjectionFn interface so it can be used interchangeably with the stereographic and the cylindrical projections coming in article 04.
import type { ProjectionFn, ProjectedPoint } from './03-projections.org?name=projection-types';
const DEG = Math.PI / 180;
/**
* Orthographic projection.
*
* Parallel rays from infinity — the "globe photograph" view.
* Neither conformal nor equal-area, but visually intuitive.
* Only shows one hemisphere; points on the far side are invisible.
*
* Same math as article 01's projectOrtho, now conforming to ProjectionFn.
*/
export const orthographic: ProjectionFn = (
lat: number, lon: number,
lat0: number, lon0: number,
): ProjectedPoint => {
const φ = lat * DEG, λ = lon * DEG;
const φ0 = lat0 * DEG, λ0 = lon0 * DEG;
const dλ = λ - λ0;
const cosφ = Math.cos(φ), sinφ = Math.sin(φ);
const cosφ0 = Math.cos(φ0), sinφ0 = Math.sin(φ0);
const cosdλ = Math.cos(dλ);
const x = cosφ * Math.sin(dλ);
const y = cosφ0 * sinφ - sinφ0 * cosφ * cosdλ;
const visible = sinφ0 * sinφ + cosφ0 * cosφ * cosdλ > 0;
return { x, y, visible };
};
Vitruvius (c. 80–15 BCE) described the orthographic projection for sundial design in De architectura. It was used for architectural and astronomical drawings throughout the Middle Ages. In 1972, the crew of Apollo 17 took the famous "Blue Marble" photograph — an orthographic view of Earth that became the most reproduced photograph in history.
Drag to rotate the orthographic projection. Compare with the stereographic above: the orthographic preserves the intuitive "globe" appearance but clips the far hemisphere entirely, and distorts areas near the edge by compressing them.
Comparing the Two
The same stars, the same grid, two different projections — side by side. Both are centered on the same point and linked: drag either view to rotate both simultaneously. This is the fundamental choice in azimuthal projections: stereographic preserves angles (good for navigation and astrolabes), orthographic preserves visual intuition (good for illustrations and "globe" views).
| Property | Stereographic | Orthographic |
|---|---|---|
| Conformal (angles) | Yes | No |
| Equal-area | No | No |
| Visible hemisphere | All except antipode | Front hemisphere only |
| Circles → circles | Yes | No |
| Distortion pattern | Size increases away from center | Compression near edges |
| Historical use | Astrolabes, polar star charts | Globe illustrations, sundials |
Projection Grid Renderer — generic grid drawer for any ProjectionFn
A reusable function that draws a lat/lon grid on a canvas for any ProjectionFn. This is used above and will be reused in article 04 and beyond for rendering any projection with a consistent style.
import type { ProjectionFn } from './03-projections.org?name=projection-types';
import { strokeCircle } from './01-coordinates.org?name=draw-helpers';
export interface GridOpts {
ctx: CanvasRenderingContext2D;
proj: ProjectionFn;
centerLat: number;
centerLon: number;
/** Center x on canvas */
cx: number;
/** Center y on canvas */
cy: number;
/** Scale factor from projection units to pixels */
scale: number;
/** Clipping radius in pixels (0 = no clip) */
clipRadius?: number;
/** Grid line spacing for latitude */
latStep?: number;
/** Grid line spacing for longitude */
lonStep?: number;
/** Grid line color */
gridColor?: string;
/** Equator/prime meridian color */
axisColor?: string;
/** Whether to draw an outline circle at clipRadius */
outline?: boolean;
}
/**
* Draw a lat/lon grid for any projection function.
*
* Handles clipping, line drawing, and visibility checks.
* Reusable across articles.
*/
export function drawProjectionGrid(opts: GridOpts) {
const {
ctx, proj, centerLat, centerLon, cx, cy, scale,
clipRadius = 0,
latStep = 20, lonStep = 20,
gridColor = "#1e2030",
axisColor = "#3a3a5a",
outline = true,
} = opts;
const clipR2 = clipRadius * clipRadius;
function inBounds(sx: number, sy: number): boolean {
if (!clipRadius) return true;
const dx = sx - cx, dy = sy - cy;
return dx * dx + dy * dy <= clipR2;
}
// Latitude lines
for (let lat = -80; lat <= 80; lat += latStep) {
ctx.strokeStyle = lat === 0 ? axisColor : gridColor;
ctx.lineWidth = lat === 0 ? 1.2 : 0.6;
ctx.beginPath();
let started = false;
for (let lon = -180; lon <= 180; lon += 2) {
const p = proj(lat, lon, centerLat, centerLon);
if (!p.visible) { started = false; continue; }
const sx = cx + p.x * scale, sy = cy - p.y * scale;
if (!inBounds(sx, sy)) { started = false; continue; }
if (!started) { ctx.moveTo(sx, sy); started = true; }
else ctx.lineTo(sx, sy);
}
ctx.stroke();
}
// Longitude lines
for (let lon = -180; lon < 180; lon += lonStep) {
ctx.strokeStyle = lon === 0 ? axisColor : gridColor;
ctx.lineWidth = lon === 0 ? 1.2 : 0.6;
ctx.beginPath();
let started = false;
for (let lat = -90; lat <= 90; lat += 2) {
const p = proj(lat, lon, centerLat, centerLon);
if (!p.visible) { started = false; continue; }
const sx = cx + p.x * scale, sy = cy - p.y * scale;
if (!inBounds(sx, sy)) { started = false; continue; }
if (!started) { ctx.moveTo(sx, sy); started = true; }
else ctx.lineTo(sx, sy);
}
ctx.stroke();
}
if (outline && clipRadius) {
strokeCircle(ctx, cx, cy, clipRadius, "#4a4a5a", 1.5);
}
}
The merchant wraps the astrolabe back in its leather. He has seen the elder's demonstration. So the astrolabe is a stereographic lie? he asks.
The best kind of lie, the elder replies. It preserves the shape of every constellation. An archer still looks like an archer. A scorpion still curves. But these projections see from a single point. They show one part of the sky well and distort the rest. What if you want all the stars at once?
The merchant pauses. You would need to wrap the sphere in something. A cylinder, perhaps.
The elder nods. That is tomorrow's lesson.
Next
In the next article we wrap the sphere in a cylinder and an ellipse — Mercator for navigation, Mollweide for surveys — and build a registry of all four projections.
Do you like this article? I am building it with org-press, a project that allows you to create notebooks with live code examples. Check it out.
If this resonates.
I make these as gifts for births, anniversaries, moments worth keeping. I am considering opening this practice more widely, carefully and in small numbers.
Each constellation is a 25×25 cm slate plaque, laser-engraved and painted by hand in gold, boxed in carved larch. Around six hours of focused work go into each piece. Indicative price, €150 to €200.
Because each piece takes real time, I ask for a short motivation. I prefer to make them for people who genuinely care about the moment they are marking. Any profit beyond materials and time will go to a children's association, with full transparency.
If enough thoughtful interest gathers, I may open a limited number of commissions.