@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,231 @@
1
+ /**
2
+ * The ground, and everything rooted in it.
3
+ *
4
+ * The sand, the hills standing behind it, the reef's mound, the wall at the
5
+ * back, and every plant growing on any of them. It is the one part of the
6
+ * picture that does not move under its own steam: a plant leans on the current
7
+ * and nothing here goes anywhere.
8
+ *
9
+ * ## Why a renderer on a card does not use this
10
+ *
11
+ * Because it can do better. A plant is a short tree of limbs, so a renderer
12
+ * with a vertex shader takes the shape once at rest and bends it on the card,
13
+ * and a frame after that costs two numbers a plant. `flora.ts` hands out
14
+ * `madeOf` for exactly that, and the desktop's renderer reads it.
15
+ *
16
+ * This is the other bargain, and it is the right one for a processor: take the
17
+ * points the plant has already cut for itself and stroke them. `flora.ts` only
18
+ * cuts a plant when the water has moved it far enough to be worth redrawing, so
19
+ * the tolerance it was given is what decides how much of a frame this costs.
20
+ *
21
+ * Both draw the same bed. What differs is where the bending happens.
22
+ */
23
+ import type { Caustics } from "../caustics.ts"
24
+ import type { Flora } from "../flora.ts"
25
+ import type { Reef } from "../reef.ts"
26
+ import type { Seabed } from "../seabed.ts"
27
+ import { type Pen, type Point, TONE } from "./pen.ts"
28
+ import { far, floored } from "./rock.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 FLORA_INK = 0.33
34
+ const SAND_INK = 0.19
35
+ const STONE_INK = 0.25
36
+
37
+ /** How thick a blade is against the strand it grows off. */
38
+ const BLADE_GIRTH = 0.8
39
+
40
+ /**
41
+ * How stout a strand has to be before its blades are drawn thinner than it.
42
+ *
43
+ * Under this the difference between the two widths is less than the width of a
44
+ * line, so the whole plant is drawn at one weight and nothing is lost. Over it
45
+ * the strand reads as the thing the blades hang off.
46
+ */
47
+ const BLADE_SPLIT = 0.5
48
+
49
+ /**
50
+ * Where each band of ground sorts.
51
+ *
52
+ * Ground is on a scale of its own, behind everything that swims, and the bands
53
+ * are kept a fifth of a lane apart so a hill never crosses the sand in front of
54
+ * it. The sand is flat rather than a function of distance because there is only
55
+ * one of it and it is the nearest thing in the picture that is not rock.
56
+ */
57
+ const LANE = { cliff: -3.4, hill: -3, mound: -2.6, sand: -2 }
58
+
59
+ /**
60
+ * The water itself, which everything else is drawn into.
61
+ *
62
+ * Behind every band of ground and behind the murk, so it is the one thing in
63
+ * the picture with nothing at all in front of it. Its tone is the wall, which
64
+ * is whatever the surface underneath the water happens to be showing: a
65
+ * desktop's own wallpaper, or a page's own background.
66
+ */
67
+ export function paintWater(pen: Pen, box: Box): void {
68
+ pen.fill(
69
+ [
70
+ [
71
+ { x: 0, y: 0 },
72
+ { x: box.width, y: 0 },
73
+ { x: box.width, y: box.height },
74
+ { x: 0, y: box.height },
75
+ ],
76
+ ],
77
+ { lane: -9, tone: TONE.wall, weight: 0 },
78
+ )
79
+ }
80
+
81
+ /**
82
+ * The ground at every distance it has, each band shut under the box.
83
+ *
84
+ * Far first, near last, which is the order they are drawn in as well as the
85
+ * order they stand in: a band closed off downwards covers everything behind it
86
+ * from its own ridge to the bottom edge, so drawing the near sand first would
87
+ * leave every hill behind it painted over its face.
88
+ */
89
+ export function paintGround(pen: Pen, seabed: Seabed, reef: Reef | null, box: Box): void {
90
+ for (const cliff of seabed.cliffs) {
91
+ pen.fill([floored(cliff.ridge, box)], {
92
+ lane: LANE.cliff + cliff.depth,
93
+ shade: box.height * 0.5,
94
+ weight: far(SAND_INK, cliff.depth),
95
+ })
96
+ }
97
+
98
+ for (const band of seabed.ranges) {
99
+ pen.fill([floored(band.ridge, box)], {
100
+ lane: LANE.hill + band.depth,
101
+ weight: far(SAND_INK, band.depth),
102
+ })
103
+ }
104
+
105
+ if (reef) {
106
+ pen.fill([floored(reef.crest, box)], {
107
+ lane: LANE.mound + reef.depth,
108
+ weight: far(STONE_INK, reef.depth),
109
+ })
110
+ }
111
+
112
+ pen.fill([floored(seabed.ridge, box)], { lane: LANE.sand, weight: SAND_INK })
113
+ }
114
+
115
+ /**
116
+ * Every plant, at the distance it grew at.
117
+ *
118
+ * A plant is a strand and whatever hangs off it, both handed over as points the
119
+ * plant cut for itself, and a coral is a stencil that never moves. Which of the
120
+ * two a plant is, is its own business and not read here beyond that: anything
121
+ * carrying twigs is drawn as twigs and anything else is drawn as lines.
122
+ */
123
+ /**
124
+ * How bright the light on the ground gets, and the least of it at night.
125
+ *
126
+ * Faint. This palette is one colour and its shadow, so a bright web on the sand
127
+ * stops being water and starts being decoration at a threshold well under where
128
+ * instinct puts it. What it is worth is that the ground is no longer one flat
129
+ * tone: a ridge with light moving over it has a shape, and a ridge without has
130
+ * an outline.
131
+ */
132
+ const CAUSTIC_LIGHT = 0.32
133
+
134
+ const MOON_CAUSTIC = 0.22
135
+
136
+ /**
137
+ * How wide one patch of it is against the box, how far apart they are put along
138
+ * a ridge, and how flat.
139
+ *
140
+ * Laid as a run of soft round lights rather than as a line, because a line has
141
+ * one strength for the whole of its length and the entire point of a caustic is
142
+ * that it does not. Overlapping blobs at their own strengths are what a bright
143
+ * band with a dark end is made of here.
144
+ */
145
+ const CAUSTIC_REACH = 0.026
146
+
147
+ const CAUSTIC_STEP = 0.011
148
+
149
+ const CAUSTIC_FLAT = 0.42
150
+
151
+ /** Under this there is nothing to see and a drawing would be a drawing wasted. */
152
+ const CAUSTIC_SEEN = 0.06
153
+
154
+ /**
155
+ * The light on the ground, ridge by ridge.
156
+ *
157
+ * Walked along the outlines the bed already has rather than laid over the box,
158
+ * since the only ground a reader can see is the top of each band and that is
159
+ * exactly what a ridge is. Each one is asked at its own distance, so the bands
160
+ * are lit differently and the bed keeps the depth it is drawn with.
161
+ */
162
+ export function paintCaustics(
163
+ pen: Pen,
164
+ seabed: Seabed,
165
+ caustics: Caustics,
166
+ box: Box,
167
+ daylight: number,
168
+ ): void {
169
+ const overcast = MOON_CAUSTIC + (1 - MOON_CAUSTIC) * daylight
170
+ const reach = box.width * CAUSTIC_REACH
171
+ const apart = Math.max(1, box.width * CAUSTIC_STEP)
172
+
173
+ const along = (ridge: readonly Point[], depth: number, lane: number): void => {
174
+ if (ridge.length < 2) return
175
+
176
+ let last = Number.NEGATIVE_INFINITY
177
+ for (const point of ridge) {
178
+ if (point.x - last < apart) continue
179
+ last = point.x
180
+
181
+ const on = caustics.at(point.x / box.width, depth)
182
+ const lit = CAUSTIC_LIGHT * on * overcast * far(1, depth)
183
+ if (lit <= CAUSTIC_SEEN * CAUSTIC_LIGHT) continue
184
+
185
+ pen.light(point.x, point.y, {
186
+ alpha: lit,
187
+ fall: 1.7,
188
+ lane,
189
+ thin: CAUSTIC_FLAT,
190
+ tone: TONE.sun,
191
+ width: reach,
192
+ })
193
+ }
194
+ }
195
+
196
+ for (const cliff of seabed.cliffs) along(cliff.ridge, cliff.depth, LANE.cliff + cliff.depth)
197
+ for (const band of seabed.ranges) along(band.ridge, band.depth, LANE.hill + band.depth)
198
+ along(seabed.ridge, 1, LANE.sand)
199
+ }
200
+
201
+ export function paintFlora(pen: Pen, flora: Flora): void {
202
+ for (const plant of flora.plants) {
203
+ const weight = far(FLORA_INK, plant.depth)
204
+ const mark = { lane: plant.depth, shade: plant.y, weight }
205
+
206
+ if (plant.twigs.length > 0) {
207
+ const spot = { scale: plant.scale, x: plant.x, y: plant.y }
208
+ for (const twig of plant.twigs) {
209
+ pen.line(trace(twig.d), { ...mark, width: twig.width * plant.scale }, spot)
210
+ }
211
+ continue
212
+ }
213
+
214
+ if (plant.points.length < 2 && plant.blades.length === 0) continue
215
+
216
+ // The strand and its limbs at the plant's own weight, and the blades at a
217
+ // share of it where the plant is stout enough for the two to read apart.
218
+ const parted = plant.girth * (1 - BLADE_GIRTH) >= BLADE_SPLIT
219
+ const strand: Point[][] = plant.points.length > 1 ? [plant.points] : []
220
+
221
+ if (parted) {
222
+ if (strand.length) pen.line(strand, { ...mark, width: plant.girth })
223
+ if (plant.blades.length) {
224
+ pen.line(plant.blades, { ...mark, width: plant.girth * BLADE_GIRTH })
225
+ }
226
+ continue
227
+ }
228
+
229
+ pen.line([...strand, ...plant.blades], { ...mark, width: plant.girth })
230
+ }
231
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * A frame of the water, read back.
3
+ *
4
+ * `pen.ts` writes drawings into a block of numbers and this walks them out
5
+ * again. It exists because there is more than one renderer: a canvas walks the
6
+ * frame with a path, a graphics card cuts it into triangles, and both of them
7
+ * are reading the same block, so the one place that knows what the numbers mean
8
+ * should be a file rather than a habit.
9
+ *
10
+ * ## Nothing is allocated
11
+ *
12
+ * A busy sea is a few thousand drawings a frame at sixty frames a second, and
13
+ * an object per drawing is a few hundred thousand objects a second for the
14
+ * collector to take away again. So a reader is one object whose fields are
15
+ * rewritten as it walks, and points stay where they are: what a renderer is
16
+ * handed is where in the block a part starts and how many points it has.
17
+ *
18
+ * Read a field while the reader is standing on the drawing it belongs to. Keep
19
+ * it and it is the next drawing's answer.
20
+ */
21
+ import { FORM, OWN, SPENT, TONE } from "./pen.ts"
22
+
23
+ export { FORM, OWN, SPENT, TONE }
24
+
25
+ /** How many numbers stand in front of the count. */
26
+ const HEAD = 6
27
+
28
+ /** How many numbers one drawing spends before its points. */
29
+ const MARK = 12
30
+
31
+ export interface Held {
32
+ /** How light it is above the water, 0 through 1. */
33
+ daylight: number
34
+ /** How much bigger than its box the scene is drawn. */
35
+ overscan: number
36
+ /** How far the frame has wandered, in px. */
37
+ swayX: number
38
+ swayY: number
39
+ /** How far it has rolled, in radians. */
40
+ tilt: number
41
+ /** Which frame of grain this is. */
42
+ turn: number
43
+ }
44
+
45
+ /**
46
+ * Where a box is pointed at the frame, for a window on a water bigger than it.
47
+ *
48
+ * A scene draws itself at its own size and is held with a hand's own wander,
49
+ * which is the header above. A plate is the other case: the water is larger
50
+ * than the box, the box is pointed at one animal in it, and the animal is
51
+ * magnified until it fills the frame. Both are the same drawing seen from a
52
+ * different place, which is why this is a camera rather than a second kind of
53
+ * frame.
54
+ */
55
+ export interface View {
56
+ /** How far in, 1 being the water's own size. */
57
+ scale: number
58
+ /** The point of the water to put in the middle of the box. */
59
+ x: number
60
+ y: number
61
+ }
62
+
63
+ /** Where the camera is holding the frame, off the front of the block. */
64
+ export function held(floats: Float32Array): Held {
65
+ return {
66
+ daylight: floats[5] ?? 1,
67
+ overscan: floats[3] ?? 1,
68
+ swayX: floats[0] ?? 0,
69
+ swayY: floats[1] ?? 0,
70
+ tilt: floats[2] ?? 0,
71
+ turn: floats[4] ?? 0,
72
+ }
73
+ }
74
+
75
+ /**
76
+ * One drawing at a time, in the order they were made.
77
+ *
78
+ * The order is the picture. Depth sorts anything solid, but two lights over one
79
+ * another are a painting rather than a stack of distances, so a renderer that
80
+ * shuffles this is a renderer drawing a different scene.
81
+ */
82
+ export class Reader {
83
+ /** How many drawings are in this frame. */
84
+ readonly count: number
85
+
86
+ form = 0
87
+ alpha = 1
88
+ fall = 1
89
+ fadeSpan = 0
90
+ fadeTop = 0
91
+ lane = 0
92
+ shade = OWN
93
+ soft = 0
94
+ thin = 1
95
+ tone = 0
96
+ weight = 0
97
+ width = 0
98
+
99
+ /** How many pieces this drawing is in. */
100
+ parts = 0
101
+
102
+ /** The block being walked, for a renderer reading points straight out of it. */
103
+ readonly floats: Float32Array
104
+
105
+ private at = 0
106
+ private read = 0
107
+ private mark = 0
108
+
109
+ constructor(floats: Float32Array) {
110
+ this.floats = floats
111
+ this.count = floats[HEAD] ?? 0
112
+ this.at = HEAD + 1
113
+ }
114
+
115
+ /** Whether the drawing is a round light rather than a shape. */
116
+ get round(): boolean {
117
+ return this.form === FORM.light
118
+ }
119
+
120
+ /** Whether it ends on the light's own curve rather than on an exponent. */
121
+ get spent(): boolean {
122
+ return (this.tone & SPENT) !== 0
123
+ }
124
+
125
+ /** Which colour it is mixed from, with the flags taken off. */
126
+ get hue(): number {
127
+ return this.tone & 0xff
128
+ }
129
+
130
+ /**
131
+ * Stand on the next drawing, or say there is not one.
132
+ *
133
+ * The points are stepped over here rather than by the caller, so a renderer
134
+ * that skips a drawing it cannot use does not have to know how long it was.
135
+ */
136
+ next(): boolean {
137
+ if (this.read >= this.count) return false
138
+ this.read++
139
+ this.at = this.stand(this.at)
140
+ return true
141
+ }
142
+
143
+ /** Where the drawing being stood on begins, for a caller keeping the place. */
144
+ get offset(): number {
145
+ return this.mark
146
+ }
147
+
148
+ /**
149
+ * Stand on the drawing at a given place, and say where the next one begins.
150
+ *
151
+ * A renderer that has to put the frame in another order reads it twice: once
152
+ * to note where each drawing is and how far back it stands, and again in the
153
+ * order it decided on. Both passes are this.
154
+ */
155
+ stand(from: number): number {
156
+ const floats = this.floats
157
+ let at = from
158
+ this.mark = from
159
+
160
+ this.form = floats[at] ?? 0
161
+ this.tone = floats[at + 1] ?? 0
162
+ this.weight = floats[at + 2] ?? 0
163
+ this.shade = floats[at + 3] ?? OWN
164
+ this.alpha = floats[at + 4] ?? 1
165
+ this.lane = floats[at + 5] ?? 0
166
+ this.width = floats[at + 6] ?? 0
167
+ this.fall = floats[at + 7] ?? 1
168
+ this.fadeTop = floats[at + 8] ?? 0
169
+ this.fadeSpan = floats[at + 9] ?? 0
170
+ this.thin = floats[at + 10] ?? 1
171
+ this.soft = floats[at + 11] ?? 0
172
+
173
+ at += MARK
174
+ this.parts = floats[at++] ?? 0
175
+ this.first = at
176
+
177
+ for (let part = 0; part < this.parts; part++) {
178
+ at += 1 + 2 * (floats[at] ?? 0)
179
+ }
180
+ return at
181
+ }
182
+
183
+ /** Where this drawing's first part begins. */
184
+ first = 0
185
+
186
+ /**
187
+ * Every piece of this drawing, as where its points start and how many.
188
+ *
189
+ * Several pieces rather than one, because an animal is not one outline: a
190
+ * fish is a body, a tail swung off its joint and a fin rooted inside the
191
+ * back, and they are meant to overlap. Filled as one silhouette they are the
192
+ * animal; filled one after another, every overlap shows as a seam.
193
+ */
194
+ each(walk: (at: number, points: number) => void): void {
195
+ const floats = this.floats
196
+ let at = this.first
197
+
198
+ for (let part = 0; part < this.parts; part++) {
199
+ const points = floats[at] ?? 0
200
+ walk(at + 1, points)
201
+ at += 1 + 2 * points
202
+ }
203
+ }
204
+ }