@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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The light in the water, and what is falling through it.
|
|
3
|
+
*
|
|
4
|
+
* Shafts from the surface, marine snow, and the bubbles off a vent. The
|
|
5
|
+
* numbers are `Seascape.qml`'s, because the two renderers are meant to be the
|
|
6
|
+
* same sea.
|
|
7
|
+
*/
|
|
8
|
+
import type { Drift } from "../drift.ts"
|
|
9
|
+
import { type Rays, SPREAD } from "../rays.ts"
|
|
10
|
+
import { OWN, type Pen, type Point, TONE } from "./pen.ts"
|
|
11
|
+
|
|
12
|
+
/** How dark each of these may be, as a share of full ink. */
|
|
13
|
+
const RAY_INK = 0.07
|
|
14
|
+
const SNOW_INK = 0.15
|
|
15
|
+
const BUBBLE_INK = 0.3
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How much light there is to make shafts out of, at the hour it is.
|
|
19
|
+
*
|
|
20
|
+
* Not none at night. A moon over calm water throws the same shafts a sun does,
|
|
21
|
+
* and a scene whose light went out altogether at dusk would be a scene that
|
|
22
|
+
* turned into a black rectangle for half of every day.
|
|
23
|
+
*/
|
|
24
|
+
const MOON_RAYS = 0.28
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How many copies each shaft is drawn as, and how much shorter each one inside
|
|
28
|
+
* it is.
|
|
29
|
+
*
|
|
30
|
+
* The plies are drawn one over another, so at any height they are the same fade
|
|
31
|
+
* repeated, and copies hung to the same depth step down at exactly the same
|
|
32
|
+
* heights: the eye is offered one edge as strong as all of them. Staggering
|
|
33
|
+
* them gives each step its own height, and the core of the shaft runs out above
|
|
34
|
+
* the glow around it, which is what a shaft does.
|
|
35
|
+
*/
|
|
36
|
+
const PLIES = 4
|
|
37
|
+
const TAPER = 0.13
|
|
38
|
+
|
|
39
|
+
/** How many bubbles are ever drawn at once. */
|
|
40
|
+
const BUBBLES = 96
|
|
41
|
+
|
|
42
|
+
/** Where these sit against everything else. */
|
|
43
|
+
const LANE = { ray: -1 }
|
|
44
|
+
|
|
45
|
+
export function paintRays(pen: Pen, light: Rays, daylight: number): void {
|
|
46
|
+
const overcast = MOON_RAYS + (1 - MOON_RAYS) * daylight
|
|
47
|
+
|
|
48
|
+
for (const ray of light.rays) {
|
|
49
|
+
const slope = Math.tan(ray.tilt)
|
|
50
|
+
const mouth = ray.span / 2
|
|
51
|
+
const hem = (ray.span * SPREAD) / 2
|
|
52
|
+
const lit = (RAY_INK / PLIES) * overcast * ray.glow
|
|
53
|
+
if (lit <= 0) continue
|
|
54
|
+
|
|
55
|
+
for (let ply = 0; ply < PLIES; ply++) {
|
|
56
|
+
const inset = 1 - ply / PLIES
|
|
57
|
+
const drop = ray.reach * (1 - ply * TAPER)
|
|
58
|
+
const lean = drop * slope
|
|
59
|
+
const lip = mouth * inset
|
|
60
|
+
const flare = (mouth + (hem - mouth) * (ray.reach > 0 ? drop / ray.reach : 0)) * inset
|
|
61
|
+
|
|
62
|
+
pen.fill(
|
|
63
|
+
[
|
|
64
|
+
[
|
|
65
|
+
{ x: ray.x - lip, y: 0 },
|
|
66
|
+
{ x: ray.x + lip, y: 0 },
|
|
67
|
+
{ x: ray.x + flare + lean, y: drop },
|
|
68
|
+
{ x: ray.x - flare + lean, y: drop },
|
|
69
|
+
],
|
|
70
|
+
],
|
|
71
|
+
{
|
|
72
|
+
alpha: lit,
|
|
73
|
+
fade: [0, drop],
|
|
74
|
+
lane: LANE.ray,
|
|
75
|
+
spent: true,
|
|
76
|
+
tone: TONE.ink,
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Marine snow, sorted into the fish by depth rather than laid over them, so a
|
|
85
|
+
* near mote passes in front of a far animal and the water gets a thickness the
|
|
86
|
+
* fish alone cannot give it.
|
|
87
|
+
*/
|
|
88
|
+
export function paintSnow(pen: Pen, drift: Drift): void {
|
|
89
|
+
for (const mote of drift.motes) {
|
|
90
|
+
pen.fill([disc(mote.x, mote.y, mote.r)], {
|
|
91
|
+
lane: mote.depth,
|
|
92
|
+
shade: mote.y,
|
|
93
|
+
weight: SNOW_INK * mote.depth,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Rings rather than discs. A filled circle at this size is a dot, and a dot
|
|
98
|
+
// going up is a dot going up; an outline with the water showing through it is
|
|
99
|
+
// a bubble.
|
|
100
|
+
let drawn = 0
|
|
101
|
+
for (const bubble of drift.bubbles) {
|
|
102
|
+
if (drawn++ >= BUBBLES) break
|
|
103
|
+
|
|
104
|
+
pen.line([ring(bubble.x, bubble.y, bubble.r)], {
|
|
105
|
+
alpha: BUBBLE_INK * bubble.depth,
|
|
106
|
+
lane: bubble.depth,
|
|
107
|
+
shade: OWN,
|
|
108
|
+
tone: TONE.ink,
|
|
109
|
+
width: 1,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** How many sides a small round thing is worth cutting into. */
|
|
115
|
+
const SIDES = 16
|
|
116
|
+
|
|
117
|
+
export function disc(cx: number, cy: number, r: number): Point[] {
|
|
118
|
+
const points: Point[] = []
|
|
119
|
+
for (let i = 0; i < SIDES; i++) {
|
|
120
|
+
const turn = (i / SIDES) * Math.PI * 2
|
|
121
|
+
points.push({ x: cx + r * Math.cos(turn), y: cy + r * Math.sin(turn) })
|
|
122
|
+
}
|
|
123
|
+
return points
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The same, as a line that comes back to where it started. */
|
|
127
|
+
export function ring(cx: number, cy: number, r: number): Point[] {
|
|
128
|
+
const points = disc(cx, cy, r)
|
|
129
|
+
const first = points[0]
|
|
130
|
+
if (first) points.push(first)
|
|
131
|
+
return points
|
|
132
|
+
}
|
package/src/paint/pen.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one thing a renderer is told, and the whole of it.
|
|
3
|
+
*
|
|
4
|
+
* `scene.ts` says where the ground is and how the plants are bending, which is
|
|
5
|
+
* a bed that stands still and is cut once. Everything else in this water moves:
|
|
6
|
+
* a fish is somewhere new, a mote has fallen, a shaft has breathed, and the
|
|
7
|
+
* moon is a different shape tonight than it was last night. All of that is
|
|
8
|
+
* written here, as drawings.
|
|
9
|
+
*
|
|
10
|
+
* A drawing is small on purpose. A shape, a colour to mix it from, how heavy it
|
|
11
|
+
* is, how it gives out, and how far back it stands. There is no fish in this
|
|
12
|
+
* file and no moon either: what a thing is stays in the ornament, and this says
|
|
13
|
+
* only what it looks like.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** How a drawing is drawn. */
|
|
17
|
+
export const FORM = { fill: 0, light: 2, stroke: 1, wash: 3 } as const
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Which colour it is mixed from.
|
|
21
|
+
*
|
|
22
|
+
* `water` is a weight towards the ink at the height it is drawn at, which is
|
|
23
|
+
* anything in the water lit from where the reader is standing; `shadow` is the
|
|
24
|
+
* same weight towards the surface colour, which is near rock with the light
|
|
25
|
+
* behind it. `wall` is the one thing here you can see through, and what you see
|
|
26
|
+
* through it is whatever the surface under the water is showing. The rest are
|
|
27
|
+
* the lights, and they are the only colours in this scene that are not the
|
|
28
|
+
* theme's.
|
|
29
|
+
*/
|
|
30
|
+
export const TONE = {
|
|
31
|
+
dusk: 4,
|
|
32
|
+
ink: 6,
|
|
33
|
+
moon: 3,
|
|
34
|
+
shadow: 1,
|
|
35
|
+
sun: 2,
|
|
36
|
+
surface: 5,
|
|
37
|
+
wall: 7,
|
|
38
|
+
water: 0,
|
|
39
|
+
} as const
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A light that gives out the way a shaft of sunlight in water does.
|
|
43
|
+
*
|
|
44
|
+
* `Ornament.fade`, which no single exponent draws: it holds most of its
|
|
45
|
+
* strength for the first half of its length and then goes quickly. Added to a
|
|
46
|
+
* tone rather than named as one, since it says how a thing fades rather than
|
|
47
|
+
* what colour it is.
|
|
48
|
+
*/
|
|
49
|
+
export const SPENT = 0x200
|
|
50
|
+
|
|
51
|
+
/** Read the water at the height the fragment itself is at. */
|
|
52
|
+
export const OWN = -1
|
|
53
|
+
|
|
54
|
+
export interface Mark {
|
|
55
|
+
/** How much of it there is where it starts, before it fades. */
|
|
56
|
+
alpha?: number
|
|
57
|
+
/** Where a fade down the box starts and how far it runs. No span, no fade. */
|
|
58
|
+
fade?: readonly [number, number]
|
|
59
|
+
/** How fast it gives out, whether down the box or out from a middle. */
|
|
60
|
+
fall?: number
|
|
61
|
+
lane: number
|
|
62
|
+
/** The height the water under it is read at, or `OWN`. */
|
|
63
|
+
shade?: number
|
|
64
|
+
/**
|
|
65
|
+
* How far out of focus it is, in pixels of blur.
|
|
66
|
+
*
|
|
67
|
+
* Rock only, and it is the lens rather than the water: what is a hand from
|
|
68
|
+
* the glass is further outside the field than what is a long way behind the
|
|
69
|
+
* subject. Anything with this set is drawn on its own and softened before it
|
|
70
|
+
* is laid in, so drawings meant to be softened together, a mass of rock and
|
|
71
|
+
* what is growing on it, have to be written one after another.
|
|
72
|
+
*/
|
|
73
|
+
soft?: number
|
|
74
|
+
/** Whether it ends on the light's own curve rather than on an exponent. */
|
|
75
|
+
spent?: boolean
|
|
76
|
+
/** How much of its height a round light keeps. Under 1 is a streak. */
|
|
77
|
+
thin?: number
|
|
78
|
+
tone?: number
|
|
79
|
+
weight?: number
|
|
80
|
+
/** How wide a line is, or how far a round light reaches. */
|
|
81
|
+
width?: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type Point = { x: number; y: number }
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Where a unit drawing is stood, since most of what swims is written about the
|
|
88
|
+
* origin and put in the water afterwards.
|
|
89
|
+
*
|
|
90
|
+
* `facing` is a mirror rather than a turn, and it is applied before the turn:
|
|
91
|
+
* the ornament works out the whole angle for an animal that is swimming left,
|
|
92
|
+
* so turning a mirrored drawing again is how every renderer here has got a
|
|
93
|
+
* leftward fish upside down at least once.
|
|
94
|
+
*/
|
|
95
|
+
export interface Spot {
|
|
96
|
+
facing?: number
|
|
97
|
+
scale?: number
|
|
98
|
+
tilt?: number
|
|
99
|
+
x: number
|
|
100
|
+
y: number
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Drawings into the block of numbers the renderer reads.
|
|
105
|
+
*
|
|
106
|
+
* The count goes in first and is written again at the end, because how many
|
|
107
|
+
* there are is not known until the last one is drawn and the renderer has to
|
|
108
|
+
* read it before any of them.
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* How much room one drawing is assumed to want.
|
|
112
|
+
*
|
|
113
|
+
* A ridge is a few hundred points and a disc is seventy-two, so this is over
|
|
114
|
+
* the top of almost everything and the block is sized to hold far more than a
|
|
115
|
+
* frame anyway. It is a floor under the check rather than a budget: what it
|
|
116
|
+
* buys is that a block which does run out ends on a whole drawing rather than
|
|
117
|
+
* halfway through one, so a reader walking it never falls off the end.
|
|
118
|
+
*/
|
|
119
|
+
const ELBOW = 4096
|
|
120
|
+
|
|
121
|
+
export class Pen {
|
|
122
|
+
private drawn = 0
|
|
123
|
+
private held = 0
|
|
124
|
+
private full = false
|
|
125
|
+
|
|
126
|
+
private readonly floats: Float32Array
|
|
127
|
+
private at: number
|
|
128
|
+
|
|
129
|
+
constructor(floats: Float32Array, at: number) {
|
|
130
|
+
this.floats = floats
|
|
131
|
+
this.at = at
|
|
132
|
+
this.held = at
|
|
133
|
+
this.put(0)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** How many numbers were written, once every drawing is in. */
|
|
137
|
+
close(): number {
|
|
138
|
+
this.floats[this.held] = this.drawn
|
|
139
|
+
return this.at
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fill(parts: readonly (readonly Point[])[], mark: Mark, spot?: Spot): void {
|
|
143
|
+
if (this.head(FORM.fill, mark)) this.parts(parts, spot)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
line(parts: readonly (readonly Point[])[], mark: Mark, spot?: Spot): void {
|
|
147
|
+
if (this.head(FORM.stroke, mark)) this.parts(parts, spot)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A round light, which is a middle and a reach rather than a shape. */
|
|
151
|
+
light(x: number, y: number, mark: Mark): void {
|
|
152
|
+
if (!this.head(FORM.light, mark)) return
|
|
153
|
+
this.put(1)
|
|
154
|
+
this.put(1)
|
|
155
|
+
this.put(x)
|
|
156
|
+
this.put(y)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A wash over the whole box, which is what a night is. */
|
|
160
|
+
wash(mark: Mark): void {
|
|
161
|
+
if (this.head(FORM.wash, mark)) this.put(0)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Whether there was room for the drawing that was about to be made. */
|
|
165
|
+
private head(form: number, mark: Mark): boolean {
|
|
166
|
+
if (this.full) return false
|
|
167
|
+
if (this.at + ELBOW > this.floats.length) {
|
|
168
|
+
this.full = true
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
this.drawn++
|
|
172
|
+
this.put(form)
|
|
173
|
+
this.put((mark.tone ?? TONE.water) | (mark.spent ? SPENT : 0))
|
|
174
|
+
this.put(mark.weight ?? 0)
|
|
175
|
+
this.put(mark.shade ?? OWN)
|
|
176
|
+
this.put(mark.alpha ?? 1)
|
|
177
|
+
this.put(mark.lane)
|
|
178
|
+
this.put(mark.width ?? 0)
|
|
179
|
+
this.put(mark.fall ?? 1)
|
|
180
|
+
this.put(mark.fade ? mark.fade[0] : 0)
|
|
181
|
+
this.put(mark.fade ? mark.fade[1] : 0)
|
|
182
|
+
this.put(mark.thin ?? 1)
|
|
183
|
+
this.put(mark.soft ?? 0)
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Every piece of one drawing, stood where it is seen.
|
|
189
|
+
*
|
|
190
|
+
* Several pieces rather than one, because an animal is not one outline: a
|
|
191
|
+
* fish is a body, a tail swung off its joint and a fin rooted inside the
|
|
192
|
+
* back, and they are meant to overlap. Handed over as one drawing they are
|
|
193
|
+
* filled as one silhouette; handed over as three they would each be painted
|
|
194
|
+
* over the last, and every overlap would show as a seam.
|
|
195
|
+
*/
|
|
196
|
+
private parts(parts: readonly (readonly Point[])[], spot?: Spot): void {
|
|
197
|
+
this.put(parts.length)
|
|
198
|
+
|
|
199
|
+
const scale = spot?.scale ?? 1
|
|
200
|
+
const mirror = spot?.facing ?? 1
|
|
201
|
+
const turn = spot?.tilt ?? 0
|
|
202
|
+
const cos = Math.cos(turn)
|
|
203
|
+
const sin = Math.sin(turn)
|
|
204
|
+
|
|
205
|
+
for (const points of parts) {
|
|
206
|
+
this.put(points.length)
|
|
207
|
+
for (const point of points) {
|
|
208
|
+
if (!spot) {
|
|
209
|
+
this.put(point.x)
|
|
210
|
+
this.put(point.y)
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
const x = point.x * scale * mirror
|
|
214
|
+
const y = point.y * scale
|
|
215
|
+
this.put(spot.x + x * cos - y * sin)
|
|
216
|
+
this.put(spot.y + x * sin + y * cos)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private put(value: number): void {
|
|
222
|
+
if (this.at < this.floats.length) this.floats[this.at] = value
|
|
223
|
+
this.at++
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the water sits on and what stands out of it: stones, near rock, the
|
|
3
|
+
* reef's heads, an island, and whatever is lying on the bottom.
|
|
4
|
+
*
|
|
5
|
+
* Everything here recedes the way the ground recedes rather than the way an
|
|
6
|
+
* animal does. The rock in front of the picture is the exception and is painted
|
|
7
|
+
* away from the ink instead: it is between the light and the reader, so what
|
|
8
|
+
* there is to see of it is that none of the light came back.
|
|
9
|
+
*/
|
|
10
|
+
import { type Crags, SPRIGS } from "../crags.ts"
|
|
11
|
+
import type { Reef } from "../reef.ts"
|
|
12
|
+
import {
|
|
13
|
+
BLOCK_CARD,
|
|
14
|
+
BLOCK_LINES,
|
|
15
|
+
CHEST_BANDS,
|
|
16
|
+
CHEST_BODY,
|
|
17
|
+
CHEST_LOCK,
|
|
18
|
+
LAPTOP_BASE,
|
|
19
|
+
LAPTOP_LINES,
|
|
20
|
+
LAPTOP_SCREEN,
|
|
21
|
+
type Relics,
|
|
22
|
+
SMOKER,
|
|
23
|
+
WRECK,
|
|
24
|
+
WRECK_SPAR,
|
|
25
|
+
} from "../relics.ts"
|
|
26
|
+
import type { Seabed } from "../seabed.ts"
|
|
27
|
+
import { disc } from "./light.ts"
|
|
28
|
+
import { OWN, type Pen, type Point, TONE } from "./pen.ts"
|
|
29
|
+
import type { Box } from "./sky.ts"
|
|
30
|
+
import { trace } from "./trace.ts"
|
|
31
|
+
|
|
32
|
+
/** How dark each of these may be at the front of the picture. */
|
|
33
|
+
const CRAG_INK = 0.94
|
|
34
|
+
const FLORA_INK = 0.33
|
|
35
|
+
const HAZE_INK = 0.04
|
|
36
|
+
const ISLE_INK = 0.88
|
|
37
|
+
const PLUME_INK = 0.2
|
|
38
|
+
const RELIC_INK = 0.24
|
|
39
|
+
const STONE_INK = 0.25
|
|
40
|
+
|
|
41
|
+
/** How much of a weight is punched back out for a strap, a lock or a line. */
|
|
42
|
+
const CUT_SHADE = 0.34
|
|
43
|
+
|
|
44
|
+
/** What is left of a puff's own body over the last of its life. */
|
|
45
|
+
const PLUME_LET_GO = 0.25
|
|
46
|
+
|
|
47
|
+
/** How far forward a mass has to stand before it is rock you are among rather
|
|
48
|
+
* than rock you are looking at, and what grows on it. */
|
|
49
|
+
const CRAG_NEAR = 0.8
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* How far the lens gives up on the near rock, in pixels, at the front of the
|
|
53
|
+
* water, and each mass takes its own share of it by how far forward it stands.
|
|
54
|
+
*
|
|
55
|
+
* `Seascape.qml` says the same in a blur of 0.24 out of a `blurMax` of 48.
|
|
56
|
+
*/
|
|
57
|
+
const CRAG_BLUR = 0.24 * 48
|
|
58
|
+
const PERCH_SCALE = 1.15
|
|
59
|
+
const PERCH_SHADE = 0.72
|
|
60
|
+
|
|
61
|
+
/** How thick a blade is against the strand it grows off. */
|
|
62
|
+
const BLADE_GIRTH = 0.8
|
|
63
|
+
|
|
64
|
+
/** How many relics and how much smoke are ever drawn at once. */
|
|
65
|
+
const MOST = { plume: 76, relics: 4 }
|
|
66
|
+
|
|
67
|
+
/** Where these sit against everything else. */
|
|
68
|
+
const LANE = { isle: -1.15, plume: -1.6 }
|
|
69
|
+
|
|
70
|
+
export function far(full: number, depth: number): number {
|
|
71
|
+
return HAZE_INK + (full - HAZE_INK) * Math.min(1, Math.max(0, depth))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A face of near rock, closed off past the edge it hangs from.
|
|
76
|
+
*
|
|
77
|
+
* The ground closes downwards because the ground is what the box is standing
|
|
78
|
+
* on. A wall is not: it comes in from the side and the mass of it is off the
|
|
79
|
+
* picture, so closing it under the box would fill the water in front of it as
|
|
80
|
+
* well. The roof is the same statement turned on its end.
|
|
81
|
+
*/
|
|
82
|
+
function faced(points: readonly Point[], edge: string, box: Box): Point[] {
|
|
83
|
+
const out = points.map((p) => ({ x: p.x, y: p.y }))
|
|
84
|
+
if (!out.length) return out
|
|
85
|
+
|
|
86
|
+
const first = out[0]
|
|
87
|
+
const last = out.at(-1)
|
|
88
|
+
if (!first || !last) return out
|
|
89
|
+
|
|
90
|
+
const past = 60
|
|
91
|
+
|
|
92
|
+
if (edge === "top") {
|
|
93
|
+
out.push({ x: last.x, y: -past })
|
|
94
|
+
out.push({ x: first.x, y: -past })
|
|
95
|
+
return out
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const side = edge === "left" ? -past : box.width + past
|
|
99
|
+
out.push({ x: side, y: last.y })
|
|
100
|
+
out.push({ x: side, y: first.y })
|
|
101
|
+
return out
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A ridge with the ground under it closed off, below everything it is made of.
|
|
106
|
+
*
|
|
107
|
+
* Below the ridge rather than below the box, and the difference is one that
|
|
108
|
+
* only shows up in a renderer that cares whether an outline crosses itself. The
|
|
109
|
+
* near sand sits a hundredth of the box off the bottom and rolls a good deal
|
|
110
|
+
* further than that, so parts of it are under the box already: a line drawn
|
|
111
|
+
* across at the box's own floor would pass through the ridge rather than under
|
|
112
|
+
* it, and the shape would be a bowtie in as many places as the sand dips.
|
|
113
|
+
*
|
|
114
|
+
* Nobody sees the difference. Everything below the bottom edge is off the
|
|
115
|
+
* picture either way, and what it buys is an outline that is what it looks
|
|
116
|
+
* like.
|
|
117
|
+
*/
|
|
118
|
+
export function floored(points: readonly Point[], box: Box): Point[] {
|
|
119
|
+
const out = points.map((p) => ({ x: p.x, y: p.y }))
|
|
120
|
+
const first = out[0]
|
|
121
|
+
const last = out.at(-1)
|
|
122
|
+
if (!first || !last) return out
|
|
123
|
+
|
|
124
|
+
let under = box.height
|
|
125
|
+
for (const point of out) under = Math.max(under, point.y)
|
|
126
|
+
under += 2
|
|
127
|
+
|
|
128
|
+
out.push({ x: last.x, y: under })
|
|
129
|
+
out.push({ x: first.x, y: under })
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Stones, lying where they fell.
|
|
135
|
+
*
|
|
136
|
+
* Rounded rather than drawn: at this weight a stone is a mass with a lean on
|
|
137
|
+
* it, and an outline would make it a pebble somebody had illustrated. A
|
|
138
|
+
* round-capped line is the same shape as a rectangle with a radius of half its
|
|
139
|
+
* height, which is what the other renderer draws one as.
|
|
140
|
+
*/
|
|
141
|
+
export function paintStones(pen: Pen, seabed: Seabed): void {
|
|
142
|
+
for (const stone of seabed.stones) {
|
|
143
|
+
const run = Math.max(0, stone.span - stone.rise) / 2
|
|
144
|
+
|
|
145
|
+
pen.line(
|
|
146
|
+
[
|
|
147
|
+
[
|
|
148
|
+
{ x: -run, y: 0 },
|
|
149
|
+
{ x: run, y: 0 },
|
|
150
|
+
],
|
|
151
|
+
],
|
|
152
|
+
{
|
|
153
|
+
lane: -2 + stone.depth * 0.3,
|
|
154
|
+
shade: stone.y,
|
|
155
|
+
weight: far(STONE_INK, stone.depth),
|
|
156
|
+
width: stone.rise,
|
|
157
|
+
},
|
|
158
|
+
{ tilt: stone.lean, x: stone.x, y: stone.y - stone.rise * 0.12 },
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The reef's heads: what took a place on the mound.
|
|
165
|
+
*
|
|
166
|
+
* A head is drawn the way a plant is, and which of the two kinds of drawing it
|
|
167
|
+
* carries is its own business: a stencil hands over twigs, and an animal hands
|
|
168
|
+
* over a stalk with whatever it has grown on the end of it.
|
|
169
|
+
*/
|
|
170
|
+
export function paintHeads(pen: Pen, reef: Reef): void {
|
|
171
|
+
for (const head of reef.heads) {
|
|
172
|
+
const weight = far(FLORA_INK, head.depth)
|
|
173
|
+
const mark = { lane: -3 + head.depth, shade: head.y, weight }
|
|
174
|
+
|
|
175
|
+
if (head.blades.length > 0) {
|
|
176
|
+
pen.line([head.points], { ...mark, width: head.girth })
|
|
177
|
+
pen.line(head.blades, { ...mark, width: head.girth * BLADE_GIRTH })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (head.twigs.length === 0) continue
|
|
181
|
+
|
|
182
|
+
// A stencil: the same drawing every frame, leaned where it grew and bent by
|
|
183
|
+
// the swell it is standing in. Coral does not move under its own steam, and
|
|
184
|
+
// with everything else on this rock moving it is what the movement is
|
|
185
|
+
// measured against.
|
|
186
|
+
const spot = { scale: head.scale, tilt: head.lean + head.bend, x: head.x, y: head.y }
|
|
187
|
+
for (const twig of head.twigs) {
|
|
188
|
+
pen.line(trace(twig.d), { ...mark, width: twig.width * head.scale }, spot)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The rock you are among, and what is living on it.
|
|
195
|
+
*
|
|
196
|
+
* Last of the ground and over everything including the boats: on the days this
|
|
197
|
+
* water has a roof, the roof is between the picture and the surface, and a hull
|
|
198
|
+
* drawn through it would be a hull inside the rock.
|
|
199
|
+
*/
|
|
200
|
+
export function paintCrags(pen: Pen, crags: Crags, box: Box): void {
|
|
201
|
+
for (const crag of crags.rocks) {
|
|
202
|
+
const weight = CRAG_INK * crag.depth
|
|
203
|
+
const near = crag.depth >= CRAG_NEAR
|
|
204
|
+
const adrift = near ? (crag.depth - CRAG_NEAR) / Math.max(0.01, 1 - CRAG_NEAR) : 0
|
|
205
|
+
const lane = near ? crag.depth + 0.5 : crag.depth
|
|
206
|
+
const soft = CRAG_BLUR * adrift
|
|
207
|
+
|
|
208
|
+
pen.fill([faced(crag.outline, crag.edge, box)], {
|
|
209
|
+
lane,
|
|
210
|
+
shade: OWN,
|
|
211
|
+
soft,
|
|
212
|
+
tone: TONE.shadow,
|
|
213
|
+
weight,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// What is growing on it, one colony at a time. Drawn a shade off the rock
|
|
217
|
+
// rather than in the rock's own weight: a silhouette on a silhouette is a
|
|
218
|
+
// silhouette, and the whole reason these are here is that a bare wall is a
|
|
219
|
+
// shape somebody cut out of paper.
|
|
220
|
+
for (const perch of crag.perches) {
|
|
221
|
+
const unit = perch.size * PERCH_SCALE * crag.depth
|
|
222
|
+
// The drawing grows up out of nothing; the perch says which way is out of
|
|
223
|
+
// the rock, and a quarter turn is the difference between them.
|
|
224
|
+
const spot = { scale: unit, tilt: perch.lean + Math.PI / 2, x: perch.x, y: perch.y }
|
|
225
|
+
|
|
226
|
+
for (const twig of SPRIGS[perch.kind]) {
|
|
227
|
+
pen.line(
|
|
228
|
+
trace(twig.d),
|
|
229
|
+
{
|
|
230
|
+
lane,
|
|
231
|
+
shade: perch.y,
|
|
232
|
+
soft,
|
|
233
|
+
tone: TONE.shadow,
|
|
234
|
+
weight: weight * PERCH_SHADE,
|
|
235
|
+
width: twig.width * unit,
|
|
236
|
+
},
|
|
237
|
+
spot,
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// An island, where the ground climbed the whole way and came out. It goes
|
|
244
|
+
// dark rather than bright for the reason near rock does: a shore lit from
|
|
245
|
+
// behind is a silhouette, and everybody has seen that one.
|
|
246
|
+
if (crags.isle) {
|
|
247
|
+
pen.fill([floored(crags.isle.outline, box)], {
|
|
248
|
+
lane: LANE.isle,
|
|
249
|
+
shade: OWN,
|
|
250
|
+
tone: TONE.shadow,
|
|
251
|
+
weight: ISLE_INK,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* What is lying on the bottom, and what is coming out of it.
|
|
258
|
+
*
|
|
259
|
+
* Behind the sand rather than in front of it: a wreck lies half buried, and the
|
|
260
|
+
* sand is what buries it.
|
|
261
|
+
*/
|
|
262
|
+
export function paintRelics(pen: Pen, wreckage: Relics): void {
|
|
263
|
+
let drawn = 0
|
|
264
|
+
for (const one of wreckage.relics) {
|
|
265
|
+
if (drawn++ >= MOST.relics) break
|
|
266
|
+
|
|
267
|
+
const weight = RELIC_INK * one.depth
|
|
268
|
+
const spot = { scale: one.scale, tilt: one.lean, x: one.x, y: one.y }
|
|
269
|
+
const mark = { lane: -3 + one.depth, shade: one.y, weight }
|
|
270
|
+
const cut = { ...mark, weight: weight * CUT_SHADE }
|
|
271
|
+
|
|
272
|
+
const body =
|
|
273
|
+
one.kind === "wreck"
|
|
274
|
+
? WRECK
|
|
275
|
+
: one.kind === "smoker"
|
|
276
|
+
? SMOKER
|
|
277
|
+
: one.kind === "chest"
|
|
278
|
+
? CHEST_BODY
|
|
279
|
+
: BLOCK_CARD
|
|
280
|
+
pen.fill(trace(body), mark, spot)
|
|
281
|
+
|
|
282
|
+
// The wreck's spar, which is what stops leaning verticals reading
|
|
283
|
+
// as two dead trees.
|
|
284
|
+
if (one.kind === "wreck") {
|
|
285
|
+
pen.line([WRECK_SPAR], { ...mark, width: 0.03 * one.scale }, spot)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// The code block's three lines, cut out of the card in the surface rather
|
|
289
|
+
// than drawn in ink, the way a fish's eye is. A snippet is a card with
|
|
290
|
+
// light coming through it, not a card with marks on it.
|
|
291
|
+
if (one.kind === "block") {
|
|
292
|
+
for (const line of BLOCK_LINES) pen.fill(trace(line), cut, spot)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// The chest, and the laptop still open on its lid. The straps and the lock
|
|
296
|
+
// are cut out in the surface; the screen is the one thing in the whole sea
|
|
297
|
+
// drawn at full ink.
|
|
298
|
+
if (one.kind === "chest") {
|
|
299
|
+
for (const band of CHEST_BANDS) pen.fill(trace(band), cut, spot)
|
|
300
|
+
pen.fill(trace(CHEST_LOCK), cut, spot)
|
|
301
|
+
pen.fill(trace(LAPTOP_BASE), mark, spot)
|
|
302
|
+
pen.fill(trace(LAPTOP_SCREEN), { ...mark, tone: TONE.ink, weight: 1 }, spot)
|
|
303
|
+
for (const row of LAPTOP_LINES) {
|
|
304
|
+
pen.fill(trace(row), { ...mark, tone: TONE.surface, weight: 1 }, spot)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// What the smoker is pouring: the only thing on the bottom that moves under
|
|
310
|
+
// its own steam rather than the water's, which is what makes a vent read as
|
|
311
|
+
// hot. Opaque for most of its life, like everything else that is a thing
|
|
312
|
+
// rather than light, and letting go of the last of the alpha as well, because
|
|
313
|
+
// by then the puff is the colour of what is behind it.
|
|
314
|
+
let puffed = 0
|
|
315
|
+
for (const puff of wreckage.plume) {
|
|
316
|
+
if (puffed++ >= MOST.plume) break
|
|
317
|
+
|
|
318
|
+
pen.fill([disc(puff.x, puff.y, puff.r)], {
|
|
319
|
+
alpha: Math.min(1, (1 - puff.age) / PLUME_LET_GO),
|
|
320
|
+
lane: LANE.plume,
|
|
321
|
+
shade: puff.y,
|
|
322
|
+
weight: PLUME_INK * Math.sqrt(Math.max(0, 1 - puff.age)),
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
}
|