Skip to content
Back to Blog
· By Swaroop Shere

Thicker Air, Longer Exposures: How eclipseClick Computes Atmospheric Extinction

The August 2026 eclipse puts totality about 10° above the horizon in northern Spain — and barely 2° up in Mallorca. At those altitudes you are shooting the corona through five to twenty times more atmosphere than someone under a high Sun. Here is exactly how eclipseClick quantifies that loss and folds it into your capture sequence.

ExposureAtmospheric ExtinctionAirmassAstrophotographyMathematics

The Problem

Every published eclipse exposure table — Fred Espenak's included — assumes an unobstructed view of the corona. In practice you are looking at it through the atmosphere, and the atmosphere absorbs and scatters light on the way through. Straight up, the loss is small enough to ignore. Near the horizon it is not: the same corona seen at 5° altitude arrives roughly ten times fainter than the same corona at the zenith, because your line of sight has crossed ten times as much air.

This is atmospheric extinction, and it is a first-order effect for eclipse photography rather than a refinement. Espenak's own guidance is to open up about a stop for a low-altitude eclipse. eclipseClick does that arithmetic for you, from your actual location and your actual eclipse, and applies it phenomenon by phenomenon.

The whole calculation is four steps, each of which is a single published formula:

Sun altitude airmass magnitudes lost photographic stops shutter speed
Besselian elements · Kasten–Young · extinction coefficient k · log2(10)/2.5 · Espenak's Q formula

1. Sun Altitude, for Free

Extinction depends on how high the Sun is at the moment of each contact — which means the calculation needs the Sun's altitude at C1, C2, maximum, C3 and C4, not a single number for the day. eclipseClick does not call out to an ephemeris library for this. The Besselian elements it already ships for every eclipse (see How eclipseClick Computes Eclipse Contact Times) carry the Sun's declination d and the Greenwich hour angle μ as polynomials in time. Those are exactly the two quantities the standard altitude formula needs.

H = μ + λEω · ΔT
sin h = sin φ · sin d + cos φ · cos d · cos H

φ and λE are the observer's geodetic latitude and east longitude, and ω · ΔT is the Earth-rotation term that reconciles the elements' TT timescale with the observer's UT — the same correction the contact solver applies, and the subject of a bug we fixed in July. Once the contact solver has produced C1–C4 as timestamps, each is converted back to Besselian time and run through the formula above.

A deliberate approximation: this is the true, unrefracted, geodetic-based altitude — accurate to roughly 0.2°. Atmospheric refraction lifts the apparent Sun by about 0.5° near the horizon, so at very low altitudes the geometric value is slightly pessimistic. That is the right side to err on for an exposure estimate, and 0.2° of altitude error is worth a few hundredths of a stop above 10°. Contact times are computed to far tighter tolerances; this helper exists only to drive the exposure correction.

2. Airmass: Kasten–Young

Airmass (X) is the length of your line of sight through the atmosphere, expressed as a multiple of the straight-up path. Looking at the zenith, X = 1 by definition. The naive model is X = 1/sin h, which treats the atmosphere as a flat slab — fine at high altitude, but it diverges to infinity at the horizon, where the real answer is about 38.

eclipseClick uses the Kasten–Young (1989) empirical fit, the standard choice in observational astronomy, which stays accurate to within about 1% all the way down to the horizon:

X = 1 / [ sin(h) + 0.50572 · (hdeg + 6.07995)−1.6364 ]

The mixed units are not a typo and they matter if you ever reimplement this: the sine takes the altitude in radians, while the correction term takes the same altitude in degrees. Kasten and Young fitted those three constants against tabulated atmospheric data in degrees; converting the second term to radians silently changes the model. Here is the implementation, verbatim:

exposure-calculator.ts
export function extinctionStopsForAltitude(
  altitudeDeg: number,
  visibilityKm: number = 100
): number {
  if (altitudeDeg <= 0) return 0
  const hRad = (altitudeDeg * Math.PI) / 180
  const airMass =
    1.0 /
    (Math.sin(hRad) + 0.50572 * Math.pow(altitudeDeg + 6.07995, -1.6364))
  const extinctionCoeff = 0.25 + (100.0 - visibilityKm) * 0.005
  const magnitudes = airMass * extinctionCoeff
  return magnitudes * MAG_TO_STOPS
}

The altitudeDeg <= 0 guard returns zero stops rather than a nonsense value: if the Sun is below the horizon there is no exposure to correct, and the wizard surfaces that as an error instead.

3. How Much Light Each Airmass Costs

Airmass tells you how much atmosphere you are looking through. The extinction coefficient k tells you how much light one airmass of it eats, in astronomical magnitudes. Total loss is simply the product:

Δm = X · k

k is the one genuinely local, genuinely unpredictable term in the chain. It depends on aerosols, humidity, dust and altitude above sea level, and it can double between a clear morning and a hazy afternoon at the same site. eclipseClick derives it from a visibility figure using a linear rule anchored at clear sky:

k = 0.25 + (100 − visibilitykm) × 0.005

At the shipped default of 100 km visibility this gives k = 0.25 mag/airmass — a good clear-sky observing site. A hazy urban 20 km day would be k = 0.65, nearly three times worse.

Why the default is optimistic, on purpose: the visibility term is a function parameter, not a setting in the app — every call site today uses the 100 km clear-sky default. Eclipse photographers travel to clear sites and check forecasts obsessively; defaulting to a hazy-urban k would over-lengthen every exposure by roughly a stop at typical altitudes, and an overexposed corona loses inner detail permanently. An under-correction, by contrast, is recoverable in RAW and is covered by the brackets. So the number eclipseClick shows you is deliberately a lower bound, and the wizard says so in as many words: on a hazy or Saharan-dust day the true loss can be two to three times larger, which is why the low-altitude warning tells you to bracket wider rather than to trust the figure.

4. Magnitudes → Photographic Stops

Astronomers measure brightness in magnitudes, where 5 magnitudes is a factor of 100 in flux (so one magnitude is 1001/5 ≈ 2.512×). Photographers measure it in stops, where one stop is a factor of exactly 2. Converting is one constant:

stops = Δm × log2(10) / 2.5 = Δm × 1.3288

Put the three steps together and you get the table below — the actual output of extinctionStopsForAltitude() at the shipped default:

Sun altitude Airmass X Loss (mag) Stops
90° (zenith)1.000.250.33
60°1.150.290.38
45°1.410.350.47
30°1.990.500.66
20°2.900.730.96
15°3.810.951.27
10°5.591.401.86
6.861.712.28
10.312.583.42
19.434.866.46

Two things stand out. First, even a high Sun costs a third of a stop — small, but not nothing when you are chasing inner-corona detail. Second, the curve is brutally non-linear below 15°: dropping from 10° to 5° costs another 1.6 stops, and the last couple of degrees cost more than everything above them combined. That is why the correction exists at all, and why site choice matters so much for eclipses like August 2026.

5. Where the Correction Lands

eclipseClick computes every shutter speed from Espenak's exposure equation, where Q is a brightness exponent tabulated per phenomenon — 12 for Bailey's beads, 5 for the inner corona, 0 for the outer corona (see the partial-phase calibration post for how Q works and how the solar-filter case is derived):

Espenak, uncorrected
t = f2 / (ISO × 2Q)
with extinction
t = f2 / (ISO × 2(QE))

The entire correction is one subtraction: E stops of extinction come straight off the brightness exponent, because Q is already a base-2 exponent — a dimmer subject and a subject seen through more air are the same thing to the formula. Higher extinction → lower effective Q → longer exposure. When E is zero or absent the expression is bit-for-bit the original, which is what keeps every uncorrected script byte-identical to before the feature existed.

What happens next matters just as much. The ideal exposure is a continuous number; a camera only has rungs. Two things then act on it, in order:

  1. Snap to the body's own ladder. When the connected camera has reported its real shutter choice list, the corrected time is snapped to the nearest rung of that list in log space — so the value written into the script is one the body has already said it can set. Otherwise a built-in 1 EV ladder is used and the result is clamped to the reported fastest/slowest.
  2. Check against the body's range. A separate predicate asks whether the corrected ideal exposure fell off the end of the ladder entirely — that, and not mere rounding, is what lights the "camera limits applied" banner in the wizard.

The ordering has a consequence worth internalising: a fractional-stop correction can either vanish or snap a full rung, depending on where the uncorrected value already sat. Real output, at f/8 and ISO 100, inner corona (Q = 5):

Site Totality altitude Extinction Inner corona Outer corona
Dallas, 2024 Apr 08 64.6° 0.37 stops 1/60 → 1/30 1/2 → 1
Oviedo, 2026 Aug 12 10.0° 1.86 stops 1/60 → 1/15 1/2 → 2
Palma, 2026 Aug 12 2.2° 6.09 stops 1/60 → 1 1/2 → 30

Dallas is the instructive case: 0.37 stops is a small correction, but the uncorrected ideal exposure sat close to the midpoint between two rungs, so it still moves one whole step. Palma is the cautionary one — it sits inside the umbral path, but at 2.2° the model asks for a 30-second frame on the outer corona, roughly a third of that site's 96 seconds of totality. The arithmetic is right; what it is really telling you is that the outer corona is not realistically photographable when the Sun is two degrees up, and that the honest plan there is a wide-field shot of a low, red, eclipsed Sun over the Mediterranean rather than a corona bracket.

6. Per Contact, Not One Number

An eclipse takes two to three hours from C1 to C4, and the Sun moves. At the 2026 Spain eclipse it is still climbing at C1 and well down toward the horizon by C4 — the partial phases at either end can differ by a stop or more of extinction from each other. A single whole-eclipse correction would be wrong at both ends.

So eclipseClick computes a small table, keyed by reference contact:

ScriptWizard.tsx
const stopsByContact = {
  C1:  alts.c1 != null ? extinctionStopsForAltitude(alts.c1) : undefined,
  C2:  alts.c2 != null ? extinctionStopsForAltitude(alts.c2) : undefined,
  C3:  alts.c3 != null ? extinctionStopsForAltitude(alts.c3) : undefined,
  C4:  alts.c4 != null ? extinctionStopsForAltitude(alts.c4) : undefined,
  MAX: extinctionStopsForAltitude(alts.max),
}

Every phenomenon eclipseClick can schedule is already anchored to one of those contacts — that is how its capture window is defined in the first place. Bailey's beads at second contact, prominences and both corona zones and earthshine all hang off C2; their C3 counterparts hang off C3; the first partial series hangs off C1 and the closing one off C3; the C4 contact shot off C4. When the timeline builder computes a shutter speed for a phenomenon, it looks up that phenomenon's own reference event and applies the matching correction:

timeline-builder.ts
const q = getEffectiveQ(w.phenomenon, filterNd)
const extinctionStops = extinctionStopsByContact?.[w.referenceEvent]

pref.exposureOverride = {
  shutterSpeed: computeShutterSpeed(fNumber, iso, q, limits, extinctionStops),
  aperture: fNumber,
  iso: iso,
  ...
}

Note the two corrections stacking cleanly on one line. getEffectiveQ handles the solar filter — for partial phases and contacts it replaces the bare-sun Q with a filter-adjusted one derived from the filter's ND — and the extinction term is applied on top of whatever Q that produced. A filtered partial phase at low Sun gets both. The two are independent in the code and in the physics, and the unit tests assert exactly that composition.

For bracketed phenomena the correction lands on the line's base exposure. A symmetric bracket steps ±EV around that corrected pivot, so the whole ladder moves with it. A Q-walk bracket — eclipseClick's preferred corona bracket, which fires one frame per named Espenak table row (Q = 11, 9, 7, 5…) rather than blind ±EV steps — defines each frame by its own row instead of by an offset from the pivot.

7. Auto, On, Off

The correction is not unconditionally applied. It is governed by a three-state setting — 'auto', 'on', 'off' — where 'auto' is the untouched default and the other two are pinned by clicking the wizard's Apply correction toggle.

camera-store.ts
export const AUTO_EXTINCTION_ALTITUDE_DEG = 10

export function isExtinctionActive(
  mode: ExtinctionMode,
  driverAltitudeDeg: number | null | undefined
): boolean {
  if (mode === 'on') return true
  if (mode === 'off') return false
  return (
    driverAltitudeDeg != null &&
    driverAltitudeDeg > 0 &&
    driverAltitudeDeg < AUTO_EXTINCTION_ALTITUDE_DEG
  )
}

In 'auto' the decision is made by the driver altitude: the lowest Sun altitude during totality, i.e. min(C2, C3), falling back to the altitude at maximum for partial and annular eclipses where no totality exists. Totality is when the corona — the faintest, least forgiving subject in the sequence — is actually on the sensor, so it is the phase that should decide. Taking the minimum rather than the mean is deliberately conservative: it errs toward correcting.

At exactly 10° the correction is 1.86 stops and it climbs steeply below that — well past the point where a straight-from-the-table exposure produces a visibly thin corona. Above it, the loss is under two stops and largely inside what your brackets already cover, so the default leaves your exposures alone unless you ask. Explicit 'on' and 'off' ignore altitude entirely — if you have told the app what you want, it does not second-guess you at a different location.

Upgrade note: this setting used to be a plain boolean persisted as '1' / '0'. On load, a stored '1' is mapped to an explicit 'on' — someone who deliberately enabled it keeps it enabled — while a legacy '0' or an absent value becomes 'auto', so users who simply never found the toggle inherit the smart default.

8. What You Actually See

The stops figure is computed the moment you pick an eclipse and a location — independently of whether the correction is switched on, so the wizard can tell you what you would gain by enabling it. It drives a five-tier banner on the location step, graded by predicted stops rather than by altitude alone:

Sun below horizon during totality

Not observable from this location. The toggle is disabled — there is no exposure to correct.

Below 10° — low totality altitude

Correction on by default. Warns that the estimate is clear-sky, that haze can be 2–3× worse, that seeing is turbulent this low, and that blue light is selectively lost (a real, uncorrected reddening of the corona).

≥ 1.5 stops — extinction significant

Recommends enabling it; suggests bracketing a stop or two longer on a hazy day.

≥ 0.7 stops — optional

Offered as a refinement for more faithful corona brightness.

Below 0.7 stops — negligible

Says so plainly, rather than nudging you toward a setting that will not change your photographs.

The banner copy also changes tense with the toggle state — "extinction correction is on, lengthening the corona exposures by ≈1.9 stops" versus "turn on Apply correction to…" — because a static "consider enabling this" reads as advice to skip it when it is already doing its job.

One more piece of plumbing worth mentioning, because it is the kind of thing that quietly ruins a wizard: when you change f-number or ISO on the camera-setup step, every exposure has to be recomputed — with the extinction table reapplied — without stamping over shutter speeds you edited by hand. eclipseClick keeps a snapshot of the last auto-computed set and only rewrites values that still match it, so a hand-tuned Bailey's beads exposure survives an ISO change while everything you did not touch tracks the new settings and the correct per-contact correction.

9. What This Model Does Not Do

Being explicit about the edges of a model is more useful than pretending it has none. This one is a deliberately simple, deliberately conservative estimator, and these are its known boundaries:

It is grey, not chromatic

Extinction is strongly wavelength-dependent — Rayleigh scattering removes blue far faster than red, which is why a low Sun is orange. eclipseClick applies a single achromatic stops figure to the exposure. The reddening is real and is left to your white balance and RAW processing; the wizard warns about it rather than modelling it.

One k for everybody

The extinction coefficient is fixed at the clear-sky 0.25 mag/airmass default; site elevation, humidity and aerosol load are not modelled, and there is no visibility control in the UI today. Treat the number as a floor. Sites well above sea level see less extinction than the model says; hazy or dusty days see considerably more.

Geometric altitude, no refraction

The altitude driving the model is the true unrefracted one, good to about 0.2°. Near the horizon the Sun appears roughly half a degree higher than it geometrically is, so the model slightly over-estimates the loss there — the safe direction, and negligible at the altitudes where the correction is subtle.

Your camera has the last word

The correction is applied before snapping and clamping, so a coarse whole-stop shutter ladder can partly absorb a fractional correction, and a body that runs out of shutter will clamp — flagged in the wizard, not applied silently.

The script audit scores without it

The "how close is this script to Espenak's table" audit on the script page runs at zero extinction, because a saved script's metadata records the observing site's elevation in metres, not the Sun's altitude in degrees — there is nothing to drive the correction from at that point. The wizard's own coverage panel, which does have the timeline in hand, applies it normally.

None of these change the headline: if you are shooting an eclipse with the Sun low — northern Spain in August 2026, and anyone chasing totality near sunrise or sunset — you are losing one to three stops that no published exposure table accounts for. eclipseClick works out how many, applies them to the right phenomena at the right contacts, and tells you when the answer is "not enough to bother with."

Plan for the sky you'll actually have

Download eclipseClick, pick your site, and see the extinction estimate for your eclipse.