@codincod/ornament 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/LICENSE +661 -0
- package/README.md +52 -2
- package/package.json +35 -3
- package/src/biome.ts +157 -0
- package/src/caustics.ts +81 -0
- package/src/census.ts +262 -0
- package/src/cephalopods.ts +1041 -0
- package/src/cloud.ts +308 -0
- package/src/crags.ts +974 -0
- package/src/drift.ts +435 -0
- package/src/eels.ts +667 -0
- package/src/fish_shape.ts +410 -0
- package/src/flora.ts +1955 -0
- package/src/flow_field.ts +491 -0
- package/src/jellies.ts +1794 -0
- package/src/nemos.ts +606 -0
- package/src/paint/bed.ts +231 -0
- package/src/paint/frame.ts +204 -0
- package/src/paint/life.ts +611 -0
- package/src/paint/light.ts +132 -0
- package/src/paint/pen.ts +225 -0
- package/src/paint/rock.ts +325 -0
- package/src/paint/sea.ts +809 -0
- package/src/paint/shade.ts +109 -0
- package/src/paint/sky.ts +377 -0
- package/src/paint/specimen.ts +300 -0
- package/src/paint/trace.ts +164 -0
- package/src/passers.ts +730 -0
- package/src/perlin.ts +168 -0
- package/src/plenty.ts +65 -0
- package/src/puffers.ts +510 -0
- package/src/rays.ts +225 -0
- package/src/reef.ts +945 -0
- package/src/relics.ts +451 -0
- package/src/seabed.ts +525 -0
- package/src/shoal.ts +1658 -0
- package/src/sizes.ts +86 -0
- package/src/sun.ts +500 -0
- package/src/swarm.ts +587 -0
- package/src/visitors.ts +1936 -0
- package/src/walkers.ts +861 -0
package/src/sizes.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How big a thing is drawn, given how big it is and how far off it is.
|
|
3
|
+
*
|
|
4
|
+
* One statement of scale for the whole scene, because the alternative is what
|
|
5
|
+
* this water had: every file picking a share of the box's width that looked
|
|
6
|
+
* right on its own, and a boat coming out the size of a turtle. Nothing catches
|
|
7
|
+
* that. Each number is defensible where it is written and the picture is still
|
|
8
|
+
* wrong, because the thing being got wrong lives between the files.
|
|
9
|
+
*
|
|
10
|
+
* So each animal and each hull is written down in metres, at a distance in
|
|
11
|
+
* metres, and this turns the pair into a share of the box's width. A reader who
|
|
12
|
+
* doubts a size can look the animal up.
|
|
13
|
+
*
|
|
14
|
+
* ## Why it is not the angle a thing subtends
|
|
15
|
+
*
|
|
16
|
+
* Drawn strictly to the angle, this scene has no picture in it. A reef fish is
|
|
17
|
+
* a fifth of a metre and a green turtle is well over a metre, so the fish comes
|
|
18
|
+
* out at a handful of pixels: a speck, and a speck will not hold a drawing. Go
|
|
19
|
+
* the other way and size the fish to read, and a submarine is wider than the
|
|
20
|
+
* screen.
|
|
21
|
+
*
|
|
22
|
+
* The root is the compression that fixes it. Doubling a real length still draws
|
|
23
|
+
* bigger, so the order of things is never wrong and a shark never comes out
|
|
24
|
+
* under a turtle; but the ends are pulled in, so the smallest thing in the
|
|
25
|
+
* water is still a shape and the largest still fits. It is the same bargain a
|
|
26
|
+
* map makes with a river's width, and for the same reason: what is being kept
|
|
27
|
+
* is which is bigger, not by how much.
|
|
28
|
+
*
|
|
29
|
+
* ## The distances are the honest part
|
|
30
|
+
*
|
|
31
|
+
* A boat is not small. It is far: the surface is a long way up from a seabed,
|
|
32
|
+
* and a hull crossing it is a shape on the ceiling rather than a thing you are
|
|
33
|
+
* swimming beside. Writing that down as a distance rather than as a smaller
|
|
34
|
+
* boat is what keeps the next boat from being wrong again.
|
|
35
|
+
*
|
|
36
|
+
* ## Where the animals are written down
|
|
37
|
+
*
|
|
38
|
+
* Here is the sum and nothing else. What each thing measures stays beside the
|
|
39
|
+
* thing, because a table of lengths in one file is a table nobody updates when
|
|
40
|
+
* an animal changes: `shoal.ts` carries the fish, `visitors.ts` the large ones
|
|
41
|
+
* crossing, `jellies.ts` the bells, `walkers.ts` the floor, `cephalopods.ts`
|
|
42
|
+
* the squid and the octopus, `passers.ts` the hulls. Two of them are deliberate
|
|
43
|
+
* exceptions and say so where they stand. A fish inside a flock is measured
|
|
44
|
+
* against the flock (`swarm.ts`) and a clownfish against its anemone
|
|
45
|
+
* (`nemos.ts`), because in both cases the two things are at one distance and
|
|
46
|
+
* nothing between them wants compressing. Marine snow (`drift.ts`) is a texture
|
|
47
|
+
* rather than an animal.
|
|
48
|
+
*
|
|
49
|
+
* How many of each there are is the other half, and it is `census.ts`.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* How much of the box's width a thing a metre off would take.
|
|
54
|
+
*
|
|
55
|
+
* The one number the scene is tuned by. It was fixed by the shark, which is the
|
|
56
|
+
* animal this water was drawn around: at four and a half metres and eight off,
|
|
57
|
+
* it comes out where it always was, and everything else moved to meet it.
|
|
58
|
+
*/
|
|
59
|
+
const AT_ARM = 0.15
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* How far off the things that swim past you are.
|
|
63
|
+
*
|
|
64
|
+
* They are in the same water, so they share a distance, and what still tells
|
|
65
|
+
* them apart is `Visitor.depth`, which is the nearer and further of this.
|
|
66
|
+
*/
|
|
67
|
+
export const SWIMMING_OFF = 8
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* How far off the floor of the picture is.
|
|
71
|
+
*
|
|
72
|
+
* Further than the animals crossing the water, because the bed is under them
|
|
73
|
+
* and behind them: what a reader is looking at is a stretch of ground that
|
|
74
|
+
* starts a little beyond arm's reach and goes back until the haze takes it.
|
|
75
|
+
* Everything that lives down there shares it, which is the point of writing it
|
|
76
|
+
* once. A crab, a starfish and an octopus at the mouth of a den are the sizes
|
|
77
|
+
* they are against each other because they are the same distance away, and a
|
|
78
|
+
* crab that came out smaller than the stones it walks between is a crab that
|
|
79
|
+
* was given a number of its own.
|
|
80
|
+
*/
|
|
81
|
+
export const BED_OFF = 10
|
|
82
|
+
|
|
83
|
+
/** A share of the box's width, for something this long and this far away. */
|
|
84
|
+
export function drawnAt(metres: number, off: number): number {
|
|
85
|
+
return AT_ARM * Math.sqrt(Math.max(metres, 0) / Math.max(off, 0.001))
|
|
86
|
+
}
|
package/src/sun.ts
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the sun is.
|
|
3
|
+
*
|
|
4
|
+
* The ornament draws a window into water, and water at four in the morning is
|
|
5
|
+
* not water at noon. This works out which of those it is, from the clock and
|
|
6
|
+
* from where the clock says it is being read, and hands back the two numbers
|
|
7
|
+
* every renderer needs: how light it is, and how near the light is to a sunset.
|
|
8
|
+
*
|
|
9
|
+
* The moon is here too, and it is the real one: where it is tonight, and what
|
|
10
|
+
* shape the sun has left of it. It rises later each night, is a different
|
|
11
|
+
* sliver each night, and comes back to the same shape after 29.53 of them.
|
|
12
|
+
*
|
|
13
|
+
* It lives in this package rather than beside the site's hook because the desktop
|
|
14
|
+
* background wants the same sky. `seascape` bundles this package into its
|
|
15
|
+
* renderer, so a moon that rises on the site at
|
|
16
|
+
* half past nine rises on the desktop at half past nine, from these lines. That
|
|
17
|
+
* is the whole reason the two are one piece of code.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here asks for a permission and nothing leaves the machine. The
|
|
20
|
+
* position is estimated: longitude from the time zone's current offset from
|
|
21
|
+
* UTC, which is 15 degrees an hour and already correct for daylight saving, and
|
|
22
|
+
* latitude from the zone's own name. That is accurate to a few hundred
|
|
23
|
+
* kilometres, which moves sunset by minutes. Nobody is navigating by this.
|
|
24
|
+
*
|
|
25
|
+
* Daylight saving needs no handling of its own. The altitude is computed from
|
|
26
|
+
* absolute time, so a clock that jumps an hour changes nothing about where the
|
|
27
|
+
* sun is; only the offset used for longitude moves, and it moves with the zone.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const RAD = Math.PI / 180
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Days since J2000.0, the epoch every term below is expressed in.
|
|
34
|
+
*/
|
|
35
|
+
const J2000_OFFSET_DAYS = 10957.5
|
|
36
|
+
const DAY_MS = 86_400_000
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Earth's axial tilt.
|
|
40
|
+
*/
|
|
41
|
+
const OBLIQUITY = 23.4397 * RAD
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Latitude by time-zone region, in degrees, north positive.
|
|
45
|
+
*
|
|
46
|
+
* A region is a continent and a continent is not a latitude, so these are the
|
|
47
|
+
* middle of where people in that region actually live, and they are what a zone
|
|
48
|
+
* the table below has never heard of falls back to. The error that shows is a
|
|
49
|
+
* reader put in the wrong hemisphere: it inverts the seasons, and a scene that
|
|
50
|
+
* is dark at eight in the evening in Buenos Aires in January would be wrong by
|
|
51
|
+
* hours rather than by minutes.
|
|
52
|
+
*/
|
|
53
|
+
const REGION_LATITUDE: Record<string, number> = {
|
|
54
|
+
Africa: 5,
|
|
55
|
+
America: 38,
|
|
56
|
+
Antarctica: -70,
|
|
57
|
+
Arctic: 78,
|
|
58
|
+
Asia: 30,
|
|
59
|
+
Atlantic: 35,
|
|
60
|
+
Australia: -30,
|
|
61
|
+
Europe: 50,
|
|
62
|
+
Indian: -15,
|
|
63
|
+
Pacific: -20,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Where a zone's people actually are, north and east positive.
|
|
68
|
+
*
|
|
69
|
+
* A zone is named after a city and a city is not its meridian: Brussels keeps
|
|
70
|
+
* the meridian of Prague, which is ten degrees and forty minutes of moonset. So
|
|
71
|
+
* the zones most readers are in say where they are, and everything else falls
|
|
72
|
+
* back to the middle of its region and the middle of its offset.
|
|
73
|
+
*/
|
|
74
|
+
const ZONE_PLACE: Record<string, readonly [number, number]> = {
|
|
75
|
+
"Africa/Cairo": [30, 31.2],
|
|
76
|
+
"Africa/Casablanca": [33.6, -7.6],
|
|
77
|
+
"Africa/Johannesburg": [-26.2, 28],
|
|
78
|
+
"Africa/Lagos": [6.5, 3.4],
|
|
79
|
+
"Africa/Nairobi": [-1.3, 36.8],
|
|
80
|
+
"America/Argentina/Buenos_Aires": [-34.6, -58.4],
|
|
81
|
+
"America/Bogota": [4.7, -74.1],
|
|
82
|
+
"America/Chicago": [41.9, -87.6],
|
|
83
|
+
"America/Denver": [39.7, -105],
|
|
84
|
+
"America/Halifax": [44.6, -63.6],
|
|
85
|
+
"America/Lima": [-12, -77],
|
|
86
|
+
"America/Los_Angeles": [34.1, -118.2],
|
|
87
|
+
"America/Mexico_City": [19.4, -99.1],
|
|
88
|
+
"America/New_York": [40.7, -74],
|
|
89
|
+
"America/Phoenix": [33.4, -112.1],
|
|
90
|
+
"America/Santiago": [-33.4, -70.7],
|
|
91
|
+
"America/Sao_Paulo": [-23.5, -46.6],
|
|
92
|
+
"America/Toronto": [43.7, -79.4],
|
|
93
|
+
"America/Vancouver": [49.3, -123.1],
|
|
94
|
+
"Asia/Bangkok": [13.8, 100.5],
|
|
95
|
+
"Asia/Dubai": [25.2, 55.3],
|
|
96
|
+
"Asia/Hong_Kong": [22.3, 114.2],
|
|
97
|
+
"Asia/Jakarta": [-6.2, 106.8],
|
|
98
|
+
"Asia/Jerusalem": [31.8, 35.2],
|
|
99
|
+
"Asia/Karachi": [24.9, 67],
|
|
100
|
+
"Asia/Kolkata": [22.6, 88.4],
|
|
101
|
+
"Asia/Manila": [14.6, 121],
|
|
102
|
+
"Asia/Seoul": [37.6, 127],
|
|
103
|
+
"Asia/Shanghai": [31.2, 121.5],
|
|
104
|
+
"Asia/Singapore": [1.3, 103.8],
|
|
105
|
+
"Asia/Taipei": [25, 121.5],
|
|
106
|
+
"Asia/Tehran": [35.7, 51.4],
|
|
107
|
+
"Asia/Tokyo": [35.7, 139.7],
|
|
108
|
+
"Atlantic/Reykjavik": [64.1, -21.9],
|
|
109
|
+
"Australia/Brisbane": [-27.5, 153],
|
|
110
|
+
"Australia/Melbourne": [-37.8, 145],
|
|
111
|
+
"Australia/Perth": [-31.9, 115.9],
|
|
112
|
+
"Australia/Sydney": [-33.9, 151.2],
|
|
113
|
+
"Europe/Amsterdam": [52.4, 4.9],
|
|
114
|
+
"Europe/Athens": [38, 23.7],
|
|
115
|
+
"Europe/Berlin": [52.5, 13.4],
|
|
116
|
+
"Europe/Brussels": [50.8, 4.4],
|
|
117
|
+
"Europe/Bucharest": [44.4, 26.1],
|
|
118
|
+
"Europe/Dublin": [53.3, -6.3],
|
|
119
|
+
"Europe/Helsinki": [60.2, 24.9],
|
|
120
|
+
"Europe/Istanbul": [41, 29],
|
|
121
|
+
"Europe/Kyiv": [50.5, 30.5],
|
|
122
|
+
"Europe/Lisbon": [38.7, -9.1],
|
|
123
|
+
"Europe/London": [51.5, -0.1],
|
|
124
|
+
"Europe/Madrid": [40.4, -3.7],
|
|
125
|
+
"Europe/Moscow": [55.8, 37.6],
|
|
126
|
+
"Europe/Oslo": [59.9, 10.8],
|
|
127
|
+
"Europe/Paris": [48.9, 2.4],
|
|
128
|
+
"Europe/Prague": [50.1, 14.4],
|
|
129
|
+
"Europe/Rome": [41.9, 12.5],
|
|
130
|
+
"Europe/Stockholm": [59.3, 18.1],
|
|
131
|
+
"Europe/Vienna": [48.2, 16.4],
|
|
132
|
+
"Europe/Warsaw": [52.2, 21],
|
|
133
|
+
"Europe/Zurich": [47.4, 8.5],
|
|
134
|
+
"Pacific/Auckland": [-36.9, 174.8],
|
|
135
|
+
"Pacific/Fiji": [-18.1, 178.4],
|
|
136
|
+
"Pacific/Honolulu": [21.3, -157.9],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface Position {
|
|
140
|
+
latitude: number
|
|
141
|
+
longitude: number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A guess at where this is being read, from the time zone alone.
|
|
146
|
+
*
|
|
147
|
+
* The `try` is not defensive dressing. This runs under Qt's JavaScript engine
|
|
148
|
+
* as well as in a browser, and an engine built without ICU has no `Intl` to
|
|
149
|
+
* ask; the fallback is the middle of the northern hemisphere, which is wrong by
|
|
150
|
+
* a few degrees for most readers and never wrong by a season.
|
|
151
|
+
*/
|
|
152
|
+
export function position(now: Date): Position {
|
|
153
|
+
const longitude = clamp(-standard(now) / 4, -180, 180)
|
|
154
|
+
|
|
155
|
+
let zone = ""
|
|
156
|
+
try {
|
|
157
|
+
zone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? ""
|
|
158
|
+
} catch {
|
|
159
|
+
zone = ""
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const place = ZONE_PLACE[zone]
|
|
163
|
+
if (place) return { latitude: place[0], longitude: place[1] }
|
|
164
|
+
|
|
165
|
+
const region = zone.split("/")[0] ?? ""
|
|
166
|
+
return { latitude: REGION_LATITUDE[region] ?? 45, longitude }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The zone's own offset from UTC in minutes, with summer time taken back out.
|
|
171
|
+
*
|
|
172
|
+
* A zone is a meridian and summer time is a law, so a clock put forward an hour
|
|
173
|
+
* is not a reader who moved fifteen degrees east. Reading the offset as it
|
|
174
|
+
* stands put Brussels on the meridian of Kyiv every summer, which is an hour of
|
|
175
|
+
* error in every rise and set: the moon left the water while it was still up
|
|
176
|
+
* outside the window, and the sun set an hour early from March to October.
|
|
177
|
+
*
|
|
178
|
+
* The standard offset is the larger of midwinter's and midsummer's, since
|
|
179
|
+
* `getTimezoneOffset` counts minutes behind UTC and summer time can only take
|
|
180
|
+
* that number down. That holds in both hemispheres and on zones that never move
|
|
181
|
+
* their clocks at all.
|
|
182
|
+
*/
|
|
183
|
+
function standard(now: Date): number {
|
|
184
|
+
const year = now.getFullYear()
|
|
185
|
+
const january = new Date(year, 0, 1).getTimezoneOffset()
|
|
186
|
+
const july = new Date(year, 6, 1).getTimezoneOffset()
|
|
187
|
+
|
|
188
|
+
return Math.max(january, july)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The sun's altitude above the horizon in degrees.
|
|
193
|
+
*
|
|
194
|
+
* The low-precision solar position from the Astronomical Almanac, the same
|
|
195
|
+
* formulation SunCalc uses: mean anomaly, then ecliptic longitude, then a
|
|
196
|
+
* rotation into the observer's horizon. Good to about a minute of arc, which is
|
|
197
|
+
* four orders of magnitude better than the position it is fed.
|
|
198
|
+
*/
|
|
199
|
+
export function solarAltitude(now: Date, position: Position): number {
|
|
200
|
+
const days = epochDays(now)
|
|
201
|
+
|
|
202
|
+
return horizon(days, position, equatorial(sunEcliptic(days), 0)).altitude
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The moon's altitude above the horizon in degrees.
|
|
207
|
+
*
|
|
208
|
+
* The sibling of `solarAltitude`, and the reason there is one: a moon is up for
|
|
209
|
+
* its own hours rather than for the hours the sun is not. It is a couple of
|
|
210
|
+
* tenths of a degree out, which is a fraction of the disc's own width and a few
|
|
211
|
+
* minutes of the time it rises.
|
|
212
|
+
*/
|
|
213
|
+
export function lunarAltitude(now: Date, position: Position): number {
|
|
214
|
+
const days = epochDays(now)
|
|
215
|
+
const { latitude, longitude } = moonEcliptic(days)
|
|
216
|
+
|
|
217
|
+
return horizon(days, position, equatorial(longitude, latitude)).altitude
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Days since J2000.0, which every series below is written in.
|
|
222
|
+
*/
|
|
223
|
+
function epochDays(now: Date): number {
|
|
224
|
+
return now.getTime() / DAY_MS - J2000_OFFSET_DAYS
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The sun's ecliptic longitude in radians.
|
|
229
|
+
*
|
|
230
|
+
* Mean anomaly, then the equation of the centre, then round to the longitude of
|
|
231
|
+
* perihelion. Good to about a minute of arc, which is four orders of magnitude
|
|
232
|
+
* better than the position it is fed.
|
|
233
|
+
*/
|
|
234
|
+
function sunEcliptic(days: number): number {
|
|
235
|
+
const meanAnomaly = (357.5291 + 0.98560028 * days) * RAD
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
meanAnomaly +
|
|
239
|
+
(1.9148 * Math.sin(meanAnomaly) +
|
|
240
|
+
0.02 * Math.sin(2 * meanAnomaly) +
|
|
241
|
+
0.0003 * Math.sin(3 * meanAnomaly)) *
|
|
242
|
+
RAD +
|
|
243
|
+
102.9372 * RAD +
|
|
244
|
+
Math.PI
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The moon's ecliptic longitude and latitude in radians.
|
|
250
|
+
*
|
|
251
|
+
* The shortest lunar theory anybody publishes: a mean longitude, a mean
|
|
252
|
+
* anomaly and an argument of latitude, each turning at its own rate, each with
|
|
253
|
+
* the one correction that matters. It lands within a fraction of a degree,
|
|
254
|
+
* which moves moonrise by a few minutes and the shape of the disc by an hour or
|
|
255
|
+
* two of a month. Nobody can tell an hour of a month by looking.
|
|
256
|
+
*
|
|
257
|
+
* Those three rates are the month. The longitude turns a little over thirteen
|
|
258
|
+
* degrees a day and the sun turns one, so the moon laps it every 29.53 days,
|
|
259
|
+
* and that lap is what the disc's shape is.
|
|
260
|
+
*/
|
|
261
|
+
function moonEcliptic(days: number): { latitude: number; longitude: number } {
|
|
262
|
+
const argument = (93.272 + 13.22935 * days) * RAD
|
|
263
|
+
const meanAnomaly = (134.963 + 13.064993 * days) * RAD
|
|
264
|
+
const meanLongitude = (218.316 + 13.176396 * days) * RAD
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
latitude: 5.128 * RAD * Math.sin(argument),
|
|
268
|
+
longitude: meanLongitude + 6.289 * RAD * Math.sin(meanAnomaly),
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* An ecliptic position turned into the frame the sky turns in.
|
|
274
|
+
*
|
|
275
|
+
* The sun never leaves the ecliptic and is handed a latitude of zero, which
|
|
276
|
+
* drops the two terms that then vanish; the moon is up to five degrees off it,
|
|
277
|
+
* and dropping them for the moon would put it a disc's width from where it is.
|
|
278
|
+
*/
|
|
279
|
+
function equatorial(
|
|
280
|
+
longitude: number,
|
|
281
|
+
latitude: number,
|
|
282
|
+
): { declination: number; rightAscension: number } {
|
|
283
|
+
return {
|
|
284
|
+
declination: Math.asin(
|
|
285
|
+
Math.sin(latitude) * Math.cos(OBLIQUITY) +
|
|
286
|
+
Math.cos(latitude) * Math.sin(OBLIQUITY) * Math.sin(longitude),
|
|
287
|
+
),
|
|
288
|
+
rightAscension: Math.atan2(
|
|
289
|
+
Math.sin(longitude) * Math.cos(OBLIQUITY) - Math.tan(latitude) * Math.sin(OBLIQUITY),
|
|
290
|
+
Math.cos(longitude),
|
|
291
|
+
),
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* How high a body is and which side of the sky it is on, from where it is read.
|
|
297
|
+
*
|
|
298
|
+
* The hour angle comes back with the altitude because it is what says which
|
|
299
|
+
* side, and a body that rises where it set is a body on a loop. It is zero at
|
|
300
|
+
* local transit, negative on the way up and positive on the way down.
|
|
301
|
+
*/
|
|
302
|
+
function horizon(
|
|
303
|
+
days: number,
|
|
304
|
+
{ latitude, longitude }: Position,
|
|
305
|
+
{ declination, rightAscension }: { declination: number; rightAscension: number },
|
|
306
|
+
): { altitude: number; hourAngle: number } {
|
|
307
|
+
const siderealTime = (280.16 + 360.9856235 * days) * RAD + longitude * RAD
|
|
308
|
+
const hourAngle = siderealTime - rightAscension
|
|
309
|
+
|
|
310
|
+
const phi = latitude * RAD
|
|
311
|
+
const altitude = Math.asin(
|
|
312
|
+
Math.sin(phi) * Math.sin(declination) +
|
|
313
|
+
Math.cos(phi) * Math.cos(declination) * Math.cos(hourAngle),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return { altitude: altitude / RAD, hourAngle }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Where a body is in its own passage across the sky.
|
|
321
|
+
*
|
|
322
|
+
* `march` is 0 where it comes up and 1 where it goes down, so a renderer that
|
|
323
|
+
* knows nothing about astronomy can put a disc at `left + march * (right -
|
|
324
|
+
* left)` and have it cross by the clock. `arc` is 0 at either horizon and 1 at
|
|
325
|
+
* the top, which is what lifts the middle of the passage and lets the light
|
|
326
|
+
* under it stand more upright the higher the body gets. `up` is whether the
|
|
327
|
+
* thing is there to be drawn at all: 1 once it is clear of the horizon and 0
|
|
328
|
+
* while it is under it.
|
|
329
|
+
*
|
|
330
|
+
* Twelve hours from one horizon to the other, because that is what an hour
|
|
331
|
+
* angle gives: half a turn of the Earth. The real figure is longer in summer
|
|
332
|
+
* and shorter in winter and this does not model that. It is a disc on a
|
|
333
|
+
* wallpaper, and being an hour out at the solstice costs nothing; what would
|
|
334
|
+
* cost something is the moon jumping.
|
|
335
|
+
*
|
|
336
|
+
* `up` is the one that has to be honest, and it is honest because it is the
|
|
337
|
+
* altitude rather than the hour angle. A moon three days past full is under the
|
|
338
|
+
* ground for the first half of the night, and a renderer that took `march`
|
|
339
|
+
* alone would park it on the horizon and leave it there, lit, in an empty sky.
|
|
340
|
+
* The fade runs over a few degrees so it goes out where it sets rather than
|
|
341
|
+
* being switched off.
|
|
342
|
+
*/
|
|
343
|
+
export interface Passage {
|
|
344
|
+
arc: number
|
|
345
|
+
march: number
|
|
346
|
+
up: number
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function passage({ altitude, hourAngle }: { altitude: number; hourAngle: number }): Passage {
|
|
350
|
+
const wrapped = wrap(hourAngle)
|
|
351
|
+
const march = clamp(0.5 + wrapped / Math.PI, 0, 1)
|
|
352
|
+
|
|
353
|
+
return { arc: Math.sin(march * Math.PI), march, up: clamp((altitude + 1) / 6, 0, 1) }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* How much of the moon the sun has hold of, and which way it is going.
|
|
358
|
+
*
|
|
359
|
+
* `lit` is the share of the disc in the light, 0 at the new moon and 1 at the
|
|
360
|
+
* full one. `waxing` is which side that share is on, and it is the right side
|
|
361
|
+
* of the disc for the half of the month the moon is filling up.
|
|
362
|
+
*
|
|
363
|
+
* The elongation is taken along the ecliptic alone, which ignores the moon's
|
|
364
|
+
* few degrees off it and the fact that the sun is not infinitely far away.
|
|
365
|
+
* Both are worth about a percent of the lit share at the quarters and nothing
|
|
366
|
+
* at either end of the month, and neither changes the shape a person sees.
|
|
367
|
+
*/
|
|
368
|
+
export interface Phase {
|
|
369
|
+
lit: number
|
|
370
|
+
waxing: boolean
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function phase(now: Date): Phase {
|
|
374
|
+
const days = epochDays(now)
|
|
375
|
+
const elongation = wrap(moonEcliptic(days).longitude - sunEcliptic(days))
|
|
376
|
+
|
|
377
|
+
return { lit: (1 - Math.cos(elongation)) / 2, waxing: elongation > 0 }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The lit part of the moon, as an SVG path about a point.
|
|
382
|
+
*
|
|
383
|
+
* Two arcs. The limb is the disc's own edge, a half circle on the side the sun
|
|
384
|
+
* is; the terminator is the other edge of the light, and it is a half ellipse
|
|
385
|
+
* because it is a circle around the moon seen at an angle. That ellipse is
|
|
386
|
+
* squeezed flat at the quarters, bulges away from the light through the
|
|
387
|
+
* crescent, and bulges into the dark through the gibbous, which is the whole of
|
|
388
|
+
* why a gibbous moon looks fat and a crescent looks thin.
|
|
389
|
+
*
|
|
390
|
+
* The centre is asked for rather than assumed, because a renderer that has to
|
|
391
|
+
* move the path afterwards has to move the box it is drawn in as well, and a
|
|
392
|
+
* shape that leaves its box is a shape somebody's toolkit will quietly cut in
|
|
393
|
+
* half.
|
|
394
|
+
*
|
|
395
|
+
* At the quarters the ellipse has no width left, and an arc with a zero radius
|
|
396
|
+
* is a straight line by the SVG rules, which is exactly the edge a half moon
|
|
397
|
+
* has. Nothing here special-cases it.
|
|
398
|
+
*/
|
|
399
|
+
export function crescent(x: number, y: number, radius: number, { lit, waxing }: Phase): string {
|
|
400
|
+
const limb = waxing ? 1 : 0
|
|
401
|
+
const terminator = lit > 0.5 ? limb : 1 - limb
|
|
402
|
+
const waist = round(radius * Math.abs(2 * lit - 1))
|
|
403
|
+
const r = round(radius)
|
|
404
|
+
const top = `${round(x)} ${round(y - radius)}`
|
|
405
|
+
const foot = `${round(x)} ${round(y + radius)}`
|
|
406
|
+
|
|
407
|
+
return `M${top}A${r} ${r} 0 0 ${limb} ${foot}A${waist} ${r} 0 0 ${terminator} ${top}Z`
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* How far into the light a point on the disc is, as a share of the radius.
|
|
412
|
+
*
|
|
413
|
+
* Positive in the light, negative in the dark, and near zero on the terminator
|
|
414
|
+
* itself. It is here so a renderer can put the same craters on the moon every
|
|
415
|
+
* night and have them go out as the terminator crosses them, rather than
|
|
416
|
+
* carrying a mark on a part of the moon nothing is lighting.
|
|
417
|
+
*
|
|
418
|
+
* The point is in units of the radius, x to the right and y down, which is what
|
|
419
|
+
* a renderer holds anyway.
|
|
420
|
+
*/
|
|
421
|
+
export function sunlit(x: number, y: number, { lit, waxing }: Phase): number {
|
|
422
|
+
const across = waxing ? x : -x
|
|
423
|
+
|
|
424
|
+
return across + (2 * lit - 1) * Math.sqrt(Math.max(0, 1 - y * y))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The two numbers a renderer reads, from an altitude in degrees.
|
|
429
|
+
*
|
|
430
|
+
* Daylight runs out at -6 degrees, the end of civil twilight, which is when a
|
|
431
|
+
* person outside stops being able to read. It is full by +6, a little after
|
|
432
|
+
* sunrise, because the half hour either side of the horizon is the part worth
|
|
433
|
+
* drawing differently. Dusk peaks at the horizon itself and is gone 8 degrees
|
|
434
|
+
* away, so it rises and falls twice a day and is zero at noon and at midnight.
|
|
435
|
+
*/
|
|
436
|
+
export function light(altitude: number): { daylight: number; dusk: number } {
|
|
437
|
+
return {
|
|
438
|
+
daylight: clamp((altitude + 6) / 12, 0, 1),
|
|
439
|
+
dusk: clamp(1 - Math.abs(altitude) / 8, 0, 1),
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The whole sky, right now and where this is running.
|
|
445
|
+
*
|
|
446
|
+
* How light it is, how near a sunset, where each of the two bodies has got to
|
|
447
|
+
* in its own crossing, and what shape the moon is in tonight. `Date` is read
|
|
448
|
+
* here rather than passed, because every caller wants the actual time and the
|
|
449
|
+
* arithmetic above is already testable without it.
|
|
450
|
+
*
|
|
451
|
+
* Both bodies come back, not the one that happens to be up. They are drawn as
|
|
452
|
+
* two discs that fade with `daylight` and against it, which is the only way the
|
|
453
|
+
* changeover works: a single disc that swapped identity at dusk would set in
|
|
454
|
+
* the west and reappear a moment later in the east, and the sky would have
|
|
455
|
+
* jumped. Fading a setting sun out where it set while a moon comes up on the
|
|
456
|
+
* other side is what actually happens, and it costs one more circle.
|
|
457
|
+
*
|
|
458
|
+
* The moon is worked out rather than put opposite the sun. Opposite the sun is
|
|
459
|
+
* the full moon's path and only the full moon's: it has the moon rising at
|
|
460
|
+
* sunset every night of the year, which is right twice a month and an hour or
|
|
461
|
+
* more out the rest of the time. The real one rises about fifty minutes later
|
|
462
|
+
* each night and is a different shape every one of them, and a wallpaper that
|
|
463
|
+
* gets that right is a wallpaper somebody can look up from and check.
|
|
464
|
+
*/
|
|
465
|
+
export function sunNow(): {
|
|
466
|
+
daylight: number
|
|
467
|
+
dusk: number
|
|
468
|
+
moon: Passage & Phase
|
|
469
|
+
sun: Passage
|
|
470
|
+
} {
|
|
471
|
+
const now = new Date()
|
|
472
|
+
const here = position(now)
|
|
473
|
+
const days = epochDays(now)
|
|
474
|
+
|
|
475
|
+
const sun = horizon(days, here, equatorial(sunEcliptic(days), 0))
|
|
476
|
+
const moon = moonEcliptic(days)
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
...light(sun.altitude),
|
|
480
|
+
moon: {
|
|
481
|
+
...passage(horizon(days, here, equatorial(moon.longitude, moon.latitude))),
|
|
482
|
+
...phase(now),
|
|
483
|
+
},
|
|
484
|
+
sun: passage(sun),
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function clamp(value: number, low: number, high: number): number {
|
|
489
|
+
return Math.min(high, Math.max(low, value))
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** An angle brought back into the half turn either side of zero. */
|
|
493
|
+
function wrap(angle: number): number {
|
|
494
|
+
return Math.atan2(Math.sin(angle), Math.cos(angle))
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Enough of a number for a path, and no more of one. */
|
|
498
|
+
function round(value: number): number {
|
|
499
|
+
return Math.round(value * 1000) / 1000
|
|
500
|
+
}
|