Metadata-Version: 2.4
Name: skygrad
Version: 1.0.0
Summary: Realistic sky gradient PNGs from latitude, longitude, and time
Project-URL: Homepage, https://github.com/Xof/skygrad
Project-URL: Source, https://github.com/Xof/skygrad
Project-URL: Issues, https://github.com/Xof/skygrad/issues
Author-email: Christophe Pettus <christophe.pettus@pgexperts.com>
License-Expression: MIT
License-File: LICENSE
Keywords: astronomy,color,gradient,oklab,png,sky,solar
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Scientific/Engineering :: Astronomy
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# skygrad

Realistic sky gradient PNGs from latitude, longitude, and time. Zenith at
the top, horizon at the bottom — one color per row, or the sun's glow
lobe across the frame when you pass `facing`. The sun's position
drives every color in the image; the sun itself is never drawn — no disc,
no moon, no stars, no clouds. Zero runtime dependencies.

![24 hours over Los Angeles](assets/contact-sheet.png)

## Usage

```python
from datetime import datetime
import skygrad

# One-shot PNG bytes
png = skygrad.render(lat=34.05, lon=-118.24,
                     when=datetime(1994, 6, 21, 19, 30),
                     width=512, height=1024)

# Or write straight to a file
skygrad.write_png("dusk.png", lat=34.05, lon=-118.24,
                  when=datetime(1994, 6, 21, 19, 30))

# Layered access: colors without pixels
sky = skygrad.Sky.at(lat=34.05, lon=-118.24, when=datetime(1994, 6, 21, 19, 30))
sky.solar_elevation          # degrees; e.g. streetlights on below -4
sky.color(0.0)               # (r, g, b) at the zenith; 1.0 is the horizon
sky.stops()                  # [(u, (r, g, b)), ...] — build a CSS gradient
```

## Facing (azimuthal glow)

Pass a compass direction and the image gains the sun's glow lobe —
brightest toward the sun, fading with angular distance, gone when you
face away. Without `facing`, output is byte-identical to skygrad 0.1.

```python
sky = skygrad.Sky.at(lat=34.05, lon=-118.24, when=datetime(1994, 6, 21, 19, 40))
sky.solar_azimuth                      # ≈ 303° at this dusk — west-northwest
toward = sky.png(facing=sky.solar_azimuth)            # sunset glow, centered
away = sky.png(facing=(sky.solar_azimuth + 180) % 360)  # plain dusk gradient
wide = sky.png(facing=0.0, fov=360.0, width=2048)     # full panorama strip
```

`facing` is degrees clockwise from true north (any finite value,
normalized mod 360; radians callers use `math.degrees()`). `fov` is the
horizontal span in degrees, default 90, valid (0, 360] — and requires
`facing`.

A facing render is computed per pixel, so it's capped at ~4 million pixels
(`width × height ≤ 2048×2048`); go larger and it raises `ValueError`. Tile,
shrink, or drop `facing` (the plain vertical gradient is per-row and has no
such cap). Requires Python 3.11+.

![24 hours facing the sunset](assets/contact-sheet-facing.png)

## Time semantics (read this)

`when` is a full datetime — the **date matters** (June and December at the
same hour give completely different skies).

- **Timezone-aware** datetimes are honored exactly.
- **Naive** datetimes mean **local mean solar time** at the given
  longitude: sundial time, where 12:00 puts the sun on the meridian.
  This is deliberate — it makes "local" meaningful for fictional places —
  but it can differ from civil wall-clock time by an hour or more. If you
  want wall-clock local time, attach a tzinfo.

## Determinism

Same inputs → same decoded pixels for a given `skygrad.MODEL_VERSION`
(and byte-identical PNGs within one zlib build). `MODEL_VERSION` bumps
whenever any input would render different colors, so golden tests in
consumers break deliberately, never silently. Compare decoded pixels,
not compressed bytes.

## Development

    uv sync
    uv run ruff check . && uv run ruff format --check .
    uv run mypy
    uv run pytest -q

Golden skies are pinned under `tests/golden/`; regenerate deliberately
with `uv run pytest --regen-golden` and bump `MODEL_VERSION` if colors
changed. Regeneration rewrites all ten goldens, but on one zlib build the
untouched ones are byte-identical rewrites — so a deliberate single-color
change surfaces as exactly the goldens it should. The module map and
invariants live in `ARCHITECTURE.md`, and the design
rationale (theory of operation) in `THEORY.md`.
