@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/src/perlin.ts ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Ken Perlin's gradient noise, in two dimensions, as first published.
3
+ *
4
+ * Perlin wrote it in 1983 because the computer graphics he was working on
5
+ * looked machine-made, and the film he was working on was Tron. That is the
6
+ * whole reason this file exists here: the one surface that draws it is the page
7
+ * of the world that plays that film's game, and the joke only lands if the
8
+ * noise is really his.
9
+ *
10
+ * The shape of the algorithm: every integer lattice point owns a fixed random
11
+ * unit gradient. To sample a point, take the four lattice corners around it,
12
+ * dot each corner's gradient with the vector running from that corner to the
13
+ * point, and blend the four results together. The blend runs through Perlin's
14
+ * S-curve rather than straight lerps, and that is the part that matters; with
15
+ * linear blending the lattice shows up as visible creases on every cell edge.
16
+ *
17
+ * Kept deliberately small: one seedable 2D field, no octaves, no simplex.
18
+ * Nothing else in the codebase wants coherent noise, and a general noise
19
+ * library would be a lot of unused surface. If a second caller ever needs
20
+ * fractal detail, sum a few of these at doubling frequencies rather than
21
+ * growing this file.
22
+ *
23
+ * Because the gradients come from a fixed 256-entry table, the field repeats
24
+ * every 256 units. That is the algorithm's well-known limitation, and it costs
25
+ * nothing here: the flow field samples a few dozen units across at most.
26
+ */
27
+
28
+ const SIZE = 256
29
+ const MASK = SIZE - 1
30
+
31
+ /**
32
+ * Two-dimensional gradient noise, returning roughly [-1, 1].
33
+ *
34
+ * "Roughly", because the theoretical bound is what a sample can reach when it
35
+ * lands exactly where two opposed gradients line up, which almost never
36
+ * happens. Callers that need a hard range must clamp; the flow field does not,
37
+ * since it only reads the value as an angle.
38
+ */
39
+ export type Noise2 = (x: number, y: number) => number
40
+
41
+ /**
42
+ * Gradient noise in 2D peaks at sqrt(2)/2 rather than 1, so the raw dot
43
+ * products come back short. Scaling by its reciprocal puts the field back on
44
+ * the [-1, 1] every caller expects to be handed.
45
+ */
46
+ const NORMALISE = Math.SQRT2
47
+
48
+ /**
49
+ * xorshift32, so the field is identical on every load and in every browser.
50
+ *
51
+ * `Math.random()` would do the job and would also mean the page looked subtly
52
+ * different every time it was opened, which makes an ornament feel like a
53
+ * glitch rather than a decision. A fixed seed makes the field a drawing
54
+ * somebody chose.
55
+ *
56
+ * Exported because the flow field wants the same guarantee for its riders'
57
+ * starting positions. Noise is the wrong tool for that: its values cluster
58
+ * around zero rather than spreading evenly, so twelve riders placed by noise
59
+ * all start in a knot in the middle of the screen.
60
+ */
61
+ export function makeRandom(seed: number): () => number {
62
+ let state = seed | 0 || 1
63
+ return () => {
64
+ state ^= state << 13
65
+ state ^= state >>> 17
66
+ state ^= state << 5
67
+ return (state >>> 0) / 0x1_0000_0000
68
+ }
69
+ }
70
+
71
+ /**
72
+ * One number scattered into another.
73
+ *
74
+ * Two seeds that differ in one low bit are two xorshift states that differ in
75
+ * one low bit, and those walk two neighbouring paths for their first few
76
+ * numbers: a day and the day after it laying out the same sea nudged sideways,
77
+ * or a plan that says the same thing on the first of the month as on the
78
+ * second. This is the avalanche step that stops it, one multiply and two folds,
79
+ * so a bit anywhere reaches every bit.
80
+ *
81
+ * Wanted wherever a seed is derived from something that counts rather than
82
+ * something arbitrary: a date, a day of the month, the id of an element. A seed
83
+ * somebody picked out of the air needs none of it.
84
+ */
85
+ export function stir(from: number): number {
86
+ let seed = from >>> 0
87
+ seed = Math.imul(seed ^ (seed >>> 16), 0x45d9f3b) >>> 0
88
+ return (seed ^ (seed >>> 16)) >>> 0
89
+ }
90
+
91
+ export function makeNoise2(seed: number): Noise2 {
92
+ const random = makeRandom(seed)
93
+
94
+ const permutation = new Int32Array(SIZE + SIZE + 2)
95
+ const gradientX = new Float64Array(SIZE + SIZE + 2)
96
+ const gradientY = new Float64Array(SIZE + SIZE + 2)
97
+
98
+ for (let i = 0; i < SIZE; i++) {
99
+ permutation[i] = i
100
+ const angle = random() * Math.PI * 2
101
+ gradientX[i] = Math.cos(angle)
102
+ gradientY[i] = Math.sin(angle)
103
+ }
104
+
105
+ for (let i = SIZE - 1; i > 0; i--) {
106
+ const j = Math.floor(random() * (i + 1))
107
+ const held = permutation[i]!
108
+ permutation[i] = permutation[j]!
109
+ permutation[j] = held
110
+ }
111
+
112
+ for (let i = 0; i < SIZE + 2; i++) {
113
+ permutation[SIZE + i] = permutation[i]!
114
+ gradientX[SIZE + i] = gradientX[i]!
115
+ gradientY[SIZE + i] = gradientY[i]!
116
+ }
117
+
118
+ const dot = (gradient: number, x: number, y: number): number =>
119
+ gradientX[gradient]! * x + gradientY[gradient]! * y
120
+
121
+ return (x, y) => {
122
+ const cellX = Math.floor(x)
123
+ const cellY = Math.floor(y)
124
+
125
+ const dx0 = x - cellX
126
+ const dy0 = y - cellY
127
+ const dx1 = dx0 - 1
128
+ const dy1 = dy0 - 1
129
+
130
+ const bx0 = cellX & MASK
131
+ const by0 = cellY & MASK
132
+ const bx1 = (bx0 + 1) & MASK
133
+ const by1 = (by0 + 1) & MASK
134
+
135
+ const i = permutation[bx0]!
136
+ const j = permutation[bx1]!
137
+
138
+ const g00 = permutation[i + by0]!
139
+ const g10 = permutation[j + by0]!
140
+ const g01 = permutation[i + by1]!
141
+ const g11 = permutation[j + by1]!
142
+
143
+ const sx = sCurve(dx0)
144
+ const sy = sCurve(dy0)
145
+
146
+ const top = lerp(sx, dot(g00, dx0, dy0), dot(g10, dx1, dy0))
147
+ const bottom = lerp(sx, dot(g01, dx0, dy1), dot(g11, dx1, dy1))
148
+
149
+ return lerp(sy, top, bottom) * NORMALISE
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Perlin's original ease, 3t² - 2t³: an S from (0,0) to (1,1) whose first
155
+ * derivative is zero at both ends, which is what hides the lattice.
156
+ *
157
+ * His 1997 revision uses 6t⁵ - 15t⁴ + 10t³ instead, whose *second* derivative
158
+ * also vanishes at the ends and so survives being differentiated for normal
159
+ * maps. The field here is only ever read as an angle, never differentiated, so
160
+ * the cheaper original is the honest choice as well as the historical one.
161
+ */
162
+ function sCurve(t: number): number {
163
+ return t * t * (3 - 2 * t)
164
+ }
165
+
166
+ function lerp(t: number, a: number, b: number): number {
167
+ return a + t * (b - a)
168
+ }
package/src/plenty.ts ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * How much of everything a day grew.
3
+ *
4
+ * Every count in this scene is worked out from the size of the box: so many
5
+ * plants per thousand pixels, so many fish per so much area. That is right, and
6
+ * it has one consequence nobody wanted. The box does not change, so neither
7
+ * does the sea. A desktop reseeded at midnight puts a different bed in a
8
+ * different place and grows exactly as much of it as yesterday did, and the
9
+ * thing a reader actually notices about a week of water is not where the kelp
10
+ * is, it is whether there is much of it.
11
+ *
12
+ * So the day picks a number and everything counted is counted through it. Two
13
+ * days apart are a thin sand flat and a bed you cannot see the floor through,
14
+ * and neither one is a bug.
15
+ *
16
+ * ## Why it is not evenly spread
17
+ *
18
+ * A range applied flat makes the ends as common as the middle, and then the
19
+ * spectacular day comes round often enough to stop being spectacular. Weighted
20
+ * the way it is here, most water is ordinary or a little under, a lush week is
21
+ * something you notice, and the bed you cannot see the floor through is
22
+ * something you wait for. Rare is the whole point of the top of the range.
23
+ *
24
+ * ## What it does not decide
25
+ *
26
+ * How much a renderer can afford. Each one caps what it asks for, and the caps
27
+ * are different: the desktop's bed is tessellated on the processor every frame
28
+ * and the native one uploads it once and bends it on the card, so the second
29
+ * can hold ten thousand plants where the first cannot hold one. This number is
30
+ * the day's appetite, not anybody's budget.
31
+ */
32
+
33
+ import { makeRandom } from "./perlin.ts"
34
+
35
+ /** The leanest day, as a share of an ordinary one. */
36
+ const LEAN = 0.55
37
+
38
+ /** How far above that the richest day reaches. */
39
+ const RANGE = 1.75
40
+
41
+ /**
42
+ * How the range is leaned on.
43
+ *
44
+ * Above one, and the low end is where most days land. It was picked against a
45
+ * decade of seeds, for a middle day that grows about what this scene grew
46
+ * before there was a number at all, and a top of the range you wait weeks for.
47
+ */
48
+ const BIAS = 2.0
49
+
50
+ /**
51
+ * A multiplier for anything counted, given the day's seed.
52
+ *
53
+ * `at` separates one kind of count from another where a caller wants them
54
+ * loose: the same seed and the same `at` always give the same number, so it can
55
+ * be asked for again at a refit without the sea changing under the reader.
56
+ */
57
+ export function thriving(seed: number, at = 0): number {
58
+ const random = makeRandom((seed ^ 0x5eed) + at * 0x9e37)
59
+ // Two draws thrown away first: `makeRandom` is a small integer generator and
60
+ // its opening number tracks its seed closely, which for seeds a day apart is
61
+ // a week of water sliding steadily from lean to lush.
62
+ random()
63
+ random()
64
+ return LEAN + RANGE * random() ** BIAS
65
+ }