@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
package/src/paint/bed.ts
ADDED
|
@@ -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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A theme's colour, as numbers a renderer can mix.
|
|
3
|
+
*
|
|
4
|
+
* A renderer is handed its two colours as whatever a page's stylesheet hands
|
|
5
|
+
* back, and needs them as sRGB in 0 to 1 to mix the water from them.
|
|
6
|
+
*
|
|
7
|
+
* ## Why the colours are parsed rather than measured
|
|
8
|
+
*
|
|
9
|
+
* The obvious way to turn a theme token into numbers is to fill a pixel with it
|
|
10
|
+
* and read the pixel back, which is the browser doing the colour conversion for
|
|
11
|
+
* you. It is also wrong here. A browser with fingerprinting protection on
|
|
12
|
+
* (Librewolf, Brave, Firefox with `resistFingerprinting`) perturbs canvas
|
|
13
|
+
* readback on purpose, so the same token comes back a slightly different colour
|
|
14
|
+
* on every load, and an ornament whose hue wanders between reloads is an
|
|
15
|
+
* ornament that looks broken.
|
|
16
|
+
*
|
|
17
|
+
* Parsing is therefore the only stable answer. Only what a computed style can
|
|
18
|
+
* actually hand back has to be understood, which is `rgb()` for anything the
|
|
19
|
+
* page states in sRGB and `oklch()` for the theme tokens, since a computed
|
|
20
|
+
* colour keeps the space it was authored in. Anything else returns null and the
|
|
21
|
+
* caller falls back to colours of its own.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type Rgb = readonly [number, number, number]
|
|
25
|
+
|
|
26
|
+
const clamp01 = (value: number): number => Math.min(1, Math.max(0, value))
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Oklab to linear sRGB, then to sRGB. Ottosson's matrices, as published.
|
|
30
|
+
*
|
|
31
|
+
* Out-of-gamut values are clamped per channel, which is what a browser does
|
|
32
|
+
* when it paints one.
|
|
33
|
+
*/
|
|
34
|
+
function oklabToRgb(lightness: number, a: number, b: number): Rgb {
|
|
35
|
+
const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3
|
|
36
|
+
const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3
|
|
37
|
+
const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3
|
|
38
|
+
|
|
39
|
+
const linear = [
|
|
40
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
41
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
42
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
return linear.map((channel) => {
|
|
46
|
+
const value = clamp01(channel)
|
|
47
|
+
return value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055
|
|
48
|
+
}) as unknown as Rgb
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const numbers = (css: string): number[] =>
|
|
52
|
+
css
|
|
53
|
+
.slice(css.indexOf("(") + 1, css.lastIndexOf(")"))
|
|
54
|
+
.split(/[\s,/]+/)
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.map((part) => (part.endsWith("%") ? Number.parseFloat(part) / 100 : Number.parseFloat(part)))
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A computed colour as sRGB in 0 to 1, or null for anything not understood,
|
|
60
|
+
* which includes the transparent an element with no background of its own
|
|
61
|
+
* reports.
|
|
62
|
+
*/
|
|
63
|
+
export function parseColour(css: string): Rgb | null {
|
|
64
|
+
const value = css.trim().toLowerCase()
|
|
65
|
+
|
|
66
|
+
if (value.startsWith("rgb")) {
|
|
67
|
+
const [r, g, b, alpha] = numbers(value)
|
|
68
|
+
if (r == null || g == null || b == null || alpha === 0) return null
|
|
69
|
+
return [clamp01(r / 255), clamp01(g / 255), clamp01(b / 255)]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (value.startsWith("oklch")) {
|
|
73
|
+
const [lightness, chroma, hue, alpha] = numbers(value)
|
|
74
|
+
if (lightness == null || chroma == null || alpha === 0) return null
|
|
75
|
+
const angle = ((hue ?? 0) * Math.PI) / 180
|
|
76
|
+
return oklabToRgb(lightness, chroma * Math.cos(angle), chroma * Math.sin(angle))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (value.startsWith("oklab")) {
|
|
80
|
+
const [lightness, a, b, alpha] = numbers(value)
|
|
81
|
+
if (lightness == null || a == null || b == null || alpha === 0) return null
|
|
82
|
+
return oklabToRgb(lightness, a, b)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
@@ -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
|
+
}
|
package/src/paint/ink.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What colour a drawing comes out, and how much of it there is.
|
|
3
|
+
*
|
|
4
|
+
* Every colour in this scene is worked out from two: the theme's own ink and
|
|
5
|
+
* the surface it is standing on. That is what lets a theme switch recolour the
|
|
6
|
+
* whole sea without a single drawing knowing it happened, and it is why nothing
|
|
7
|
+
* in `paint/` names a colour. A drawing says how heavy it is and which of the
|
|
8
|
+
* two it is mixed from, and the answer is worked out here.
|
|
9
|
+
*
|
|
10
|
+
* The exceptions are the lights, which are the only colours in the picture that
|
|
11
|
+
* are not the theme's. A sun is the colour a sun is.
|
|
12
|
+
*
|
|
13
|
+
* ## Where it is read
|
|
14
|
+
*
|
|
15
|
+
* A card reads it per pixel, in the fragment shader, so a shape spanning half
|
|
16
|
+
* the box takes the water's own gradient across its whole height. A canvas
|
|
17
|
+
* cannot: a fill is one colour, or one gradient built before the fill. So the
|
|
18
|
+
* same arithmetic is here in TypeScript, and `flat.ts` decides how many times to
|
|
19
|
+
* ask it. The two agree on every number, which is the point of the file.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Rgb } from "./colour.ts"
|
|
23
|
+
import { TONE } from "./pen.ts"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How much ink is in the water at the surface.
|
|
27
|
+
*
|
|
28
|
+
* The water is not the page's background with things drawn on it. It is a
|
|
29
|
+
* column, lightest at the top where the light comes in and giving out with
|
|
30
|
+
* depth, and this is how far from the background the lit top of it stands.
|
|
31
|
+
*/
|
|
32
|
+
const LIT = 0.13
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* How fast the light gives out going down.
|
|
36
|
+
*
|
|
37
|
+
* Over 1, so the top half of the box keeps most of it and the bottom goes
|
|
38
|
+
* quickly, which is what looking down a water column actually looks like. A
|
|
39
|
+
* straight line reads as a printed gradient.
|
|
40
|
+
*/
|
|
41
|
+
const SINKAGE = 2.1
|
|
42
|
+
|
|
43
|
+
/** The three lights, which are the only colours here the theme does not own. */
|
|
44
|
+
const SUNLIGHT: Rgb = [1, 0.93, 0.76]
|
|
45
|
+
const MOONLIGHT: Rgb = [0.95, 0.92, 0.85]
|
|
46
|
+
const DUSKLIGHT: Rgb = [0.91, 0.64, 0.3]
|
|
47
|
+
|
|
48
|
+
export interface Palette {
|
|
49
|
+
/** The theme's own ink, which everything in the water is drawn towards. */
|
|
50
|
+
ink: Rgb
|
|
51
|
+
/** What the water is standing on, which everything fades into. */
|
|
52
|
+
surface: Rgb
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mix = (a: Rgb, b: Rgb, at: number): Rgb => [
|
|
56
|
+
a[0] + (b[0] - a[0]) * at,
|
|
57
|
+
a[1] + (b[1] - a[1]) * at,
|
|
58
|
+
a[2] + (b[2] - a[2]) * at,
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
/** The water at a height in the box, lit from above and giving up with depth. */
|
|
62
|
+
export function waterAt(palette: Palette, y: number, height: number): Rgb {
|
|
63
|
+
const t = Math.min(1, Math.max(0, y / Math.max(1, height)))
|
|
64
|
+
return mix(palette.surface, mix(palette.surface, palette.ink, LIT), (1 - t) ** SINKAGE)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One drawing's colour: the water where it is, with its own weight mixed in.
|
|
69
|
+
*
|
|
70
|
+
* `weight` is how much of the thing stopped the light. A body is one weight,
|
|
71
|
+
* because a body at this distance is one shape the light did not come back
|
|
72
|
+
* through, and that is what a silhouette is.
|
|
73
|
+
*/
|
|
74
|
+
export function hueOf(
|
|
75
|
+
palette: Palette,
|
|
76
|
+
tone: number,
|
|
77
|
+
weight: number,
|
|
78
|
+
y: number,
|
|
79
|
+
height: number,
|
|
80
|
+
): Rgb {
|
|
81
|
+
const base = waterAt(palette, y, height)
|
|
82
|
+
const w = Math.min(1, Math.max(0, weight))
|
|
83
|
+
|
|
84
|
+
switch (tone & 0xff) {
|
|
85
|
+
case TONE.shadow:
|
|
86
|
+
return mix(base, palette.surface, w)
|
|
87
|
+
case TONE.sun:
|
|
88
|
+
return SUNLIGHT
|
|
89
|
+
case TONE.moon:
|
|
90
|
+
return MOONLIGHT
|
|
91
|
+
case TONE.dusk:
|
|
92
|
+
return DUSKLIGHT
|
|
93
|
+
case TONE.surface:
|
|
94
|
+
return palette.surface
|
|
95
|
+
case TONE.ink:
|
|
96
|
+
return palette.ink
|
|
97
|
+
// The water column, which on a page is the page itself: whatever is behind
|
|
98
|
+
// the canvas is what you see through it, so nothing is painted at all.
|
|
99
|
+
case TONE.wall:
|
|
100
|
+
return base
|
|
101
|
+
default:
|
|
102
|
+
return mix(base, palette.ink, w)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const css = (hue: Rgb, alpha: number): string =>
|
|
107
|
+
`rgba(${Math.round(hue[0] * 255)},${Math.round(hue[1] * 255)},${Math.round(hue[2] * 255)},${alpha})`
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What is left of a shape's own light at a height in the box.
|
|
111
|
+
*
|
|
112
|
+
* Two shapes of falling off and no others. Everything that fades down the box
|
|
113
|
+
* takes the same exponent, because a light in water does not thin in a straight
|
|
114
|
+
* line and a straight line is what two ends of a gradient draw. `spent` is the
|
|
115
|
+
* one that does not: it holds most of its strength for the first half of its
|
|
116
|
+
* length and then goes quickly, which is what a shaft of sunlight does and no
|
|
117
|
+
* single exponent draws.
|
|
118
|
+
*/
|
|
119
|
+
export function leftAt(
|
|
120
|
+
alpha: number,
|
|
121
|
+
fadeTop: number,
|
|
122
|
+
fadeSpan: number,
|
|
123
|
+
fall: number,
|
|
124
|
+
spent: boolean,
|
|
125
|
+
y: number,
|
|
126
|
+
): number {
|
|
127
|
+
if (fadeSpan <= 0) return alpha
|
|
128
|
+
const down = Math.min(1, Math.max(0, (y - fadeTop) / fadeSpan))
|
|
129
|
+
if (spent) {
|
|
130
|
+
const held = 1 - down * down
|
|
131
|
+
return alpha * held * held
|
|
132
|
+
}
|
|
133
|
+
return alpha * (1 - down) ** fall
|
|
134
|
+
}
|