skytone 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +171 -2
- package/dist/index.d.ts +295 -0
- package/dist/index.js +1135 -0
- package/package.json +55 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- Sun and moon positions, sunrise and sunset, polar day and night.
|
|
8
|
+
- Palettes keyed to the sun's altitude, with separate morning and evening tints.
|
|
9
|
+
- A watercolour plate painter: graded washes, sweeps, wisps, low-sun curtains, rain, snow, fog, storms and land at dusk.
|
|
10
|
+
- `SkyRenderer`: WebGL2 with cubic B-spline upsampling, watercolour texture, easing between plates, offscreen pausing and context-loss recovery.
|
|
11
|
+
- Open-Meteo forecast and place search, with interpolation between hours.
|
|
12
|
+
- PNG, JPEG and SVG stills.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Skytone contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,172 @@
|
|
|
1
|
-
#
|
|
1
|
+
# skytone
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A living watercolour sky for any interface. Give it a place and a moment, and it paints the sky there: the sun and moon where they really are, the weather from the live forecast, rendered as soft washes by a small WebGL2 renderer. No dependencies.
|
|
4
|
+
|
|
5
|
+
It's the engine behind [Skytone Studio](https://skytone.site).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install skytone
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { SkyRenderer, skyAt, presetConditions } from 'skytone';
|
|
15
|
+
|
|
16
|
+
const canvas = document.querySelector('canvas')!;
|
|
17
|
+
const sky = new SkyRenderer(canvas); // sizes itself to the canvas and pauses offscreen
|
|
18
|
+
|
|
19
|
+
const { plate } = skyAt({
|
|
20
|
+
latitude: 19.08,
|
|
21
|
+
longitude: 72.88,
|
|
22
|
+
time: Date.now(),
|
|
23
|
+
conditions: presetConditions('partly'),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
sky.setPlate(plate);
|
|
27
|
+
sky.start();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Give the canvas a size with CSS (`width: 100%; height: 100%`). The sky covers it at any aspect ratio.
|
|
31
|
+
|
|
32
|
+
## With the live forecast
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { SkyRenderer, conditionsAt, fetchForecast, skyAt } from 'skytone';
|
|
36
|
+
|
|
37
|
+
const latitude = 51.51, longitude = -0.13;
|
|
38
|
+
const sky = new SkyRenderer(canvas);
|
|
39
|
+
const forecast = await fetchForecast(latitude, longitude); // Open-Meteo, no key needed
|
|
40
|
+
|
|
41
|
+
function paint() {
|
|
42
|
+
const { conditions } = conditionsAt(forecast, Date.now());
|
|
43
|
+
const { plate } = skyAt({ latitude, longitude, time: Date.now(), conditions });
|
|
44
|
+
sky.setPlate(plate, { storm: conditions.storm }); // changes ease in over a second
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
paint();
|
|
48
|
+
sky.start();
|
|
49
|
+
setInterval(paint, 60_000);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Text on top
|
|
53
|
+
|
|
54
|
+
`skyAt` also says which text colour reads best on the current sky:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const { foreground } = skyAt({ latitude, longitude, time: Date.now(), conditions });
|
|
58
|
+
document.body.dataset.foreground = foreground; // 'light' or 'dark'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## React
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { useEffect, useRef } from 'react';
|
|
65
|
+
import { SkyRenderer, type Plate } from 'skytone';
|
|
66
|
+
|
|
67
|
+
export function Sky({ plate }: { plate: Plate }) {
|
|
68
|
+
const ref = useRef<HTMLCanvasElement>(null);
|
|
69
|
+
const sky = useRef<SkyRenderer | null>(null);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
sky.current = new SkyRenderer(ref.current!);
|
|
73
|
+
sky.current.start();
|
|
74
|
+
return () => sky.current?.destroy();
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
useEffect(() => sky.current?.setPlate(plate), [plate]);
|
|
78
|
+
|
|
79
|
+
return <canvas ref={ref} style={{ width: '100%', height: '100%' }} aria-hidden />;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## How it works
|
|
84
|
+
|
|
85
|
+
1. **Sun and moon.** Positions come from low-precision astronomical formulas. The tests check sunrise and sunset against published times to within three minutes, including polar night.
|
|
86
|
+
2. **A plate.** The scene is painted into an 80×45 colour grid, the way a watercolourist paints a sky: a graded wash, then soft brush strokes laid wet into it. The palette follows the sun's *altitude*, not the clock, so 6 pm in Oslo in December and 6 pm in Mumbai in June are different skies. Clouds come in as broad sweeps, high wisps, and at low sun, curtains rising from the horizon. Rain, snow, fog, storms and land at dusk are layered on top.
|
|
87
|
+
3. **The renderer.** A WebGL2 shader upsamples the plate with a cubic B-spline, which never draws an edge. It adds a gentle current, faint smoky texture and paper grain, and eases between plates so the sky morphs instead of cutting.
|
|
88
|
+
|
|
89
|
+
Painting a plate is pure and deterministic: the same place, time, conditions and seed always give the same sky.
|
|
90
|
+
|
|
91
|
+
## API
|
|
92
|
+
|
|
93
|
+
### Painting a sky
|
|
94
|
+
|
|
95
|
+
| Export | What it does |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `skyAt(options, out?)` | Paints the sky. Returns `{ plate, scene, phase, foreground }`. |
|
|
98
|
+
| `SkyOptions` | `{ latitude, longitude, time, conditions, seed?, viewAzimuth? }`. `time` is epoch ms. `seed` varies the composition (defaults to one per place). `viewAzimuth` is the compass direction the view faces (defaults to facing the equator). |
|
|
99
|
+
| `sceneFor(options)` | The sun, moon and conditions for a moment, without painting. |
|
|
100
|
+
| `paintPlate(scene, out?)` | Paints a scene into a plate. Pass `out` to reuse memory. |
|
|
101
|
+
| `phaseOf(altitude)` | `'night' \| 'astronomical' \| 'nautical' \| 'civil' \| 'golden' \| 'day'`. |
|
|
102
|
+
| `seedFor(latitude, longitude)` | The default seed for a place. |
|
|
103
|
+
| `createPlate()`, `PLATE_W`, `PLATE_H` | A plate is a `Float32Array` of sRGB values, row 0 at the top. |
|
|
104
|
+
| `plateAverage(plate, v0?, v1?)` | Average colour, optionally of a horizontal band (0 bottom, 1 top). |
|
|
105
|
+
| `foregroundFor(plate)` | `'light'` or `'dark'` text for this sky. |
|
|
106
|
+
|
|
107
|
+
### Rendering
|
|
108
|
+
|
|
109
|
+
`new SkyRenderer(canvas, options?)`
|
|
110
|
+
|
|
111
|
+
| Option | Default | |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| `renderScale` | `0.5` | Resolution relative to CSS pixels × devicePixelRatio. The sky is soft, so half resolution looks the same and costs a quarter. |
|
|
114
|
+
| `size` | | A fixed `{ width, height }` drawing buffer (for exports). Turns off automatic resizing. |
|
|
115
|
+
| `look` | `DEFAULT_LOOK` | Partial `Look`. |
|
|
116
|
+
| `preserveDrawingBuffer` | `false` | Keep frames readable (for exports). |
|
|
117
|
+
|
|
118
|
+
| Method | |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `setPlate(plate, { instant?, storm? })` | Shows a plate. Changes ease in; large jumps soften briefly. `storm` (0..1) adds distant flashes. |
|
|
121
|
+
| `setLook(look)` | `{ watercolor, motion, softness, grain }`, each 0..1 (`softness` 0..2). |
|
|
122
|
+
| `start()`, `stop()` | The animation loop. It also pauses on its own when the canvas is offscreen or the tab is hidden. |
|
|
123
|
+
| `renderFrame(seconds)` | Draws one frame at an explicit time (exports, video). |
|
|
124
|
+
| `setSize(width, height)` | Sets the drawing buffer when `size` was given. |
|
|
125
|
+
| `destroy({ releaseContext? })` | Frees GPU resources. Pass `releaseContext: true` for a throwaway canvas. |
|
|
126
|
+
|
|
127
|
+
`SkyRenderer` needs WebGL2, which every current browser has. It recovers from a lost GPU context.
|
|
128
|
+
|
|
129
|
+
### Weather
|
|
130
|
+
|
|
131
|
+
| Export | |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| `presetConditions(preset, intensity?)` | Conditions without a network call. Presets: `clear`, `partly`, `cloudy`, `overcast`, `rain`, `snow`, `fog`, `storm`. `intensity` is 0..1. |
|
|
134
|
+
| `fetchForecast(latitude, longitude, options?)` | Hourly forecast from Open-Meteo: the past 7 days and the next 16. |
|
|
135
|
+
| `conditionsAt(forecast, time)` | `{ conditions, known }`, interpolated between hours. Outside the forecast it returns a typical sky with `known: false`. |
|
|
136
|
+
| `searchPlaces(query, options?)` | Place search (Open-Meteo geocoding). Returns `{ name, region, country, latitude, longitude, timezone }[]`. |
|
|
137
|
+
| `placeQueryFromTimezone(tz?)` | A city name from the device timezone, for a first guess with no permission prompt. |
|
|
138
|
+
| `lerpConditions(a, b, t)`, `describe(conditions)`, `CONDITION_PRESETS` | Helpers. |
|
|
139
|
+
|
|
140
|
+
`Conditions` is `{ cloud, cloudLow, cloudHigh, rain, snow, fog, storm, windSpeed, windDirection, temperature }`, each normalised to 0..1 except wind (m/s, degrees) and temperature (°C or `null`).
|
|
141
|
+
|
|
142
|
+
The forecast functions accept `{ baseUrl, geocodingUrl, apiKey, signal }` to point at Open-Meteo's commercial endpoint or cancel a request.
|
|
143
|
+
|
|
144
|
+
### Sun and moon
|
|
145
|
+
|
|
146
|
+
| Export | |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `sunPosition(time, latitude, longitude)` | `{ altitude, azimuth, rising }` in degrees. Azimuth is clockwise from north. |
|
|
149
|
+
| `moonPosition(time, latitude, longitude)` | `{ altitude, azimuth, fraction }`, where `fraction` is the illuminated part (0 new, 1 full). |
|
|
150
|
+
| `sunEvents(dayStart, latitude, longitude)` | `{ sunrise, sunset, always }` within 24 hours of `dayStart`. `always` is `'up'` or `'down'` for polar day and night. |
|
|
151
|
+
|
|
152
|
+
### Images
|
|
153
|
+
|
|
154
|
+
| Export | |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| `renderStill(plate, { width, height, look?, time? })` | Renders a plate at full resolution into a new canvas. |
|
|
157
|
+
| `stillBlob(plate, options, 'image/png' \| 'image/jpeg', quality?)` | The same, as a `Blob`. |
|
|
158
|
+
| `plateToSVG(plate, width, height)` | A resolution-independent SVG of the washes (no paper texture or grain). |
|
|
159
|
+
|
|
160
|
+
## Where it runs
|
|
161
|
+
|
|
162
|
+
- **Browsers:** everything. The renderer and image functions need a DOM and WebGL2.
|
|
163
|
+
- **Node 18+, Deno, Bun, workers:** the sky model (`skyAt`, `paintPlate`, sun, moon, palettes) and the weather functions. These need no DOM, so you can paint plates on a server and render them on the client.
|
|
164
|
+
|
|
165
|
+
## Data and credits
|
|
166
|
+
|
|
167
|
+
- Weather and place search use [Open-Meteo](https://open-meteo.com/), whose data is licensed CC BY 4.0. Credit Open-Meteo wherever you show its weather. Its free API is for non-commercial use; commercial apps need an Open-Meteo plan (`baseUrl` and `apiKey`) or their own provider, which works with `presetConditions` or your own `Conditions`.
|
|
168
|
+
- The sun and moon formulas follow those used by [SunCalc](https://github.com/mourner/suncalc) by Vladimir Agafonkin.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
interface BodyPosition {
|
|
2
|
+
/** Degrees above the horizon. Negative below. */
|
|
3
|
+
altitude: number;
|
|
4
|
+
/** Degrees clockwise from north. */
|
|
5
|
+
azimuth: number;
|
|
6
|
+
}
|
|
7
|
+
interface SunPosition extends BodyPosition {
|
|
8
|
+
/** True while the sun climbs (morning). */
|
|
9
|
+
rising: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface MoonPosition extends BodyPosition {
|
|
12
|
+
/** Illuminated fraction, 0 (new) to 1 (full). */
|
|
13
|
+
fraction: number;
|
|
14
|
+
}
|
|
15
|
+
declare function sunPosition(time: number, lat: number, lon: number): SunPosition;
|
|
16
|
+
declare function moonPosition(time: number, lat: number, lon: number): MoonPosition;
|
|
17
|
+
interface SunEvents {
|
|
18
|
+
sunrise: number | null;
|
|
19
|
+
sunset: number | null;
|
|
20
|
+
/** Polar day or night: the sun never crosses the horizon in this window. */
|
|
21
|
+
always: 'up' | 'down' | null;
|
|
22
|
+
}
|
|
23
|
+
/** Sunrise and sunset inside [start, start + 24h), found by scanning altitude. */
|
|
24
|
+
declare function sunEvents(start: number, lat: number, lon: number): SunEvents;
|
|
25
|
+
|
|
26
|
+
type RGB = [number, number, number];
|
|
27
|
+
declare function rgbToOklab([r, g, b]: RGB): RGB;
|
|
28
|
+
declare function oklabToRgb([L, a, b]: RGB): RGB;
|
|
29
|
+
/** Perceptual blend. Sunsets pass through pink, not grey. */
|
|
30
|
+
declare function mixOklab(a: RGB, b: RGB, t: number): RGB;
|
|
31
|
+
declare const luminance: (c: RGB) => number;
|
|
32
|
+
declare const toHex: (c: RGB) => string;
|
|
33
|
+
|
|
34
|
+
interface Palette {
|
|
35
|
+
zenith: RGB;
|
|
36
|
+
mid: RGB;
|
|
37
|
+
horizon: RGB;
|
|
38
|
+
/** Deeper sky pooled at the top of the frame. */
|
|
39
|
+
deep: RGB;
|
|
40
|
+
/** Pale sky showing between masses. */
|
|
41
|
+
highlight: RGB;
|
|
42
|
+
/** The lit side of cloud. */
|
|
43
|
+
light: RGB;
|
|
44
|
+
/** The shaded side of cloud. */
|
|
45
|
+
shadow: RGB;
|
|
46
|
+
/** Warm dust near the horizon. */
|
|
47
|
+
warm: RGB;
|
|
48
|
+
/** Land silhouette at dusk and night. */
|
|
49
|
+
ground: RGB;
|
|
50
|
+
/** Light around the sun. */
|
|
51
|
+
glow: RGB;
|
|
52
|
+
}
|
|
53
|
+
declare function paletteAt(altitude: number, rising?: boolean): Palette;
|
|
54
|
+
|
|
55
|
+
/** Everything the painter needs to know about the weather, normalised to 0..1. */
|
|
56
|
+
interface Conditions {
|
|
57
|
+
/** Total cloud cover. */
|
|
58
|
+
cloud: number;
|
|
59
|
+
/** Low cloud: heavy masses near the horizon. */
|
|
60
|
+
cloudLow: number;
|
|
61
|
+
/** High cloud: thin wisps up top. */
|
|
62
|
+
cloudHigh: number;
|
|
63
|
+
rain: number;
|
|
64
|
+
snow: number;
|
|
65
|
+
fog: number;
|
|
66
|
+
storm: number;
|
|
67
|
+
/** Metres per second. */
|
|
68
|
+
windSpeed: number;
|
|
69
|
+
/** Degrees the wind blows *from*, clockwise from north. */
|
|
70
|
+
windDirection: number;
|
|
71
|
+
/** Celsius, when known. */
|
|
72
|
+
temperature: number | null;
|
|
73
|
+
}
|
|
74
|
+
type ConditionPreset = 'clear' | 'partly' | 'cloudy' | 'overcast' | 'rain' | 'snow' | 'fog' | 'storm';
|
|
75
|
+
declare const CONDITION_PRESETS: {
|
|
76
|
+
id: ConditionPreset;
|
|
77
|
+
name: string;
|
|
78
|
+
}[];
|
|
79
|
+
/** A preset at a given intensity (0..1). Intensity 0.5 is the typical case. */
|
|
80
|
+
declare function presetConditions(preset: ConditionPreset, intensity?: number): Conditions;
|
|
81
|
+
declare function lerpConditions(a: Conditions, b: Conditions, t: number): Conditions;
|
|
82
|
+
/** A believable sky for dates outside any forecast: some cloud, nothing falling. */
|
|
83
|
+
declare const SEASONAL_CONDITIONS: Conditions;
|
|
84
|
+
/** Which preset best describes some conditions (for labels). */
|
|
85
|
+
declare function describe(c: Conditions): ConditionPreset;
|
|
86
|
+
|
|
87
|
+
interface Place {
|
|
88
|
+
name: string;
|
|
89
|
+
region?: string;
|
|
90
|
+
country?: string;
|
|
91
|
+
latitude: number;
|
|
92
|
+
longitude: number;
|
|
93
|
+
/** IANA zone, e.g. "Asia/Kolkata". */
|
|
94
|
+
timezone: string;
|
|
95
|
+
}
|
|
96
|
+
interface Forecast {
|
|
97
|
+
latitude: number;
|
|
98
|
+
longitude: number;
|
|
99
|
+
timezone: string;
|
|
100
|
+
/** Hour starts, epoch ms. */
|
|
101
|
+
times: number[];
|
|
102
|
+
hours: Conditions[];
|
|
103
|
+
/** Raw readings for display, aligned with `times`. */
|
|
104
|
+
readings: {
|
|
105
|
+
cloudCover: number;
|
|
106
|
+
precipitation: number;
|
|
107
|
+
snowfall: number;
|
|
108
|
+
code: number;
|
|
109
|
+
windKmh: number;
|
|
110
|
+
temperature: number;
|
|
111
|
+
}[];
|
|
112
|
+
}
|
|
113
|
+
interface OpenMeteoOptions {
|
|
114
|
+
/** Override for the commercial endpoint, e.g. https://customer-api.open-meteo.com */
|
|
115
|
+
baseUrl?: string;
|
|
116
|
+
geocodingUrl?: string;
|
|
117
|
+
apiKey?: string;
|
|
118
|
+
pastDays?: number;
|
|
119
|
+
forecastDays?: number;
|
|
120
|
+
signal?: AbortSignal;
|
|
121
|
+
}
|
|
122
|
+
/** WMO weather code + readings → normalised conditions. */
|
|
123
|
+
declare function conditionsFromReading(r: {
|
|
124
|
+
cloud: number;
|
|
125
|
+
low: number;
|
|
126
|
+
high: number;
|
|
127
|
+
precip: number;
|
|
128
|
+
rain: number;
|
|
129
|
+
snowfall: number;
|
|
130
|
+
code: number;
|
|
131
|
+
visibility: number;
|
|
132
|
+
wind: number;
|
|
133
|
+
windDir: number;
|
|
134
|
+
temp: number | null;
|
|
135
|
+
}): Conditions;
|
|
136
|
+
declare function fetchForecast(latitude: number, longitude: number, opts?: OpenMeteoOptions): Promise<Forecast>;
|
|
137
|
+
/** Conditions at any instant: interpolated inside the forecast, seasonal outside it. */
|
|
138
|
+
declare function conditionsAt(forecast: Forecast | null, time: number): {
|
|
139
|
+
conditions: Conditions;
|
|
140
|
+
known: boolean;
|
|
141
|
+
index: number;
|
|
142
|
+
};
|
|
143
|
+
declare function searchPlaces(query: string, opts?: OpenMeteoOptions): Promise<Place[]>;
|
|
144
|
+
/**
|
|
145
|
+
* A place guessed from the device timezone alone: no permission prompt, no network.
|
|
146
|
+
* "Asia/Kolkata" → search "Kolkata". Callers refine it with geolocation if granted.
|
|
147
|
+
*/
|
|
148
|
+
declare function placeQueryFromTimezone(tz?: string): string | null;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A plate is the whole sky as a small colour grid, painted the way a watercolourist paints
|
|
152
|
+
* a sky: a graded wash, then soft brush strokes laid wet into it. The renderer upsamples it
|
|
153
|
+
* with a smooth cubic filter. Row 0 is the top of the sky. Values are sRGB 0..255.
|
|
154
|
+
*/
|
|
155
|
+
declare const PLATE_W = 80;
|
|
156
|
+
declare const PLATE_H = 45;
|
|
157
|
+
type Plate = Float32Array;
|
|
158
|
+
declare const createPlate: () => Plate;
|
|
159
|
+
interface SceneInput {
|
|
160
|
+
/** Epoch ms. Drives the slow drift of cloud across the frame. */
|
|
161
|
+
time: number;
|
|
162
|
+
sun: SunPosition;
|
|
163
|
+
moon: MoonPosition;
|
|
164
|
+
conditions: Conditions;
|
|
165
|
+
/** Compass direction the view faces, degrees. */
|
|
166
|
+
viewAzimuth: number;
|
|
167
|
+
/** Varies the composition. Same seed, same sky. */
|
|
168
|
+
seed: number;
|
|
169
|
+
}
|
|
170
|
+
/** Paint a scene into `out` (or a new plate). Pure: same input, same plate. */
|
|
171
|
+
declare function paintPlate(scene: SceneInput, out?: Plate): Plate;
|
|
172
|
+
/** Average colour of the plate (or a horizontal band of it, v from 0 bottom to 1 top). */
|
|
173
|
+
declare function plateAverage(plate: Plate, v0?: number, v1?: number): RGB;
|
|
174
|
+
/** Light or dark text reads best on this sky. */
|
|
175
|
+
declare function foregroundFor(plate: Plate): 'light' | 'dark';
|
|
176
|
+
|
|
177
|
+
type Phase = 'night' | 'astronomical' | 'nautical' | 'civil' | 'golden' | 'day';
|
|
178
|
+
declare function phaseOf(altitude: number): Phase;
|
|
179
|
+
/** A stable seed for a place, so a city keeps its own character. */
|
|
180
|
+
declare function seedFor(latitude: number, longitude: number): number;
|
|
181
|
+
interface SkyOptions {
|
|
182
|
+
latitude: number;
|
|
183
|
+
longitude: number;
|
|
184
|
+
time: number;
|
|
185
|
+
conditions: Conditions;
|
|
186
|
+
seed?: number;
|
|
187
|
+
/** Defaults to facing the equator, so the sun arcs left to right. */
|
|
188
|
+
viewAzimuth?: number;
|
|
189
|
+
}
|
|
190
|
+
declare function sceneFor(o: SkyOptions): SceneInput;
|
|
191
|
+
interface SkyState {
|
|
192
|
+
scene: SceneInput;
|
|
193
|
+
plate: Plate;
|
|
194
|
+
phase: Phase;
|
|
195
|
+
foreground: 'light' | 'dark';
|
|
196
|
+
}
|
|
197
|
+
declare function skyAt(o: SkyOptions, out?: Plate): SkyState;
|
|
198
|
+
|
|
199
|
+
interface Look {
|
|
200
|
+
/** Speed of the slow ambient drift. 0 holds the sky still. */
|
|
201
|
+
motion: number;
|
|
202
|
+
/** Extra softness, in plate cells. */
|
|
203
|
+
softness: number;
|
|
204
|
+
/** Film grain, 0..1. */
|
|
205
|
+
grain: number;
|
|
206
|
+
/** Watercolour, 0..1: flowing wet-in-wet edges, pigment pooling and paper. 0 is a pure colour field. */
|
|
207
|
+
watercolor: number;
|
|
208
|
+
}
|
|
209
|
+
declare const DEFAULT_LOOK: Look;
|
|
210
|
+
interface RendererOptions {
|
|
211
|
+
/** Resolution relative to CSS pixels × devicePixelRatio. The sky is soft, so 0.5 looks identical. */
|
|
212
|
+
renderScale?: number;
|
|
213
|
+
/** Fixed drawing-buffer size (exports). Disables automatic resizing. */
|
|
214
|
+
size?: {
|
|
215
|
+
width: number;
|
|
216
|
+
height: number;
|
|
217
|
+
};
|
|
218
|
+
look?: Partial<Look>;
|
|
219
|
+
/** Keep the drawing buffer so frames can be read back (exports). */
|
|
220
|
+
preserveDrawingBuffer?: boolean;
|
|
221
|
+
}
|
|
222
|
+
declare class SkyRenderer {
|
|
223
|
+
readonly canvas: HTMLCanvasElement | OffscreenCanvas;
|
|
224
|
+
private gl;
|
|
225
|
+
private u;
|
|
226
|
+
private tex;
|
|
227
|
+
private program;
|
|
228
|
+
private buffer;
|
|
229
|
+
private shown;
|
|
230
|
+
private target;
|
|
231
|
+
private rgba;
|
|
232
|
+
private hasPlate;
|
|
233
|
+
private morph;
|
|
234
|
+
private storm;
|
|
235
|
+
private look;
|
|
236
|
+
private opts;
|
|
237
|
+
private raf;
|
|
238
|
+
private last;
|
|
239
|
+
private clock;
|
|
240
|
+
private visible;
|
|
241
|
+
private running;
|
|
242
|
+
private observers;
|
|
243
|
+
private lost;
|
|
244
|
+
private onVisibility;
|
|
245
|
+
constructor(canvas: HTMLCanvasElement | OffscreenCanvas, opts?: RendererOptions);
|
|
246
|
+
private init;
|
|
247
|
+
private onLost;
|
|
248
|
+
private onRestored;
|
|
249
|
+
private observe;
|
|
250
|
+
private fit;
|
|
251
|
+
setSize(width: number, height: number): void;
|
|
252
|
+
setLook(look: Partial<Look>): void;
|
|
253
|
+
/** Show a plate. Changes ease in; large jumps soften briefly, the way the sky morphs. */
|
|
254
|
+
setPlate(plate: Plate, opts?: {
|
|
255
|
+
instant?: boolean;
|
|
256
|
+
storm?: number;
|
|
257
|
+
}): void;
|
|
258
|
+
private upload;
|
|
259
|
+
private step;
|
|
260
|
+
private flash;
|
|
261
|
+
/** Draw one frame at an explicit time (seconds). Used for exports. */
|
|
262
|
+
renderFrame(timeSec: number): void;
|
|
263
|
+
private draw;
|
|
264
|
+
/** Advance easing toward the target instantly (exports, tests). */
|
|
265
|
+
settle(): void;
|
|
266
|
+
start(): void;
|
|
267
|
+
stop(): void;
|
|
268
|
+
private wake;
|
|
269
|
+
/**
|
|
270
|
+
* Stop and free GPU resources. The canvas keeps its context so a new renderer can reuse it
|
|
271
|
+
* (React remounts do exactly that). Pass `releaseContext` for throwaway canvases, such as exports.
|
|
272
|
+
*/
|
|
273
|
+
destroy(opts?: {
|
|
274
|
+
releaseContext?: boolean;
|
|
275
|
+
}): void;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface StillOptions {
|
|
279
|
+
width: number;
|
|
280
|
+
height: number;
|
|
281
|
+
look?: Partial<Look>;
|
|
282
|
+
storm?: number;
|
|
283
|
+
/** Seconds on the drift clock. */
|
|
284
|
+
time?: number;
|
|
285
|
+
}
|
|
286
|
+
/** Render a plate at full resolution into a fresh canvas. */
|
|
287
|
+
declare function renderStill(plate: Plate, o: StillOptions): HTMLCanvasElement;
|
|
288
|
+
declare function stillBlob(plate: Plate, o: StillOptions, type: 'image/png' | 'image/jpeg', quality?: number): Promise<Blob>;
|
|
289
|
+
/**
|
|
290
|
+
* A resolution-independent SVG: the plate as a grid of colour cells under a Gaussian blur.
|
|
291
|
+
* Edge cells are repeated outward so the blur never darkens the frame's edges.
|
|
292
|
+
*/
|
|
293
|
+
declare function plateToSVG(plate: Plate, width: number, height: number): string;
|
|
294
|
+
|
|
295
|
+
export { type BodyPosition, CONDITION_PRESETS, type ConditionPreset, type Conditions, DEFAULT_LOOK, type Forecast, type Look, type MoonPosition, type OpenMeteoOptions, PLATE_H, PLATE_W, type Palette, type Phase, type Place, type Plate, type RGB, type RendererOptions, SEASONAL_CONDITIONS, type SceneInput, type SkyOptions, SkyRenderer, type SkyState, type StillOptions, type SunEvents, type SunPosition, conditionsAt, conditionsFromReading, createPlate, describe, fetchForecast, foregroundFor, lerpConditions, luminance, mixOklab, moonPosition, oklabToRgb, paintPlate, paletteAt, phaseOf, placeQueryFromTimezone, plateAverage, plateToSVG, presetConditions, renderStill, rgbToOklab, sceneFor, searchPlaces, seedFor, skyAt, stillBlob, sunEvents, sunPosition, toHex };
|