@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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * What stands between the light and the sand, put on the sand.
3
+ *
4
+ * The one thing this water had no answer for. Every other cue in the scene says
5
+ * how far back a thing is: it is smaller, it is hazier, it is behind something.
6
+ * None of them says how far *up* it is, so a fish a hand off the sand and a fish
7
+ * halfway to the surface are the same drawing at two heights of the picture, and
8
+ * the eye has nothing to read the gap with.
9
+ *
10
+ * A shadow is the whole of the answer, and it is cheap: the light in this sea
11
+ * comes from the surface, so what a thing puts on the ground is directly under
12
+ * it. What the reader is actually reading is the two of them together. Close to
13
+ * the sand the patch is small, dark and right under the animal; further up it is
14
+ * wide, faint and further from it, and somewhere above that it is gone.
15
+ *
16
+ * Nothing here is a shape. A round light squashed flat is what a soft-edged
17
+ * patch on the ground is, and a shadow with an outline is a cutout laid on the
18
+ * sand rather than an absence of light on it.
19
+ */
20
+ import { OWN, type Pen, TONE } from "./pen.ts"
21
+
22
+ /** Anything that can come between the light and the ground. */
23
+ export interface Caster {
24
+ readonly depth: number
25
+ readonly size: number
26
+ readonly x: number
27
+ readonly y: number
28
+ }
29
+
30
+ /**
31
+ * How dark the patch is where a thing is sitting on the sand, and the least
32
+ * that is left of it at the far end of the water.
33
+ *
34
+ * The haze takes it the same way it takes everything else. A shadow at the back
35
+ * of the picture that stayed as dark as one at the front would be doing the
36
+ * opposite of its job: it would say near.
37
+ */
38
+ const SHADE_INK = 0.85
39
+
40
+ const HAZE = 0.06
41
+
42
+ /**
43
+ * How far above the sand a thing still puts anything on it, as a share of the
44
+ * box, and how much wider the patch has got by then.
45
+ *
46
+ * Very short, and it was three times this to begin with. The ground here is
47
+ * seen almost edge on, so a hundred fish shadows spread over it do not read as
48
+ * a hundred shadows: they overlap into one dark band along the sand and the
49
+ * picture has gained a smear rather than a dimension. What does read is contact,
50
+ * a thing close enough to the sand that the patch is still tight and still under
51
+ * it, so this is set to the height at which that is true and nothing further up
52
+ * puts anything down at all.
53
+ */
54
+ const DROP = 0.09
55
+
56
+ const SPREAD = 1.3
57
+
58
+ /**
59
+ * How flat the patch is, and how it gives out from the middle.
60
+ *
61
+ * Flat because the ground is being seen almost edge on: a circle of shade on
62
+ * sand that is nearly side on to the reader is an ellipse, and one this shallow
63
+ * before it is anything else. The falloff is soft, since the light making it
64
+ * comes from a whole surface rather than from a point.
65
+ */
66
+ const FLAT = 0.17
67
+
68
+ const FALL = 1.2
69
+
70
+ /**
71
+ * What each of them puts on the ground, in one pass.
72
+ *
73
+ * Handed the whole crowd rather than called per animal, because these belong
74
+ * under everything that swims and there is exactly one moment in the scene
75
+ * where that is true.
76
+ */
77
+ export function paintShade(
78
+ pen: Pen,
79
+ things: readonly Caster[],
80
+ floor: (x: number, depth?: number) => number,
81
+ height: number,
82
+ ): void {
83
+ const reach = Math.max(1, height * DROP)
84
+
85
+ for (const one of things) {
86
+ const sand = floor(one.x, one.depth)
87
+ const up = (sand - one.y) / reach
88
+ if (up < 0 || up >= 1) continue
89
+
90
+ // Steeper than straight, so a thing loses its shadow over the first part of
91
+ // the climb rather than at the end of it. Light off a surface spreads as it
92
+ // goes, and what spreads is gone long before it has run out of distance.
93
+ const held = (1 - up) ** 1.4
94
+
95
+ pen.light(one.x, sand, {
96
+ alpha: (HAZE + (SHADE_INK - HAZE) * Math.min(1, Math.max(0, one.depth))) * held,
97
+ fall: FALL,
98
+ lane: one.depth,
99
+ // Read where the patch is rather than where the animal is: a shadow is
100
+ // the ground with the light taken out of it, and the ground is what it
101
+ // has to be dark against.
102
+ shade: OWN,
103
+ thin: FLAT,
104
+ tone: TONE.shadow,
105
+ weight: 1,
106
+ width: one.size * (1 + SPREAD * up),
107
+ })
108
+ }
109
+ }
@@ -0,0 +1,377 @@
1
+ /**
2
+ * What is above the water: the two bodies, the light off them, and the wash the
3
+ * hour puts over everything.
4
+ *
5
+ * The hour comes from `sun.ts`, so the sun sets on a desktop at the minute it
6
+ * sets in the porthole on the website, and the moon is the shape tonight's moon
7
+ * actually is.
8
+ */
9
+ import type { Clouds } from "../cloud.ts"
10
+ import { type Passage, type Phase, sunlit, sunNow } from "../sun.ts"
11
+ import { OWN, type Pen, type Point, TONE } from "./pen.ts"
12
+
13
+ /** The ends of a crossing, the top and bottom of the arc between them, and how
14
+ * big a body is drawn. */
15
+ const EAST = 0.12
16
+ const WEST = 0.88
17
+ const HIGH = 0.055
18
+ const LOW = 0.17
19
+ const DISC = 0.028
20
+
21
+ /** How far the shaft's foot swings either side of the body, and how far it has
22
+ * opened by the bottom of the box. */
23
+ const SWING = 0.34
24
+ const SPREAD = 0.085
25
+
26
+ /** How far down the box the shaft goes, how heavy it is at its mouth, and how
27
+ * fast it gives up. */
28
+ const FALL = 0.34
29
+ const DISC_INK = 0.012
30
+ const DISC_GIVE = 2.6
31
+
32
+ /** The three lights off a body: the veil out past everything, the halo that is
33
+ * most of the light there is, and the streak a lens gives a bright thing. */
34
+ const BLOOM = { fall: 2.4, ink: 0.016, reach: 13, thin: 1 }
35
+ const HALO = { fall: 1.6, ink: 0.07, reach: 4.2, thin: 1 }
36
+ const STREAK = { fall: 2.1, ink: 0.06, reach: 12, thin: 0.1 }
37
+
38
+ /** How much of the box a dusk reaches down, and how heavy the two washes are. */
39
+ const DUSK_REACH = 0.62
40
+ const DUSK_INK = 0.09
41
+ const NIGHT_INK = 0.3
42
+
43
+ /**
44
+ * Where the bodies sit against everything else, and where the washes do.
45
+ *
46
+ * Behind the whole sea, cliffs included. A reader of this picture is under the
47
+ * water looking up, so everything in it is between them and the sky: a moon
48
+ * drawn in front of the far wall is a moon in the room. What that costs is a
49
+ * low moon going behind the rock at the edge of the picture, which is what a
50
+ * low moon does.
51
+ *
52
+ * The clouds are the one thing between the bodies and the water, since a cloud
53
+ * is the only thing here that is above the sea and below the sky.
54
+ */
55
+ const LANE = { body: -4.2, cloud: -4, dusk: 3, night: 3.1 }
56
+
57
+ /**
58
+ * The weather, as the light it holds back.
59
+ *
60
+ * `ink` is how much of the water's own colour one lobe lays over what is behind
61
+ * it, `give` is how quickly that runs out towards the lobe's edge, and `squat`
62
+ * is how much shorter than it is wide a lobe is drawn.
63
+ *
64
+ * Painted in the water rather than in a colour of its own, because from under
65
+ * here a cloud is not a white shape on a night sky. It is the light not
66
+ * arriving, and the water is what is left where it does not.
67
+ */
68
+ const CLOUD = { give: 1.4, ink: 0.62, lit: 0.5, rim: 0.82, shade: 0.34, squat: 0.5 }
69
+
70
+ /**
71
+ * How much of a body's own light the thickest cloud takes.
72
+ *
73
+ * The shape covers what it covers, and this is the rest of it: a moon behind
74
+ * cloud throws a fainter shaft into the water than a clear one, and the shaft
75
+ * leaves the moon a long way from any cloud. Not all of it, since a covered
76
+ * moon still lights the cloud it is behind.
77
+ */
78
+ const MUTE = 0.8
79
+
80
+ /**
81
+ * The craters, off centre and unevenly sized on purpose.
82
+ *
83
+ * Three round spots spaced evenly on a disc is a face, and the moment anybody
84
+ * sees a face there they stop seeing a moon. In units of the moon's own radius,
85
+ * with the size third.
86
+ */
87
+ const CRATERS = [
88
+ [-0.34, -0.22, 0.22],
89
+ [0.19, -0.45, 0.1],
90
+ [0.31, 0.3, 0.15],
91
+ ] as const
92
+
93
+ /** How dark a crater is against the moon it is on. */
94
+ const CRATER_INK = 0.34
95
+
96
+ /** How many sides a disc is cut into. Enough that the edge of a moon a
97
+ * fingernail across is a curve rather than a decision. */
98
+ const ROUND = 72
99
+
100
+ export interface Box {
101
+ height: number
102
+ width: number
103
+ }
104
+
105
+ /**
106
+ * A body, as everything else in the sky needs it: where it is, how much light
107
+ * it is throwing, how far that light carries and what colour it is.
108
+ */
109
+ interface Lamp {
110
+ cx: number
111
+ cy: number
112
+ glow: number
113
+ reach: number
114
+ tone: number
115
+ }
116
+
117
+ /**
118
+ * An hour asked for rather than read off the clock, for a harness holding one
119
+ * renderer against another.
120
+ *
121
+ * Two renderers can only be compared on the same sky, and the sky is a function
122
+ * of the minute the still was taken in. Every harness over this ornament takes
123
+ * the same four numbers for the same reason.
124
+ */
125
+ export interface Asked {
126
+ daylight: number
127
+ dusk: number
128
+ lit: number
129
+ march: number
130
+ waxing: boolean
131
+ }
132
+
133
+ /**
134
+ * The sky, drawn.
135
+ *
136
+ * Both bodies every time, not the one that happens to be up: a single disc that
137
+ * swapped identity at dusk would set in the west and reappear a moment later in
138
+ * the east. Fading a setting sun out where it set while a moon comes up on the
139
+ * other side is what actually happens, and it costs one more circle.
140
+ */
141
+ export function paintSky(
142
+ pen: Pen,
143
+ box: Box,
144
+ clouds: Clouds | null = null,
145
+ asked: Asked | null = null,
146
+ ): { daylight: number; dusk: number } {
147
+ const sky = asked
148
+ ? {
149
+ daylight: asked.daylight,
150
+ dusk: asked.dusk,
151
+ moon: {
152
+ arc: Math.sin(asked.march * Math.PI),
153
+ lit: asked.lit,
154
+ march: asked.march,
155
+ up: 1,
156
+ waxing: asked.waxing,
157
+ },
158
+ sun: { arc: Math.sin(asked.march * Math.PI), march: asked.march, up: 1 },
159
+ }
160
+ : sunNow()
161
+ const bodies = [
162
+ {
163
+ moon: false,
164
+ passage: sky.sun,
165
+ phase: null as Phase | null,
166
+ show: sky.daylight,
167
+ tone: TONE.sun,
168
+ },
169
+ {
170
+ moon: true,
171
+ passage: sky.moon,
172
+ phase: sky.moon as Phase,
173
+ show: (1 - sky.daylight) * sky.moon.up,
174
+ tone: TONE.moon,
175
+ },
176
+ ]
177
+
178
+ const lamps: Lamp[] = []
179
+ for (const body of bodies) {
180
+ if (body.show <= 0.004) continue
181
+ lamps.push(paintBody(pen, box, body.passage, body.phase, body.show, body.tone, clouds))
182
+ }
183
+
184
+ // After both of them, because what a cloud does is cover a light that has
185
+ // already been drawn, and what light it has of its own is theirs.
186
+ if (clouds) paintClouds(pen, clouds, lamps)
187
+
188
+ // Dusk falls off with depth and night does not, which is the difference
189
+ // between the two. A sunset is a colour arriving through the surface and it
190
+ // gets no further down than the light it came in on; laid flat over the whole
191
+ // box it lifts the floor off black and the water goes to one olive haze.
192
+ if (sky.dusk > 0) {
193
+ pen.wash({
194
+ alpha: DUSK_INK * sky.dusk,
195
+ fade: [0, box.height * DUSK_REACH],
196
+ lane: LANE.dusk,
197
+ tone: TONE.dusk,
198
+ })
199
+ }
200
+
201
+ if (sky.daylight < 1) {
202
+ pen.wash({ alpha: NIGHT_INK * (1 - sky.daylight), lane: LANE.night, tone: TONE.surface })
203
+ }
204
+
205
+ return { daylight: sky.daylight, dusk: sky.dusk }
206
+ }
207
+
208
+ /**
209
+ * One body: the light around it, the shaft under it, and the disc itself.
210
+ *
211
+ * The disc is drawn at full strength whatever shape it is in, because a
212
+ * crescent is as bright per inch as a full moon and there is simply less of it.
213
+ * What there is less of is everything around it, so the halo, the bloom, the
214
+ * streak and the shaft all take `lit`. A new moon throws nothing, which is what
215
+ * a new moon does.
216
+ */
217
+ function paintBody(
218
+ pen: Pen,
219
+ box: Box,
220
+ passage: Passage,
221
+ phase: Phase | null,
222
+ show: number,
223
+ tone: number,
224
+ clouds: Clouds | null,
225
+ ): Lamp {
226
+ const r = box.height * DISC
227
+ const cx = box.width * (EAST + passage.march * (WEST - EAST))
228
+ const cy = box.height * (LOW + passage.arc * (HIGH - LOW))
229
+ const veiled = clouds ? 1 - MUTE * clouds.cover(cx, cy) : 1
230
+ const glow = show * (phase ? phase.lit : 1) * veiled
231
+
232
+ // A body over on the left throws its shaft down and to the right, and one
233
+ // directly overhead throws it straight down.
234
+ const lean = (0.5 - passage.march) * SWING
235
+
236
+ for (const light of [BLOOM, HALO, STREAK]) {
237
+ pen.light(cx, cy, {
238
+ alpha: light.ink * glow,
239
+ fall: light.fall,
240
+ lane: LANE.body,
241
+ shade: OWN,
242
+ thin: light.thin,
243
+ tone,
244
+ width: r * light.reach,
245
+ })
246
+ }
247
+
248
+ // The shaft, its mouth the disc's own diameter at the disc's own centre, so
249
+ // the two edges are tangent to it and the lower half of the disc sits inside
250
+ // its own light. Started anywhere below that line the mouth is wider than the
251
+ // disc is at that height, and the light appears to come out of its sides.
252
+ pen.fill(
253
+ [
254
+ [
255
+ { x: cx - r, y: cy },
256
+ { x: cx + r, y: cy },
257
+ { x: cx + box.width * (lean + SPREAD), y: box.height },
258
+ { x: cx + box.width * (lean - SPREAD), y: box.height },
259
+ ],
260
+ ],
261
+ {
262
+ alpha: DISC_INK * glow,
263
+ fade: [cy, Math.max(1, box.height * FALL - cy)],
264
+ fall: DISC_GIVE,
265
+ lane: LANE.body,
266
+ tone,
267
+ },
268
+ )
269
+
270
+ pen.fill([phase ? crescent(cx, cy, r, phase) : disc(cx, cy, r)], {
271
+ alpha: show,
272
+ lane: LANE.body,
273
+ tone,
274
+ })
275
+
276
+ if (!phase) return { cx, cy, glow, reach: r * HALO.reach, tone }
277
+
278
+ // A crater sitting where there is no moon under it is a grey spot on the sky,
279
+ // so one is drawn only once it is clear of the terminator by its own width,
280
+ // and fades in over the next. Through the month they go out one after another
281
+ // as the shadow crosses them, which is what a terminator is for looking at.
282
+ for (const [x, y, size] of CRATERS) {
283
+ const clear = sunlit(x, y, phase) / size - 1
284
+ if (clear <= 0) continue
285
+
286
+ pen.fill([disc(cx + r * x, cy + r * y, r * size)], {
287
+ alpha: CRATER_INK * show * Math.min(1, clear),
288
+ lane: LANE.body,
289
+ tone: TONE.surface,
290
+ })
291
+ }
292
+
293
+ return { cx, cy, glow, reach: r * HALO.reach, tone }
294
+ }
295
+
296
+ /**
297
+ * The weather, as lobes of water laid over whatever light is behind them.
298
+ *
299
+ * A round light rather than an outline, which is the one shape in this pen
300
+ * that has a soft edge: a cloud cut as a polygon has a rim, and a rim is the
301
+ * one thing a cloud does not have. The mass is the lobes overlapping, so it is
302
+ * densest where it is thickest and gives out at the edges without being told
303
+ * to.
304
+ */
305
+ function paintClouds(pen: Pen, clouds: Clouds, lamps: readonly Lamp[]): void {
306
+ for (const one of clouds.clouds) {
307
+ for (const lobe of one.lobes) {
308
+ const x = one.x + lobe.dx
309
+ const y = one.y + lobe.dy
310
+
311
+ pen.light(x, y, {
312
+ alpha: CLOUD.ink * one.thick,
313
+ fall: CLOUD.give,
314
+ lane: LANE.cloud,
315
+ thin: CLOUD.squat,
316
+ tone: TONE.shadow,
317
+ weight: CLOUD.shade * one.thick,
318
+ width: lobe.r,
319
+ })
320
+
321
+ // What the mass gives back, which is the light it just took. A lobe near
322
+ // the body is standing in that body's halo and is lit by it; one at the
323
+ // other end of the sky is a shadow and nothing else. Smaller than the
324
+ // mass that took the light, so what shows is the light around the edge
325
+ // of a cloud rather than a cloud turned into a lamp.
326
+ for (const lamp of lamps) {
327
+ const near = 1 - Math.hypot(x - lamp.cx, y - lamp.cy) / lamp.reach
328
+ if (near <= 0) continue
329
+
330
+ pen.light(x, y, {
331
+ alpha: CLOUD.lit * lamp.glow * near * one.thick,
332
+ fall: CLOUD.give,
333
+ lane: LANE.cloud,
334
+ thin: CLOUD.squat,
335
+ tone: lamp.tone,
336
+ width: lobe.r * CLOUD.rim,
337
+ })
338
+ }
339
+ }
340
+ }
341
+ }
342
+
343
+ /** A disc, as a polygon. */
344
+ function disc(cx: number, cy: number, r: number): Point[] {
345
+ const points: Point[] = []
346
+ for (let i = 0; i < ROUND; i++) {
347
+ const turn = (i / ROUND) * Math.PI * 2
348
+ points.push({ x: cx + r * Math.cos(turn), y: cy + r * Math.sin(turn) })
349
+ }
350
+ return points
351
+ }
352
+
353
+ /**
354
+ * The lit part of the moon, as a polygon.
355
+ *
356
+ * The same two arcs `crescent` in the ornament writes as a path, walked instead
357
+ * of written: the limb, which is the disc's own edge on the side the sun is,
358
+ * and the terminator, which is a circle around the moon seen edge on and so is
359
+ * an ellipse squeezed by how much of the disc is in the light.
360
+ */
361
+ function crescent(cx: number, cy: number, r: number, { lit, waxing }: Phase): Point[] {
362
+ const side = waxing ? 1 : -1
363
+ const waist = r * (2 * lit - 1)
364
+ const points: Point[] = []
365
+ const half = ROUND / 2
366
+
367
+ for (let i = 0; i <= half; i++) {
368
+ const turn = (i / half) * Math.PI
369
+ points.push({ x: cx + side * r * Math.sin(turn), y: cy - r * Math.cos(turn) })
370
+ }
371
+ for (let i = half; i >= 0; i--) {
372
+ const turn = (i / half) * Math.PI
373
+ points.push({ x: cx - side * waist * Math.sin(turn), y: cy - r * Math.cos(turn) })
374
+ }
375
+
376
+ return points
377
+ }