skygrad turns a latitude, a longitude, and a datetime into a PNG of the sky at that place and moment: zenith at the top, horizon at the bottom, one color per row. The sun’s position sets every color in the image, and the sun is never drawn. No disc, no moon, no stars, no clouds; just the gradient the sky actually is at that instant. It has zero runtime dependencies, and it is deterministic to the pixel.

1import skygrad
2from datetime import datetime
3
4png = skygrad.render(lat=34.05, lon=-118.24,
5 when=datetime(1994, 6, 21, 19, 30),
6 width=512, height=1024)

That is dusk over Los Angeles on a particular evening, returned as bytes. If you want the colors instead of the pixels, Sky.at(...) hands them back as data: sky.solar_elevation in degrees, sky.color(0.0) for the zenith and sky.color(1.0) for the horizon, or sky.stops() for a list you can drop straight into a CSS gradient.

Palette over physics

The colors don’t come from a physical simulation of atmospheric scattering. They come from a hand-tuned set of per-elevation anchors that skygrad blends in Oklab, the perceptually uniform color space, so the midpoints between stops look right instead of going muddy the way naive RGB interpolation does. It’s art direction, not Rayleigh scattering: a small console of hex values that says what the sky looks like at each sun elevation, and a renderer faithful to that console rather than to the equations. The solar geometry underneath is real, good to a few tenths of a degree for the modern era; the palette on top of it is a deliberate aesthetic choice.

Determinism is the other fixed point. The same inputs give the same decoded pixels for a given MODEL_VERSION, with no RNG anywhere; the dithering that keeps the gradient from banding is positional Bayer, not noise. The PNG encoder is hand-rolled from the standard library (8-bit RGB, an sRGB chunk, no timestamps, no metadata), which is why the whole thing installs with nothing else attached. MODEL_VERSION bumps whenever a change would alter a rendered color, so anyone pinning golden images breaks on purpose rather than silently.

What “when” means

when is a full datetime, and the date is not decorative: June and December at the same clock time are entirely different skies. A timezone-aware datetime is honored exactly. A naive one is read as local mean solar time at that longitude, which is to say sundial time, where noon puts the sun on the meridian. That can differ from civil wall-clock time by an hour or more, and it’s deliberate, because it makes “local” mean something for a place that doesn’t have a timezone, like one you invented.

The glow lobe

By default you get the vertical gradient. Pass facing, a compass bearing, and the sun’s glow lobe appears across the frame: brightest toward the sun, fading with angular distance, gone once you turn away from it. fov sets how wide a slice you’re looking at, up to a full 360-degree panorama strip. That path is computed per pixel rather than per row, so it’s capped at about four million pixels; the plain vertical gradient has no such limit. Face away from the sun, or render at night, and you’re back to a sky byte-for-byte identical to the no-facing path.

pip install skygrad (or uv add skygrad); it’s pure Python, standard library only, wants Python 3.11 or newer, and is MIT licensed. A small program that does exactly one thing, which is the most satisfying kind of side project to have.