@pixerate/flair 0.21.0 → 0.22.0

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