@codincod/ornament 0.0.0-stage → 0.2.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 +62 -2
- package/package.json +35 -3
- package/src/biome.ts +157 -0
- package/src/canvas/flat.ts +336 -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/colour.ts +86 -0
- package/src/paint/frame.ts +204 -0
- package/src/paint/ink.ts +134 -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/svg.ts +326 -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
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything alive, and everything going over the top of it.
|
|
3
|
+
*
|
|
4
|
+
* The shoal, the clownfish in their anemones, the bait ball, the big animals
|
|
5
|
+
* that cross once in a while, the cephalopods, what walks on the bottom, and
|
|
6
|
+
* the boats. All of it is the ornament's own simulation; what is here is what
|
|
7
|
+
* `Seascape.qml` decides, which is how heavy each of them is drawn and how far
|
|
8
|
+
* back it stands.
|
|
9
|
+
*/
|
|
10
|
+
import type { Cephalopods } from "../cephalopods.ts"
|
|
11
|
+
import { octopusArms, octopusHead, squidArms, squidBody } from "../cephalopods.ts"
|
|
12
|
+
import type { Eels } from "../eels.ts"
|
|
13
|
+
import { frameAt, SPAN } from "../fish_shape.ts"
|
|
14
|
+
import {
|
|
15
|
+
armsOf,
|
|
16
|
+
bell,
|
|
17
|
+
combRows,
|
|
18
|
+
HAIR_GIRTH,
|
|
19
|
+
hairOf,
|
|
20
|
+
ARM_GIRTH as JELLY_ARM,
|
|
21
|
+
type Jellies,
|
|
22
|
+
moonMarks,
|
|
23
|
+
sparks,
|
|
24
|
+
washOf,
|
|
25
|
+
} from "../jellies.ts"
|
|
26
|
+
import type { Nemos } from "../nemos.ts"
|
|
27
|
+
import { HULL, type Passers, PING_RINGS, ringAt, SCREWS, SUB_SCREW, SUBMARINE } from "../passers.ts"
|
|
28
|
+
import type { Puffers } from "../puffers.ts"
|
|
29
|
+
import { type Shoal, SPECIES } from "../shoal.ts"
|
|
30
|
+
import type { Swarm } from "../swarm.ts"
|
|
31
|
+
import { BODIES, VEIL_INK, VEILS, type Visitors } from "../visitors.ts"
|
|
32
|
+
import type { Walkers } from "../walkers.ts"
|
|
33
|
+
import { disc, ring } from "./light.ts"
|
|
34
|
+
import { OWN, type Pen, TONE } from "./pen.ts"
|
|
35
|
+
import { trace } from "./trace.ts"
|
|
36
|
+
|
|
37
|
+
/** How dark an animal in the open water may be, and how much of that weight it
|
|
38
|
+
* keeps at the back of it. */
|
|
39
|
+
const OPEN_WATER = 0.22
|
|
40
|
+
const DEPTH_INK = 0.65
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How much further back a veil stands than the animal wearing it.
|
|
44
|
+
*
|
|
45
|
+
* Enough for the depth buffer to settle it and not enough to be a distance. The
|
|
46
|
+
* solid pass takes its turn by how far back a thing stands rather than by the
|
|
47
|
+
* order it arrived in, so two drawings at one depth is two drawings fighting
|
|
48
|
+
* over the same pixels.
|
|
49
|
+
*/
|
|
50
|
+
const BEHIND = 0.002
|
|
51
|
+
|
|
52
|
+
/** The rest of the ceilings, each one thing's share of full ink. */
|
|
53
|
+
const CRAWLER_INK = 0.42
|
|
54
|
+
const CRAWLER_LIFT = 0.8
|
|
55
|
+
const CRAWLER_RISEN = 0.62
|
|
56
|
+
const EEL_INK = 0.46
|
|
57
|
+
const PUFFER_INK = 0.44
|
|
58
|
+
const FROTH_INK = 0.3
|
|
59
|
+
const FROTH_LET_GO = 0.45
|
|
60
|
+
const HAZE_INK = 0.04
|
|
61
|
+
const HULL_INK = 0.16
|
|
62
|
+
const JELLY_INK = 0.17
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* How much of a jellyfish's own light there is, and how much of it survives a
|
|
66
|
+
* sunlit afternoon.
|
|
67
|
+
*
|
|
68
|
+
* Almost none. A flash is measured against the water around it, so a display
|
|
69
|
+
* that reads across a whole screen at midnight is nothing at all at noon, and
|
|
70
|
+
* the animal has not changed: the water it is being seen against has. What is
|
|
71
|
+
* kept in daylight is a hint, because a jellyfish that went perfectly dark at
|
|
72
|
+
* dawn would be an animal switching off on a schedule.
|
|
73
|
+
*/
|
|
74
|
+
const JELLY_LIGHT = 0.62
|
|
75
|
+
const JELLY_DAY = 0.12
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* And how much of the light a comb jelly does not make.
|
|
79
|
+
*
|
|
80
|
+
* The rows are a diffraction grating, so what runs along them is the water's
|
|
81
|
+
* own light taken apart, and the rule runs the other way: it is at its best in
|
|
82
|
+
* the sun and there is nothing to take apart at midnight. Two lights on two
|
|
83
|
+
* animals swimming past each other, one needing the day and one needing the
|
|
84
|
+
* dark, which is the sort of thing this water is for.
|
|
85
|
+
*/
|
|
86
|
+
const JELLY_SHEEN = 0.5
|
|
87
|
+
const JELLY_NIGHT = 0.15
|
|
88
|
+
const PING_INK = 0.62
|
|
89
|
+
const SPECK_INK = 0.16
|
|
90
|
+
const VISITOR_INK = 0.42
|
|
91
|
+
|
|
92
|
+
/** How much of a weight is punched back out again for an eye or a cut line. */
|
|
93
|
+
const CUT_SHADE = 0.34
|
|
94
|
+
|
|
95
|
+
/** What the bodies are drawn as lines at, in their own units. */
|
|
96
|
+
const ARM_GIRTH = 0.075
|
|
97
|
+
const TENTACLE_GIRTH = 0.035
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* How thick an eel is drawn, against how tall it stands.
|
|
101
|
+
*
|
|
102
|
+
* Thin, and the roundness of the cap does the rest. What a colony reads as at
|
|
103
|
+
* this distance is a texture with a grain to it, and the grain is the gaps: a
|
|
104
|
+
* girth that closed them would draw a patch of grass.
|
|
105
|
+
*/
|
|
106
|
+
const EEL_GIRTH = 0.12
|
|
107
|
+
|
|
108
|
+
/** How big an eye is against the body it is in. */
|
|
109
|
+
const EYE_SIZE = 0.11
|
|
110
|
+
|
|
111
|
+
/** How many of each are ever drawn at once. */
|
|
112
|
+
const MOST = { froth: 220, passers: 4, specks: 3000, visitors: 6 }
|
|
113
|
+
|
|
114
|
+
/** Where the boats sit, which is over everything in the water. */
|
|
115
|
+
const LANE = { froth: 1.15, passer: 1.2 }
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* What a weight is worth at a distance, for anything in the open water.
|
|
119
|
+
*
|
|
120
|
+
* It keeps a share of its weight whatever the distance, because an animal that
|
|
121
|
+
* faded into the murk as far as the ground does is an animal the eye loses in
|
|
122
|
+
* the middle of a stroke.
|
|
123
|
+
*/
|
|
124
|
+
function afloat(full: number, depth: number): number {
|
|
125
|
+
return full * (1 - DEPTH_INK + DEPTH_INK * depth)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** And what it is worth for anything standing on the bottom, which does fade
|
|
129
|
+
* the whole way: the murk it is seen through is the ground's. */
|
|
130
|
+
function far(full: number, depth: number): number {
|
|
131
|
+
return HAZE_INK + (full - HAZE_INK) * Math.min(1, Math.max(0, depth))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function paintShoal(pen: Pen, shoal: Shoal): void {
|
|
135
|
+
for (const fish of shoal.fish) {
|
|
136
|
+
const weight = afloat(OPEN_WATER, fish.depth)
|
|
137
|
+
if (weight <= 0.002) continue
|
|
138
|
+
|
|
139
|
+
const frame = frameAt(fish.tail)
|
|
140
|
+
const spot = {
|
|
141
|
+
facing: fish.facing,
|
|
142
|
+
scale: fish.size / SPAN,
|
|
143
|
+
tilt: fish.tilt,
|
|
144
|
+
x: fish.x,
|
|
145
|
+
y: fish.y,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The billed kinds are a whole drawing of their own rather than this one
|
|
149
|
+
// with a bill laid over it. Two fills would put a seam across the
|
|
150
|
+
// swordfish's face wherever the bill's buried root crossed the head.
|
|
151
|
+
pen.fill(
|
|
152
|
+
trace(SPECIES[fish.kind].bill > 0 ? frame.billed : frame.d),
|
|
153
|
+
{
|
|
154
|
+
lane: fish.depth,
|
|
155
|
+
shade: fish.y,
|
|
156
|
+
weight,
|
|
157
|
+
},
|
|
158
|
+
spot,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
// The eye is punched out in the surface behind it rather than drawn in an
|
|
162
|
+
// ink of its own, which is why a fish reads as a cutout in the water
|
|
163
|
+
// instead of a sticker on it.
|
|
164
|
+
pen.fill(
|
|
165
|
+
[disc(frame.eye.x, frame.eye.y, frame.eye.r)],
|
|
166
|
+
{
|
|
167
|
+
lane: fish.depth,
|
|
168
|
+
shade: fish.y,
|
|
169
|
+
weight: weight * CUT_SHADE,
|
|
170
|
+
},
|
|
171
|
+
spot,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The clownfish, each in the anemone it lives in.
|
|
178
|
+
*
|
|
179
|
+
* `face` is the drawn width and its sign is the mirror, so the fish squeezes to
|
|
180
|
+
* nothing and opens out the other way, which is what a fish that has stopped
|
|
181
|
+
* actually does. A fish all the way into the tentacles is not drawn at all
|
|
182
|
+
* rather than drawn faintly: it has not become hard to see, it is behind
|
|
183
|
+
* something.
|
|
184
|
+
*/
|
|
185
|
+
export function paintNemos(pen: Pen, nemos: Nemos): void {
|
|
186
|
+
for (const nemo of nemos.nemos) {
|
|
187
|
+
const weight = afloat(OPEN_WATER, nemo.host.depth) * (1 - nemo.cover)
|
|
188
|
+
if (weight <= 0.002) continue
|
|
189
|
+
|
|
190
|
+
pen.fill(
|
|
191
|
+
trace(frameAt(nemo.beat).d),
|
|
192
|
+
{
|
|
193
|
+
lane: -3 + nemo.host.depth,
|
|
194
|
+
shade: nemo.y,
|
|
195
|
+
weight,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
facing: nemo.face,
|
|
199
|
+
scale: nemo.length / SPAN,
|
|
200
|
+
tilt: nemo.tilt,
|
|
201
|
+
x: nemo.x,
|
|
202
|
+
y: nemo.y,
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A flock of thousands, which is a few hundred specks and a shape round them.
|
|
210
|
+
*
|
|
211
|
+
* Marks rather than fish. At the size one of these is on the screen a fish
|
|
212
|
+
* drawing is four pixels of nothing, and what makes a bait ball read is that it
|
|
213
|
+
* is a crowd with a body: hundreds of identical small marks, packed, all
|
|
214
|
+
* leaning the same way and all leaning a little differently.
|
|
215
|
+
*/
|
|
216
|
+
export function paintSwarm(pen: Pen, flock: Swarm): void {
|
|
217
|
+
let drawn = 0
|
|
218
|
+
for (const speck of flock.specks) {
|
|
219
|
+
if (drawn++ >= MOST.specks) break
|
|
220
|
+
|
|
221
|
+
const weight = afloat(SPECK_INK, speck.depth) * flock.weight
|
|
222
|
+
if (weight <= 0.002) continue
|
|
223
|
+
|
|
224
|
+
const girth = Math.max(0.7, speck.size * 0.42)
|
|
225
|
+
pen.line(
|
|
226
|
+
[
|
|
227
|
+
[
|
|
228
|
+
{ x: -speck.size / 2, y: 0 },
|
|
229
|
+
{ x: speck.size / 2, y: 0 },
|
|
230
|
+
],
|
|
231
|
+
],
|
|
232
|
+
{
|
|
233
|
+
lane: speck.depth,
|
|
234
|
+
shade: speck.y,
|
|
235
|
+
weight,
|
|
236
|
+
width: girth,
|
|
237
|
+
},
|
|
238
|
+
{ tilt: speck.tilt, x: speck.x, y: speck.y },
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The big animals: a turtle, a pod of dolphins, a manta, a shark going
|
|
245
|
+
* somewhere, and once in a very long while a thing that is none of those.
|
|
246
|
+
* Heavier than a fish, because what is being read across the whole picture is
|
|
247
|
+
* the shape, and a shape drawn at the shoal's weight is a smudge.
|
|
248
|
+
*
|
|
249
|
+
* One of them is not all body, and `VEILS` in the ornament says which and why.
|
|
250
|
+
* What it hands over is laid in first and a hair behind, so the animal's own
|
|
251
|
+
* outline takes the depth where the two overlap and only what is streaming
|
|
252
|
+
* clear of it is drawn sheer. Both or neither: a visitor drawn without the veil
|
|
253
|
+
* it declares is missing a part of itself rather than drawn more simply.
|
|
254
|
+
*/
|
|
255
|
+
export function paintVisitors(pen: Pen, visitors: Visitors): void {
|
|
256
|
+
let drawn = 0
|
|
257
|
+
for (const guest of visitors.crossing) {
|
|
258
|
+
if (drawn++ >= MOST.visitors) break
|
|
259
|
+
|
|
260
|
+
const weight = afloat(VISITOR_INK * guest.weight, guest.depth)
|
|
261
|
+
if (weight <= 0.002) continue
|
|
262
|
+
|
|
263
|
+
const spot = {
|
|
264
|
+
facing: guest.facing,
|
|
265
|
+
scale: guest.size,
|
|
266
|
+
tilt: guest.tilt,
|
|
267
|
+
x: guest.x,
|
|
268
|
+
y: guest.y,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const veil = VEILS[guest.kind]?.(guest.stroke)
|
|
272
|
+
if (veil) {
|
|
273
|
+
pen.fill(
|
|
274
|
+
trace(veil),
|
|
275
|
+
{
|
|
276
|
+
lane: guest.depth - BEHIND,
|
|
277
|
+
shade: guest.y,
|
|
278
|
+
weight: weight * VEIL_INK,
|
|
279
|
+
},
|
|
280
|
+
spot,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
pen.fill(
|
|
285
|
+
trace(BODIES[guest.kind](guest.stroke)),
|
|
286
|
+
{
|
|
287
|
+
lane: guest.depth,
|
|
288
|
+
shade: guest.y,
|
|
289
|
+
weight,
|
|
290
|
+
},
|
|
291
|
+
spot,
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Squid and octopus.
|
|
298
|
+
*
|
|
299
|
+
* A squid jets and drifts, which is the opposite rhythm to a tail. An octopus
|
|
300
|
+
* is on the bottom rather than in the water, except for the once in a long
|
|
301
|
+
* while when one lets go, so it recedes the way the bottom does and it runs
|
|
302
|
+
* between two weights by how far off the floor it is.
|
|
303
|
+
*/
|
|
304
|
+
export function paintInklings(pen: Pen, inklings: Cephalopods): void {
|
|
305
|
+
for (const squid of inklings.squids) {
|
|
306
|
+
const weight = afloat(OPEN_WATER, squid.depth)
|
|
307
|
+
if (weight <= 0.002) continue
|
|
308
|
+
|
|
309
|
+
const spot = {
|
|
310
|
+
facing: squid.facing,
|
|
311
|
+
scale: squid.size,
|
|
312
|
+
tilt: squid.tilt,
|
|
313
|
+
x: squid.x,
|
|
314
|
+
y: squid.y,
|
|
315
|
+
}
|
|
316
|
+
const mark = { lane: squid.depth, shade: squid.y, weight }
|
|
317
|
+
|
|
318
|
+
pen.fill(trace(squidBody(squid.squeeze)), mark, spot)
|
|
319
|
+
pen.line(squidArms(squid.squeeze), { ...mark, width: TENTACLE_GIRTH * squid.size }, spot)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const octopus of inklings.octopuses) {
|
|
323
|
+
const aloft = Math.max(-1, Math.min(1, octopus.lift / CRAWLER_LIFT))
|
|
324
|
+
const bedded = far(CRAWLER_INK, octopus.depth)
|
|
325
|
+
const risen = far(CRAWLER_RISEN, octopus.depth)
|
|
326
|
+
|
|
327
|
+
// Under the sand is under the haze. A buried one is not a faint animal, it
|
|
328
|
+
// is a bump in the ground with an animal somewhere inside it.
|
|
329
|
+
const weight = aloft < 0 ? bedded * (1 + aloft * 0.7) : bedded + (risen - bedded) * aloft
|
|
330
|
+
if (weight <= 0.002) continue
|
|
331
|
+
|
|
332
|
+
const spot = { scale: octopus.size, x: octopus.x, y: octopus.y }
|
|
333
|
+
const mark = { lane: octopus.depth, shade: octopus.y, weight }
|
|
334
|
+
|
|
335
|
+
pen.fill(trace(octopusHead(octopus.breath)), mark, spot)
|
|
336
|
+
pen.line(octopusArms(octopus), { ...mark, width: ARM_GIRTH * octopus.size }, spot)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The jellyfish, all seven, and one of them is a float and two are not
|
|
342
|
+
* jellyfish at all.
|
|
343
|
+
*
|
|
344
|
+
* Three of the eight make their own light, and none of it is on until
|
|
345
|
+
* something touches them. It is drawn in the moon's colour, since this palette
|
|
346
|
+
* has no blue in it and nothing else in the water glows at all, and it is
|
|
347
|
+
* weighed against the hour: a flash is measured against the water around it,
|
|
348
|
+
* so these are a night thing. The comb jelly's rows are the exception that
|
|
349
|
+
* proves the rule, and they run the other way; see the note on `JELLY_SHEEN`.
|
|
350
|
+
*
|
|
351
|
+
* Lighter than anything else in the water on purpose. A bell is a bag of water
|
|
352
|
+
* inside water: it stops a little of the light and passes the rest, and drawn
|
|
353
|
+
* at a fish's weight it comes out as a cut-out mushroom. What hangs under one
|
|
354
|
+
* is lighter again, at the share `visitors.ts` settled on for a mermaid's
|
|
355
|
+
* hair, and for the reason it gives there: a fine mass scatters light where a
|
|
356
|
+
* body stops it.
|
|
357
|
+
*
|
|
358
|
+
* The order is the animal's own. Whatever hangs goes in first and a hair
|
|
359
|
+
* further back, so the bell takes the depth where the two overlap and only
|
|
360
|
+
* what is streaming clear of it is drawn sheer. Then the marks, punched out of
|
|
361
|
+
* the bell the way a fish's eye is punched out of its head, and last the two
|
|
362
|
+
* kinds that carry a light of their own.
|
|
363
|
+
*/
|
|
364
|
+
export function paintJellies(pen: Pen, jellies: Jellies, daylight: number): void {
|
|
365
|
+
const hour = Math.min(1, Math.max(0, daylight))
|
|
366
|
+
// What a made light is worth at this hour, and what a borrowed one is.
|
|
367
|
+
const dark = JELLY_DAY + (1 - JELLY_DAY) * (1 - hour)
|
|
368
|
+
const sun = JELLY_NIGHT + (1 - JELLY_NIGHT) * hour
|
|
369
|
+
|
|
370
|
+
for (const one of jellies.jellies) {
|
|
371
|
+
const weight = afloat(JELLY_INK, one.depth)
|
|
372
|
+
if (weight <= 0.002) continue
|
|
373
|
+
|
|
374
|
+
const spot = {
|
|
375
|
+
facing: one.facing,
|
|
376
|
+
scale: one.size,
|
|
377
|
+
tilt: one.tilt,
|
|
378
|
+
x: one.x,
|
|
379
|
+
y: one.y,
|
|
380
|
+
}
|
|
381
|
+
const mark = { lane: one.depth, shade: one.y, weight }
|
|
382
|
+
const behind = { ...mark, lane: one.depth - BEHIND }
|
|
383
|
+
|
|
384
|
+
const hair = hairOf(one)
|
|
385
|
+
if (hair.length > 0) {
|
|
386
|
+
pen.line(
|
|
387
|
+
hair,
|
|
388
|
+
{
|
|
389
|
+
...behind,
|
|
390
|
+
weight: weight * VEIL_INK,
|
|
391
|
+
width: Math.max(0.6, HAIR_GIRTH[one.kind] * one.size),
|
|
392
|
+
},
|
|
393
|
+
spot,
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const arms = armsOf(one)
|
|
398
|
+
if (arms.length > 0) {
|
|
399
|
+
pen.line(arms, { ...behind, width: JELLY_ARM[one.kind] * one.size }, spot)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
pen.fill(trace(bell(one.kind, one.squeeze)), mark, spot)
|
|
403
|
+
|
|
404
|
+
// The four horseshoes, which are the reason a moon jelly is the jellyfish
|
|
405
|
+
// everybody can name. Punched back out of the bell rather than drawn over
|
|
406
|
+
// it, so they read as the animal being see-through.
|
|
407
|
+
if (one.kind === "moon") {
|
|
408
|
+
pen.line(
|
|
409
|
+
moonMarks(one.squeeze),
|
|
410
|
+
{
|
|
411
|
+
...mark,
|
|
412
|
+
weight: weight * CUT_SHADE,
|
|
413
|
+
width: Math.max(0.8, 0.03 * one.size),
|
|
414
|
+
},
|
|
415
|
+
spot,
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// The comb rows carry both kinds of light and they carry them on one set
|
|
420
|
+
// of lines: the wave the grating takes out of the water, which wants the
|
|
421
|
+
// sun, and the animal's own flash along the same eight canals, which wants
|
|
422
|
+
// the dark. One drawing, two sources, and the reader can tell them apart
|
|
423
|
+
// because only one of them moves.
|
|
424
|
+
for (const row of combRows(one)) {
|
|
425
|
+
const alpha = JELLY_SHEEN * row.glow * sun + JELLY_LIGHT * one.glow * dark
|
|
426
|
+
if (alpha <= 0.01) continue
|
|
427
|
+
|
|
428
|
+
pen.line(
|
|
429
|
+
[row.points],
|
|
430
|
+
{
|
|
431
|
+
alpha,
|
|
432
|
+
lane: one.depth,
|
|
433
|
+
shade: OWN,
|
|
434
|
+
tone: TONE.moon,
|
|
435
|
+
width: Math.max(0.8, 0.05 * one.size),
|
|
436
|
+
},
|
|
437
|
+
spot,
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// A whole animal alight, bell, arms and tentacles at once, which is what
|
|
442
|
+
// most of the sea's bioluminescence looks like and is why the reach here
|
|
443
|
+
// goes well past the margin.
|
|
444
|
+
const wash = washOf(one)
|
|
445
|
+
if (wash) {
|
|
446
|
+
pen.light(one.x, one.y, {
|
|
447
|
+
alpha: JELLY_LIGHT * wash.glow * dark,
|
|
448
|
+
fall: 2.2,
|
|
449
|
+
lane: one.depth,
|
|
450
|
+
shade: OWN,
|
|
451
|
+
tone: TONE.moon,
|
|
452
|
+
width: wash.reach,
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (const flare of sparks(one)) {
|
|
457
|
+
// A light is a middle and a reach, so it cannot be handed a spot and has
|
|
458
|
+
// to be stood in the water here. Mirrored and then turned, in that
|
|
459
|
+
// order, which is the order `Spot` says and the order everything in this
|
|
460
|
+
// scene has got wrong at least once.
|
|
461
|
+
const across = flare.x * one.size * one.facing
|
|
462
|
+
const down = flare.y * one.size
|
|
463
|
+
const cos = Math.cos(one.tilt)
|
|
464
|
+
const sin = Math.sin(one.tilt)
|
|
465
|
+
|
|
466
|
+
pen.light(one.x + across * cos - down * sin, one.y + across * sin + down * cos, {
|
|
467
|
+
alpha: JELLY_LIGHT * flare.glow * dark,
|
|
468
|
+
fall: 1.6,
|
|
469
|
+
lane: one.depth,
|
|
470
|
+
shade: OWN,
|
|
471
|
+
tone: TONE.moon,
|
|
472
|
+
width: one.size * 0.34,
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Crabs and starfish, which is everything down there that has legs and never
|
|
480
|
+
* uses them to leave.
|
|
481
|
+
*
|
|
482
|
+
* The legs first and the body over them, so a claw folded across the shell
|
|
483
|
+
* reads as a claw in front of a shell rather than a crack in it.
|
|
484
|
+
*/
|
|
485
|
+
/**
|
|
486
|
+
* The colony, standing out of the sand.
|
|
487
|
+
*
|
|
488
|
+
* A line each, and nothing else. The head is the round cap on the far end of
|
|
489
|
+
* it, which costs nothing and is what a garden eel actually looks like from any
|
|
490
|
+
* distance a reader is at: a stalk with a knot on the end.
|
|
491
|
+
*
|
|
492
|
+
* One drawing an animal rather than one for the bed, because the whole of the
|
|
493
|
+
* thing is that they are not doing the same thing at the same moment. A colony
|
|
494
|
+
* drawn as one shape could not have a wave cross it.
|
|
495
|
+
*/
|
|
496
|
+
export function paintEels(pen: Pen, eels: Eels): void {
|
|
497
|
+
for (const one of eels.eels) {
|
|
498
|
+
if (one.body.length < 2) continue
|
|
499
|
+
|
|
500
|
+
const weight = far(EEL_INK, one.depth)
|
|
501
|
+
if (weight <= 0.002) continue
|
|
502
|
+
|
|
503
|
+
pen.line([one.body], {
|
|
504
|
+
lane: one.depth,
|
|
505
|
+
shade: one.y,
|
|
506
|
+
weight,
|
|
507
|
+
width: Math.max(1, EEL_GIRTH * one.size * one.girth),
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* The one that stays.
|
|
514
|
+
*
|
|
515
|
+
* Drawn in the order it would be seen: the spines stand off the body, so they
|
|
516
|
+
* go under it and the body covers their roots. The eye is punched back out of
|
|
517
|
+
* the body at a lighter weight, which is the same trick every other face in
|
|
518
|
+
* this water is drawn with.
|
|
519
|
+
*/
|
|
520
|
+
export function paintPuffers(pen: Pen, puffers: Puffers): void {
|
|
521
|
+
for (const one of puffers.puffers) {
|
|
522
|
+
if (one.body.length < 3) continue
|
|
523
|
+
|
|
524
|
+
const weight = far(PUFFER_INK, one.depth)
|
|
525
|
+
if (weight <= 0.002) continue
|
|
526
|
+
|
|
527
|
+
const mark = { lane: one.depth, shade: one.y, weight }
|
|
528
|
+
|
|
529
|
+
if (one.spines.length > 0) pen.fill(one.spines, mark)
|
|
530
|
+
pen.fill([one.body], mark)
|
|
531
|
+
pen.fill([disc(one.eye.x, one.eye.y, one.size * EYE_SIZE)], {
|
|
532
|
+
...mark,
|
|
533
|
+
weight: weight * CUT_SHADE,
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function paintWalkers(pen: Pen, walkers: Walkers): void {
|
|
539
|
+
for (const one of walkers.walkers) {
|
|
540
|
+
const weight = far(CRAWLER_INK, one.depth)
|
|
541
|
+
if (weight <= 0.002) continue
|
|
542
|
+
|
|
543
|
+
const spot = { facing: one.facing, scale: one.size, x: one.x, y: one.y }
|
|
544
|
+
const mark = { lane: one.depth, shade: one.y, weight }
|
|
545
|
+
|
|
546
|
+
pen.line(one.legs, { ...mark, width: ARM_GIRTH * one.size }, spot)
|
|
547
|
+
pen.fill(trace(one.body), mark, spot)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* What goes past overhead, and what is looking for you.
|
|
553
|
+
*
|
|
554
|
+
* Minutes apart rather than seconds, and most sittings will hold neither: a
|
|
555
|
+
* scene you can watch the whole vocabulary of in a minute is wallpaper.
|
|
556
|
+
*/
|
|
557
|
+
export function paintPassers(pen: Pen, passers: Passers): void {
|
|
558
|
+
let drawn = 0
|
|
559
|
+
for (const one of passers.passing) {
|
|
560
|
+
if (drawn++ >= MOST.passers) break
|
|
561
|
+
|
|
562
|
+
const weight = HULL_INK * one.weight
|
|
563
|
+
const spot = { facing: one.facing, scale: one.scale, x: one.x, y: one.y }
|
|
564
|
+
const mark = { lane: LANE.passer, shade: one.y, weight }
|
|
565
|
+
|
|
566
|
+
if (one.kind === "boat") {
|
|
567
|
+
pen.fill(trace(HULL), mark, spot)
|
|
568
|
+
pen.fill(trace(SCREWS), mark, spot)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (one.kind === "submarine") {
|
|
572
|
+
pen.fill(trace(SUBMARINE), mark, spot)
|
|
573
|
+
pen.fill(trace(SUB_SCREW), mark, spot)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Sonar: three rings, staggered, going out and losing themselves in the
|
|
577
|
+
// water. One ring would be a bubble.
|
|
578
|
+
if (one.kind === "sonar") {
|
|
579
|
+
for (const stagger of PING_RINGS) {
|
|
580
|
+
const at = ringAt(one.along, stagger)
|
|
581
|
+
if (!at) continue
|
|
582
|
+
|
|
583
|
+
pen.line([ring(one.x, one.y, at.reach * one.scale)], {
|
|
584
|
+
alpha: PING_INK * at.weight * one.weight,
|
|
585
|
+
lane: LANE.passer,
|
|
586
|
+
shade: OWN,
|
|
587
|
+
tone: TONE.ink,
|
|
588
|
+
// Thick where it leaves and a hairline by the time it is spent. A
|
|
589
|
+
// front carries what it was sent with, spread over a circle that
|
|
590
|
+
// keeps growing.
|
|
591
|
+
width: Math.max(1, Math.round(4 * at.weight)),
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// The churn a hull leaves: white water, filled rather than outlined, so
|
|
598
|
+
// overlapping puffs merge into one patch instead of piling their alphas into
|
|
599
|
+
// a bright knot wherever three of them meet.
|
|
600
|
+
let churned = 0
|
|
601
|
+
for (const puff of passers.wake) {
|
|
602
|
+
if (churned++ >= MOST.froth) break
|
|
603
|
+
|
|
604
|
+
pen.fill([disc(puff.x, puff.y, puff.r)], {
|
|
605
|
+
alpha: Math.min(1, (1 - puff.age) / FROTH_LET_GO),
|
|
606
|
+
lane: LANE.froth,
|
|
607
|
+
shade: puff.y,
|
|
608
|
+
weight: FROTH_INK * (1 - puff.age),
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
}
|