koi-ascii 0.3.1 → 0.3.2
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 +1 -1
- package/dist/index.js +396 -163
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -463,7 +463,7 @@ console.log(await page.evaluate(() => koi.help()));
|
|
|
463
463
|
| `setTime` | `mode?`, `phase?`, `cycle?`, `hold?` | Time of day; `hold` keeps the phase until a call with `hold: false`. |
|
|
464
464
|
| `setDayPart` | `part` | Jumps to a lighting keyframe (`night`, `blue-hour-am`, … `noon` …). |
|
|
465
465
|
| `getPerf` | | fps, 1% low, frame / CPU / GPU ms (GPU `null` without the timer extension), canvas size, GPU string, software flag, seed. |
|
|
466
|
-
| `profile` | `frames?` (400) | GPU µs per pass (`scene`, `glow`, `ascii`, `fly`; `frame` is all of them), median and p90, from timer queries around each draw over `frames` redraws of the paused frame. `{ gpuTimer: false }` without the timer extension. |
|
|
466
|
+
| `profile` | `frames?` (400) | GPU µs per pass (`scene`, `emission`, `glow`, `ascii`, `fly`; `frame` is all of them), median and p90, from timer queries around each draw over `frames` redraws of the paused frame. `{ gpuTimer: false }` without the timer extension. |
|
|
467
467
|
| `setRender` | `view?`, `glow?` | `final` / `raw` view and the glow halo. The raw view ignores the palette and intensity. |
|
|
468
468
|
| `setPalette` | `palette` | Switches to a preset (`pond`, `mono`, `phosphor`, `amber`, `paper`, `brand`). |
|
|
469
469
|
| `setIntensity` | `intensity` | 0 background only … 1 full pond. |
|
package/dist/index.js
CHANGED
|
@@ -304,30 +304,33 @@ var mix3 = (a, b, t) => [
|
|
|
304
304
|
mix$1(a[2], b[2], t)
|
|
305
305
|
];
|
|
306
306
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
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
309
|
*/
|
|
310
|
-
|
|
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) {
|
|
311
318
|
const el = mix$1(lo, hi, Math.max(0, Math.sin(angle)));
|
|
312
|
-
|
|
313
|
-
const h = Math.cos(el) /
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
Math.sin(el)
|
|
318
|
-
];
|
|
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);
|
|
319
324
|
}
|
|
320
|
-
/** A body's azimuth on screen at `angle` (see arcDir), tilted; its length is the same before and after. */
|
|
325
|
+
/** A body's azimuth on screen at `angle` (see arcDir), tilted, into AZ; its length is the same before and after. */
|
|
321
326
|
function azimuth(angle) {
|
|
322
327
|
const ax = -Math.cos(angle);
|
|
323
328
|
const ay = -.8 * Math.sin(angle);
|
|
324
329
|
const c = Math.cos(PATH_TILT);
|
|
325
330
|
const s = Math.sin(PATH_TILT);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
length: Math.hypot(ax, ay) || 1
|
|
330
|
-
};
|
|
331
|
+
AZ[0] = ax * c - ay * s;
|
|
332
|
+
AZ[1] = ax * s + ay * c;
|
|
333
|
+
AZ[2] = Math.hypot(ax, ay) || 1;
|
|
331
334
|
}
|
|
332
335
|
/** Half-extents of the path reflections follow, as a share of the pond's width and height. */
|
|
333
336
|
var BODY_BAND = [.38, .36];
|
|
@@ -358,7 +361,9 @@ var SUN_PACE = 2 * HANDOFF[0] / (1 - 2 * HANDOFF[1]);
|
|
|
358
361
|
* keeps clear of the middle 40 % of the pond at any aspect (P1).
|
|
359
362
|
*/
|
|
360
363
|
function bodyPos(angle) {
|
|
361
|
-
|
|
364
|
+
azimuth(angle);
|
|
365
|
+
const x = AZ[0];
|
|
366
|
+
const y = AZ[1];
|
|
362
367
|
const k = ((Math.abs(x) / BODY_BAND[0]) ** BODY_ROUND + (Math.abs(y) / BODY_BAND[1]) ** BODY_ROUND) ** (-1 / BODY_ROUND);
|
|
363
368
|
return [.5 + x * k, .5 + y * k];
|
|
364
369
|
}
|
|
@@ -438,19 +443,28 @@ function lightingAt(phase, options = {}) {
|
|
|
438
443
|
const a = KEYS[i];
|
|
439
444
|
const b = KEYS[i + 1];
|
|
440
445
|
const t = smooth(a.at, b.at, p);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
446
|
+
arcDir(TAU$1 * (p - .25), SUN_LOW, SUN_HIGH, SUN_D);
|
|
447
|
+
arcDir(TAU$1 * (p - .75), MOON_LOW, MOON_HIGH, MOON_D);
|
|
448
|
+
const w = smooth(HANDOFF[0], HANDOFF[1], p) * (1 - smooth(1 - HANDOFF[1], 1 - HANDOFF[0], p));
|
|
449
|
+
const d0 = mix$1(MOON_D[0], SUN_D[0], w);
|
|
450
|
+
const d1 = mix$1(MOON_D[1], SUN_D[1], w);
|
|
451
|
+
const d2 = mix$1(MOON_D[2], SUN_D[2], w);
|
|
452
|
+
const dl = Math.hypot(d0, d1, d2);
|
|
444
453
|
const dir = [
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
454
|
+
d0 / dl,
|
|
455
|
+
d1 / dl,
|
|
456
|
+
d2 / dl
|
|
448
457
|
];
|
|
449
458
|
const horiz = Math.hypot(dir[0], dir[1]);
|
|
450
459
|
const len = Math.min(SHADOW_MAX, SHADOW_PER_COT * horiz / Math.max(dir[2], .001));
|
|
451
460
|
const shadow = horiz > 1e-6 ? [-dir[0] / horiz * len, -dir[1] / horiz * len] : [0, 0];
|
|
452
461
|
const floor = mix$1(a.floor, b.floor, t);
|
|
453
|
-
const water =
|
|
462
|
+
const water = [
|
|
463
|
+
Math.max(mix$1(a.water[0], b.water[0], t), floor),
|
|
464
|
+
Math.max(mix$1(a.water[1], b.water[1], t), floor),
|
|
465
|
+
Math.max(mix$1(a.water[2], b.water[2], t), floor)
|
|
466
|
+
];
|
|
467
|
+
const night = options.darkNight ? nightWeight(p) : 0;
|
|
454
468
|
return {
|
|
455
469
|
dir,
|
|
456
470
|
color: mix3(a.color, b.color, t),
|
|
@@ -463,21 +477,12 @@ function lightingAt(phase, options = {}) {
|
|
|
463
477
|
shadow,
|
|
464
478
|
shadowAlpha: mix$1(a.shadowAlpha, b.shadowAlpha, t),
|
|
465
479
|
glowStrength: mix$1(a.glow, b.glow, t),
|
|
466
|
-
|
|
480
|
+
darkWater: night <= 0 ? 1 : mix$1(1, DARK_WATER, night),
|
|
481
|
+
darkCaustic: night <= 0 ? 1 : mix$1(1, DARK_CAUSTIC, night),
|
|
467
482
|
moon: moonAt(p),
|
|
468
483
|
sun: sunAt(p)
|
|
469
484
|
};
|
|
470
485
|
}
|
|
471
|
-
function darkNight(night) {
|
|
472
|
-
if (night <= 0) return {
|
|
473
|
-
darkWater: 1,
|
|
474
|
-
darkCaustic: 1
|
|
475
|
-
};
|
|
476
|
-
return {
|
|
477
|
-
darkWater: mix$1(1, DARK_WATER, night),
|
|
478
|
-
darkCaustic: mix$1(1, DARK_CAUSTIC, night)
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
486
|
/** Wraps any number into [0, 1). */
|
|
482
487
|
function wrapPhase(p) {
|
|
483
488
|
return p - Math.floor(p);
|
|
@@ -1024,6 +1029,8 @@ function layoutDefines() {
|
|
|
1024
1029
|
J_TAIL: 6,
|
|
1025
1030
|
J_TOTAL: 22,
|
|
1026
1031
|
TRAIL_N: 40,
|
|
1032
|
+
TRAIL_CHUNK: 8,
|
|
1033
|
+
TRAIL_CHUNKS: 5,
|
|
1027
1034
|
T_PEC_L: 22,
|
|
1028
1035
|
T_PEC_R: 23,
|
|
1029
1036
|
T_FIN: 24,
|
|
@@ -1033,6 +1040,7 @@ function layoutDefines() {
|
|
|
1033
1040
|
T_TBOX: 28,
|
|
1034
1041
|
T_P1: 29,
|
|
1035
1042
|
T_TRAIL: 30,
|
|
1043
|
+
T_CBOX: 70,
|
|
1036
1044
|
MAX_DROPS: 32
|
|
1037
1045
|
}).map(([k, v]) => `#define ${k} ${v}\n`).join("");
|
|
1038
1046
|
}
|
|
@@ -1061,24 +1069,56 @@ function profile(u) {
|
|
|
1061
1069
|
}
|
|
1062
1070
|
return PROFILE[PROFILE.length - 1][1];
|
|
1063
1071
|
}
|
|
1072
|
+
/** profile() at each body joint, u = i / (J_BODY - 1); the same for every fish and frame. */
|
|
1073
|
+
var JOINT_PROFILE = Float64Array.from({ length: 16 }, (_, i) => profile(i / 15));
|
|
1074
|
+
var PEDUNCLE_PROFILE = profile(1);
|
|
1064
1075
|
var clamp = (x, lo, hi) => x < lo ? lo : x > hi ? hi : x;
|
|
1065
1076
|
var lerp = (a, b, t) => a + (b - a) * t;
|
|
1066
1077
|
function smoothstep(e0, e1, x) {
|
|
1067
1078
|
const t = clamp((x - e0) / (e1 - e0), 0, 1);
|
|
1068
1079
|
return t * t * (3 - 2 * t);
|
|
1069
1080
|
}
|
|
1081
|
+
/** How far tail joint J_BODY + n has spread from the peduncle width to the full tail width. */
|
|
1082
|
+
var TAIL_SPREAD = Float64Array.from({ length: 6 }, (_, n) => Math.pow(smoothstep(0, .85, (n + 1) / 6), .75));
|
|
1070
1083
|
function wrapAngle(a) {
|
|
1071
1084
|
a = (a + Math.PI) % TAU;
|
|
1072
1085
|
return (a < 0 ? a + TAU : a) - Math.PI;
|
|
1073
1086
|
}
|
|
1074
1087
|
/** Frame-rate independent smoothing factor. */
|
|
1075
1088
|
var ease = (rate, dt) => 1 - Math.exp(-rate * dt);
|
|
1089
|
+
/**
|
|
1090
|
+
* Math.hypot(x, y) to the bit, but inlinable: V8 never inlines its builtin, so
|
|
1091
|
+
* each call boxes both arguments and the result as heap numbers. For two
|
|
1092
|
+
* arguments the builtin's normalised Kahan sum comes down to this. The NaN case
|
|
1093
|
+
* returns ax + ay, not the global NaN: that load never runs, so TurboFan leaves
|
|
1094
|
+
* it untyped and would box every inlined call's result.
|
|
1095
|
+
*/
|
|
1096
|
+
function hypot(x, y) {
|
|
1097
|
+
const ax = Math.abs(x);
|
|
1098
|
+
const ay = Math.abs(y);
|
|
1099
|
+
if (ax === Infinity || ay === Infinity) return Infinity;
|
|
1100
|
+
if (ax !== ax || ay !== ay) return ax + ay;
|
|
1101
|
+
const hi = ax > ay ? ax : ay;
|
|
1102
|
+
if (hi === 0) return 0;
|
|
1103
|
+
const q = (ax > ay ? ay : ax) / hi;
|
|
1104
|
+
return Math.sqrt(1 + q * q) * hi;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Age at which writeTrail ends a trail. Ages only grow along it, so a segment
|
|
1108
|
+
* cut off joins samples at least this old, where trails.glsl's I is at most
|
|
1109
|
+
* 0.15^1.3 < 0.085; even times the largest variety factor, 1.3 (gnoise stays
|
|
1110
|
+
* within [-1, 1]), that is under the 0.12 a trail cell needs. So the strongest
|
|
1111
|
+
* segment of any cell drawn is kept, and is still the first that strong.
|
|
1112
|
+
*/
|
|
1113
|
+
var TRAIL_CUT = .85;
|
|
1076
1114
|
var Koi = class {
|
|
1077
1115
|
variety;
|
|
1078
1116
|
/** Small offset into pattern noise space; gives each fish its own markings. */
|
|
1079
1117
|
patternSeed;
|
|
1080
1118
|
rng;
|
|
1081
1119
|
noiseSeed;
|
|
1120
|
+
/** cos and sin of +maxBend, then of -maxBend: body joints from 0, tail joints from 4. */
|
|
1121
|
+
bendTrig;
|
|
1082
1122
|
length = 1;
|
|
1083
1123
|
x = 0;
|
|
1084
1124
|
y = 0;
|
|
@@ -1106,6 +1146,8 @@ var Koi = class {
|
|
|
1106
1146
|
nextRipple = 0;
|
|
1107
1147
|
nextGulp = 0;
|
|
1108
1148
|
posed = false;
|
|
1149
|
+
/** sampleBody's result: x, y, radius, then the backward axis bx, by. Each call overwrites it. */
|
|
1150
|
+
sample = /* @__PURE__ */ new Float64Array(5);
|
|
1109
1151
|
/** Spine centreline that follows the head's path: x, y per joint. */
|
|
1110
1152
|
spine = /* @__PURE__ */ new Float32Array(44);
|
|
1111
1153
|
/** Joints with the swimming wave applied: x, y, radius. */
|
|
@@ -1119,8 +1161,10 @@ var Koi = class {
|
|
|
1119
1161
|
eyeRadius = 1;
|
|
1120
1162
|
/** Bounds of body and fins: min.xy, max.xy. */
|
|
1121
1163
|
bbox = /* @__PURE__ */ new Float32Array(4);
|
|
1122
|
-
/** Bounds of the trail written by the last writeTrail call. */
|
|
1164
|
+
/** Bounds of the trail written by the last writeTrail call: the union of trailChunks. */
|
|
1123
1165
|
trailBox = /* @__PURE__ */ new Float32Array(4);
|
|
1166
|
+
/** Bounds of each chunk of that trail: min.xy, max.xy per chunk (see writeTrail). */
|
|
1167
|
+
trailChunks = /* @__PURE__ */ new Float32Array(20);
|
|
1124
1168
|
/** Ring buffer of trail samples: x, y, time. */
|
|
1125
1169
|
trail = /* @__PURE__ */ new Float32Array(117);
|
|
1126
1170
|
trailHead = 0;
|
|
@@ -1128,6 +1172,12 @@ var Koi = class {
|
|
|
1128
1172
|
nextTrail = 0;
|
|
1129
1173
|
constructor(variety, seed, world) {
|
|
1130
1174
|
this.variety = variety;
|
|
1175
|
+
this.bendTrig = Float64Array.from([variety.bodyBend, variety.tailBend].flatMap((m) => [
|
|
1176
|
+
Math.cos(m),
|
|
1177
|
+
Math.sin(m),
|
|
1178
|
+
Math.cos(-m),
|
|
1179
|
+
Math.sin(-m)
|
|
1180
|
+
]));
|
|
1131
1181
|
this.rng = new Rng(seed);
|
|
1132
1182
|
this.noiseSeed = this.rng.range(0, 100);
|
|
1133
1183
|
this.patternSeed = this.rng.range(0, 40);
|
|
@@ -1219,11 +1269,11 @@ var Koi = class {
|
|
|
1219
1269
|
const t = w.time;
|
|
1220
1270
|
const ptr = w.pointer;
|
|
1221
1271
|
const ptrActive = (ptr.inside || ptr.linger) && t - ptr.lastMove < v.attention;
|
|
1222
|
-
const pd = ptrActive ?
|
|
1272
|
+
const pd = ptrActive ? hypot(ptr.x - this.x, ptr.y - this.y) : Infinity;
|
|
1223
1273
|
for (const tap of w.taps) {
|
|
1224
1274
|
if (tap.t <= this.lastTap) continue;
|
|
1225
1275
|
this.lastTap = tap.t;
|
|
1226
|
-
const d =
|
|
1276
|
+
const d = hypot(tap.x - this.x, tap.y - this.y);
|
|
1227
1277
|
if (!w.calm && d < w.unit * .55 * v.skittish) this.startle(tap.x, tap.y, t, w);
|
|
1228
1278
|
else if (v.roams && d < w.unit * 1.5) {
|
|
1229
1279
|
this.poi = {
|
|
@@ -1266,12 +1316,12 @@ var Koi = class {
|
|
|
1266
1316
|
else if (ax > w.width - margin) sx -= (ax - (w.width - margin)) / margin * 3;
|
|
1267
1317
|
if (ay < margin) sy += (margin - ay) / margin * 3;
|
|
1268
1318
|
else if (ay > w.height - margin) sy -= (ay - (w.height - margin)) / margin * 3;
|
|
1269
|
-
const wallK =
|
|
1319
|
+
const wallK = hypot(sx - sx0, sy - sy0);
|
|
1270
1320
|
for (const o of w.fish) {
|
|
1271
1321
|
if (o === this) continue;
|
|
1272
1322
|
const dx = this.x - o.x;
|
|
1273
1323
|
const dy = this.y - o.y;
|
|
1274
|
-
const d =
|
|
1324
|
+
const d = hypot(dx, dy);
|
|
1275
1325
|
const range = (L + o.length) * .5;
|
|
1276
1326
|
const dz = Math.abs(this.depth - o.depth);
|
|
1277
1327
|
if (d > .001 && d < range && dz < .3) {
|
|
@@ -1310,7 +1360,7 @@ var Koi = class {
|
|
|
1310
1360
|
y: w.height * this.rng.range(.15, .85)
|
|
1311
1361
|
});
|
|
1312
1362
|
let poi = pick();
|
|
1313
|
-
for (let k = 0; k < 6 &&
|
|
1363
|
+
for (let k = 0; k < 6 && hypot(poi.x - this.x, poi.y - this.y) < .35 * w.unit; k++) poi = pick();
|
|
1314
1364
|
this.poi = poi;
|
|
1315
1365
|
this.poiUntil = t + this.rng.range(8, 14);
|
|
1316
1366
|
}
|
|
@@ -1325,7 +1375,7 @@ var Koi = class {
|
|
|
1325
1375
|
if (tw !== 0) {
|
|
1326
1376
|
const dx = tx - this.x;
|
|
1327
1377
|
const dy = ty - this.y;
|
|
1328
|
-
td =
|
|
1378
|
+
td = hypot(dx, dy);
|
|
1329
1379
|
if (td > .001) {
|
|
1330
1380
|
const near = tw > 0 ? smoothstep(L * .25, L * .6, td) : 1;
|
|
1331
1381
|
sx += dx / td * tw * 2.2 * near;
|
|
@@ -1409,7 +1459,7 @@ var Koi = class {
|
|
|
1409
1459
|
startle(x, y, t, w) {
|
|
1410
1460
|
const dx = this.x - x;
|
|
1411
1461
|
const dy = this.y - y;
|
|
1412
|
-
const d =
|
|
1462
|
+
const d = hypot(dx, dy) || 1;
|
|
1413
1463
|
this.fleeX = dx / d;
|
|
1414
1464
|
this.fleeY = dy / d;
|
|
1415
1465
|
this.mode = "flee";
|
|
@@ -1437,7 +1487,7 @@ var Koi = class {
|
|
|
1437
1487
|
const maxBend = i < 16 ? v.bodyBend : v.tailBend;
|
|
1438
1488
|
let dx = s[i * 2] - s[i * 2 - 2];
|
|
1439
1489
|
let dy = s[i * 2 + 1] - s[i * 2 - 1];
|
|
1440
|
-
const d =
|
|
1490
|
+
const d = hypot(dx, dy);
|
|
1441
1491
|
if (d < 1e-6) {
|
|
1442
1492
|
dx = px;
|
|
1443
1493
|
dy = py;
|
|
@@ -1445,11 +1495,19 @@ var Koi = class {
|
|
|
1445
1495
|
dx /= d;
|
|
1446
1496
|
dy /= d;
|
|
1447
1497
|
}
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1498
|
+
const cross = px * dy - py * dx;
|
|
1499
|
+
const dot = px * dx + py * dy;
|
|
1500
|
+
const t = i < 16 ? 0 : 4;
|
|
1501
|
+
const past = Math.abs(cross) * this.bendTrig[t] - dot * this.bendTrig[t + 1];
|
|
1502
|
+
let side = 0;
|
|
1503
|
+
if (past > 1e-9 && cross !== 0) side = cross > 0 ? 1 : -1;
|
|
1504
|
+
else if (past >= -1e-9) {
|
|
1505
|
+
const bend = Math.atan2(cross, dot);
|
|
1506
|
+
if (Math.abs(bend) > maxBend) side = Math.sign(bend);
|
|
1507
|
+
}
|
|
1508
|
+
if (side !== 0) {
|
|
1509
|
+
const c = this.bendTrig[side > 0 ? t : t + 2];
|
|
1510
|
+
const sn = this.bendTrig[side > 0 ? t + 1 : t + 3];
|
|
1453
1511
|
dx = px * c - py * sn;
|
|
1454
1512
|
dy = px * sn + py * c;
|
|
1455
1513
|
}
|
|
@@ -1467,26 +1525,25 @@ var Koi = class {
|
|
|
1467
1525
|
const tailLen = L * v.tailLength;
|
|
1468
1526
|
const total = L + tailLen;
|
|
1469
1527
|
const maxR = v.radius * L;
|
|
1470
|
-
const pedR = maxR *
|
|
1528
|
+
const pedR = maxR * PEDUNCLE_PROFILE;
|
|
1471
1529
|
for (let i = 0; i < 22; i++) {
|
|
1472
1530
|
const i0 = Math.max(i - 1, 0);
|
|
1473
1531
|
const i1 = Math.min(i + 1, 21);
|
|
1474
1532
|
let tx = s[i1 * 2] - s[i0 * 2];
|
|
1475
1533
|
let ty = s[i1 * 2 + 1] - s[i0 * 2 + 1];
|
|
1476
|
-
const tl =
|
|
1534
|
+
const tl = hypot(tx, ty) || 1;
|
|
1477
1535
|
tx /= tl;
|
|
1478
1536
|
ty /= tl;
|
|
1479
1537
|
let a;
|
|
1480
1538
|
let r;
|
|
1481
1539
|
let lag = 0;
|
|
1482
1540
|
if (i < 16) {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
r = maxR * profile(u);
|
|
1541
|
+
a = i / 15 * L / total;
|
|
1542
|
+
r = maxR * JOINT_PROFILE[i];
|
|
1486
1543
|
} else {
|
|
1487
1544
|
const k = (i - 16 + 1) / 6;
|
|
1488
1545
|
a = (L + k * tailLen) / total;
|
|
1489
|
-
r = pedR + (maxR * v.tailSpread - pedR) *
|
|
1546
|
+
r = pedR + (maxR * v.tailSpread - pedR) * TAIL_SPREAD[i - 16];
|
|
1490
1547
|
lag = k * v.tailBend * 1.6;
|
|
1491
1548
|
}
|
|
1492
1549
|
const off = this.amp * (.1 + .9 * a * a) * Math.sin(TAU * v.waveK * a - this.phase - lag);
|
|
@@ -1501,15 +1558,22 @@ var Koi = class {
|
|
|
1501
1558
|
this.pecMax = v.finWidth * L;
|
|
1502
1559
|
this.pecRoot = this.pecMax * .4;
|
|
1503
1560
|
const root = this.sampleBody(.2);
|
|
1561
|
+
const rootX = root[0];
|
|
1562
|
+
const rootY = root[1];
|
|
1563
|
+
const rootR = root[2];
|
|
1564
|
+
const rootBx = root[3];
|
|
1565
|
+
const rootBy = root[4];
|
|
1566
|
+
const rootNx = -rootBy;
|
|
1567
|
+
const rootNy = rootBx;
|
|
1504
1568
|
for (let side = -1; side <= 1; side += 2) {
|
|
1505
1569
|
const out = side > 0 ? this.pecL : this.pecR;
|
|
1506
1570
|
const ang = sweep + slow * .35 * Math.sin(t * 3.4 + (side > 0 ? 0 : Math.PI));
|
|
1507
1571
|
const c = Math.cos(ang);
|
|
1508
1572
|
const sn = Math.sin(ang) * side;
|
|
1509
|
-
const dx =
|
|
1510
|
-
const dy =
|
|
1511
|
-
const rx =
|
|
1512
|
-
const ry =
|
|
1573
|
+
const dx = rootBx * c + rootNx * sn;
|
|
1574
|
+
const dy = rootBy * c + rootNy * sn;
|
|
1575
|
+
const rx = rootX + rootNx * side * rootR * .85;
|
|
1576
|
+
const ry = rootY + rootNy * side * rootR * .85;
|
|
1513
1577
|
let tipX = rx + dx * finLen;
|
|
1514
1578
|
let tipY = ry + dy * finLen;
|
|
1515
1579
|
if (this.posed) {
|
|
@@ -1518,7 +1582,7 @@ var Koi = class {
|
|
|
1518
1582
|
tipY = lerp(out[3], tipY, k);
|
|
1519
1583
|
const lx = tipX - rx;
|
|
1520
1584
|
const ly = tipY - ry;
|
|
1521
|
-
const ll =
|
|
1585
|
+
const ll = hypot(lx, ly) || 1;
|
|
1522
1586
|
tipX = rx + lx / ll * finLen;
|
|
1523
1587
|
tipY = ry + ly / ll * finLen;
|
|
1524
1588
|
}
|
|
@@ -1528,10 +1592,15 @@ var Koi = class {
|
|
|
1528
1592
|
out[3] = tipY;
|
|
1529
1593
|
}
|
|
1530
1594
|
const head = this.sampleBody(.075);
|
|
1531
|
-
|
|
1595
|
+
const headX = head[0];
|
|
1596
|
+
const headY = head[1];
|
|
1597
|
+
const headR = head[2];
|
|
1598
|
+
const headNx = -head[4];
|
|
1599
|
+
const headNy = head[3];
|
|
1600
|
+
this.eyeRadius = Math.max(headR * .17, 1);
|
|
1532
1601
|
for (let side = 1, o = 0; side >= -1; side -= 2, o += 2) {
|
|
1533
|
-
this.eyes[o] =
|
|
1534
|
-
this.eyes[o + 1] =
|
|
1602
|
+
this.eyes[o] = headX + headNx * side * headR * .55;
|
|
1603
|
+
this.eyes[o + 1] = headY + headNy * side * headR * .55;
|
|
1535
1604
|
}
|
|
1536
1605
|
let minX = Infinity;
|
|
1537
1606
|
let minY = Infinity;
|
|
@@ -1544,11 +1613,14 @@ var Koi = class {
|
|
|
1544
1613
|
maxX = Math.max(maxX, j[i * 3] + r);
|
|
1545
1614
|
maxY = Math.max(maxY, j[i * 3 + 1] + r);
|
|
1546
1615
|
}
|
|
1547
|
-
for (
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1616
|
+
for (let f = 0; f < 2; f++) {
|
|
1617
|
+
const fin = f ? this.pecR : this.pecL;
|
|
1618
|
+
for (let k = 0; k < 4; k += 2) {
|
|
1619
|
+
minX = Math.min(minX, fin[k] - this.pecMax);
|
|
1620
|
+
minY = Math.min(minY, fin[k + 1] - this.pecMax);
|
|
1621
|
+
maxX = Math.max(maxX, fin[k] + this.pecMax);
|
|
1622
|
+
maxY = Math.max(maxY, fin[k + 1] + this.pecMax);
|
|
1623
|
+
}
|
|
1552
1624
|
}
|
|
1553
1625
|
this.bbox[0] = minX;
|
|
1554
1626
|
this.bbox[1] = minY;
|
|
@@ -1556,7 +1628,10 @@ var Koi = class {
|
|
|
1556
1628
|
this.bbox[3] = maxY;
|
|
1557
1629
|
this.posed = true;
|
|
1558
1630
|
}
|
|
1559
|
-
/**
|
|
1631
|
+
/**
|
|
1632
|
+
* Point on the rendered body centreline at u (0 nose .. 1 peduncle), with
|
|
1633
|
+
* its radius and backward (head → tail) axis, written into `sample`.
|
|
1634
|
+
*/
|
|
1560
1635
|
sampleBody(u) {
|
|
1561
1636
|
const j = this.joints;
|
|
1562
1637
|
const f = u * 15;
|
|
@@ -1566,20 +1641,16 @@ var Koi = class {
|
|
|
1566
1641
|
const b = Math.min(i + 2, 15);
|
|
1567
1642
|
let bx = j[b * 3] - j[a * 3];
|
|
1568
1643
|
let by = j[b * 3 + 1] - j[a * 3 + 1];
|
|
1569
|
-
const bl =
|
|
1644
|
+
const bl = hypot(bx, by) || 1;
|
|
1570
1645
|
bx /= bl;
|
|
1571
1646
|
by /= bl;
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
/** fish's left in y-down screen space */
|
|
1580
|
-
nx: -by,
|
|
1581
|
-
ny: bx
|
|
1582
|
-
};
|
|
1647
|
+
const o = this.sample;
|
|
1648
|
+
o[0] = lerp(j[i * 3], j[(i + 1) * 3], k);
|
|
1649
|
+
o[1] = lerp(j[i * 3 + 1], j[(i + 1) * 3 + 1], k);
|
|
1650
|
+
o[2] = lerp(j[i * 3 + 2], j[(i + 1) * 3 + 2], k);
|
|
1651
|
+
o[3] = bx;
|
|
1652
|
+
o[4] = by;
|
|
1653
|
+
return o;
|
|
1583
1654
|
}
|
|
1584
1655
|
/**
|
|
1585
1656
|
* Where the trail is laid: 35% of the way from the spine's tail tip to the
|
|
@@ -1604,37 +1675,65 @@ var Koi = class {
|
|
|
1604
1675
|
}
|
|
1605
1676
|
/**
|
|
1606
1677
|
* Writes trail samples newest-first as texels (x, y, age, 0) starting at
|
|
1607
|
-
* `base`, beginning with the live tail-tip position
|
|
1678
|
+
* `base`, beginning with the live tail-tip position and ending with the
|
|
1679
|
+
* first sample TRAIL_CUT old. Returns the count.
|
|
1680
|
+
* Chunk c of trailChunks bounds segments 1 + c * TRAIL_CHUNK to
|
|
1681
|
+
* (c + 1) * TRAIL_CHUNK (segment k joins samples k - 1 and k), grown by the
|
|
1682
|
+
* width they draw at the chunk's oldest sample (trails.glsl trailSeg, less
|
|
1683
|
+
* its cell term, which the shader adds) and 1 px for rounding; a chunk past
|
|
1684
|
+
* the last segment is empty. trailBox is their union, empty with no segments.
|
|
1608
1685
|
*/
|
|
1609
1686
|
writeTrail(out, base, t) {
|
|
1610
1687
|
const cap = 39;
|
|
1611
1688
|
const life = this.variety.trailTime;
|
|
1612
|
-
|
|
1689
|
+
const [x, y] = this.trailPoint();
|
|
1613
1690
|
out[base] = x;
|
|
1614
1691
|
out[base + 1] = y;
|
|
1615
1692
|
out[base + 2] = 0;
|
|
1616
1693
|
out[base + 3] = 0;
|
|
1617
|
-
let minX = x;
|
|
1618
|
-
let minY = y;
|
|
1619
|
-
let maxX = x;
|
|
1620
|
-
let maxY = y;
|
|
1621
1694
|
let n = 1;
|
|
1622
1695
|
for (let k = 0; k < this.trailLen; k++) {
|
|
1623
1696
|
const idx = (this.trailHead - 1 - k + cap) % cap;
|
|
1624
1697
|
const age = (t - this.trail[idx * 3 + 2]) / life;
|
|
1625
|
-
x = this.trail[idx * 3];
|
|
1626
|
-
y = this.trail[idx * 3 + 1];
|
|
1627
1698
|
const o = base + n * 4;
|
|
1628
|
-
out[o] =
|
|
1629
|
-
out[o + 1] =
|
|
1699
|
+
out[o] = this.trail[idx * 3];
|
|
1700
|
+
out[o + 1] = this.trail[idx * 3 + 1];
|
|
1630
1701
|
out[o + 2] = Math.min(age, 1);
|
|
1631
1702
|
out[o + 3] = 0;
|
|
1632
|
-
minX = Math.min(minX, x);
|
|
1633
|
-
minY = Math.min(minY, y);
|
|
1634
|
-
maxX = Math.max(maxX, x);
|
|
1635
|
-
maxY = Math.max(maxY, y);
|
|
1636
1703
|
n++;
|
|
1637
|
-
if (age >=
|
|
1704
|
+
if (age >= TRAIL_CUT) break;
|
|
1705
|
+
}
|
|
1706
|
+
const boxes = this.trailChunks;
|
|
1707
|
+
const w0 = this.variety.trailWidth * this.length;
|
|
1708
|
+
let minX = 1e9;
|
|
1709
|
+
let minY = 1e9;
|
|
1710
|
+
let maxX = -1e9;
|
|
1711
|
+
let maxY = -1e9;
|
|
1712
|
+
for (let c = 0; c < 5; c++) {
|
|
1713
|
+
const first = c * 8;
|
|
1714
|
+
const last = first + 1 < n ? Math.min(first + 8, n - 1) : -1;
|
|
1715
|
+
let x0 = 1e9;
|
|
1716
|
+
let y0 = 1e9;
|
|
1717
|
+
let x1 = -1e9;
|
|
1718
|
+
let y1 = -1e9;
|
|
1719
|
+
let maxAge = 0;
|
|
1720
|
+
for (let i = first; i <= last; i++) {
|
|
1721
|
+
const o = base + i * 4;
|
|
1722
|
+
x0 = Math.min(x0, out[o]);
|
|
1723
|
+
y0 = Math.min(y0, out[o + 1]);
|
|
1724
|
+
x1 = Math.max(x1, out[o]);
|
|
1725
|
+
y1 = Math.max(y1, out[o + 1]);
|
|
1726
|
+
maxAge = Math.max(maxAge, out[o + 2]);
|
|
1727
|
+
}
|
|
1728
|
+
const g = last < 0 ? 0 : w0 * (.5 + 1.4 * maxAge) + 1;
|
|
1729
|
+
boxes[c * 4] = x0 - g;
|
|
1730
|
+
boxes[c * 4 + 1] = y0 - g;
|
|
1731
|
+
boxes[c * 4 + 2] = x1 + g;
|
|
1732
|
+
boxes[c * 4 + 3] = y1 + g;
|
|
1733
|
+
minX = Math.min(minX, boxes[c * 4]);
|
|
1734
|
+
minY = Math.min(minY, boxes[c * 4 + 1]);
|
|
1735
|
+
maxX = Math.max(maxX, boxes[c * 4 + 2]);
|
|
1736
|
+
maxY = Math.max(maxY, boxes[c * 4 + 3]);
|
|
1638
1737
|
}
|
|
1639
1738
|
this.trailBox[0] = minX;
|
|
1640
1739
|
this.trailBox[1] = minY;
|
|
@@ -1807,7 +1906,7 @@ var Pond = class {
|
|
|
1807
1906
|
/** Ripple impulses waiting for the next simulation step. */
|
|
1808
1907
|
drops = [];
|
|
1809
1908
|
/** Fish data texture contents, see koi/layout.ts. */
|
|
1810
|
-
data = /* @__PURE__ */ new Float32Array(
|
|
1909
|
+
data = /* @__PURE__ */ new Float32Array(1200);
|
|
1811
1910
|
/** Fish rows in draw order, deepest first. */
|
|
1812
1911
|
order = /* @__PURE__ */ new Int32Array(4);
|
|
1813
1912
|
/** Seeded from the pond's seed; simulated whether or not they show (the sky decides that). */
|
|
@@ -1921,7 +2020,7 @@ var Pond = class {
|
|
|
1921
2020
|
const n = Math.min(this.fish.length, 4);
|
|
1922
2021
|
for (let f = 0; f < n; f++) {
|
|
1923
2022
|
const k = this.fish[f];
|
|
1924
|
-
const row = f *
|
|
2023
|
+
const row = f * 75 * 4;
|
|
1925
2024
|
for (let i = 0; i < 22; i++) {
|
|
1926
2025
|
const o = row + i * 4;
|
|
1927
2026
|
d[o] = k.joints[i * 3];
|
|
@@ -1931,28 +2030,26 @@ var Pond = class {
|
|
|
1931
2030
|
}
|
|
1932
2031
|
d.set(k.pecL, row + 88);
|
|
1933
2032
|
d.set(k.pecR, row + 92);
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
], row + 96);
|
|
2033
|
+
const fin = row + 96;
|
|
2034
|
+
d[fin] = k.pecRoot;
|
|
2035
|
+
d[fin + 1] = k.pecMax;
|
|
2036
|
+
d[fin + 2] = k.length * k.variety.tailLength;
|
|
2037
|
+
d[fin + 3] = k.variety.tailFork;
|
|
1940
2038
|
d.set(k.eyes, row + 100);
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
], row + 104);
|
|
2039
|
+
const p0 = row + 104;
|
|
2040
|
+
d[p0] = k.variety.id;
|
|
2041
|
+
d[p0 + 1] = k.patternSeed;
|
|
2042
|
+
d[p0 + 2] = k.eyeRadius;
|
|
2043
|
+
d[p0 + 3] = k.depth;
|
|
1947
2044
|
d.set(k.bbox, row + 108);
|
|
1948
2045
|
const count = k.writeTrail(d, row + 120, this.time);
|
|
1949
2046
|
d.set(k.trailBox, row + 112);
|
|
1950
|
-
d.set(
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
]
|
|
2047
|
+
d.set(k.trailChunks, row + 280);
|
|
2048
|
+
const p1 = row + 116;
|
|
2049
|
+
d[p1] = count;
|
|
2050
|
+
d[p1 + 1] = k.variety.trailWidth * k.length;
|
|
2051
|
+
d[p1 + 2] = 0;
|
|
2052
|
+
d[p1 + 3] = 0;
|
|
1956
2053
|
}
|
|
1957
2054
|
if (this.orderN !== n) {
|
|
1958
2055
|
const idx = Array.from({ length: n }, (_, i) => i).sort((a, b) => this.fish[b].depth - this.fish[a].depth);
|
|
@@ -1976,22 +2073,56 @@ var Pond = class {
|
|
|
1976
2073
|
function overlaps(a, b) {
|
|
1977
2074
|
return !(a[2] < b[0] || b[2] < a[0] || a[3] < b[1] || b[3] < a[1]);
|
|
1978
2075
|
}
|
|
1979
|
-
var fullscreen_default = "
|
|
1980
|
-
var common_default = "precision highp float;\nprecision highp int;\nprecision highp sampler2D;\n\n
|
|
1981
|
-
var ripple_default = "
|
|
1982
|
-
var scene_default = "// Scene pass: one fragment per character cell. Decides what the cell shows\n// (water, ripple, trail, fin, body outline, body, eye), its colour and which\n// glyph draws it. The ASCII pass only copies that glyph from the atlas.\n// World space is CSS pixels with y pointing down; cell (0, 0) is top-left.\n\nuniform sampler2D uFish;\nuniform sampler2D uRipple;\nuniform ivec2 uRippleSize;\nuniform float uRipplePx;\nuniform int uFishCount;\nuniform int uOrder[MAX_FISH];\nuniform vec2 uCell;\nuniform vec2 uOriginCss; // world position of the grid's top-left corner\nuniform float uTime;\n// Time-of-day lighting, derived on the CPU from the phase (lighting.ts).\n// Light space: x right, y down the screen, z up toward the viewer.\nuniform vec3 uLightDir; // unit, toward the sun or moon\nuniform vec3 uLightCol; // direct light, intensity included\nuniform vec3 uAmbient; // sky light on the fish\nuniform vec3 uWaterTint; // water, ripples and trails\nuniform vec3 uSky; // sky reflection added to lit water\nuniform float uGlint;\nuniform float uCaustic;\nuniform float uFloor; // minimum light per channel on fish (night legibility)\nuniform float uDarkWater; // darker night: flat water colour ×, 1 when off or by day\nuniform float uDarkCaustic; // darker night: flat water density × (fewer caustics), 1 when off or by day\nuniform vec2 uShadow; // shadow offset, px, away from the light\nuniform float uShadowAlpha;\nuniform float uCalm; // 1 under reduced motion: flicker slows further\nuniform float uStars; // reflected stars (sky feature stars): 0 none … 1 full night\nuniform float uMoteThin; // share of motes taken away (light palettes with stars); 0 keeps every mote\nuniform vec2 uView; // the pond's size, CSS px\n// The moon's reflection (sky feature moon; lighting.ts moonAt): xy centre as\n// a share of uView, y down; z radius as a share of its shorter side; w alpha, 0 off.\nuniform vec4 uMoon;\n// The sun's glitter (sky feature sun; lighting.ts sunAt), as uMoon; z is the\n// patch's outer edge.\nuniform vec4 uSun;\n\nlayout(location = 0) out vec4 oColor; // rgb colour, a density (for the raw view)\nlayout(location = 1) out vec4 oInfo; // r glyph index, g cell class\n\n// Colours, set by the renderer (look.ts): today's pond, or the palette's\n// roles (Q1). Outside roles mode they hold the pond's original constants.\nuniform vec3 uWaterDim;\nuniform vec3 uWaterLit;\nuniform vec3 uRippleCol;\nuniform vec3 uKohakuWhite;\nuniform vec3 uHiRed;\nuniform vec3 uSumi;\nuniform vec3 uShowaWhite;\nuniform vec3 uOgonGold;\nuniform vec3 uOgonShade; // Ogon's flank\nuniform vec3 uOgonGlint;\nuniform vec3 uEyeCol;\n// Light rim drawn over sumi so the Showa's black flank keeps its silhouette.\nuniform vec3 uSumiRim;\nuniform vec3 uKohakuFin;\nuniform vec3 uKohakuTail;\nuniform vec3 uOgonFin;\nuniform vec3 uOgonTail; // tail tip; the root is uOgonGold\nuniform vec3 uSilk;\nuniform vec3 uBubble;\nuniform vec3 uGoldDust;\nuniform vec3 uGlintCol; // water glints in roles mode (the highlight)\nuniform vec3 uAddTint; // multiplies light added on a fish: specular, rim, caustics\nuniform float uRoles; // 1 in roles mode: glints take uGlintCol, not the light's hue\nuniform vec3 uStarCool; // reflected stars, coolest …\nuniform vec3 uStarWarm; // … to warmest\nuniform vec3 uMoonCol; // the moon's reflection at full strength\nuniform vec3 uSunCol; // the sun's glitter at full strength\n\n// Ripple slope (height per px) that reads as a full-strength crest.\nconst float RIPPLE_GAIN = 28.0;\n// Refraction offset (px) per unit of surface slope.\nconst float REFRACT = 220.0;\n// Surface tilt (normal.xy / normal.z) per unit of RIPPLE_GAIN-scaled slope.\n// A full crest tilts ~63°, so a sun ~6° above the horizon is mirrored\n// (half vector ~42° off vertical) at about half a crest; noon needs only\n// ~13°, the rings' soft flanks.\nconst float GLINT_TILT = 2.0;\n// Flicker rate under reduced motion, on top of the pond's own 0.75 slowdown.\nconst float CALM_FLICKER = 0.3;\n// The moon's brightness at full night (D7): dimmer than full strength, so it\n// reads as a reflection rather than an icon.\nconst float MOON_GAIN = 0.85;\n// The sun's glitter, half the prototype's density (D8): the share of would-be\n// flashes dropped each flash, and the brightness of the faintest and brightest,\n// below the Ogon's glints.\nconst float SUN_THIN = 0.5;\nconst vec2 SUN_GAIN = vec2(0.6, 0.9);\n// Glitter strength below which a cell shows plain water, and the share of the\n// patch's radius at its full-strength core.\nconst float SUN_MIN = 0.45;\nconst float SUN_CORE = 0.115;\n// Body specular: broad, so the highlight runs along the back.\nconst float SPEC_POW = 18.0;\n\nfloat flicker() { return mix(1.0, CALM_FLICKER, uCalm); }\n\n// Seen from above, a fish is mostly back: a sun near the horizon would barely\n// reach it. Diffuse light and glyph weight therefore use the light's azimuth\n// at no less than ~30° elevation; the highlight and glints keep the true\n// direction, so they still slide to the lit edge as the sun sinks.\nvec3 diffuseDir() {\n return normalize(vec3(uLightDir.xy, max(uLightDir.z, 0.5)));\n}\n\n// Light falling on a surface with unit normal n: sky + sun/moon, never below\n// the night floor (per channel, which also neutralises the moon's blue).\nvec3 lightOn(vec3 n) {\n return max(uAmbient + uLightCol * max(dot(n, diffuseDir()), 0.0), vec3(uFloor));\n}\n\n// The light's own hue at full brightness, for glints and highlights.\nvec3 lightHue() {\n return uLightCol / max(max(uLightCol.r, max(uLightCol.g, uLightCol.b)), 1e-3);\n}\n\n// The sun or moon mirrored on the water (glints, the sun's glitter): the\n// light's hue, part way to white.\nvec3 mirrorHue() {\n return mix(lightHue(), vec3(1.0), 0.4);\n}\n\nstruct Cell {\n vec3 col;\n float dens;\n int mat;\n vec2 dir; // cell space: outward normal for edges, line direction otherwise\n float aux;\n float style;\n};\n\nstruct Hit {\n float d; // signed distance, px\n float u; // 0..1 along the polyline\n float v; // -1..1 across it\n vec2 n; // outward normal, world\n};\n\nvec4 F(int f, int i) { return texelFetch(uFish, ivec2(i, f), 0); }\n\n// Directions are converted to cell space so glyph slopes match on screen.\nvec2 lineDir(vec2 d) {\n vec2 c = d / uCell;\n float l = length(c);\n return l > 1e-6 ? c / l : vec2(1.0, 0.0);\n}\n\nvec2 cellNormal(vec2 n) {\n vec2 c = n * uCell;\n float l = length(c);\n return l > 1e-6 ? c / l : vec2(1.0, 0.0);\n}\n\n// Fraction of the cell covered, estimated from the distance along the normal.\nfloat coverage(float d, vec2 n) {\n float span = abs(n.x) * uCell.x + abs(n.y) * uCell.y;\n return clamp(0.5 - d / span, 0.0, 1.0);\n}\n\nfloat sq(float x) { return x * x; }\n\n// ---------------------------------------------------------------- glyphs\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) {\n const int B[16] = int[16](0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5);\n return (float(B[(c.x & 3) + (c.y & 3) * 4]) + 0.5) / 16.0;\n}\n\n// Sign-free line direction (y down) → horizontal, '\\', vertical or '/'.\nint lineGlyph(vec2 d, int horizontal, int vertical) {\n float a = atan(d.y, d.x);\n if (a < 0.0) a += PI;\n int s = int(floor(a / (PI * 0.25) + 0.5)) % 4;\n if (s == 0) return horizontal;\n if (s == 1) return G_BSLASH;\n if (s == 2) return vertical;\n return G_SLASH;\n}\n\n// Outward normal (y down) of a silhouette → outline glyph. `cov` says whether\n// the boundary sits in the upper or lower half of the cell.\nint edgeGlyph(vec2 n, float cov) {\n int o = int(floor((atan(n.y, n.x) + TAU) / (PI * 0.25) + 0.5)) % 8;\n if (o == 0) return G_RPAREN;\n if (o == 4) return G_LPAREN;\n if (o == 1 || o == 5) return G_SLASH;\n if (o == 3 || o == 7) return G_BSLASH;\n if (o == 2) return cov > 0.5 ? G_UNDERSCORE : G_MINUS; // bottom edge\n return cov > 0.5 ? G_MINUS : G_UNDERSCORE; // top edge\n}\n\nbool inBox(vec2 p, vec4 box, float pad) {\n return all(greaterThanEqual(p, box.xy - pad)) && all(lessThanEqual(p, box.zw + pad));\n}\n\n// ---------------------------------------------------------------- ripples\n\nfloat rippleTexel(ivec2 c) {\n c = clamp(c, ivec2(0), uRippleSize - 1);\n#ifdef RIPPLE_PACKED\n return dec16(texelFetch(uRipple, c, 0).rg);\n#else\n return texelFetch(uRipple, c, 0).r;\n#endif\n}\n\nfloat rippleAt(vec2 p) {\n vec2 t = p / uRipplePx - 0.5;\n vec2 i = floor(t);\n vec2 f = t - i;\n ivec2 c = ivec2(i);\n return mix(mix(rippleTexel(c), rippleTexel(c + ivec2(1, 0)), f.x),\n mix(rippleTexel(c + ivec2(0, 1)), rippleTexel(c + ivec2(1, 1)), f.x), f.y);\n}\n\n// ---------------------------------------------------------------- water\n\n// Voronoi edge network, animated: bright where two cells meet.\nfloat caustic(vec2 x, float t) {\n vec2 i = floor(x);\n vec2 f = fract(x);\n float f1 = 8.0;\n float f2 = 8.0;\n for (int y = -1; y <= 1; y++) {\n for (int k = -1; k <= 1; k++) {\n vec2 b = vec2(float(k), float(y));\n vec2 o = 0.5 + 0.42 * sin(t + TAU * hash22(i + b));\n float d = length(b + o - f);\n if (d < f1) {\n f2 = f1;\n f1 = d;\n } else if (d < f2) {\n f2 = d;\n }\n }\n }\n return 1.0 - smoothstep(0.0, 0.22, f2 - f1);\n}\n\nCell water(vec2 p, float h, vec2 g) {\n Cell c;\n // Motes drifting with a slow current.\n vec2 mp = floor((p + vec2(7.0, -4.0) * uTime) / uCell);\n // On a light palette at night, a quarter of them, so the stars read (D10).\n float mote = step(mix(0.96, 0.99, uMoteThin), hash12(mp)) * (0.4 + 0.6 * hash12(mp + 17.0));\n // Caustic light on the pond floor, bent a little by the surface. fbm spans\n // only about ±0.3 across a screen, so shimmer stays within ~[0.62, 0.98];\n // the smoothstep's low edge must stay below that range's top.\n float shimmer = 0.8 + 0.6 * fbm(p * 0.0016 + vec2(uTime * 0.012, -uTime * 0.008));\n float lit = smoothstep(0.45, 0.95, caustic(p * 0.011 + g * 3.0, uTime * 0.45) * shimmer);\n c.mat = M_WATER;\n // Fewer floor caustics as the light weakens; motes stay.\n c.dens = max(mote * 0.5, lit * mix(0.6, 1.0, uCaustic));\n // Kept dimmer than ripples and trails, which share the light glyphs.\n c.col = mix(uWaterDim, uWaterLit, 0.6 * lit * min(uCaustic, 1.0)) * uWaterTint + uSky;\n // The darker night (sky feature darkNight). Each is skipped at 1, so\n // without it the water is exactly 0.2's (D4).\n if (uDarkWater < 1.0) c.col *= uDarkWater;\n if (uDarkCaustic < 1.0) c.dens *= uDarkCaustic;\n c.aux = hash12(mp + 3.1);\n c.dir = vec2(1.0, 0.0);\n c.style = 0.0;\n\n float rip = smoothstep(0.15, 0.6, length(g) * RIPPLE_GAIN);\n if (rip > 0.25) {\n c.mat = M_RIPPLE;\n c.dens = rip;\n c.col = mix(uWaterLit, uRippleCol, rip) * uWaterTint + uSky;\n c.dir = lineDir(vec2(-g.y, g.x)); // along the crest\n c.aux = h > 0.0 ? 1.0 : 0.0;\n }\n\n // Glints: the cell mirrors the sun/moon to the viewer (looking straight\n // down) when its surface tilt matches the half vector's. Flat water never\n // glints; only ripples do.\n vec3 H = normalize(uLightDir + vec3(0.0, 0.0, 1.0));\n vec2 az = normalize(uLightDir.xy + vec2(1e-5, 0.0));\n vec2 across = vec2(-az.y, az.x);\n float low = 1.0 - uLightDir.z; // ~0.1 at noon, ~0.9 for a setting sun\n vec2 e = -g * RIPPLE_GAIN * GLINT_TILT - H.xy / H.z;\n float ea = dot(e, az);\n float ec = dot(e, across);\n // A low light is mirrored by a wider spread of slopes across its direction,\n // so glints run further along the crests that face it.\n float gl = exp(-sq(ea / (0.3 + 0.4 * low)) - sq(ec / (0.3 + 0.8 * low)));\n gl *= smoothstep(0.06, 0.25, length(g) * RIPPLE_GAIN);\n // Twinkle on a grid stretched along the light: with a low sun neighbouring\n // cells in that direction flash together, so glints read as long dashes.\n vec2 r = vec2(dot(p, az), dot(p, across)) / (max(uCell.x, uCell.y) * vec2(1.0 + 5.0 * low, 1.0));\n float tw = hash12(floor(r) + floor(uTime * 6.0 * flicker()) * 0.613);\n gl *= uGlint * (0.45 + 1.1 * tw);\n if (gl > 0.45) {\n c.mat = M_GLINT;\n c.dens = min(gl, 1.0);\n // Full brightness in the light's hue whatever the hour: a mirror image of\n // the sun or moon, not lit water.\n vec3 gc = uRoles > 0.5 ? uGlintCol : mirrorHue();\n c.col = gc * mix(0.8, 1.0, smoothstep(0.45, 1.0, gl));\n c.dir = lineDir(az);\n c.aux = low;\n }\n return c;\n}\n\n// A sparse field of reflected stars on flat water: fixed to the screen (the\n// sky does not drift with the current, unlike the motes), but ripples shift\n// which cell each star lands in, so a passing ring makes them jump.\nvoid stars(vec2 p, vec2 g, inout Cell c) {\n if (uStars < 0.02 || c.mat != M_WATER) return;\n vec2 cell = floor((p + g * REFRACT * 2.0) / uCell);\n // 1.6 % of cells; the faintest drop out below, leaving about 1.4 %.\n if (hash12(cell * 0.917 + 41.0) < 0.984) return;\n float mag = hash12(cell + 7.7);\n float ph = hash12(cell + 2.3) * TAU;\n // Twinkle: slow breathing plus a rare sharp scintillation. Calm holds\n // each star still at about its average, so the field looks the same.\n float tw = 0.75 + 0.25 * sin(uTime * (0.8 + 1.6 * mag) + ph);\n tw += 0.5 * pow(max(sin(uTime * 0.37 + ph * 3.0), 0.0), 40.0);\n tw = mix(tw, 0.8, uCalm);\n float v = mag * tw * uStars;\n if (v < 0.15) return;\n c.mat = M_STAR;\n c.dens = v;\n c.aux = hash12(cell + 9.1);\n c.style = 0.0;\n // Brighter stars stand out more. The pond dims toward black; roles fade\n // toward the deep water, so on a light palette a faint star is light grey.\n float k = 0.35 + 0.6 * min(v, 1.0);\n vec3 col = mix(uStarCool, uStarWarm, c.aux);\n c.col = uRoles > 0.5 ? mix(uWaterDim, col, k) : col * k;\n}\n\n// The moon's reflection: a full disc with maria and a drawn rim. Ripple\n// slopes shift where each cell looks, so a passing ring tears it into\n// shards; steep cells inside become bright crest dashes. Drawn over the\n// water and stars, under everything else.\nvoid moon(vec2 p, vec2 g, inout Cell c) {\n float a = uMoon.w;\n if (a < 0.02) return;\n float R = uMoon.z * min(uView.x, uView.y);\n // A slow sideways wobble, as on moving water; slower under reduced motion.\n vec2 wob = vec2(sin(p.y * 0.21 + uTime * 1.3 * flicker()) * 1.2, 0.0);\n vec2 dq = p + g * REFRACT * 3.0 + wob - uMoon.xy * uView;\n float dist = length(dq);\n if (dist > R * 1.4) return;\n vec2 n = dist > 1e-3 ? dq / dist : vec2(1.0, 0.0);\n float cv = coverage(dist - R, n);\n if (cv < 0.25) return;\n float r = dist / R;\n float maria = smoothstep(-0.05, 0.2, fbm(dq / R * 1.7 + 5.0));\n float lum = (0.95 - 0.45 * maria) * (1.0 - 0.25 * r * r);\n c.mat = M_MOON;\n c.aux = 0.0;\n // The pond dims toward black; roles fade toward the deep water, so on a\n // light palette the moon is an ink drawing that fades out to the water.\n float k = (0.35 + 0.5 * lum) * MOON_GAIN * a;\n c.col = uRoles > 0.5 ? mix(uWaterDim, uMoonCol, k) : uMoonCol * k;\n if (length(g) * RIPPLE_GAIN > 0.35) {\n c.style = 2.0; // a shard along the crest\n c.dens = a;\n c.dir = lineDir(vec2(-g.y, g.x));\n c.col = mix(c.col, uRoles > 0.5 ? uGlintCol : vec3(1.0), 0.3 * a);\n } else if (cv < 0.8) {\n c.style = 1.0; // rim\n c.dens = cv;\n c.dir = cellNormal(n);\n } else {\n c.style = 0.0; // face: o and : mostly, O only at the brightest centre\n c.dens = 0.72 * lum * a;\n }\n}\n\n// The sun's reflection by day: a patch of glitter, densest at the centre,\n// that flashes cell by cell. Ripple slopes shift where each cell looks, so a\n// ring spreads it. Gold at noon, amber with a low sun, never as bright as an\n// Ogon's glint. Drawn over the water and stars, under everything else; the\n// water's own glints keep their cells.\nvoid sun(vec2 p, vec2 g, inout Cell c) {\n float a = uSun.w;\n if (a < 0.02 || c.mat == M_GLINT) return;\n float R = uSun.z * min(uView.x, uView.y);\n vec2 dq = p + g * REFRACT * 3.0 - uSun.xy * uView;\n float r = length(dq) / R;\n if (r > 1.0) return;\n vec2 cell = floor(p / uCell);\n // Flashes 5 times a second; reduced motion slows that and holds most of\n // each cell's brightness still.\n float flash = floor(uTime * 5.0 * flicker());\n // Rising and setting, the patch thins out rather than shrinking.\n if (hash12(cell * 1.31 + flash * 0.29 + 3.0) > (1.0 - SUN_THIN) * a) return;\n float tw = hash12(cell + flash * 0.613);\n tw = mix(tw, hash12(cell + 0.37), uCalm * 0.7);\n float v = (1.0 - smoothstep(SUN_CORE, 1.0, r)) * (0.35 + 0.9 * tw);\n if (v < SUN_MIN) return;\n c.mat = M_SUN;\n c.dens = min(v, 1.0);\n c.aux = 0.0;\n c.style = 0.0;\n // The pond tints its gold by the light's hue, so it deepens with a low sun;\n // roles take the highlight and fade toward the deep water.\n float k = mix(SUN_GAIN.x, SUN_GAIN.y, smoothstep(SUN_MIN, 1.0, v));\n c.col = uRoles > 0.5 ? mix(uWaterDim, uSunCol, k) : uSunCol * mirrorHue() * k;\n}\n\n// ---------------------------------------------------------------- fish geometry\n\n// Closest point on the joint polyline [i0, i0 + count] with linearly\n// interpolated radius: a chain of tapered capsules.\nHit polyline(int f, int i0, int count, vec2 p) {\n Hit h;\n h.d = 1e9;\n h.u = 0.0;\n h.v = 0.0;\n h.n = vec2(1.0, 0.0);\n vec4 A = F(f, i0);\n for (int k = 0; k < count; k++) {\n vec4 B = F(f, i0 + k + 1);\n vec2 ab = B.xy - A.xy;\n vec2 ap = p - A.xy;\n float t = clamp(dot(ap, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);\n vec2 dq = ap - ab * t;\n float dist = length(dq);\n float r = mix(A.z, B.z, t);\n float d = dist - r;\n if (d < h.d) {\n h.d = d;\n h.u = (float(k) + t) / float(count);\n float side = ab.x * ap.y - ab.y * ap.x < 0.0 ? -1.0 : 1.0;\n h.v = side * dist / max(r, 1e-3);\n h.n = dist > 1e-4 ? dq / dist : normalize(vec2(-ab.y, ab.x) + 1e-6);\n }\n A = B;\n }\n return h;\n}\n\n// Pectoral fin: a blade that widens from the root and rounds off at the tip.\nfloat finDist(vec2 p, vec2 a, vec2 b, float r0, float r1, out float t) {\n vec2 ab = b - a;\n vec2 ap = p - a;\n t = clamp(dot(ap, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);\n float r = mix(r0, r1, smoothstep(0.0, 0.5, t)) * (1.0 - 0.45 * smoothstep(0.55, 1.0, t));\n return length(ap - ab * t) - r;\n}\n\n// Caudal fin: the tail polyline with a forked notch cut into its end and the\n// tip capped flat, so the rim is a closed outline.\nfloat tailSD(int f, vec2 q, vec4 fin) {\n Hit th = polyline(f, J_BODY - 1, J_TAIL, q);\n vec2 tip = F(f, J_TOTAL - 1).xy;\n vec2 tdir = normalize(tip - F(f, J_TOTAL - 2).xy + 1e-6);\n return 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\n// ---------------------------------------------------------------- markings\n\n// Kohaku: porcelain white with large hi (red) patches over the back, a red\n// head patch, clean white nose and tail root.\nvec3 kohaku(float u, float v, float seed) {\n float 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));\n return mix(uKohakuWhite, uHiRed, smoothstep(-0.02, 0.04, n));\n}\n\n// Showa: sumi (black) bands wrapping the body, big red patches, some white.\nvec3 showa(float u, float v, float seed, out float sumiAmt) {\n float s = fbm(vec2(u * 3.5 + v * 0.9, v * 1.4) + seed * 1.7) + 0.04;\n float 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));\n float sumi = smoothstep(0.0, 0.04, s);\n float hi = smoothstep(0.0, 0.04, r);\n sumiAmt = sumi * (1.0 - 0.5 * hi);\n return mix(mix(uShowaWhite, uHiRed, hi), uSumi, sumiAmt);\n}\n\n// Ogon: metallic gold; individual scales catch the light and a soft\n// highlight band rolls along the flank.\nvec3 ogon(float u, float v, float seed, out float glint) {\n vec2 sc = vec2(u * 26.0, v * 4.0 + 4.0);\n vec2 id = floor(vec2(sc.x + 0.5 * mod(floor(sc.y), 2.0), sc.y));\n float ph = hash12(id + seed);\n float twinkle = pow(0.5 + 0.5 * sin(uTime * 2.3 * flicker() + ph * TAU + u * 7.0), 14.0) * step(0.55, ph);\n float band = exp(-sq((v - 0.35 * sin(uTime * 0.6 + u * 4.0 + seed)) * 2.2));\n glint = max(twinkle, band * 0.6);\n vec3 base = mix(uOgonShade, uOgonGold, 1.0 - abs(v));\n return mix(base, uOgonGlint, glint);\n}\n\nvec3 pectoralColor(int variety, float t) {\n if (variety == V_KOHAKU) return uKohakuFin;\n if (variety == V_SHOWA) return mix(uSumi, uShowaWhite, smoothstep(0.15, 0.45, t)); // motoguro\n return uOgonFin;\n}\n\nvec3 tailColor(int variety, float u, float v, float seed) {\n if (variety == V_KOHAKU) return uKohakuTail;\n if (variety == V_SHOWA) {\n float streak = smoothstep(0.1, 0.2, gnoise(vec2(v * 3.0, u * 1.5) + seed));\n return mix(uShowaWhite, uSumi, streak * (1.0 - u * 0.6));\n }\n return mix(uOgonGold, uOgonTail, u);\n}\n\n// ---------------------------------------------------------------- composition\n\nfloat shadow(int f, vec2 p) {\n float lift = 1.25 - F(f, T_P0).w;\n vec2 q = p - uShadow * lift;\n float blur = max(uCell.x, uCell.y) * (0.5 + 1.5 * lift);\n if (!inBox(q, F(f, T_BBOX), blur)) return 0.0;\n float d = min(polyline(f, 0, J_BODY - 1, q).d,\n polyline(f, J_BODY - 1, J_TAIL, q).d + max(uCell.x, uCell.y) * 0.5);\n return (1.0 - smoothstep(-blur, blur, d)) * (0.95 - 0.35 * lift) * uShadowAlpha;\n}\n\nvoid trails(vec2 p, inout Cell c) {\n float best = 0.0;\n vec2 bestDir = vec2(1.0, 0.0);\n int bestVariety = 0;\n for (int f = 0; f < MAX_FISH; f++) {\n if (f >= uFishCount) break;\n vec4 P1 = F(f, T_P1);\n int n = int(P1.x + 0.5);\n float w0 = P1.y;\n if (n < 2 || !inBox(p, F(f, T_TBOX), w0 * 2.5 + uCell.y)) continue;\n int variety = int(F(f, T_P0).x + 0.5);\n vec4 A = F(f, T_TRAIL);\n for (int k = 1; k < TRAIL_N; k++) {\n if (k >= n) break;\n vec4 B = F(f, T_TRAIL + k);\n vec2 ab = B.xy - A.xy;\n vec2 ap = p - A.xy;\n float t = clamp(dot(ap, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);\n float dist = length(ap - ab * t);\n float age = mix(A.z, B.z, t);\n float w = w0 * (0.5 + 1.4 * age) + 0.3 * min(uCell.x, uCell.y);\n float I = pow(1.0 - age, 1.3) * (1.0 - smoothstep(w * 0.35, w, dist));\n if (I > best) {\n best = I;\n bestDir = ab;\n bestVariety = variety;\n }\n A = B;\n }\n }\n if (best < 0.05) return;\n\n vec2 cell = floor(p / uCell);\n vec3 col;\n if (bestVariety == V_KOHAKU) {\n // Silk: a smooth warm-white ribbon that frays as it fades.\n best *= 0.7 + 0.6 * gnoise(p * 0.035 - uTime * 0.5);\n col = uSilk;\n } else if (bestVariety == V_SHOWA) {\n // Bubbles: sparse dots that stay put while the trail fades.\n float hb = hash12(cell * 1.37 + 5.0);\n best *= step(0.62, hb) * (0.7 + 0.6 * hb);\n col = uBubble;\n } else {\n // Gold dust that twinkles.\n float tw = hash12(cell + floor(uTime * 7.0 * flicker()) * 0.731);\n best *= step(0.5, tw) * 1.2;\n col = uGoldDust;\n }\n if (best < 0.12) return;\n c.mat = M_TRAIL;\n c.dens = min(best, 1.0);\n // Silk keeps its warm hue and only dims as it fades; the others fade into the water.\n c.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;\n c.dir = lineDir(bestDir);\n c.aux = 0.0;\n c.style = float(bestVariety);\n}\n\n// Surface refraction at one point, as main() takes it per cell.\nvec2 refrAt(vec2 x) {\n float e = uRipplePx;\n return vec2(rippleAt(x + vec2(e, 0.0)) - rippleAt(x - vec2(e, 0.0)),\n rippleAt(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) {\n vec4 P0 = F(f, T_P0);\n int variety = int(P0.x + 0.5);\n float seed = P0.y;\n float depth = P0.w;\n // Deeper fish are displaced more by the moving surface.\n vec2 q = p + refr * (0.5 + depth);\n if (!inBox(q, F(f, T_BBOX), max(uCell.x, uCell.y) * 1.5)) return;\n float fog = 0.55 * depth;\n vec3 fogCol = uWaterDim * uWaterTint;\n float fade = 1.0 - 0.3 * depth;\n float style = float(variety);\n vec4 fin = F(f, T_FIN);\n // Fins are thin and lie nearly flat: lit as if facing straight up.\n vec3 flatLight = lightOn(vec3(0.0, 0.0, 1.0));\n\n // Fin normals come from a numeric gradient over half a cell, so outline\n // glyphs follow the fin's actual rim (fork and tip included).\n float reach = max(uCell.x, uCell.y);\n vec2 ex = vec2(uCell.x * 0.5, 0.0);\n vec2 ey = vec2(0.0, uCell.y * 0.5);\n\n // Pectoral fins sit beneath the body: outline on the rim, rays inside.\n for (int s = 0; s < 2; s++) {\n vec4 pf = F(f, T_PEC_L + s);\n float t, t2;\n float d = finDist(q, pf.xy, pf.zw, fin.x, fin.y, t);\n if (d > reach) continue;\n vec2 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),\n finDist(q + ey, pf.xy, pf.zw, fin.x, fin.y, t2) - finDist(q - ey, pf.xy, pf.zw, fin.x, fin.y, t2)) / uCell;\n vec2 n = normalize(gr + 1e-6);\n float cv = coverage(d, n);\n if (cv > 0.25) {\n c.col = mix(pectoralColor(variety, t) * flatLight, fogCol, fog);\n c.aux = 0.0;\n c.style = style;\n if (cv < 0.8) {\n c.mat = M_EDGE;\n c.dens = cv;\n c.dir = cellNormal(n);\n } else {\n c.mat = M_FIN;\n c.col *= 0.75;\n c.dens = 0.6 * fade;\n c.dir = lineDir(q - pf.xy);\n }\n }\n }\n\n // Caudal fin with a forked notch: outline on the rim, rays fanning out from\n // the peduncle inside.\n float td = tailSD(f, q, fin);\n if (td < reach) {\n vec2 gr = vec2(tailSD(f, q + ex, fin) - tailSD(f, q - ex, fin),\n tailSD(f, q + ey, fin) - tailSD(f, q - ey, fin)) / uCell;\n vec2 n = normalize(gr + 1e-6);\n float tcv = coverage(td, n);\n if (tcv > 0.25) {\n Hit th = polyline(f, J_BODY - 1, J_TAIL, q);\n c.col = mix(tailColor(variety, th.u, th.v, seed) * flatLight, fogCol, fog);\n c.aux = 0.0;\n c.style = style;\n if (tcv < 0.8) {\n c.mat = M_EDGE;\n c.dens = tcv;\n c.dir = cellNormal(n);\n } else {\n c.mat = M_FIN;\n c.col *= 0.75;\n c.dens = (0.65 - 0.2 * th.u) * fade;\n c.dir = lineDir(q - F(f, J_BODY - 1).xy);\n }\n }\n }\n\n // Body: dense fill inside, outline glyphs along the silhouette.\n Hit bh = polyline(f, 0, J_BODY - 1, q);\n float bcv = coverage(bh.d, bh.n);\n if (bcv > 0.18) {\n float glint = 0.0;\n float sumiAmt = 0.0;\n vec3 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 // The body as a cylinder seen from above: |v| = 1 at the silhouette.\n // 0.92 keeps the edge from turning fully sideways (a koi's back is broad).\n float a = min(abs(bh.v), 1.0) * 0.92;\n vec3 N = vec3(bh.n * a, sqrt(1.0 - a * a));\n float ndl = dot(N, diffuseDir());\n vec3 lit = lightOn(N);\n // Highlight along the back where the normal meets the half vector; it\n // slides across the back as the light moves. Sumi is lacquer-glossy and\n // ogon metallic (tinted gold).\n vec3 H = normalize(uLightDir + vec3(0.0, 0.0, 1.0));\n float spec = pow(max(dot(N, H), 0.0), SPEC_POW);\n vec3 hue = lightHue() * length(uLightCol) * 0.82 * uAddTint; // ~1 at noon, dim at night\n vec3 hi = variety == V_OGON ? hue * uOgonGold * 0.9 : hue * (variety == V_SHOWA ? 0.32 + 0.25 * sumiAmt : 0.3);\n // Rim on the flank away from the light, lit by the light wrapping round.\n vec2 az = normalize(uLightDir.xy + vec2(1e-5, 0.0));\n float rim = a * a * a * max(-dot(bh.n, az), 0.0) * (0.35 + 0.65 * (1.0 - uLightDir.z));\n // Caustics on the back, only for cells this fish covers. Shallower fish\n // catch sharper, brighter ones; the pattern slides as the fish swims.\n float cst = smoothstep(0.5, 0.95, caustic(q * 0.014 + refr * 0.012 + seed, uTime * 0.45));\n vec3 caust = uLightCol * uAddTint * cst * uCaustic * 0.4 * max(ndl, 0.0) * (1.0 - depth);\n c.col = mix(bc * lit + hi * spec + uLightCol * uAddTint * rim * 0.6 + caust, fogCol, fog);\n // Glyph weight follows the light too, so the lit flank is heavier.\n float shade = mix(0.58, 1.05, smoothstep(-0.2, 0.9, ndl)) + 0.25 * spec;\n c.style = style;\n if (bcv > 0.8) {\n // Glyph weight follows perceived lightness, so markings read as texture\n // and not only as colour: white scales are heavier than sumi.\n float Yl = dot(bc * bc, vec3(0.2126, 0.7152, 0.0722));\n float Ls = 1.16 * pow(Yl, 1.0 / 3.0) - 0.16;\n c.mat = M_BODY;\n c.dens = mix(0.6, 1.0, Ls) * shade * fade;\n c.aux = glint;\n } else {\n c.mat = M_EDGE;\n c.dens = bcv;\n c.dir = cellNormal(bh.n);\n c.aux = 0.0;\n c.col = mix(c.col, mix(uSumiRim * lit, fogCol, fog), 0.65 * sumiAmt);\n }\n }\n\n // Eyes: always claim the cell nearest their centre so they never vanish.\n vec4 E = F(f, T_EYE);\n for (int s = 0; s < 2; s++) {\n vec2 e = s == 0 ? E.xy : E.zw;\n // Refraction taken at the eye, not per cell: adjacent cells refract by different\n // amounts, so per-cell offsets can leave the eye with no cell (or two).\n vec2 de = abs(p + refrAt(e) * (0.5 + depth) - e);\n if (length(q - e) < P0.z || (de.x < uCell.x * 0.5 && de.y < uCell.y * 0.5)) {\n c.mat = M_EYE;\n c.col = mix(uEyeCol * max(flatLight, vec3(0.75)), fogCol, fog * 0.5);\n c.dens = 1.0;\n c.aux = 0.0;\n c.style = style;\n }\n }\n}\n\nint pickGlyph(Cell c, ivec2 cell) {\n float dens = clamp(c.dens, 0.0, 1.0);\n float aux = clamp(c.aux, 0.0, 1.0);\n int style = int(c.style + 0.5);\n if (c.mat == M_WATER) {\n if (dens < 0.12) return G_SPACE;\n if (dens < 0.6) return aux < 0.4 ? G_DOT : aux < 0.65 ? G_COMMA : aux < 0.85 ? G_BACKTICK : G_QUOTE;\n return G_COLON;\n }\n if (c.mat == M_RIPPLE) {\n if (dens < 0.45) return G_DOT;\n return lineGlyph(c.dir, aux > 0.5 ? G_TILDE : G_MINUS, G_PIPE);\n }\n if (c.mat == M_TRAIL) {\n if (style == V_SHOWA) return dens > 0.55 ? G_O : G_DOT;\n if (style == V_OGON) return dens > 0.65 ? G_STAR : dens > 0.35 ? G_QUOTE : G_DOT;\n if (dens < 0.3) return G_DOT;\n if (dens < 0.55) return G_MINUS;\n return lineGlyph(c.dir, G_TILDE, G_COLON);\n }\n if (c.mat == M_FIN) {\n if (dens < 0.3) return G_DOT;\n if (dens < 0.45) return G_QUOTE;\n return lineGlyph(c.dir, style == V_OGON ? G_TILDE : G_MINUS, G_PIPE);\n }\n if (c.mat == M_EDGE) return edgeGlyph(c.dir, dens);\n if (c.mat == M_BODY) {\n if (aux > 0.75) return G_STAR;\n float x = smoothstep(0.3, 0.95, dens) * float(BODY_N - 1) + (bayer4(cell) - 0.5) * 0.5;\n return BODY_RAMP[clamp(int(floor(x + 0.5)), 0, BODY_N - 1)];\n }\n if (c.mat == M_EYE) return G_O;\n if (c.mat == M_STAR) {\n // Mostly small points; '*' only for the brightest, mostly at a scintillation.\n if (dens > 0.95) return G_STAR;\n if (dens > 0.65) return G_PLUS;\n if (dens > 0.4) return aux < 0.5 ? G_QUOTE : G_BACKTICK;\n return G_DOT;\n }\n if (c.mat == M_MOON) {\n if (style == 2) return lineGlyph(c.dir, G_EQUALS, G_PIPE);\n if (style == 1) return edgeGlyph(c.dir, dens);\n return dens < 0.33 ? G_DOT : dens < 0.56 ? G_COLON : dens < 0.67 ? G_O : G_BIG_O;\n }\n if (c.mat == M_SUN) return dens > 0.8 ? G_STAR : dens > 0.6 ? G_PLUS : G_DOT;\n if (c.mat == M_GLINT) {\n // High light: star bursts. Low light: dashes along its direction.\n if (aux > 0.55) return dens > 0.6 ? lineGlyph(c.dir, dens > 0.85 ? G_EQUALS : G_MINUS, G_PIPE) : G_DOT;\n return dens > 0.75 ? G_STAR : G_PLUS;\n }\n return G_SPACE;\n}\n\nvoid main() {\n vec2 p = (floor(gl_FragCoord.xy) + 0.5) * uCell + uOriginCss;\n\n float e = uRipplePx;\n float h = rippleAt(p);\n vec2 g = vec2(rippleAt(p + vec2(e, 0.0)) - rippleAt(p - vec2(e, 0.0)),\n rippleAt(p + vec2(0.0, e)) - rippleAt(p - vec2(0.0, e))) / (2.0 * e);\n\n Cell c = water(p, h, g);\n stars(p, g, c);\n moon(p, g, c);\n sun(p, g, c);\n\n float sh = 0.0;\n for (int f = 0; f < MAX_FISH; f++) {\n if (f >= uFishCount) break;\n sh = max(sh, shadow(f, p));\n }\n c.dens *= clamp(1.0 - 1.4 * sh, 0.0, 1.0);\n c.col *= 1.0 - 0.5 * sh;\n\n trails(p, c);\n\n vec2 refr = g * REFRACT;\n for (int k = 0; k < MAX_FISH; k++) {\n if (k >= uFishCount) break;\n drawFish(uOrder[k], p, refr, c);\n }\n\n int glyph = pickGlyph(c, ivec2(gl_FragCoord.xy));\n oColor = vec4(c.col, clamp(c.dens, 0.0, 1.0));\n oInfo = vec4(float(glyph) / 255.0, float(c.mat) / 255.0, 0.0, 0.0);\n}\n";
|
|
1983
|
-
var
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2076
|
+
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";
|
|
2078
|
+
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";
|
|
2079
|
+
/** Each chunk's file name and source, in compile order. */
|
|
2080
|
+
var sceneChunks = [
|
|
2081
|
+
["uniforms.glsl", "\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"],
|
|
2082
|
+
["light.glsl", "\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"],
|
|
2083
|
+
["cell.glsl", "\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"],
|
|
2084
|
+
["ripples.glsl", "\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"],
|
|
2085
|
+
["water.glsl", "\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"],
|
|
2086
|
+
["sky.glsl", "\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"],
|
|
2087
|
+
["fish.glsl", "\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"],
|
|
2088
|
+
["markings.glsl", "\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"],
|
|
2089
|
+
["trails.glsl", "\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"],
|
|
2090
|
+
["compose.glsl", "\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"],
|
|
2091
|
+
["glyphs.glsl", "\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"],
|
|
2092
|
+
["main.frag", "\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\nint glyph = pickGlyph(c, ivec2(gl_FragCoord.xy));\noColor = vec4(c.col, clamp(c.dens, 0.0, 1.0));\noInfo = vec4(float(glyph) / 255.0, float(c.mat) / 255.0, 0.0, 0.0);\n}\n"]
|
|
2093
|
+
];
|
|
2094
|
+
var sceneSrc = sceneChunks.map(([, src]) => src).join("\n");
|
|
2095
|
+
/** The line each chunk starts on in sceneSrc, 1-based, so compile errors can name `sky.glsl:42`. */
|
|
2096
|
+
var line = 1;
|
|
2097
|
+
var sceneLines = sceneChunks.map(([name, src]) => {
|
|
2098
|
+
const start = {
|
|
2099
|
+
name,
|
|
2100
|
+
line
|
|
2101
|
+
};
|
|
2102
|
+
line += src.split("\n").length;
|
|
2103
|
+
return start;
|
|
2104
|
+
});
|
|
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}\nint glyph = int(texelFetch(uScene1, cell, 0).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}\nfragColor = vec4(c, 1.0);\n}\n";
|
|
2106
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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 = v > 0.7 ? G_STAR : v > 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, v);\noInfo = vec4(float(glyph) / 255.0, float(M_FLY) / 255.0, 0.0, 0.0);\n}\n";
|
|
2112
|
+
/** `file:line` for line n of a compiled shader, or just n before the first file. */
|
|
2113
|
+
function sourceLine(files, n) {
|
|
2114
|
+
let file;
|
|
2115
|
+
for (const f of files) if (f.line <= n) file = f;
|
|
2116
|
+
return file ? `${file.name}:${n - file.line + 1}` : String(n);
|
|
2117
|
+
}
|
|
2118
|
+
function compile(gl, type, source, label, files = []) {
|
|
1988
2119
|
const shader = gl.createShader(type);
|
|
1989
2120
|
if (!shader) throw new Error(`${label}: could not create shader`);
|
|
1990
2121
|
gl.shaderSource(shader, source);
|
|
1991
2122
|
gl.compileShader(shader);
|
|
1992
2123
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS) && !gl.isContextLost()) {
|
|
1993
|
-
const log = gl.getShaderInfoLog(shader) ?? "";
|
|
1994
|
-
const numbered = source.split("\n").map((l, i) => `${
|
|
2124
|
+
const log = (gl.getShaderInfoLog(shader) ?? "").replace(/\b0:(\d+)\b/g, (_, n) => sourceLine(files, Number(n)));
|
|
2125
|
+
const numbered = source.split("\n").map((l, i) => `${sourceLine(files, i + 1).padStart(files.length ? 18 : 4)} ${l}`).join("\n");
|
|
1995
2126
|
gl.deleteShader(shader);
|
|
1996
2127
|
throw new Error(`${label}: shader compile failed\n${log}\n${numbered}`);
|
|
1997
2128
|
}
|
|
@@ -2002,10 +2133,11 @@ var Program = class {
|
|
|
2002
2133
|
handle;
|
|
2003
2134
|
gl;
|
|
2004
2135
|
locs = /* @__PURE__ */ new Map();
|
|
2005
|
-
|
|
2136
|
+
/** `fragmentFiles`: where each file starts in `fragment`, for compile errors. */
|
|
2137
|
+
constructor(gl, vertex, fragment, label, fragmentFiles = []) {
|
|
2006
2138
|
this.gl = gl;
|
|
2007
2139
|
const vs = compile(gl, gl.VERTEX_SHADER, vertex, `${label}.vert`);
|
|
2008
|
-
const fs = compile(gl, gl.FRAGMENT_SHADER, fragment, `${label}.frag
|
|
2140
|
+
const fs = compile(gl, gl.FRAGMENT_SHADER, fragment, `${label}.frag`, fragmentFiles);
|
|
2009
2141
|
const program = gl.createProgram();
|
|
2010
2142
|
if (!program) throw new Error(`${label}: could not create program`);
|
|
2011
2143
|
gl.attachShader(program, vs);
|
|
@@ -2114,7 +2246,7 @@ function deleteTarget(gl, t) {
|
|
|
2114
2246
|
for (const tex of t.textures) gl.deleteTexture(tex);
|
|
2115
2247
|
}
|
|
2116
2248
|
var OGON_GOLD = POND_METALLIC;
|
|
2117
|
-
/** Today's pond, exactly as the constants scene
|
|
2249
|
+
/** Today's pond, exactly as the constants the scene shader had before palettes (A3). */
|
|
2118
2250
|
var pondScene = {
|
|
2119
2251
|
uWaterDim: [
|
|
2120
2252
|
.1,
|
|
@@ -2339,6 +2471,19 @@ function asciiLook(p, light) {
|
|
|
2339
2471
|
outBg: !p.background ? pondBg : p.light ? p.background : graded(p.background, light)
|
|
2340
2472
|
};
|
|
2341
2473
|
}
|
|
2474
|
+
/**
|
|
2475
|
+
* The ramp's stops (5 vec3s, zero past the last and for other modes) and each
|
|
2476
|
+
* stop's step to the next (4 vec3s) for ascii.frag's ramp(). It adds f × step
|
|
2477
|
+
* to a stop in one fused multiply-add, which is how radeonsi runs mix(stop,
|
|
2478
|
+
* next, f). The step is the difference of the float32 stops, taken in double
|
|
2479
|
+
* and stored as float32: the GPU's own float32 subtraction, bit for bit, so
|
|
2480
|
+
* the colours are the same.
|
|
2481
|
+
*/
|
|
2482
|
+
function rampUniforms(p, stops, deltas) {
|
|
2483
|
+
stops.fill(0);
|
|
2484
|
+
if (p.mode === "ramp") p.stops.forEach((c, i) => stops.set(c, i * 3));
|
|
2485
|
+
for (let i = 0; i < deltas.length; i++) deltas[i] = stops[i + 3] - stops[i];
|
|
2486
|
+
}
|
|
2342
2487
|
/** The feature names, in the order the docs and the demo list them. */
|
|
2343
2488
|
var SKY_FEATURES = [
|
|
2344
2489
|
"stars",
|
|
@@ -2445,15 +2590,20 @@ var FONT_MAX = 22;
|
|
|
2445
2590
|
var FONT_MIN_DEVICE = 11;
|
|
2446
2591
|
/** Halo brightness at glowStrength 1; kept low so glyphs never wash into blobs. */
|
|
2447
2592
|
var GLOW_GAIN = 2.2;
|
|
2593
|
+
/** Halo blur reach in cells either side: columns, rows (RX, RY in glow.frag). */
|
|
2594
|
+
var GLOW_RX = 4;
|
|
2595
|
+
var GLOW_RY = 2;
|
|
2448
2596
|
/** Firefly window weight below which no point is drawn (fly.vert's FLY_MIN then hides each one anyway). */
|
|
2449
2597
|
var FLIES_MIN = .02;
|
|
2450
2598
|
/**
|
|
2451
2599
|
* Four passes per frame:
|
|
2452
2600
|
* 1. ripple — wave equation on a coarse grid (ping-pong)
|
|
2453
|
-
* 2. scene — one fragment per character cell → colour and glyph
|
|
2601
|
+
* 2. scene — one fragment per character cell → colour and glyph, in two
|
|
2602
|
+
* draws (the water and all on it, then the fish); then the
|
|
2454
2603
|
* fireflies as one point each into the same cells, over the fish
|
|
2455
|
-
* 3. glow — one fragment per cell → blurred halo of the bright cells
|
|
2456
|
-
*
|
|
2604
|
+
* 3. glow — one fragment per cell → blurred halo of the bright cells,
|
|
2605
|
+
* from each cell's emission, stored first where float targets
|
|
2606
|
+
* exist (skipped on software renderers: no target, no program)
|
|
2457
2607
|
* 4. ascii — per screen pixel, copy the cell's glyph from the atlas
|
|
2458
2608
|
*/
|
|
2459
2609
|
var Renderer = class {
|
|
@@ -2481,6 +2631,9 @@ var Renderer = class {
|
|
|
2481
2631
|
sceneProg;
|
|
2482
2632
|
asciiProg;
|
|
2483
2633
|
glowProg;
|
|
2634
|
+
/** Each cell's emission for the glow pass; null without float32 targets (glow works it out per tap). */
|
|
2635
|
+
emissionProg;
|
|
2636
|
+
emissionFormat;
|
|
2484
2637
|
flyProg;
|
|
2485
2638
|
/** Vertex array and buffer for the fireflies: one vec4 per point (fireflies.ts). */
|
|
2486
2639
|
flyVao;
|
|
@@ -2488,14 +2641,30 @@ var Renderer = class {
|
|
|
2488
2641
|
fishTex;
|
|
2489
2642
|
dropBuf = /* @__PURE__ */ new Float32Array(128);
|
|
2490
2643
|
stopBuf = /* @__PURE__ */ new Float32Array(15);
|
|
2644
|
+
stopDeltaBuf = /* @__PURE__ */ new Float32Array(12);
|
|
2491
2645
|
/** Scene colours for the active palette (roles replace the pond's). */
|
|
2492
2646
|
sceneColors = POND_SCENE;
|
|
2493
2647
|
uploadedColors = null;
|
|
2494
2648
|
flyColors = null;
|
|
2649
|
+
/**
|
|
2650
|
+
* What render() derives besides `light`, and the phase, palette and sky it
|
|
2651
|
+
* last derived them from: pure functions of those three, so a frame that
|
|
2652
|
+
* repeats all three (a fixed time) reuses them.
|
|
2653
|
+
*/
|
|
2654
|
+
sceneLight = this.light;
|
|
2655
|
+
stars = starsAt(0, DEFAULT_SKY, false);
|
|
2656
|
+
flies = 0;
|
|
2657
|
+
look = asciiLook(POND, this.light);
|
|
2658
|
+
litPhase = NaN;
|
|
2659
|
+
litPalette = null;
|
|
2660
|
+
litSky = null;
|
|
2495
2661
|
atlas = null;
|
|
2496
2662
|
ripple = null;
|
|
2497
2663
|
scene = null;
|
|
2498
2664
|
glowTarget = null;
|
|
2665
|
+
/** glowTarget holds a drawn halo (false once it is cleared, or new and zero-filled). */
|
|
2666
|
+
haloInTarget = false;
|
|
2667
|
+
emissionTarget = null;
|
|
2499
2668
|
rippleClock = 0;
|
|
2500
2669
|
devW = 1;
|
|
2501
2670
|
devH = 1;
|
|
@@ -2529,13 +2698,34 @@ var Renderer = class {
|
|
|
2529
2698
|
format: gl.RGBA,
|
|
2530
2699
|
type: gl.UNSIGNED_BYTE
|
|
2531
2700
|
} : half;
|
|
2532
|
-
|
|
2701
|
+
this.emissionFormat = {
|
|
2702
|
+
internal: gl.RGBA32F,
|
|
2703
|
+
format: gl.RGBA,
|
|
2704
|
+
type: gl.FLOAT
|
|
2705
|
+
};
|
|
2706
|
+
let emission = false;
|
|
2707
|
+
if (!software && gl.getExtension("EXT_color_buffer_float")) {
|
|
2708
|
+
const probe = createTarget(gl, 4, 4, [this.emissionFormat]);
|
|
2709
|
+
if (probe) {
|
|
2710
|
+
deleteTarget(gl, probe);
|
|
2711
|
+
emission = true;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
const defines = layoutDefines() + glyphDefines() + varietyDefines() + `#define RX ${GLOW_RX}\n#define RY ${GLOW_RY}\n` + (packed ? "#define RIPPLE_PACKED\n" : "");
|
|
2533
2715
|
const fragment = (src) => `#version 300 es\n${defines}${common_default}\n${src}`;
|
|
2534
2716
|
const vertex = `#version 300 es\n${fullscreen_default}`;
|
|
2535
|
-
|
|
2536
|
-
this.
|
|
2537
|
-
|
|
2538
|
-
|
|
2717
|
+
const pixelVertex = `#version 300 es\n${pixel_default}`;
|
|
2718
|
+
this.rippleProg = new Program(gl, pixelVertex, fragment(ripple_default), "ripple");
|
|
2719
|
+
const sceneAt = fragment("").split("\n").length;
|
|
2720
|
+
const sceneFiles = sceneLines.map(({ name, line }) => ({
|
|
2721
|
+
name,
|
|
2722
|
+
line: line + sceneAt - 1
|
|
2723
|
+
}));
|
|
2724
|
+
this.sceneProg = new Program(gl, vertex, fragment(sceneSrc), "scene", sceneFiles);
|
|
2725
|
+
this.emissionProg = emission ? new Program(gl, vertex, fragment(`${emission_default$1}\n${emission_default}`), "emission") : null;
|
|
2726
|
+
const glow = emission ? `#define EMIT_TEX\n${glow_default}` : `${emission_default$1}\n${glow_default}`;
|
|
2727
|
+
this.glowProg = software ? null : new Program(gl, vertex, fragment(glow), "glow");
|
|
2728
|
+
this.asciiProg = new Program(gl, pixelVertex, fragment((software ? "" : "#define GLOW\n") + ascii_default), "ascii");
|
|
2539
2729
|
this.flyProg = new Program(gl, `#version 300 es\n${common_default}\n${fly_default$1}`, fragment(fly_default), "fly");
|
|
2540
2730
|
const vao = gl.createVertexArray();
|
|
2541
2731
|
const flyVao = gl.createVertexArray();
|
|
@@ -2551,7 +2741,7 @@ var Renderer = class {
|
|
|
2551
2741
|
gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0);
|
|
2552
2742
|
gl.bindVertexArray(null);
|
|
2553
2743
|
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
2554
|
-
this.fishTex = createTexture(gl,
|
|
2744
|
+
this.fishTex = createTexture(gl, 75, 4, {
|
|
2555
2745
|
internal: gl.RGBA32F,
|
|
2556
2746
|
format: gl.RGBA,
|
|
2557
2747
|
type: gl.FLOAT
|
|
@@ -2572,8 +2762,7 @@ var Renderer = class {
|
|
|
2572
2762
|
this.palette = palette;
|
|
2573
2763
|
this.intensity = intensity;
|
|
2574
2764
|
this.sceneColors = palette.mode === "roles" ? rolesScene(palette.roles) : POND_SCENE;
|
|
2575
|
-
this.stopBuf.
|
|
2576
|
-
if (palette.mode === "ramp") palette.stops.forEach((c, i) => this.stopBuf.set(c, i * 3));
|
|
2765
|
+
rampUniforms(palette, this.stopBuf, this.stopDeltaBuf);
|
|
2577
2766
|
}
|
|
2578
2767
|
/** Sets the night-sky features; the next render() draws with them. */
|
|
2579
2768
|
setSky(sky) {
|
|
@@ -2607,10 +2796,17 @@ var Renderer = class {
|
|
|
2607
2796
|
this.scene = createTarget(gl, cols, rows, [rgba8, rgba8]);
|
|
2608
2797
|
if (!this.scene) throw new Error("Scene render target is not supported");
|
|
2609
2798
|
if (this.glowTarget) deleteTarget(gl, this.glowTarget);
|
|
2799
|
+
if (this.emissionTarget) deleteTarget(gl, this.emissionTarget);
|
|
2610
2800
|
this.glowTarget = null;
|
|
2801
|
+
this.emissionTarget = null;
|
|
2611
2802
|
if (this.glowProg) {
|
|
2612
2803
|
this.glowTarget = createTarget(gl, cols, rows, [rgba8], gl.LINEAR);
|
|
2613
2804
|
if (!this.glowTarget) throw new Error("Glow render target is not supported");
|
|
2805
|
+
this.haloInTarget = false;
|
|
2806
|
+
}
|
|
2807
|
+
if (this.emissionProg) {
|
|
2808
|
+
this.emissionTarget = createTarget(gl, cols + 8, rows + 4, [this.emissionFormat]);
|
|
2809
|
+
if (!this.emissionTarget) throw new Error("Emission render target is not supported");
|
|
2614
2810
|
}
|
|
2615
2811
|
this.cols = cols;
|
|
2616
2812
|
this.rows = rows;
|
|
@@ -2638,12 +2834,22 @@ var Renderer = class {
|
|
|
2638
2834
|
render(pond, dt, phase) {
|
|
2639
2835
|
this.phase = phase;
|
|
2640
2836
|
const palette = this.raw ? POND : this.palette;
|
|
2641
|
-
|
|
2837
|
+
if (phase !== this.litPhase || palette !== this.litPalette || this.sky !== this.litSky) {
|
|
2838
|
+
this.light = lightingAt(phase, { darkNight: this.sky.darkNight && !palette.light });
|
|
2839
|
+
this.sceneLight = palette.mode === "roles" ? brightnessOnly(this.light) : this.light;
|
|
2840
|
+
this.stars = starsAt(phase, this.sky, palette.light);
|
|
2841
|
+
this.flies = firefliesAt(phase, this.sky);
|
|
2842
|
+
this.look = asciiLook(palette, this.light);
|
|
2843
|
+
this.litPhase = phase;
|
|
2844
|
+
this.litPalette = palette;
|
|
2845
|
+
this.litSky = this.sky;
|
|
2846
|
+
}
|
|
2847
|
+
const light = this.light;
|
|
2642
2848
|
const roles = palette.mode === "roles";
|
|
2643
2849
|
const colors = this.raw ? POND_SCENE : this.sceneColors;
|
|
2644
|
-
const sceneLight =
|
|
2645
|
-
const stars =
|
|
2646
|
-
const flies =
|
|
2850
|
+
const sceneLight = this.sceneLight;
|
|
2851
|
+
const stars = this.stars;
|
|
2852
|
+
const flies = this.flies;
|
|
2647
2853
|
const gl = this.gl;
|
|
2648
2854
|
const atlas = this.atlas;
|
|
2649
2855
|
const scene = this.scene;
|
|
@@ -2656,22 +2862,23 @@ var Renderer = class {
|
|
|
2656
2862
|
this.rippleClock -= steps / RIPPLE_HZ;
|
|
2657
2863
|
steps = Math.min(steps, 3);
|
|
2658
2864
|
const rp = this.rippleProg.use();
|
|
2659
|
-
rp.ivec2("uSize", this.simW, this.simH).float("uDamping", RIPPLE_DAMPING);
|
|
2660
|
-
|
|
2865
|
+
rp.vec2("uDev", this.simW, this.simH).ivec2("uSize", this.simW, this.simH).float("uDamping", RIPPLE_DAMPING);
|
|
2866
|
+
const ripple = this.ripple;
|
|
2661
2867
|
for (let s = 0; s < steps; s++) {
|
|
2662
|
-
const
|
|
2868
|
+
const src = ripple[0];
|
|
2869
|
+
const dst = ripple[1];
|
|
2663
2870
|
gl.bindFramebuffer(gl.FRAMEBUFFER, dst.fbo);
|
|
2664
2871
|
gl.viewport(0, 0, this.simW, this.simH);
|
|
2665
2872
|
rp.texture("uState", 0, src.textures[0]);
|
|
2666
2873
|
rp.int("uDropCount", s === 0 ? this.takeDrops(pond) : 0).vec4s("uDrops", this.dropBuf);
|
|
2667
2874
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
2668
|
-
ripple =
|
|
2875
|
+
ripple[0] = dst;
|
|
2876
|
+
ripple[1] = src;
|
|
2669
2877
|
}
|
|
2670
|
-
this.ripple = ripple;
|
|
2671
2878
|
const count = pond.pack();
|
|
2672
2879
|
gl.activeTexture(gl.TEXTURE0);
|
|
2673
2880
|
gl.bindTexture(gl.TEXTURE_2D, this.fishTex);
|
|
2674
|
-
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0,
|
|
2881
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 75, 4, gl.RGBA, gl.FLOAT, pond.data);
|
|
2675
2882
|
gl.bindFramebuffer(gl.FRAMEBUFFER, scene.fbo);
|
|
2676
2883
|
gl.viewport(0, 0, this.cols, this.rows);
|
|
2677
2884
|
const unit = pond.unit;
|
|
@@ -2680,8 +2887,13 @@ var Renderer = class {
|
|
|
2680
2887
|
for (const [name, c] of Object.entries(colors)) sp.vec3(name, c[0], c[1], c[2]);
|
|
2681
2888
|
this.uploadedColors = colors;
|
|
2682
2889
|
}
|
|
2683
|
-
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",
|
|
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);
|
|
2891
|
+
sp.int("uLayer", 0).int("uFishDraw", 0);
|
|
2684
2892
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
2893
|
+
if (count > 0) {
|
|
2894
|
+
sp.int("uLayer", 1).int("uFishDraw", count);
|
|
2895
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
2896
|
+
}
|
|
2685
2897
|
if (flies >= FLIES_MIN) {
|
|
2686
2898
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.flyBuf);
|
|
2687
2899
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, pond.packFireflies());
|
|
@@ -2697,33 +2909,54 @@ var Renderer = class {
|
|
|
2697
2909
|
gl.bindVertexArray(this.vao);
|
|
2698
2910
|
}
|
|
2699
2911
|
const glowProg = this.raw ? null : this.glowProg;
|
|
2912
|
+
const haloDrawn = !!(glowProg && this.glowTarget) && this.glow && !palette.light;
|
|
2700
2913
|
if (glowProg && this.glowTarget && (!this.glow || palette.light)) {
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2914
|
+
if (this.haloInTarget) {
|
|
2915
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.glowTarget.fbo);
|
|
2916
|
+
gl.clearColor(0, 0, 0, 0);
|
|
2917
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
2918
|
+
this.haloInTarget = false;
|
|
2919
|
+
}
|
|
2704
2920
|
} else if (glowProg && this.glowTarget) {
|
|
2921
|
+
const emission = this.emissionTarget;
|
|
2922
|
+
if (this.emissionProg && emission) {
|
|
2923
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, emission.fbo);
|
|
2924
|
+
gl.viewport(GLOW_RX, GLOW_RY, this.cols, this.rows);
|
|
2925
|
+
this.emissionProg.use().texture("uScene0", 0, scene.textures[0]).texture("uScene1", 1, scene.textures[1]);
|
|
2926
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
2927
|
+
gl.viewport(0, 0, this.cols, this.rows);
|
|
2928
|
+
}
|
|
2705
2929
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.glowTarget.fbo);
|
|
2706
|
-
glowProg.use()
|
|
2930
|
+
glowProg.use();
|
|
2931
|
+
if (emission) glowProg.texture("uEmit", 0, emission.textures[0]);
|
|
2932
|
+
else glowProg.texture("uScene0", 0, scene.textures[0]).texture("uScene1", 1, scene.textures[1]);
|
|
2933
|
+
glowProg.ivec2("uGrid", this.cols, this.rows).float("uAspect", atlas.cellW / atlas.cellH).float("uGain", light.glowStrength * GLOW_GAIN);
|
|
2707
2934
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
2935
|
+
this.haloInTarget = true;
|
|
2708
2936
|
}
|
|
2709
2937
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2710
2938
|
gl.viewport(0, 0, this.devW, this.devH);
|
|
2711
|
-
const look =
|
|
2939
|
+
const look = this.look;
|
|
2712
2940
|
const filter = palette.mode === "filter" ? palette : null;
|
|
2713
|
-
const
|
|
2714
|
-
|
|
2941
|
+
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);
|
|
2943
|
+
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);
|
|
2715
2944
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
2716
2945
|
}
|
|
2717
2946
|
/** Moves up to MAX_DROPS pending impulses into the uniform buffer, in sim texels. */
|
|
2718
2947
|
takeDrops(pond) {
|
|
2719
|
-
const
|
|
2720
|
-
|
|
2948
|
+
const drops = pond.drops;
|
|
2949
|
+
const n = Math.min(drops.length, 32);
|
|
2950
|
+
for (let i = 0; i < n; i++) {
|
|
2951
|
+
const d = drops[i];
|
|
2721
2952
|
this.dropBuf[i * 4] = d.x / RIPPLE_PX;
|
|
2722
2953
|
this.dropBuf[i * 4 + 1] = d.y / RIPPLE_PX;
|
|
2723
2954
|
this.dropBuf[i * 4 + 2] = Math.max(d.r / RIPPLE_PX, 1.5);
|
|
2724
2955
|
this.dropBuf[i * 4 + 3] = d.a;
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2956
|
+
}
|
|
2957
|
+
drops.copyWithin(0, n);
|
|
2958
|
+
drops.length -= n;
|
|
2959
|
+
return n;
|
|
2727
2960
|
}
|
|
2728
2961
|
};
|
|
2729
2962
|
var DEFAULT_FONT_FAMILY = "\"JetBrains Mono\", ui-monospace, Menlo, Consolas, monospace";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koi-ascii",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "An ASCII-art koi pond for page backgrounds. WebGL2, no dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"koi",
|
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
"shot": "node --experimental-strip-types scripts/shot.mjs",
|
|
49
49
|
"shots": "node --experimental-strip-types scripts/shots.mjs",
|
|
50
50
|
"perf": "node --experimental-strip-types scripts/perf.mjs",
|
|
51
|
+
"batch": "node --experimental-strip-types scripts/batch.mjs",
|
|
52
|
+
"cpu": "node --experimental-strip-types scripts/cpu.mjs",
|
|
53
|
+
"startup": "node --experimental-strip-types scripts/startup.mjs",
|
|
51
54
|
"release:check": "node --experimental-strip-types scripts/release-check.mjs"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|