koi-ascii 0.3.2 → 0.4.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/README.md +85 -5
- package/dist/index.d.ts +36 -0
- package/dist/index.js +1203 -894
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,934 +1,1150 @@
|
|
|
1
|
-
/**
|
|
2
|
-
var
|
|
3
|
-
SPACE: " ",
|
|
4
|
-
DOT: ".",
|
|
5
|
-
COMMA: ",",
|
|
6
|
-
QUOTE: "'",
|
|
7
|
-
BACKTICK: "`",
|
|
8
|
-
COLON: ":",
|
|
9
|
-
MINUS: "-",
|
|
10
|
-
UNDERSCORE: "_",
|
|
11
|
-
TILDE: "~",
|
|
12
|
-
EQUALS: "=",
|
|
13
|
-
PLUS: "+",
|
|
14
|
-
STAR: "*",
|
|
15
|
-
O: "o",
|
|
16
|
-
HASH: "#",
|
|
17
|
-
PERCENT: "%",
|
|
18
|
-
AT: "@",
|
|
19
|
-
LPAREN: "(",
|
|
20
|
-
RPAREN: ")",
|
|
21
|
-
SLASH: "/",
|
|
22
|
-
BSLASH: "\\",
|
|
23
|
-
PIPE: "|",
|
|
24
|
-
BIG_O: "O"
|
|
25
|
-
};
|
|
26
|
-
var CHARS = Object.values(GLYPHS);
|
|
27
|
-
var LINE_HEIGHT = 1.2;
|
|
28
|
-
function glyphDefines() {
|
|
29
|
-
return Object.keys(GLYPHS).map((k, i) => `#define G_${k} ${i}\n`).join("");
|
|
30
|
-
}
|
|
1
|
+
/** The pond's own background (`#06131a`), before time-of-day grading. */
|
|
2
|
+
var POND_BACKGROUND = "#06131a";
|
|
31
3
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
4
|
+
* The same background as the shaders take it (`#06131a` to the byte). These
|
|
5
|
+
* exact values have fed the shaders since before palettes; keep them so the
|
|
6
|
+
* default pond stays pixel-identical.
|
|
34
7
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
ctx.clip();
|
|
70
|
-
ctx.translate(i * cellW + cellW / 2, 0);
|
|
71
|
-
ctx.scale(sx, 1);
|
|
72
|
-
ctx.fillText(c, -cx, baseline);
|
|
73
|
-
ctx.restore();
|
|
74
|
-
});
|
|
75
|
-
const texture = gl.createTexture();
|
|
76
|
-
if (!texture) throw new Error("Could not allocate the glyph atlas texture");
|
|
77
|
-
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
78
|
-
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
79
|
-
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
80
|
-
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
81
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, gl.RED, gl.UNSIGNED_BYTE, canvas);
|
|
82
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
83
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
84
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
85
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
86
|
-
return {
|
|
87
|
-
texture,
|
|
88
|
-
cellW,
|
|
89
|
-
cellH,
|
|
90
|
-
fontPx
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
var NIGHT = {
|
|
94
|
-
color: [
|
|
95
|
-
.36,
|
|
96
|
-
.4,
|
|
97
|
-
.58
|
|
98
|
-
],
|
|
99
|
-
ambient: [
|
|
100
|
-
.36,
|
|
101
|
-
.35,
|
|
102
|
-
.44
|
|
103
|
-
],
|
|
104
|
-
water: [
|
|
105
|
-
.58,
|
|
106
|
-
.7,
|
|
107
|
-
1
|
|
108
|
-
],
|
|
109
|
-
sky: [
|
|
110
|
-
0,
|
|
111
|
-
.01,
|
|
112
|
-
.04
|
|
113
|
-
],
|
|
114
|
-
glint: .6,
|
|
115
|
-
caustic: .25,
|
|
116
|
-
floor: .5,
|
|
117
|
-
shadowAlpha: .4,
|
|
118
|
-
glow: .55
|
|
119
|
-
};
|
|
120
|
-
var BLUE = {
|
|
121
|
-
color: [
|
|
122
|
-
.1,
|
|
123
|
-
.11,
|
|
124
|
-
.18
|
|
125
|
-
],
|
|
126
|
-
ambient: [
|
|
127
|
-
.36,
|
|
128
|
-
.36,
|
|
129
|
-
.48
|
|
130
|
-
],
|
|
131
|
-
water: [
|
|
132
|
-
.62,
|
|
133
|
-
.68,
|
|
134
|
-
.95
|
|
135
|
-
],
|
|
136
|
-
sky: [
|
|
137
|
-
.03,
|
|
138
|
-
.02,
|
|
139
|
-
.06
|
|
140
|
-
],
|
|
141
|
-
glint: .3,
|
|
142
|
-
caustic: .2,
|
|
143
|
-
floor: .48,
|
|
144
|
-
shadowAlpha: .1,
|
|
145
|
-
glow: .5
|
|
146
|
-
};
|
|
147
|
-
var GOLDEN = {
|
|
148
|
-
color: [
|
|
149
|
-
1,
|
|
150
|
-
.6,
|
|
151
|
-
.3
|
|
152
|
-
],
|
|
153
|
-
ambient: [
|
|
154
|
-
.46,
|
|
155
|
-
.36,
|
|
156
|
-
.4
|
|
157
|
-
],
|
|
158
|
-
water: [
|
|
159
|
-
.8,
|
|
160
|
-
.72,
|
|
161
|
-
.7
|
|
162
|
-
],
|
|
163
|
-
sky: [
|
|
164
|
-
.2,
|
|
165
|
-
.09,
|
|
166
|
-
.02
|
|
167
|
-
],
|
|
168
|
-
glint: 1.5,
|
|
169
|
-
caustic: .55,
|
|
170
|
-
floor: .3,
|
|
171
|
-
shadowAlpha: .75,
|
|
172
|
-
glow: .65
|
|
173
|
-
};
|
|
174
|
-
var MORNING = {
|
|
175
|
-
color: [
|
|
176
|
-
.78,
|
|
177
|
-
.7,
|
|
178
|
-
.58
|
|
179
|
-
],
|
|
180
|
-
ambient: [
|
|
181
|
-
.42,
|
|
182
|
-
.42,
|
|
183
|
-
.46
|
|
184
|
-
],
|
|
185
|
-
water: [
|
|
186
|
-
.98,
|
|
187
|
-
.96,
|
|
188
|
-
.96
|
|
189
|
-
],
|
|
190
|
-
sky: [
|
|
191
|
-
.03,
|
|
192
|
-
.02,
|
|
193
|
-
.01
|
|
194
|
-
],
|
|
195
|
-
glint: 1.1,
|
|
196
|
-
caustic: .85,
|
|
197
|
-
floor: .25,
|
|
198
|
-
shadowAlpha: .9,
|
|
199
|
-
glow: .45
|
|
200
|
-
};
|
|
201
|
-
var NOON = {
|
|
202
|
-
color: [
|
|
203
|
-
.72,
|
|
204
|
-
.71,
|
|
205
|
-
.67
|
|
206
|
-
],
|
|
207
|
-
ambient: [
|
|
208
|
-
.44,
|
|
209
|
-
.46,
|
|
210
|
-
.5
|
|
211
|
-
],
|
|
212
|
-
water: [
|
|
213
|
-
1.08,
|
|
214
|
-
1.08,
|
|
215
|
-
1.04
|
|
216
|
-
],
|
|
217
|
-
sky: [
|
|
218
|
-
0,
|
|
219
|
-
0,
|
|
220
|
-
0
|
|
221
|
-
],
|
|
222
|
-
glint: 1,
|
|
223
|
-
caustic: 1,
|
|
224
|
-
floor: .25,
|
|
225
|
-
shadowAlpha: 1,
|
|
226
|
-
glow: .35
|
|
8
|
+
var POND_BG = [
|
|
9
|
+
.024,
|
|
10
|
+
.075,
|
|
11
|
+
.102
|
|
12
|
+
];
|
|
13
|
+
/** Pond's red markings and Ogon gold (the scene's colours, look.ts): the role defaults for accent and metallic. */
|
|
14
|
+
var POND_ACCENT = [
|
|
15
|
+
.95,
|
|
16
|
+
.31,
|
|
17
|
+
.17
|
|
18
|
+
];
|
|
19
|
+
var POND_METALLIC = [
|
|
20
|
+
.97,
|
|
21
|
+
.72,
|
|
22
|
+
.24
|
|
23
|
+
];
|
|
24
|
+
var WHITE = [
|
|
25
|
+
1,
|
|
26
|
+
1,
|
|
27
|
+
1
|
|
28
|
+
];
|
|
29
|
+
/** Derived highlight on a light background. */
|
|
30
|
+
var NEAR_BLACK = [
|
|
31
|
+
.08,
|
|
32
|
+
.09,
|
|
33
|
+
.1
|
|
34
|
+
];
|
|
35
|
+
/** Q13: a background brighter than this (WCAG relative luminance) is light. */
|
|
36
|
+
var LIGHT_LUMINANCE = .4;
|
|
37
|
+
/** Missing role slots sit this far from background toward highlight (Q12). */
|
|
38
|
+
var DERIVE = {
|
|
39
|
+
water: .3,
|
|
40
|
+
ink: .45,
|
|
41
|
+
pale: .85
|
|
227
42
|
};
|
|
228
|
-
var
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
part: "morning",
|
|
247
|
-
...MORNING
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
at: .5,
|
|
251
|
-
part: "noon",
|
|
252
|
-
...NOON
|
|
43
|
+
var POND = Object.freeze({
|
|
44
|
+
mode: "filter",
|
|
45
|
+
background: null,
|
|
46
|
+
css: POND_BACKGROUND,
|
|
47
|
+
light: false,
|
|
48
|
+
tint: WHITE,
|
|
49
|
+
saturation: 1,
|
|
50
|
+
invert: false
|
|
51
|
+
});
|
|
52
|
+
var presets = {
|
|
53
|
+
pond: { mode: "filter" },
|
|
54
|
+
mono: {
|
|
55
|
+
mode: "ramp",
|
|
56
|
+
colors: [
|
|
57
|
+
"#0b0b0b",
|
|
58
|
+
"#5c5c5c",
|
|
59
|
+
"#f2f2f2"
|
|
60
|
+
]
|
|
253
61
|
},
|
|
254
|
-
{
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
62
|
+
phosphor: {
|
|
63
|
+
mode: "ramp",
|
|
64
|
+
colors: [
|
|
65
|
+
"#020a04",
|
|
66
|
+
"#1b7a35",
|
|
67
|
+
"#b6ffbf"
|
|
68
|
+
]
|
|
258
69
|
},
|
|
259
|
-
{
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
70
|
+
amber: {
|
|
71
|
+
mode: "ramp",
|
|
72
|
+
colors: [
|
|
73
|
+
"#0c0600",
|
|
74
|
+
"#8a4f00",
|
|
75
|
+
"#ffe2a8"
|
|
76
|
+
]
|
|
263
77
|
},
|
|
264
|
-
{
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
78
|
+
paper: {
|
|
79
|
+
mode: "roles",
|
|
80
|
+
background: "#f3efe6",
|
|
81
|
+
water: "#b3bcbb",
|
|
82
|
+
highlight: "#1d2629",
|
|
83
|
+
fish: {
|
|
84
|
+
pale: "#4c5759",
|
|
85
|
+
ink: "#111416",
|
|
86
|
+
accent: "#c8452c",
|
|
87
|
+
metallic: "#b5832a"
|
|
88
|
+
}
|
|
268
89
|
},
|
|
269
|
-
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
90
|
+
brand: {
|
|
91
|
+
mode: "roles",
|
|
92
|
+
background: "#14102a",
|
|
93
|
+
water: "#3b2f6e",
|
|
94
|
+
highlight: "#f1edff",
|
|
95
|
+
fish: {
|
|
96
|
+
pale: "#c8c2ff",
|
|
97
|
+
ink: "#5a4a9a",
|
|
98
|
+
accent: "#ff5c8a",
|
|
99
|
+
metallic: "#ffc2d6"
|
|
100
|
+
}
|
|
273
101
|
}
|
|
274
|
-
];
|
|
275
|
-
KEYS.slice(0, -1).map(({ at, part }) => ({
|
|
276
|
-
at,
|
|
277
|
-
name: part
|
|
278
|
-
}));
|
|
279
|
-
/** The moon hands over to the sun across [a, b] and back across [1 - b, 1 - a]. */
|
|
280
|
-
var HANDOFF = [.19, .23];
|
|
281
|
-
/**
|
|
282
|
-
* The sun's path is tilted on screen so the noon light still comes from one
|
|
283
|
-
* side: a fish swimming any way but along that diagonal shows a lit flank.
|
|
284
|
-
*/
|
|
285
|
-
var PATH_TILT = -.45;
|
|
286
|
-
/** Sun elevation (rad) at sunrise/sunset and at noon; never 0 so it always lights something. */
|
|
287
|
-
var SUN_LOW = .1;
|
|
288
|
-
var SUN_HIGH = 1;
|
|
289
|
-
var MOON_LOW = .25;
|
|
290
|
-
var MOON_HIGH = .95;
|
|
291
|
-
/** Shadow length per unit of cot(elevation), in pond units, and its cap for a low sun. */
|
|
292
|
-
var SHADOW_PER_COT = .16;
|
|
293
|
-
var SHADOW_MAX = .24;
|
|
294
|
-
var TAU$1 = Math.PI * 2;
|
|
295
|
-
/** GLSL smoothstep. */
|
|
296
|
-
var smooth = (a, b, x) => {
|
|
297
|
-
const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
|
|
298
|
-
return t * t * (3 - 2 * t);
|
|
299
102
|
};
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Scratch that azimuth() (x, y, length) and arcDir() (sun, moon) write into, so a frame
|
|
308
|
-
* allocates no temporaries: single-threaded, and each is read straight after its write.
|
|
309
|
-
*/
|
|
310
|
-
var AZ = /* @__PURE__ */ new Float64Array(3);
|
|
311
|
-
var SUN_D = /* @__PURE__ */ new Float64Array(3);
|
|
312
|
-
var MOON_D = /* @__PURE__ */ new Float64Array(3);
|
|
313
|
-
/**
|
|
314
|
-
* Direction toward a body, into `out`, on an arc that rises at angle 0 (screen
|
|
315
|
-
* left) and sets at π (screen right), with elevation lo..hi following sin(angle).
|
|
316
|
-
*/
|
|
317
|
-
function arcDir(angle, lo, hi, out) {
|
|
318
|
-
const el = mix$1(lo, hi, Math.max(0, Math.sin(angle)));
|
|
319
|
-
azimuth(angle);
|
|
320
|
-
const h = Math.cos(el) / AZ[2];
|
|
321
|
-
out[0] = AZ[0] * h;
|
|
322
|
-
out[1] = AZ[1] * h;
|
|
323
|
-
out[2] = Math.sin(el);
|
|
103
|
+
function deepFreeze(value) {
|
|
104
|
+
if (value && typeof value === "object") {
|
|
105
|
+
for (const v of Object.values(value)) deepFreeze(v);
|
|
106
|
+
Object.freeze(value);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
324
109
|
}
|
|
325
|
-
/**
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const c = Math.cos(PATH_TILT);
|
|
330
|
-
const s = Math.sin(PATH_TILT);
|
|
331
|
-
AZ[0] = ax * c - ay * s;
|
|
332
|
-
AZ[1] = ax * s + ay * c;
|
|
333
|
-
AZ[2] = Math.hypot(ax, ay) || 1;
|
|
110
|
+
/** The presets behind the public `koiPalettes`, frozen all the way down. */
|
|
111
|
+
var PRESETS = deepFreeze(presets);
|
|
112
|
+
function warn(message) {
|
|
113
|
+
console.warn(`koi-ascii: ${message}`);
|
|
334
114
|
}
|
|
335
|
-
/**
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
* the left side, over the top, out near the top-right corner. At the light's
|
|
353
|
-
* own pace it would also run down the right side (P1: the top edge and one side).
|
|
354
|
-
*/
|
|
355
|
-
var SUN_PACE = 2 * HANDOFF[0] / (1 - 2 * HANDOFF[1]);
|
|
356
|
-
/**
|
|
357
|
-
* Where a body's reflection sits: toward its azimuth from the pond's centre,
|
|
358
|
-
* out on a rounded rectangle near the edges (half-extents BODY_BAND, as a
|
|
359
|
-
* share of width and height). Every point of it is at least 0.89 of the way
|
|
360
|
-
* out in one axis, so with a radius under 0.12 of the shorter side the disc
|
|
361
|
-
* keeps clear of the middle 40 % of the pond at any aspect (P1).
|
|
362
|
-
*/
|
|
363
|
-
function bodyPos(angle) {
|
|
364
|
-
azimuth(angle);
|
|
365
|
-
const x = AZ[0];
|
|
366
|
-
const y = AZ[1];
|
|
367
|
-
const k = ((Math.abs(x) / BODY_BAND[0]) ** BODY_ROUND + (Math.abs(y) / BODY_BAND[1]) ** BODY_ROUND) ** (-1 / BODY_ROUND);
|
|
368
|
-
return [.5 + x * k, .5 + y * k];
|
|
115
|
+
/** WCAG 2 relative luminance of an sRGB colour. */
|
|
116
|
+
function relativeLuminance([r, g, b]) {
|
|
117
|
+
const lin = (c) => c <= .04045 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
|
|
118
|
+
return .2126 * lin(r) + .7152 * lin(g) + .0722 * lin(b);
|
|
119
|
+
}
|
|
120
|
+
function isLight(color) {
|
|
121
|
+
return relativeLuminance(color) > LIGHT_LUMINANCE;
|
|
122
|
+
}
|
|
123
|
+
function mix$1(a, b, t) {
|
|
124
|
+
return [
|
|
125
|
+
a[0] + (b[0] - a[0]) * t,
|
|
126
|
+
a[1] + (b[1] - a[1]) * t,
|
|
127
|
+
a[2] + (b[2] - a[2]) * t
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
function toCss(c) {
|
|
131
|
+
return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
369
132
|
}
|
|
370
133
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* night, and gone before either handoff starts, so it never jumps.
|
|
134
|
+
* Resolves a palette. Never throws: an invalid field falls back on its own,
|
|
135
|
+
* an unknown preset/mode (or anything unreadable) falls back to `pond`, each
|
|
136
|
+
* with a `console.warn` (Q8). `undefined`/`null` is `pond`, silently.
|
|
375
137
|
*/
|
|
376
|
-
function
|
|
377
|
-
|
|
378
|
-
|
|
138
|
+
function resolvePalette(input, parse) {
|
|
139
|
+
if (input === void 0 || input === null) return POND;
|
|
140
|
+
try {
|
|
141
|
+
return resolve(input, parse);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
warn(`could not read the palette (${String(err)}); using 'pond'.`);
|
|
144
|
+
return POND;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function resolve(input, parse) {
|
|
148
|
+
if (typeof input === "string") {
|
|
149
|
+
if (Object.hasOwn(PRESETS, input)) return resolve(PRESETS[input], parse);
|
|
150
|
+
warn(`unknown palette '${input}'; using 'pond'.`);
|
|
151
|
+
return POND;
|
|
152
|
+
}
|
|
153
|
+
const p = typeof input === "object" ? input : {};
|
|
154
|
+
const mode = p.mode;
|
|
155
|
+
if (mode === "ramp") return resolveRamp(p, parse);
|
|
156
|
+
if (mode === "roles") return resolveRoles(p, parse);
|
|
157
|
+
if (mode === "filter") return resolveFilter(p, parse);
|
|
158
|
+
warn(`unknown palette mode '${String(mode)}' (expected 'ramp', 'roles' or 'filter'); using 'pond'.`);
|
|
159
|
+
return POND;
|
|
160
|
+
}
|
|
161
|
+
/** A colour field: undefined when absent (no warning), null when invalid (warned). */
|
|
162
|
+
function color(value, field, parse) {
|
|
163
|
+
if (value === void 0) return void 0;
|
|
164
|
+
const parsed = typeof value === "string" ? safeParse(value, parse) : null;
|
|
165
|
+
if (!parsed) warn(`palette ${field} ${show$2(value)} is not a CSS colour (var() is not supported); ignoring it.`);
|
|
166
|
+
return parsed;
|
|
167
|
+
}
|
|
168
|
+
function resolveRamp(p, parse) {
|
|
169
|
+
const colors = p.colors;
|
|
170
|
+
if (!Array.isArray(colors) || colors.length < 2 || colors.length > 5) {
|
|
171
|
+
warn(`a ramp palette needs 2 to 5 colors; using 'pond'.`);
|
|
172
|
+
return POND;
|
|
173
|
+
}
|
|
174
|
+
const stops = [];
|
|
175
|
+
for (let i = 0; i < colors.length; i++) {
|
|
176
|
+
const c = typeof colors[i] === "string" ? safeParse(colors[i], parse) : null;
|
|
177
|
+
if (!c) {
|
|
178
|
+
warn(`ramp colors[${i}] ${show$2(colors[i])} is not a CSS colour; using 'pond'.`);
|
|
179
|
+
return POND;
|
|
180
|
+
}
|
|
181
|
+
stops.push(c);
|
|
182
|
+
}
|
|
183
|
+
const background = stops[0];
|
|
379
184
|
return {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
185
|
+
mode: "ramp",
|
|
186
|
+
background,
|
|
187
|
+
css: toCss(background),
|
|
188
|
+
light: isLight(background),
|
|
189
|
+
stops
|
|
383
190
|
};
|
|
384
191
|
}
|
|
385
|
-
/**
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
192
|
+
/** A value for a warning message; never throws. */
|
|
193
|
+
function show$2(value) {
|
|
194
|
+
return typeof value === "string" ? JSON.stringify(value) : typeof value === "object" && value ? "object" : String(value);
|
|
195
|
+
}
|
|
196
|
+
function safeParse(css, parse) {
|
|
197
|
+
try {
|
|
198
|
+
return parse(css);
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function resolveRoles(p, parse) {
|
|
204
|
+
let background = color(p.background, "background", parse);
|
|
205
|
+
if (background === void 0) warn(`a roles palette needs a background; using ${POND_BACKGROUND}.`);
|
|
206
|
+
background ??= POND_BG;
|
|
207
|
+
const light = isLight(background);
|
|
208
|
+
const highlight = color(p.highlight, "highlight", parse) ?? (light ? NEAR_BLACK : WHITE);
|
|
209
|
+
const toward = (t) => mix$1(background, highlight, t);
|
|
210
|
+
const water = color(p.water, "water", parse) ?? toward(DERIVE.water);
|
|
211
|
+
let fish = {};
|
|
212
|
+
if (p.fish !== void 0) {
|
|
213
|
+
if (p.fish && typeof p.fish === "object") fish = p.fish;
|
|
214
|
+
else warn(`palette fish must be an object; deriving the fish colours.`);
|
|
215
|
+
}
|
|
216
|
+
const roles = {
|
|
217
|
+
background,
|
|
218
|
+
water,
|
|
219
|
+
highlight,
|
|
220
|
+
pale: color(fish.pale, "fish.pale", parse) ?? toward(DERIVE.pale),
|
|
221
|
+
ink: color(fish.ink, "fish.ink", parse) ?? toward(DERIVE.ink),
|
|
222
|
+
accent: color(fish.accent, "fish.accent", parse) ?? POND_ACCENT,
|
|
223
|
+
metallic: color(fish.metallic, "fish.metallic", parse) ?? POND_METALLIC
|
|
224
|
+
};
|
|
394
225
|
return {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
226
|
+
mode: "roles",
|
|
227
|
+
background,
|
|
228
|
+
css: toCss(background),
|
|
229
|
+
light,
|
|
230
|
+
roles
|
|
398
231
|
};
|
|
399
232
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
233
|
+
function resolveFilter(p, parse) {
|
|
234
|
+
const background = color(p.background, "background", parse) ?? null;
|
|
235
|
+
const tint = color(p.tint, "tint", parse) ?? WHITE;
|
|
236
|
+
let saturation = 1;
|
|
237
|
+
const s = p.saturation;
|
|
238
|
+
if (typeof s === "number" && !Number.isNaN(s)) {
|
|
239
|
+
saturation = Math.min(1, Math.max(0, s));
|
|
240
|
+
if (saturation !== s) warn(`palette saturation ${s} is outside [0, 1]; using ${saturation}.`);
|
|
241
|
+
} else if (s !== void 0) warn(`palette saturation must be a number from 0 to 1; using 1.`);
|
|
242
|
+
let invert = false;
|
|
243
|
+
if (typeof p.invert === "boolean") invert = p.invert;
|
|
244
|
+
else if (p.invert !== void 0) warn(`palette invert must be true or false; using false.`);
|
|
245
|
+
const light = background ? isLight(background) : false;
|
|
246
|
+
if (light && !invert) warn(`a light filter background needs invert: true, or the glyphs barely show.`);
|
|
247
|
+
if (!light && invert) warn(`invert: true needs a light filter background, or the glyphs barely show.`);
|
|
248
|
+
return {
|
|
249
|
+
mode: "filter",
|
|
250
|
+
background,
|
|
251
|
+
css: background ? toCss(background) : POND_BACKGROUND,
|
|
252
|
+
light,
|
|
253
|
+
tint,
|
|
254
|
+
saturation,
|
|
255
|
+
invert
|
|
256
|
+
};
|
|
415
257
|
}
|
|
416
|
-
/** Night weight below which no star shows: they arrive after the night and leave before it. */
|
|
417
|
-
var STARS_FROM = .35;
|
|
418
258
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
259
|
+
* Validates an intensity (Q9, R3): a number in [0, 1]. Out-of-range numbers
|
|
260
|
+
* clamp, anything else (NaN, a string, a missing setter argument) keeps
|
|
261
|
+
* `fallback`; both with a warning. `optional`: undefined is the omitted
|
|
262
|
+
* option, which takes `fallback` silently.
|
|
422
263
|
*/
|
|
423
|
-
function
|
|
424
|
-
|
|
264
|
+
function resolveIntensity(value, fallback, optional) {
|
|
265
|
+
if (value === void 0 && optional) return fallback;
|
|
266
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
267
|
+
warn(`intensity must be a number from 0 to 1; keeping ${fallback}.`);
|
|
268
|
+
return fallback;
|
|
269
|
+
}
|
|
270
|
+
const clamped = Math.min(1, Math.max(0, value));
|
|
271
|
+
if (clamped !== value) warn(`intensity ${value} is outside [0, 1]; using ${clamped}.`);
|
|
272
|
+
return clamped;
|
|
425
273
|
}
|
|
426
|
-
|
|
427
|
-
var FIREFLIES_IN = [.745, .8];
|
|
428
|
-
var FIREFLIES_OUT = [.88, .95];
|
|
274
|
+
var parserCtx;
|
|
429
275
|
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
276
|
+
* The browser's own colour parser (Q7): any CSS colour a canvas accepts, in
|
|
277
|
+
* sRGB, alpha dropped. `var()` and junk are rejected by `fillStyle`. Needs a
|
|
278
|
+
* DOM; call it only when a palette is set, never at import.
|
|
433
279
|
*/
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
280
|
+
var parseCssColor = (css) => {
|
|
281
|
+
if (parserCtx === void 0) {
|
|
282
|
+
const canvas = document.createElement("canvas");
|
|
283
|
+
canvas.width = canvas.height = 1;
|
|
284
|
+
parserCtx = canvas.getContext("2d", { willReadFrequently: true });
|
|
285
|
+
}
|
|
286
|
+
const ctx = parserCtx;
|
|
287
|
+
if (!ctx) return null;
|
|
288
|
+
ctx.fillStyle = "#000000";
|
|
289
|
+
ctx.fillStyle = css;
|
|
290
|
+
const a = String(ctx.fillStyle);
|
|
291
|
+
ctx.fillStyle = "#ffffff";
|
|
292
|
+
ctx.fillStyle = css;
|
|
293
|
+
if (String(ctx.fillStyle) !== a) return null;
|
|
294
|
+
const hex = /^#([0-9a-f]{6})$/i.exec(a);
|
|
295
|
+
if (hex) {
|
|
296
|
+
const n = parseInt(hex[1], 16);
|
|
297
|
+
return [
|
|
298
|
+
(n >> 16 & 255) / 255,
|
|
299
|
+
(n >> 8 & 255) / 255,
|
|
300
|
+
(n & 255) / 255
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
const rgba = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i.exec(a);
|
|
304
|
+
if (rgba) return [
|
|
305
|
+
Number(rgba[1]) / 255,
|
|
306
|
+
Number(rgba[2]) / 255,
|
|
307
|
+
Number(rgba[3]) / 255
|
|
457
308
|
];
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
309
|
+
ctx.fillStyle = a.replace(/\s*\/\s*[^/)]+\)$/, ")");
|
|
310
|
+
ctx.globalCompositeOperation = "copy";
|
|
311
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
312
|
+
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
313
|
+
return [
|
|
314
|
+
r / 255,
|
|
315
|
+
g / 255,
|
|
316
|
+
b / 255
|
|
466
317
|
];
|
|
467
|
-
|
|
318
|
+
};
|
|
319
|
+
var DEFAULT_FADE = Object.freeze({
|
|
320
|
+
strength: 1,
|
|
321
|
+
feather: 56
|
|
322
|
+
});
|
|
323
|
+
function avoidAmounts(strength) {
|
|
468
324
|
return {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
sky: mix3(a.sky, b.sky, t),
|
|
474
|
-
glint: mix$1(a.glint, b.glint, t),
|
|
475
|
-
caustic: mix$1(a.caustic, b.caustic, t),
|
|
476
|
-
floor,
|
|
477
|
-
shadow,
|
|
478
|
-
shadowAlpha: mix$1(a.shadowAlpha, b.shadowAlpha, t),
|
|
479
|
-
glowStrength: mix$1(a.glow, b.glow, t),
|
|
480
|
-
darkWater: night <= 0 ? 1 : mix$1(1, DARK_WATER, night),
|
|
481
|
-
darkCaustic: night <= 0 ? 1 : mix$1(1, DARK_CAUSTIC, night),
|
|
482
|
-
moon: moonAt(p),
|
|
483
|
-
sun: sunAt(p)
|
|
325
|
+
waterThin: .8 * strength,
|
|
326
|
+
fishThin: .51 * strength,
|
|
327
|
+
waterDim: .6 * strength,
|
|
328
|
+
fishDim: .85 * strength
|
|
484
329
|
};
|
|
485
330
|
}
|
|
486
|
-
/**
|
|
487
|
-
function
|
|
488
|
-
|
|
331
|
+
/** A value for a warning message; never throws. */
|
|
332
|
+
function show$1(value) {
|
|
333
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
334
|
+
if (Array.isArray(value)) return "an array";
|
|
335
|
+
return typeof value === "object" && value ? "an object" : String(value);
|
|
489
336
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
337
|
+
var isNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
338
|
+
/**
|
|
339
|
+
* Resolves `avoidFade` (D9). Fields left out keep `current` (the defaults for
|
|
340
|
+
* the option, the fade in use for setAvoidFade), so animating `strength`
|
|
341
|
+
* keeps a custom feather. `strength` clamps to [0, 1]; a non-finite one, a
|
|
342
|
+
* negative or non-finite feather, or a non-object warn and fall back to the
|
|
343
|
+
* defaults (1 and 56).
|
|
344
|
+
*/
|
|
345
|
+
function resolveAvoidFade(input, current = DEFAULT_FADE) {
|
|
346
|
+
if (input === void 0 || input === null) return current;
|
|
347
|
+
if (typeof input !== "object" || Array.isArray(input)) {
|
|
348
|
+
warn(`avoidFade must be an object; got ${show$1(input)}, keeping the fade as it is.`);
|
|
349
|
+
return current;
|
|
350
|
+
}
|
|
351
|
+
const fade = input;
|
|
352
|
+
let { strength, feather } = current;
|
|
353
|
+
if (fade.strength !== void 0) {
|
|
354
|
+
if (!isNumber(fade.strength)) {
|
|
355
|
+
warn(`avoidFade.strength must be a number from 0 to 1; got ${show$1(fade.strength)}, using ${DEFAULT_FADE.strength}.`);
|
|
356
|
+
strength = DEFAULT_FADE.strength;
|
|
357
|
+
} else {
|
|
358
|
+
strength = Math.min(1, Math.max(0, fade.strength));
|
|
359
|
+
if (strength !== fade.strength) warn(`avoidFade.strength ${fade.strength} is outside [0, 1]; using ${strength}.`);
|
|
360
|
+
}
|
|
502
361
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
cycleS;
|
|
509
|
-
constructor(cycle, phase) {
|
|
510
|
-
this.cycleS = validCycle(cycle) ?? 600;
|
|
511
|
-
this.phase = wrapPhase(phase);
|
|
362
|
+
if (fade.feather !== void 0) {
|
|
363
|
+
if (!isNumber(fade.feather) || fade.feather < 0) {
|
|
364
|
+
warn(`avoidFade.feather must be a number of CSS px, 0 or more; got ${show$1(fade.feather)}, using 56.`);
|
|
365
|
+
feather = 56;
|
|
366
|
+
} else feather = fade.feather;
|
|
512
367
|
}
|
|
513
|
-
|
|
514
|
-
|
|
368
|
+
return strength === current.strength && feather === current.feather ? current : {
|
|
369
|
+
strength,
|
|
370
|
+
feather
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
var NO_RADIUS = Object.freeze({
|
|
374
|
+
px: 0,
|
|
375
|
+
frac: 0
|
|
376
|
+
});
|
|
377
|
+
/**
|
|
378
|
+
* Parses a computed `border-top-left-radius` ("8px", "50%", "8px 4px" for an
|
|
379
|
+
* ellipse, whose horizontal radius it takes). One radius per zone (M13).
|
|
380
|
+
*/
|
|
381
|
+
function parseRadius(css) {
|
|
382
|
+
const first = (css ?? "").trim().split(/\s+/)[0] ?? "";
|
|
383
|
+
const n = parseFloat(first);
|
|
384
|
+
if (!Number.isFinite(n) || n <= 0) return NO_RADIUS;
|
|
385
|
+
return first.endsWith("%") ? {
|
|
386
|
+
px: 0,
|
|
387
|
+
frac: n / 100
|
|
388
|
+
} : {
|
|
389
|
+
px: n,
|
|
390
|
+
frac: 0
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Builds the frame's zones (A5, A8–A10): each element's box, canvas-relative
|
|
395
|
+
* and grown by its pad, with a corner radius of its border radius plus the
|
|
396
|
+
* pad. Zero-size boxes are skipped, and so are zones outside the visible part
|
|
397
|
+
* of the canvas (its rect ∩ the viewport, `viewW` × `viewH`) grown by
|
|
398
|
+
* CULL_MARGIN plus the feather. Past AVOID_MAX zones, the first in list
|
|
399
|
+
* order win. Writes `rects` (x, y, w, h per zone) and `radii` (one per zone),
|
|
400
|
+
* all in world px: the canvas's CSS px, whatever the device pixel ratio or
|
|
401
|
+
* backing-store size. Returns the visible zones: the first
|
|
402
|
+
* min(visible, AVOID_MAX) were written, the rest dropped. A plain number, so
|
|
403
|
+
* a frame allocates nothing here.
|
|
404
|
+
*/
|
|
405
|
+
function buildZones(sources, canvas, viewW, viewH, feather, rects, radii) {
|
|
406
|
+
const reach = 50 + feather;
|
|
407
|
+
const x0 = Math.max(canvas.left, 0) - reach;
|
|
408
|
+
const y0 = Math.max(canvas.top, 0) - reach;
|
|
409
|
+
const x1 = Math.min(canvas.left + canvas.width, viewW) + reach;
|
|
410
|
+
const y1 = Math.min(canvas.top + canvas.height, viewH) + reach;
|
|
411
|
+
let count = 0;
|
|
412
|
+
for (let i = 0; i < sources.length; i++) {
|
|
413
|
+
const { rect, pad, radius } = sources[i];
|
|
414
|
+
if (!(rect.width > 0 && rect.height > 0)) continue;
|
|
415
|
+
const left = rect.left - pad;
|
|
416
|
+
const top = rect.top - pad;
|
|
417
|
+
const w = rect.width + 2 * pad;
|
|
418
|
+
const h = rect.height + 2 * pad;
|
|
419
|
+
if (left + w < x0 || left > x1 || top + h < y0 || top > y1) continue;
|
|
420
|
+
if (count >= 16) {
|
|
421
|
+
count++;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
rects[count * 4] = left - canvas.left;
|
|
425
|
+
rects[count * 4 + 1] = top - canvas.top;
|
|
426
|
+
rects[count * 4 + 2] = w;
|
|
427
|
+
rects[count * 4 + 3] = h;
|
|
428
|
+
radii[count] = radius.px + radius.frac * Math.min(rect.width, rect.height) + pad;
|
|
429
|
+
count++;
|
|
430
|
+
}
|
|
431
|
+
return count;
|
|
432
|
+
}
|
|
433
|
+
var isElement = (v) => !!v && typeof v === "object" && typeof v.getBoundingClientRect === "function" && "isConnected" in v;
|
|
434
|
+
/**
|
|
435
|
+
* Validates an `avoid` list (M4): not an array warns and is empty; an entry
|
|
436
|
+
* that is neither an Element nor `{ element, pad? }` (null, a DOMRect, a
|
|
437
|
+
* string) warns and is skipped. A bad pad warns and falls back to 12 (D9).
|
|
438
|
+
*/
|
|
439
|
+
function resolveAvoidList(input) {
|
|
440
|
+
if (input === void 0 || input === null) return [];
|
|
441
|
+
if (!Array.isArray(input)) {
|
|
442
|
+
warn(`avoid must be an array of elements; got ${show$1(input)}, avoiding nothing.`);
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
const out = [];
|
|
446
|
+
input.forEach((item, i) => {
|
|
447
|
+
if (isElement(item)) {
|
|
448
|
+
out.push({
|
|
449
|
+
element: item,
|
|
450
|
+
pad: 12
|
|
451
|
+
});
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const entry = item;
|
|
455
|
+
if (!entry || typeof entry !== "object" || !isElement(entry.element)) {
|
|
456
|
+
warn(`avoid[${i}] must be an Element or { element, pad }; got ${show$1(item)}, skipping it.`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
let pad = 12;
|
|
460
|
+
if (entry.pad !== void 0) {
|
|
461
|
+
if (isNumber(entry.pad) && entry.pad >= 0) pad = entry.pad;
|
|
462
|
+
else warn(`avoid[${i}].pad must be a number of CSS px, 0 or more; got ${show$1(entry.pad)}, using 12.`);
|
|
463
|
+
}
|
|
464
|
+
out.push({
|
|
465
|
+
element: entry.element,
|
|
466
|
+
pad
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
var readRadius = (el) => parseRadius(getComputedStyle(el).borderTopLeftRadius);
|
|
472
|
+
/**
|
|
473
|
+
* The pond's zone list. Holds its elements until set() replaces them or
|
|
474
|
+
* clear() (destroy, failure) drops them. Reads `border-radius` only in
|
|
475
|
+
* set() and readRadii() (mount and pond resize), and once more for an
|
|
476
|
+
* element coming back from zero size (R1); never per frame otherwise.
|
|
477
|
+
*/
|
|
478
|
+
var AvoidZones = class {
|
|
479
|
+
rects = /* @__PURE__ */ new Float32Array(64);
|
|
480
|
+
radii = /* @__PURE__ */ new Float32Array(16);
|
|
481
|
+
entries = [];
|
|
482
|
+
/** The cap warning has been given for this list. */
|
|
483
|
+
warned = false;
|
|
484
|
+
get size() {
|
|
485
|
+
return this.entries.length;
|
|
486
|
+
}
|
|
487
|
+
/** Replaces the list; `read`: read the radii now (setAvoid), not at the next resize (mount). */
|
|
488
|
+
set(list, read = true) {
|
|
489
|
+
this.entries = resolveAvoidList(list).map(({ element, pad }) => ({
|
|
490
|
+
element,
|
|
491
|
+
pad,
|
|
492
|
+
rect: {
|
|
493
|
+
left: 0,
|
|
494
|
+
top: 0,
|
|
495
|
+
width: 0,
|
|
496
|
+
height: 0
|
|
497
|
+
},
|
|
498
|
+
radius: NO_RADIUS,
|
|
499
|
+
zero: false
|
|
500
|
+
}));
|
|
501
|
+
this.warned = false;
|
|
502
|
+
if (read) this.readRadii();
|
|
515
503
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
this.cycleS = validCycle(s) ?? this.cycleS;
|
|
504
|
+
clear() {
|
|
505
|
+
this.entries = [];
|
|
519
506
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
507
|
+
/** Re-reads every element's border radius (mount, pond resize). */
|
|
508
|
+
readRadii() {
|
|
509
|
+
for (const e of this.entries) {
|
|
510
|
+
e.radius = readRadius(e.element);
|
|
511
|
+
e.zero = !e.element.isConnected;
|
|
512
|
+
}
|
|
523
513
|
}
|
|
524
|
-
|
|
525
|
-
|
|
514
|
+
/**
|
|
515
|
+
* Measures every element against the canvas's rect, writes `rects` and
|
|
516
|
+
* `radii`, and returns the zone count. Warns once per list when visible
|
|
517
|
+
* zones were dropped past AVOID_MAX.
|
|
518
|
+
*/
|
|
519
|
+
measure(canvas, viewW, viewH, feather) {
|
|
520
|
+
for (let i = 0; i < this.entries.length; i++) {
|
|
521
|
+
const e = this.entries[i];
|
|
522
|
+
e.rect = e.element.getBoundingClientRect();
|
|
523
|
+
const zero = !(e.rect.width > 0 && e.rect.height > 0);
|
|
524
|
+
if (!zero && e.zero) e.radius = readRadius(e.element);
|
|
525
|
+
e.zero = zero;
|
|
526
|
+
}
|
|
527
|
+
const visible = buildZones(this.entries, canvas, viewW, viewH, feather, this.rects, this.radii);
|
|
528
|
+
if (visible > 16 && !this.warned) {
|
|
529
|
+
this.warned = true;
|
|
530
|
+
warn(`avoid: ${visible} zones are visible but the pond has 16 slots; the first 16 in list order are used.`);
|
|
531
|
+
}
|
|
532
|
+
return Math.min(visible, 16);
|
|
526
533
|
}
|
|
527
534
|
};
|
|
535
|
+
/** Every glyph the ASCII pass can emit, keyed by its GLSL constant (G_<KEY>). */
|
|
536
|
+
var GLYPHS = {
|
|
537
|
+
SPACE: " ",
|
|
538
|
+
DOT: ".",
|
|
539
|
+
COMMA: ",",
|
|
540
|
+
QUOTE: "'",
|
|
541
|
+
BACKTICK: "`",
|
|
542
|
+
COLON: ":",
|
|
543
|
+
MINUS: "-",
|
|
544
|
+
UNDERSCORE: "_",
|
|
545
|
+
TILDE: "~",
|
|
546
|
+
EQUALS: "=",
|
|
547
|
+
PLUS: "+",
|
|
548
|
+
STAR: "*",
|
|
549
|
+
O: "o",
|
|
550
|
+
HASH: "#",
|
|
551
|
+
PERCENT: "%",
|
|
552
|
+
AT: "@",
|
|
553
|
+
LPAREN: "(",
|
|
554
|
+
RPAREN: ")",
|
|
555
|
+
SLASH: "/",
|
|
556
|
+
BSLASH: "\\",
|
|
557
|
+
PIPE: "|",
|
|
558
|
+
BIG_O: "O"
|
|
559
|
+
};
|
|
560
|
+
var CHARS = Object.values(GLYPHS);
|
|
561
|
+
var LINE_HEIGHT = 1.2;
|
|
562
|
+
function glyphDefines() {
|
|
563
|
+
return Object.keys(GLYPHS).map((k, i) => `#define G_${k} ${i}\n`).join("");
|
|
564
|
+
}
|
|
528
565
|
/**
|
|
529
|
-
*
|
|
530
|
-
*
|
|
566
|
+
* Rasterises the glyph set into a single-row R8 texture, one cell per glyph,
|
|
567
|
+
* at the exact device-pixel cell size so the ASCII pass can copy texels 1:1.
|
|
531
568
|
*/
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
569
|
+
function buildAtlas(gl, fontPx, family) {
|
|
570
|
+
const font = `600 ${fontPx}px ${family}`;
|
|
571
|
+
const probe = document.createElement("canvas").getContext("2d");
|
|
572
|
+
if (!probe) throw new Error("2D canvas unavailable for the glyph atlas");
|
|
573
|
+
probe.font = font;
|
|
574
|
+
const cellW = Math.max(1, Math.round(probe.measureText("M").width));
|
|
575
|
+
const cellH = Math.max(1, Math.round(fontPx * LINE_HEIGHT));
|
|
576
|
+
let ascent = 0;
|
|
577
|
+
let descent = 0;
|
|
578
|
+
for (const c of CHARS) {
|
|
579
|
+
const m = probe.measureText(c);
|
|
580
|
+
ascent = Math.max(ascent, m.actualBoundingBoxAscent);
|
|
581
|
+
descent = Math.max(descent, m.actualBoundingBoxDescent);
|
|
541
582
|
}
|
|
583
|
+
const baseline = Math.round((cellH - (ascent + descent)) / 2 + ascent);
|
|
584
|
+
const canvas = document.createElement("canvas");
|
|
585
|
+
canvas.width = cellW * CHARS.length;
|
|
586
|
+
canvas.height = cellH;
|
|
587
|
+
const ctx = canvas.getContext("2d");
|
|
588
|
+
if (!ctx) throw new Error("2D canvas unavailable for the glyph atlas");
|
|
589
|
+
ctx.fillStyle = "#000";
|
|
590
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
591
|
+
ctx.font = font;
|
|
592
|
+
ctx.fillStyle = "#fff";
|
|
593
|
+
ctx.textAlign = "center";
|
|
594
|
+
ctx.textBaseline = "alphabetic";
|
|
595
|
+
CHARS.forEach((c, i) => {
|
|
596
|
+
const m = ctx.measureText(c);
|
|
597
|
+
const inkW = m.actualBoundingBoxLeft + m.actualBoundingBoxRight;
|
|
598
|
+
const sx = inkW > cellW - .5 ? (cellW - .5) / inkW : 1;
|
|
599
|
+
const cx = (m.actualBoundingBoxRight - m.actualBoundingBoxLeft) / 2;
|
|
600
|
+
ctx.save();
|
|
601
|
+
ctx.beginPath();
|
|
602
|
+
ctx.rect(i * cellW, 0, cellW, cellH);
|
|
603
|
+
ctx.clip();
|
|
604
|
+
ctx.translate(i * cellW + cellW / 2, 0);
|
|
605
|
+
ctx.scale(sx, 1);
|
|
606
|
+
ctx.fillText(c, -cx, baseline);
|
|
607
|
+
ctx.restore();
|
|
608
|
+
});
|
|
609
|
+
const texture = gl.createTexture();
|
|
610
|
+
if (!texture) throw new Error("Could not allocate the glyph atlas texture");
|
|
611
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
612
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
613
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
614
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
615
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, gl.RED, gl.UNSIGNED_BYTE, canvas);
|
|
616
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
617
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
618
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
619
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
620
|
+
return {
|
|
621
|
+
texture,
|
|
622
|
+
cellW,
|
|
623
|
+
cellH,
|
|
624
|
+
fontPx
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
var NIGHT = {
|
|
628
|
+
color: [
|
|
629
|
+
.36,
|
|
630
|
+
.4,
|
|
631
|
+
.58
|
|
632
|
+
],
|
|
633
|
+
ambient: [
|
|
634
|
+
.36,
|
|
635
|
+
.35,
|
|
636
|
+
.44
|
|
637
|
+
],
|
|
638
|
+
water: [
|
|
639
|
+
.58,
|
|
640
|
+
.7,
|
|
641
|
+
1
|
|
642
|
+
],
|
|
643
|
+
sky: [
|
|
644
|
+
0,
|
|
645
|
+
.01,
|
|
646
|
+
.04
|
|
647
|
+
],
|
|
648
|
+
glint: .6,
|
|
649
|
+
caustic: .25,
|
|
650
|
+
floor: .5,
|
|
651
|
+
shadowAlpha: .4,
|
|
652
|
+
glow: .55
|
|
653
|
+
};
|
|
654
|
+
var BLUE = {
|
|
655
|
+
color: [
|
|
656
|
+
.1,
|
|
657
|
+
.11,
|
|
658
|
+
.18
|
|
659
|
+
],
|
|
660
|
+
ambient: [
|
|
661
|
+
.36,
|
|
662
|
+
.36,
|
|
663
|
+
.48
|
|
664
|
+
],
|
|
665
|
+
water: [
|
|
666
|
+
.62,
|
|
667
|
+
.68,
|
|
668
|
+
.95
|
|
669
|
+
],
|
|
670
|
+
sky: [
|
|
671
|
+
.03,
|
|
672
|
+
.02,
|
|
673
|
+
.06
|
|
674
|
+
],
|
|
675
|
+
glint: .3,
|
|
676
|
+
caustic: .2,
|
|
677
|
+
floor: .48,
|
|
678
|
+
shadowAlpha: .1,
|
|
679
|
+
glow: .5
|
|
680
|
+
};
|
|
681
|
+
var GOLDEN = {
|
|
682
|
+
color: [
|
|
683
|
+
1,
|
|
684
|
+
.6,
|
|
685
|
+
.3
|
|
686
|
+
],
|
|
687
|
+
ambient: [
|
|
688
|
+
.46,
|
|
689
|
+
.36,
|
|
690
|
+
.4
|
|
691
|
+
],
|
|
692
|
+
water: [
|
|
693
|
+
.8,
|
|
694
|
+
.72,
|
|
695
|
+
.7
|
|
696
|
+
],
|
|
697
|
+
sky: [
|
|
698
|
+
.2,
|
|
699
|
+
.09,
|
|
700
|
+
.02
|
|
701
|
+
],
|
|
702
|
+
glint: 1.5,
|
|
703
|
+
caustic: .55,
|
|
704
|
+
floor: .3,
|
|
705
|
+
shadowAlpha: .75,
|
|
706
|
+
glow: .65
|
|
542
707
|
};
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
update(dt) {
|
|
570
|
-
this.phase = this.held ?? this.source.tick(dt);
|
|
571
|
-
return this.phase;
|
|
572
|
-
}
|
|
573
|
-
/** Takes over from the source (slider drag); applies immediately. */
|
|
574
|
-
hold(phase) {
|
|
575
|
-
this.held = wrapPhase(phase);
|
|
576
|
-
this.phase = this.held;
|
|
577
|
-
}
|
|
578
|
-
release() {
|
|
579
|
-
if (this.held === null) return;
|
|
580
|
-
this.source.seek?.(this.held);
|
|
581
|
-
this.held = null;
|
|
582
|
-
}
|
|
708
|
+
var MORNING = {
|
|
709
|
+
color: [
|
|
710
|
+
.78,
|
|
711
|
+
.7,
|
|
712
|
+
.58
|
|
713
|
+
],
|
|
714
|
+
ambient: [
|
|
715
|
+
.42,
|
|
716
|
+
.42,
|
|
717
|
+
.46
|
|
718
|
+
],
|
|
719
|
+
water: [
|
|
720
|
+
.98,
|
|
721
|
+
.96,
|
|
722
|
+
.96
|
|
723
|
+
],
|
|
724
|
+
sky: [
|
|
725
|
+
.03,
|
|
726
|
+
.02,
|
|
727
|
+
.01
|
|
728
|
+
],
|
|
729
|
+
glint: 1.1,
|
|
730
|
+
caustic: .85,
|
|
731
|
+
floor: .25,
|
|
732
|
+
shadowAlpha: .9,
|
|
733
|
+
glow: .45
|
|
583
734
|
};
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
};
|
|
611
|
-
slots.set(handle, slot);
|
|
612
|
-
return slot;
|
|
613
|
-
}
|
|
614
|
-
/** The pond's own background (`#06131a`), before time-of-day grading. */
|
|
615
|
-
var POND_BACKGROUND = "#06131a";
|
|
616
|
-
/**
|
|
617
|
-
* The same background as the shaders take it (`#06131a` to the byte). These
|
|
618
|
-
* exact values have fed the shaders since before palettes; keep them so the
|
|
619
|
-
* default pond stays pixel-identical.
|
|
620
|
-
*/
|
|
621
|
-
var POND_BG = [
|
|
622
|
-
.024,
|
|
623
|
-
.075,
|
|
624
|
-
.102
|
|
625
|
-
];
|
|
626
|
-
/** Pond's red markings and Ogon gold (the scene's colours, look.ts): the role defaults for accent and metallic. */
|
|
627
|
-
var POND_ACCENT = [
|
|
628
|
-
.95,
|
|
629
|
-
.31,
|
|
630
|
-
.17
|
|
631
|
-
];
|
|
632
|
-
var POND_METALLIC = [
|
|
633
|
-
.97,
|
|
634
|
-
.72,
|
|
635
|
-
.24
|
|
636
|
-
];
|
|
637
|
-
var WHITE = [
|
|
638
|
-
1,
|
|
639
|
-
1,
|
|
640
|
-
1
|
|
641
|
-
];
|
|
642
|
-
/** Derived highlight on a light background. */
|
|
643
|
-
var NEAR_BLACK = [
|
|
644
|
-
.08,
|
|
645
|
-
.09,
|
|
646
|
-
.1
|
|
647
|
-
];
|
|
648
|
-
/** Q13: a background brighter than this (WCAG relative luminance) is light. */
|
|
649
|
-
var LIGHT_LUMINANCE = .4;
|
|
650
|
-
/** Missing role slots sit this far from background toward highlight (Q12). */
|
|
651
|
-
var DERIVE = {
|
|
652
|
-
water: .3,
|
|
653
|
-
ink: .45,
|
|
654
|
-
pale: .85
|
|
735
|
+
var NOON = {
|
|
736
|
+
color: [
|
|
737
|
+
.72,
|
|
738
|
+
.71,
|
|
739
|
+
.67
|
|
740
|
+
],
|
|
741
|
+
ambient: [
|
|
742
|
+
.44,
|
|
743
|
+
.46,
|
|
744
|
+
.5
|
|
745
|
+
],
|
|
746
|
+
water: [
|
|
747
|
+
1.08,
|
|
748
|
+
1.08,
|
|
749
|
+
1.04
|
|
750
|
+
],
|
|
751
|
+
sky: [
|
|
752
|
+
0,
|
|
753
|
+
0,
|
|
754
|
+
0
|
|
755
|
+
],
|
|
756
|
+
glint: 1,
|
|
757
|
+
caustic: 1,
|
|
758
|
+
floor: .25,
|
|
759
|
+
shadowAlpha: 1,
|
|
760
|
+
glow: .35
|
|
655
761
|
};
|
|
656
|
-
var
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
tint: WHITE,
|
|
662
|
-
saturation: 1,
|
|
663
|
-
invert: false
|
|
664
|
-
});
|
|
665
|
-
var presets = {
|
|
666
|
-
pond: { mode: "filter" },
|
|
667
|
-
mono: {
|
|
668
|
-
mode: "ramp",
|
|
669
|
-
colors: [
|
|
670
|
-
"#0b0b0b",
|
|
671
|
-
"#5c5c5c",
|
|
672
|
-
"#f2f2f2"
|
|
673
|
-
]
|
|
762
|
+
var KEYS = [
|
|
763
|
+
{
|
|
764
|
+
at: 0,
|
|
765
|
+
part: "night",
|
|
766
|
+
...NIGHT
|
|
674
767
|
},
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
"#1b7a35",
|
|
680
|
-
"#b6ffbf"
|
|
681
|
-
]
|
|
768
|
+
{
|
|
769
|
+
at: .21,
|
|
770
|
+
part: "blue hour",
|
|
771
|
+
...BLUE
|
|
682
772
|
},
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
"#8a4f00",
|
|
688
|
-
"#ffe2a8"
|
|
689
|
-
]
|
|
773
|
+
{
|
|
774
|
+
at: .265,
|
|
775
|
+
part: "golden hour",
|
|
776
|
+
...GOLDEN
|
|
690
777
|
},
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
highlight: "#1d2629",
|
|
696
|
-
fish: {
|
|
697
|
-
pale: "#4c5759",
|
|
698
|
-
ink: "#111416",
|
|
699
|
-
accent: "#c8452c",
|
|
700
|
-
metallic: "#b5832a"
|
|
701
|
-
}
|
|
778
|
+
{
|
|
779
|
+
at: .34,
|
|
780
|
+
part: "morning",
|
|
781
|
+
...MORNING
|
|
702
782
|
},
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
783
|
+
{
|
|
784
|
+
at: .5,
|
|
785
|
+
part: "noon",
|
|
786
|
+
...NOON
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
at: .66,
|
|
790
|
+
part: "afternoon",
|
|
791
|
+
...MORNING
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
at: .745,
|
|
795
|
+
part: "golden hour",
|
|
796
|
+
...GOLDEN
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
at: .79,
|
|
800
|
+
part: "blue hour",
|
|
801
|
+
...BLUE
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
at: 1,
|
|
805
|
+
part: "night",
|
|
806
|
+
...NIGHT
|
|
714
807
|
}
|
|
808
|
+
];
|
|
809
|
+
KEYS.slice(0, -1).map(({ at, part }) => ({
|
|
810
|
+
at,
|
|
811
|
+
name: part
|
|
812
|
+
}));
|
|
813
|
+
/** The moon hands over to the sun across [a, b] and back across [1 - b, 1 - a]. */
|
|
814
|
+
var HANDOFF = [.19, .23];
|
|
815
|
+
/**
|
|
816
|
+
* The sun's path is tilted on screen so the noon light still comes from one
|
|
817
|
+
* side: a fish swimming any way but along that diagonal shows a lit flank.
|
|
818
|
+
*/
|
|
819
|
+
var PATH_TILT = -.45;
|
|
820
|
+
/** Sun elevation (rad) at sunrise/sunset and at noon; never 0 so it always lights something. */
|
|
821
|
+
var SUN_LOW = .1;
|
|
822
|
+
var SUN_HIGH = 1;
|
|
823
|
+
var MOON_LOW = .25;
|
|
824
|
+
var MOON_HIGH = .95;
|
|
825
|
+
/** Shadow length per unit of cot(elevation), in pond units, and its cap for a low sun. */
|
|
826
|
+
var SHADOW_PER_COT = .16;
|
|
827
|
+
var SHADOW_MAX = .24;
|
|
828
|
+
var TAU$1 = Math.PI * 2;
|
|
829
|
+
/** GLSL smoothstep. */
|
|
830
|
+
var smooth = (a, b, x) => {
|
|
831
|
+
const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
|
|
832
|
+
return t * t * (3 - 2 * t);
|
|
715
833
|
};
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
834
|
+
var mix = (a, b, t) => a + (b - a) * t;
|
|
835
|
+
var mix3 = (a, b, t) => [
|
|
836
|
+
mix(a[0], b[0], t),
|
|
837
|
+
mix(a[1], b[1], t),
|
|
838
|
+
mix(a[2], b[2], t)
|
|
839
|
+
];
|
|
840
|
+
/**
|
|
841
|
+
* Scratch that azimuth() (x, y, length) and arcDir() (sun, moon) write into, so a frame
|
|
842
|
+
* allocates no temporaries: single-threaded, and each is read straight after its write.
|
|
843
|
+
*/
|
|
844
|
+
var AZ = /* @__PURE__ */ new Float64Array(3);
|
|
845
|
+
var SUN_D = /* @__PURE__ */ new Float64Array(3);
|
|
846
|
+
var MOON_D = /* @__PURE__ */ new Float64Array(3);
|
|
847
|
+
/**
|
|
848
|
+
* Direction toward a body, into `out`, on an arc that rises at angle 0 (screen
|
|
849
|
+
* left) and sets at π (screen right), with elevation lo..hi following sin(angle).
|
|
850
|
+
*/
|
|
851
|
+
function arcDir(angle, lo, hi, out) {
|
|
852
|
+
const el = mix(lo, hi, Math.max(0, Math.sin(angle)));
|
|
853
|
+
azimuth(angle);
|
|
854
|
+
const h = Math.cos(el) / AZ[2];
|
|
855
|
+
out[0] = AZ[0] * h;
|
|
856
|
+
out[1] = AZ[1] * h;
|
|
857
|
+
out[2] = Math.sin(el);
|
|
727
858
|
}
|
|
728
|
-
/**
|
|
729
|
-
function
|
|
730
|
-
const
|
|
731
|
-
|
|
859
|
+
/** A body's azimuth on screen at `angle` (see arcDir), tilted, into AZ; its length is the same before and after. */
|
|
860
|
+
function azimuth(angle) {
|
|
861
|
+
const ax = -Math.cos(angle);
|
|
862
|
+
const ay = -.8 * Math.sin(angle);
|
|
863
|
+
const c = Math.cos(PATH_TILT);
|
|
864
|
+
const s = Math.sin(PATH_TILT);
|
|
865
|
+
AZ[0] = ax * c - ay * s;
|
|
866
|
+
AZ[1] = ax * s + ay * c;
|
|
867
|
+
AZ[2] = Math.hypot(ax, ay) || 1;
|
|
732
868
|
}
|
|
733
|
-
|
|
734
|
-
|
|
869
|
+
/** Half-extents of the path reflections follow, as a share of the pond's width and height. */
|
|
870
|
+
var BODY_BAND = [.38, .36];
|
|
871
|
+
/** Superellipse exponent of that path: high enough to hug the edges, low enough to round the corners. */
|
|
872
|
+
var BODY_ROUND = 6;
|
|
873
|
+
/** The moon's radius as a share of the pond's shorter side. */
|
|
874
|
+
var MOON_RADIUS = .065;
|
|
875
|
+
/** The moon fades in after the evening handoff and out before the morning one (D6). */
|
|
876
|
+
var MOON_RISE = [1 - HANDOFF[0], 1 - HANDOFF[0] + .05];
|
|
877
|
+
var MOON_SET = [HANDOFF[0] - .05, HANDOFF[0]];
|
|
878
|
+
/** The sun's glitter patch: its outer edge as a share of the pond's shorter side. */
|
|
879
|
+
var SUN_RADIUS = .1;
|
|
880
|
+
/** The sun fades in after the morning handoff and out before the evening one (D6). */
|
|
881
|
+
var SUN_RISE = [HANDOFF[1], HANDOFF[1] + .05];
|
|
882
|
+
var SUN_SET = [1 - HANDOFF[1] - .05, 1 - HANDOFF[1]];
|
|
883
|
+
/**
|
|
884
|
+
* The sun's path runs at this share of its light's pace, so its visible day
|
|
885
|
+
* (HANDOFF[1] … 1 − HANDOFF[1]) covers exactly the moon's visible night: up
|
|
886
|
+
* the left side, over the top, out near the top-right corner. At the light's
|
|
887
|
+
* own pace it would also run down the right side (P1: the top edge and one side).
|
|
888
|
+
*/
|
|
889
|
+
var SUN_PACE = 2 * HANDOFF[0] / (1 - 2 * HANDOFF[1]);
|
|
890
|
+
/**
|
|
891
|
+
* Where a body's reflection sits: toward its azimuth from the pond's centre,
|
|
892
|
+
* out on a rounded rectangle near the edges (half-extents BODY_BAND, as a
|
|
893
|
+
* share of width and height). Every point of it is at least 0.89 of the way
|
|
894
|
+
* out in one axis, so with a radius under 0.12 of the shorter side the disc
|
|
895
|
+
* keeps clear of the middle 40 % of the pond at any aspect (P1).
|
|
896
|
+
*/
|
|
897
|
+
function bodyPos(angle) {
|
|
898
|
+
azimuth(angle);
|
|
899
|
+
const x = AZ[0];
|
|
900
|
+
const y = AZ[1];
|
|
901
|
+
const k = ((Math.abs(x) / BODY_BAND[0]) ** BODY_ROUND + (Math.abs(y) / BODY_BAND[1]) ** BODY_ROUND) ** (-1 / BODY_ROUND);
|
|
902
|
+
return [.5 + x * k, .5 + y * k];
|
|
735
903
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
904
|
+
/**
|
|
905
|
+
* The moon's reflection at `phase`, from the phase alone (not the blended
|
|
906
|
+
* light direction, which swings to the sun at the handoff). It follows the
|
|
907
|
+
* moon's own azimuth: up the left side after dusk, over the top through the
|
|
908
|
+
* night, and gone before either handoff starts, so it never jumps.
|
|
909
|
+
*/
|
|
910
|
+
function moonAt(phase) {
|
|
911
|
+
const p = (phase % 1 + 1) % 1;
|
|
912
|
+
const alpha = 1 - smooth(MOON_SET[0], MOON_SET[1], p) * (1 - smooth(MOON_RISE[0], MOON_RISE[1], p));
|
|
913
|
+
return {
|
|
914
|
+
pos: bodyPos(TAU$1 * (p - .75)),
|
|
915
|
+
radius: MOON_RADIUS,
|
|
916
|
+
alpha
|
|
917
|
+
};
|
|
742
918
|
}
|
|
743
|
-
|
|
744
|
-
|
|
919
|
+
/**
|
|
920
|
+
* The sun's glitter at `phase`, from the phase alone, on the moon's path
|
|
921
|
+
* (see moonAt and SUN_PACE): rises on the left after the morning handoff,
|
|
922
|
+
* over the top at noon, gone near the top-right corner before the evening
|
|
923
|
+
* handoff, so it never jumps and never shares the sky with the moon.
|
|
924
|
+
*/
|
|
925
|
+
function sunAt(phase) {
|
|
926
|
+
const p = (phase % 1 + 1) % 1;
|
|
927
|
+
const alpha = smooth(SUN_RISE[0], SUN_RISE[1], p) * (1 - smooth(SUN_SET[0], SUN_SET[1], p));
|
|
928
|
+
return {
|
|
929
|
+
pos: bodyPos(TAU$1 * (.25 + (p - .5) * SUN_PACE)),
|
|
930
|
+
radius: SUN_RADIUS,
|
|
931
|
+
alpha
|
|
932
|
+
};
|
|
745
933
|
}
|
|
934
|
+
/** Flat water and a graded background at full night with `darkNight` on. */
|
|
935
|
+
var DARK_WATER = .7;
|
|
936
|
+
/** Flat-water density at full night with `darkNight` on: about half the caustics show. */
|
|
937
|
+
var DARK_CAUSTIC = .5;
|
|
938
|
+
/** The blue hours the night fades across: out at dawn, back in at dusk. */
|
|
939
|
+
var NIGHT_DAWN = [.17, .24];
|
|
940
|
+
var NIGHT_DUSK = [.76, .83];
|
|
746
941
|
/**
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
942
|
+
* How much of the night shows at a phase: 1 from the end of the evening blue
|
|
943
|
+
* hour to the start of the morning one, 0 by day, a smooth fade across each
|
|
944
|
+
* blue hour (so golden hour is still pure day).
|
|
750
945
|
*/
|
|
751
|
-
function
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
return resolve(input, parse);
|
|
755
|
-
} catch (err) {
|
|
756
|
-
warn(`could not read the palette (${String(err)}); using 'pond'.`);
|
|
757
|
-
return POND;
|
|
758
|
-
}
|
|
946
|
+
function nightWeight(phase) {
|
|
947
|
+
const p = (phase % 1 + 1) % 1;
|
|
948
|
+
return 1 - smooth(NIGHT_DAWN[0], NIGHT_DAWN[1], p) * (1 - smooth(NIGHT_DUSK[0], NIGHT_DUSK[1], p));
|
|
759
949
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if (mode === "roles") return resolveRoles(p, parse);
|
|
770
|
-
if (mode === "filter") return resolveFilter(p, parse);
|
|
771
|
-
warn(`unknown palette mode '${String(mode)}' (expected 'ramp', 'roles' or 'filter'); using 'pond'.`);
|
|
772
|
-
return POND;
|
|
950
|
+
/** Night weight below which no star shows: they arrive after the night and leave before it. */
|
|
951
|
+
var STARS_FROM = .35;
|
|
952
|
+
/**
|
|
953
|
+
* How strongly the reflected stars show at a phase (the `stars` sky
|
|
954
|
+
* feature): `smoothstep(0.35, 1, nightWeight)`, so 0 through the golden
|
|
955
|
+
* hours and the start of each blue hour, 1 at full night.
|
|
956
|
+
*/
|
|
957
|
+
function starWeight(phase) {
|
|
958
|
+
return smooth(STARS_FROM, 1, nightWeight(phase));
|
|
773
959
|
}
|
|
774
|
-
/**
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
960
|
+
/** The fireflies' dusk window: in after the golden hour, out into the night (never at dawn). */
|
|
961
|
+
var FIREFLIES_IN = [.745, .8];
|
|
962
|
+
var FIREFLIES_OUT = [.88, .95];
|
|
963
|
+
/**
|
|
964
|
+
* How strongly the fireflies show at a phase (the `fireflies` sky feature):
|
|
965
|
+
* 0 until the golden hour ends, 1 through the evening blue hour and early
|
|
966
|
+
* night, 0 again from 0.95 through the night, the morning and the day.
|
|
967
|
+
*/
|
|
968
|
+
function fireflyWeight(phase) {
|
|
969
|
+
const p = (phase % 1 + 1) % 1;
|
|
970
|
+
return smooth(FIREFLIES_IN[0], FIREFLIES_IN[1], p) * (1 - smooth(FIREFLIES_OUT[0], FIREFLIES_OUT[1], p));
|
|
780
971
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
972
|
+
/** Lighting at `phase`; without options (or `darkNight` false) exactly the 0.1 lighting. */
|
|
973
|
+
function lightingAt(phase, options = {}) {
|
|
974
|
+
const p = (phase % 1 + 1) % 1;
|
|
975
|
+
let i = 0;
|
|
976
|
+
while (i < KEYS.length - 2 && p >= KEYS[i + 1].at) i++;
|
|
977
|
+
const a = KEYS[i];
|
|
978
|
+
const b = KEYS[i + 1];
|
|
979
|
+
const t = smooth(a.at, b.at, p);
|
|
980
|
+
arcDir(TAU$1 * (p - .25), SUN_LOW, SUN_HIGH, SUN_D);
|
|
981
|
+
arcDir(TAU$1 * (p - .75), MOON_LOW, MOON_HIGH, MOON_D);
|
|
982
|
+
const w = smooth(HANDOFF[0], HANDOFF[1], p) * (1 - smooth(1 - HANDOFF[1], 1 - HANDOFF[0], p));
|
|
983
|
+
const d0 = mix(MOON_D[0], SUN_D[0], w);
|
|
984
|
+
const d1 = mix(MOON_D[1], SUN_D[1], w);
|
|
985
|
+
const d2 = mix(MOON_D[2], SUN_D[2], w);
|
|
986
|
+
const dl = Math.hypot(d0, d1, d2);
|
|
987
|
+
const dir = [
|
|
988
|
+
d0 / dl,
|
|
989
|
+
d1 / dl,
|
|
990
|
+
d2 / dl
|
|
991
|
+
];
|
|
992
|
+
const horiz = Math.hypot(dir[0], dir[1]);
|
|
993
|
+
const len = Math.min(SHADOW_MAX, SHADOW_PER_COT * horiz / Math.max(dir[2], .001));
|
|
994
|
+
const shadow = horiz > 1e-6 ? [-dir[0] / horiz * len, -dir[1] / horiz * len] : [0, 0];
|
|
995
|
+
const floor = mix(a.floor, b.floor, t);
|
|
996
|
+
const water = [
|
|
997
|
+
Math.max(mix(a.water[0], b.water[0], t), floor),
|
|
998
|
+
Math.max(mix(a.water[1], b.water[1], t), floor),
|
|
999
|
+
Math.max(mix(a.water[2], b.water[2], t), floor)
|
|
1000
|
+
];
|
|
1001
|
+
const night = options.darkNight ? nightWeight(p) : 0;
|
|
1002
|
+
return {
|
|
1003
|
+
dir,
|
|
1004
|
+
color: mix3(a.color, b.color, t),
|
|
1005
|
+
ambient: mix3(a.ambient, b.ambient, t),
|
|
1006
|
+
water,
|
|
1007
|
+
sky: mix3(a.sky, b.sky, t),
|
|
1008
|
+
glint: mix(a.glint, b.glint, t),
|
|
1009
|
+
caustic: mix(a.caustic, b.caustic, t),
|
|
1010
|
+
floor,
|
|
1011
|
+
shadow,
|
|
1012
|
+
shadowAlpha: mix(a.shadowAlpha, b.shadowAlpha, t),
|
|
1013
|
+
glowStrength: mix(a.glow, b.glow, t),
|
|
1014
|
+
darkWater: night <= 0 ? 1 : mix(1, DARK_WATER, night),
|
|
1015
|
+
darkCaustic: night <= 0 ? 1 : mix(1, DARK_CAUSTIC, night),
|
|
1016
|
+
moon: moonAt(p),
|
|
1017
|
+
sun: sunAt(p)
|
|
803
1018
|
};
|
|
804
1019
|
}
|
|
805
|
-
/**
|
|
806
|
-
function
|
|
807
|
-
return
|
|
1020
|
+
/** Wraps any number into [0, 1). */
|
|
1021
|
+
function wrapPhase(p) {
|
|
1022
|
+
return p - Math.floor(p);
|
|
808
1023
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1024
|
+
/** Stays at whatever phase it was last set to. */
|
|
1025
|
+
var FixedSource = class {
|
|
1026
|
+
kind = "fixed";
|
|
1027
|
+
phase;
|
|
1028
|
+
constructor(phase) {
|
|
1029
|
+
this.phase = wrapPhase(phase);
|
|
814
1030
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
let background = color(p.background, "background", parse);
|
|
818
|
-
if (background === void 0) warn(`a roles palette needs a background; using ${POND_BACKGROUND}.`);
|
|
819
|
-
background ??= POND_BG;
|
|
820
|
-
const light = isLight(background);
|
|
821
|
-
const highlight = color(p.highlight, "highlight", parse) ?? (light ? NEAR_BLACK : WHITE);
|
|
822
|
-
const toward = (t) => mix(background, highlight, t);
|
|
823
|
-
const water = color(p.water, "water", parse) ?? toward(DERIVE.water);
|
|
824
|
-
let fish = {};
|
|
825
|
-
if (p.fish !== void 0) {
|
|
826
|
-
if (p.fish && typeof p.fish === "object") fish = p.fish;
|
|
827
|
-
else warn(`palette fish must be an object; deriving the fish colours.`);
|
|
1031
|
+
tick() {
|
|
1032
|
+
return this.phase;
|
|
828
1033
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
else if (p.invert !== void 0) warn(`palette invert must be true or false; using false.`);
|
|
858
|
-
const light = background ? isLight(background) : false;
|
|
859
|
-
if (light && !invert) warn(`a light filter background needs invert: true, or the glyphs barely show.`);
|
|
860
|
-
if (!light && invert) warn(`invert: true needs a light filter background, or the glyphs barely show.`);
|
|
861
|
-
return {
|
|
862
|
-
mode: "filter",
|
|
863
|
-
background,
|
|
864
|
-
css: background ? toCss(background) : POND_BACKGROUND,
|
|
865
|
-
light,
|
|
866
|
-
tint,
|
|
867
|
-
saturation,
|
|
868
|
-
invert
|
|
869
|
-
};
|
|
870
|
-
}
|
|
1034
|
+
seek(phase) {
|
|
1035
|
+
this.phase = wrapPhase(phase);
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
/** Runs through a whole day every `cycle` seconds. */
|
|
1039
|
+
var LoopSource = class {
|
|
1040
|
+
kind = "loop";
|
|
1041
|
+
phase;
|
|
1042
|
+
cycleS;
|
|
1043
|
+
constructor(cycle, phase) {
|
|
1044
|
+
this.cycleS = validCycle(cycle) ?? 600;
|
|
1045
|
+
this.phase = wrapPhase(phase);
|
|
1046
|
+
}
|
|
1047
|
+
get cycle() {
|
|
1048
|
+
return this.cycleS;
|
|
1049
|
+
}
|
|
1050
|
+
/** Ignores lengths outside [MIN_CYCLE_S, MAX_CYCLE_S]; keeps the phase. */
|
|
1051
|
+
set cycle(s) {
|
|
1052
|
+
this.cycleS = validCycle(s) ?? this.cycleS;
|
|
1053
|
+
}
|
|
1054
|
+
tick(dt) {
|
|
1055
|
+
this.phase = wrapPhase(this.phase + dt / this.cycleS);
|
|
1056
|
+
return this.phase;
|
|
1057
|
+
}
|
|
1058
|
+
seek(phase) {
|
|
1059
|
+
this.phase = wrapPhase(phase);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
871
1062
|
/**
|
|
872
|
-
*
|
|
873
|
-
*
|
|
874
|
-
* `fallback`; both with a warning. `optional`: undefined is the omitted
|
|
875
|
-
* option, which takes `fallback` silently.
|
|
1063
|
+
* The visitor's local time of day: 06:00 is 0.25 (sunrise), 18:00 is 0.75
|
|
1064
|
+
* (sunset). Fixed hours, no latitude; rendered time and seeks are ignored.
|
|
876
1065
|
*/
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1066
|
+
var ClockSource = class {
|
|
1067
|
+
now;
|
|
1068
|
+
kind = "clock";
|
|
1069
|
+
constructor(now = () => /* @__PURE__ */ new Date()) {
|
|
1070
|
+
this.now = now;
|
|
882
1071
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
}
|
|
887
|
-
|
|
1072
|
+
tick() {
|
|
1073
|
+
const d = this.now();
|
|
1074
|
+
return wrapPhase((d.getHours() + d.getMinutes() / 60 + d.getSeconds() / 3600 + d.getMilliseconds() / 36e5) / 24);
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
888
1077
|
/**
|
|
889
|
-
* The
|
|
890
|
-
*
|
|
891
|
-
*
|
|
1078
|
+
* The pond's clock: a time source plus a manual override. While the overlay
|
|
1079
|
+
* slider is held the phase follows it; on release the source continues from
|
|
1080
|
+
* there (a loop resumes from the released phase, a slider stays put).
|
|
892
1081
|
*/
|
|
893
|
-
var
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1082
|
+
var Sky = class {
|
|
1083
|
+
/** Phase used for the current frame. */
|
|
1084
|
+
phase;
|
|
1085
|
+
held = null;
|
|
1086
|
+
src;
|
|
1087
|
+
constructor(source) {
|
|
1088
|
+
this.src = source;
|
|
1089
|
+
this.phase = source.tick(0);
|
|
898
1090
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1091
|
+
get source() {
|
|
1092
|
+
return this.src;
|
|
1093
|
+
}
|
|
1094
|
+
/** Swaps the source (setTime); a held phase still wins until release. */
|
|
1095
|
+
setSource(source) {
|
|
1096
|
+
this.src = source;
|
|
1097
|
+
this.phase = this.held ?? source.tick(0);
|
|
1098
|
+
}
|
|
1099
|
+
get holding() {
|
|
1100
|
+
return this.held !== null;
|
|
1101
|
+
}
|
|
1102
|
+
/** Call once per rendered frame with unscaled dt; returns the phase. */
|
|
1103
|
+
update(dt) {
|
|
1104
|
+
this.phase = this.held ?? this.source.tick(dt);
|
|
1105
|
+
return this.phase;
|
|
1106
|
+
}
|
|
1107
|
+
/** Takes over from the source (slider drag); applies immediately. */
|
|
1108
|
+
hold(phase) {
|
|
1109
|
+
this.held = wrapPhase(phase);
|
|
1110
|
+
this.phase = this.held;
|
|
1111
|
+
}
|
|
1112
|
+
release() {
|
|
1113
|
+
if (this.held === null) return;
|
|
1114
|
+
this.source.seek?.(this.held);
|
|
1115
|
+
this.held = null;
|
|
915
1116
|
}
|
|
916
|
-
const rgba = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i.exec(a);
|
|
917
|
-
if (rgba) return [
|
|
918
|
-
Number(rgba[1]) / 255,
|
|
919
|
-
Number(rgba[2]) / 255,
|
|
920
|
-
Number(rgba[3]) / 255
|
|
921
|
-
];
|
|
922
|
-
ctx.fillStyle = a.replace(/\s*\/\s*[^/)]+\)$/, ")");
|
|
923
|
-
ctx.globalCompositeOperation = "copy";
|
|
924
|
-
ctx.fillRect(0, 0, 1, 1);
|
|
925
|
-
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
926
|
-
return [
|
|
927
|
-
r / 255,
|
|
928
|
-
g / 255,
|
|
929
|
-
b / 255
|
|
930
|
-
];
|
|
931
1117
|
};
|
|
1118
|
+
function validCycle(s) {
|
|
1119
|
+
return Number.isFinite(s) && s >= 1 && s <= 86400 ? s : null;
|
|
1120
|
+
}
|
|
1121
|
+
/** A phase in [0, 1] (1 wraps to 0), or null for anything else. */
|
|
1122
|
+
function validPhase(p) {
|
|
1123
|
+
return typeof p === "number" && p >= 0 && p <= 1 ? wrapPhase(p) : null;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Turns a `time` option into a source. Never throws: an unknown mode is a
|
|
1127
|
+
* loop, a cycle outside [MIN_CYCLE_S, MAX_CYCLE_S] is DEFAULT_CYCLE_S and a
|
|
1128
|
+
* phase outside [0, 1] is 0. A loop without a phase starts from `current`,
|
|
1129
|
+
* so switching to it carries on from the phase on screen.
|
|
1130
|
+
*/
|
|
1131
|
+
function timeSource(time, current) {
|
|
1132
|
+
const t = time ?? {};
|
|
1133
|
+
if (t.mode === "clock") return new ClockSource();
|
|
1134
|
+
if (t.mode === "fixed") return new FixedSource(validPhase(t.phase) ?? 0);
|
|
1135
|
+
const phase = t.phase === void 0 ? current : validPhase(t.phase) ?? 0;
|
|
1136
|
+
return new LoopSource(typeof t.cycle === "number" ? t.cycle : NaN, phase);
|
|
1137
|
+
}
|
|
1138
|
+
var slots = /* @__PURE__ */ new WeakMap();
|
|
1139
|
+
/** Called by createKoiPond for every instance. */
|
|
1140
|
+
function register(handle) {
|
|
1141
|
+
const slot = {
|
|
1142
|
+
hooks: {},
|
|
1143
|
+
core: null
|
|
1144
|
+
};
|
|
1145
|
+
slots.set(handle, slot);
|
|
1146
|
+
return slot;
|
|
1147
|
+
}
|
|
932
1148
|
/** Small seeded PRNG (mulberry32) so a `?seed=` URL reproduces a pond. */
|
|
933
1149
|
var Rng = class {
|
|
934
1150
|
s;
|
|
@@ -2074,22 +2290,35 @@ function overlaps(a, b) {
|
|
|
2074
2290
|
return !(a[2] < b[0] || b[2] < a[0] || a[3] < b[1] || b[3] < a[1]);
|
|
2075
2291
|
}
|
|
2076
2292
|
var fullscreen_default = "\nvoid main() {\nvec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\ngl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
|
|
2077
|
-
var common_default = "precision highp float;\nprecision highp int;\nprecision highp sampler2D;\n\n\n#define M_WATER 0\n#define M_RIPPLE 1\n#define M_TRAIL 2\n#define M_FIN 3\n#define M_EDGE 4\n#define M_BODY 5\n#define M_EYE 6\n#define M_GLINT 7\n#define M_STAR 8\n#define M_MOON 9\n#define M_FLY 10\n#define M_SUN 11\n\n#define PI 3.14159265\n#define TAU 6.28318531\n\n\nfloat hash12(vec2 p) {\nvec3 p3 = fract(vec3(p.xyx) * 0.1031);\np3 += dot(p3, p3.yzx + 33.33);\nreturn fract((p3.x + p3.y) * p3.z);\n}\n\nvec2 hash22(vec2 p) {\nvec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));\np3 += dot(p3, p3.yzx + 33.33);\nreturn fract((p3.xx + p3.yz) * p3.zy);\n}\n\n\nfloat gnoise(vec2 p) {\nvec2 i = floor(p);\nvec2 f = fract(p);\nvec2 u = f * f * (3.0 - 2.0 * f);\nfloat a = dot(hash22(i) * 2.0 - 1.0, f);\nfloat b = dot(hash22(i + vec2(1.0, 0.0)) * 2.0 - 1.0, f - vec2(1.0, 0.0));\nfloat c = dot(hash22(i + vec2(0.0, 1.0)) * 2.0 - 1.0, f - vec2(0.0, 1.0));\nfloat d = dot(hash22(i + vec2(1.0, 1.0)) * 2.0 - 1.0, f - vec2(1.0, 1.0));\nreturn mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(vec2 p) {\nfloat s = 0.0;\nfloat a = 0.5;\nfor (int i = 0; i < 4; i++) {\ns += a * gnoise(p);\np = mat2(1.6, 1.2, -1.2, 1.6) * p;\na *= 0.5;\n}\nreturn s;\n}\n\n#ifdef RIPPLE_PACKED\n\n\nvec2 enc16(float v) {\nfloat x = floor(clamp(v * 0.5 + 0.5, 0.0, 1.0) * 65535.0 + 0.5);\nfloat hi = floor(x / 256.0);\nreturn vec2(hi, x - hi * 256.0) / 255.0;\n}\n\nfloat dec16(vec2 e) {\nvec2 b = floor(e * 255.0 + 0.5);\nreturn (b.x * 256.0 + b.y) / 65535.0 * 2.0 - 1.0;\n}\n#endif\n";
|
|
2293
|
+
var common_default = "precision highp float;\nprecision highp int;\nprecision highp sampler2D;\n\n\n#define M_WATER 0\n#define M_RIPPLE 1\n#define M_TRAIL 2\n#define M_FIN 3\n#define M_EDGE 4\n#define M_BODY 5\n#define M_EYE 6\n#define M_GLINT 7\n#define M_STAR 8\n#define M_MOON 9\n#define M_FLY 10\n#define M_SUN 11\n\n\n\nbool avoidWater(int mat) { return mat <= M_TRAIL || mat == M_GLINT; }\n\n#define PI 3.14159265\n#define TAU 6.28318531\n\n\nfloat hash12(vec2 p) {\nvec3 p3 = fract(vec3(p.xyx) * 0.1031);\np3 += dot(p3, p3.yzx + 33.33);\nreturn fract((p3.x + p3.y) * p3.z);\n}\n\nvec2 hash22(vec2 p) {\nvec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));\np3 += dot(p3, p3.yzx + 33.33);\nreturn fract((p3.xx + p3.yz) * p3.zy);\n}\n\n\nfloat gnoise(vec2 p) {\nvec2 i = floor(p);\nvec2 f = fract(p);\nvec2 u = f * f * (3.0 - 2.0 * f);\nfloat a = dot(hash22(i) * 2.0 - 1.0, f);\nfloat b = dot(hash22(i + vec2(1.0, 0.0)) * 2.0 - 1.0, f - vec2(1.0, 0.0));\nfloat c = dot(hash22(i + vec2(0.0, 1.0)) * 2.0 - 1.0, f - vec2(0.0, 1.0));\nfloat d = dot(hash22(i + vec2(1.0, 1.0)) * 2.0 - 1.0, f - vec2(1.0, 1.0));\nreturn mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(vec2 p) {\nfloat s = 0.0;\nfloat a = 0.5;\nfor (int i = 0; i < 4; i++) {\ns += a * gnoise(p);\np = mat2(1.6, 1.2, -1.2, 1.6) * p;\na *= 0.5;\n}\nreturn s;\n}\n\n#ifdef RIPPLE_PACKED\n\n\nvec2 enc16(float v) {\nfloat x = floor(clamp(v * 0.5 + 0.5, 0.0, 1.0) * 65535.0 + 0.5);\nfloat hi = floor(x / 256.0);\nreturn vec2(hi, x - hi * 256.0) / 255.0;\n}\n\nfloat dec16(vec2 e) {\nvec2 b = floor(e * 255.0 + 0.5);\nreturn (b.x * 256.0 + b.y) / 65535.0 * 2.0 - 1.0;\n}\n#endif\n";
|
|
2078
2294
|
var ripple_default = "\n\nuniform sampler2D uState;\nuniform ivec2 uSize;\nuniform float uDamping;\nuniform vec4 uDrops[MAX_DROPS];\nuniform int uDropCount;\n\nin vec2 vPx;\nout vec4 fragColor;\n\nvec2 state(ivec2 c) {\nc = clamp(c, ivec2(0), uSize - 1);\nvec4 s = texelFetch(uState, c, 0);\n#ifdef RIPPLE_PACKED\nreturn vec2(dec16(s.rg), dec16(s.ba));\n#else\nreturn s.rg;\n#endif\n}\n\nvoid main() {\n\n\nivec2 c = ivec2(vPx);\nvec2 s = state(c);\nfloat lap = state(c + ivec2(1, 0)).x + state(c - ivec2(1, 0)).x\n+ state(c + ivec2(0, 1)).x + state(c - ivec2(0, 1)).x - 4.0 * s.x;\nfloat h = 2.0 * s.x - s.y + 0.3 * lap;\n\n\nvec2 fc = vec2(c) + 0.5;\nfloat edge = min(min(fc.x, fc.y), min(float(uSize.x) - fc.x, float(uSize.y) - fc.y));\nh *= uDamping * mix(0.86, 1.0, smoothstep(0.0, 8.0, edge));\n\nfor (int i = 0; i < MAX_DROPS; i++) {\nif (i >= uDropCount) break;\nvec4 d = uDrops[i];\nfloat x = length(fc - d.xy) / d.z;\nif (x < 1.0) {\nfloat k = 1.0 - x * x;\nh += d.w * k * k;\n}\n}\nh = clamp(h, -1.0, 1.0);\n\n#ifdef RIPPLE_PACKED\nfragColor = vec4(enc16(h), enc16(s.x));\n#else\nfragColor = vec4(h, s.x, 0.0, 1.0);\n#endif\n}\n";
|
|
2295
|
+
var uniforms_default = "\n\n\nuniform float uRipplePx;\nuniform int uFishCount;\nuniform vec2 uCell;\nuniform float uTime;\n\n\n\nuniform vec3 uLightDir;\nuniform vec3 uLightCol;\nuniform vec3 uWaterTint;\nuniform float uCaustic;\nuniform float uCalm;\n\n\n\nuniform vec3 uWaterDim;\nuniform vec3 uWaterLit;\nuniform vec3 uOgonGold;\nuniform vec3 uGlintCol;\nuniform float uRoles;\n\n\nconst float RIPPLE_GAIN = 28.0;\n\n\nconst float REFRACT = 220.0;\n";
|
|
2296
|
+
var light_default = "\n\n\n\n\n\n\nuniform vec3 uAmbient;\nuniform float uFloor;\n\n\nconst float CALM_FLICKER = 0.3;\n\nfloat flicker() { return mix(1.0, CALM_FLICKER, uCalm); }\n\n\n\n\n\nvec3 diffuseDir() {\nreturn normalize(vec3(uLightDir.xy, max(uLightDir.z, 0.5)));\n}\n\n\n\nvec3 lightOn(vec3 n) {\nreturn max(uAmbient + uLightCol * max(dot(n, diffuseDir()), 0.0), vec3(uFloor));\n}\n\n\nvec3 lightHue() {\nreturn uLightCol / max(max(uLightCol.r, max(uLightCol.g, uLightCol.b)), 1e-3);\n}\n\n\n\nvec3 mirrorHue() {\nreturn mix(lightHue(), vec3(1.0), 0.4);\n}\n";
|
|
2297
|
+
var cell_default = "\n\n\n\n\n\nstruct Cell {\nvec3 col;\nfloat dens;\nint mat;\nvec2 dir;\nfloat aux;\nfloat style;\n};\n\n\nvec2 lineDir(vec2 d) {\nvec2 c = d / uCell;\nfloat l = length(c);\nreturn l > 1e-6 ? c / l : vec2(1.0, 0.0);\n}\n\nvec2 cellNormal(vec2 n) {\nvec2 c = n * uCell;\nfloat l = length(c);\nreturn l > 1e-6 ? c / l : vec2(1.0, 0.0);\n}\n\n\nfloat coverage(float d, vec2 n) {\nfloat span = abs(n.x) * uCell.x + abs(n.y) * uCell.y;\nreturn clamp(0.5 - d / span, 0.0, 1.0);\n}\n\nfloat sq(float x) { return x * x; }\n\nbool inBox(vec2 p, vec4 box, float pad) {\nreturn all(greaterThanEqual(p, box.xy - pad)) && all(lessThanEqual(p, box.zw + pad));\n}\n";
|
|
2298
|
+
var ripples_default = "\n\n\n\n\n\nuniform sampler2D uRipple;\nuniform ivec2 uRippleSize;\n\nfloat rippleTexel(ivec2 c) {\nc = clamp(c, ivec2(0), uRippleSize - 1);\n#ifdef RIPPLE_PACKED\nreturn dec16(texelFetch(uRipple, c, 0).rg);\n#else\nreturn texelFetch(uRipple, c, 0).r;\n#endif\n}\n\nfloat rippleAt(vec2 p) {\nvec2 t = p / uRipplePx - 0.5;\nvec2 i = floor(t);\nvec2 f = t - i;\nivec2 c = ivec2(i);\nreturn mix(mix(rippleTexel(c), rippleTexel(c + ivec2(1, 0)), f.x),\nmix(rippleTexel(c + ivec2(0, 1)), rippleTexel(c + ivec2(1, 1)), f.x), f.y);\n}\n";
|
|
2299
|
+
var water_default = "\n\n\n\n\n\n\n\nuniform vec3 uSky;\nuniform float uGlint;\nuniform float uDarkWater;\nuniform float uDarkCaustic;\nuniform float uMoteThin;\nuniform vec3 uRippleCol;\n\n\n\n\n\nconst float GLINT_TILT = 2.0;\n\n\nfloat caustic(vec2 x, float t) {\nvec2 i = floor(x);\nvec2 f = fract(x);\nfloat f1 = 8.0;\nfloat f2 = 8.0;\nfor (int y = -1; y <= 1; y++) {\nfor (int k = -1; k <= 1; k++) {\nvec2 b = vec2(float(k), float(y));\nvec2 o = 0.5 + 0.42 * sin(t + TAU * hash22(i + b));\nfloat d = length(b + o - f);\nif (d < f1) {\nf2 = f1;\nf1 = d;\n} else if (d < f2) {\nf2 = d;\n}\n}\n}\nreturn 1.0 - smoothstep(0.0, 0.22, f2 - f1);\n}\n\nCell water(vec2 p, float h, vec2 g) {\nCell c;\n\nvec2 mp = floor((p + vec2(7.0, -4.0) * uTime) / uCell);\n\nfloat mote = step(mix(0.96, 0.99, uMoteThin), hash12(mp)) * (0.4 + 0.6 * hash12(mp + 17.0));\n\n\n\nfloat shimmer = 0.8 + 0.6 * fbm(p * 0.0016 + vec2(uTime * 0.012, -uTime * 0.008));\nfloat lit = smoothstep(0.45, 0.95, caustic(p * 0.011 + g * 3.0, uTime * 0.45) * shimmer);\nc.mat = M_WATER;\n\nc.dens = max(mote * 0.5, lit * mix(0.6, 1.0, uCaustic));\n\nc.col = mix(uWaterDim, uWaterLit, 0.6 * lit * min(uCaustic, 1.0)) * uWaterTint + uSky;\n\n\nif (uDarkWater < 1.0) c.col *= uDarkWater;\nif (uDarkCaustic < 1.0) c.dens *= uDarkCaustic;\nc.aux = hash12(mp + 3.1);\nc.dir = vec2(1.0, 0.0);\nc.style = 0.0;\n\nfloat rip = smoothstep(0.15, 0.6, length(g) * RIPPLE_GAIN);\nif (rip > 0.25) {\nc.mat = M_RIPPLE;\nc.dens = rip;\nc.col = mix(uWaterLit, uRippleCol, rip) * uWaterTint + uSky;\nc.dir = lineDir(vec2(-g.y, g.x));\nc.aux = h > 0.0 ? 1.0 : 0.0;\n}\n\n\n\n\nvec3 H = normalize(uLightDir + vec3(0.0, 0.0, 1.0));\nvec2 az = normalize(uLightDir.xy + vec2(1e-5, 0.0));\nvec2 across = vec2(-az.y, az.x);\nfloat low = 1.0 - uLightDir.z;\nvec2 e = -g * RIPPLE_GAIN * GLINT_TILT - H.xy / H.z;\nfloat ea = dot(e, az);\nfloat ec = dot(e, across);\n\n\nfloat gl = exp(-sq(ea / (0.3 + 0.4 * low)) - sq(ec / (0.3 + 0.8 * low)));\ngl *= smoothstep(0.06, 0.25, length(g) * RIPPLE_GAIN);\n\n\nvec2 r = vec2(dot(p, az), dot(p, across)) / (max(uCell.x, uCell.y) * vec2(1.0 + 5.0 * low, 1.0));\nfloat tw = hash12(floor(r) + floor(uTime * 6.0 * flicker()) * 0.613);\ngl *= uGlint * (0.45 + 1.1 * tw);\nif (gl > 0.45) {\nc.mat = M_GLINT;\nc.dens = min(gl, 1.0);\n\n\nvec3 gc = uRoles > 0.5 ? uGlintCol : mirrorHue();\nc.col = gc * mix(0.8, 1.0, smoothstep(0.45, 1.0, gl));\nc.dir = lineDir(az);\nc.aux = low;\n}\nreturn c;\n}\n";
|
|
2300
|
+
var sky_default = "\n\n\n\n\n\n\nuniform float uStars;\nuniform vec2 uView;\n\n\n\nuniform vec4 uMoon;\n\n\n\nuniform vec4 uSun;\nuniform vec3 uStarCool;\nuniform vec3 uStarWarm;\nuniform vec3 uMoonCol;\nuniform vec3 uSunCol;\n\n\n\nconst float MOON_GAIN = 0.85;\n\n\n\n\nconst float SUN_THIN = 0.5;\nconst vec2 SUN_GAIN = vec2(0.6, 0.9);\n\n\n\nconst float SUN_MIN = 0.45;\nconst float SUN_CORE = 0.115;\n\n\n\n\nvoid stars(vec2 p, vec2 g, inout Cell c) {\nif (uStars < 0.02 || c.mat != M_WATER) return;\nvec2 cell = floor((p + g * REFRACT * 2.0) / uCell);\n\nif (hash12(cell * 0.917 + 41.0) < 0.984) return;\nfloat mag = hash12(cell + 7.7);\nfloat ph = hash12(cell + 2.3) * TAU;\n\n\nfloat tw = 0.75 + 0.25 * sin(uTime * (0.8 + 1.6 * mag) + ph);\ntw += 0.5 * pow(max(sin(uTime * 0.37 + ph * 3.0), 0.0), 40.0);\ntw = mix(tw, 0.8, uCalm);\nfloat v = mag * tw * uStars;\nif (v < 0.15) return;\nc.mat = M_STAR;\nc.dens = v;\nc.aux = hash12(cell + 9.1);\nc.style = 0.0;\n\n\nfloat k = 0.35 + 0.6 * min(v, 1.0);\nvec3 col = mix(uStarCool, uStarWarm, c.aux);\nc.col = uRoles > 0.5 ? mix(uWaterDim, col, k) : col * k;\n}\n\n\n\n\n\nvoid moon(vec2 p, vec2 g, inout Cell c) {\nfloat a = uMoon.w;\nif (a < 0.02) return;\nfloat R = uMoon.z * min(uView.x, uView.y);\n\nvec2 wob = vec2(sin(p.y * 0.21 + uTime * 1.3 * flicker()) * 1.2, 0.0);\nvec2 dq = p + g * REFRACT * 3.0 + wob - uMoon.xy * uView;\nfloat dist = length(dq);\nif (dist > R * 1.4) return;\nvec2 n = dist > 1e-3 ? dq / dist : vec2(1.0, 0.0);\nfloat cv = coverage(dist - R, n);\nif (cv < 0.25) return;\nfloat r = dist / R;\nfloat maria = smoothstep(-0.05, 0.2, fbm(dq / R * 1.7 + 5.0));\nfloat lum = (0.95 - 0.45 * maria) * (1.0 - 0.25 * r * r);\nc.mat = M_MOON;\nc.aux = 0.0;\n\n\nfloat k = (0.35 + 0.5 * lum) * MOON_GAIN * a;\nc.col = uRoles > 0.5 ? mix(uWaterDim, uMoonCol, k) : uMoonCol * k;\nif (length(g) * RIPPLE_GAIN > 0.35) {\nc.style = 2.0;\nc.dens = a;\nc.dir = lineDir(vec2(-g.y, g.x));\nc.col = mix(c.col, uRoles > 0.5 ? uGlintCol : vec3(1.0), 0.3 * a);\n} else if (cv < 0.8) {\nc.style = 1.0;\nc.dens = cv;\nc.dir = cellNormal(n);\n} else {\nc.style = 0.0;\nc.dens = 0.72 * lum * a;\n}\n}\n\n\n\n\n\n\nvoid sun(vec2 p, vec2 g, inout Cell c) {\nfloat a = uSun.w;\nif (a < 0.02 || c.mat == M_GLINT) return;\nfloat R = uSun.z * min(uView.x, uView.y);\nvec2 dq = p + g * REFRACT * 3.0 - uSun.xy * uView;\nfloat r = length(dq) / R;\nif (r > 1.0) return;\nvec2 cell = floor(p / uCell);\n\n\nfloat flash = floor(uTime * 5.0 * flicker());\n\nif (hash12(cell * 1.31 + flash * 0.29 + 3.0) > (1.0 - SUN_THIN) * a) return;\nfloat tw = hash12(cell + flash * 0.613);\ntw = mix(tw, hash12(cell + 0.37), uCalm * 0.7);\nfloat v = (1.0 - smoothstep(SUN_CORE, 1.0, r)) * (0.35 + 0.9 * tw);\nif (v < SUN_MIN) return;\nc.mat = M_SUN;\nc.dens = min(v, 1.0);\nc.aux = 0.0;\nc.style = 0.0;\n\n\nfloat k = mix(SUN_GAIN.x, SUN_GAIN.y, smoothstep(SUN_MIN, 1.0, v));\nc.col = uRoles > 0.5 ? mix(uWaterDim, uSunCol, k) : uSunCol * mirrorHue() * k;\n}\n";
|
|
2301
|
+
var fish_default = "\n\n\n\n\n\nuniform sampler2D uFish;\n\nstruct Hit {\nfloat d;\nfloat u;\nfloat v;\nvec2 n;\n};\n\nvec4 F(int f, int i) { return texelFetch(uFish, ivec2(i, f), 0); }\n\n\n\nHit polyline(int f, int i0, int count, vec2 p) {\nHit h;\nh.d = 1e9;\nh.u = 0.0;\nh.v = 0.0;\nh.n = vec2(1.0, 0.0);\n\n\n\nfloat bu = 0.0, br = 0.0;\nvec2 bdq = vec2(0.0), bab = vec2(0.0), bap = vec2(0.0);\nvec4 A = F(f, i0);\nfor (int k = 0; k < count; k++) {\nvec4 B = F(f, i0 + k + 1);\nvec2 ab = B.xy - A.xy;\nvec2 ap = p - A.xy;\nfloat t = clamp(dot(ap, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);\nvec2 dq = ap - ab * t;\nfloat dist = length(dq);\nfloat r = mix(A.z, B.z, t);\nfloat d = dist - r;\nbool w = d < h.d;\nh.d = w ? d : h.d;\nbu = w ? (float(k) + t) / float(count) : bu;\nbr = w ? r : br;\nbdq = w ? dq : bdq;\nbab = w ? ab : bab;\nbap = w ? ap : bap;\nA = B;\n}\n\n\nif (h.d < 1e9) {\nfloat dist = length(bdq);\nh.u = bu;\nfloat side = bab.x * bap.y - bab.y * bap.x < 0.0 ? -1.0 : 1.0;\nh.v = side * dist / max(br, 1e-3);\nh.n = dist > 1e-4 ? bdq / dist : normalize(vec2(-bab.y, bab.x) + 1e-6);\n}\nreturn h;\n}\n\n\nfloat finDist(vec2 p, vec2 a, vec2 b, float r0, float r1, out float t) {\nvec2 ab = b - a;\nvec2 ap = p - a;\nt = clamp(dot(ap, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);\nfloat r = mix(r0, r1, smoothstep(0.0, 0.5, t)) * (1.0 - 0.45 * smoothstep(0.55, 1.0, t));\nreturn length(ap - ab * t) - r;\n}\n\n\n\nfloat tailSD(int f, vec2 q, vec4 fin) {\nHit th = polyline(f, J_BODY - 1, J_TAIL, q);\nvec2 tip = F(f, J_TOTAL - 1).xy;\nvec2 tdir = normalize(tip - F(f, J_TOTAL - 2).xy + 1e-6);\nreturn max(max(th.d, (th.u - 1.0 + fin.w * (1.0 - min(abs(th.v), 1.0))) * fin.z), dot(q - tip, tdir));\n}\n";
|
|
2302
|
+
var markings_default = "\n\n\n\n\n\n\n\nuniform vec3 uKohakuWhite;\nuniform vec3 uHiRed;\nuniform vec3 uSumi;\nuniform vec3 uShowaWhite;\nuniform vec3 uOgonShade;\nuniform vec3 uOgonGlint;\nuniform vec3 uKohakuFin;\nuniform vec3 uKohakuTail;\nuniform vec3 uOgonFin;\nuniform vec3 uOgonTail;\n\n\n\nvec3 kohaku(float u, float v, float seed) {\nfloat n = fbm(vec2(u * 3.0, v * 0.8) + seed)\n+ 0.08\n+ 0.35 * exp(-sq((u - 0.14) / 0.08))\n- 0.45 * v * v\n- 0.7 * smoothstep(0.75, 0.92, u)\n- 0.8 * (1.0 - smoothstep(0.02, 0.07, u));\nreturn mix(uKohakuWhite, uHiRed, smoothstep(-0.02, 0.04, n));\n}\n\n\nvec3 showa(float u, float v, float seed, out float sumiAmt) {\nfloat s = fbm(vec2(u * 3.5 + v * 0.9, v * 1.4) + seed * 1.7) + 0.04;\nfloat r = fbm(vec2(u * 2.6, v * 0.9) + seed * 2.3 + 31.0)\n+ 0.06 - 0.3 * v * v + 0.2 * exp(-sq((u - 0.12) / 0.1));\nfloat sumi = smoothstep(0.0, 0.04, s);\nfloat hi = smoothstep(0.0, 0.04, r);\nsumiAmt = sumi * (1.0 - 0.5 * hi);\nreturn mix(mix(uShowaWhite, uHiRed, hi), uSumi, sumiAmt);\n}\n\n\n\nvec3 ogon(float u, float v, float seed, out float glint) {\nvec2 sc = vec2(u * 26.0, v * 4.0 + 4.0);\nvec2 id = floor(vec2(sc.x + 0.5 * mod(floor(sc.y), 2.0), sc.y));\nfloat ph = hash12(id + seed);\nfloat twinkle = pow(0.5 + 0.5 * sin(uTime * 2.3 * flicker() + ph * TAU + u * 7.0), 14.0) * step(0.55, ph);\nfloat band = exp(-sq((v - 0.35 * sin(uTime * 0.6 + u * 4.0 + seed)) * 2.2));\nglint = max(twinkle, band * 0.6);\nvec3 base = mix(uOgonShade, uOgonGold, 1.0 - abs(v));\nreturn mix(base, uOgonGlint, glint);\n}\n\nvec3 pectoralColor(int variety, float t) {\nif (variety == V_KOHAKU) return uKohakuFin;\nif (variety == V_SHOWA) return mix(uSumi, uShowaWhite, smoothstep(0.15, 0.45, t));\nreturn uOgonFin;\n}\n\nvec3 tailColor(int variety, float u, float v, float seed) {\nif (variety == V_KOHAKU) return uKohakuTail;\nif (variety == V_SHOWA) {\nfloat streak = smoothstep(0.1, 0.2, gnoise(vec2(v * 3.0, u * 1.5) + seed));\nreturn mix(uShowaWhite, uSumi, streak * (1.0 - u * 0.6));\n}\nreturn mix(uOgonGold, uOgonTail, u);\n}\n";
|
|
2303
|
+
var trails_default = "\n\n\n\n\n\nuniform vec3 uSilk;\nuniform vec3 uBubble;\nuniform vec3 uGoldDust;\n\n\nvoid trailSeg(vec2 p, vec4 A, vec4 B, float w0, bool live, int variety,\ninout float best, inout vec2 bestDir, inout int bestVariety) {\nvec2 ab = B.xy - A.xy;\nvec2 ap = p - A.xy;\nfloat t = clamp(dot(ap, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);\nfloat dist = length(ap - ab * t);\nfloat age = mix(A.z, B.z, t);\nfloat w = w0 * (0.5 + 1.4 * age) + 0.3 * min(uCell.x, uCell.y);\nfloat I = pow(1.0 - age, 1.3) * (1.0 - smoothstep(w * 0.35, w, dist));\nif (live && I > best) {\nbest = I;\nbestDir = ab;\nbestVariety = variety;\n}\n}\n\n\n\n#if TRAIL_CHUNK % 4 != 0\n#error trails.glsl unrolls by 4\n#endif\n#if TRAIL_CHUNKS * TRAIL_CHUNK > TRAIL_N + TRAIL_CHUNKS - 1\n#error trails.glsl fetches past the fish row\n#endif\n\nvoid trails(vec2 p, inout Cell c) {\nfloat best = 0.0;\nvec2 bestDir = vec2(1.0, 0.0);\nint bestVariety = 0;\n\n\n\n\nfloat cellPad = 0.3 * min(uCell.x, uCell.y);\nfor (int f = 0; f < MAX_FISH; f++) {\nif (f >= uFishCount) break;\nif (!inBox(p, F(f, T_TBOX), cellPad)) continue;\nvec4 P1 = F(f, T_P1);\nint n = int(P1.x + 0.5);\nfloat w0 = P1.y;\nint variety = int(F(f, T_P0).x + 0.5);\n\n\n\n\nfor (int ch = 0; 1 + ch * TRAIL_CHUNK < n; ch++) {\nint k0 = 1 + ch * TRAIL_CHUNK;\nif (!inBox(p, F(f, T_CBOX + ch), cellPad)) continue;\nvec4 A = F(f, T_TRAIL + k0 - 1);\nfor (int j = 0; j < TRAIL_CHUNK; j += 4) {\nint k = k0 + j;\nif (k >= n) break;\n\n\n\nvec4 B0 = F(f, T_TRAIL + k), B1 = F(f, T_TRAIL + k + 1);\nvec4 B2 = F(f, T_TRAIL + k + 2), B3 = F(f, T_TRAIL + k + 3);\ntrailSeg(p, A, B0, w0, true, variety, best, bestDir, bestVariety);\ntrailSeg(p, B0, B1, w0, k + 1 < n, variety, best, bestDir, bestVariety);\ntrailSeg(p, B1, B2, w0, k + 2 < n, variety, best, bestDir, bestVariety);\ntrailSeg(p, B2, B3, w0, k + 3 < n, variety, best, bestDir, bestVariety);\nA = B3;\n}\n}\n}\nif (best < 0.05) return;\n\nvec2 cell = floor(p / uCell);\nvec3 col;\nif (bestVariety == V_KOHAKU) {\n\nbest *= 0.7 + 0.6 * gnoise(p * 0.035 - uTime * 0.5);\ncol = uSilk;\n} else if (bestVariety == V_SHOWA) {\n\nfloat hb = hash12(cell * 1.37 + 5.0);\nbest *= step(0.62, hb) * (0.7 + 0.6 * hb);\ncol = uBubble;\n} else {\n\nfloat tw = hash12(cell + floor(uTime * 7.0 * flicker()) * 0.731);\nbest *= step(0.5, tw) * 1.2;\ncol = uGoldDust;\n}\nif (best < 0.12) return;\nc.mat = M_TRAIL;\nc.dens = min(best, 1.0);\n\nc.col = (bestVariety == V_KOHAKU ? col * (0.6 + 0.4 * smoothstep(0.1, 0.6, best)) : mix(uWaterLit, col, smoothstep(0.1, 0.6, best))) * uWaterTint;\nc.dir = lineDir(bestDir);\nc.aux = 0.0;\nc.style = float(bestVariety);\n}\n";
|
|
2304
|
+
var compose_default = "\n\n\n\n\n\n\n\n\nuniform vec2 uShadow;\nuniform float uShadowAlpha;\nuniform vec3 uEyeCol;\n\n\nuniform vec3 uSumiRim;\nuniform vec3 uAddTint;\n\n\nconst float SPEC_POW = 18.0;\n\nfloat shadow(int f, vec2 p) {\nfloat lift = 1.25 - F(f, T_P0).w;\nvec2 q = p - uShadow * lift;\nfloat blur = max(uCell.x, uCell.y) * (0.5 + 1.5 * lift);\nif (!inBox(q, F(f, T_BBOX), blur)) return 0.0;\nfloat d = min(polyline(f, 0, J_BODY - 1, q).d,\npolyline(f, J_BODY - 1, J_TAIL, q).d + max(uCell.x, uCell.y) * 0.5);\nreturn (1.0 - smoothstep(-blur, blur, d)) * (0.95 - 0.35 * lift) * uShadowAlpha;\n}\n\n\nvec2 refrAt(vec2 x) {\nfloat e = uRipplePx;\nreturn vec2(rippleAt(x + vec2(e, 0.0)) - rippleAt(x - vec2(e, 0.0)),\nrippleAt(x + vec2(0.0, e)) - rippleAt(x - vec2(0.0, e))) / (2.0 * e) * REFRACT;\n}\n\nvoid drawFish(int f, vec2 p, vec2 refr, inout Cell c) {\nvec4 P0 = F(f, T_P0);\nint variety = int(P0.x + 0.5);\nfloat seed = P0.y;\nfloat depth = P0.w;\n\nvec2 q = p + refr * (0.5 + depth);\nif (!inBox(q, F(f, T_BBOX), max(uCell.x, uCell.y) * 1.5)) return;\nfloat fog = 0.55 * depth;\nvec3 fogCol = uWaterDim * uWaterTint;\nfloat fade = 1.0 - 0.3 * depth;\nfloat style = float(variety);\nvec4 fin = F(f, T_FIN);\n\nvec3 flatLight = lightOn(vec3(0.0, 0.0, 1.0));\n\n\n\nfloat reach = max(uCell.x, uCell.y);\nvec2 ex = vec2(uCell.x * 0.5, 0.0);\nvec2 ey = vec2(0.0, uCell.y * 0.5);\n\n\nfor (int s = 0; s < 2; s++) {\nvec4 pf = F(f, T_PEC_L + s);\nfloat t, t2;\nfloat d = finDist(q, pf.xy, pf.zw, fin.x, fin.y, t);\nif (d > reach) continue;\nvec2 gr = vec2(finDist(q + ex, pf.xy, pf.zw, fin.x, fin.y, t2) - finDist(q - ex, pf.xy, pf.zw, fin.x, fin.y, t2),\nfinDist(q + ey, pf.xy, pf.zw, fin.x, fin.y, t2) - finDist(q - ey, pf.xy, pf.zw, fin.x, fin.y, t2)) / uCell;\nvec2 n = normalize(gr + 1e-6);\nfloat cv = coverage(d, n);\nif (cv > 0.25) {\nc.col = mix(pectoralColor(variety, t) * flatLight, fogCol, fog);\nc.aux = 0.0;\nc.style = style;\nif (cv < 0.8) {\nc.mat = M_EDGE;\nc.dens = cv;\nc.dir = cellNormal(n);\n} else {\nc.mat = M_FIN;\nc.col *= 0.75;\nc.dens = 0.6 * fade;\nc.dir = lineDir(q - pf.xy);\n}\n}\n}\n\n\n\nfloat td = tailSD(f, q, fin);\nif (td < reach) {\nvec2 gr = vec2(tailSD(f, q + ex, fin) - tailSD(f, q - ex, fin),\ntailSD(f, q + ey, fin) - tailSD(f, q - ey, fin)) / uCell;\nvec2 n = normalize(gr + 1e-6);\nfloat tcv = coverage(td, n);\nif (tcv > 0.25) {\nHit th = polyline(f, J_BODY - 1, J_TAIL, q);\nc.col = mix(tailColor(variety, th.u, th.v, seed) * flatLight, fogCol, fog);\nc.aux = 0.0;\nc.style = style;\nif (tcv < 0.8) {\nc.mat = M_EDGE;\nc.dens = tcv;\nc.dir = cellNormal(n);\n} else {\nc.mat = M_FIN;\nc.col *= 0.75;\nc.dens = (0.65 - 0.2 * th.u) * fade;\nc.dir = lineDir(q - F(f, J_BODY - 1).xy);\n}\n}\n}\n\n\nHit bh = polyline(f, 0, J_BODY - 1, q);\nfloat bcv = coverage(bh.d, bh.n);\nif (bcv > 0.18) {\nfloat glint = 0.0;\nfloat sumiAmt = 0.0;\nvec3 bc = variety == V_KOHAKU ? kohaku(bh.u, bh.v, seed)\n: variety == V_SHOWA ? showa(bh.u, bh.v, seed, sumiAmt)\n: ogon(bh.u, bh.v, seed, glint);\n\n\nfloat a = min(abs(bh.v), 1.0) * 0.92;\nvec3 N = vec3(bh.n * a, sqrt(1.0 - a * a));\nfloat ndl = dot(N, diffuseDir());\nvec3 lit = lightOn(N);\n\n\n\nvec3 H = normalize(uLightDir + vec3(0.0, 0.0, 1.0));\nfloat spec = pow(max(dot(N, H), 0.0), SPEC_POW);\nvec3 hue = lightHue() * length(uLightCol) * 0.82 * uAddTint;\nvec3 hi = variety == V_OGON ? hue * uOgonGold * 0.9 : hue * (variety == V_SHOWA ? 0.32 + 0.25 * sumiAmt : 0.3);\n\nvec2 az = normalize(uLightDir.xy + vec2(1e-5, 0.0));\nfloat rim = a * a * a * max(-dot(bh.n, az), 0.0) * (0.35 + 0.65 * (1.0 - uLightDir.z));\n\n\nfloat cst = smoothstep(0.5, 0.95, caustic(q * 0.014 + refr * 0.012 + seed, uTime * 0.45));\nvec3 caust = uLightCol * uAddTint * cst * uCaustic * 0.4 * max(ndl, 0.0) * (1.0 - depth);\nc.col = mix(bc * lit + hi * spec + uLightCol * uAddTint * rim * 0.6 + caust, fogCol, fog);\n\nfloat shade = mix(0.58, 1.05, smoothstep(-0.2, 0.9, ndl)) + 0.25 * spec;\nc.style = style;\nif (bcv > 0.8) {\n\n\nfloat Yl = dot(bc * bc, vec3(0.2126, 0.7152, 0.0722));\nfloat Ls = 1.16 * pow(Yl, 1.0 / 3.0) - 0.16;\nc.mat = M_BODY;\nc.dens = mix(0.6, 1.0, Ls) * shade * fade;\nc.aux = glint;\n} else {\nc.mat = M_EDGE;\nc.dens = bcv;\nc.dir = cellNormal(bh.n);\nc.aux = 0.0;\nc.col = mix(c.col, mix(uSumiRim * lit, fogCol, fog), 0.65 * sumiAmt);\n}\n}\n\n\nvec4 E = F(f, T_EYE);\n\n\n\n\nvec2 eyeReach = 0.5 * uCell + REFRACT / uRipplePx * abs(0.5 + depth) + 1.0;\nfor (int s = 0; s < 2; s++) {\nvec2 e = s == 0 ? E.xy : E.zw;\nbool hit = length(q - e) < P0.z;\nif (!hit && all(lessThanEqual(abs(p - e), eyeReach))) {\n\n\nvec2 de = abs(p + refrAt(e) * (0.5 + depth) - e);\nhit = de.x < uCell.x * 0.5 && de.y < uCell.y * 0.5;\n}\nif (hit) {\nc.mat = M_EYE;\nc.col = mix(uEyeCol * max(flatLight, vec3(0.75)), fogCol, fog * 0.5);\nc.dens = 1.0;\nc.aux = 0.0;\nc.style = style;\n}\n}\n}\n";
|
|
2305
|
+
var glyphs_default = "\n\n\n\n\nconst int BODY_N = 6;\nconst int BODY_RAMP[BODY_N] = int[BODY_N](G_EQUALS, G_PLUS, G_STAR, G_HASH, G_PERCENT, G_AT);\n\nfloat bayer4(ivec2 c) {\nconst int B[16] = int[16](0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5);\nreturn (float(B[(c.x & 3) + (c.y & 3) * 4]) + 0.5) / 16.0;\n}\n\n\nint lineGlyph(vec2 d, int horizontal, int vertical) {\nfloat a = atan(d.y, d.x);\nif (a < 0.0) a += PI;\nint s = int(floor(a / (PI * 0.25) + 0.5)) % 4;\nif (s == 0) return horizontal;\nif (s == 1) return G_BSLASH;\nif (s == 2) return vertical;\nreturn G_SLASH;\n}\n\n\n\nint edgeGlyph(vec2 n, float cov) {\nint o = int(floor((atan(n.y, n.x) + TAU) / (PI * 0.25) + 0.5)) % 8;\nif (o == 0) return G_RPAREN;\nif (o == 4) return G_LPAREN;\nif (o == 1 || o == 5) return G_SLASH;\nif (o == 3 || o == 7) return G_BSLASH;\nif (o == 2) return cov > 0.5 ? G_UNDERSCORE : G_MINUS;\nreturn cov > 0.5 ? G_MINUS : G_UNDERSCORE;\n}\n\nint pickGlyph(Cell c, ivec2 cell) {\nfloat dens = clamp(c.dens, 0.0, 1.0);\nfloat aux = clamp(c.aux, 0.0, 1.0);\nint style = int(c.style + 0.5);\nif (c.mat == M_WATER) {\nif (dens < 0.12) return G_SPACE;\nif (dens < 0.6) return aux < 0.4 ? G_DOT : aux < 0.65 ? G_COMMA : aux < 0.85 ? G_BACKTICK : G_QUOTE;\nreturn G_COLON;\n}\nif (c.mat == M_RIPPLE) {\nif (dens < 0.45) return G_DOT;\nreturn lineGlyph(c.dir, aux > 0.5 ? G_TILDE : G_MINUS, G_PIPE);\n}\nif (c.mat == M_TRAIL) {\nif (style == V_SHOWA) return dens > 0.55 ? G_O : G_DOT;\nif (style == V_OGON) return dens > 0.65 ? G_STAR : dens > 0.35 ? G_QUOTE : G_DOT;\nif (dens < 0.3) return G_DOT;\nif (dens < 0.55) return G_MINUS;\nreturn lineGlyph(c.dir, G_TILDE, G_COLON);\n}\nif (c.mat == M_FIN) {\nif (dens < 0.3) return G_DOT;\nif (dens < 0.45) return G_QUOTE;\nreturn lineGlyph(c.dir, style == V_OGON ? G_TILDE : G_MINUS, G_PIPE);\n}\nif (c.mat == M_EDGE) return edgeGlyph(c.dir, dens);\nif (c.mat == M_BODY) {\nif (aux > 0.75) return G_STAR;\nfloat x = smoothstep(0.3, 0.95, dens) * float(BODY_N - 1) + (bayer4(cell) - 0.5) * 0.5;\nreturn BODY_RAMP[clamp(int(floor(x + 0.5)), 0, BODY_N - 1)];\n}\nif (c.mat == M_EYE) return G_O;\nif (c.mat == M_STAR) {\n\nif (dens > 0.95) return G_STAR;\nif (dens > 0.65) return G_PLUS;\nif (dens > 0.4) return aux < 0.5 ? G_QUOTE : G_BACKTICK;\nreturn G_DOT;\n}\nif (c.mat == M_MOON) {\nif (style == 2) return lineGlyph(c.dir, G_EQUALS, G_PIPE);\nif (style == 1) return edgeGlyph(c.dir, dens);\nreturn dens < 0.33 ? G_DOT : dens < 0.56 ? G_COLON : dens < 0.67 ? G_O : G_BIG_O;\n}\nif (c.mat == M_SUN) return dens > 0.8 ? G_STAR : dens > 0.6 ? G_PLUS : G_DOT;\nif (c.mat == M_GLINT) {\n\nif (aux > 0.55) return dens > 0.6 ? lineGlyph(c.dir, dens > 0.85 ? G_EQUALS : G_MINUS, G_PIPE) : G_DOT;\nreturn dens > 0.75 ? G_STAR : G_PLUS;\n}\nreturn G_SPACE;\n}\n";
|
|
2306
|
+
var avoid_default = "\n\n\n\n\n\n\n\n\n\n#define AVOID_MAX 16\n\nuniform int uAvoidN;\nuniform vec4 uAvoid[AVOID_MAX];\nuniform vec4 uAvoidR[AVOID_MAX / 4];\nuniform vec4 uAvoidFade;\n\n\nfloat avoidDist(vec2 p) {\nfloat d = 1e9;\n\nfor (int i = 0; i < uAvoidN; i++) {\nvec2 hs = uAvoid[i].zw * 0.5;\nfloat r = min(uAvoidR[i >> 2][i & 3], min(hs.x, hs.y));\nvec2 q = abs(p - uAvoid[i].xy - hs) - hs + r;\nd = min(d, length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r);\n}\nreturn d;\n}\n\n\n\nfloat avoidMask(float d) {\nreturn 1.0 - smoothstep(0.0, max(uAvoidFade.z, 0.5), d);\n}\n\n\n\nfloat avoidThin(float m, int mat) {\nreturn 1.0 - (avoidWater(mat) ? uAvoidFade.x : uAvoidFade.y) * m;\n}\n";
|
|
2079
2307
|
/** Each chunk's file name and source, in compile order. */
|
|
2080
2308
|
var sceneChunks = [
|
|
2081
|
-
["uniforms.glsl",
|
|
2082
|
-
["light.glsl",
|
|
2083
|
-
["cell.glsl",
|
|
2084
|
-
["ripples.glsl",
|
|
2085
|
-
["water.glsl",
|
|
2086
|
-
["sky.glsl",
|
|
2087
|
-
["fish.glsl",
|
|
2088
|
-
["markings.glsl",
|
|
2089
|
-
["trails.glsl",
|
|
2090
|
-
["compose.glsl",
|
|
2091
|
-
["glyphs.glsl",
|
|
2092
|
-
["
|
|
2309
|
+
["uniforms.glsl", uniforms_default],
|
|
2310
|
+
["light.glsl", light_default],
|
|
2311
|
+
["cell.glsl", cell_default],
|
|
2312
|
+
["ripples.glsl", ripples_default],
|
|
2313
|
+
["water.glsl", water_default],
|
|
2314
|
+
["sky.glsl", sky_default],
|
|
2315
|
+
["fish.glsl", fish_default],
|
|
2316
|
+
["markings.glsl", markings_default],
|
|
2317
|
+
["trails.glsl", trails_default],
|
|
2318
|
+
["compose.glsl", compose_default],
|
|
2319
|
+
["glyphs.glsl", glyphs_default],
|
|
2320
|
+
["avoid.glsl", avoid_default],
|
|
2321
|
+
["main.frag", "\n\n\n\n\n\n\n\n\n\n\n\n\n\nuniform int uOrder[MAX_FISH];\nuniform vec2 uOriginCss;\nuniform int uLayer;\nuniform int uFishDraw;\n\nlayout(location = 0) out vec4 oColor;\nlayout(location = 1) out vec4 oInfo;\n\nvoid main() {\nvec2 p = (floor(gl_FragCoord.xy) + 0.5) * uCell + uOriginCss;\n\nfloat e = uRipplePx;\nfloat h = rippleAt(p);\nvec2 g = vec2(rippleAt(p + vec2(e, 0.0)) - rippleAt(p - vec2(e, 0.0)),\nrippleAt(p + vec2(0.0, e)) - rippleAt(p - vec2(0.0, e))) / (2.0 * e);\n\nCell c;\nif (uLayer == 0) {\nc = water(p, h, g);\nstars(p, g, c);\nmoon(p, g, c);\nsun(p, g, c);\n\nfloat sh = 0.0;\n\n\n\nfor (int f = 0; f < uFishCount; f++) {\nsh = max(sh, shadow(f, p));\n}\nc.dens *= clamp(1.0 - 1.4 * sh, 0.0, 1.0);\nc.col *= 1.0 - 0.5 * sh;\n\ntrails(p, c);\n} else {\n\n\nc = Cell(vec3(0.0), 0.0, -1, vec2(1.0, 0.0), 0.0, 0.0);\n}\n\nvec2 refr = g * REFRACT;\nfor (int k = 0; k < MAX_FISH; k++) {\nif (k >= uFishDraw) break;\ndrawFish(uOrder[k], p, refr, c);\n}\n\nif (uLayer == 1 && c.mat < 0) discard;\n\n\n\n\nfloat m = 0.0;\nbool outline = false;\nif (uAvoidN > 0) {\nfloat d = avoidDist(p);\nm = avoidMask(d);\nif (m > 0.0) c.dens *= avoidThin(m, c.mat);\n\noutline = uAvoidFade.w > 0.5 && abs(d) < 0.5 * max(uCell.x, uCell.y);\n}\n\nint glyph = pickGlyph(c, ivec2(gl_FragCoord.xy));\nif (outline) {\nc.col = uHiRed;\nglyph = G_DOT;\nm = 0.0;\n}\noColor = vec4(c.col, clamp(c.dens, 0.0, 1.0));\noInfo = vec4(float(glyph) / 255.0, float(c.mat) / 255.0, m, 0.0);\n}\n"]
|
|
2093
2322
|
];
|
|
2094
2323
|
var sceneSrc = sceneChunks.map(([, src]) => src).join("\n");
|
|
2095
2324
|
/** The line each chunk starts on in sceneSrc, 1-based, so compile errors can name `sky.glsl:42`. */
|
|
@@ -2102,13 +2331,13 @@ var sceneLines = sceneChunks.map(([name, src]) => {
|
|
|
2102
2331
|
line += src.split("\n").length;
|
|
2103
2332
|
return start;
|
|
2104
2333
|
});
|
|
2105
|
-
var ascii_default = "\n\n\nuniform sampler2D uScene0;\nuniform sampler2D uScene1;\nuniform sampler2D uAtlas;\nuniform ivec2 uCellPx;\nuniform ivec2 uOrigin;\nuniform ivec2 uGrid;\nuniform int uCanvasH;\nuniform vec3 uBg;\nuniform vec3 uRawBg;\nuniform int uRaw;\n\nuniform int uLook;\nuniform vec3 uOutBg;\nuniform vec3 uStops[5];\nuniform vec3 uStopDeltas[4];\nuniform int uStopCount;\nuniform vec3 uTint;\nuniform float uSaturation;\nuniform int uInvert;\nuniform float uIntensity;\n#ifdef GLOW\nuniform sampler2D uGlow;\nuniform vec2 uGlowScale;\nuniform int uGlowOn;\n#endif\n\nin vec2 vPx;\nout vec4 fragColor;\n\n\n\n\nfloat lum(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }\n\n\nvec3 ramp(vec3 pix) {\nfloat t = clamp((lum(pix) - lum(uBg)) / (0.9 - lum(uBg)), 0.0, 1.0);\nfloat x = t * float(uStopCount - 1);\n\n\nfloat fi = min(floor(x), float(uStopCount - 2));\n\n\n\n\nvec3 a = uStops[0], d = uStopDeltas[0];\nif (uStopCount > 2) {\nif (fi >= 1.0) { a = uStops[1]; d = uStopDeltas[1]; }\nif (uStopCount > 3) {\nif (fi >= 2.0) { a = uStops[2]; d = uStopDeltas[2]; }\nif (uStopCount > 4 && fi >= 3.0) { a = uStops[3]; d = uStopDeltas[3]; }\n}\n}\n\n\nreturn a + (x - fi) * d;\n}\n\n\n\nvec3 filtered(vec3 pix) {\nvec3 d = pix - uBg;\nfloat y = lum(d);\nvec3 chroma = (d - vec3(y)) * uSaturation;\n\n\n\n\nfloat tl = lum(uTint);\nif (uInvert == 1) return uOutBg - y * 1.25 * tl * (vec3(1.0 + tl) - uTint) + chroma * 0.9;\nreturn uOutBg + (vec3(y) + chroma) * uTint;\n}\n\nvoid main() {\n\n\nivec2 q = ivec2(int(vPx.x), uCanvasH - 1 - int(vPx.y)) - uOrigin;\n\n\nvec2 cf = floor((vec2(q) + 0.5) / vec2(uCellPx));\nivec2 cell = ivec2(cf);\n\n\nif (any(greaterThanEqual(uvec2(cell), uvec2(uGrid)))) {\nfragColor = vec4(uRaw == 1 ? uRawBg : uOutBg, 1.0);\nreturn;\n}\n\n\nivec2 local = ivec2(vec2(q) - cf * vec2(uCellPx));\nvec4 s0 = texelFetch(uScene0, cell, 0);\nif (uRaw == 1) {\nfragColor = vec4(mix(uRawBg, s0.rgb, s0.a), 1.0);\nreturn;\n}\
|
|
2334
|
+
var ascii_default = "\n\n\nuniform sampler2D uScene0;\nuniform sampler2D uScene1;\nuniform sampler2D uAtlas;\nuniform ivec2 uCellPx;\nuniform ivec2 uOrigin;\nuniform ivec2 uGrid;\nuniform int uCanvasH;\nuniform vec3 uBg;\nuniform vec3 uRawBg;\nuniform int uRaw;\n\nuniform int uLook;\nuniform vec3 uOutBg;\nuniform vec3 uStops[5];\nuniform vec3 uStopDeltas[4];\nuniform int uStopCount;\nuniform vec3 uTint;\nuniform float uSaturation;\nuniform int uInvert;\nuniform float uIntensity;\nuniform vec2 uAvoidDim;\n#ifdef GLOW\nuniform sampler2D uGlow;\nuniform vec2 uGlowScale;\nuniform int uGlowOn;\n#endif\n\nin vec2 vPx;\nout vec4 fragColor;\n\n\n\n\nfloat lum(vec3 c) { return dot(c, vec3(0.299, 0.587, 0.114)); }\n\n\nvec3 ramp(vec3 pix) {\nfloat t = clamp((lum(pix) - lum(uBg)) / (0.9 - lum(uBg)), 0.0, 1.0);\nfloat x = t * float(uStopCount - 1);\n\n\nfloat fi = min(floor(x), float(uStopCount - 2));\n\n\n\n\nvec3 a = uStops[0], d = uStopDeltas[0];\nif (uStopCount > 2) {\nif (fi >= 1.0) { a = uStops[1]; d = uStopDeltas[1]; }\nif (uStopCount > 3) {\nif (fi >= 2.0) { a = uStops[2]; d = uStopDeltas[2]; }\nif (uStopCount > 4 && fi >= 3.0) { a = uStops[3]; d = uStopDeltas[3]; }\n}\n}\n\n\nreturn a + (x - fi) * d;\n}\n\n\n\nvec3 filtered(vec3 pix) {\nvec3 d = pix - uBg;\nfloat y = lum(d);\nvec3 chroma = (d - vec3(y)) * uSaturation;\n\n\n\n\nfloat tl = lum(uTint);\nif (uInvert == 1) return uOutBg - y * 1.25 * tl * (vec3(1.0 + tl) - uTint) + chroma * 0.9;\nreturn uOutBg + (vec3(y) + chroma) * uTint;\n}\n\nvoid main() {\n\n\nivec2 q = ivec2(int(vPx.x), uCanvasH - 1 - int(vPx.y)) - uOrigin;\n\n\nvec2 cf = floor((vec2(q) + 0.5) / vec2(uCellPx));\nivec2 cell = ivec2(cf);\n\n\nif (any(greaterThanEqual(uvec2(cell), uvec2(uGrid)))) {\nfragColor = vec4(uRaw == 1 ? uRawBg : uOutBg, 1.0);\nreturn;\n}\n\n\nivec2 local = ivec2(vec2(q) - cf * vec2(uCellPx));\nvec4 s0 = texelFetch(uScene0, cell, 0);\nif (uRaw == 1) {\nfragColor = vec4(mix(uRawBg, s0.rgb, s0.a), 1.0);\nreturn;\n}\nvec4 s1 = texelFetch(uScene1, cell, 0);\nint glyph = int(s1.r * 255.0 + 0.5);\nfloat ink = texelFetch(uAtlas, ivec2(int(float(glyph) * float(uCellPx.x)) + local.x, local.y), 0).r;\nvec3 bg = uBg;\n#ifdef GLOW\n\n\n\n\n\nif (uGlowOn == 1) bg += textureLod(uGlow, (vec2(q) + 0.5) * uGlowScale, 0.0).rgb;\n#endif\nvec3 c = mix(bg, s0.rgb, ink);\n\n\nif (uLook != 0 || uIntensity < 1.0) {\nif (uLook == 1) c = ramp(c);\nelse if (uLook == 2) c = clamp(filtered(c), 0.0, 1.0);\nif (uIntensity < 1.0) c = mix(uOutBg, c, uIntensity);\n}\n\n\n\n\nif (uAvoidDim != vec2(0.0) && s1.b > 0.0) {\nint mat = int(s1.g * 255.0 + 0.5);\nc = mix(uOutBg, c, 1.0 - (avoidWater(mat) ? uAvoidDim.x : uAvoidDim.y) * s1.b);\n}\nfragColor = vec4(c, 1.0);\n}\n";
|
|
2106
2335
|
var pixel_default = "\n\n\n\n\nuniform vec2 uDev;\nout vec2 vPx;\n\nvoid main() {\nvec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\ngl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\nvPx = p * uDev;\n}\n";
|
|
2107
2336
|
var glow_default = "\n\n\n\n\n\nuniform ivec2 uGrid;\nuniform float uAspect;\nuniform float uGain;\n\nout vec4 oGlow;\n\n\n\n#ifdef EMIT_TEX\n\n\n\nuniform sampler2D uEmit;\n#endif\n\nvoid main() {\nivec2 cell = ivec2(gl_FragCoord.xy);\nvec3 sum = vec3(0.0);\nfloat wsum = 0.0;\nfor (int y = -RY; y <= RY; y++) {\nfor (int x = -RX; x <= RX; x++) {\n\nvec2 d = vec2(float(x) * uAspect, float(y));\nfloat k = exp(-dot(d, d) * 0.6);\nwsum += k;\n#ifdef EMIT_TEX\nvec4 e = texelFetch(uEmit, cell + ivec2(x + RX, y + RY), 0);\nsum += e.rgb * e.a * k;\n#else\nivec2 n = cell + ivec2(x, y);\n\n\nivec2 nc = clamp(n, ivec2(0), uGrid - 1);\nvec4 e = emission(nc);\nif (all(equal(n, nc))) sum += e.rgb * e.a * k;\n#endif\n}\n}\n\n\nvec3 halo = sum * (uGain / wsum);\noGlow = vec4(halo / (1.0 + 2.5 * max(halo.r, max(halo.g, halo.b))), 1.0);\n}\n";
|
|
2108
2337
|
var emission_default$1 = "\n\n\n\nuniform sampler2D uScene0;\nuniform sampler2D uScene1;\n\n\n\n\n\n\nvec4 emission(ivec2 cell) {\nvec4 s0 = texelFetch(uScene0, cell, 0);\nint mat = int(texelFetch(uScene1, cell, 0).g * 255.0 + 0.5);\nfloat w = mat == M_FLY ? 1.3\n: mat == M_GLINT ? 1.0\n: mat == M_SUN ? 1.2\n: mat == M_EYE ? 0.7\n: mat == M_MOON ? 0.5\n: mat == M_STAR ? 0.35\n: mat == M_RIPPLE ? 0.45\n: mat == M_BODY || mat == M_EDGE || mat == M_FIN ? 0.4\n: 0.0;\nfloat lum = dot(s0.rgb, vec3(0.299, 0.587, 0.114));\nreturn vec4(s0.rgb, w * s0.a * smoothstep(0.3, 0.85, lum));\n}\n";
|
|
2109
2338
|
var emission_default = "\n\n\n\n\n\nout vec4 oEmit;\n\nvoid main() {\noEmit = emission(ivec2(gl_FragCoord.xy) - ivec2(RX, RY));\n}\n";
|
|
2110
2339
|
var fly_default$1 = "\n\n\n\nlayout(location = 0) in vec4 aFly;\n\nuniform vec2 uCell;\nuniform vec2 uOriginCss;\nuniform ivec2 uGrid;\nuniform float uFlies;\n\nout float vBright;\n\n\nconst float FLY_MIN = 0.22;\n\nvoid main() {\nfloat v = aFly.z * uFlies;\nvBright = v;\ngl_PointSize = 1.0;\n\nvec2 cell = floor((aFly.xy - uOriginCss) / uCell);\nvec2 ndc = (cell + 0.5) / vec2(uGrid) * 2.0 - 1.0;\n\ngl_Position = v < FLY_MIN ? vec4(2.0, 2.0, 0.0, 1.0) : vec4(ndc, 0.0, 1.0);\n}\n";
|
|
2111
|
-
var fly_default = "\n\n\n\n\nuniform vec3 uFlyCol;\nuniform vec3 uWaterDim;\nuniform float uRoles;\n\nin float vBright;\n\nlayout(location = 0) out vec4 oColor;\nlayout(location = 1) out vec4 oInfo;\n\nvoid main() {\nfloat v = min(vBright, 1.0);\nint glyph =
|
|
2340
|
+
var fly_default = "\n\n\n\n\n\n\n\nuniform vec3 uFlyCol;\nuniform vec3 uWaterDim;\nuniform float uRoles;\nuniform vec2 uCell;\nuniform vec2 uOriginCss;\n\nin float vBright;\n\nlayout(location = 0) out vec4 oColor;\nlayout(location = 1) out vec4 oInfo;\n\nvoid main() {\nfloat v = min(vBright, 1.0);\n\nfloat m = uAvoidN > 0 ? avoidMask(avoidDist((floor(gl_FragCoord.xy) + 0.5) * uCell + uOriginCss)) : 0.0;\n\nfloat vt = m > 0.0 ? v * avoidThin(m, M_FLY) : v;\nint glyph = vt > 0.7 ? G_STAR : vt > 0.45 ? G_PLUS : G_DOT;\nfloat k = min(0.45 + 0.6 * v, 1.0);\nvec3 col = uRoles > 0.5 ? mix(uWaterDim, uFlyCol, k) : uFlyCol * k;\noColor = vec4(col, vt);\noInfo = vec4(float(glyph) / 255.0, float(M_FLY) / 255.0, m, 0.0);\n}\n";
|
|
2112
2341
|
/** `file:line` for line n of a compiled shader, or just n before the first file. */
|
|
2113
2342
|
function sourceLine(files, n) {
|
|
2114
2343
|
let file;
|
|
@@ -2377,34 +2606,34 @@ function scale(a, k) {
|
|
|
2377
2606
|
* deep water and ripples come from water, highlight and background only (Q11).
|
|
2378
2607
|
*/
|
|
2379
2608
|
function rolesScene(r) {
|
|
2380
|
-
const ripple = mix(r.water, r.highlight, .6);
|
|
2609
|
+
const ripple = mix$1(r.water, r.highlight, .6);
|
|
2381
2610
|
return {
|
|
2382
|
-
uWaterDim: mix(r.background, r.water, .37),
|
|
2611
|
+
uWaterDim: mix$1(r.background, r.water, .37),
|
|
2383
2612
|
uWaterLit: r.water,
|
|
2384
2613
|
uRippleCol: ripple,
|
|
2385
2614
|
uKohakuWhite: r.pale,
|
|
2386
2615
|
uHiRed: r.accent,
|
|
2387
2616
|
uSumi: r.ink,
|
|
2388
|
-
uShowaWhite: mix(r.pale, r.background, .05),
|
|
2617
|
+
uShowaWhite: mix$1(r.pale, r.background, .05),
|
|
2389
2618
|
uOgonGold: r.metallic,
|
|
2390
2619
|
uOgonShade: scale(r.metallic, .8),
|
|
2391
|
-
uOgonGlint: mix(r.metallic, r.highlight, .6),
|
|
2620
|
+
uOgonGlint: mix$1(r.metallic, r.highlight, .6),
|
|
2392
2621
|
uEyeCol: r.highlight,
|
|
2393
|
-
uSumiRim: mix(r.ink, r.pale, .7),
|
|
2394
|
-
uKohakuFin: mix(r.pale, r.water, .15),
|
|
2395
|
-
uKohakuTail: mix(r.pale, r.water, .12),
|
|
2396
|
-
uOgonFin: mix(r.metallic, r.highlight, .25),
|
|
2397
|
-
uOgonTail: mix(r.metallic, r.highlight, .45),
|
|
2622
|
+
uSumiRim: mix$1(r.ink, r.pale, .7),
|
|
2623
|
+
uKohakuFin: mix$1(r.pale, r.water, .15),
|
|
2624
|
+
uKohakuTail: mix$1(r.pale, r.water, .12),
|
|
2625
|
+
uOgonFin: mix$1(r.metallic, r.highlight, .25),
|
|
2626
|
+
uOgonTail: mix$1(r.metallic, r.highlight, .45),
|
|
2398
2627
|
uSilk: r.pale,
|
|
2399
|
-
uBubble: mix(r.water, r.highlight, .6),
|
|
2400
|
-
uGoldDust: mix(r.metallic, r.highlight, .15),
|
|
2628
|
+
uBubble: mix$1(r.water, r.highlight, .6),
|
|
2629
|
+
uGoldDust: mix$1(r.metallic, r.highlight, .15),
|
|
2401
2630
|
uGlintCol: r.highlight,
|
|
2402
2631
|
uAddTint: r.highlight,
|
|
2403
2632
|
uStarCool: r.highlight,
|
|
2404
|
-
uStarWarm: mix(r.highlight, r.water, .25),
|
|
2405
|
-
uMoonCol: mix(r.highlight, r.pale, .25),
|
|
2633
|
+
uStarWarm: mix$1(r.highlight, r.water, .25),
|
|
2634
|
+
uMoonCol: mix$1(r.highlight, r.pale, .25),
|
|
2406
2635
|
uSunCol: r.highlight,
|
|
2407
|
-
uFlyCol: mix(r.metallic, r.highlight, .32)
|
|
2636
|
+
uFlyCol: mix$1(r.metallic, r.highlight, .32)
|
|
2408
2637
|
};
|
|
2409
2638
|
}
|
|
2410
2639
|
var luma = (c) => .2126 * c[0] + .7152 * c[1] + .0722 * c[2];
|
|
@@ -2578,6 +2807,8 @@ function starsAt(phase, sky, light) {
|
|
|
2578
2807
|
function firefliesAt(phase, sky) {
|
|
2579
2808
|
return sky.fireflies ? fireflyWeight(phase) : 0;
|
|
2580
2809
|
}
|
|
2810
|
+
/** Stands in for mount()'s zone arrays until the first setAvoid. */
|
|
2811
|
+
var NO_ZONES = /* @__PURE__ */ new Float32Array(0);
|
|
2581
2812
|
/** World px per ripple simulation texel. */
|
|
2582
2813
|
var RIPPLE_PX = 5;
|
|
2583
2814
|
var RIPPLE_HZ = 60;
|
|
@@ -2622,6 +2853,8 @@ var Renderer = class {
|
|
|
2622
2853
|
intensity = 1;
|
|
2623
2854
|
/** Night-sky features, already resolved; mount() owns them like the palette. */
|
|
2624
2855
|
sky = DEFAULT_SKY;
|
|
2856
|
+
/** Debug switch: outlines the readability zones (in the palette's hi red) and draws them even at strength 0. */
|
|
2857
|
+
showAvoid = false;
|
|
2625
2858
|
gl;
|
|
2626
2859
|
fontFamily;
|
|
2627
2860
|
vao;
|
|
@@ -2642,6 +2875,18 @@ var Renderer = class {
|
|
|
2642
2875
|
dropBuf = /* @__PURE__ */ new Float32Array(128);
|
|
2643
2876
|
stopBuf = /* @__PURE__ */ new Float32Array(15);
|
|
2644
2877
|
stopDeltaBuf = /* @__PURE__ */ new Float32Array(12);
|
|
2878
|
+
/**
|
|
2879
|
+
* Readability zones for the next frames, as mount() last measured them: x,
|
|
2880
|
+
* y, w, h and a radius each, world px. mount()'s arrays (setAvoid); never
|
|
2881
|
+
* read while avoidCount is 0.
|
|
2882
|
+
*/
|
|
2883
|
+
avoidRects = NO_ZONES;
|
|
2884
|
+
avoidRadii = NO_ZONES;
|
|
2885
|
+
avoidCount = 0;
|
|
2886
|
+
/** The readability fade, already resolved; mount() owns it like the palette. */
|
|
2887
|
+
avoidFade = DEFAULT_FADE;
|
|
2888
|
+
/** What avoidFade's strength does to each material (worked out once per setAvoidFade, not per frame). */
|
|
2889
|
+
avoidAmounts = avoidAmounts(DEFAULT_FADE.strength);
|
|
2645
2890
|
/** Scene colours for the active palette (roles replace the pond's). */
|
|
2646
2891
|
sceneColors = POND_SCENE;
|
|
2647
2892
|
uploadedColors = null;
|
|
@@ -2726,7 +2971,7 @@ var Renderer = class {
|
|
|
2726
2971
|
const glow = emission ? `#define EMIT_TEX\n${glow_default}` : `${emission_default$1}\n${glow_default}`;
|
|
2727
2972
|
this.glowProg = software ? null : new Program(gl, vertex, fragment(glow), "glow");
|
|
2728
2973
|
this.asciiProg = new Program(gl, pixelVertex, fragment((software ? "" : "#define GLOW\n") + ascii_default), "ascii");
|
|
2729
|
-
this.flyProg = new Program(gl, `#version 300 es\n${common_default}\n${fly_default$1}`, fragment(fly_default), "fly");
|
|
2974
|
+
this.flyProg = new Program(gl, `#version 300 es\n${common_default}\n${fly_default$1}`, fragment(`${avoid_default}\n${fly_default}`), "fly");
|
|
2730
2975
|
const vao = gl.createVertexArray();
|
|
2731
2976
|
const flyVao = gl.createVertexArray();
|
|
2732
2977
|
const flyBuf = gl.createBuffer();
|
|
@@ -2768,6 +3013,28 @@ var Renderer = class {
|
|
|
2768
3013
|
setSky(sky) {
|
|
2769
3014
|
this.sky = sky;
|
|
2770
3015
|
}
|
|
3016
|
+
/**
|
|
3017
|
+
* Sets the readability zones the next render() fades under: `count` zones,
|
|
3018
|
+
* `rects` x, y, w, h and `radii` one per zone, in world px. The arrays are
|
|
3019
|
+
* kept, not copied (mount() refills them before each draw).
|
|
3020
|
+
*/
|
|
3021
|
+
setAvoid(rects, radii, count) {
|
|
3022
|
+
this.avoidRects = rects;
|
|
3023
|
+
this.avoidRadii = radii;
|
|
3024
|
+
this.avoidCount = count;
|
|
3025
|
+
}
|
|
3026
|
+
/**
|
|
3027
|
+
* Whether render() draws zones at all: not in the raw view (A14), nor at
|
|
3028
|
+
* strength 0 unless outlined. mount() skips measuring when it doesn't.
|
|
3029
|
+
*/
|
|
3030
|
+
get drawsAvoid() {
|
|
3031
|
+
return !this.raw && (this.avoidFade.strength > 0 || this.showAvoid);
|
|
3032
|
+
}
|
|
3033
|
+
/** Sets the readability fade; the next render() draws with it. */
|
|
3034
|
+
setAvoidFade(fade) {
|
|
3035
|
+
this.avoidFade = fade;
|
|
3036
|
+
this.avoidAmounts = avoidAmounts(fade.strength);
|
|
3037
|
+
}
|
|
2771
3038
|
/** Sizes everything for a drawing buffer of devW × devH device pixels. */
|
|
2772
3039
|
resize(devW, devH, dpr) {
|
|
2773
3040
|
const gl = this.gl;
|
|
@@ -2850,6 +3117,8 @@ var Renderer = class {
|
|
|
2850
3117
|
const sceneLight = this.sceneLight;
|
|
2851
3118
|
const stars = this.stars;
|
|
2852
3119
|
const flies = this.flies;
|
|
3120
|
+
const amounts = this.avoidAmounts;
|
|
3121
|
+
const zones = this.drawsAvoid ? this.avoidCount : 0;
|
|
2853
3122
|
const gl = this.gl;
|
|
2854
3123
|
const atlas = this.atlas;
|
|
2855
3124
|
const scene = this.scene;
|
|
@@ -2887,7 +3156,8 @@ var Renderer = class {
|
|
|
2887
3156
|
for (const [name, c] of Object.entries(colors)) sp.vec3(name, c[0], c[1], c[2]);
|
|
2888
3157
|
this.uploadedColors = colors;
|
|
2889
3158
|
}
|
|
2890
|
-
sp.float("uRoles", roles ? 1 : 0).texture("uFish", 0, this.fishTex).texture("uRipple", 1, ripple[0].textures[0]).ivec2("uRippleSize", this.simW, this.simH).float("uRipplePx", RIPPLE_PX).int("uFishCount", count).ints("uOrder", pond.order).vec2("uCell", atlas.cellW / this.dpr, atlas.cellH / this.dpr).vec2("uOriginCss", this.originX / this.dpr, this.originY / this.dpr).float("uTime", pond.time % 65536).vec3("uLightDir", light.dir[0], light.dir[1], light.dir[2]).vec3("uLightCol", sceneLight.color[0], sceneLight.color[1], sceneLight.color[2]).vec3("uAmbient", sceneLight.ambient[0], sceneLight.ambient[1], sceneLight.ambient[2]).vec3("uWaterTint", sceneLight.water[0], sceneLight.water[1], sceneLight.water[2]).vec3("uSky", sceneLight.sky[0], sceneLight.sky[1], sceneLight.sky[2]).float("uGlint", light.glint).float("uCaustic", light.caustic).float("uFloor", light.floor).float("uDarkWater", light.darkWater).float("uDarkCaustic", light.darkCaustic).vec2("uShadow", unit * light.shadow[0], unit * light.shadow[1]).float("uShadowAlpha", light.shadowAlpha).float("uCalm", pond.calm ? 1 : 0).float("uStars", stars.stars).float("uMoteThin", stars.moteThin).vec2("uView", this.devW / this.dpr, this.devH / this.dpr).vec4("uMoon", light.moon.pos[0], light.moon.pos[1], light.moon.radius, this.sky.moon ? light.moon.alpha : 0).vec4("uSun", light.sun.pos[0], light.sun.pos[1], light.sun.radius, this.sky.sun ? light.sun.alpha : 0);
|
|
3159
|
+
sp.float("uRoles", roles ? 1 : 0).texture("uFish", 0, this.fishTex).texture("uRipple", 1, ripple[0].textures[0]).ivec2("uRippleSize", this.simW, this.simH).float("uRipplePx", RIPPLE_PX).int("uFishCount", count).ints("uOrder", pond.order).vec2("uCell", atlas.cellW / this.dpr, atlas.cellH / this.dpr).vec2("uOriginCss", this.originX / this.dpr, this.originY / this.dpr).float("uTime", pond.time % 65536).vec3("uLightDir", light.dir[0], light.dir[1], light.dir[2]).vec3("uLightCol", sceneLight.color[0], sceneLight.color[1], sceneLight.color[2]).vec3("uAmbient", sceneLight.ambient[0], sceneLight.ambient[1], sceneLight.ambient[2]).vec3("uWaterTint", sceneLight.water[0], sceneLight.water[1], sceneLight.water[2]).vec3("uSky", sceneLight.sky[0], sceneLight.sky[1], sceneLight.sky[2]).float("uGlint", light.glint).float("uCaustic", light.caustic).float("uFloor", light.floor).float("uDarkWater", light.darkWater).float("uDarkCaustic", light.darkCaustic).vec2("uShadow", unit * light.shadow[0], unit * light.shadow[1]).float("uShadowAlpha", light.shadowAlpha).float("uCalm", pond.calm ? 1 : 0).float("uStars", stars.stars).float("uMoteThin", stars.moteThin).vec2("uView", this.devW / this.dpr, this.devH / this.dpr).vec4("uMoon", light.moon.pos[0], light.moon.pos[1], light.moon.radius, this.sky.moon ? light.moon.alpha : 0).vec4("uSun", light.sun.pos[0], light.sun.pos[1], light.sun.radius, this.sky.sun ? light.sun.alpha : 0).int("uAvoidN", zones);
|
|
3160
|
+
if (zones > 0) this.avoidUniforms(sp, amounts);
|
|
2891
3161
|
sp.int("uLayer", 0).int("uFishDraw", 0);
|
|
2892
3162
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
2893
3163
|
if (count > 0) {
|
|
@@ -2904,7 +3174,8 @@ var Renderer = class {
|
|
|
2904
3174
|
for (const [name, c] of Object.entries(colors)) fp.vec3(name, c[0], c[1], c[2]);
|
|
2905
3175
|
this.flyColors = colors;
|
|
2906
3176
|
}
|
|
2907
|
-
fp.float("uRoles", roles ? 1 : 0).vec2("uCell", atlas.cellW / this.dpr, atlas.cellH / this.dpr).vec2("uOriginCss", this.originX / this.dpr, this.originY / this.dpr).ivec2("uGrid", this.cols, this.rows).float("uFlies", flies);
|
|
3177
|
+
fp.float("uRoles", roles ? 1 : 0).vec2("uCell", atlas.cellW / this.dpr, atlas.cellH / this.dpr).vec2("uOriginCss", this.originX / this.dpr, this.originY / this.dpr).ivec2("uGrid", this.cols, this.rows).float("uFlies", flies).int("uAvoidN", zones);
|
|
3178
|
+
if (zones > 0) this.avoidUniforms(fp, amounts);
|
|
2908
3179
|
gl.drawArrays(gl.POINTS, 0, 14);
|
|
2909
3180
|
gl.bindVertexArray(this.vao);
|
|
2910
3181
|
}
|
|
@@ -2939,10 +3210,14 @@ var Renderer = class {
|
|
|
2939
3210
|
const look = this.look;
|
|
2940
3211
|
const filter = palette.mode === "filter" ? palette : null;
|
|
2941
3212
|
const tint = filter?.tint ?? WHITE;
|
|
2942
|
-
const ascii = this.asciiProg.use().texture("uScene0", 0, scene.textures[0]).texture("uScene1", 1, scene.textures[1]).texture("uAtlas", 2, atlas.texture).vec2("uDev", this.devW, this.devH).ivec2("uCellPx", atlas.cellW, atlas.cellH).ivec2("uOrigin", this.originX, this.originY).ivec2("uGrid", this.cols, this.rows).int("uCanvasH", this.devH).vec3("uBg", look.bg[0], look.bg[1], look.bg[2]).vec3("uRawBg", POND_BG[0], POND_BG[1], POND_BG[2]).int("uRaw", this.raw ? 1 : 0).int("uLook", look.look).vec3("uOutBg", look.outBg[0], look.outBg[1], look.outBg[2]).vec3s("uStops", this.stopBuf).vec3s("uStopDeltas", this.stopDeltaBuf).int("uStopCount", palette.mode === "ramp" ? palette.stops.length : 2).vec3("uTint", tint[0], tint[1], tint[2]).float("uSaturation", filter?.saturation ?? 1).int("uInvert", filter?.invert ? 1 : 0).float("uIntensity", this.raw ? 1 : this.intensity);
|
|
3213
|
+
const ascii = this.asciiProg.use().texture("uScene0", 0, scene.textures[0]).texture("uScene1", 1, scene.textures[1]).texture("uAtlas", 2, atlas.texture).vec2("uDev", this.devW, this.devH).ivec2("uCellPx", atlas.cellW, atlas.cellH).ivec2("uOrigin", this.originX, this.originY).ivec2("uGrid", this.cols, this.rows).int("uCanvasH", this.devH).vec3("uBg", look.bg[0], look.bg[1], look.bg[2]).vec3("uRawBg", POND_BG[0], POND_BG[1], POND_BG[2]).int("uRaw", this.raw ? 1 : 0).int("uLook", look.look).vec3("uOutBg", look.outBg[0], look.outBg[1], look.outBg[2]).vec3s("uStops", this.stopBuf).vec3s("uStopDeltas", this.stopDeltaBuf).int("uStopCount", palette.mode === "ramp" ? palette.stops.length : 2).vec3("uTint", tint[0], tint[1], tint[2]).float("uSaturation", filter?.saturation ?? 1).int("uInvert", filter?.invert ? 1 : 0).float("uIntensity", this.raw ? 1 : this.intensity).vec2("uAvoidDim", zones > 0 ? amounts.waterDim : 0, zones > 0 ? amounts.fishDim : 0);
|
|
2943
3214
|
if (this.glowTarget) ascii.texture("uGlow", 3, this.glowTarget.textures[0]).vec2("uGlowScale", 1 / (this.cols * atlas.cellW), 1 / (this.rows * atlas.cellH)).int("uGlowOn", haloDrawn ? 1 : 0);
|
|
2944
3215
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
2945
3216
|
}
|
|
3217
|
+
/** The zones and their thinning, for the scene and firefly programs (scene/avoid.glsl). */
|
|
3218
|
+
avoidUniforms(prog, amounts) {
|
|
3219
|
+
prog.vec4s("uAvoid", this.avoidRects).vec4s("uAvoidR", this.avoidRadii).vec4("uAvoidFade", amounts.waterThin, amounts.fishThin, this.avoidFade.feather, this.showAvoid ? 1 : 0);
|
|
3220
|
+
}
|
|
2946
3221
|
/** Moves up to MAX_DROPS pending impulses into the uniform buffer, in sim texels. */
|
|
2947
3222
|
takeDrops(pond) {
|
|
2948
3223
|
const drops = pond.drops;
|
|
@@ -2980,15 +3255,19 @@ function mount(container, options) {
|
|
|
2980
3255
|
const interactive = options.interactive === "canvas" || options.interactive === "none" ? options.interactive : "window";
|
|
2981
3256
|
const seed = typeof options.seed === "number" && Number.isFinite(options.seed) ? options.seed >>> 0 : Math.random() * 2 ** 32 >>> 0;
|
|
2982
3257
|
const sky = new Sky(timeSource(options.time, 0));
|
|
3258
|
+
const { background, onError } = options;
|
|
2983
3259
|
let palette = resolvePalette(options.palette, parseCssColor);
|
|
2984
3260
|
let intensity = resolveIntensity(options.intensity, 1, true);
|
|
2985
3261
|
let skyFeatures = resolveSky(options.sky);
|
|
3262
|
+
const zones = new AvoidZones();
|
|
3263
|
+
zones.set(options.avoid, false);
|
|
3264
|
+
let avoidFade = resolveAvoidFade(options.avoidFade);
|
|
2986
3265
|
let canvas = document.createElement("canvas");
|
|
2987
3266
|
canvas.setAttribute("aria-hidden", "true");
|
|
2988
3267
|
canvas.style.display = "block";
|
|
2989
3268
|
canvas.style.width = "100%";
|
|
2990
3269
|
canvas.style.height = "100%";
|
|
2991
|
-
canvas.style.backgroundColor =
|
|
3270
|
+
canvas.style.backgroundColor = background || palette.css;
|
|
2992
3271
|
if (interactive === "canvas") canvas.style.touchAction = "none";
|
|
2993
3272
|
container.append(canvas);
|
|
2994
3273
|
const listeners = new AbortController();
|
|
@@ -3020,6 +3299,7 @@ function mount(container, options) {
|
|
|
3020
3299
|
cancelAnimationFrame(raf);
|
|
3021
3300
|
raf = 0;
|
|
3022
3301
|
renderer = null;
|
|
3302
|
+
zones.clear();
|
|
3023
3303
|
if (gl && !gl.isContextLost()) gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
3024
3304
|
};
|
|
3025
3305
|
const fail = (code, message, cause) => {
|
|
@@ -3032,12 +3312,12 @@ function mount(container, options) {
|
|
|
3032
3312
|
canvas = blank;
|
|
3033
3313
|
}
|
|
3034
3314
|
const error = koiPondError(code, message, cause);
|
|
3035
|
-
if (!
|
|
3315
|
+
if (!onError) {
|
|
3036
3316
|
console.error(error);
|
|
3037
3317
|
return;
|
|
3038
3318
|
}
|
|
3039
3319
|
try {
|
|
3040
|
-
|
|
3320
|
+
onError(error);
|
|
3041
3321
|
} catch (err) {
|
|
3042
3322
|
console.error(err);
|
|
3043
3323
|
}
|
|
@@ -3049,7 +3329,7 @@ function mount(container, options) {
|
|
|
3049
3329
|
setPalette(next) {
|
|
3050
3330
|
if (destroyed) return;
|
|
3051
3331
|
palette = resolvePalette(next, parseCssColor);
|
|
3052
|
-
if (!
|
|
3332
|
+
if (!background) canvas.style.backgroundColor = palette.css;
|
|
3053
3333
|
renderer?.setLook(palette, intensity);
|
|
3054
3334
|
},
|
|
3055
3335
|
setIntensity(next) {
|
|
@@ -3062,6 +3342,15 @@ function mount(container, options) {
|
|
|
3062
3342
|
skyFeatures = resolveSky(next);
|
|
3063
3343
|
renderer?.setSky(skyFeatures);
|
|
3064
3344
|
},
|
|
3345
|
+
setAvoid(list) {
|
|
3346
|
+
if (destroyed || failed) return;
|
|
3347
|
+
zones.set(list);
|
|
3348
|
+
},
|
|
3349
|
+
setAvoidFade(next) {
|
|
3350
|
+
if (destroyed || failed) return;
|
|
3351
|
+
avoidFade = resolveAvoidFade(next, avoidFade);
|
|
3352
|
+
renderer?.setAvoidFade(avoidFade);
|
|
3353
|
+
},
|
|
3065
3354
|
pause() {
|
|
3066
3355
|
paused = true;
|
|
3067
3356
|
schedule();
|
|
@@ -3144,6 +3433,7 @@ function mount(container, options) {
|
|
|
3144
3433
|
renderer = new Renderer(ctx, fontFamily, software);
|
|
3145
3434
|
renderer.setLook(palette, intensity);
|
|
3146
3435
|
renderer.setSky(skyFeatures);
|
|
3436
|
+
renderer.setAvoidFade(avoidFade);
|
|
3147
3437
|
sizeDirty = true;
|
|
3148
3438
|
return true;
|
|
3149
3439
|
} catch (err) {
|
|
@@ -3256,6 +3546,7 @@ function mount(container, options) {
|
|
|
3256
3546
|
}
|
|
3257
3547
|
pond.resize(size.w / size.dpr, size.h / size.dpr);
|
|
3258
3548
|
r.resize(bw, bh, size.dpr * bw / size.w);
|
|
3549
|
+
zones.readRadii();
|
|
3259
3550
|
};
|
|
3260
3551
|
/** Applies a pending resize; false while there is nothing to draw into. */
|
|
3261
3552
|
const prepare = () => {
|
|
@@ -3268,8 +3559,26 @@ function mount(container, options) {
|
|
|
3268
3559
|
sizeDirty = false;
|
|
3269
3560
|
applySize(renderer);
|
|
3270
3561
|
}
|
|
3562
|
+
measureZones(renderer);
|
|
3271
3563
|
return renderer;
|
|
3272
3564
|
};
|
|
3565
|
+
/**
|
|
3566
|
+
* Measures the readability zones for the draw about to happen. On every
|
|
3567
|
+
* drawn frame and manual draw (D8), never while the loop is stopped: the
|
|
3568
|
+
* page's elements move without telling anyone (scroll, transforms,
|
|
3569
|
+
* animations), so a fresh read is the only way to be right. No reads
|
|
3570
|
+
* without zones, or when the renderer won't draw them (strength 0 without
|
|
3571
|
+
* the debug outline, the raw view); the next frame that draws them
|
|
3572
|
+
* measures afresh.
|
|
3573
|
+
*/
|
|
3574
|
+
const measureZones = (r) => {
|
|
3575
|
+
let count = 0;
|
|
3576
|
+
if (zones.size > 0 && r.drawsAvoid) {
|
|
3577
|
+
const c = canvas.getBoundingClientRect();
|
|
3578
|
+
count = zones.measure(c, window.innerWidth, window.innerHeight, avoidFade.feather);
|
|
3579
|
+
}
|
|
3580
|
+
r.setAvoid(zones.rects, zones.radii, count);
|
|
3581
|
+
};
|
|
3273
3582
|
/** Simulates `dt` seconds and draws; shared by the frame loop and step(). */
|
|
3274
3583
|
const advance = (r, dt) => {
|
|
3275
3584
|
pond.calm = reduceMotion.matches;
|