@pixerate/flair 0.21.0 → 0.21.1

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.
@@ -6992,16 +6992,25 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
6992
6992
  float px = 1.0 / iResolution.y;
6993
6993
  vec2 p = fragCoord * px;
6994
6994
  vec4 acc = vec4(0.0);
6995
+ // The clicks are the same for every bubble, so look them up once.
6996
+ vec4 clicks[8];
6997
+ for (int j = 0; j < 8; j++) clicks[j] = clickAt(j, uDemo);
6998
+ // Nothing of a bubble reaches further than this from where it is now: its pop (up to 0.7 s
6999
+ // ago, so it has drifted at most this far since) throws droplets out to 3.6 radii + 0.1.
7000
+ float drift = 0.7 * (uRise + 0.085 * uWobble + 0.04);
7001
+ float far = max(uSize * 0.12, 0.01) * 3.6 + 0.1 + drift + 2.0 * px;
6995
7002
  for (int i = 0; i < 16; i++) {
6996
7003
  if (i >= uCount) break;
6997
7004
  float age;
6998
7005
  float life;
6999
7006
  vec4 b = bubbleAt(i, iTime, age, life);
7007
+ vec2 d0 = p - b.xy;
7008
+ if (dot(d0, d0) > far * far) continue;
7000
7009
  float birth = iTime - age;
7001
7010
  // It pops when its life is up, or at the first click that lands on it.
7002
7011
  float popAt = birth + life;
7003
7012
  for (int j = 0; j < 8; j++) {
7004
- vec4 c = clickAt(j, uDemo);
7013
+ vec4 c = clicks[j];
7005
7014
  if (c.w < 0.5 || c.z < birth || c.z > iTime || c.z >= popAt) continue;
7006
7015
  float a2;
7007
7016
  float l2;
@@ -8240,10 +8249,36 @@ uniform float uClump; // @control min=0 max=1 default=0.5 label="Clumping"
8240
8249
  uniform float uPart; // @control min=0 max=1 default=0.8 label="Parting"`,
8241
8250
  HASH,
8242
8251
  NOISE,
8243
- `vec2 strandDir(vec2 root, vec2 ptr, float hov) {
8244
- float a = radians(uLie) + (vnoise(root * 0.07) - 0.5) * 1.6 * uClump;
8245
- a += uSway * 0.7 * sin(iTime * 1.3 + root.x * 0.05 + root.y * 0.03 + 2.0 * vnoise(root * 0.02 + iTime * 0.15));
8252
+ `// The two slow noise fields that turn the strands, hashed once per pixel on a
8253
+ // 3 x 3 lattice that covers every root the pixel looks at.
8254
+ mat3 clumpGrid;
8255
+ vec2 clumpBase;
8256
+ mat3 swayGrid;
8257
+ vec2 swayBase;
8258
+
8259
+ mat3 noiseGrid(vec2 base) {
8260
+ mat3 g;
8261
+ for (int x = 0; x < 3; x++) {
8262
+ for (int y = 0; y < 3; y++) g[x][y] = hash12(base + vec2(float(x), float(y)));
8263
+ }
8264
+ return g;
8265
+ }
8266
+
8267
+ // vnoise(q), with its corners read from a lattice hashed at base.
8268
+ float gridNoise(vec2 q, vec2 base, mat3 g) {
8269
+ vec2 f = fract(q);
8270
+ vec2 u = f * f * (3.0 - 2.0 * f);
8271
+ ivec2 k = clamp(ivec2(floor(q) - base), 0, 1);
8272
+ vec3 c0 = g[k.x];
8273
+ vec3 c1 = g[k.x + 1];
8274
+ return mix(mix(c0[k.y], c1[k.y], u.x), mix(c0[k.y + 1], c1[k.y + 1], u.x), u.y);
8275
+ }
8276
+
8277
+ vec2 strandDir(vec2 root, vec2 ptr, float hov) {
8278
+ float a = radians(uLie) + (gridNoise(root * 0.07, clumpBase, clumpGrid) - 0.5) * 1.6 * uClump;
8279
+ a += uSway * 0.7 * sin(iTime * 1.3 + root.x * 0.05 + root.y * 0.03 + 2.0 * gridNoise(root * 0.02 + iTime * 0.15, swayBase, swayGrid));
8246
8280
  vec2 dir = vec2(cos(a), sin(a));
8281
+ if (uPart * hov <= 0.0) return dir;
8247
8282
  vec2 away = root - ptr;
8248
8283
  float reach = 0.16 * uDensity;
8249
8284
  float f = uPart * hov * exp(-dot(away, away) / (reach * reach));
@@ -8254,23 +8289,93 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
8254
8289
  vec2 p = fragCoord / iResolution.y * uDensity;
8255
8290
  vec2 ptr = iPointer.xy / iResolution.y * uDensity;
8256
8291
  float hov = clamp(iPointer.z, 0.0, 1.0);
8292
+ // Every root looked at below lies within 7 cells of p.
8293
+ clumpBase = floor((p - 7.0) * 0.07);
8294
+ clumpGrid = noiseGrid(clumpBase);
8295
+ swayBase = floor((p - 7.0) * 0.02 + iTime * 0.15);
8296
+ swayGrid = noiseGrid(swayBase);
8257
8297
  vec2 here = strandDir(p, ptr, hov);
8258
8298
  float top = -1.0;
8259
8299
  float across = 0.0;
8260
8300
  float tint = 0.0;
8261
8301
  vec2 topDir = here;
8302
+ // Clumping and breeze turn a strand by at most 0.8 * uClump and 0.7 * uSway, and
8303
+ // both fields are smooth, so strands rooted within reach of this pixel point
8304
+ // inside a cone around its own direction. Roots whose strands can't reach the
8305
+ // pixel, or can't rise above the strand already found, are skipped before their
8306
+ // directions are worked out. Roots are visited upstream first, so the topmost
8307
+ // strand tends to be found early and the rest skipped. (Hovering parts the fur
8308
+ // near the pointer in any direction, so the cone isn't used there.)
8309
+ float lie = radians(uLie);
8310
+ float R = 1.25 * uLength + uWidth;
8311
+ float cp = (gridNoise(p * 0.07, clumpBase, clumpGrid) - 0.5) * 1.6 * uClump;
8312
+ float sp = uSway * 0.7 * sin(iTime * 1.3 + p.x * 0.05 + p.y * 0.03 + 2.0 * gridNoise(p * 0.02 + iTime * 0.15, swayBase, swayGrid));
8313
+ // Bounds on each field's slope: 0.2376 * uClump and 0.10024 * uSway per cell.
8314
+ float dc = 0.2376 * uClump * R;
8315
+ float ds = 0.10024 * uSway * R;
8316
+ vec2 lo = vec2(max(-0.8 * uClump, cp - dc), max(-0.7 * uSway, sp - ds));
8317
+ vec2 hi = vec2(min(0.8 * uClump, cp + dc), min(0.7 * uSway, sp + ds));
8318
+ float mid = lie + 0.5 * (lo.x + hi.x + lo.y + hi.y);
8319
+ // At most 1.54, under a quarter turn.
8320
+ float cone = 0.5 * (hi.x - lo.x + hi.y - lo.y) + 0.04;
8321
+ vec2 axis = vec2(cos(mid), sin(mid));
8322
+ float cosCone = cos(cone);
8323
+ float sinCone = sin(cone);
8324
+ float reach = 0.16 * uDensity;
8325
+ bool parting = uPart * hov > 0.0;
8326
+ // The box roots that reach this pixel lie in, relative to it: behind it within
8327
+ // the cone (anywhere within reach where the pointer may be parting the fur).
8328
+ float back = 1.25 * uLength;
8329
+ vec2 e1 = -back * vec2(cos(mid + cone), sin(mid + cone));
8330
+ vec2 e2 = -back * vec2(cos(mid - cone), sin(mid - cone));
8331
+ vec2 boxLo = min(vec2(0.0), min(e1, e2));
8332
+ vec2 boxHi = max(vec2(0.0), max(e1, e2));
8333
+ if (-axis.x >= cosCone) boxHi.x = back;
8334
+ if (axis.x >= cosCone) boxLo.x = -back;
8335
+ if (-axis.y >= cosCone) boxHi.y = back;
8336
+ if (axis.y >= cosCone) boxLo.y = -back;
8337
+ float ptrGap = max(length(p - ptr) - R, 0.0);
8338
+ if (uPart * hov * exp(-ptrGap * ptrGap / (reach * reach)) * 1.4 > 0.02) {
8339
+ boxLo = vec2(-back);
8340
+ boxHi = vec2(back);
8341
+ }
8342
+ boxLo += p - uWidth;
8343
+ boxHi += p + uWidth;
8344
+ bvec2 up = lessThan(here, vec2(0.0));
8262
8345
  for (int layer = 0; layer < 3; layer++) {
8263
8346
  float fl = float(layer);
8264
8347
  vec2 off = vec2(0.37, 0.71) * fl;
8348
+ // Each layer's 5 x 5 cells around the middle of the strands through here,
8349
+ // cut to those whose roots (in [id + off, id + off + 1)) can lie in the box.
8265
8350
  vec2 base = floor(p - here * uLength * 0.5 - off);
8266
- for (int j = -2; j <= 2; j++) {
8267
- for (int i = -2; i <= 2; i++) {
8351
+ ivec2 first = ivec2(max(ceil(boxLo - off - 1.0), base - 2.0) - base);
8352
+ ivec2 last = ivec2(min(floor(boxHi - off), base + 2.0) - base);
8353
+ for (int row = 0; row < 5; row++) {
8354
+ if (row > last.y - first.y) break;
8355
+ int j = up.y ? last.y - row : first.y + row;
8356
+ for (int column = 0; column < 5; column++) {
8357
+ if (column > last.x - first.x) break;
8358
+ int i = up.x ? last.x - column : first.x + column;
8268
8359
  vec2 id = base + vec2(float(i), float(j));
8269
8360
  vec2 h = hash22(id + fl * 31.7);
8270
8361
  vec2 root = id + off + h;
8271
- vec2 dir = strandDir(root, ptr, hov);
8272
8362
  float len = uLength * (0.75 + 0.5 * h.y);
8273
8363
  vec2 rp = p - root;
8364
+ float r2 = dot(rp, rp);
8365
+ // Too far away, or can't rise above the strand already found (along <= r / len).
8366
+ float reachLen = len + uWidth;
8367
+ if (r2 >= reachLen * reachLen || top >= 1.0 || (top >= 0.0 && r2 <= top * top * len * len)) continue;
8368
+ bool parted = false;
8369
+ if (parting) {
8370
+ vec2 away = root - ptr;
8371
+ parted = uPart * hov * exp(-dot(away, away) / (reach * reach)) * 1.4 > 0.02;
8372
+ }
8373
+ if (r2 > uWidth * uWidth && !parted) {
8374
+ // Off the cone widened by the strand's width: it can't cover this pixel.
8375
+ float da = dot(rp, axis);
8376
+ if (da < cosCone * sqrt(r2 - uWidth * uWidth) - sinCone * uWidth) continue;
8377
+ }
8378
+ vec2 dir = strandDir(root, ptr, hov);
8274
8379
  float along = clamp(dot(rp, dir) / len, 0.0, 1.0);
8275
8380
  float w = uWidth * (1.0 - 0.75 * along);
8276
8381
  float dist = length(rp - dir * along * len);
@@ -9465,29 +9570,61 @@ float poolY(float x, float m) {
9465
9570
  return m * (0.1 + 0.012 * sin(x * 9.0 + iTime * 0.7854) + 0.025 * vnoise(vec2(x * 6.0, 2.0))) - 0.006;
9466
9571
  }
9467
9572
 
9468
- // Distance to the liquid (negative inside): a lip along the slumped edge, drips hanging from it, the puddle.
9469
- float liquid(vec2 uv, float m) {
9573
+ vec4 smin4(vec4 a, vec4 b, float k) {
9574
+ k = max(k, 1e-4);
9575
+ vec4 h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
9576
+ return mix(b, a, h) - k * h * (1.0 - h);
9577
+ }
9578
+
9579
+ // Distances to the liquid (negative inside) at four nearby points (xs, ys) at
9580
+ // once: a lip along the slumped edge, drips hanging from it, the puddle. Each
9581
+ // point sees the drips of the five columns around its own; those columns'
9582
+ // drips are worked out once for all four. edge and pool are edgeY and poolY
9583
+ // at the points; a point's distance only matters below its entry in matter.
9584
+ vec4 liquid(vec4 xs, vec4 ys, vec4 edge, vec4 pool, float m, vec4 matter) {
9470
9585
  float n = float(uDrips);
9471
9586
  float grow = smoothstep(0.0, 0.2, m);
9472
- float e = edgeY(uv.x, m) + 0.01 * (1.0 - grow);
9587
+ vec4 e = edge + 0.01 * (1.0 - grow);
9473
9588
  float lip = 0.022 * grow;
9474
- float d = max(uv.y - e, e - lip - uv.y);
9475
- float c = floor(uv.x * n);
9476
- for (int k = -2; k <= 2; k++) {
9589
+ vec4 d = max(ys - e, e - lip - ys);
9590
+ vec4 cs = floor(xs * n);
9591
+ float c = cs.y;
9592
+ // Where any drip's top and head can be, from the bounds of edgeY and poolY.
9593
+ float topMin = 1.0 - m * 0.34 - lip * 0.5;
9594
+ float topMax = 1.0 - m * 0.04 - lip * 0.5;
9595
+ float poolMin = m * 0.088 - 0.006;
9596
+ // A drip farther than this beyond where a distance matters can't change it
9597
+ // there: the blends that follow can pull a distance down by at most this much.
9598
+ vec4 reach = matter + 0.16;
9599
+ for (int k = -3; k <= 3; k++) {
9477
9600
  float ci = c + float(k);
9601
+ bvec4 near = lessThanEqual(abs(cs - ci), vec4(2.0));
9602
+ if (!any(near)) continue;
9478
9603
  vec2 h = hash22(vec2(ci, 3.1));
9479
9604
  float xi = (ci + 0.2 + 0.6 * h.x) / n;
9480
9605
  float w = min(uViscosity * (0.7 + 0.6 * hash12(vec2(ci, 7.7))), 0.6 * aspect / n) * grow;
9606
+ // The nearest this drip can be to each point, before working out its shape:
9607
+ // skip it if it can't change any of them.
9608
+ float f = pow(m, 1.4) * min(0.2 + 0.95 * h.y, 1.0);
9609
+ float headMin = min(topMin, topMin * (1.0 - f) + f * poolMin);
9610
+ vec4 gap = vec4(abs(xs - xi) * aspect);
9611
+ vec4 rise = max(max(headMin - ys, ys - topMax), 0.0);
9612
+ vec4 nearest = sqrt(gap * gap + rise * rise) - 1.3 * w;
9613
+ bvec4 counts = bvec4(vec4(near) * step(nearest, d + 0.012 * grow) * step(nearest, reach));
9614
+ if (!any(counts)) continue;
9481
9615
  float top = edgeY(xi, m) - lip * 0.5;
9482
- float len = pow(m, 1.4) * min(0.2 + 0.95 * h.y, 1.0) * max(top - poolY(xi, m), 0.0);
9616
+ float len = f * max(top - poolY(xi, m), 0.0);
9483
9617
  float head = top - len;
9484
- float along = clamp(uv.y, head, top);
9618
+ vec4 along = clamp(ys, head, top);
9485
9619
  // A stem thinning as it runs, then a round, heavier head.
9486
- float r = w * mix(0.85, 0.6, smoothstep(top, top - 0.3, uv.y));
9487
- r = mix(r, w * 1.3, smoothstep(head + 3.0 * w, head, uv.y));
9488
- d = smin(d, length(vec2((uv.x - xi) * aspect, uv.y - along)) - r, 0.012 * grow);
9620
+ vec4 r = w * mix(vec4(0.85), vec4(0.6), smoothstep(top, top - 0.3, ys));
9621
+ r = mix(r, vec4(w * 1.3), smoothstep(head + 3.0 * w, head, ys));
9622
+ vec4 dx = (xs - xi) * aspect;
9623
+ vec4 dy = ys - along;
9624
+ vec4 dd = smin4(d, sqrt(dx * dx + dy * dy) - r, 0.012 * grow);
9625
+ d = mix(d, dd, near);
9489
9626
  }
9490
- return smin(d, uv.y - poolY(uv.x, m), 0.03 * grow);
9627
+ return smin4(d, ys - pool, 0.03 * grow);
9491
9628
  }
9492
9629
 
9493
9630
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
@@ -9496,23 +9633,32 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
9496
9633
  float px = 1.0 / iResolution.y;
9497
9634
  float m = uAnimate ? meltCycle(uSpeed) : uProgress;
9498
9635
 
9636
+ // The liquid is sampled at four points: under the shadow, here, and a step
9637
+ // right and up for its normal. They share three columns of x.
9638
+ float eps = 0.0015;
9639
+ vec4 xs = uv.x + vec4(-0.004, 0.0, eps / aspect, 0.0);
9640
+ vec4 ys = uv.y + vec4(0.009, 0.0, 0.0, eps);
9641
+ vec3 edge3 = vec3(edgeY(xs.x, m), edgeY(xs.y, m), edgeY(xs.z, m));
9642
+ vec3 pool3 = vec3(poolY(xs.x, m), poolY(xs.y, m), poolY(xs.z, m));
9643
+ // The shadow shows below 0.012, the liquid below 4 px (its neighbours a little farther).
9644
+ vec4 liq = liquid(xs, ys, edge3.xyzy, pool3.xyzy, m, vec4(0.012, 4.0 * px, vec2(4.0 * px + 0.005)));
9645
+
9499
9646
  // The picture slumped under its edge: squashed towards the bottom, stretched thin at the top.
9500
- float e = edgeY(uv.x, m);
9647
+ float e = edge3.y;
9501
9648
  float h = clamp(uv.y / e, 0.0, 1.0);
9502
- float ys = 1.0 - pow(1.0 - h, 1.0 + m * 1.3);
9503
- float xs = uv.x + m * 0.006 * sin(uv.y * 40.0 + uv.x * 13.0);
9504
- vec4 col = pictureRGBA(uImage, vec2(xs, ys));
9649
+ float ys0 = 1.0 - pow(1.0 - h, 1.0 + m * 1.3);
9650
+ float xs0 = uv.x + m * 0.006 * sin(uv.y * 40.0 + uv.x * 13.0);
9651
+ vec4 col = pictureRGBA(uImage, vec2(xs0, ys0));
9505
9652
  vec4 behind = vec4(uBackground * (0.75 + 0.5 * uv.y), uBackdropAlpha);
9506
9653
  col = mix(col, behind, smoothstep(e - px, e + px, uv.y));
9507
9654
 
9508
9655
  // The liquid's shadow, down and to the right.
9509
- float shadow = liquid(uv + vec2(-0.004, 0.009), m);
9656
+ float shadow = liq.x;
9510
9657
  col.rgb *= 1.0 - 0.4 * smoothstep(0.012, -0.004, shadow) * smoothstep(0.0, 0.1, m);
9511
9658
 
9512
- float d = liquid(uv, m);
9659
+ float d = liq.y;
9513
9660
  if (d < 4.0 * px) {
9514
- float eps = 0.0015;
9515
- vec2 g = vec2(liquid(uv + vec2(eps / aspect, 0.0), m) - d, liquid(uv + vec2(0.0, eps), m) - d);
9661
+ vec2 g = vec2(liq.z - d, liq.w - d);
9516
9662
  g /= max(length(g), 1e-5);
9517
9663
  float thick = sqrt(clamp(-d / 0.018, 0.0, 1.0));
9518
9664
  vec3 nrm = normalize(vec3(g * (1.0 - thick) * 1.6, 0.4 + thick));
@@ -9520,7 +9666,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
9520
9666
  vec4 top = pictureRGBA(uImage, vec2(uv.x + 0.01 * sin(uv.y * 25.0), 0.965));
9521
9667
  float sx = uv.x + 0.04 * sin(uv.x * 11.0 + uv.y * 30.0);
9522
9668
  vec4 pool = (pictureRGBA(uImage, vec2(sx - 0.06, 0.25)) + pictureRGBA(uImage, vec2(sx + 0.06, 0.4)) + pictureRGBA(uImage, vec2(sx, 0.9))) / 3.0;
9523
- float inPool = smoothstep(poolY(uv.x, m) + 0.04, poolY(uv.x, m) - 0.01, uv.y);
9669
+ float inPool = smoothstep(pool3.y + 0.04, pool3.y - 0.01, uv.y);
9524
9670
  vec4 base = mix(top, pool, inPool);
9525
9671
  base.rgb *= 0.9 + 0.12 * vnoise(vec2(uv.x * 90.0, uv.y * 3.0));
9526
9672
  vec3 wet = liquidShade(base.rgb * 1.05, nrm, uGloss);
@@ -10449,7 +10595,18 @@ uniform float uIntensity; // @control min=0.2 max=3 default=1 label="Intensity"`
10449
10595
  float w = max(0.0022, px);
10450
10596
  float k = 1.4;
10451
10597
  float g = 1.5;
10598
+ // Past this distance a streak's glow is far below one level, so sparks farther
10599
+ // away are skipped, and so is the whole loop outside the fountain's reach.
10600
+ float margin = 30.0 * w;
10601
+ float vmax = 1.35 * uHeight * 1.25;
10602
+ float xmax = vmax * sin(min(uSpread * 1.2, 1.5708)) * (1.0 - exp(-k * 1.8)) / k;
10603
+ float apex = log(1.0 + vmax * k / g) / k;
10604
+ float ea = (1.0 - exp(-k * apex)) / k;
10605
+ float ymax = vmax * ea - g / k * (apex - ea);
10606
+ vec2 rel = p - base;
10607
+ bool inReach = abs(rel.x) < xmax + margin && rel.y > -0.01 - margin && rel.y < ymax + margin;
10452
10608
  for (int j = 0; j < 112; j++) {
10609
+ if (!inReach) break;
10453
10610
  float fj = float(j);
10454
10611
  vec2 h = hash22(vec2(fj, 2.31));
10455
10612
  float life = 1.0 + 0.8 * h.y;
@@ -10459,11 +10616,14 @@ uniform float uIntensity; // @control min=0.2 max=3 default=1 label="Intensity"`
10459
10616
  vec3 s = vec3(hash22(vec2(fj * 1.7, n)), hash12(vec2(n, fj + 0.37)));
10460
10617
  if (s.z > uDensity) continue;
10461
10618
  float a = (s.x - 0.5) * 2.0 * uSpread * 1.2;
10619
+ // A spark only ever moves to its own side of the base.
10620
+ if (a * rel.x < 0.0 && abs(rel.x) > margin) continue;
10462
10621
  vec2 v = vec2(sin(a), cos(a)) * mix(0.9, 1.35, s.y) * uHeight * 1.25;
10463
10622
  vec2 head = base + ballistic(v, k, g, age);
10464
10623
  vec2 tail = base + ballistic(v, k, g, max(age - 0.07, 0.0));
10465
10624
  // Sparks that fall back past the base are gone.
10466
10625
  if (head.y < base.y - 0.01) continue;
10626
+ if (any(lessThan(p, min(head, tail) - margin)) || any(greaterThan(p, max(head, tail) + margin))) continue;
10467
10627
  float fade = 1.0 - smoothstep(0.5, 1.0, age / life);
10468
10628
  float twinkle = 0.6 + 0.4 * hash12(vec2(fj, floor(iTime * 24.0)));
10469
10629
  vec3 tint = mix(vec3(1.0, 0.96, 0.85), uColor, smoothstep(0.0, 0.5, age));
@@ -11708,15 +11868,15 @@ ${BACKDROP_CONTROLS}`,
11708
11868
  ROTATE,
11709
11869
  PICTURE,
11710
11870
  COMMON,
11711
- `const vec2 RING[4] = vec2[4](vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 1.0));
11712
-
11713
- // Where cell c's particle lands: cells trade places around each 2 \xD7 2 block, turned 1\u20133 steps.
11871
+ `// Where cell c's particle lands: cells trade places around each 2 \xD7 2 block, turned 1\u20133 steps.
11872
+ // Round the block the corners go (0, 0), (1, 0), (1, 1), (0, 1); worked out rather than looked up in an array.
11714
11873
  vec2 landing(vec2 c) {
11715
11874
  vec2 block = floor(c * 0.5);
11716
11875
  vec2 local = c - block * 2.0;
11717
- int i = local.y < 0.5 ? int(local.x) : 3 - int(local.x);
11718
- int turn = 1 + int(hash12(block + 7.3) * 2.999);
11719
- return block * 2.0 + RING[(i + turn) % 4];
11876
+ float i = local.y < 0.5 ? local.x : 3.0 - local.x;
11877
+ float turn = 1.0 + floor(hash12(block + 7.3) * 2.999);
11878
+ float r = mod(i + turn, 4.0);
11879
+ return block * 2.0 + vec2(r > 0.5 && r < 2.5 ? 1.0 : 0.0, r > 1.5 ? 1.0 : 0.0);
11720
11880
  }
11721
11881
 
11722
11882
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
@@ -11730,26 +11890,43 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
11730
11890
  vec2 hitTo = vec2(0.0);
11731
11891
  float hitT = 0.0;
11732
11892
  float hitCover = 0.0;
11733
- // Particles travel at most about 1.5 cells, so the 5 \xD7 5 cells around are every candidate.
11734
- for (int j = -2; j <= 2; j++) {
11735
- for (int i = -2; i <= 2; i++) {
11736
- vec2 c = base + vec2(float(i), float(j));
11893
+ // How far a particle's corners stray from the straight line between its two places: the curl, then its turned corners.
11894
+ float reach = max(0.7072, 0.6 * abs(uScatter + uSpin * 0.3) + 0.4951) + 0.01;
11895
+ // Particles travel at most about 1.5 cells, so the 5 \xD7 5 cells around are every candidate;
11896
+ // of those, only the cells within reach (one cell either side of the block) need looking at.
11897
+ vec2 lo = max(base - 2.0, ceil(p - 1.5 - reach));
11898
+ vec2 hi = min(base + 2.0, floor(p + 0.5 + reach));
11899
+ for (int j = 0; j < 5; j++) {
11900
+ if (lo.y + float(j) > hi.y) break;
11901
+ for (int i = 0; i < 5; i++) {
11902
+ if (lo.x + float(i) > hi.x) break;
11903
+ vec2 c = lo + vec2(float(i), float(j));
11904
+ // It stays in its 2 \xD7 2 block, so skip it if this pixel is out of its reach from there.
11905
+ vec2 corner = c - 2.0 * floor(c * 0.5);
11906
+ if (any(lessThan(p, c + 0.5 - corner - reach)) || any(greaterThan(p, c + 1.5 - corner + reach))) continue;
11737
11907
  vec2 h = hash22(c);
11738
11908
  // Staggered: a sweep from the left plus some randomness.
11739
11909
  float delay = 0.5 * (0.55 * clamp(c.x / grid.x, 0.0, 1.0) + 0.45 * h.x);
11740
11910
  float k = smoothstep(0.0, 1.0, clamp((t - delay) / 0.5, 0.0, 1.0));
11741
11911
  float lift = sin(3.14159 * k);
11912
+ float z = h.y + lift;
11913
+ // Behind what is already here, or (now its lift is known) out of reach: skip it before working out where it is.
11914
+ if (z <= top) continue;
11915
+ float near = 0.6 * abs(uScatter + uSpin * 0.3) * lift + 0.7072 * (1.0 - 0.3 * lift) + 0.01;
11916
+ if (any(lessThan(p, c + 0.5 - corner - near)) || any(greaterThan(p, c + 1.5 - corner + near))) continue;
11742
11917
  vec2 from = c + 0.5;
11743
11918
  vec2 to = landing(c) + 0.5;
11744
11919
  vec2 path = to - from;
11745
11920
  vec2 side = normalize(vec2(-path.y, path.x) + (h - 0.5) * 0.8);
11746
11921
  vec2 pos = mix(from, to, k) + side * lift * (uScatter + uSpin * 0.3) * (h.y > 0.5 ? 0.6 : -0.6);
11747
11922
  float size = 1.0 - 0.3 * lift;
11923
+ // Out of reach of its corners, however it is turned.
11924
+ vec2 rel = p - pos;
11925
+ if (dot(rel, rel) > 0.501 * size * size) continue;
11748
11926
  float angle = (h.y - 0.5) * 6.28318 * uSpin * lift;
11749
- vec2 l = rot(-angle) * (p - pos) / size;
11927
+ vec2 l = rot(-angle) * rel / size;
11750
11928
  float m = max(abs(l.x), abs(l.y));
11751
- float z = h.y + lift;
11752
- if (m <= 0.5 && z > top) {
11929
+ if (m <= 0.5) {
11753
11930
  top = z;
11754
11931
  hitFrom = from + l;
11755
11932
  hitTo = to + l;
@@ -12688,6 +12865,24 @@ ${BACKDROP_CONTROLS}`,
12688
12865
  return cell + 0.15 + 0.7 * hash22(cell);
12689
12866
  }
12690
12867
 
12868
+ // No point is further than this from its own shard's site (sites sit in the middle 70% of each cell).
12869
+ const float SHARD_REACH = 1.25;
12870
+
12871
+ // Whether grid point q lies on shard c: c's site is the nearest of those in the 3 \xD7 3 cells around q.
12872
+ bool onShard(vec2 q, vec2 c) {
12873
+ vec2 base = floor(q);
12874
+ vec2 off = c - base;
12875
+ if (abs(off.x) > 1.0 || abs(off.y) > 1.0) return false;
12876
+ float own = distance(q, shardSite(c));
12877
+ for (int j = -1; j <= 1; j++) {
12878
+ for (int i = -1; i <= 1; i++) {
12879
+ vec2 n = base + vec2(float(i), float(j));
12880
+ if (n != c && distance(q, shardSite(n)) < own) return false;
12881
+ }
12882
+ }
12883
+ return true;
12884
+ }
12885
+
12691
12886
  // The shard (cell) under grid point q, and how far q is from its edge (xy: cell, z: edge gap).
12692
12887
  vec3 shardAt(vec2 q) {
12693
12888
  vec2 base = floor(q);
@@ -12716,28 +12911,42 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
12716
12911
  // Shard centres move out from the middle; shards keep their size, so gaps open between them.
12717
12912
  float grow = 1.0 + uSpread * k;
12718
12913
  vec2 base = floor(p / grow);
12719
- vec4 col = vec4(uBackground, uBackdropAlpha);
12720
12914
  float top = -1.0;
12721
- for (int j = -2; j <= 2; j++) {
12722
- for (int i = -2; i <= 2; i++) {
12723
- vec2 c = base + vec2(float(i), float(j));
12724
- vec2 site = shardSite(c);
12915
+ vec2 hitQ = vec2(0.0);
12916
+ float hitA = 0.0;
12917
+ // Every shard that could reach this pixel (of the 5 \xD7 5 cells around, those whose site, wherever it sits
12918
+ // in its cell and however far it is thrown, lands close enough); the one on top (highest h.y) wins.
12919
+ float slack = 0.6 * k + SHARD_REACH;
12920
+ vec2 lo = max(base - 2.0, ceil((p - slack) / grow - 0.85));
12921
+ vec2 hi = min(base + 2.0, floor((p + slack) / grow - 0.15));
12922
+ for (int j = 0; j < 5; j++) {
12923
+ if (lo.y + float(j) > hi.y) break;
12924
+ for (int i = 0; i < 5; i++) {
12925
+ if (lo.x + float(i) > hi.x) break;
12926
+ vec2 c = lo + vec2(float(i), float(j));
12725
12927
  vec2 h = hash22(c + 41.7);
12928
+ if (h.y <= top) continue;
12929
+ vec2 site = shardSite(c);
12726
12930
  vec2 centre = site * grow + (h - 0.5) * 1.2 * k;
12931
+ vec2 rel = p - centre;
12932
+ if (dot(rel, rel) > SHARD_REACH * SHARD_REACH) continue;
12727
12933
  float a = (h.x - 0.5) * 2.0 * uSpin * k;
12728
12934
  // Back to where this pixel would sit in the unbroken image, if it is on this shard.
12729
- vec2 q = rot(-a) * (p - centre) + site;
12730
- vec3 s = shardAt(q);
12731
- if (s.x == c.x && s.y == c.y && h.y > top) {
12935
+ vec2 q = rot(-a) * rel + site;
12936
+ if (onShard(q, c)) {
12732
12937
  top = h.y;
12733
- vec2 uv = q / uCount * iResolution.y / iResolution.xy + 0.5;
12734
- vec4 shard = pictureRGBA(uImage, uv);
12735
- shard.rgb *= 1.0 - 0.35 * k * abs(sin(a));
12736
- shard.rgb += k * 0.35 * smoothstep(0.06, 0.0, s.z);
12737
- col = shard;
12938
+ hitQ = q;
12939
+ hitA = a;
12738
12940
  }
12739
12941
  }
12740
12942
  }
12943
+ vec4 col = vec4(uBackground, uBackdropAlpha);
12944
+ if (top > -1.0) {
12945
+ vec2 uv = hitQ / uCount * iResolution.y / iResolution.xy + 0.5;
12946
+ col = pictureRGBA(uImage, uv);
12947
+ col.rgb *= 1.0 - 0.35 * k * abs(sin(hitA));
12948
+ col.rgb += k * 0.35 * smoothstep(0.06, 0.0, shardAt(hitQ).z);
12949
+ }
12741
12950
  fragColor = col;
12742
12951
  }`
12743
12952
  ),
@@ -13182,31 +13391,48 @@ uniform float uSharpness; // @control min=0.5 max=8 default=4 label="Sharpness"
13182
13391
  uniform float uSaturation; // @control min=0 max=2 default=1.15 label="Saturation"
13183
13392
  ${BACKDROP_CONTROLS}`,
13184
13393
  PICTURE,
13185
- `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
13394
+ `// Sums of colour and colour\xB2 over four overlapping squares (down-left, down-right, up-left, up-right),
13395
+ // each pixel read once: the shared middle row and column count towards both squares they border.
13396
+ // A macro so the image and the test card each get their own loop, rather than every read paying for both.
13397
+ #define GATHER(TAP) { \\
13398
+ vec3 c = TAP(uv); \\
13399
+ vec3 cc = c * c; \\
13400
+ m0 = c; m1 = c; m2 = c; m3 = c; \\
13401
+ s0 = cc; s1 = cc; s2 = cc; s3 = cc; \\
13402
+ for (int i = 1; i <= 8; i++) { \\
13403
+ if (i > uRadius) break; \\
13404
+ float o = float(i); \\
13405
+ c = TAP(uv - vec2(o * px.x, 0.0)); cc = c * c; m0 += c; s0 += cc; m2 += c; s2 += cc; \\
13406
+ c = TAP(uv + vec2(o * px.x, 0.0)); cc = c * c; m1 += c; s1 += cc; m3 += c; s3 += cc; \\
13407
+ c = TAP(uv - vec2(0.0, o * px.y)); cc = c * c; m0 += c; s0 += cc; m1 += c; s1 += cc; \\
13408
+ c = TAP(uv + vec2(0.0, o * px.y)); cc = c * c; m2 += c; s2 += cc; m3 += c; s3 += cc; \\
13409
+ } \\
13410
+ for (int j = 1; j <= 8; j++) { \\
13411
+ if (j > uRadius) break; \\
13412
+ for (int i = 1; i <= 8; i++) { \\
13413
+ if (i > uRadius) break; \\
13414
+ vec2 o = vec2(float(i), float(j)) * px; \\
13415
+ c = TAP(uv - o); m0 += c; s0 += c * c; \\
13416
+ c = TAP(uv + vec2(o.x, -o.y)); m1 += c; s1 += c * c; \\
13417
+ c = TAP(uv + vec2(-o.x, o.y)); m2 += c; s2 += c * c; \\
13418
+ c = TAP(uv + o); m3 += c; s3 += c * c; \\
13419
+ } \\
13420
+ } \\
13421
+ }
13422
+
13423
+ vec3 imageTap(vec2 uv) {
13424
+ return overBackdrop(texture(uImage, uv)).rgb;
13425
+ }
13426
+
13427
+ vec3 cardTap(vec2 uv) {
13428
+ return testCard(fract(uv));
13429
+ }
13430
+
13431
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
13186
13432
  vec2 uv = fragCoord / iResolution.xy;
13187
13433
  vec2 px = 1.0 / iResolution.xy;
13188
- // Sums of colour and colour\xB2 over four overlapping squares: down-left, down-right, up-left, up-right.
13189
- vec3 m0 = vec3(0.0), m1 = vec3(0.0), m2 = vec3(0.0), m3 = vec3(0.0);
13190
- vec3 s0 = vec3(0.0), s1 = vec3(0.0), s2 = vec3(0.0), s3 = vec3(0.0);
13191
- for (int j = 0; j <= 8; j++) {
13192
- if (j > uRadius) break;
13193
- for (int i = 0; i <= 8; i++) {
13194
- if (i > uRadius) break;
13195
- vec2 o = vec2(float(i), float(j)) * px;
13196
- vec3 c = picture(uImage, uv + vec2(-o.x, -o.y));
13197
- m0 += c;
13198
- s0 += c * c;
13199
- c = picture(uImage, uv + vec2(o.x, -o.y));
13200
- m1 += c;
13201
- s1 += c * c;
13202
- c = picture(uImage, uv + vec2(-o.x, o.y));
13203
- m2 += c;
13204
- s2 += c * c;
13205
- c = picture(uImage, uv + o);
13206
- m3 += c;
13207
- s3 += c * c;
13208
- }
13209
- }
13434
+ vec3 m0, m1, m2, m3, s0, s1, s2, s3;
13435
+ if (textureSize(uImage, 0).x > 1) GATHER(imageTap) else GATHER(cardTap)
13210
13436
  float n = float((uRadius + 1) * (uRadius + 1));
13211
13437
  m0 /= n; m1 /= n; m2 /= n; m3 /= n;
13212
13438
  // Weight each square by how calm (low-variance) it is; sharpness sets how strongly the calmest wins.
@@ -13244,7 +13470,7 @@ var imageTreatments = [
13244
13470
  ];
13245
13471
 
13246
13472
  // src/catalog/shaders/trail.ts
13247
- function trailHelper(dt, speed) {
13473
+ function trailHelper(dt, speed, walk = false) {
13248
13474
  return `
13249
13475
  const int TRAIL_POINTS = 32;
13250
13476
  vec2 demoPath(float t) {
@@ -13261,6 +13487,52 @@ vec4 trailPoint(int i, bool demo) {
13261
13487
  const float dt = ${dt.toFixed(5)};
13262
13488
  float t = i == 0 ? iTime : (floor(iTime / dt) + 1.0 - float(i)) * dt;
13263
13489
  return vec4(demoPath(t), t, 1.0);
13490
+ }${walk ? `
13491
+ ${trailWalk(dt, speed)}` : ""}`.trim();
13492
+ }
13493
+ function trailWalk(dt, speed) {
13494
+ const rates = [1, 2.3, 2, 3.1];
13495
+ const turn = rates.map((r) => r * dt * speed);
13496
+ const vec = (f) => `vec4(${turn.map((x) => f(x).toFixed(8)).join(", ")})`;
13497
+ return `
13498
+ bool trailLive(bool demo) {
13499
+ return !demo || (iTrail[0].w > 0.5 && iPointer.z > 0.0);
13500
+ }
13501
+ vec4 trailAt(int i, bool live) {
13502
+ if (live) return iTrail[i];
13503
+ const float dt = ${dt.toFixed(5)};
13504
+ float t = i == 0 ? iTime : (floor(iTime / dt) + 1.0 - float(i)) * dt;
13505
+ return vec4(demoPath(t), t, 1.0);
13506
+ }
13507
+ int gWalkI;
13508
+ float gWalkBase;
13509
+ vec4 gWalkCos;
13510
+ vec4 gWalkSin;
13511
+ // Starts a walk at point i (1 or more).
13512
+ void trailFrom(int i, bool live) {
13513
+ gWalkI = i;
13514
+ if (live) return;
13515
+ const float dt = ${dt.toFixed(5)};
13516
+ gWalkBase = floor(iTime / dt) + 1.0;
13517
+ float s = (gWalkBase - float(i)) * dt * ${speed.toFixed(3)};
13518
+ vec4 phase = vec4(s, s * 2.3 + 1.0, s * 2.0 + 0.4, s * 3.1);
13519
+ gWalkCos = cos(phase);
13520
+ gWalkSin = sin(phase);
13521
+ }
13522
+ // The walk's next point: as trailAt(i, live) for i = the start, then one more each call.
13523
+ vec4 trailNext(bool live) {
13524
+ int i = gWalkI;
13525
+ gWalkI++;
13526
+ if (live) return iTrail[min(i, TRAIL_POINTS - 1)];
13527
+ const float dt = ${dt.toFixed(5)};
13528
+ vec2 q = vec2(0.34 * gWalkSin.x + 0.05 * gWalkSin.y, 0.27 * gWalkSin.z + 0.04 * gWalkCos.w);
13529
+ vec4 point = vec4(iResolution.xy * (0.5 + q), (gWalkBase - float(i)) * dt, 1.0);
13530
+ const vec4 turnCos = ${vec(Math.cos)};
13531
+ const vec4 turnSin = ${vec(Math.sin)};
13532
+ vec4 c = gWalkCos * turnCos + gWalkSin * turnSin;
13533
+ gWalkSin = gWalkSin * turnCos - gWalkCos * turnSin;
13534
+ gWalkCos = c;
13535
+ return point;
13264
13536
  }`.trim();
13265
13537
  }
13266
13538
  var SEGMENT3 = `
@@ -13275,6 +13547,14 @@ vec2 catmull(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) {
13275
13547
  float t3 = t2 * t;
13276
13548
  return 0.5 * (2.0 * p1 + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (3.0 * p1 - p0 - 3.0 * p2 + p3) * t3);
13277
13549
  }`.trim();
13550
+ var CATMULL_BOX = `
13551
+ float trailBoxDist(vec2 p, vec2 p0, vec2 p1, vec2 p2, vec2 p3, float scale) {
13552
+ vec2 c1 = p1 + (p2 - p0) / 6.0;
13553
+ vec2 c2 = p2 - (p3 - p1) / 6.0;
13554
+ vec2 lo = min(min(p1, p2), min(c1, c2)) * scale;
13555
+ vec2 hi = max(max(p1, p2), max(c1, c2)) * scale;
13556
+ return length(max(max(lo - p, p - hi), 0.0));
13557
+ }`.trim();
13278
13558
  var GLOW5 = `
13279
13559
  vec4 glow(vec3 c) {
13280
13560
  c = 1.0 - exp(-max(c, 0.0));
@@ -13371,14 +13651,16 @@ uniform float uSpin; // @control min=0 max=10 default=2 label="Spin"
13371
13651
  uniform float uShine; // @control min=0 max=1 default=0.7 label="Shine"
13372
13652
  uniform float uLife; // @control min=0.1 max=3 default=1.1 label="Fade time"
13373
13653
  uniform bool uDemo; // @control default=true label="Demo when idle"`,
13374
- trailHelper(1 / 30, 1.5),
13654
+ trailHelper(1 / 30, 1.5, true),
13375
13655
  SEGMENT3,
13376
13656
  CATMULL,
13657
+ CATMULL_BOX,
13377
13658
  `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
13378
13659
  float px = 1.0 / iResolution.y;
13379
13660
  vec2 p = fragCoord * px;
13380
- vec4 a = trailPoint(0, uDemo);
13381
- vec4 b = trailPoint(1, uDemo);
13661
+ bool live = trailLive(uDemo);
13662
+ vec4 a = trailAt(0, live);
13663
+ vec4 b = trailAt(1, live);
13382
13664
  if (a.w < 0.5 || b.w < 0.5) {
13383
13665
  fragColor = vec4(0.0);
13384
13666
  return;
@@ -13389,14 +13671,16 @@ uniform bool uDemo; // @control default=true label="Demo when idle"`,
13389
13671
  float bestFace = 1.0;
13390
13672
  float bestV = 0.0;
13391
13673
  float bestFade = 0.0;
13674
+ trailFrom(2, live);
13392
13675
  for (int k = 0; k < TRAIL_POINTS - 1; k++) {
13393
- b = trailPoint(k + 1, uDemo);
13394
13676
  if (b.w < 0.5 || trailJump(a, b)) break;
13395
- vec4 c = k + 2 < TRAIL_POINTS ? trailPoint(min(k + 2, TRAIL_POINTS - 1), uDemo) : vec4(0.0);
13677
+ // The point after b: read once, it is the next segment's b.
13678
+ vec4 c = k + 2 < TRAIL_POINTS ? trailNext(live) : vec4(0.0);
13396
13679
  vec2 next = c.w > 0.5 && !trailJump(b, c) ? c.xy : 2.0 * b.xy - a.xy;
13397
13680
  vec2 lo = min(min(prev, a.xy), min(b.xy, next)) * px - uWidth - 2.0 * px;
13398
13681
  vec2 hi = max(max(prev, a.xy), max(b.xy, next)) * px + uWidth + 2.0 * px;
13399
- if (all(greaterThan(p, lo)) && all(lessThan(p, hi))) {
13682
+ // The ribbon is at most half its width either side of the spline: further off than that, a segment cannot be nearer.
13683
+ if (all(greaterThan(p, lo)) && all(lessThan(p, hi)) && trailBoxDist(p, prev, a.xy, b.xy, next, px) - uWidth * 0.5 < best) {
13400
13684
  vec2 q0 = a.xy * px;
13401
13685
  for (int j = 1; j <= 4; j++) {
13402
13686
  float t1 = float(j) / 4.0;
@@ -13422,6 +13706,7 @@ uniform bool uDemo; // @control default=true label="Demo when idle"`,
13422
13706
  }
13423
13707
  prev = a.xy;
13424
13708
  a = b;
13709
+ b = c;
13425
13710
  }
13426
13711
  float cover = (1.0 - smoothstep(-px, px, best)) * smoothstep(0.0, 0.15, bestFade);
13427
13712
  if (cover <= 0.0) {
@@ -13466,21 +13751,31 @@ uniform float uTwinkle; // @control min=0 max=1 default=0.7 label="Twinkle"
13466
13751
  uniform float uIntensity; // @control min=0.2 max=3 default=1.2 label="Intensity"
13467
13752
  uniform bool uDemo; // @control default=true label="Demo when idle"`,
13468
13753
  HASH,
13469
- trailHelper(1 / 40, 1.4),
13754
+ trailHelper(1 / 40, 1.4, true),
13470
13755
  GLOW5,
13471
13756
  `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
13472
13757
  float px = 1.0 / iResolution.y;
13473
13758
  vec2 p = fragCoord * px;
13474
13759
  vec3 col = vec3(0.0);
13760
+ bool live = trailLive(uDemo);
13761
+ // How far a sparkle's light reaches from where it is (twice its largest size), and its sideways sway.
13762
+ float reach = 2.5 * uSize + px;
13763
+ float side = uSpread + 0.012 + reach;
13475
13764
  // Point 0 is still moving with the pointer, so sparkles come from the settled points after it.
13765
+ trailFrom(1, live);
13476
13766
  for (int k = 1; k < TRAIL_POINTS; k++) {
13477
- vec4 q = trailPoint(k, uDemo);
13767
+ vec4 q = trailNext(live);
13478
13768
  if (q.w < 0.5) break;
13479
13769
  float age = iTime - q.z;
13480
13770
  if (age < 0.0 || age > uLife) continue;
13771
+ // Skip points none of whose sparkles (spread, swaying and drifting down) can reach this pixel.
13772
+ vec2 rel = p - q.xy * px;
13773
+ float fall = uFall * age * (0.4 + 0.6 * age);
13774
+ if (abs(rel.x) > side || rel.y > uSpread + reach || rel.y < -uSpread - fall - reach) continue;
13481
13775
  float life = age / uLife;
13482
13776
  // Points drop off the end of the trail: fade those about to go.
13483
13777
  float keep = 1.0 - smoothstep(float(TRAIL_POINTS) - 6.0, float(TRAIL_POINTS) - 1.0, float(k));
13778
+ float env = smoothstep(0.0, 0.08, life) * (1.0 - life) * (1.0 - life) * keep;
13484
13779
  for (int j = 0; j < 4; j++) {
13485
13780
  if (j >= uCount) break;
13486
13781
  vec2 h = hash22(vec2(q.z * 61.37 + float(j) * 7.13, float(j) + 0.5));
@@ -13493,13 +13788,12 @@ uniform bool uDemo; // @control default=true label="Demo when idle"`,
13493
13788
  // Turn some of them a little.
13494
13789
  float an = (h.y - 0.5) * 0.9;
13495
13790
  d = mat2(cos(an), sin(an), -sin(an), cos(an)) * d;
13496
- float env = smoothstep(0.0, 0.08, life) * (1.0 - life) * (1.0 - life);
13497
13791
  float tw = mix(1.0, 0.35 + 0.65 * (0.5 + 0.5 * sin(age * (12.0 + 12.0 * h.y) + h.x * 6.28318)), uTwinkle);
13498
13792
  float rays = exp(-abs(d.x) / (s * 0.05)) * exp(-abs(d.y) / (s * 0.45));
13499
13793
  rays += exp(-abs(d.y) / (s * 0.05)) * exp(-abs(d.x) / (s * 0.45));
13500
13794
  float core = exp(-dot(d, d) / (s * s * 0.02));
13501
13795
  vec3 tint = h3 < 0.5 ? uColorA : uColorB;
13502
- col += (tint * (rays * 0.9 + core * 0.6) + vec3(1.0) * core) * env * tw * keep;
13796
+ col += (tint * (rays * 0.9 + core * 0.6) + vec3(1.0) * core) * env * tw;
13503
13797
  }
13504
13798
  }
13505
13799
  fragColor = glow(col * uIntensity);
@@ -13532,7 +13826,7 @@ uniform bool uDemo; // @control default=true label="Demo when idle"`,
13532
13826
  HASH,
13533
13827
  NOISE,
13534
13828
  FBM,
13535
- trailHelper(1 / 30, 1.3),
13829
+ trailHelper(1 / 30, 1.3, true),
13536
13830
  SEGMENT3,
13537
13831
  `// The stroke's width at a point from how fast the segment from a to b was drawn: faster, thinner.
13538
13832
  float pressWidth(vec4 a, vec4 b, float px) {
@@ -13543,47 +13837,61 @@ float pressWidth(vec4 a, vec4 b, float px) {
13543
13837
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
13544
13838
  float px = 1.0 / iResolution.y;
13545
13839
  vec2 p = fragCoord * px;
13546
- vec4 a = trailPoint(0, uDemo);
13547
- vec4 b = trailPoint(1, uDemo);
13840
+ bool live = trailLive(uDemo);
13841
+ vec4 a = trailAt(0, live);
13842
+ vec4 b = trailAt(1, live);
13548
13843
  float best = 1e9;
13549
13844
  float bestU = 1.0;
13550
13845
  float bestV = 0.0;
13551
13846
  float bestFade = 0.0;
13847
+ // Ragged edges: the same noise for every segment, so read it once.
13848
+ float rag = 0.9 + 0.2 * vnoise(p * 180.0);
13552
13849
  if (a.w > 0.5 && b.w > 0.5) {
13553
- float wa = pressWidth(a, b, px);
13850
+ // Faster strokes are thinner, never wider than uWidth: ragged, at most 1.1 \xD7 that from the line.
13851
+ float reach = 1.1 * uWidth;
13852
+ trailFrom(2, live);
13554
13853
  for (int k = 0; k < TRAIL_POINTS - 1; k++) {
13555
- b = trailPoint(k + 1, uDemo);
13556
13854
  if (b.w < 0.5 || trailJump(a, b)) break;
13557
- vec4 c = k + 2 < TRAIL_POINTS ? trailPoint(min(k + 2, TRAIL_POINTS - 1), uDemo) : vec4(0.0);
13558
- if (trailJump(b, c)) c = vec4(0.0);
13559
- float wb = c.w > 0.5 ? pressWidth(b, c, px) : wa;
13560
- vec2 s = segment(p, a.xy * px, b.xy * px);
13561
- float u = (float(k) + s.y) / float(TRAIL_POINTS - 1);
13562
- float fade = clamp(1.0 - (iTime - mix(a.z, b.z, s.y)) / uLife, 0.0, 1.0);
13563
- // A rounded start and a tail that thins out.
13564
- float w = mix(wa, wb, s.y) * (1.0 - 0.6 * smoothstep(0.55, 1.0, u)) * smoothstep(0.0, 0.04, u + 0.01);
13565
- w *= 0.4 + 0.6 * fade;
13566
- // Ragged edges.
13567
- float sd = s.x - w * (0.9 + 0.2 * vnoise(p * 180.0));
13568
- if (sd < best) {
13569
- vec2 dir = b.xy - a.xy;
13570
- float side = dir.x * (p.y - a.y * px) - dir.y * (p.x - a.x * px);
13571
- best = sd;
13572
- bestU = u;
13573
- bestV = clamp(s.x / max(w, 1e-5), 0.0, 1.0) * (side < 0.0 ? -1.0 : 1.0);
13574
- bestFade = fade;
13855
+ // The point after b: read once, it is the next segment's b.
13856
+ vec4 next = k + 2 < TRAIL_POINTS ? trailNext(live) : vec4(0.0);
13857
+ // A segment whose box is further off than the stroke reaches cannot be nearer than the best so far.
13858
+ vec2 off = max(max(min(a.xy, b.xy) * px - p, p - max(a.xy, b.xy) * px), 0.0);
13859
+ float r = best + reach;
13860
+ if (r > 0.0 && dot(off, off) < r * r) {
13861
+ // The width where the segment starts is from it, where it ends from the next (or the same at the end).
13862
+ float wa = pressWidth(a, b, px);
13863
+ float wb = next.w > 0.5 && !trailJump(b, next) ? pressWidth(b, next, px) : wa;
13864
+ vec2 s = segment(p, a.xy * px, b.xy * px);
13865
+ float u = (float(k) + s.y) / float(TRAIL_POINTS - 1);
13866
+ float fade = clamp(1.0 - (iTime - mix(a.z, b.z, s.y)) / uLife, 0.0, 1.0);
13867
+ // A rounded start and a tail that thins out.
13868
+ float w = mix(wa, wb, s.y) * (1.0 - 0.6 * smoothstep(0.55, 1.0, u)) * smoothstep(0.0, 0.04, u + 0.01);
13869
+ w *= 0.4 + 0.6 * fade;
13870
+ float sd = s.x - w * rag;
13871
+ if (sd < best) {
13872
+ vec2 dir = b.xy - a.xy;
13873
+ float side = dir.x * (p.y - a.y * px) - dir.y * (p.x - a.x * px);
13874
+ best = sd;
13875
+ bestU = u;
13876
+ bestV = clamp(s.x / max(w, 1e-5), 0.0, 1.0) * (side < 0.0 ? -1.0 : 1.0);
13877
+ bestFade = fade;
13878
+ }
13575
13879
  }
13576
13880
  a = b;
13577
- wa = wb;
13881
+ b = next;
13578
13882
  }
13579
13883
  }
13580
13884
  // Bristles streak along the stroke (they vary across it); the paper's tooth breaks them up.
13581
- float bristle = 0.6 * vnoise(vec2(bestV * uBristles, 3.7)) + 0.4 * fbm(p * vec2(55.0, 55.0), 3);
13582
- float dry = uDry * smoothstep(0.15, 1.0, bestU) + 0.35 * uDry * smoothstep(0.6, 1.0, abs(bestV)) + 0.7 * (1.0 - bestFade);
13583
- float ink = smoothstep(dry - 0.06, dry + 0.06, bristle * 1.1);
13584
- float cover = (1.0 - smoothstep(-px, px, best)) * ink * smoothstep(0.0, 0.1, bestFade);
13585
- // Ink pools darker at the edges and thins where a bristle runs dry.
13586
- vec3 col = uInk * (1.0 + 0.6 * (1.0 - bristle) * (1.0 - abs(bestV))) + 0.04 * (1.0 - bristle);
13885
+ // Away from the stroke nothing shows, so the bristles are not worked out there.
13886
+ float cover = (1.0 - smoothstep(-px, px, best)) * smoothstep(0.0, 0.1, bestFade);
13887
+ vec3 col = uInk;
13888
+ if (cover > 0.0) {
13889
+ float bristle = 0.6 * vnoise(vec2(bestV * uBristles, 3.7)) + 0.4 * fbm(p * vec2(55.0, 55.0), 3);
13890
+ float dry = uDry * smoothstep(0.15, 1.0, bestU) + 0.35 * uDry * smoothstep(0.6, 1.0, abs(bestV)) + 0.7 * (1.0 - bestFade);
13891
+ cover *= smoothstep(dry - 0.06, dry + 0.06, bristle * 1.1);
13892
+ // Ink pools darker at the edges and thins where a bristle runs dry.
13893
+ col = uInk * (1.0 + 0.6 * (1.0 - bristle) * (1.0 - abs(bestV))) + 0.04 * (1.0 - bristle);
13894
+ }
13587
13895
  if (uPaper) {
13588
13896
  vec3 paper = uPaperColor * (0.95 + 0.07 * fbm(p * 9.0, 4)) - 0.03 * smoothstep(0.55, 0.9, vnoise(p * vec2(400.0, 30.0)));
13589
13897
  fragColor = vec4(mix(paper, col, cover * 0.95), 1.0);
@@ -13701,9 +14009,10 @@ uniform float uLife; // @control min=0.5 max=10 default=4 label="Fade time"
13701
14009
  uniform float uFlicker; // @control min=0 max=1 default=0.25 label="Flicker"
13702
14010
  uniform bool uDemo; // @control default=true label="Demo when idle"`,
13703
14011
  HASH,
13704
- trailHelper(0.05, 1.1),
14012
+ trailHelper(0.05, 1.1, true),
13705
14013
  SEGMENT3,
13706
14014
  CATMULL,
14015
+ CATMULL_BOX,
13707
14016
  `vec3 spectrum(float h) {
13708
14017
  return 0.5 + 0.5 * cos(6.28318 * (h + vec3(0.0, 0.33, 0.67)));
13709
14018
  }
@@ -13716,8 +14025,9 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
13716
14025
  vec3 col = uBackground * (1.0 - 0.45 * dot(uv - 0.5, uv - 0.5) * 2.0);
13717
14026
  col += (hash12(fragCoord + fract(iTime) * 113.0) - 0.5) * 0.012;
13718
14027
 
13719
- vec4 a = trailPoint(0, uDemo);
13720
- vec4 b = trailPoint(1, uDemo);
14028
+ bool live = trailLive(uDemo);
14029
+ vec4 a = trailAt(0, live);
14030
+ vec4 b = trailAt(1, live);
13721
14031
  if (a.w > 0.5 && b.w > 0.5) {
13722
14032
  float glowR = uWidth * 4.0 * max(uGlow, 0.05);
13723
14033
  float margin = uWidth * 3.0 + glowR * 6.0 + 2.0 * px;
@@ -13726,35 +14036,44 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
13726
14036
  float halo = 0.0;
13727
14037
  float best = 1e9;
13728
14038
  float bestWhen = a.z;
14039
+ trailFrom(2, live);
13729
14040
  for (int k = 0; k < TRAIL_POINTS - 1; k++) {
13730
- b = trailPoint(k + 1, uDemo);
13731
14041
  if (b.w < 0.5) break;
13732
14042
  // Earlier strokes stay lit, but the jump between strokes is not drawn.
13733
14043
  bool jump = trailJump(a, b);
13734
- vec4 c = k + 2 < TRAIL_POINTS ? trailPoint(min(k + 2, TRAIL_POINTS - 1), uDemo) : vec4(0.0);
14044
+ // The point after b: read once, it is the next segment's b.
14045
+ vec4 c = k + 2 < TRAIL_POINTS ? trailNext(live) : vec4(0.0);
13735
14046
  vec2 next = c.w > 0.5 && !trailJump(b, c) ? c.xy : 2.0 * b.xy - a.xy;
13736
14047
  vec2 lo = min(min(prev, a.xy), min(b.xy, next)) * px - margin;
13737
14048
  vec2 hi = max(max(prev, a.xy), max(b.xy, next)) * px + margin;
13738
14049
  if (!jump && all(greaterThan(p, lo)) && all(lessThan(p, hi))) {
13739
- vec2 q0 = a.xy * px;
13740
- for (int j = 1; j <= 4; j++) {
13741
- vec2 q1 = catmull(prev, a.xy, b.xy, next, float(j) / 4.0) * px;
13742
- vec2 s = segment(p, q0, q1);
13743
- float when = mix(a.z, b.z, (float(j - 1) + s.y) / 4.0);
13744
- float fade = clamp(1.0 - (iTime - when) / uLife, 0.0, 1.0);
13745
- fade *= fade;
13746
- float d = s.x;
13747
- core = max(core, exp(-d * d / (uWidth * uWidth)) * fade);
13748
- halo = max(halo, exp(-d / glowR) * fade);
13749
- if (d < best) {
13750
- best = d;
13751
- bestWhen = when;
14050
+ // Nothing on the segment is brighter than its newer end: skip it when it can be neither nearer
14051
+ // nor brighter than what is found.
14052
+ float dmin = trailBoxDist(p, prev, a.xy, b.xy, next, px);
14053
+ float top = clamp(1.0 - (iTime - max(a.z, b.z)) / uLife, 0.0, 1.0);
14054
+ top *= top;
14055
+ if (dmin < best || exp(-dmin / glowR) * top > halo || exp(-dmin * dmin / (uWidth * uWidth)) * top > core) {
14056
+ vec2 q0 = a.xy * px;
14057
+ for (int j = 1; j <= 4; j++) {
14058
+ vec2 q1 = catmull(prev, a.xy, b.xy, next, float(j) / 4.0) * px;
14059
+ vec2 s = segment(p, q0, q1);
14060
+ float when = mix(a.z, b.z, (float(j - 1) + s.y) / 4.0);
14061
+ float fade = clamp(1.0 - (iTime - when) / uLife, 0.0, 1.0);
14062
+ fade *= fade;
14063
+ float d = s.x;
14064
+ core = max(core, exp(-d * d / (uWidth * uWidth)) * fade);
14065
+ halo = max(halo, exp(-d / glowR) * fade);
14066
+ if (d < best) {
14067
+ best = d;
14068
+ bestWhen = when;
14069
+ }
14070
+ q0 = q1;
13752
14071
  }
13753
- q0 = q1;
13754
14072
  }
13755
14073
  }
13756
14074
  prev = jump ? 2.0 * b.xy - next : a.xy;
13757
14075
  a = b;
14076
+ b = c;
13758
14077
  }
13759
14078
  // Colour from when the light passed, so each part keeps its colour as it fades.
13760
14079
  vec3 tint = spectrum(uHue + bestWhen * uCycle);
@@ -13803,6 +14122,16 @@ vec4 clickAt(int i, bool demo, float period) {
13803
14122
  vec2 h = hash22(vec2(k * 1.37 + 0.5, 3.7));
13804
14123
  return vec4((0.2 + 0.6 * h) * iResolution.xy, k * period, 1.0);
13805
14124
  }`.trim();
14125
+ var CORNER_HASHES = `
14126
+ vec4 cornerHashes(vec2 i) {
14127
+ vec2 x = fract(vec2(i.x, i.x + 1.0) * 0.1031);
14128
+ vec2 y = fract(vec2(i.y, i.y + 1.0) * 0.1031);
14129
+ vec4 X = x.xyxy;
14130
+ vec4 Y = y.xxyy;
14131
+ vec4 d = X * (Y + 33.33) + Y * (X + 33.33) + X * (X + 33.33);
14132
+ vec4 px = X + d;
14133
+ return fract((px + (Y + d)) * px);
14134
+ }`.trim();
13806
14135
  var flowParticles = {
13807
14136
  kind: "shader",
13808
14137
  id: "flow-particles",
@@ -13825,19 +14154,25 @@ uniform vec3 uFast; // @control color default=#7CFFE0 label="Fast colour
13825
14154
  uniform vec3 uBackground; // @control color default=#04060E label="Background"
13826
14155
  uniform bool uTransparent; // @control default=false label="Transparent"`,
13827
14156
  HASH,
13828
- NOISE,
14157
+ CORNER_HASHES,
13829
14158
  GLOW6,
13830
- `float potential(vec2 p, float t) {
13831
- return vnoise(p + vec2(t, -0.6 * t)) + uTurbulence * 0.5 * vnoise(p * 2.3 - vec2(0.7 * t, t) + 9.1);
14159
+ `// The gradient of vnoise, worked out exactly (its value is not needed here).
14160
+ vec2 vnoiseGrad(vec2 p) {
14161
+ vec2 i = floor(p);
14162
+ vec2 f = fract(p);
14163
+ vec2 u = f * f * (3.0 - 2.0 * f);
14164
+ vec2 du = 6.0 * f * (1.0 - f);
14165
+ vec4 h = cornerHashes(i);
14166
+ float k = h.x - h.y - h.z + h.w;
14167
+ return du * vec2(h.y - h.x + k * u.y, h.z - h.x + k * u.x);
13832
14168
  }
13833
14169
 
13834
- // The curl of the potential (divergence free, so streams swirl rather than pool), plus a gentle drift.
14170
+ // The curl of the potential vnoise(p + vec2(t, -0.6 t)) + turbulence * 0.5 * vnoise(p * 2.3 - vec2(0.7 t, t) + 9.1)
14171
+ // (divergence free, so streams swirl rather than pool), plus a gentle drift.
13835
14172
  vec2 flow(vec2 p, float t) {
13836
- const float e = 0.01;
13837
- float c = potential(p, t);
13838
- float dx = potential(p + vec2(e, 0.0), t) - c;
13839
- float dy = potential(p + vec2(0.0, e), t) - c;
13840
- return vec2(dy, -dx) / e + vec2(0.35, 0.08);
14173
+ vec2 g = vnoiseGrad(p + vec2(t, -0.6 * t));
14174
+ if (uTurbulence > 0.0) g += uTurbulence * 1.15 * vnoiseGrad(p * 2.3 - vec2(0.7 * t, t) + 9.1);
14175
+ return vec2(g.y, -g.x) + vec2(0.35, 0.08);
13841
14176
  }
13842
14177
 
13843
14178
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
@@ -13847,22 +14182,38 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
13847
14182
  float ft = iTime * uSpeed * 0.04;
13848
14183
  float tail = max(uTail * float(STEPS), 1.0);
13849
14184
  vec3 col = vec3(0.0);
14185
+ // The walk stays a few steps in each cell, so its particle is worked out once per cell.
14186
+ vec2 cellAt = vec2(-1e9);
14187
+ bool alive = false;
14188
+ float life = 0.0;
14189
+ vec2 at = vec2(0.0);
14190
+ vec2 v = vec2(0.0);
14191
+ float sp = 0.0;
13850
14192
  for (int k = 0; k < STEPS; k++) {
13851
- vec2 v = flow(q * uScale, ft);
13852
- float sp = length(v);
14193
+ // The field barely turns over a step, so it is looked up every other step.
14194
+ if ((k & 1) == 0) {
14195
+ v = flow(q * uScale, ft);
14196
+ sp = length(v);
14197
+ }
13853
14198
  // One particle per cell at most; each life starts somewhere new in the cell.
13854
14199
  vec2 c = floor(q * uDensity);
13855
- float h0 = hash12(c + 0.31);
13856
- float phase = iTime * uSpeed * (0.3 + 0.3 * h0) + hash12(c + 7.7);
13857
- float life = fract(phase);
13858
- float cyc = floor(phase);
13859
- if (hash12(c * 1.7 + cyc * 3.1) < uAmount) {
13860
- vec2 hd = hash22(c + cyc * 17.13);
13861
- float d = length(q - (c + 0.25 + 0.5 * hd) * cell) / cell;
13862
- float spot = exp(-d * d * 14.0);
13863
- // This pixel lies k steps downstream of the particle's start; its head is "head" steps along.
13864
- float behind = life * float(STEPS) - float(k);
13865
- if (spot > 0.01 && behind >= 0.0 && behind < tail) {
14200
+ if (c != cellAt) {
14201
+ cellAt = c;
14202
+ float h0 = hash12(c + 0.31);
14203
+ float phase = iTime * uSpeed * (0.3 + 0.3 * h0) + hash12(c + 7.7);
14204
+ life = fract(phase);
14205
+ float cyc = floor(phase);
14206
+ alive = hash12(c * 1.7 + cyc * 3.1) < uAmount;
14207
+ if (alive) at = (c + 0.25 + 0.5 * hash22(c + cyc * 17.13)) * cell;
14208
+ }
14209
+ // This pixel lies k steps downstream of the particle's start; its head is "head" steps along.
14210
+ float behind = life * float(STEPS) - float(k);
14211
+ if (alive && behind >= 0.0 && behind < tail) {
14212
+ vec2 dq = (q - at) / cell;
14213
+ float d2 = dot(dq, dq);
14214
+ // exp(-14 d^2) > 0.01
14215
+ if (d2 < 0.329) {
14216
+ float spot = exp(-d2 * 14.0);
13866
14217
  float w = 1.0 - behind / tail;
13867
14218
  float fade = smoothstep(0.0, 0.08, life) * (1.0 - smoothstep(0.75, 1.0, life));
13868
14219
  vec3 hue = mix(uSlow, uFast, smoothstep(0.3, 2.2, sp));
@@ -14130,12 +14481,40 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
14130
14481
  float t = iTime * uSpeed * 0.5;
14131
14482
  float len = 0.018 * uSize;
14132
14483
  float cover = 0.0;
14484
+ // A bird is within its formation offset, jitter and size of the line between the two flocks
14485
+ // it is moving between, so first find which pairs of flocks pass near this pixel (bit a * 4 + b).
14486
+ float groups = float(uGroups);
14487
+ vec2 flocks[4];
14488
+ for (int g = 0; g < 4; g++) flocks[g] = flockAt(float(g), t, aspect);
14489
+ float margin = 1.89 * uSpread + 0.036 * uJitter + 2.04 * len + 2.0 * px;
14490
+ int near = 0;
14491
+ for (int a = 0; a < 4; a++) {
14492
+ if (a >= uGroups) break;
14493
+ for (int b = 0; b < 4; b++) {
14494
+ if (b >= uGroups) break;
14495
+ vec2 pa = p - flocks[a];
14496
+ vec2 ba = flocks[b] - flocks[a];
14497
+ vec2 off = pa - ba * clamp(dot(pa, ba) / max(dot(ba, ba), 1e-8), 0.0, 1.0);
14498
+ if (dot(off, off) < margin * margin) near |= 1 << (a * 4 + b);
14499
+ }
14500
+ }
14133
14501
  for (int i = 0; i < 120; i++) {
14134
- if (i >= uCount) break;
14502
+ if (near == 0 || i >= uCount) break;
14135
14503
  float fi = float(i);
14504
+ // birdAt, with the flock centres looked up and far birds dropped before the rest of the work.
14505
+ vec2 h = hash22(vec2(fi, 3.1));
14506
+ float sw = t * 0.06 * (0.6 + 0.8 * h.y) + h.x * 10.0;
14507
+ float k = floor(sw);
14508
+ int ga = int(floor(hash12(vec2(fi, k)) * groups));
14509
+ float glide = smoothstep(0.6, 1.0, fract(sw));
14510
+ int gb = glide > 0.0 ? int(floor(hash12(vec2(fi, k + 1.0)) * groups)) : ga;
14511
+ if ((near & (1 << (ga * 4 + gb))) == 0) continue;
14512
+ vec2 o = (hash22(vec2(fi, 8.3)) - 0.5) * 2.0 * uSpread;
14513
+ o.x *= 1.6;
14514
+ vec2 jitter = uJitter * 0.025 * vec2(sin(t * (1.1 + h.x) + fi), cos(t * (1.3 + h.y) + fi * 1.7));
14515
+ vec2 pos = mix(flocks[ga], flocks[gb], glide) + o + jitter;
14136
14516
  float scale = 0.7 + 0.5 * hash12(vec2(fi, 6.6));
14137
14517
  float l = len * scale;
14138
- vec2 pos = birdAt(fi, t, aspect);
14139
14518
  vec2 dp = p - pos;
14140
14519
  if (abs(dp.x) > l * 1.2 || abs(dp.y) > l * 1.2) continue;
14141
14520
  vec2 v = pos - birdAt(fi, t - 0.03, aspect);
@@ -14194,7 +14573,7 @@ uniform float uDensity; // @control min=40 max=240 default=120 step=1 label
14194
14573
  uniform float uScatter; // @control min=0.1 max=1.5 default=0.4 label="Scatter distance"
14195
14574
  uniform float uDirection; // @control min=-180 max=180 default=20 label="Drift direction"`,
14196
14575
  HASH,
14197
- NOISE,
14576
+ CORNER_HASHES,
14198
14577
  `// The text image's alpha at frame point p (height 1, centred), the image contained in uSize of the frame.
14199
14578
  float letters(vec2 p) {
14200
14579
  vec2 size = vec2(textureSize(uText, 0));
@@ -14206,6 +14585,15 @@ float letters(vec2 p) {
14206
14585
  return texture(uText, uv).a;
14207
14586
  }
14208
14587
 
14588
+ // Half the size of the text's box in the frame (letters is 0 outside it).
14589
+ vec2 lettersHalf() {
14590
+ vec2 size = vec2(textureSize(uText, 0));
14591
+ float aspect = size.x / max(size.y, 1.0);
14592
+ float frame = iResolution.x / iResolution.y;
14593
+ float h = min(uSize, frame * uSize / aspect);
14594
+ return 0.5 * vec2(h * aspect, h);
14595
+ }
14596
+
14209
14597
  // How far through dissolving the particle at home is (0 in place, 1 gone): left to right, then back together.
14210
14598
  float scattered(vec2 home, float phase) {
14211
14599
  float frame = iResolution.x / iResolution.y;
@@ -14216,16 +14604,40 @@ float scattered(vec2 home, float phase) {
14216
14604
  return a * a * (3.0 - 2.0 * a);
14217
14605
  }
14218
14606
 
14607
+ // vnoise, keeping the corner hashes of the lattice cell it last looked in: the wind is smooth and
14608
+ // its lattice coarse, so most lookups land in the same cell as the one before.
14609
+ vec2 eddyCellA = vec2(-1e9);
14610
+ vec2 eddyCellB = vec2(-1e9);
14611
+ vec4 eddyHashA = vec4(0.0);
14612
+ vec4 eddyHashB = vec4(0.0);
14613
+
14614
+ float eddy(vec2 p, inout vec2 cellAt, inout vec4 h) {
14615
+ vec2 i = floor(p);
14616
+ if (i != cellAt) {
14617
+ cellAt = i;
14618
+ h = cornerHashes(i);
14619
+ }
14620
+ vec2 f = fract(p);
14621
+ vec2 u = f * f * (3.0 - 2.0 * f);
14622
+ return mix(mix(h.x, h.y, u.x), mix(h.z, h.w, u.x), u.y);
14623
+ }
14624
+
14625
+ // Group g's gust: its wind (xy) and how much it lifts what it carries (z).
14626
+ vec3 gustOf(float g) {
14627
+ float ang = radians(uDirection) + (fract(g * 0.618) - 0.5) * 1.2;
14628
+ return vec3(vec2(cos(ang), sin(ang)) * (0.3 + 0.18 * g), 0.3 * (0.5 + 0.2 * g));
14629
+ }
14630
+
14219
14631
  // Where the wind has carried a particle of group g from home: smooth in home, so it can be inverted.
14220
14632
  // Each of the six groups rides its own gust (speed, heading and eddies), so they spread apart.
14221
- vec2 carried(vec2 home, float phase, float g) {
14633
+ vec2 carried(vec2 home, float phase, float g, vec3 gust) {
14222
14634
  float a = scattered(home, phase);
14223
- float ang = radians(uDirection) + (fract(g * 0.618) - 0.5) * 1.2;
14224
- vec2 wind = vec2(cos(ang), sin(ang)) * (0.3 + 0.18 * g);
14635
+ // Still at home (most of the frame, most of the time): no need for the eddies.
14636
+ if (a <= 0.0) return vec2(0.0);
14225
14637
  float t = phase * 3.0;
14226
14638
  vec2 q = home * 3.0 + g * 5.3;
14227
- vec2 swirl = vec2(vnoise(q + vec2(t, 0.0)), vnoise(q + vec2(4.7, t))) - 0.5;
14228
- return a * uScatter * (wind + swirl * 0.8 + vec2(0.0, 0.3 * a * (0.5 + 0.2 * g)));
14639
+ vec2 swirl = vec2(eddy(q + vec2(t, 0.0), eddyCellA, eddyHashA), eddy(q + vec2(4.7, t), eddyCellB, eddyHashB)) - 0.5;
14640
+ return a * uScatter * (gust.xy + swirl * 0.8 + vec2(0.0, gust.z * a));
14229
14641
  }
14230
14642
 
14231
14643
  void mainImage(out vec4 fragColor, in vec2 fragCoord) {
@@ -14239,22 +14651,28 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
14239
14651
  vec3 col = uColor;
14240
14652
  float cover = solid;
14241
14653
 
14654
+ // Every particle is home outside this part of the loop.
14655
+ bool moving = phase > 0.12 && phase < 0.94;
14656
+ vec2 box = lettersHalf() + 2.0 * cell;
14242
14657
  for (int gi = 0; gi < 6; gi++) {
14658
+ if (!moving) break;
14243
14659
  float g = float(gi);
14660
+ vec3 gust = gustOf(g);
14244
14661
  // Trace back to the home a particle of this group here would have come from, then search the cells round it.
14245
14662
  vec2 home = p;
14246
- for (int i = 0; i < 4; i++) home = p - carried(home, phase, g);
14663
+ for (int i = 0; i < 4; i++) home = p - carried(home, phase, g, gust);
14664
+ // The cells searched are all off the text.
14665
+ if (any(greaterThan(abs(home), box))) continue;
14247
14666
  vec2 base = floor(home / cell);
14248
14667
  for (int y = -1; y <= 1; y++) {
14249
14668
  for (int x = -1; x <= 1; x++) {
14250
14669
  vec2 c = base + vec2(float(x), float(y));
14251
14670
  if (floor(hash12(c + 0.73) * 6.0) != g) continue;
14252
14671
  vec2 hc = (c + 0.5) * cell;
14253
- if (letters(hc) < 0.5) continue;
14254
14672
  float a = scattered(hc, phase);
14255
- if (a < 0.002) continue;
14673
+ if (a < 0.002 || letters(hc) < 0.5) continue;
14256
14674
  vec2 j = hash22(c);
14257
- vec2 pos = hc + carried(hc, phase, g) + (j - 0.5) * cell * 1.1 * a;
14675
+ vec2 pos = hc + carried(hc, phase, g, gust) + (j - 0.5) * cell * 1.1 * a;
14258
14676
  float r = cell * (0.5 - 0.1 * a) * (0.75 + 0.5 * j.x);
14259
14677
  float d = length(p - pos);
14260
14678
  float disc = (1.0 - smoothstep(r - px, r + px * 0.5, d)) * (1.0 - smoothstep(0.6, 1.0, a) * 0.6 * j.y);
@@ -14312,14 +14730,32 @@ vec4 blast(vec4 acc, vec2 p, vec2 origin, float angle, float spread, float speed
14312
14730
  float e = (1.0 - exp(-k * age)) / k;
14313
14731
  float fade = 1.0 - smoothstep(LIFE - 1.0, LIFE, age);
14314
14732
  float reach = 0.03 * uSize;
14733
+ // Every piece is within its sway and the spread of fall speeds of the fan its launch sweeps out
14734
+ // (speeds 0.5 to 1 times speed, within spread of angle, all dropped by the middle fall speed),
14735
+ // so skip the blast where this pixel is clear of that.
14736
+ float drop = 0.16 * uFall * (age - e);
14737
+ vec2 rel = p - origin + vec2(0.0, drop);
14738
+ float slack = reach * 1.42 + 0.035 + 0.3 * drop + px;
14739
+ float r = length(rel);
14740
+ float far = speed * e;
14741
+ if (r > far + slack || r < 0.5 * far - slack) return acc;
14742
+ if (spread < 6.2 && r > slack) {
14743
+ float off = abs(mod(atan(rel.y, rel.x) - angle + 3.14159265, 6.28318531) - 3.14159265);
14744
+ if (off > spread * 0.5 + asin(slack / r) + 0.01) return acc;
14745
+ }
14315
14746
  for (int j = 0; j < 80; j++) {
14316
14747
  if (j >= count) break;
14317
14748
  float fj = float(j);
14318
14749
  vec2 h = hash22(vec2(fj * 7.13 + 1.0, seed));
14750
+ float launch = speed * (0.5 + 0.5 * h.y);
14751
+ // The same tests for this piece before working out the rest of it: how far out, then where.
14752
+ if (abs(r - launch * e) > slack) continue;
14753
+ float a = angle + (h.x - 0.5) * spread;
14754
+ vec2 v0 = vec2(cos(a), sin(a)) * launch;
14755
+ vec2 dv = rel - v0 * e;
14756
+ if (abs(dv.x) > reach + 0.035 || abs(dv.y) > reach + 0.3 * drop) continue;
14319
14757
  vec2 h2 = hash22(h * 53.1 + seed);
14320
14758
  float h3 = hash12(h2 * 17.7 + fj);
14321
- float a = angle + (h.x - 0.5) * spread;
14322
- vec2 v0 = vec2(cos(a), sin(a)) * speed * (0.5 + 0.5 * h.y);
14323
14759
  float fall = 0.16 * uFall * (0.7 + 0.6 * h2.x);
14324
14760
  vec2 pos = origin + v0 * e - vec2(0.0, fall * (age - e));
14325
14761
  pos.x += 0.035 * sin(age * (2.0 + 2.0 * h2.y) + h.x * 6.28) * smoothstep(0.3, 1.2, age);
@@ -14732,18 +15168,6 @@ vec3 shardOf(vec2 q) {
14732
15168
  return vec3(sid, ring, edge);
14733
15169
  }
14734
15170
 
14735
- // A shard's middle, which it turns about, and how far it reaches from there.
14736
- vec3 shardCentre(float sid, float ring) {
14737
- float a0 = spoke(sid);
14738
- float a1 = spoke(sid + 1.0);
14739
- float mid = 0.5 * (a0 + a1);
14740
- float inner = ring > 0.0 ? ringAt(sid, ring) : 0.0;
14741
- float outer = ring + 1.0 < float(RINGS) ? ringAt(sid, ring + 1.0) : inner * uGrowth;
14742
- float rc = ring > 0.0 ? 0.5 * (inner + outer) : 0.66 * outer;
14743
- float reach = length(vec2(max(outer - rc, rc - inner), outer * tan(0.5 * (a1 - a0))));
14744
- return vec3(vec2(cos(mid), sin(mid)) * rc, reach + 0.01);
14745
- }
14746
-
14747
15171
  // When a shard lets go: the middle first, the outer pane last.
14748
15172
  float release(float sid, float ring) {
14749
15173
  return 0.3 + 0.16 * ring + 0.25 * hash12(vec2(sid, ring + 9.0));
@@ -14765,33 +15189,109 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
14765
15189
  vec4 col = vec4(uBackground, 1.0);
14766
15190
 
14767
15191
  // Shards that have let go, falling and spinning: the first to fall is in front.
15192
+ // A ring at a time: the rings all of whose shards are too far from this pixel, wherever they are
15193
+ // in their fall, are passed over whole; later rings let go later, so once one is not yet falling
15194
+ // (or would be behind what is already here) neither are the rest.
14768
15195
  float best = 1e3;
14769
15196
  vec4 hit = vec4(0.0);
14770
15197
  vec2 hitCentre = vec2(0.0);
14771
- for (int s = 0; s < 16; s++) {
14772
- if (s >= uSpokes) break;
14773
- for (int j = 0; j < RINGS; j++) {
14774
- float sid = float(s);
14775
- float ring = float(j);
15198
+ float n = float(uSpokes);
15199
+ float spreadMax = tan(min(0.8 * TAU / n, 1.5));
15200
+ for (int j = 0; j < RINGS; j++) {
15201
+ float ring = float(j);
15202
+ float firstGo = 0.3 + 0.16 * ring;
15203
+ float dtHi = age - firstGo;
15204
+ if (dtHi <= 0.0 || firstGo >= best) break;
15205
+ float dtLo = max(dtHi - 0.25, 0.0);
15206
+ // Bounds on this ring's shards: how far out their middles are, how far they reach, how far they have drifted and dropped.
15207
+ float span = uScale * pow(uGrowth, ring);
15208
+ float lo = j > 0 ? span / uGrowth : 0.0;
15209
+ float rcLo = j > 0 ? 0.425 * (lo + span) : 0.561 * span;
15210
+ float rcHi = j > 0 ? 0.575 * (lo + span) : 0.759 * span;
15211
+ float radial = j > 0 ? 0.5 * max(1.15 * span - 0.85 * lo, 1.15 * lo - 0.85 * span) : rcHi;
15212
+ float reachMax = length(vec2(radial, 1.15 * span * spreadMax)) + 0.02;
15213
+ float rMin = rcLo + 0.05 * (1.0 - exp(-0.6 * dtLo)) / 0.6 / (1.0 + ring) - reachMax - 0.01;
15214
+ float rMax = rcHi + 0.2 * (1.0 - exp(-0.6 * dtHi)) / 0.6 / (1.0 + ring) + reachMax + 0.01;
15215
+ float yA = ballistic(vec2(0.0, 0.08), 0.6, uGravity, dtLo).y;
15216
+ float yB = ballistic(vec2(0.0, 0.08), 0.6, uGravity, dtHi).y;
15217
+ float yLo = min(yA, yB) - 0.02;
15218
+ float yHi = max(yA, yB) + 0.02;
15219
+ float dNear = length(vec2(q.x, q.y - clamp(q.y, yLo, yHi)));
15220
+ float dFar = max(length(q - vec2(0.0, yLo)), length(q - vec2(0.0, yHi)));
15221
+ if (dFar < rMin || dNear > rMax) continue;
15222
+ // And of its shards, only those whose sector points (near enough) at this pixel from where the impact point has dropped to:
15223
+ // each sector's middle lies 0.2\u20130.8 of the way round its share of the circle.
15224
+ float first = 0.0;
15225
+ float count = n;
15226
+ float slack = reachMax + 0.01;
15227
+ if (dNear > slack) {
15228
+ vec2 na = normalize(q - vec2(0.0, yLo));
15229
+ vec2 nb = normalize(q - vec2(0.0, yHi));
15230
+ float w = 0.5 * acos(clamp(dot(na, nb), -1.0, 1.0)) + asin(slack / dNear);
15231
+ vec2 bisect = na + nb;
15232
+ float toward = atan(bisect.y, bisect.x) / (TAU / n);
15233
+ w /= TAU / n;
15234
+ first = ceil(toward - w - 0.8);
15235
+ count = min(floor(toward + w - 0.2) - first + 1.0, n);
15236
+ }
15237
+ for (int k = 0; k < 16; k++) {
15238
+ if (float(k) >= count) break;
15239
+ float sid = mod(first + float(k), n);
14776
15240
  float tr = release(sid, ring);
14777
15241
  float dt = age - tr;
14778
15242
  if (dt <= 0.0 || tr >= best) continue;
15243
+ // How far it has drifted (along e) and dropped since it let go: ballistic(), split so the drop can be used first.
15244
+ float e = (1.0 - exp(-0.6 * dt)) / 0.6;
15245
+ // Seen from where the impact point has dropped to by now, too near or far, or not in this sector's direction.
15246
+ vec2 dropped = q - vec2(0.0, 0.08 * e - uGravity / 0.6 * (dt - e));
15247
+ float far = length(dropped);
15248
+ if (far < rMin || far > rMax) continue;
15249
+ // Its two cracks, the direction halfway between them and tan of half the angle they make.
15250
+ float a0 = spoke(sid);
15251
+ float a1 = spoke(sid + 1.0);
15252
+ vec2 d0 = vec2(cos(a0), sin(a0));
15253
+ vec2 d1 = vec2(cos(a1), sin(a1));
15254
+ vec2 dir = normalize(d0 + d1);
15255
+ float spread = (d0.x * d1.y - d0.y * d1.x) / (1.0 + dot(d0, d1));
15256
+ // Not pointing (near enough) this way: further off its middle than any of its corners could be.
15257
+ float near = length(vec2(radial, 1.15 * span * spread)) + 0.03;
15258
+ if (far > near && dot(dropped, dir) < sqrt(far * far - near * near)) continue;
15259
+ // Its middle, which it turns about, and how far it reaches from there (ringAt, with the ring's scale worked out above).
15260
+ float inner = j > 0 ? lo * (0.85 + 0.3 * hash12(vec2(sid, ring + 0.5))) : 0.0;
15261
+ float outer = j + 1 < RINGS ? span * (0.85 + 0.3 * hash12(vec2(sid, ring + 1.5))) : inner * uGrowth;
15262
+ float rc = j > 0 ? 0.5 * (inner + outer) : 0.66 * outer;
15263
+ float reach = length(vec2(max(outer - rc, rc - inner), outer * spread)) + 0.01;
15264
+ vec2 cen = dir * rc;
14779
15265
  vec2 h = hash22(vec2(sid, ring) + 17.3);
14780
- vec3 cen = shardCentre(sid, ring);
14781
- vec2 v0 = normalize(cen.xy + 1e-4) * (0.05 + 0.15 * h.x) / (1.0 + ring) + vec2(0.0, 0.08);
14782
- vec2 rel = q - cen.xy - ballistic(v0, 0.6, uGravity, dt);
14783
- if (length(rel) > cen.z) continue;
15266
+ vec2 rel = dropped - cen - normalize(cen + 1e-4) * (0.05 + 0.15 * h.x) / (1.0 + ring) * e;
15267
+ if (dot(rel, rel) > reach * reach) continue;
14784
15268
  float ang = (h.y - 0.5) * 2.0 * uSpin * dt;
14785
15269
  // It also turns about its own axis, narrowing and catching the light.
14786
15270
  float tilt = cos(dt * (0.6 + 1.2 * h.x) * uSpin);
14787
15271
  vec2 local = rot(-ang) * rel;
14788
15272
  local.x /= sign(tilt) * max(abs(tilt), 0.05);
14789
- vec2 orig = cen.xy + local;
14790
- vec3 sh = shardOf(orig);
14791
- if (sh.x != sid || sh.y != ring) continue;
15273
+ vec2 orig = cen + local;
15274
+ // Back on the pane, is that point inside this shard: between its two cracks and its two rings?
15275
+ float side0 = d0.x * orig.y - d0.y * orig.x;
15276
+ float side1 = d1.x * orig.y - d1.y * orig.x;
15277
+ float d = dot(orig, dir);
15278
+ float rim = j + 1 < RINGS ? outer : 1e3;
15279
+ if (side0 < 0.0 || side1 >= 0.0 || d >= rim) continue;
15280
+ // Past every ring inside it (they are a little ragged, so not only the nearest).
15281
+ bool past = true;
15282
+ for (int i = 1; i < RINGS; i++) {
15283
+ if (i > j) break;
15284
+ if (d < ringAt(sid, float(i))) {
15285
+ past = false;
15286
+ break;
15287
+ }
15288
+ }
15289
+ if (!past) continue;
15290
+ float edge = min(rim - d, min(abs(side0), abs(side1)));
15291
+ if (j > 0) edge = min(edge, d - inner);
14792
15292
  best = tr;
14793
- hit = vec4(orig, tilt, sh.z);
14794
- hitCentre = cen.xy;
15293
+ hit = vec4(orig, tilt, edge);
15294
+ hitCentre = cen;
14795
15295
  }
14796
15296
  }
14797
15297
 
@@ -15273,6 +15773,14 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
15273
15773
  float b = now - float(k);
15274
15774
  float a = e - b * BATCH;
15275
15775
  if (a < 0.0 || a > LIFE) continue;
15776
+ // Where its grains came from, give or take the turbulence: skip the batch unless that is in
15777
+ // the shape and in the slab of it this batch was taken from (see batchOf).
15778
+ vec2 from = (p - vec2(dirX * uWind * (0.15 * a + 0.55 * a * a), 0.05 * a + 0.03 * a * a) - origin) / vec2(shapeW, shapeH);
15779
+ vec2 sway = uTurbulence * a * vec2(0.04, 0.11) / vec2(shapeW, shapeH) + 1e-3;
15780
+ if (uFromRight) from.x = 1.0 - from.x;
15781
+ float slabLo = max((b * BATCH - 0.775) / 1.5, 0.0);
15782
+ float slabHi = min(((b + 1.0) * BATCH - 0.125) / 1.5, 1.0);
15783
+ if (from.x + sway.x < slabLo || from.x - sway.x > slabHi || from.y + sway.y < 0.0 || from.y - sway.y > 1.0) continue;
15276
15784
  // Back from where a grain is to where it came from.
15277
15785
  vec2 s = p - blown(a, p);
15278
15786
  s = p - blown(a, s);
@@ -16256,12 +16764,64 @@ uniform float uSpeed; // @control min=0 max=3 default=0.6 label="Drift spe
16256
16764
  `const float BOTTOM = 1.5;
16257
16765
  const float TOP = 3.2;
16258
16766
 
16259
- float cloudDensity(vec3 p, int octaves) {
16260
- vec3 q = p * 0.42 + vec3(iTime * uSpeed * 0.12, 0.0, iTime * uSpeed * 0.05);
16261
- float n = fbm3(q, octaves);
16767
+ // The noise's lattice cell last used, and its eight corner hashes. Successive
16768
+ // samples along a ray (and toward the sun) mostly fall in the same cell of the
16769
+ // coarse octaves, so they reuse its hashes instead of hashing it again.
16770
+ struct Cell {
16771
+ vec3 id;
16772
+ vec4 lo;
16773
+ vec4 hi;
16774
+ };
16775
+
16776
+ Cell noCell() {
16777
+ return Cell(vec3(1e9), vec4(0.0), vec4(0.0));
16778
+ }
16779
+
16780
+ // vnoise3(p), with its corners taken from the cell when p is still in it.
16781
+ float cellNoise(vec3 p, inout Cell c) {
16782
+ vec3 i = floor(p);
16783
+ if (i != c.id) {
16784
+ c.id = i;
16785
+ c.lo = vec4(hash13(i), hash13(i + vec3(1.0, 0.0, 0.0)), hash13(i + vec3(0.0, 1.0, 0.0)), hash13(i + vec3(1.0, 1.0, 0.0)));
16786
+ c.hi = vec4(hash13(i + vec3(0.0, 0.0, 1.0)), hash13(i + vec3(1.0, 0.0, 1.0)), hash13(i + vec3(0.0, 1.0, 1.0)), hash13(i + vec3(1.0, 1.0, 1.0)));
16787
+ }
16788
+ vec3 f = fract(p);
16789
+ vec3 u = f * f * (3.0 - 2.0 * f);
16790
+ float a = mix(c.lo.x, c.lo.y, u.x);
16791
+ float b = mix(c.lo.z, c.lo.w, u.x);
16792
+ float cc = mix(c.hi.x, c.hi.y, u.x);
16793
+ float d = mix(c.hi.z, c.hi.w, u.x);
16794
+ return mix(mix(a, b, u.y), mix(cc, d, u.y), u.z);
16795
+ }
16796
+
16797
+ // fbm3(p, octaves) for three octaves or more, the first three through cells c0, c1 and c2.
16798
+ float cloudNoise(vec3 p, int octaves, inout Cell c0, inout Cell c1, inout Cell c2) {
16799
+ float sum = 0.5 * cellNoise(p, c0);
16800
+ p = p * 2.03 + vec3(11.7, 3.1, 7.9);
16801
+ sum += 0.25 * cellNoise(p, c1);
16802
+ p = p * 2.03 + vec3(11.7, 3.1, 7.9);
16803
+ sum += 0.125 * cellNoise(p, c2);
16804
+ p = p * 2.03 + vec3(11.7, 3.1, 7.9);
16805
+ float amp = 0.0625;
16806
+ for (int i = 3; i < 6; i++) {
16807
+ if (i >= octaves) break;
16808
+ sum += amp * vnoise3(p);
16809
+ p = p * 2.03 + vec3(11.7, 3.1, 7.9);
16810
+ amp *= 0.5;
16811
+ }
16812
+ return sum;
16813
+ }
16814
+
16815
+ // The cloud's density at p from its noise to the given octaves; rest stands in
16816
+ // for the octaves left out (their average), so the cloud keeps its mass.
16817
+ float cloudDensity(vec3 p, int octaves, float rest, inout Cell c0, inout Cell c1, inout Cell c2) {
16262
16818
  float h = clamp((p.y - BOTTOM) / (TOP - BOTTOM), 0.0, 1.0);
16263
16819
  // Rounded tops and flat-ish bases.
16264
16820
  float shape = smoothstep(0.0, 0.15, h) * (1.0 - smoothstep(0.45, 1.0, h));
16821
+ // Outside the slab there's no cloud, whatever the noise.
16822
+ if (shape <= 0.0) return 0.0;
16823
+ vec3 q = p * 0.42 + vec3(iTime * uSpeed * 0.12, 0.0, iTime * uSpeed * 0.05);
16824
+ float n = cloudNoise(q, octaves, c0, c1, c2) + rest;
16265
16825
  return clamp((n - (1.0 - uCoverage) * 0.75) * 3.0 * shape, 0.0, 1.0) * uDensity;
16266
16826
  }
16267
16827
 
@@ -16280,21 +16840,35 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16280
16840
  if (rd.y > 0.02) {
16281
16841
  float t0 = BOTTOM / rd.y;
16282
16842
  float t1 = min(TOP / rd.y, t0 + 14.0);
16283
- float dt = (t1 - t0) / 40.0;
16843
+ // Steps of about 0.35 (as long as a ray toward the horizon takes in 40), so
16844
+ // short, steep rays take fewer: between 28 and 40 (fewer shows the jitter as grain).
16845
+ float steps = clamp(ceil((t1 - t0) / 0.35), 28.0, 40.0);
16846
+ float dt = (t1 - t0) / steps;
16284
16847
  float t = t0 + dt * fract(sin(dot(fragCoord, vec2(12.9898, 78.233))) * 43758.5453);
16285
16848
  // Henyey-Greenstein: a forward lobe for the silver lining plus a soft back lobe.
16286
16849
  float g = 0.65;
16287
16850
  float hg = (1.0 - g * g) / pow(1.0 + g * g - 2.0 * g * mu, 1.5) / 12.566;
16288
16851
  float phase = mix(0.08, hg * 4.0, 0.7);
16852
+ // Noise cells along the ray (m) and toward the sun (l).
16853
+ Cell m0 = noCell();
16854
+ Cell m1 = noCell();
16855
+ Cell m2 = noCell();
16856
+ Cell l0 = noCell();
16857
+ Cell l1 = noCell();
16858
+ Cell l2 = noCell();
16289
16859
  for (int i = 0; i < 40; i++) {
16290
- if (trans < 0.02 || t > 60.0) break;
16860
+ if (trans < 0.02 || t > 60.0 || float(i) >= steps) break;
16291
16861
  vec3 pos = rd * t;
16292
- int oct = t < 18.0 ? 5 : 3;
16293
- float d = cloudDensity(pos, oct);
16862
+ // Fine detail where it shows: five octaves near and in front, fewer once
16863
+ // the cloud in front hides most of this sample, three far away.
16864
+ int oct = t < 18.0 ? (trans > 0.5 ? 5 : trans > 0.15 ? 4 : 3) : 3;
16865
+ float rest = t < 18.0 ? 0.5 * (exp2(-float(oct)) - exp2(-5.0)) : 0.0;
16866
+ float d = cloudDensity(pos, oct, rest, m0, m1, m2);
16294
16867
  if (d > 0.01) {
16295
- // Light march toward the sun for self-shadowing.
16868
+ // Light march toward the sun for self-shadowing: the cloud between 0.11 and
16869
+ // 0.99 toward it, sampled at the middles of its two halves.
16296
16870
  float shade = 0.0;
16297
- for (int j = 1; j <= 4; j++) shade += cloudDensity(pos + sun * 0.22 * float(j), 3);
16871
+ for (int j = 0; j < 2; j++) shade += 2.0 * cloudDensity(pos + sun * (0.33 + 0.44 * float(j)), 3, 0.0, l0, l1, l2);
16298
16872
  float light = exp(-shade * 1.4);
16299
16873
  float powder = 1.0 - exp(-d * 4.0);
16300
16874
  vec3 lit = mix(uShadowColor, uCloudColor, light) + uSunColor * light * powder * phase * 2.5;
@@ -16769,6 +17343,75 @@ float coverage(vec2 p, float radius) {
16769
17343
  }
16770
17344
  return sum / 16.0;
16771
17345
  }`.trim();
17346
+ function goldenTaps(n11) {
17347
+ const taps = Array.from({ length: n11 }, (_, i) => {
17348
+ const r = Math.sqrt((i + 0.5) / n11);
17349
+ const a = i * 2.39996;
17350
+ return `vec2(${(r * Math.cos(a)).toFixed(5)}, ${(r * Math.sin(a)).toFixed(5)})`;
17351
+ });
17352
+ return `const vec2 GOLDEN${n11}[${n11}] = vec2[${n11}](${taps.join(", ")});`;
17353
+ }
17354
+ function coverageTaps(name, n11, blur) {
17355
+ return `
17356
+ ${goldenTaps(n11)}
17357
+ float ${name}(vec2 p, float radius) {
17358
+ float sum = 0.0;
17359
+ if (gMaskPlace.w < 0.5) {
17360
+ for (int i = 0; i < ${n11}; i++) sum += shapeAt(p + radius * GOLDEN${n11}[i], radius * ${blur.toFixed(2)});
17361
+ } else {
17362
+ float lod = log2(max(radius * ${blur.toFixed(2)} * gMaskPlace.z, 1.0));
17363
+ for (int i = 0; i < ${n11}; i++) sum += shapeLod(p + radius * GOLDEN${n11}[i], lod);
17364
+ }
17365
+ return sum / ${n11}.0;
17366
+ }`.trim();
17367
+ }
17368
+ var LITE_TAPS = 10;
17369
+ var MASK_FAST = `
17370
+ vec3 gKeyColour;
17371
+ float gKeyTolerance;
17372
+ // Picture units per frame unit (xy), texels per frame unit (z), 1 with a picture (w).
17373
+ vec4 gMaskPlace;
17374
+
17375
+ // The colour in the picture's top-left corner: the ground to key out; and where the picture sits.
17376
+ void setKey(float tolerance) {
17377
+ gKeyTolerance = tolerance;
17378
+ vec4 c = textureLod(uMask, vec2(0.01, 0.99), 0.0);
17379
+ gKeyColour = c.rgb / max(c.a, 1e-3);
17380
+ vec2 size = vec2(textureSize(uMask, 0));
17381
+ float aspect = size.x / size.y;
17382
+ float h = min(uSize, iResolution.x / iResolution.y * uSize / aspect);
17383
+ gMaskPlace = vec4(1.0 / (h * aspect), 1.0 / h, size.y / h, size.x > 1.0 ? 1.0 : 0.0);
17384
+ }
17385
+
17386
+ // Where frame point p falls in the picture (xy) and texels per frame unit (z).
17387
+ vec3 maskPlace(vec2 p) {
17388
+ return vec3(p * gMaskPlace.xy + 0.5, gMaskPlace.z);
17389
+ }
17390
+
17391
+ // The picture's coverage at p read at mip level lod (needs a picture).
17392
+ float shapeLod(vec2 p, float lod) {
17393
+ vec2 uv = p * gMaskPlace.xy + 0.5;
17394
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) return 0.0;
17395
+ vec4 c = textureLod(uMask, uv, lod);
17396
+ float a = c.a;
17397
+ if (gKeyTolerance > 0.0) {
17398
+ float away = distance(c.rgb / max(c.a, 1e-3), gKeyColour);
17399
+ a *= smoothstep(gKeyTolerance, gKeyTolerance + 0.12, away);
17400
+ }
17401
+ return a;
17402
+ }
17403
+
17404
+ float shapeAt(vec2 p, float blur) {
17405
+ if (gMaskPlace.w < 0.5) {
17406
+ float d = length(p) - 0.3 * uSize;
17407
+ return clamp(0.5 - d / max(blur, 0.004), 0.0, 1.0);
17408
+ }
17409
+ return shapeLod(p, log2(max(blur * gMaskPlace.z, 1.0)));
17410
+ }
17411
+
17412
+ ${coverageTaps("coverage", 16, 0.4)}
17413
+
17414
+ ${coverageTaps("coverageLite", LITE_TAPS, 0.5)}`.trim();
16772
17415
  var aura = {
16773
17416
  kind: "shader",
16774
17417
  id: "aura",
@@ -16793,7 +17436,7 @@ uniform float uIntensity; // @control min=0 max=3 default=1.1 label="Intensit
16793
17436
  HASH,
16794
17437
  NOISE,
16795
17438
  FBM,
16796
- MASK,
17439
+ MASK_FAST,
16797
17440
  `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
16798
17441
  setKey(uKey);
16799
17442
  vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
@@ -16805,10 +17448,11 @@ uniform float uIntensity; // @control min=0 max=3 default=1.1 label="Intensit
16805
17448
  float n2 = fbm(vec2(q.x * 1.7 + 4.1, q.y * 0.9 - t * 2.9), 4);
16806
17449
  float pulse = 1.0 + uPulse * (0.3 * sin(t * 3.0) + 0.12 * sin(t * 7.3 + 1.0));
16807
17450
  // The silhouette blurred at three widths, swayed and stretched upward by the noise, so light gathers above it.
17451
+ // The two wide blurs, torn by the noise anyway, take fewer taps.
16808
17452
  vec2 warp = vec2((n2 - 0.5) * uSpread * 1.5, -(n - 0.3) * uHeight);
16809
17453
  float inner = coverage(p + warp * 0.3, uSpread * 0.5);
16810
- float outer = coverage(p + warp, uSpread * 1.4);
16811
- float tall = coverage(p + warp * 1.5 - vec2(0.0, uHeight), uSpread * 2.2);
17454
+ float outer = coverageLite(p + warp, uSpread * 1.4);
17455
+ float tall = coverageLite(p + warp * 1.5 - vec2(0.0, uHeight), uSpread * 2.2);
16812
17456
  float field = (inner * 0.9 + outer * 0.8 + tall * 0.6) * pulse;
16813
17457
  // Torn into tongues: the noise breaks the thin outer field into licks, the thick inner field holds.
16814
17458
  float tongues = smoothstep(0.12, 0.95, field * (0.45 + 1.1 * n));
@@ -16818,7 +17462,7 @@ uniform float uIntensity; // @control min=0 max=3 default=1.1 label="Intensit
16818
17462
  light *= uIntensity;
16819
17463
  vec3 col = uBackground + (1.0 - exp(-light));
16820
17464
  // The figure in front, its own colours, rim-lit by the aura.
16821
- if (textureSize(uMask, 0).x > 1 && body > 0.0) {
17465
+ if (gMaskPlace.w > 0.5 && body > 0.0) {
16822
17466
  vec3 m = maskPlace(p);
16823
17467
  vec4 c = texture(uMask, m.xy);
16824
17468
  vec3 own = c.rgb / max(c.a, 1e-3);
@@ -17059,7 +17703,7 @@ uniform float uSpeed; // @control min=0 max=2 default=0.4 label="Ray drif
17059
17703
  uniform float uIntensity; // @control min=0 max=3 default=1.3 label="Intensity"`,
17060
17704
  HASH,
17061
17705
  NOISE,
17062
- MASK,
17706
+ MASK_FAST,
17063
17707
  `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
17064
17708
  setKey(uKey);
17065
17709
  float px = 1.0 / iResolution.y;
@@ -17068,17 +17712,21 @@ uniform float uIntensity; // @control min=0 max=3 default=1.3 label="Intensit
17068
17712
  vec2 toL = L - p;
17069
17713
  float dl = length(toL);
17070
17714
  // Light reaching here: march toward the light, the shape shadowing what is behind it.
17715
+ // 24 jittered steps (decay and sum scaled to match 0.965 per 1/40 of the way); steps three light
17716
+ // sizes or more from it add almost nothing, so they skip reading the shape.
17071
17717
  float shafts = 0.0;
17072
17718
  float decay = 1.0;
17073
17719
  float jitter = hash12(fragCoord);
17074
- for (int i = 0; i < 40; i++) {
17075
- float s = (float(i) + jitter) / 40.0;
17076
- vec2 q = p + toL * s;
17077
- float open = 1.0 - shapeAt(q, 2.0 * px + 0.01 * s);
17078
- shafts += open * exp(-pow(length(q - L) / uGlowSize, 2.0)) * decay;
17079
- decay *= 0.965;
17080
- }
17081
- shafts /= 20.0;
17720
+ float far = 3.0 * uGlowSize;
17721
+ for (int i = 0; i < 24; i++) {
17722
+ float s = (float(i) + jitter) / 24.0;
17723
+ float r = dl * (1.0 - s);
17724
+ float open = r < far ? 1.0 - shapeAt(p + toL * s, 2.0 * px + 0.01 * s) : 1.0;
17725
+ float g = r / uGlowSize;
17726
+ shafts += open * exp(-g * g) * decay;
17727
+ decay *= 0.94236;
17728
+ }
17729
+ shafts /= 12.0;
17082
17730
  // Streaks round the light, drifting slowly.
17083
17731
  vec2 dir = -toL / max(dl, 1e-4);
17084
17732
  float a = iTime * uSpeed * 0.2;
@@ -17086,7 +17734,7 @@ uniform float uIntensity; // @control min=0 max=3 default=1.3 label="Intensit
17086
17734
  float rays = shafts * mix(1.0, 0.25 + 1.5 * streak * streak, uRays);
17087
17735
  // Spill: light wrapping just round the edges.
17088
17736
  float body = shapeAt(p, 1.2 * px);
17089
- float spill = coverage(p, 0.03) * 0.8 + coverage(p, 0.08) * 0.5;
17737
+ float spill = coverage(p, 0.03) * 0.8 + coverageLite(p, 0.08) * 0.5;
17090
17738
  float glowAt = exp(-pow(dl / (uGlowSize * 1.6), 2.0));
17091
17739
  vec3 light = uLight * (rays * 1.4 + spill * (1.0 - body) * glowAt * 1.2) * uIntensity;
17092
17740
  vec3 col = uBackground + (1.0 - exp(-light));
@@ -19534,4 +20182,4 @@ export {
19534
20182
  effectControls,
19535
20183
  effectDefaults
19536
20184
  };
19537
- //# sourceMappingURL=chunk-KD7E3RWF.js.map
20185
+ //# sourceMappingURL=chunk-ECBG5K6E.js.map