@pixerate/flair 0.31.0 → 0.33.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +5 -1
  3. package/dist/catalog/index.d.ts +2 -2
  4. package/dist/catalog/index.js +2 -2
  5. package/dist/{chunk-7F4ITXGK.js → chunk-4FFBIVPI.js} +3253 -33
  6. package/dist/chunk-4FFBIVPI.js.map +1 -0
  7. package/dist/{chunk-PIWJOW76.js → chunk-6ZG2RIWA.js} +6 -6
  8. package/dist/{chunk-45YUAQNZ.js → chunk-ARYM5VEJ.js} +3 -3
  9. package/dist/{chunk-DA4UUSDO.js → chunk-JETRHCP3.js} +9 -277
  10. package/dist/chunk-JETRHCP3.js.map +1 -0
  11. package/dist/{chunk-AUH3PEFW.js → chunk-MOLJPWME.js} +5 -3
  12. package/dist/{chunk-AUH3PEFW.js.map → chunk-MOLJPWME.js.map} +1 -1
  13. package/dist/{chunk-X3C2VFTM.js → chunk-N5HXMQOK.js} +7 -7
  14. package/dist/{chunk-ORAFCHX3.js → chunk-VKIDRHBG.js} +278 -2
  15. package/dist/chunk-VKIDRHBG.js.map +1 -0
  16. package/dist/cli.js +2 -2
  17. package/dist/editor/index.d.ts +1 -1
  18. package/dist/editor/index.js +5 -4
  19. package/dist/editor/index.js.map +1 -1
  20. package/dist/{effect-DHYWPXD3.js → effect-6PRVCU55.js} +8 -8
  21. package/dist/embed/index.d.ts +1 -1
  22. package/dist/embed/index.js +4 -4
  23. package/dist/index.js +19 -19
  24. package/dist/node.d.ts +1 -1
  25. package/dist/node.js +7 -7
  26. package/dist/react/index.d.ts +1 -1
  27. package/dist/react/index.js +5 -5
  28. package/dist/svelte/index.d.ts +1 -1
  29. package/dist/svelte/index.js +5 -5
  30. package/dist/{types-BDIzGWIS.d.ts → types-c8cnPzuE.d.ts} +1 -1
  31. package/dist/vector/index.js +14 -14
  32. package/package.json +1 -1
  33. package/dist/chunk-7F4ITXGK.js.map +0 -1
  34. package/dist/chunk-DA4UUSDO.js.map +0 -1
  35. package/dist/chunk-ORAFCHX3.js.map +0 -1
  36. /package/dist/{chunk-PIWJOW76.js.map → chunk-6ZG2RIWA.js.map} +0 -0
  37. /package/dist/{chunk-45YUAQNZ.js.map → chunk-ARYM5VEJ.js.map} +0 -0
  38. /package/dist/{chunk-X3C2VFTM.js.map → chunk-N5HXMQOK.js.map} +0 -0
  39. /package/dist/{effect-DHYWPXD3.js.map → effect-6PRVCU55.js.map} +0 -0
@@ -10,9 +10,11 @@ import {
10
10
  rgbToHex
11
11
  } from "./chunk-VYOMXE6I.js";
12
12
  import {
13
+ formatCount,
14
+ lerpAttr,
13
15
  morphPaths,
14
16
  morphSequence
15
- } from "./chunk-ORAFCHX3.js";
17
+ } from "./chunk-VKIDRHBG.js";
16
18
 
17
19
  // src/catalog/inputs.ts
18
20
  var EFFECT_INPUTS = ["image", "video", "svg", "text", "mask", "path", "audio"];
@@ -1545,10 +1547,10 @@ var numberTicker = {
1545
1547
  const chars = Array.from(String(v.value)).slice(0, TICKER_SLOTS);
1546
1548
  const widths = chars.map((c) => (/[0-9]/.test(c) ? 0.62 : Math.max(0.3, textWidthEm(c))) * TICKER_SIZE);
1547
1549
  const total = widths.reduce((a, b) => a + b, 0);
1548
- const fit7 = Math.min(1, 296 / Math.max(1, total));
1550
+ const fit8 = Math.min(1, 296 / Math.max(1, total));
1549
1551
  const duration = Math.round(Math.min(5, Math.max(0.2, Number(v.duration) || 1.6)) * 1e3);
1550
1552
  const stagger = Math.round(Math.min(0.5, Math.max(0, Number(v.stagger) || 0)) * 1e3);
1551
- const out = { fit: +fit7.toFixed(4), ms: duration };
1553
+ const out = { fit: +fit8.toFixed(4), ms: duration };
1552
1554
  let x = 160 - total / 2;
1553
1555
  let digit = 0;
1554
1556
  for (let i = 0; i < TICKER_SLOTS; i++) {
@@ -1671,6 +1673,9 @@ var EFFECT_CATEGORIES = [
1671
1673
  "broadcast",
1672
1674
  "annotation",
1673
1675
  "ui",
1676
+ "3d",
1677
+ "weather",
1678
+ "water",
1674
1679
  "transition",
1675
1680
  "loader",
1676
1681
  "data",
@@ -5951,8 +5956,8 @@ function packGlyph(rows) {
5951
5956
  return `ivec2(${a}, ${b})`;
5952
5957
  }
5953
5958
  var vec3Hex = (hex) => {
5954
- const n15 = parseInt(hex.slice(1), 16);
5955
- const c = [n15 >> 16, n15 >> 8 & 255, n15 & 255].map((v) => +(v / 255).toFixed(3));
5959
+ const n17 = parseInt(hex.slice(1), 16);
5960
+ const c = [n17 >> 16, n17 >> 8 & 255, n17 & 255].map((v) => +(v / 255).toFixed(3));
5956
5961
  return `vec3(${c.map((v) => Number.isInteger(v) ? `${v}.0` : String(v)).join(", ")})`;
5957
5962
  };
5958
5963
  var paletteArray = (name, hexes) => `const vec3 ${name}[${hexes.length}] = vec3[${hexes.length}](
@@ -7010,7 +7015,7 @@ var chompLoader = {
7010
7015
  var pixelEffects = [pixelText, coinSpin, spaceInvader, pixelHeartHealth, chompLoader];
7011
7016
 
7012
7017
  // src/catalog/pyro.ts
7013
- var round = (n15) => Math.round(n15 * 10) / 10;
7018
+ var round = (n17) => Math.round(n17 * 10) / 10;
7014
7019
  var fireworkBurst = {
7015
7020
  kind: "vector",
7016
7021
  id: "firework-burst",
@@ -7065,7 +7070,7 @@ var fireworkBurst = {
7065
7070
  var pyroEffects = [fireworkBurst];
7066
7071
 
7067
7072
  // src/catalog/timewarp.ts
7068
- var r111 = (n15) => Math.round(n15 * 10) / 10;
7073
+ var r111 = (n17) => Math.round(n17 * 10) / 10;
7069
7074
  var at2 = (degrees, radius) => {
7070
7075
  const a = degrees * Math.PI / 180;
7071
7076
  return [r111(100 + radius * Math.sin(a)), r111(100 - radius * Math.cos(a))];
@@ -7199,16 +7204,16 @@ function around(c, r, from, to, avoid) {
7199
7204
  if (((avoid - from) % 360 + 360) % 360 < delta) delta -= 360;
7200
7205
  return arc2(c[0], c[1], r, r, from, from + delta);
7201
7206
  }
7202
- function crescent(c1, r114, c2, r2_) {
7207
+ function crescent(c1, r116, c2, r2_) {
7203
7208
  const [dx, dy] = [c2[0] - c1[0], c2[1] - c1[1]];
7204
7209
  const d = Math.hypot(dx, dy);
7205
- const a = (r114 * r114 - r2_ * r2_ + d * d) / (2 * d);
7206
- const h = Math.sqrt(Math.max(0, r114 * r114 - a * a));
7210
+ const a = (r116 * r116 - r2_ * r2_ + d * d) / (2 * d);
7211
+ const h = Math.sqrt(Math.max(0, r116 * r116 - a * a));
7207
7212
  const [px2, py2] = [c1[0] + a * dx / d, c1[1] + a * dy / d];
7208
7213
  const i1 = [px2 - h * dy / d, py2 + h * dx / d];
7209
7214
  const i2 = [px2 + h * dy / d, py2 - h * dx / d];
7210
7215
  const angle = (c, p) => Math.atan2(p[1] - c[1], p[0] - c[0]) / DEG2;
7211
- const outer = around(c1, r114, angle(c1, i1), angle(c1, i2), angle(c1, c2));
7216
+ const outer = around(c1, r116, angle(c1, i1), angle(c1, i2), angle(c1, c2));
7212
7217
  const inner = around(c2, r2_, angle(c2, i2), angle(c2, i1), angle(c2, c1) + 180);
7213
7218
  return polyline3([...outer, ...inner.slice(1, -1)], true);
7214
7219
  }
@@ -19735,26 +19740,26 @@ float coverage(vec2 p, float radius) {
19735
19740
  }
19736
19741
  return sum / 16.0;
19737
19742
  }`.trim();
19738
- function goldenTaps(n15) {
19739
- const taps = Array.from({ length: n15 }, (_, i) => {
19740
- const r = Math.sqrt((i + 0.5) / n15);
19743
+ function goldenTaps(n17) {
19744
+ const taps = Array.from({ length: n17 }, (_, i) => {
19745
+ const r = Math.sqrt((i + 0.5) / n17);
19741
19746
  const a = i * 2.39996;
19742
19747
  return `vec2(${(r * Math.cos(a)).toFixed(5)}, ${(r * Math.sin(a)).toFixed(5)})`;
19743
19748
  });
19744
- return `const vec2 GOLDEN${n15}[${n15}] = vec2[${n15}](${taps.join(", ")});`;
19749
+ return `const vec2 GOLDEN${n17}[${n17}] = vec2[${n17}](${taps.join(", ")});`;
19745
19750
  }
19746
- function coverageTaps(name, n15, blur) {
19751
+ function coverageTaps(name, n17, blur) {
19747
19752
  return `
19748
- ${goldenTaps(n15)}
19753
+ ${goldenTaps(n17)}
19749
19754
  float ${name}(vec2 p, float radius) {
19750
19755
  float sum = 0.0;
19751
19756
  if (gMaskPlace.w < 0.5) {
19752
- for (int i = 0; i < ${n15}; i++) sum += shapeAt(p + radius * GOLDEN${n15}[i], radius * ${blur.toFixed(2)});
19757
+ for (int i = 0; i < ${n17}; i++) sum += shapeAt(p + radius * GOLDEN${n17}[i], radius * ${blur.toFixed(2)});
19753
19758
  } else {
19754
19759
  float lod = log2(max(radius * ${blur.toFixed(2)} * gMaskPlace.z, 1.0));
19755
- for (int i = 0; i < ${n15}; i++) sum += shapeLod(p + radius * GOLDEN${n15}[i], lod);
19760
+ for (int i = 0; i < ${n17}; i++) sum += shapeLod(p + radius * GOLDEN${n17}[i], lod);
19756
19761
  }
19757
- return sum / ${n15}.0;
19762
+ return sum / ${n17}.0;
19758
19763
  }`.trim();
19759
19764
  }
19760
19765
  var LITE_TAPS = 10;
@@ -22799,8 +22804,2187 @@ var scrollSteps = {
22799
22804
  };
22800
22805
  var scrollEffects = [scrollDraw, scrollCounter, scrollSteps];
22801
22806
 
22807
+ // src/catalog/shaders/three-d.ts
22808
+ var SPHERE_HIT = `
22809
+ vec2 sphereHit(vec3 ro, vec3 rd, float r) {
22810
+ float b = dot(ro, rd);
22811
+ float c = dot(ro, ro) - r * r;
22812
+ float h = b * b - c;
22813
+ if (h < 0.0) return vec2(1e9, -1e9);
22814
+ h = sqrt(h);
22815
+ return vec2(-b - h, -b + h);
22816
+ }`.trim();
22817
+ var globe = {
22818
+ kind: "shader",
22819
+ id: "globe",
22820
+ name: "Globe",
22821
+ description: "A spinning Earth-like globe, dotted or realistic, with glowing arcs travelling between cities and an atmosphere rim; drag it to turn it.",
22822
+ category: "3d",
22823
+ tags: ["3d", "globe", "earth", "world", "map", "network", "data", "interactive", "press", "background"],
22824
+ source: glsl(
22825
+ `// Globe: an analytic sphere with noise continents (as dots on a lat/long grid, or shaded), great-circle
22826
+ // arcs between cities lifted off the surface, and a rim of atmosphere. Dragging turns it (iPointer, iClicks).
22827
+ uniform int uStyle; // @control min=0 max=1 default=0 label="Style (0 dots, 1 realistic)"
22828
+ uniform vec3 uBackground; // @control color default=#050816 label="Background"
22829
+ uniform vec3 uOcean; // @control color default=#0B1430 label="Ocean"
22830
+ uniform vec3 uLand; // @control color default=#5B8CFF label="Land"
22831
+ uniform vec3 uArcColor; // @control color default=#FFB547 label="Arc colour"
22832
+ uniform vec3 uAtmosphere; // @control color default=#4F8BFF label="Atmosphere"
22833
+ uniform float uSize; // @control min=0.4 max=1.6 default=1 label="Size"
22834
+ uniform float uSpin; // @control min=-1 max=1 default=0.15 label="Spin speed"
22835
+ uniform float uTilt; // @control min=-1 max=1 default=0.35 label="Tilt"
22836
+ uniform float uDots; // @control min=40 max=200 default=110 step=1 label="Dot rows"
22837
+ uniform float uLandAmount; // @control min=0 max=1 default=0.5 label="Land amount"
22838
+ uniform int uArcs; // @control min=0 max=6 default=6 label="Arcs"
22839
+ uniform float uArcSpeed; // @control min=0 max=3 default=1 label="Arc speed"
22840
+ uniform float uGlow; // @control min=0 max=2 default=1 label="Atmosphere glow"
22841
+ uniform bool uDemo; // @control default=true label="Spin when idle"`,
22842
+ HASH,
22843
+ NOISE_3D,
22844
+ SPHERE_HIT,
22845
+ `const float PI = 3.14159265;
22846
+ // Cities as (latitude, longitude) in degrees, and the pairs the arcs join.
22847
+ const vec2 CITY[6] = vec2[6](vec2(40.7, -74.0), vec2(51.5, -0.1), vec2(35.7, 139.7), vec2(-33.9, 151.2), vec2(-23.5, -46.6), vec2(25.2, 55.3));
22848
+ const ivec2 ROUTE[6] = ivec2[6](ivec2(0, 1), ivec2(1, 5), ivec2(5, 2), ivec2(2, 3), ivec2(0, 4), ivec2(4, 1));
22849
+ const float CAM = 3.4;
22850
+
22851
+ // The globe's own frame (where cities and continents live) to the view; its transpose goes back.
22852
+ mat3 view;
22853
+
22854
+ mat3 viewFrom(float yaw, float tilt) {
22855
+ float cy = cos(yaw), sy = sin(yaw), ct = cos(tilt), st = sin(tilt);
22856
+ mat3 y = mat3(cy, 0.0, -sy, 0.0, 1.0, 0.0, sy, 0.0, cy);
22857
+ mat3 x = mat3(1.0, 0.0, 0.0, 0.0, ct, st, 0.0, -st, ct);
22858
+ return x * y;
22859
+ }
22860
+
22861
+ vec3 fromLatLong(vec2 ll) {
22862
+ vec2 r = ll * PI / 180.0;
22863
+ return vec3(cos(r.x) * sin(r.y), sin(r.x), cos(r.x) * cos(r.y));
22864
+ }
22865
+
22866
+ // Above 0 on land: noise continents, ice at the poles.
22867
+ float landAt(vec3 g) {
22868
+ float n = fbm3(g * 1.7 + vec3(3.1, 7.4, 1.3), 5);
22869
+ return n - mix(0.6, 0.38, uLandAmount);
22870
+ }
22871
+
22872
+ // Where the pointer has dragged the globe: the drag so far while pressed, easing back after letting go.
22873
+ vec2 dragOffset() {
22874
+ float w = iPointer.w;
22875
+ if (w == 0.0 || iClicks[0].w < 0.5) return vec2(0.0);
22876
+ vec2 from = iClicks[0].xy;
22877
+ vec2 to = iPointer.xy;
22878
+ float ease = 1.0;
22879
+ if (w < 0.0) {
22880
+ // Let go: where the pointer was then (its trail, newest first), easing back over two seconds.
22881
+ float released = iTime + w;
22882
+ for (int i = 0; i < 32; i++) {
22883
+ if (iTrail[i].w > 0.5 && iTrail[i].z <= released) {
22884
+ to = iTrail[i].xy;
22885
+ break;
22886
+ }
22887
+ }
22888
+ ease = 1.0 - smoothstep(0.0, 2.0, -w);
22889
+ }
22890
+ return (to - from) / iResolution.y * ease;
22891
+ }
22892
+
22893
+ // The screen position (and depth toward the camera) of a point in the view frame.
22894
+ vec3 project(vec3 v, float f) {
22895
+ return vec3(v.xy * f / (CAM - v.z), v.z);
22896
+ }
22897
+
22898
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
22899
+ float px = 1.0 / iResolution.y;
22900
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
22901
+ float f = 1.15 * uSize;
22902
+ vec2 drag = dragOffset();
22903
+ bool idle = iPointer.w == 0.0 && iPointer.z < 0.01;
22904
+ float yaw = iTime * uSpin * (uDemo || !idle ? 1.0 : 0.0) + drag.x * 3.5 + 0.8;
22905
+ float tilt = clamp(uTilt * 0.6 - drag.y * 2.5, -1.3, 1.3);
22906
+ view = viewFrom(yaw, tilt);
22907
+
22908
+ vec3 ro = vec3(0.0, 0.0, CAM);
22909
+ vec3 rd = normalize(vec3(p, -f));
22910
+ vec2 hit = sphereHit(ro, rd, 1.0);
22911
+ // Closest approach of the ray to the centre, for the atmosphere outside the disc.
22912
+ float closest = length(ro - rd * dot(ro, rd));
22913
+ vec3 col = uBackground + 0.03 * (1.0 - length(p));
22914
+ // Atmosphere halo.
22915
+ float halo = exp(-max(closest - 1.0, 0.0) * 9.0) * uGlow;
22916
+ vec3 sun = normalize(vec3(-0.6, 0.45, 0.75));
22917
+
22918
+ // Size of a pixel on the sphere, in radians at the disc's centre.
22919
+ float pxAngle = px * (CAM - 1.0) / f;
22920
+ if (hit.x < hit.y) {
22921
+ vec3 pos = ro + rd * hit.x;
22922
+ vec3 n = pos;
22923
+ vec3 g = n * view;
22924
+ float facing = max(dot(n, -rd), 0.0);
22925
+ float lat = asin(clamp(g.y, -1.0, 1.0));
22926
+ float lon = atan(g.x, g.z);
22927
+ if (uStyle == 0) {
22928
+ // Dots on land, on a grid of rows whose dots keep their spacing toward the poles.
22929
+ float rowStep = PI / uDots;
22930
+ float row = floor((lat + 0.5 * PI) / rowStep);
22931
+ float rowLat = (row + 0.5) * rowStep - 0.5 * PI;
22932
+ float count = max(floor(2.0 * PI * cos(rowLat) / rowStep), 1.0);
22933
+ float lonStep = 2.0 * PI / count;
22934
+ float cell = floor((lon + PI) / lonStep);
22935
+ float cellLon = (cell + 0.5) * lonStep - PI;
22936
+ vec3 centre = vec3(cos(rowLat) * sin(cellLon), sin(rowLat), cos(rowLat) * cos(cellLon));
22937
+ float onLand = step(0.0, landAt(centre));
22938
+ vec2 d = vec2((lon - cellLon) * cos(lat), lat - rowLat);
22939
+ d.x = mod(d.x + PI, 2.0 * PI) - PI;
22940
+ float aa = pxAngle / max(facing, 0.12);
22941
+ float dotMask = 1.0 - smoothstep(rowStep * 0.3 - aa, rowStep * 0.3 + aa, length(d));
22942
+ // Dots too small to resolve blend into a soft fill.
22943
+ dotMask = mix(dotMask, 0.3, smoothstep(rowStep * 0.25, rowStep * 0.7, aa));
22944
+ vec3 body = uOcean * (0.55 + 0.45 * facing);
22945
+ float light = 0.45 + 0.55 * facing;
22946
+ col = mix(body, uLand * light, dotMask * onLand);
22947
+ } else {
22948
+ // Shaded continents: ocean with a sun glint, land by height, ice at the poles, a lit and a night side.
22949
+ float h = landAt(g);
22950
+ float coast = smoothstep(-0.015, 0.015, h);
22951
+ float relief = fbm3(g * 9.0, 3);
22952
+ vec3 shallow = mix(uOcean * 1.8, uOcean, smoothstep(-0.12, 0.0, -h));
22953
+ vec3 land = mix(uLand * 0.8, mix(uLand, vec3(0.55, 0.45, 0.3), 0.5), smoothstep(0.03, 0.15, h + relief * 0.06));
22954
+ vec3 surface = mix(shallow, land * (0.8 + 0.4 * relief), coast);
22955
+ float ice = smoothstep(0.82, 0.9, abs(g.y) + relief * 0.08);
22956
+ surface = mix(surface, vec3(0.92, 0.95, 1.0), ice);
22957
+ float diffuse = max(dot(n, sun), 0.0);
22958
+ vec3 h2 = normalize(sun - rd);
22959
+ float glint = pow(max(dot(n, h2), 0.0), 140.0) * (1.0 - coast) * (1.0 - ice);
22960
+ col = surface * (0.06 + 0.94 * diffuse) + vec3(1.0, 0.95, 0.85) * glint * 0.6;
22961
+ // City lights on the night side's land.
22962
+ float lights = step(0.86, hash12(floor(vec2(lon, lat) * 90.0))) * coast * (1.0 - ice);
22963
+ col += vec3(1.0, 0.75, 0.4) * lights * smoothstep(0.1, -0.15, dot(n, sun)) * 0.8;
22964
+ halo *= 0.6 + 0.4 * max(dot(normalize(vec3(p, 0.2)), sun), 0.0);
22965
+ }
22966
+ // Atmosphere rim, toward the edge of the disc.
22967
+ float rim = pow(1.0 - facing, 3.0);
22968
+ col += uAtmosphere * rim * 0.9 * uGlow;
22969
+ // Smooth edge.
22970
+ float edge = smoothstep(1.0, 1.0 - 2.5 * px * (CAM - 1.0) / f, closest);
22971
+ col = mix(uBackground + uAtmosphere * halo, col, edge);
22972
+ } else {
22973
+ col += uAtmosphere * halo * 0.8;
22974
+ }
22975
+
22976
+ // Arcs: great circles between cities, lifted off the surface, a glowing head and fading tail travelling along.
22977
+ float arcs = 0.0;
22978
+ float cities = 0.0;
22979
+ for (int i = 0; i < 6; i++) {
22980
+ if (i >= uArcs) break;
22981
+ vec3 a = fromLatLong(CITY[ROUTE[i].x]);
22982
+ vec3 b = fromLatLong(CITY[ROUTE[i].y]);
22983
+ float span = acos(clamp(dot(a, b), -1.0, 1.0));
22984
+ vec3 axis = normalize(b - a * dot(a, b));
22985
+ float period = 3.2;
22986
+ float phase = iTime * uArcSpeed / period + float(i) * 0.37;
22987
+ float head = fract(phase) * 1.6;
22988
+ float lift = 0.12 + 0.18 * span / PI;
22989
+ // Walk the great circle in the view frame, turning by a fixed step (one cos and sin per arc).
22990
+ vec3 va = view * a;
22991
+ vec3 vb = view * axis;
22992
+ // Pixels well away from the arc's bounding sphere skip it.
22993
+ vec3 m = view * (0.5 * (a + b));
22994
+ float radius = max(0.5 * length(a - b), 1.0 - length(m)) + lift + 0.02;
22995
+ float depth = CAM - m.z - radius;
22996
+ if (depth > 0.05 && length(p - project(m, f).xy) > radius * f / depth + 40.0 * px) continue;
22997
+ float cs = cos(span / 16.0), sn = sin(span / 16.0);
22998
+ vec2 cw = vec2(1.0, 0.0);
22999
+ vec3 prevV = va * 1.004;
23000
+ vec3 prev = project(prevV, f);
23001
+ float sPrev = 0.0;
23002
+ for (int k = 1; k <= 16; k++) {
23003
+ float s = float(k) / 16.0;
23004
+ cw = vec2(cw.x * cs - cw.y * sn, cw.y * cs + cw.x * sn);
23005
+ vec3 v = (va * cw.x + vb * cw.y) * (1.004 + lift * sin(PI * s));
23006
+ vec3 cur = project(v, f);
23007
+ vec2 e = cur.xy - prev.xy;
23008
+ float u = clamp(dot(p - prev.xy, e) / max(dot(e, e), 1e-8), 0.0, 1.0);
23009
+ float d = length(p - prev.xy - e * u);
23010
+ // Only near the segment: is that part of it hidden behind the globe?
23011
+ bool hidden = true;
23012
+ if (d < 40.0 * px) {
23013
+ vec3 near = mix(prevV, v, u);
23014
+ vec2 sh = sphereHit(ro, normalize(near - ro), 1.0);
23015
+ hidden = sh.x < sh.y && sh.x < length(near - ro) - 1e-3;
23016
+ }
23017
+ prevV = v;
23018
+ if (!hidden) {
23019
+ float along = mix(sPrev, s, u);
23020
+ float tail = smoothstep(head - 0.6, head, along) * step(along, head);
23021
+ float core = 1.0 - smoothstep(0.6 * px, 1.8 * px, d);
23022
+ float glow = exp(-d / (5.0 * px)) * 0.5;
23023
+ arcs = max(arcs, (core + glow) * tail + core * 0.12);
23024
+ // The head's spark.
23025
+ float spark = exp(-length(p - mix(prev.xy, cur.xy, clamp((head - sPrev) / (s - sPrev), 0.0, 1.0))) / (3.5 * px));
23026
+ arcs = max(arcs, spark * step(sPrev, head) * step(head, s) * 1.4);
23027
+ }
23028
+ prev = cur;
23029
+ sPrev = s;
23030
+ }
23031
+ }
23032
+ for (int i = 0; i < 6; i++) {
23033
+ vec3 c = view * fromLatLong(CITY[i]);
23034
+ if (c.z < 0.05) continue;
23035
+ vec3 q = project(c, f);
23036
+ float d = length(p - q.xy);
23037
+ float pulse = 0.5 + 0.5 * sin(iTime * 3.0 + float(i));
23038
+ cities = max(cities, (1.0 - smoothstep(1.5 * px, 3.0 * px, d)) + exp(-d / (6.0 * px)) * 0.4 * pulse);
23039
+ }
23040
+ col += uArcColor * (arcs + cities);
23041
+ fragColor = vec4(col, 1.0);
23042
+ }`
23043
+ ),
23044
+ presets: [
23045
+ { name: "Dotted dark", values: { uStyle: 0 } },
23046
+ {
23047
+ name: "Realistic",
23048
+ values: {
23049
+ uStyle: 1,
23050
+ uBackground: [0, 0, 0.02],
23051
+ uOcean: [0.02, 0.1, 0.3],
23052
+ uLand: [0.22, 0.42, 0.18],
23053
+ uArcColor: [1, 0.85, 0.45],
23054
+ uAtmosphere: [0.35, 0.6, 1]
23055
+ }
23056
+ },
23057
+ { name: "Light", values: { uStyle: 0, uBackground: [0.96, 0.97, 1], uOcean: [0.86, 0.89, 0.96], uLand: [0.15, 0.25, 0.6], uArcColor: [1, 0.3, 0.45], uAtmosphere: [0.55, 0.65, 1], uGlow: 0.5 } }
23058
+ ]
23059
+ };
23060
+ var rotatingShapes = {
23061
+ kind: "shader",
23062
+ id: "rotating-shapes",
23063
+ name: "Rotating shapes",
23064
+ description: "A torus, a rounded cube and an icosahedron floating and turning in a soft-lit studio, casting soft shadows on the backdrop.",
23065
+ category: "3d",
23066
+ tags: ["3d", "shapes", "studio", "geometry", "product", "background", "minimal"],
23067
+ source: glsl(
23068
+ `// Rotating shapes: three signed-distance primitives ray-marched inside their bounding spheres (64 steps at
23069
+ // most), a studio floor met analytically, soft shadows toward a key light and a little ambient occlusion.
23070
+ uniform vec3 uBackdrop; // @control color default=#ECE7E1 label="Backdrop"
23071
+ uniform vec3 uColorA; // @control color default=#FF6A4D label="Torus"
23072
+ uniform vec3 uColorB; // @control color default=#4C6FFF label="Cube"
23073
+ uniform vec3 uColorC; // @control color default=#FFC23D label="Icosahedron"
23074
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
23075
+ uniform float uFloat; // @control min=0 max=2 default=1 label="Float"
23076
+ uniform float uGloss; // @control min=0 max=1 default=0.6 label="Gloss"
23077
+ uniform float uSoftness; // @control min=0.1 max=2 default=0.7 label="Shadow softness"`,
23078
+ SPHERE_HIT,
23079
+ `const float PHI = 1.618034;
23080
+ const vec3 LIGHT = vec3(0.55, 0.85, 0.45);
23081
+
23082
+ vec3 centre(int i) {
23083
+ float fi = float(i);
23084
+ return vec3(-1.55 + 1.55 * fi, -0.05 + 0.18 * uFloat * sin(iTime * uSpeed * 0.9 + fi * 2.1), 0.15 * fi - 0.15);
23085
+ }
23086
+
23087
+ mat3 spin(float a, float b) {
23088
+ float ca = cos(a), sa = sin(a), cb = cos(b), sb = sin(b);
23089
+ return mat3(ca, 0.0, -sa, 0.0, 1.0, 0.0, sa, 0.0, ca) * mat3(1.0, 0.0, 0.0, 0.0, cb, sb, 0.0, -sb, cb);
23090
+ }
23091
+
23092
+ float sdTorus(vec3 p) {
23093
+ vec2 q = vec2(length(p.xz) - 0.5, p.y);
23094
+ return length(q) - 0.19;
23095
+ }
23096
+ float sdRoundBox(vec3 p) {
23097
+ vec3 q = abs(p) - vec3(0.36);
23098
+ return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - 0.08;
23099
+ }
23100
+ float sdIcosahedron(vec3 p) {
23101
+ p = abs(p);
23102
+ float d = dot(p, normalize(vec3(1.0)));
23103
+ d = max(d, dot(p, normalize(vec3(0.0, 1.0, PHI + 1.0))));
23104
+ d = max(d, dot(p, normalize(vec3(PHI + 1.0, 0.0, 1.0))));
23105
+ d = max(d, dot(p, normalize(vec3(1.0, PHI + 1.0, 0.0))));
23106
+ return d - 0.5;
23107
+ }
23108
+
23109
+ // The distance to the nearest shape, and which (0, 1, 2).
23110
+ vec2 scene(vec3 p) {
23111
+ float t = iTime * uSpeed;
23112
+ vec2 best = vec2(1e9, -1.0);
23113
+ for (int i = 0; i < 3; i++) {
23114
+ vec3 q = (p - centre(i)) * spin(t * (0.5 + 0.15 * float(i)) + float(i), t * 0.7 + float(i) * 1.3);
23115
+ float d = i == 0 ? sdTorus(q) : i == 1 ? sdRoundBox(q) : sdIcosahedron(q);
23116
+ if (d < best.x) best = vec2(d, float(i));
23117
+ }
23118
+ return best;
23119
+ }
23120
+
23121
+ // The stretch of a ray inside any shape's bounding sphere (x > y when it misses them all).
23122
+ vec2 bounds(vec3 ro, vec3 rd) {
23123
+ vec2 span = vec2(1e9, -1e9);
23124
+ for (int i = 0; i < 3; i++) {
23125
+ vec2 h = sphereHit(ro - centre(i), rd, 0.78);
23126
+ if (h.x < h.y) span = vec2(min(span.x, h.x), max(span.y, h.y));
23127
+ }
23128
+ return vec2(max(span.x, 0.0), span.y);
23129
+ }
23130
+
23131
+ vec3 normalAt(vec3 p) {
23132
+ vec2 e = vec2(1.0, -1.0) * 0.0015;
23133
+ return normalize(e.xyy * scene(p + e.xyy).x + e.yyx * scene(p + e.yyx).x + e.yxy * scene(p + e.yxy).x + e.xxx * scene(p + e.xxx).x);
23134
+ }
23135
+
23136
+ // Soft shadow toward the light from p (1 lit, 0 in shadow), marched only through the shapes' bounds.
23137
+ float softShadow(vec3 p, vec3 l) {
23138
+ vec2 b = bounds(p, l);
23139
+ if (b.x > b.y) return 1.0;
23140
+ float k = 10.0 / uSoftness;
23141
+ float res = 1.0;
23142
+ float t = max(b.x, 0.02);
23143
+ for (int i = 0; i < 28; i++) {
23144
+ if (t > b.y) break;
23145
+ float h = scene(p + l * t).x;
23146
+ res = min(res, k * h / t);
23147
+ if (res < 0.002) break;
23148
+ t += clamp(h, 0.03, 0.3);
23149
+ }
23150
+ return smoothstep(0.0, 1.0, res);
23151
+ }
23152
+
23153
+ vec3 studio(vec3 rd) {
23154
+ // The backdrop's sweep, lighter toward the top, with a softbox overhead for reflections.
23155
+ vec3 c = uBackdrop * mix(0.82, 1.05, smoothstep(-0.3, 0.6, rd.y));
23156
+ c += vec3(1.2) * smoothstep(0.75, 0.95, rd.y) * smoothstep(-0.2, 0.3, rd.z + 0.4);
23157
+ return c;
23158
+ }
23159
+
23160
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
23161
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
23162
+ vec3 ro = vec3(0.0, 0.75, 5.6);
23163
+ vec3 target = vec3(0.0, -0.15, 0.0);
23164
+ vec3 fw = normalize(target - ro);
23165
+ vec3 rt = normalize(cross(fw, vec3(0.0, 1.0, 0.0)));
23166
+ vec3 up = cross(rt, fw);
23167
+ vec3 rd = normalize(p.x * rt + p.y * up + 1.9 * fw);
23168
+ vec3 l = normalize(LIGHT);
23169
+
23170
+ // The backdrop: a floor at y = -1 sweeping up into the wall behind.
23171
+ vec3 col = studio(rd) * (0.9 + 0.1 * smoothstep(-0.3, 0.2, rd.y));
23172
+ float tFloor = rd.y < 0.0 ? (-1.0 - ro.y) / rd.y : 1e9;
23173
+ float tHit = 1e9;
23174
+ float id = -1.0;
23175
+ vec2 b = bounds(ro, rd);
23176
+ if (b.x < b.y) {
23177
+ float t = b.x;
23178
+ for (int i = 0; i < 64; i++) {
23179
+ vec2 h = scene(ro + rd * t);
23180
+ if (h.x < 0.001 * t) {
23181
+ tHit = t;
23182
+ id = h.y;
23183
+ break;
23184
+ }
23185
+ t += h.x;
23186
+ if (t > b.y) break;
23187
+ }
23188
+ }
23189
+ if (tHit < tFloor) {
23190
+ vec3 pos = ro + rd * tHit;
23191
+ vec3 n = normalAt(pos);
23192
+ vec3 base = id < 0.5 ? uColorA : id < 1.5 ? uColorB : uColorC;
23193
+ float diff = max(dot(n, l), 0.0) * softShadow(pos + n * 0.01, l);
23194
+ float ao = clamp(0.5 + 0.5 * scene(pos + n * 0.12).x / 0.12, 0.0, 1.0);
23195
+ ao *= clamp(0.6 + 0.4 * (pos.y + 1.0), 0.0, 1.0);
23196
+ float sky = 0.5 + 0.5 * n.y;
23197
+ vec3 lit = base * (0.25 * sky * ao + 0.85 * diff) + base * uBackdrop * 0.12 * (1.0 - sky) * ao;
23198
+ vec3 h = normalize(l - rd);
23199
+ float fres = pow(1.0 - max(dot(n, -rd), 0.0), 5.0);
23200
+ float spec = pow(max(dot(n, h), 0.0), mix(8.0, 90.0, uGloss)) * diff * mix(0.1, 0.9, uGloss);
23201
+ vec3 env = studio(reflect(rd, n));
23202
+ col = lit + spec + env * mix(0.02, 0.25, uGloss) * mix(0.3, 1.0, fres) * ao;
23203
+ } else if (tFloor < 1e8) {
23204
+ vec3 pos = ro + rd * tFloor;
23205
+ float shadow = softShadow(pos, l);
23206
+ // Contact darkening near the shapes.
23207
+ float near = scene(pos).x;
23208
+ float ao = mix(0.55, 1.0, smoothstep(0.0, 0.6, near));
23209
+ vec3 floorCol = uBackdrop * (0.62 + 0.38 * shadow) * ao;
23210
+ // The floor curves up into the wall: blend into the backdrop with distance.
23211
+ col = mix(floorCol, col, smoothstep(9.0, 16.0, tFloor));
23212
+ }
23213
+ // A soft vignette and gamma.
23214
+ col *= 1.0 - 0.25 * dot(p, p);
23215
+ fragColor = vec4(pow(clamp(col, 0.0, 1.0), vec3(0.9)), 1.0);
23216
+ }`
23217
+ ),
23218
+ presets: [
23219
+ { name: "Pastel", values: { uBackdrop: [0.96, 0.92, 0.95], uColorA: [1, 0.62, 0.7], uColorB: [0.55, 0.75, 1], uColorC: [0.62, 0.9, 0.75], uGloss: 0.4 } },
23220
+ { name: "Midnight", values: { uBackdrop: [0.12, 0.13, 0.18], uColorA: [1, 0.35, 0.55], uColorB: [0.3, 0.85, 1], uColorC: [0.9, 0.95, 1], uGloss: 0.9 } }
23221
+ ]
23222
+ };
23223
+ var isometricCity = {
23224
+ kind: "shader",
23225
+ id: "isometric-city",
23226
+ name: "Isometric city",
23227
+ description: "An isometric block city whose buildings rise out of the ground, drifting through day and night as windows, street lights and traffic come on.",
23228
+ category: "3d",
23229
+ tags: ["3d", "city", "isometric", "buildings", "day", "night", "urban", "background"],
23230
+ source: glsl(
23231
+ `// Isometric city: an orthographic camera walks the city's grid cell by cell (48 cells at most), meeting each
23232
+ // cell's building box or the street analytically. A sun circles for the day, windows light up at dusk.
23233
+ uniform vec3 uBuilding; // @control color default=#E8DCCB label="Buildings"
23234
+ uniform vec3 uAccent; // @control color default=#7FA7D9 label="Accent buildings"
23235
+ uniform vec3 uGround; // @control color default=#B9C7A5 label="Ground"
23236
+ uniform vec3 uRoad; // @control color default=#5B6170 label="Roads"
23237
+ uniform vec3 uWindow; // @control color default=#FFD27A label="Window light"
23238
+ uniform float uDayLength; // @control min=4 max=120 default=40 label="Day length (s)"
23239
+ uniform float uTimeOfDay; // @control min=0 max=1 default=0.15 label="Time of day"
23240
+ uniform float uHeight; // @control min=0.3 max=2 default=1 label="Building height"
23241
+ uniform float uRise; // @control min=0 max=10 default=3 label="Rise time (s)"
23242
+ uniform float uZoom; // @control min=0.5 max=2 default=1 label="Zoom"
23243
+ uniform float uPan; // @control min=0 max=2 default=0.4 label="Pan speed"`,
23244
+ HASH,
23245
+ `const float BLOCK = 4.0;
23246
+ const float MAXH = 7.0;
23247
+
23248
+ bool isRoad(vec2 c) {
23249
+ return mod(c.x, BLOCK) < 0.5 || mod(c.y, BLOCK) < 0.5;
23250
+ }
23251
+
23252
+ // The building in cell c: its height (0 for none) and kind (a hash).
23253
+ vec2 building(vec2 c) {
23254
+ if (isRoad(c)) return vec2(0.0);
23255
+ float h = hash12(c);
23256
+ float k = hash12(c + 17.3);
23257
+ // A little park.
23258
+ if (k < 0.08) return vec2(0.0, k);
23259
+ // Downtowns: taller where a slow wave of districts peaks.
23260
+ float district = 0.5 + 0.5 * sin(c.x * 0.13 + 1.3) * sin(c.y * 0.11 + 0.4);
23261
+ float height = (0.6 + 0.6 * h + 4.8 * pow(h, 4.0) * district) * uHeight;
23262
+ float start = hash12(c + 5.1) * uRise * 0.75;
23263
+ float grow = uRise <= 0.0 ? 1.0 : smoothstep(start, start + uRise * 0.25 + 0.01, iTime);
23264
+ return vec2(min(height, MAXH) * grow, k);
23265
+ }
23266
+
23267
+ // The sun's direction, and how much of the day it is (1 by day, 0 at night).
23268
+ vec4 sunAt() {
23269
+ float phase = fract(uTimeOfDay + iTime / uDayLength) * 6.28318;
23270
+ vec3 dir = normalize(vec3(cos(phase) * 0.8, sin(phase), 0.45));
23271
+ return vec4(dir, smoothstep(-0.22, 0.2, dir.y));
23272
+ }
23273
+
23274
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
23275
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
23276
+ vec3 d = normalize(vec3(-1.0, -0.82, -1.0));
23277
+ vec3 rt = normalize(cross(d, vec3(0.0, 1.0, 0.0)));
23278
+ vec3 up = cross(rt, d);
23279
+ vec3 target = vec3(1.0, 0.0, 0.3) * iTime * uPan + vec3(0.5, 0.0, 0.5);
23280
+ vec3 o = target + (p.x * rt + p.y * up) * 10.0 / uZoom - d * 40.0;
23281
+ // Start where the ray comes down to the tallest buildings.
23282
+ float t = (MAXH - o.y) / d.y;
23283
+ vec3 pos = o + d * t;
23284
+ vec2 cell = floor(pos.xz);
23285
+ vec2 stepDir = sign(d.xz);
23286
+ vec2 delta = abs(1.0 / d.xz);
23287
+ vec2 side = (stepDir * (cell - pos.xz) + stepDir * 0.5 + 0.5) * delta + t;
23288
+ vec3 inv = 1.0 / d;
23289
+
23290
+ vec4 sun = sunAt();
23291
+ float day = sun.w;
23292
+ float night = 1.0 - day;
23293
+ vec3 ambient = mix(vec3(0.22, 0.26, 0.45), vec3(1.0), day);
23294
+ vec3 col = uGround * ambient * 0.7;
23295
+ for (int i = 0; i < 48; i++) {
23296
+ float tExit = min(side.x, side.y);
23297
+ vec2 b = building(cell);
23298
+ if (b.x > 0.0) {
23299
+ // The building's box, inset from the cell's edges.
23300
+ vec3 lo = vec3(cell.x + 0.12, 0.0, cell.y + 0.12);
23301
+ vec3 hi = vec3(cell.x + 0.88, b.x, cell.y + 0.88);
23302
+ vec3 t0 = (lo - o) * inv;
23303
+ vec3 t1 = (hi - o) * inv;
23304
+ vec3 tn = min(t0, t1);
23305
+ vec3 tf = max(t0, t1);
23306
+ float tin = max(max(tn.x, tn.y), tn.z);
23307
+ float tout = min(min(tf.x, tf.y), tf.z);
23308
+ if (tin < tout && tout > 0.0) {
23309
+ vec3 hp = o + d * tin;
23310
+ vec3 n = tin == tn.y ? vec3(0.0, 1.0, 0.0) : tin == tn.x ? vec3(-stepDir.x, 0.0, 0.0) : vec3(0.0, 0.0, -stepDir.y);
23311
+ vec3 base = mix(uBuilding, uAccent, step(0.72, b.y)) * (0.85 + 0.3 * hash12(cell + 2.0));
23312
+ float light = max(dot(n, sun.xyz), 0.0) * day;
23313
+ // Faces keep the isometric look: the top brightest, the two sides apart.
23314
+ float face = n.y > 0.5 ? 1.0 : abs(n.x) > 0.5 ? 0.78 : 0.62;
23315
+ vec3 c = base * face * (ambient * 0.55 + vec3(1.0, 0.95, 0.85) * light * 0.6);
23316
+ if (n.y < 0.5) {
23317
+ // Windows on the sides: a grid of panes, glass by day and lit at random by night.
23318
+ float along = abs(n.x) > 0.5 ? hp.z : hp.x;
23319
+ vec2 g = vec2(along * 4.0, hp.y * 3.2);
23320
+ vec2 f = fract(g);
23321
+ float pane = step(0.25, f.x) * step(f.x, 0.75) * step(0.3, f.y) * step(f.y, 0.78) * step(0.25, hp.y) * step(hp.y, b.x - 0.15);
23322
+ float on = step(hash12(floor(g) + cell * 7.0 + n.xz * 3.0), night * 0.75 - 0.05);
23323
+ vec3 glass = mix(vec3(0.35, 0.5, 0.65) * ambient * face, uWindow * 1.6, on);
23324
+ c = mix(c, glass, pane);
23325
+ // Darker toward the ground.
23326
+ c *= 0.7 + 0.3 * smoothstep(0.0, 1.2, hp.y);
23327
+ } else {
23328
+ // A parapet around the roof.
23329
+ vec2 r = abs(fract(hp.xz) - 0.5);
23330
+ c *= 0.9 + 0.1 * step(max(r.x, r.y), 0.3);
23331
+ }
23332
+ col = c;
23333
+ break;
23334
+ }
23335
+ }
23336
+ // Reaching the ground in this cell.
23337
+ float tg = -o.y / d.y;
23338
+ if (tg <= tExit) {
23339
+ vec3 gp = o + d * tg;
23340
+ vec2 f = fract(gp.xz);
23341
+ vec3 g;
23342
+ if (isRoad(cell)) {
23343
+ g = uRoad;
23344
+ // Lane markings, and traffic in both directions.
23345
+ bool alongX = mod(cell.y, BLOCK) < 0.5;
23346
+ bool junction = mod(cell.x, BLOCK) < 0.5 && mod(cell.y, BLOCK) < 0.5;
23347
+ float across = alongX ? f.y : f.x;
23348
+ float run = alongX ? gp.x : gp.z;
23349
+ float lane = across < 0.5 ? 1.0 : -1.0;
23350
+ g = mix(g, vec3(0.92), step(abs(across - 0.5), 0.02) * step(0.5, fract(run * 2.0)) * (junction ? 0.0 : 1.0));
23351
+ float road = alongX ? cell.y : cell.x;
23352
+ float slot = run * 0.25 - iTime * 0.35 * lane + hash12(vec2(road, lane));
23353
+ float car = step(abs(across - 0.5 + lane * 0.22), 0.11) * step(abs(fract(slot) - 0.5), 0.07) * step(0.4, hash12(vec2(floor(slot), road + lane)));
23354
+ vec3 carCol = mix(vec3(0.9, 0.3, 0.3), vec3(0.3, 0.5, 0.9), hash12(vec2(floor(slot), road)));
23355
+ g = mix(g, mix(carCol, vec3(1.0, 0.9, 0.6) * 2.0, night), car);
23356
+ } else {
23357
+ g = b.y < 0.08 ? uGround * vec3(0.8, 1.0, 0.75) : uGround;
23358
+ // Shade around a building's foot.
23359
+ vec2 q = abs(f - 0.5);
23360
+ g *= b.x > 0.0 ? mix(0.6, 1.0, smoothstep(0.38, 0.5, max(q.x, q.y))) : 1.0;
23361
+ // Street lights at the corners of each lot by night.
23362
+ vec2 corner = q - 0.42;
23363
+ float lamp = exp(-length(max(corner, 0.0)) * 18.0) * step(0.0, corner.x) * step(0.0, corner.y);
23364
+ g += uWindow * lamp * night * 1.5;
23365
+ }
23366
+ col = g * mix(ambient * 0.7, ambient, day);
23367
+ break;
23368
+ }
23369
+ if (side.x < side.y) {
23370
+ side.x += delta.x;
23371
+ cell.x += stepDir.x;
23372
+ } else {
23373
+ side.y += delta.y;
23374
+ cell.y += stepDir.y;
23375
+ }
23376
+ }
23377
+ // Warm light at dawn and dusk.
23378
+ col *= mix(vec3(1.0), vec3(1.15, 0.85, 0.7), smoothstep(0.35, 0.0, abs(sun.y)) * 0.6);
23379
+ fragColor = vec4(col, 1.0);
23380
+ }`
23381
+ ),
23382
+ presets: [
23383
+ { name: "Night", values: { uTimeOfDay: 0.72, uDayLength: 120 } },
23384
+ { name: "Toy town", values: { uBuilding: [1, 0.86, 0.75], uAccent: [1, 0.55, 0.5], uGround: [0.66, 0.85, 0.55], uRoad: [0.55, 0.58, 0.66], uHeight: 0.6, uZoom: 1.4 } }
23385
+ ]
23386
+ };
23387
+ var wireframeTerrain = {
23388
+ kind: "shader",
23389
+ id: "wireframe-terrain",
23390
+ name: "Wireframe terrain",
23391
+ description: "A fly-over of a glowing wireframe mesh of hills and valleys, the lines coloured by height and fading into fog on the horizon.",
23392
+ category: "3d",
23393
+ tags: ["3d", "terrain", "wireframe", "mesh", "landscape", "sci-fi", "retro", "background"],
23394
+ source: glsl(
23395
+ `// Wireframe terrain: a noise heightfield ray-marched with steps that grow with distance (80 at most), a
23396
+ // triangle mesh's lines drawn on it from the hit point (anti-aliased with fwidth), fog toward the horizon.
23397
+ uniform vec3 uSky; // @control color default=#03040C label="Sky"
23398
+ uniform vec3 uHorizon; // @control color default=#3A1E5C label="Horizon glow"
23399
+ uniform vec3 uFill; // @control color default=#05060F label="Surface"
23400
+ uniform vec3 uLineLow; // @control color default=#00E5FF label="Lines (low)"
23401
+ uniform vec3 uLineHigh; // @control color default=#FF3BD4 label="Lines (high)"
23402
+ uniform float uHeight; // @control min=0 max=3 default=1 label="Height"
23403
+ uniform float uSpeed; // @control min=0 max=4 default=1 label="Speed"
23404
+ uniform float uDensity; // @control min=0.5 max=4 default=1.5 label="Mesh density"
23405
+ uniform float uLineWidth; // @control min=0.5 max=4 default=1.2 label="Line width (px)"
23406
+ uniform float uGlow; // @control min=0 max=2 default=1 label="Glow"
23407
+ uniform float uFog; // @control min=0 max=2 default=1 label="Fog"`,
23408
+ HASH,
23409
+ `float vnoise(vec2 p) {
23410
+ vec2 i = floor(p);
23411
+ vec2 f = fract(p);
23412
+ vec2 u = f * f * (3.0 - 2.0 * f);
23413
+ return mix(mix(hash12(i), hash12(i + vec2(1.0, 0.0)), u.x), mix(hash12(i + vec2(0.0, 1.0)), hash12(i + vec2(1.0, 1.0)), u.x), u.y);
23414
+ }
23415
+
23416
+ // The ground's height: rolling noise hills, lower along the valley the camera flies down.
23417
+ float terrain(vec2 xz) {
23418
+ vec2 q = xz * 0.12;
23419
+ float h = 0.0;
23420
+ float amp = 1.0;
23421
+ for (int i = 0; i < 3; i++) {
23422
+ h += amp * vnoise(q);
23423
+ q = mat2(0.8, 0.6, -0.6, 0.8) * q * 2.1 + 3.7;
23424
+ amp *= 0.45;
23425
+ }
23426
+ float valley = smoothstep(1.0, 9.0, abs(xz.x));
23427
+ return (h - 0.55) * 4.5 * uHeight * (0.25 + 0.75 * valley);
23428
+ }
23429
+
23430
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
23431
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
23432
+ float z = iTime * uSpeed * 4.0;
23433
+ vec3 ro = vec3(sin(z * 0.03) * 1.5, 0.0, z);
23434
+ ro.y = max(terrain(ro.xz), 0.0) * 0.4 + 2.6 * max(uHeight, 0.4);
23435
+ vec3 fw = normalize(vec3(0.0, -0.22, 1.0));
23436
+ vec3 rt = normalize(cross(vec3(0.0, 1.0, 0.0), fw));
23437
+ vec3 up = cross(fw, rt);
23438
+ vec3 rd = normalize(p.x * rt + p.y * up + 1.3 * fw);
23439
+
23440
+ // The sky: dark above, glowing toward the horizon.
23441
+ vec3 sky = mix(uHorizon, uSky, smoothstep(-0.05, 0.35, rd.y));
23442
+ sky += uHorizon * 0.6 * exp(-abs(rd.y) * 18.0);
23443
+ vec2 sp = floor(fragCoord / 2.0);
23444
+ sky += step(0.997, hash12(sp)) * smoothstep(0.0, 0.3, rd.y) * 0.7;
23445
+ vec3 col = sky;
23446
+
23447
+ float far = 70.0;
23448
+ float t = 0.5;
23449
+ float prevT = t;
23450
+ float prevGap = 1.0;
23451
+ bool hit = false;
23452
+ for (int i = 0; i < 80; i++) {
23453
+ vec3 pos = ro + rd * t;
23454
+ float gap = pos.y - terrain(pos.xz);
23455
+ if (gap < 0.0) {
23456
+ // Between the last step above the ground and this one below it.
23457
+ t = prevT + (t - prevT) * prevGap / (prevGap - gap);
23458
+ hit = true;
23459
+ break;
23460
+ }
23461
+ prevT = t;
23462
+ prevGap = gap;
23463
+ t += max(0.06 * t, gap * 0.5);
23464
+ // Above the highest hill and climbing: nothing left to meet.
23465
+ if (t > far || (rd.y > 0.0 && pos.y > 5.0 * uHeight)) break;
23466
+ }
23467
+ if (hit) {
23468
+ vec3 pos = ro + rd * t;
23469
+ float h = terrain(pos.xz);
23470
+ // A triangle mesh: lines along x, along z and one diagonal.
23471
+ vec2 g = pos.xz * uDensity;
23472
+ vec3 coord = vec3(g, g.x - g.y);
23473
+ vec3 dist = abs(fract(coord + 0.5) - 0.5);
23474
+ vec3 w = fwidth(coord);
23475
+ vec3 lines3 = dist / max(w, 1e-4);
23476
+ float lineD = min(min(lines3.x, lines3.y), lines3.z);
23477
+ float line = 1.0 - smoothstep(uLineWidth * 0.5, uLineWidth * 0.5 + 1.0, lineD);
23478
+ float glow = exp(-lineD / (uLineWidth * 3.0)) * 0.45 * uGlow;
23479
+ // Lines too dense to tell apart blend into a faint wash.
23480
+ float dense = smoothstep(0.25, 0.6, max(max(w.x, w.y), w.z));
23481
+ float amount = mix(line + glow, 0.35, dense);
23482
+ vec3 lineCol = mix(uLineLow, uLineHigh, smoothstep(-1.5, 2.5, h / max(uHeight, 0.1)));
23483
+ vec3 surf = uFill + lineCol * amount;
23484
+ float fog = 1.0 - exp(-t * 0.035 * uFog);
23485
+ col = mix(surf, sky, clamp(fog * fog + smoothstep(far * 0.7, far, t), 0.0, 1.0));
23486
+ }
23487
+ fragColor = vec4(col, 1.0);
23488
+ }`
23489
+ ),
23490
+ presets: [
23491
+ { name: "Matrix", values: { uSky: [0, 0.02, 0], uHorizon: [0, 0.25, 0.08], uFill: [0, 0.02, 0], uLineLow: [0, 0.6, 0.2], uLineHigh: [0.6, 1, 0.6] } },
23492
+ { name: "Blueprint", values: { uSky: [0.05, 0.15, 0.35], uHorizon: [0.2, 0.4, 0.7], uFill: [0.06, 0.18, 0.4], uLineLow: [0.75, 0.88, 1], uLineHigh: [1, 1, 1], uGlow: 0.3 } }
23493
+ ]
23494
+ };
23495
+ var metaballs3d = {
23496
+ kind: "shader",
23497
+ id: "metaballs-3d",
23498
+ name: "3D metaballs",
23499
+ description: "Glossy 3D metaballs drifting, merging and pulling apart like liquid, reflecting a soft studio around them.",
23500
+ category: "3d",
23501
+ tags: ["3d", "metaballs", "liquid", "blob", "glossy", "chrome", "background"],
23502
+ source: glsl(
23503
+ `// 3D metaballs: spheres joined with a smooth minimum, ray-marched inside their bounding sphere (56 steps
23504
+ // at most), shaded with a procedural studio environment for reflections and a fresnel rim.
23505
+ uniform vec3 uBackground; // @control color default=#0E0B1A label="Background"
23506
+ uniform vec3 uColorA; // @control color default=#FF4FA3 label="Colour A"
23507
+ uniform vec3 uColorB; // @control color default=#6B5BFF label="Colour B"
23508
+ uniform int uCount; // @control min=2 max=8 default=6 label="Balls"
23509
+ uniform float uMerge; // @control min=0.05 max=1 default=0.45 label="Merge"
23510
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
23511
+ uniform float uReflection; // @control min=0 max=1 default=0.7 label="Reflection"
23512
+ uniform float uSize; // @control min=0.5 max=1.5 default=1 label="Size"`,
23513
+ SPHERE_HIT,
23514
+ `vec3 ballAt(int i) {
23515
+ float fi = float(i);
23516
+ float t = iTime * uSpeed * 0.5;
23517
+ return vec3(sin(t * (0.7 + 0.13 * fi) + fi * 1.7) * 1.1, cos(t * (0.9 + 0.11 * fi) + fi * 2.3) * 0.7, sin(t * (0.5 + 0.07 * fi) + fi * 0.9) * 0.6) * uSize;
23518
+ }
23519
+
23520
+ float smin(float a, float b, float k) {
23521
+ float h = max(k - abs(a - b), 0.0) / k;
23522
+ return min(a, b) - h * h * k * 0.25;
23523
+ }
23524
+
23525
+ float scene(vec3 p) {
23526
+ float d = 1e9;
23527
+ for (int i = 0; i < 8; i++) {
23528
+ if (i >= uCount) break;
23529
+ float r = (0.42 + 0.12 * sin(float(i) * 3.1)) * uSize;
23530
+ d = smin(d, length(p - ballAt(i)) - r, uMerge);
23531
+ }
23532
+ return d;
23533
+ }
23534
+
23535
+ vec3 normalAt(vec3 p) {
23536
+ vec2 e = vec2(1.0, -1.0) * 0.002;
23537
+ return normalize(e.xyy * scene(p + e.xyy) + e.yyx * scene(p + e.yyx) + e.yxy * scene(p + e.yxy) + e.xxx * scene(p + e.xxx));
23538
+ }
23539
+
23540
+ // A studio around the balls: a dim room, a bright softbox overhead and two strip lights.
23541
+ vec3 env(vec3 r) {
23542
+ vec3 c = mix(uBackground * 0.6, uBackground * 2.0 + 0.08, smoothstep(-0.4, 0.6, r.y));
23543
+ c += vec3(2.2) * smoothstep(0.82, 0.9, r.y);
23544
+ float a = atan(r.x, r.z);
23545
+ c += vec3(1.4, 1.3, 1.2) * smoothstep(0.06, 0.0, abs(a - 1.9)) * smoothstep(-0.4, 0.0, r.y) * smoothstep(0.7, 0.3, r.y);
23546
+ c += vec3(0.9, 1.1, 1.4) * smoothstep(0.05, 0.0, abs(a + 2.2)) * smoothstep(-0.3, 0.1, r.y) * smoothstep(0.6, 0.2, r.y);
23547
+ return c;
23548
+ }
23549
+
23550
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
23551
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
23552
+ vec3 ro = vec3(0.0, 0.0, 4.2);
23553
+ vec3 rd = normalize(vec3(p, -1.6));
23554
+ vec3 col = uBackground * (1.2 - 0.6 * length(p));
23555
+ vec2 b = sphereHit(ro, rd, 2.3 * uSize);
23556
+ if (b.x < b.y) {
23557
+ float t = max(b.x, 0.0);
23558
+ bool hit = false;
23559
+ for (int i = 0; i < 56; i++) {
23560
+ float d = scene(ro + rd * t);
23561
+ if (d < 0.001) {
23562
+ hit = true;
23563
+ break;
23564
+ }
23565
+ t += d;
23566
+ if (t > b.y) break;
23567
+ }
23568
+ if (hit) {
23569
+ vec3 pos = ro + rd * t;
23570
+ vec3 n = normalAt(pos);
23571
+ float fres = pow(1.0 - max(dot(n, -rd), 0.0), 4.0);
23572
+ vec3 base = mix(uColorA, uColorB, smoothstep(-0.6, 0.6, n.x * 0.7 + n.y * 0.5 + 0.2 * sin(iTime * 0.3)));
23573
+ vec3 l = normalize(vec3(0.4, 0.8, 0.6));
23574
+ float diff = max(dot(n, l), 0.0);
23575
+ vec3 refl = env(reflect(rd, n));
23576
+ col = base * (0.15 + 0.6 * diff) + refl * mix(0.08, 0.9, uReflection) * mix(0.35, 1.0, fres) + base * fres * 0.3;
23577
+ col += pow(max(dot(reflect(rd, n), l), 0.0), 80.0) * 1.2;
23578
+ }
23579
+ }
23580
+ fragColor = vec4(col / (1.0 + col * 0.15), 1.0);
23581
+ }`
23582
+ ),
23583
+ presets: [
23584
+ { name: "Chrome", values: { uBackground: [0.06, 0.06, 0.08], uColorA: [0.35, 0.36, 0.4], uColorB: [0.55, 0.56, 0.6], uReflection: 1 } },
23585
+ { name: "Candy", values: { uBackground: [1, 0.9, 0.93], uColorA: [1, 0.45, 0.6], uColorB: [1, 0.75, 0.3], uReflection: 0.45 } }
23586
+ ]
23587
+ };
23588
+ var torusKnot = {
23589
+ kind: "shader",
23590
+ id: "torus-knot",
23591
+ name: "Torus knot",
23592
+ description: "A glowing torus knot tube turning slowly, its surface shimmering with iridescent thin-film colour.",
23593
+ category: "3d",
23594
+ tags: ["3d", "knot", "torus", "iridescent", "glow", "abstract", "background"],
23595
+ source: glsl(
23596
+ `// Torus knot: a (p, q) knot's tube as a distance from each of its p strands around the torus, ray-marched
23597
+ // inside a bounding sphere (80 steps at most); the march's near misses add up to a glow around it.
23598
+ uniform vec3 uBackground; // @control color default=#05040B label="Background"
23599
+ uniform vec3 uGlowColor; // @control color default=#7A5CFF label="Glow"
23600
+ uniform int uP; // @control min=1 max=7 default=2 label="Turns around (p)"
23601
+ uniform int uQ; // @control min=1 max=9 default=3 label="Turns through (q)"
23602
+ uniform float uThickness; // @control min=0.05 max=0.35 default=0.16 label="Thickness"
23603
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
23604
+ uniform float uIridescence; // @control min=0 max=1 default=0.8 label="Iridescence"
23605
+ uniform float uGlow; // @control min=0 max=2 default=1 label="Glow amount"`,
23606
+ SPHERE_HIT,
23607
+ `const float R = 1.0;
23608
+ const float STRAND = 0.45;
23609
+
23610
+ mat3 turn(float a, float b) {
23611
+ float ca = cos(a), sa = sin(a), cb = cos(b), sb = sin(b);
23612
+ return mat3(1.0, 0.0, 0.0, 0.0, cb, sb, 0.0, -sb, cb) * mat3(ca, 0.0, -sa, 0.0, 1.0, 0.0, sa, 0.0, ca);
23613
+ }
23614
+
23615
+ // The tube's distance (an underestimate: the march takes shorter steps), and how far along the knot (0-1).
23616
+ vec2 knot(vec3 p) {
23617
+ float r = length(p.xz);
23618
+ float a = atan(p.z, p.x);
23619
+ vec2 q = vec2(r - R, p.y);
23620
+ float best = 1e9;
23621
+ float along = 0.0;
23622
+ float P = float(uP);
23623
+ for (int k = 0; k < 7; k++) {
23624
+ if (k >= uP) break;
23625
+ float s = (a + 6.28318 * float(k)) / P;
23626
+ float ang = s * float(uQ);
23627
+ float d = length(q - STRAND * vec2(cos(ang), sin(ang)));
23628
+ if (d < best) {
23629
+ best = d;
23630
+ along = s / 6.28318;
23631
+ }
23632
+ }
23633
+ return vec2(best - uThickness, along);
23634
+ }
23635
+
23636
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
23637
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
23638
+ vec3 ro = vec3(0.0, 0.0, 5.2);
23639
+ vec3 rd = normalize(vec3(p, -1.3));
23640
+ float t0 = iTime * uSpeed;
23641
+ mat3 m = turn(t0 * 0.3, 1.15 + 0.25 * sin(t0 * 0.2));
23642
+ vec3 lro = m * ro;
23643
+ vec3 lrd = m * rd;
23644
+ vec3 col = uBackground + uGlowColor * 0.05 * (1.0 - length(p));
23645
+ float glow = 0.0;
23646
+ vec2 b = sphereHit(lro, lrd, R + STRAND + uThickness + 0.05);
23647
+ if (b.x < b.y) {
23648
+ float t = max(b.x, 0.0);
23649
+ bool hit = false;
23650
+ vec2 h = vec2(0.0);
23651
+ for (int i = 0; i < 80; i++) {
23652
+ h = knot(lro + lrd * t);
23653
+ if (h.x < 0.001) {
23654
+ hit = true;
23655
+ break;
23656
+ }
23657
+ // The field stretches where strands wind fast around a small radius: step within its slope.
23658
+ vec3 at = lro + lrd * t;
23659
+ float slope = STRAND * float(uQ) / (float(uP) * max(length(at.xz), 0.3));
23660
+ float dt = h.x / sqrt(1.0 + slope * slope) * 0.9;
23661
+ // Glow by the length of ray spent near the tube, so it does not depend on the step count.
23662
+ glow += exp(-h.x * 9.0) * min(dt, b.y - t) * 0.6;
23663
+ t += dt;
23664
+ if (t > b.y) break;
23665
+ }
23666
+ if (hit) {
23667
+ vec3 pos = lro + lrd * t;
23668
+ vec2 e = vec2(0.002, 0.0);
23669
+ vec3 n = normalize(vec3(knot(pos + e.xyy).x - knot(pos - e.xyy).x, knot(pos + e.yxy).x - knot(pos - e.yxy).x, knot(pos + e.yyx).x - knot(pos - e.yyx).x));
23670
+ float facing = max(dot(n, -lrd), 0.0);
23671
+ // Thin film: the colour shifts with the viewing angle and along the knot.
23672
+ vec3 film = 0.5 + 0.5 * cos(6.28318 * (vec3(0.0, 0.33, 0.67) + (1.0 - facing) * 1.4 + h.y * 2.0 + iTime * 0.05));
23673
+ vec3 base = mix(uGlowColor, film, uIridescence);
23674
+ vec3 l = normalize(m * vec3(0.5, 0.7, 0.6));
23675
+ float diff = max(dot(n, l), 0.0);
23676
+ float spec = pow(max(dot(reflect(lrd, n), l), 0.0), 60.0);
23677
+ float rim = pow(1.0 - facing, 3.0);
23678
+ col = base * (0.18 + 0.75 * diff) + spec * 0.9 + mix(uGlowColor, film, 0.5) * rim * 0.9;
23679
+ }
23680
+ }
23681
+ col += uGlowColor * glow * uGlow;
23682
+ fragColor = vec4(col, 1.0);
23683
+ }`
23684
+ ),
23685
+ presets: [
23686
+ { name: "Trefoil", values: { uP: 2, uQ: 3 } },
23687
+ { name: "Cinquefoil", values: { uP: 2, uQ: 5, uThickness: 0.12 } },
23688
+ { name: "Gold", values: { uP: 3, uQ: 4, uThickness: 0.11, uGlowColor: [1, 0.7, 0.25], uIridescence: 0.3 } }
23689
+ ]
23690
+ };
23691
+ var threeDShaders = [globe, rotatingShapes, isometricCity, wireframeTerrain, metaballs3d, torusKnot];
23692
+
23693
+ // src/catalog/shaders/space-more.ts
23694
+ var STARS2 = `
23695
+ float stars(vec2 fragCoord, float density) {
23696
+ vec2 cell = floor(fragCoord / 3.0);
23697
+ float h = hash12(cell);
23698
+ float on = step(1.0 - 0.012 * density, h);
23699
+ vec2 at = hash22(cell) * 3.0;
23700
+ float d = length(fract(fragCoord / 3.0) * 3.0 - at);
23701
+ return on * smoothstep(1.4, 0.0, d) * (0.4 + 0.6 * hash12(cell + 7.0)) * (0.75 + 0.25 * sin(iTime * (1.0 + 3.0 * h) + h * 40.0));
23702
+ }`.trim();
23703
+ var SPHERE_HIT2 = `
23704
+ vec2 sphereHit(vec3 ro, vec3 rd, float r) {
23705
+ float b = dot(ro, rd);
23706
+ float c = dot(ro, ro) - r * r;
23707
+ float h = b * b - c;
23708
+ if (h < 0.0) return vec2(1e9, -1e9);
23709
+ h = sqrt(h);
23710
+ return vec2(-b - h, -b + h);
23711
+ }`.trim();
23712
+ var planet = {
23713
+ kind: "shader",
23714
+ id: "planet",
23715
+ name: "Planet",
23716
+ description: "A turning planet, banded gas giant or rocky world, lit from one side with a soft terminator, a scattering atmosphere and optional rings.",
23717
+ category: "space",
23718
+ tags: ["space", "planet", "saturn", "rings", "astronomy", "sci-fi", "background"],
23719
+ source: glsl(
23720
+ `// Planet: an analytic sphere on a tilted axis, banded (fbm-warped latitude) or rocky (3D noise), with a
23721
+ // rim of scattered light brighter on the day side, and a ring plane that shadows the planet and is shadowed by it.
23722
+ uniform int uStyle; // @control min=0 max=1 default=0 label="Style (0 gas giant, 1 rocky)"
23723
+ uniform vec3 uBackground; // @control color default=#02030A label="Background"
23724
+ uniform vec3 uColorA; // @control color default=#EBD3A8 label="Colour A"
23725
+ uniform vec3 uColorB; // @control color default=#B0683E label="Colour B"
23726
+ uniform vec3 uAtmosphere; // @control color default=#8DB8FF label="Atmosphere"
23727
+ uniform vec3 uRingColor; // @control color default=#D8C6A2 label="Ring colour"
23728
+ uniform bool uRings; // @control default=true label="Rings"
23729
+ uniform float uSize; // @control min=0.4 max=1.6 default=1 label="Size"
23730
+ uniform float uSpin; // @control min=-1 max=1 default=0.12 label="Spin speed"
23731
+ uniform float uTilt; // @control min=-1 max=1 default=0.42 label="Axis tilt"
23732
+ uniform float uSunAngle; // @control min=-3.1416 max=3.1416 default=0.35 label="Sun direction"
23733
+ uniform float uAtmosphereAmount; // @control min=0 max=2 default=1 label="Atmosphere amount"`,
23734
+ HASH,
23735
+ NOISE_3D,
23736
+ STARS2,
23737
+ SPHERE_HIT2,
23738
+ `const float CAM = 4.2;
23739
+
23740
+ // The planet's axis tilted toward the viewer, and its frame (spin about the axis).
23741
+ mat3 frame(float spin) {
23742
+ float ct = cos(uTilt), st = sin(uTilt);
23743
+ float cv = cos(0.35), sv = sin(0.35);
23744
+ float cs = cos(spin), ss = sin(spin);
23745
+ mat3 roll = mat3(ct, st, 0.0, -st, ct, 0.0, 0.0, 0.0, 1.0);
23746
+ mat3 lean = mat3(1.0, 0.0, 0.0, 0.0, cv, sv, 0.0, -sv, cv);
23747
+ mat3 turn = mat3(cs, 0.0, -ss, 0.0, 1.0, 0.0, ss, 0.0, cs);
23748
+ return lean * roll * turn;
23749
+ }
23750
+
23751
+ vec3 surface(vec3 g) {
23752
+ if (uStyle == 0) {
23753
+ // Bands of latitude, warped by noise, and one great storm.
23754
+ float warp = fbm3(g * vec3(2.0, 7.0, 2.0) + vec3(0.0, 0.0, iTime * 0.02), 4);
23755
+ float v = g.y + 0.09 * (warp - 0.5);
23756
+ float bands = 0.5 + 0.35 * sin(v * 17.0) + 0.15 * sin(v * 43.0 + 1.3);
23757
+ vec3 c = mix(uColorB, uColorA, smoothstep(0.2, 0.8, bands));
23758
+ c *= 0.9 + 0.2 * fbm3(g * vec3(4.0, 24.0, 4.0), 3);
23759
+ vec2 storm = vec2(atan(g.x, g.z) - 0.9, (g.y + 0.38) * 3.2);
23760
+ float s = length(storm * vec2(1.0, 1.6));
23761
+ c = mix(c, mix(uColorB * 1.25, uColorA, 0.25), smoothstep(0.32, 0.18, s) * (0.7 + 0.3 * sin(s * 30.0)));
23762
+ return c;
23763
+ }
23764
+ // Rocky: two kinds of ground by noise, darker basins, polar caps.
23765
+ float n = fbm3(g * 2.4, 5);
23766
+ float fine = fbm3(g * 11.0, 3);
23767
+ vec3 c = mix(uColorB, uColorA, smoothstep(0.35, 0.65, n));
23768
+ c *= 0.75 + 0.45 * fine;
23769
+ c = mix(c, uColorB * 0.45, smoothstep(0.58, 0.72, n) * 0.6);
23770
+ c = mix(c, vec3(0.95), smoothstep(0.86, 0.92, abs(g.y) + fine * 0.08));
23771
+ return c;
23772
+ }
23773
+
23774
+ // How much the rings hold at radius r (in planet radii).
23775
+ float ringDensity(float r) {
23776
+ if (r < 1.35 || r > 2.35) return 0.0;
23777
+ float d = 0.5 + 0.2 * sin(r * 61.0) + 0.15 * sin(r * 23.0 + 1.0) + 0.15 * sin(r * 137.0 + 2.0) * sin(r * 9.0);
23778
+ d *= smoothstep(1.35, 1.45, r) * smoothstep(2.35, 2.2, r);
23779
+ // A dark gap partway out.
23780
+ d *= 1.0 - 0.85 * smoothstep(0.04, 0.0, abs(r - 1.95));
23781
+ return clamp(d, 0.0, 1.0);
23782
+ }
23783
+
23784
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
23785
+ float px = 1.0 / iResolution.y;
23786
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
23787
+ float f = 1.55 * uSize * (uRings ? 0.52 : 1.0);
23788
+ vec3 ro = vec3(0.0, 0.0, CAM);
23789
+ vec3 rd = normalize(vec3(p, -f));
23790
+ vec3 sun = normalize(vec3(cos(uSunAngle), 0.25, sin(uSunAngle)));
23791
+ mat3 m = frame(iTime * uSpin);
23792
+ vec3 axis = m * vec3(0.0, 1.0, 0.0);
23793
+
23794
+ vec3 col = uBackground + stars(fragCoord, 1.0);
23795
+ vec2 hit = sphereHit(ro, rd, 1.0);
23796
+ float closest = length(ro - rd * dot(ro, rd));
23797
+ // Scattered light beyond the limb, brightest toward the sun.
23798
+ vec2 dir = normalize(p + 1e-5);
23799
+ float sunSide = 0.25 + 0.75 * smoothstep(-0.6, 0.6, dot(dir, normalize(sun.xy + vec2(1e-4))) + sun.z * 0.3);
23800
+ float halo = exp(-max(closest - 1.0, 0.0) * 14.0) * sunSide * uAtmosphereAmount;
23801
+ col += uAtmosphere * halo * 0.7;
23802
+
23803
+ float tPlanet = hit.x < hit.y ? hit.x : 1e9;
23804
+ if (tPlanet < 1e8) {
23805
+ vec3 n = ro + rd * tPlanet;
23806
+ vec3 g = n * m;
23807
+ vec3 c = surface(g);
23808
+ float ndl = dot(n, sun);
23809
+ // A soft terminator, slightly warm where day turns to night.
23810
+ float day = smoothstep(-0.12, 0.35, ndl);
23811
+ float light = max(ndl, 0.0) * 0.85 + day * 0.15;
23812
+ vec3 lit = c * light + c * vec3(1.0, 0.55, 0.35) * 0.12 * smoothstep(0.25, 0.0, abs(ndl + 0.02));
23813
+ // The rings' shadow on the planet.
23814
+ if (uRings) {
23815
+ float tr = -dot(n, axis) / dot(sun, axis);
23816
+ if (tr > 0.0) {
23817
+ vec3 q = n + sun * tr;
23818
+ lit *= 1.0 - 0.75 * ringDensity(length(q));
23819
+ }
23820
+ }
23821
+ float facing = max(dot(n, -rd), 0.0);
23822
+ float rim = pow(1.0 - facing, 2.5) * (0.15 + smoothstep(-0.3, 0.6, ndl)) * uAtmosphereAmount;
23823
+ lit += uAtmosphere * rim * 0.9;
23824
+ float edge = smoothstep(1.0, 1.0 - 2.0 * px * (CAM - 1.0) / f, closest);
23825
+ col = mix(col, lit, edge);
23826
+ }
23827
+
23828
+ if (uRings) {
23829
+ float tr = -dot(ro, axis) / dot(rd, axis);
23830
+ if (tr > 0.0 && tr < tPlanet + (tPlanet < 1e8 ? 0.0 : 1e9)) {
23831
+ vec3 q = ro + rd * tr;
23832
+ float r = length(q);
23833
+ float dens = ringDensity(r);
23834
+ if (dens > 0.0) {
23835
+ // Lit unless the planet stands between the ring and the sun.
23836
+ vec2 sh = sphereHit(q, sun, 1.0);
23837
+ float shadow = sh.x < sh.y && sh.y > 0.0 ? 0.12 : 1.0;
23838
+ float light = 0.35 + 0.65 * abs(dot(sun, axis));
23839
+ vec3 rc = uRingColor * (0.85 + 0.15 * sin(r * 97.0)) * light * shadow;
23840
+ col = mix(col, rc, dens * 0.85);
23841
+ }
23842
+ }
23843
+ }
23844
+ fragColor = vec4(col, 1.0);
23845
+ }`
23846
+ ),
23847
+ presets: [
23848
+ { name: "Gas giant", values: { uStyle: 0, uRings: true } },
23849
+ {
23850
+ name: "Ice giant",
23851
+ values: { uStyle: 0, uRings: false, uColorA: [0.72, 0.9, 0.95], uColorB: [0.32, 0.62, 0.82], uAtmosphere: [0.6, 0.85, 1], uTilt: 0.9 }
23852
+ },
23853
+ {
23854
+ name: "Red planet",
23855
+ values: { uStyle: 1, uRings: false, uColorA: [0.86, 0.5, 0.3], uColorB: [0.45, 0.2, 0.12], uAtmosphere: [1, 0.6, 0.45], uAtmosphereAmount: 0.5 }
23856
+ }
23857
+ ]
23858
+ };
23859
+ var spiralGalaxy = {
23860
+ kind: "shader",
23861
+ id: "spiral-galaxy",
23862
+ name: "Spiral galaxy",
23863
+ description: "A spiral galaxy seen at a tilt, its arms of blue stars and pink nebulae wound around a warm core and laced with dark dust lanes, turning slowly.",
23864
+ category: "space",
23865
+ tags: ["space", "galaxy", "stars", "astronomy", "cosmic", "background"],
23866
+ source: glsl(
23867
+ `// Spiral galaxy: log-spiral arms (cos of arms \xD7 (angle \u2212 winding \xD7 ln r)) on a tilted disc, stars
23868
+ // scattered more thickly along the arms, fbm dust lanes on their inner edges and a bulge at the core.
23869
+ uniform vec3 uBackground; // @control color default=#020208 label="Background"
23870
+ uniform vec3 uCore; // @control color default=#FFE0B0 label="Core"
23871
+ uniform vec3 uArmColor; // @control color default=#8AB0FF label="Arms"
23872
+ uniform vec3 uNebula; // @control color default=#FF6FB0 label="Nebulae"
23873
+ uniform int uArms; // @control min=1 max=6 default=2 label="Arms"
23874
+ uniform float uWinding; // @control min=0.5 max=4 default=2.2 label="Winding"
23875
+ uniform float uTilt; // @control min=0 max=1.4 default=0.95 label="Tilt"
23876
+ uniform float uAngle; // @control min=-3.1416 max=3.1416 default=0.5 label="Angle"
23877
+ uniform float uSize; // @control min=0.4 max=2 default=1 label="Size"
23878
+ uniform float uSpeed; // @control min=-2 max=2 default=0.4 label="Rotation speed"
23879
+ uniform float uDust; // @control min=0 max=1 default=0.7 label="Dust"`,
23880
+ HASH,
23881
+ NOISE,
23882
+ FBM,
23883
+ STARS2,
23884
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
23885
+ float px = 1.0 / iResolution.y;
23886
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
23887
+ float ca = cos(uAngle), sa = sin(uAngle);
23888
+ vec2 q = mat2(ca, sa, -sa, ca) * p / (0.42 * uSize);
23889
+ // Undo the tilt: the disc is foreshortened along y.
23890
+ q.y /= max(cos(uTilt), 0.08);
23891
+ float r = length(q);
23892
+ float spin = iTime * uSpeed * 0.06;
23893
+ // The disc's own coordinates, turned with the galaxy.
23894
+ vec2 d = mat2(cos(spin), sin(spin), -sin(spin), cos(spin)) * q;
23895
+ float theta = atan(d.y, d.x);
23896
+ float arms = float(uArms);
23897
+ float wind = theta - uWinding * log(max(r, 1e-3));
23898
+ float arm = 0.5 + 0.5 * cos(arms * wind);
23899
+ float armSharp = pow(arm, 3.0);
23900
+ float disc = exp(-r * 2.6) * smoothstep(1.4, 0.6, r);
23901
+
23902
+ // Clumpy light along the arms.
23903
+ float clump = fbm(d * 4.0, 5);
23904
+ float light = disc * (0.25 + 1.4 * armSharp * (0.5 + clump));
23905
+ vec3 col = uBackground + stars(fragCoord, 1.0) * (1.0 - smoothstep(0.0, 0.6, disc * 4.0));
23906
+ col += uArmColor * light * 1.2;
23907
+ // Pink star-forming knots on the arms.
23908
+ float knots = smoothstep(0.62, 0.8, fbm(d * 9.0 + 3.0, 4)) * armSharp * disc;
23909
+ col += uNebula * knots * 2.2;
23910
+ // Stars along the arms, more toward the middle.
23911
+ for (int i = 0; i < 2; i++) {
23912
+ float scale = i == 0 ? 90.0 : 180.0;
23913
+ vec2 g = d * scale * uSize;
23914
+ vec2 cell = floor(g);
23915
+ float h = hash12(cell + float(i) * 31.0);
23916
+ float chance = disc * (0.4 + 2.5 * armSharp) * (i == 0 ? 1.6 : 3.0);
23917
+ float s = step(1.0 - min(chance, 0.9), h);
23918
+ vec2 at = hash22(cell) * 0.6 + 0.2;
23919
+ float dist = length(fract(g) - at) / (scale * uSize * px * 0.42 * uSize);
23920
+ col += mix(uArmColor, vec3(1.0), 0.6) * s * smoothstep(1.6, 0.0, dist) * (0.4 + 0.6 * hash12(cell + 9.0));
23921
+ }
23922
+ // Dust lanes on the arms' inner edges.
23923
+ float lane = pow(0.5 + 0.5 * cos(arms * wind - 0.9), 6.0);
23924
+ float dust = lane * smoothstep(0.08, 0.3, r) * smoothstep(1.2, 0.5, r) * (0.35 + 0.65 * fbm(d * 7.0 + 11.0, 4)) * uDust;
23925
+ col *= 1.0 - 0.85 * clamp(dust * 1.6, 0.0, 1.0);
23926
+ // The bulge.
23927
+ float bulge = exp(-r * r * 30.0) * 1.6 + exp(-r * 6.0) * 0.35;
23928
+ col += uCore * bulge;
23929
+ fragColor = vec4(col / (1.0 + 0.25 * col), 1.0);
23930
+ }`
23931
+ ),
23932
+ presets: [
23933
+ { name: "Face on", values: { uTilt: 0.15, uWinding: 2.6 } },
23934
+ { name: "Edge on", values: { uTilt: 1.3, uAngle: 0.15 } },
23935
+ { name: "Four arms", values: { uArms: 4, uWinding: 1.6, uArmColor: [0.75, 0.6, 1], uNebula: [1, 0.45, 0.6] } }
23936
+ ]
23937
+ };
23938
+ var solarSystem = {
23939
+ kind: "shader",
23940
+ id: "solar-system",
23941
+ name: "Solar system",
23942
+ description: "A stylised solar system seen at a tilt: planets circling a glowing sun on their orbit lines, the outer ones slower, one with rings.",
23943
+ category: "space",
23944
+ tags: ["space", "solar system", "planets", "orbit", "astronomy", "sun", "background"],
23945
+ source: glsl(
23946
+ `// Solar system: planets as analytic spheres on circular orbits in a plane seen at a tilt, orbit lines where
23947
+ // each pixel's ray meets that plane (anti-aliased with fwidth), lit from the sun at the centre.
23948
+ uniform vec3 uBackground; // @control color default=#03040B label="Background"
23949
+ uniform vec3 uSun; // @control color default=#FFB443 label="Sun"
23950
+ uniform vec3 uOrbit; // @control color default=#7C8BB8 label="Orbit lines"
23951
+ uniform int uPlanets; // @control min=1 max=8 default=8 label="Planets"
23952
+ uniform float uSpeed; // @control min=0 max=4 default=1 label="Speed"
23953
+ uniform float uTilt; // @control min=0.1 max=1.5 default=0.5 label="Tilt"
23954
+ uniform float uZoom; // @control min=0.5 max=2 default=1 label="Zoom"
23955
+ uniform float uOrbitOpacity; // @control min=0 max=1 default=0.45 label="Orbit opacity"
23956
+ uniform float uPlanetSize; // @control min=0.5 max=2 default=1 label="Planet size"`,
23957
+ HASH,
23958
+ NOISE_3D,
23959
+ STARS2,
23960
+ SPHERE_HIT2,
23961
+ `const vec3 COLOUR[8] = vec3[8](vec3(0.66, 0.62, 0.58), vec3(0.95, 0.78, 0.5), vec3(0.3, 0.55, 0.95), vec3(0.85, 0.42, 0.25),
23962
+ vec3(0.86, 0.72, 0.55), vec3(0.92, 0.82, 0.6), vec3(0.6, 0.88, 0.92), vec3(0.32, 0.45, 0.95));
23963
+ const float SIZE[8] = float[8](0.035, 0.05, 0.055, 0.042, 0.11, 0.095, 0.07, 0.068);
23964
+
23965
+ float orbitRadius(int i) {
23966
+ return 0.42 + float(i) * 0.2 + float(i) * float(i) * 0.008;
23967
+ }
23968
+
23969
+ vec3 planetAt(int i) {
23970
+ float r = orbitRadius(i);
23971
+ float a = iTime * uSpeed * 0.6 / pow(r / 0.42, 1.5) + float(i) * 2.4 + 0.7;
23972
+ return vec3(cos(a), 0.0, sin(a)) * r;
23973
+ }
23974
+
23975
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
23976
+ float px = 1.0 / iResolution.y;
23977
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
23978
+ float dist = 4.6 / uZoom;
23979
+ vec3 ro = vec3(0.0, sin(uTilt), cos(uTilt)) * dist;
23980
+ vec3 fw = normalize(-ro);
23981
+ vec3 rt = normalize(cross(fw, vec3(0.0, 1.0, 0.0)));
23982
+ vec3 up = cross(rt, fw);
23983
+ vec3 rd = normalize(p.x * rt + p.y * up + 1.8 * fw);
23984
+
23985
+ vec3 col = uBackground + stars(fragCoord, 1.0);
23986
+ float sunR = 0.17;
23987
+
23988
+ // The nearest planet along the ray.
23989
+ float tBest = 1e9;
23990
+ int best = -1;
23991
+ for (int i = 0; i < 8; i++) {
23992
+ if (i >= uPlanets) break;
23993
+ vec2 h = sphereHit(ro - planetAt(i), rd, SIZE[i] * uPlanetSize);
23994
+ if (h.x < h.y && h.x > 0.0 && h.x < tBest) {
23995
+ tBest = h.x;
23996
+ best = i;
23997
+ }
23998
+ }
23999
+ vec2 sunHit = sphereHit(ro, rd, sunR);
24000
+ float tSun = sunHit.x < sunHit.y ? sunHit.x : 1e9;
24001
+
24002
+ // Orbit lines, where the ray meets the plane in front of anything solid.
24003
+ float tPlane = -ro.y / rd.y;
24004
+ if (tPlane > 0.0 && tPlane < min(tBest, tSun)) {
24005
+ vec3 q = ro + rd * tPlane;
24006
+ float r = length(q.xz);
24007
+ float w = fwidth(r);
24008
+ float lines = 0.0;
24009
+ for (int i = 0; i < 8; i++) {
24010
+ if (i >= uPlanets) break;
24011
+ lines = max(lines, 1.0 - smoothstep(0.5, 1.5, abs(r - orbitRadius(i)) / max(w, 1e-5)));
24012
+ }
24013
+ col += uOrbit * lines * uOrbitOpacity;
24014
+ }
24015
+
24016
+ if (best >= 0 && tBest < tSun) {
24017
+ vec3 c = planetAt(best);
24018
+ vec3 pos = ro + rd * tBest;
24019
+ vec3 n = normalize(pos - c);
24020
+ vec3 l = normalize(-c);
24021
+ vec3 base = COLOUR[best];
24022
+ // Gas giants get bands.
24023
+ if (best >= 4) base *= 0.85 + 0.25 * sin(n.y * 26.0 + float(best) + 2.0 * vnoise3(n * 4.0));
24024
+ if (best == 2) base = mix(base, vec3(0.3, 0.6, 0.3), smoothstep(0.5, 0.6, vnoise3(n * 3.0 + 2.0)));
24025
+ float diff = max(dot(n, l), 0.0);
24026
+ col = base * (0.04 + 0.96 * diff) + uSun * pow(1.0 - max(dot(n, -rd), 0.0), 3.0) * diff * 0.4;
24027
+ } else if (tSun < 1e8) {
24028
+ vec3 n = normalize(ro + rd * tSun);
24029
+ float boil = fbm3(n * 6.0 + vec3(0.0, iTime * 0.15, 0.0), 4);
24030
+ float limb = max(dot(n, -rd), 0.0);
24031
+ col = uSun * (0.7 + 0.8 * boil) * (0.55 + 0.45 * limb) + vec3(1.0, 0.95, 0.8) * pow(limb, 3.0) * 0.5;
24032
+ }
24033
+
24034
+ // Saturn's rings (the sixth planet), drawn over whatever lies behind them.
24035
+ if (uPlanets >= 6) {
24036
+ vec3 c = planetAt(5);
24037
+ float t = -ro.y / rd.y;
24038
+ if (t > 0.0 && t < min(tSun, best == 5 ? tBest + 0.0 : 1e9)) {
24039
+ float r = length((ro + rd * t) - c) / (SIZE[5] * uPlanetSize);
24040
+ float ring = smoothstep(1.35, 1.45, r) * smoothstep(2.3, 2.15, r) * (0.6 + 0.4 * sin(r * 40.0));
24041
+ if (best != 5 || t < tBest) col = mix(col, COLOUR[5] * 0.95, ring * 0.75);
24042
+ }
24043
+ }
24044
+
24045
+ // The sun's glow over everything.
24046
+ float closest = length(ro - rd * dot(ro, rd));
24047
+ col += uSun * exp(-max(closest - sunR, 0.0) * 9.0) * 0.6;
24048
+ col += uSun * exp(-max(closest - sunR, 0.0) * 40.0) * 0.6;
24049
+ fragColor = vec4(col, 1.0);
24050
+ }`
24051
+ ),
24052
+ presets: [
24053
+ { name: "Top down", values: { uTilt: 1.45, uZoom: 0.9 } },
24054
+ { name: "Inner planets", values: { uPlanets: 4, uZoom: 1.9, uPlanetSize: 1.6 } }
24055
+ ]
24056
+ };
24057
+ var spaceShaders = [planet, spiralGalaxy, solarSystem];
24058
+
24059
+ // src/catalog/shaders/weather.ts
24060
+ var rainStreaks = {
24061
+ kind: "shader",
24062
+ id: "rain-streaks",
24063
+ name: "Rain streaks",
24064
+ description: "Rain falling in slanted streaks at several depths, splashing where it meets a ground line, to lay over any scene.",
24065
+ category: "weather",
24066
+ tags: ["weather", "rain", "storm", "overlay", "particles", "moody"],
24067
+ transparent: true,
24068
+ source: glsl(
24069
+ `// Rain streaks: layers of streaks in sheared grid cells (far layers finer, slower and fainter), and splashes
24070
+ // (a ring and a few flying droplets) in cells along the ground line.
24071
+ uniform vec3 uColor; // @control color default=#D4E4FF label="Colour"
24072
+ uniform float uDensity; // @control min=0 max=1 default=0.55 label="Density"
24073
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
24074
+ uniform float uWind; // @control min=-1 max=1 default=0.18 label="Wind (slant)"
24075
+ uniform float uLength; // @control min=0.2 max=3 default=1 label="Streak length"
24076
+ uniform int uLayers; // @control min=1 max=5 default=4 label="Layers"
24077
+ uniform float uGround; // @control min=0 max=0.6 default=0.1 label="Ground line"
24078
+ uniform float uSplashes; // @control min=0 max=1 default=0.8 label="Splashes"
24079
+ uniform float uOpacity; // @control min=0 max=1 default=0.75 label="Opacity"`,
24080
+ HASH,
24081
+ `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
24082
+ float px = 1.0 / iResolution.y;
24083
+ vec2 p = fragCoord * px;
24084
+ float a = 0.0;
24085
+ for (int i = 0; i < 5; i++) {
24086
+ if (i >= uLayers) break;
24087
+ float depth = (float(i) + 1.0) / float(uLayers);
24088
+ // Columns per screen height, and how long a cell is (in columns).
24089
+ float cols = mix(70.0, 18.0, depth);
24090
+ float cellH = 6.0;
24091
+ // Shear so streaks slant with the wind.
24092
+ vec2 q = vec2(p.x - (p.y - uGround) * uWind, p.y);
24093
+ float fall = iTime * uSpeed * mix(1.4, 3.2, depth);
24094
+ float column = floor(q.x * cols + float(i) * 7.3);
24095
+ // Each column's cells start at a height of their own, so streaks never line up in rows.
24096
+ vec2 g = vec2(q.x * cols + float(i) * 7.3, (q.y + fall) * cols / cellH + hash12(vec2(column, float(i))));
24097
+ vec2 cell = floor(g);
24098
+ vec2 f = fract(g);
24099
+ float h = hash12(cell + float(i) * 13.1);
24100
+ if (h > uDensity) continue;
24101
+ float x = 0.2 + 0.6 * hash12(cell + 3.3);
24102
+ float len = clamp(0.35 * uLength * mix(0.6, 1.0, depth) * (0.7 + 0.6 * hash12(cell + 5.7)), 0.05, 0.95);
24103
+ float top = 0.05 + (0.95 - len) * hash12(cell + 9.1);
24104
+ // Distance across the streak in pixels (a column is iResolution.y / cols pixels wide).
24105
+ float across = abs(f.x - x) / (px * cols);
24106
+ float width = mix(0.5, 1.4, depth);
24107
+ float along = (f.y - top) / len;
24108
+ float body = step(0.0, along) * step(along, 1.0) * pow(1.0 - along, 0.6) * smoothstep(0.0, 0.15, along);
24109
+ float streak = (1.0 - smoothstep(width * 0.5, width * 0.5 + 1.0, across)) * body;
24110
+ // Rain stops at the ground.
24111
+ streak *= step(uGround, p.y);
24112
+ a = max(a, streak * mix(0.25, 0.9, depth));
24113
+ }
24114
+ // Splashes along the ground: a flattened ring spreading and droplets thrown up.
24115
+ if (uSplashes > 0.0) {
24116
+ for (int k = 0; k < 2; k++) {
24117
+ float w = 0.045;
24118
+ float sx = p.x / w + float(k) * 0.5;
24119
+ float cell = floor(sx);
24120
+ float period = 0.45 + 0.5 * hash12(vec2(cell, float(k)));
24121
+ float time = iTime * uSpeed + hash12(vec2(cell, 7.0 + float(k))) * period;
24122
+ float cycle = floor(time / period);
24123
+ float age = fract(time / period) * period;
24124
+ if (hash12(vec2(cell, cycle + float(k) * 19.0)) > uSplashes * uDensity * 1.4) continue;
24125
+ vec2 centre = vec2((cell + 0.25 + 0.5 * hash12(vec2(cell, cycle + 2.0)) - float(k) * 0.5) * w, uGround);
24126
+ vec2 d = p - centre;
24127
+ float life = smoothstep(0.25, 0.0, age);
24128
+ // The ring, flattened by perspective.
24129
+ float r = 0.004 + age * 0.08;
24130
+ float ring = abs(length(d * vec2(1.0, 3.5)) - r);
24131
+ a = max(a, (1.0 - smoothstep(0.6 * px, 1.8 * px, ring)) * life * 0.7 * step(-0.003, d.y));
24132
+ // Droplets.
24133
+ for (int j = 0; j < 3; j++) {
24134
+ float fj = float(j);
24135
+ float vx = (fj - 1.0) * 0.12 + (hash12(vec2(cell, fj + cycle)) - 0.5) * 0.06;
24136
+ float vy = 0.22 + 0.12 * hash12(vec2(cell + 4.0, fj + cycle));
24137
+ vec2 at = centre + vec2(vx * age, vy * age - 2.2 * age * age);
24138
+ float drop = 1.0 - smoothstep(0.8 * px, 2.0 * px, length(p - at));
24139
+ a = max(a, drop * life * step(uGround - 0.002, at.y) * 0.9);
24140
+ }
24141
+ }
24142
+ }
24143
+ fragColor = vec4(uColor, a * uOpacity);
24144
+ }`
24145
+ ),
24146
+ presets: [
24147
+ { name: "Drizzle", values: { uDensity: 0.25, uSpeed: 0.7, uLength: 0.6, uWind: 0.05, uOpacity: 0.55 } },
24148
+ { name: "Downpour", values: { uDensity: 0.9, uSpeed: 1.6, uLength: 1.6, uWind: 0.35, uLayers: 5 } },
24149
+ { name: "No splashes", values: { uSplashes: 0, uGround: 0 } }
24150
+ ]
24151
+ };
24152
+ var thunderstorm = {
24153
+ kind: "shader",
24154
+ id: "thunderstorm",
24155
+ name: "Thunderstorm",
24156
+ description: "Dark, roiling storm clouds lit from inside by flickering lightning, with bolts to the ground and driving rain.",
24157
+ category: "weather",
24158
+ tags: ["weather", "storm", "lightning", "clouds", "rain", "dramatic", "background"],
24159
+ source: glsl(
24160
+ `// Thunderstorm: domain-warped fbm clouds, flashes in random slots that light the clouds around a point
24161
+ // (more where they are thin), a jagged bolt to the ground for some, and two layers of rain.
24162
+ uniform vec3 uSky; // @control color default=#10131C label="Sky"
24163
+ uniform vec3 uCloud; // @control color default=#454C5E label="Cloud"
24164
+ uniform vec3 uFlash; // @control color default=#D6DEFF label="Flash"
24165
+ uniform vec3 uRainColor; // @control color default=#9AA8C4 label="Rain"
24166
+ uniform float uFrequency; // @control min=0 max=2 default=0.5 label="Flashes per second"
24167
+ uniform float uRain; // @control min=0 max=1 default=0.6 label="Rain"
24168
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Cloud speed"
24169
+ uniform bool uBolts; // @control default=true label="Bolts"`,
24170
+ HASH,
24171
+ NOISE,
24172
+ FBM,
24173
+ `// The flash in slot k: its brightness now, where (x), and whether it strikes the ground.
24174
+ vec3 flashAt(float slot, float t) {
24175
+ float h = hash12(vec2(slot, 1.7));
24176
+ if (h > 0.7) return vec3(0.0);
24177
+ float start = hash12(vec2(slot, 4.1)) * 0.5;
24178
+ float tau = t - start;
24179
+ if (tau < 0.0) return vec3(0.0);
24180
+ // A few flickers: a bright strike, a dip, a restrike, then fading.
24181
+ float b = exp(-tau * 9.0) + 0.7 * exp(-max(tau - 0.12, 0.0) * 12.0) * step(0.12, tau) + 0.4 * exp(-max(tau - 0.3, 0.0) * 6.0) * step(0.3, tau);
24182
+ return vec3(b, hash12(vec2(slot, 9.3)), step(0.45, hash12(vec2(slot, 2.2))));
24183
+ }
24184
+
24185
+ float rain(vec2 p, float cols, float speed, float slant, float seed) {
24186
+ vec2 q = vec2(p.x + p.y * slant, p.y + iTime * speed);
24187
+ vec2 g = vec2(q.x * cols + seed, q.y * cols / 8.0);
24188
+ vec2 c = floor(g);
24189
+ vec2 f = fract(g);
24190
+ if (hash12(c + seed) > uRain) return 0.0;
24191
+ float x = 0.2 + 0.6 * hash12(c + 1.3);
24192
+ float across = abs(f.x - x) * iResolution.y / cols;
24193
+ float y0 = hash12(c + 2.9) * 0.5;
24194
+ float body = smoothstep(y0, y0 + 0.1, f.y) * smoothstep(y0 + 0.5, y0 + 0.3, f.y);
24195
+ return (1.0 - smoothstep(0.4, 1.4, across)) * body;
24196
+ }
24197
+
24198
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
24199
+ float px = 1.0 / iResolution.y;
24200
+ vec2 uv = fragCoord / iResolution.xy;
24201
+ vec2 p = fragCoord * px;
24202
+ float aspect = iResolution.x / iResolution.y;
24203
+ float t = iTime * uSpeed * 0.05;
24204
+
24205
+ // Clouds: warped fbm, thickest overhead, with a ragged base.
24206
+ vec2 cp = vec2(p.x, p.y * 1.6) * 1.6;
24207
+ vec2 warp = vec2(fbm(cp + vec2(t, 0.0), 4), fbm(cp + vec2(5.2, 1.3) - vec2(0.0, t * 0.7), 4));
24208
+ float dens = fbm(cp + 1.8 * warp + vec2(t * 2.0, 0.0), 5);
24209
+ float base = smoothstep(0.25, 0.75, uv.y + 0.35 * (dens - 0.5));
24210
+ float cloud = clamp(smoothstep(0.35, 0.68, dens) * 0.75 + base * 0.6, 0.0, 1.0);
24211
+ // Light from above: thick cloud is dark below.
24212
+ float shade = fbm(cp + 1.8 * warp + vec2(t * 2.0, 0.03), 5) - dens;
24213
+ vec3 col = mix(uSky * 0.7, uCloud * clamp(0.55 + 5.0 * shade + 0.45 * uv.y, 0.15, 1.6), cloud);
24214
+
24215
+ // Lightning: one possible flash in each slot.
24216
+ float slotLen = 1.0 / max(uFrequency, 0.01);
24217
+ float slot = floor(iTime / slotLen);
24218
+ vec3 fl = flashAt(slot, iTime - slot * slotLen);
24219
+ if (uFrequency > 0.0 && fl.x > 0.002) {
24220
+ vec2 at = vec2((0.1 + 0.8 * fl.y) * aspect, 0.72 + 0.15 * hash12(vec2(slot, 6.6)));
24221
+ float d = length(p - at);
24222
+ float glow = exp(-d * 3.2);
24223
+ // Light scatters through the cloud: brightest at the source, more where the cloud is thinner.
24224
+ col += uFlash * fl.x * glow * (0.35 + 0.9 * cloud * (1.2 - dens)) * 1.6;
24225
+ col += uFlash * fl.x * 0.12;
24226
+ if (uBolts && fl.z > 0.5) {
24227
+ // A jagged bolt from the cloud base to the ground, with one fork.
24228
+ float y = p.y;
24229
+ float jag = (fbm(vec2(y * 5.0, slot * 3.1), 4) - 0.5) * 0.35 + (vnoise(vec2(y * 40.0, slot)) - 0.5) * 0.03;
24230
+ float x = at.x + jag;
24231
+ float dx = abs(p.x - x);
24232
+ float span = step(y, at.y) * smoothstep(-0.02, 0.05, y);
24233
+ float core = (1.0 - smoothstep(0.6 * px, 2.2 * px, dx)) * span;
24234
+ float halo = exp(-dx * 60.0) * span * 0.5;
24235
+ float forkY = at.y - 0.2 - 0.15 * hash12(vec2(slot, 8.8));
24236
+ float fx = x + (forkY - y) * (0.3 * sign(hash12(vec2(slot, 3.3)) - 0.5)) + (vnoise(vec2(y * 30.0, slot + 9.0)) - 0.5) * 0.04;
24237
+ float fork = (1.0 - smoothstep(0.5 * px, 1.6 * px, abs(p.x - fx))) * step(y, forkY) * step(forkY - 0.18, y) * smoothstep(forkY - 0.18, forkY - 0.1, y);
24238
+ float strike = smoothstep(0.25, 0.6, fl.x);
24239
+ col += (vec3(1.0) * (core + fork * 0.8) + uFlash * halo) * strike * 1.5;
24240
+ }
24241
+ }
24242
+ // The land: a dark ridge lit by the flash.
24243
+ float ridge = 0.08 + 0.04 * fbm(vec2(p.x * 3.0, 1.0), 4);
24244
+ col = mix(col, uSky * 0.35 + uFlash * fl.x * 0.15, 1.0 - smoothstep(ridge - px, ridge + px, p.y));
24245
+ // Rain.
24246
+ float r = max(rain(p, 60.0, 2.6, 0.12, 0.0) * 0.5, rain(p, 24.0, 3.6, 0.15, 17.0));
24247
+ col = mix(col, uRainColor * (1.0 + fl.x), r * 0.3);
24248
+ fragColor = vec4(col, 1.0);
24249
+ }`
24250
+ ),
24251
+ presets: [
24252
+ { name: "Distant storm", values: { uFrequency: 0.25, uRain: 0.2, uBolts: false } },
24253
+ { name: "Supercell", values: { uSky: [0.08, 0.1, 0.1], uCloud: [0.3, 0.38, 0.36], uFlash: [0.85, 0.8, 1], uFrequency: 1.2, uRain: 0.9, uSpeed: 2 } }
24254
+ ]
24255
+ };
24256
+ var rainbow = {
24257
+ kind: "shader",
24258
+ id: "rainbow",
24259
+ name: "Rainbow",
24260
+ description: "A soft rainbow arc fading in over a picture as if after rain, with a faint second bow and optional sun rays.",
24261
+ category: "weather",
24262
+ video: "filter",
24263
+ tags: ["weather", "rainbow", "image", "light", "hopeful", "spring"],
24264
+ source: glsl(
24265
+ `// Rainbow: a primary bow (red outside, violet inside) and a fainter reversed secondary over the picture,
24266
+ // brighter inside the primary and darker between them, fading in over time, with optional sun rays.
24267
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.landscape} label="Image"
24268
+ uniform vec2 uCentre; // @control min=-1 max=2 default=0.5,-0.35 label="Bow centre"
24269
+ uniform float uRadius; // @control min=0.2 max=2 default=0.95 label="Radius"
24270
+ uniform float uWidth; // @control min=0.01 max=0.3 default=0.07 label="Width"
24271
+ uniform float uStrength; // @control min=0 max=1.5 default=0.65 label="Strength"
24272
+ uniform float uSecondary; // @control min=0 max=1 default=0.35 label="Second bow"
24273
+ uniform float uDelay; // @control min=0 max=10 default=0.5 label="Fade-in delay (s)"
24274
+ uniform float uFade; // @control min=0 max=10 default=3 label="Fade-in time (s)"
24275
+ uniform float uRays; // @control min=0 max=1 default=0.35 label="Sun rays"
24276
+ uniform vec2 uSun; // @control min=-0.5 max=1.5 default=0.1,1.05 label="Sun position"
24277
+ ${BACKDROP_CONTROLS}`,
24278
+ HASH,
24279
+ NOISE,
24280
+ PICTURE,
24281
+ `// Spectrum across a bow: 0 inside (violet) to 1 outside (red), soft at both edges.
24282
+ vec3 spectrum(float x) {
24283
+ float h = (1.0 - x) * 0.78;
24284
+ vec3 c = clamp(abs(fract(h + vec3(0.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - 3.0) - 1.0, 0.0, 1.0);
24285
+ return c * smoothstep(0.0, 0.18, x) * smoothstep(1.0, 0.82, x);
24286
+ }
24287
+
24288
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
24289
+ vec2 uv = fragCoord / iResolution.xy;
24290
+ float aspect = iResolution.x / iResolution.y;
24291
+ vec4 pic = pictureRGBA(uImage, uv);
24292
+ vec2 p = vec2(uv.x * aspect, uv.y);
24293
+ vec2 c = vec2(uCentre.x * aspect, uCentre.y);
24294
+ float r = length(p - c);
24295
+ float appear = uFade <= 0.0 ? step(uDelay, iTime) : smoothstep(uDelay, uDelay + uFade, iTime);
24296
+ // Bows fade toward the ground and shimmer faintly.
24297
+ float ground = smoothstep(0.0, 0.35, uv.y);
24298
+ float shimmer = 0.85 + 0.15 * vnoise(vec2(atan(p.y - c.y, p.x - c.x) * 6.0, iTime * 0.3));
24299
+ float x1 = (r - (uRadius - uWidth)) / uWidth;
24300
+ vec3 bow = spectrum(x1);
24301
+ float r2 = uRadius * 1.27;
24302
+ float x2 = 1.0 - (r - r2) / (uWidth * 1.6);
24303
+ bow += spectrum(x2) * uSecondary * 0.6;
24304
+ // Brighter sky inside the primary, darker between the bows.
24305
+ float inside = smoothstep(uRadius - uWidth, uRadius - uWidth - 0.25, r) * step(0.0, uRadius - uWidth - r) * 0.12;
24306
+ float between = smoothstep(uRadius, uRadius + 0.02, r) * smoothstep(r2 - uWidth * 1.6, r2 - uWidth * 1.6 - 0.02, r) * 0.08;
24307
+ float amount = appear * ground * shimmer * uStrength;
24308
+ vec3 col = pic.rgb;
24309
+ col = 1.0 - (1.0 - col) * (1.0 - bow * amount);
24310
+ col += inside * amount * 0.8;
24311
+ col *= 1.0 - between * amount;
24312
+ // Sun rays fanning from the sun.
24313
+ if (uRays > 0.0) {
24314
+ vec2 s = vec2(uSun.x * aspect, uSun.y);
24315
+ vec2 d = p - s;
24316
+ float ang = atan(d.y, d.x);
24317
+ float rays = vnoise(vec2(ang * 9.0, iTime * 0.15)) * vnoise(vec2(ang * 23.0 + 4.0, iTime * 0.1));
24318
+ float fall = exp(-length(d) * 1.4);
24319
+ col += vec3(1.0, 0.93, 0.75) * (smoothstep(0.15, 0.6, rays) * 0.7 + 0.3) * fall * uRays * appear * 0.7;
24320
+ }
24321
+ fragColor = vec4(min(col, 1.0), pic.a);
24322
+ }`
24323
+ ),
24324
+ presets: [
24325
+ { name: "Double rainbow", values: { uSecondary: 1, uStrength: 0.9, uWidth: 0.08 } },
24326
+ { name: "Low and wide", values: { uCentre: [0.5, -0.8], uRadius: 1.35, uWidth: 0.09, uRays: 0 } }
24327
+ ]
24328
+ };
24329
+ var windGust = {
24330
+ kind: "shader",
24331
+ id: "wind-gust",
24332
+ name: "Wind gust",
24333
+ description: "Gusts of wind sweeping across: curling wind lines with leaves and dust tumbling along, to lay over any scene.",
24334
+ category: "weather",
24335
+ tags: ["weather", "wind", "leaves", "autumn", "overlay", "particles"],
24336
+ transparent: true,
24337
+ source: glsl(
24338
+ `// Wind gust: wind lines (curving segments sliding along rows), leaves (tumbling ellipses with a vein) and
24339
+ // dust carried in cells by the wind's integrated push, all swelling with each gust.
24340
+ uniform vec3 uLineColor; // @control color default=#FFFFFF label="Wind lines"
24341
+ uniform vec3 uLeafA; // @control color default=#E0892E label="Leaf colour A"
24342
+ uniform vec3 uLeafB; // @control color default=#9BB13E label="Leaf colour B"
24343
+ uniform vec3 uDustColor; // @control color default=#D6C5A4 label="Dust"
24344
+ uniform float uStrength; // @control min=0 max=2 default=1 label="Strength"
24345
+ uniform float uDirection; // @control min=-1 max=1 default=1 step=2 label="Direction"
24346
+ uniform float uGustRate; // @control min=0.05 max=1 default=0.25 label="Gusts per second"
24347
+ uniform float uLines; // @control min=0 max=1 default=0.7 label="Wind lines"
24348
+ uniform float uLeaves; // @control min=0 max=1 default=0.5 label="Leaves"
24349
+ uniform float uDust; // @control min=0 max=1 default=0.5 label="Dust"`,
24350
+ HASH,
24351
+ `const float TAU = 6.28318;
24352
+
24353
+ // The gust at time t: 0.25 calm to 1 at its peak.
24354
+ float gust(float t) {
24355
+ float s = 0.5 + 0.5 * sin(t * uGustRate * TAU);
24356
+ return 0.25 + 0.75 * s * s;
24357
+ }
24358
+ // How far the wind has pushed by time t (the integral of gust: s\xB2 = 3/8 + sin/2 \u2212 cos(2x)/8).
24359
+ float pushed(float t) {
24360
+ float w = uGustRate * TAU;
24361
+ float x = t * w;
24362
+ return 0.25 * t + 0.75 * (0.375 * t - 0.5 * cos(x) / w - 0.125 * sin(2.0 * x) / (2.0 * w));
24363
+ }
24364
+
24365
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
24366
+ float px = 1.0 / iResolution.y;
24367
+ vec2 p = fragCoord * px;
24368
+ float dir = uDirection < 0.0 ? -1.0 : 1.0;
24369
+ p.x *= dir;
24370
+ float aspect = iResolution.x / iResolution.y;
24371
+ vec4 acc = vec4(0.0);
24372
+
24373
+ // Wind lines: each row holds a curved segment sliding fast along it, shown while the gust passes.
24374
+ if (uLines > 0.0) {
24375
+ for (int k = 0; k < 2; k++) {
24376
+ float rows = k == 0 ? 9.0 : 15.0;
24377
+ float y = p.y * rows + float(k) * 0.5;
24378
+ float row = floor(y);
24379
+ float h = hash12(vec2(row, float(k)));
24380
+ float speed = (1.2 + 0.8 * h) * uStrength;
24381
+ float lag = iTime - p.x * 0.35;
24382
+ float g = gust(lag);
24383
+ float u = p.x * 0.7 - iTime * speed * 0.6 - h * 9.0;
24384
+ float seg = floor(u);
24385
+ float fu = fract(u);
24386
+ if (hash12(vec2(seg, row + float(k) * 5.0)) > uLines * 0.7) continue;
24387
+ float len = 0.25 + 0.3 * hash12(vec2(seg, row + 1.0));
24388
+ float along = fu / len;
24389
+ if (along > 1.0) continue;
24390
+ // A gentle wave and, now and then, a curl at the head.
24391
+ float curve = 0.25 * sin(p.x * 5.0 + h * 6.0 + iTime * 0.5) + 0.12 * sin(p.x * 13.0 + h * 3.0);
24392
+ float curl = step(0.75, hash12(vec2(seg, row + 3.0))) * smoothstep(0.65, 1.0, along) * 0.45 * sin((along - 0.65) * 14.0);
24393
+ float centre = row + 0.5 + curve + curl;
24394
+ float d = abs(y - centre) / (rows * px);
24395
+ float taper = smoothstep(0.0, 0.3, along) * smoothstep(1.0, 0.75, along);
24396
+ float line = (1.0 - smoothstep(0.9, 2.2, d)) * taper * mix(0.25, 1.0, smoothstep(0.3, 0.9, g)) * (k == 0 ? 0.85 : 0.5);
24397
+ acc.rgb = mix(acc.rgb, uLineColor, line);
24398
+ acc.a = max(acc.a, line);
24399
+ }
24400
+ }
24401
+
24402
+ // Leaves and dust carried by the wind's push.
24403
+ float push = pushed(iTime) * uStrength;
24404
+ for (int k = 0; k < 3; k++) {
24405
+ bool leaf = k < 2;
24406
+ float amount = leaf ? uLeaves : uDust;
24407
+ if (amount <= 0.0) continue;
24408
+ float scale = k == 0 ? 5.0 : k == 1 ? 8.0 : 22.0;
24409
+ float speed = k == 0 ? 0.9 : k == 1 ? 0.65 : 1.2;
24410
+ vec2 q = p * scale;
24411
+ q.x -= push * speed * scale * 0.55;
24412
+ q.y += sin(iTime * 0.8 + q.x * 0.3) * 0.3 + iTime * 0.15 * scale * 0.1;
24413
+ vec2 cell = floor(q);
24414
+ float h = hash12(cell + float(k) * 11.0);
24415
+ if (h > amount * (leaf ? 0.45 : 0.5)) continue;
24416
+ vec2 at = 0.25 + 0.5 * hash22(cell + float(k));
24417
+ // Tumbling and bobbing within the cell.
24418
+ float spin = iTime * (1.5 + 3.0 * hash12(cell + 2.0)) * (0.5 + gust(iTime)) + h * 20.0;
24419
+ at += 0.15 * vec2(sin(spin * 0.7), cos(spin * 0.9));
24420
+ vec2 d = fract(q) - at;
24421
+ if (leaf) {
24422
+ float c = cos(spin), s = sin(spin);
24423
+ d = mat2(c, s, -s, c) * d;
24424
+ // The tumble foreshortens the leaf.
24425
+ d.y /= max(abs(sin(spin * 0.6)), 0.25);
24426
+ float size = 0.18 + 0.08 * hash12(cell + 4.0);
24427
+ vec2 e = d / vec2(size, size * 0.45);
24428
+ float r = length(e) - (1.0 - 0.35 * abs(e.x));
24429
+ float aa = px * scale / size * 1.5;
24430
+ float shape = 1.0 - smoothstep(-aa, aa, r);
24431
+ vec3 colour = mix(uLeafA, uLeafB, hash12(cell + 6.0)) * (0.8 + 0.3 * sign(sin(spin * 0.6)));
24432
+ float vein = (1.0 - smoothstep(0.0, 0.08, abs(e.y))) * step(abs(e.x), 0.85) * 0.3;
24433
+ colour *= 1.0 - vein;
24434
+ acc.rgb = mix(acc.rgb, colour, shape);
24435
+ acc.a = max(acc.a, shape);
24436
+ } else {
24437
+ // Dust: short streaks along the wind.
24438
+ float len = 0.06 + 0.1 * gust(iTime);
24439
+ float dd = length(vec2(d.x / (1.0 + len * 12.0), d.y));
24440
+ float speck = (1.0 - smoothstep(0.02, 0.05, dd)) * 0.55 * smoothstep(0.3, 0.6, gust(iTime - p.x * 0.35));
24441
+ acc.rgb = mix(acc.rgb, uDustColor, speck * (1.0 - acc.a));
24442
+ acc.a = max(acc.a, speck);
24443
+ }
24444
+ }
24445
+ fragColor = vec4(acc.rgb, acc.a);
24446
+ }`
24447
+ ),
24448
+ presets: [
24449
+ { name: "Autumn", values: { uLeaves: 0.85, uLines: 0.4, uDust: 0.2 } },
24450
+ { name: "Dust storm", values: { uLeaves: 0, uDust: 1, uLines: 0.9, uStrength: 1.8, uLineColor: [0.9, 0.82, 0.68], uDustColor: [0.8, 0.66, 0.45] } },
24451
+ { name: "Breeze", values: { uLeaves: 0.2, uDust: 0, uLines: 0.5, uStrength: 0.6, uGustRate: 0.12 } }
24452
+ ]
24453
+ };
24454
+ var hail = {
24455
+ kind: "shader",
24456
+ id: "hail",
24457
+ name: "Hail",
24458
+ description: "Hailstones pelting down and bouncing on a ground line, each bounce lower than the last, to lay over any scene.",
24459
+ category: "weather",
24460
+ tags: ["weather", "hail", "ice", "storm", "overlay", "particles", "physics"],
24461
+ transparent: true,
24462
+ source: glsl(
24463
+ `// Hail: stones on cycles in columns, each falling under gravity, bouncing three times on the ground line
24464
+ // with a loss of speed each time and scattering sideways, then melting away. A pixel checks its column and
24465
+ // its neighbours (stones scatter into them), two stones each.
24466
+ uniform vec3 uColor; // @control color default=#EEF4FA label="Colour"
24467
+ uniform vec3 uShade; // @control color default=#9FB4CC label="Shade"
24468
+ uniform float uDensity; // @control min=0 max=1 default=0.6 label="Density"
24469
+ uniform float uSize; // @control min=0.3 max=2.5 default=1 label="Size"
24470
+ uniform float uGround; // @control min=0 max=0.6 default=0.12 label="Ground line"
24471
+ uniform float uBounce; // @control min=0 max=0.8 default=0.42 label="Bounciness"
24472
+ uniform float uSpeed; // @control min=0 max=2 default=1 label="Speed"`,
24473
+ HASH,
24474
+ `const float G = 3.2;
24475
+ const float TOP = 1.1;
24476
+
24477
+ // A stone's place at local time tau (x offset from its column, height), its fall speed, and how visible it is.
24478
+ vec4 stoneAt(float tau, float r, float drift) {
24479
+ float h0 = TOP - uGround - r;
24480
+ float tf = sqrt(2.0 * h0 / G);
24481
+ if (tau < tf) return vec4(0.0, TOP - 0.5 * G * tau * tau, G * tau, 1.0);
24482
+ float v = G * tf;
24483
+ float t = tau - tf;
24484
+ float x = 0.0;
24485
+ // Each bounce: up at v\xB7e, back down after 2v\xB7e/G, drifting sideways.
24486
+ for (int b = 0; b < 3; b++) {
24487
+ v *= uBounce;
24488
+ float d = 2.0 * v / G;
24489
+ if (t < d) return vec4(drift * (x + t), uGround + r + v * t - 0.5 * G * t * t, abs(v - G * t), 1.0);
24490
+ t -= d;
24491
+ x += d;
24492
+ }
24493
+ // At rest, melting away.
24494
+ return vec4(drift * x, uGround + r, 0.0, 1.0 - smoothstep(0.2, 0.7, t));
24495
+ }
24496
+
24497
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
24498
+ float px = 1.0 / iResolution.y;
24499
+ vec2 p = fragCoord * px;
24500
+ float w = 0.035;
24501
+ float col0 = floor(p.x / w);
24502
+ vec4 acc = vec4(0.0);
24503
+ for (int c = -1; c <= 1; c++) {
24504
+ float column = col0 + float(c);
24505
+ for (int k = 0; k < 2; k++) {
24506
+ vec2 id = vec2(column, float(k));
24507
+ float period = 1.3 + 1.2 * hash12(id);
24508
+ float time = iTime * uSpeed + hash12(id + 3.1) * period;
24509
+ float cycle = floor(time / period);
24510
+ float tau = time - cycle * period;
24511
+ vec2 seed = id + cycle * 7.13;
24512
+ if (hash12(seed + 1.9) > uDensity) continue;
24513
+ float r = (0.008 + 0.009 * hash12(seed + 5.5)) * uSize;
24514
+ float drift = (hash12(seed + 8.2) - 0.5) * 0.25;
24515
+ vec4 s = stoneAt(tau, r, drift);
24516
+ if (s.w <= 0.0) continue;
24517
+ vec2 centre = vec2((column + 0.2 + 0.6 * hash12(seed + 2.4)) * w + s.x, s.y);
24518
+ vec2 d = p - centre;
24519
+ // Stretched a little along the fall when moving fast.
24520
+ d.y /= 1.0 + min(s.z * 0.12, 0.6);
24521
+ float dist = length(d);
24522
+ float disc = 1.0 - smoothstep(r - px, r + px, dist);
24523
+ if (disc <= 0.0) continue;
24524
+ // Lit from the upper left, with a hard highlight.
24525
+ vec2 n = d / r;
24526
+ float light = clamp(0.6 - 0.5 * dot(n, vec2(0.6, -0.6)), 0.0, 1.0);
24527
+ vec3 colour = mix(uShade, uColor, light);
24528
+ colour += smoothstep(0.35, 0.1, length(n - vec2(-0.35, 0.35))) * 0.5;
24529
+ float a = disc * s.w * 0.95;
24530
+ acc.rgb = mix(acc.rgb, colour, a);
24531
+ acc.a = max(acc.a, a);
24532
+ }
24533
+ }
24534
+ fragColor = vec4(min(acc.rgb, 1.0), acc.a);
24535
+ }`
24536
+ ),
24537
+ presets: [
24538
+ { name: "Light hail", values: { uDensity: 0.3, uSize: 0.7, uBounce: 0.3 } },
24539
+ { name: "Golf balls", values: { uDensity: 0.45, uSize: 2.2, uBounce: 0.5, uSpeed: 0.8 } }
24540
+ ]
24541
+ };
24542
+ var weatherShaders = [rainStreaks, thunderstorm, rainbow, windGust, hail];
24543
+
24544
+ // src/catalog/shaders/water.ts
24545
+ var oceanWaves = {
24546
+ kind: "shader",
24547
+ id: "ocean-waves",
24548
+ name: "Ocean waves",
24549
+ description: "An open ocean at sunset or by day: layered, sharp-crested waves rolling in, foam on the crests, the sky in the water and a glint of sun.",
24550
+ category: "water",
24551
+ tags: ["water", "ocean", "sea", "waves", "sunset", "nature", "3d", "background"],
24552
+ source: glsl(
24553
+ `// Ocean waves: a sum of sharp-crested waves (e^(sin x \u2212 1), each layer's position dragged by the last),
24554
+ // met by marching only between the planes of its highest and lowest points (16 steps and 4 secant steps),
24555
+ // with more layers for the normal. Fresnel mixes the reflected sky with light scattered through the crests.
24556
+ uniform vec3 uSkyTop; // @control color default=#27325E label="Sky (top)"
24557
+ uniform vec3 uSkyHorizon; // @control color default=#FF9B5C label="Sky (horizon)"
24558
+ uniform vec3 uSun; // @control color default=#FFD38A label="Sun"
24559
+ uniform vec3 uWater; // @control color default=#0A2638 label="Water"
24560
+ uniform vec3 uFoam; // @control color default=#F2EEE6 label="Foam"
24561
+ uniform float uSunHeight; // @control min=-0.1 max=1.2 default=0.06 label="Sun height"
24562
+ uniform float uSunAzimuth; // @control min=-1 max=1 default=0 label="Sun direction"
24563
+ uniform float uWaveHeight; // @control min=0 max=2.5 default=1 label="Wave height"
24564
+ uniform float uChoppiness; // @control min=0 max=2 default=1 label="Choppiness"
24565
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
24566
+ uniform float uFoamAmount; // @control min=0 max=1 default=0.5 label="Foam"`,
24567
+ HASH,
24568
+ NOISE,
24569
+ `const float AMP = 1.1;
24570
+
24571
+ // The sea's height (about 0\u20131) at p from its first n wave layers.
24572
+ float sea(vec2 p, int n) {
24573
+ float t = iTime * uSpeed;
24574
+ float h = 0.0;
24575
+ float total = 0.0;
24576
+ float a = 1.0;
24577
+ float k = 0.26;
24578
+ float ang = 0.3;
24579
+ for (int i = 0; i < 18; i++) {
24580
+ if (i >= n) break;
24581
+ vec2 d = vec2(cos(ang), sin(ang));
24582
+ float x = dot(d, p) * k + t * sqrt(9.8 * k) * 0.55;
24583
+ float e = exp(sin(x) - 1.0);
24584
+ h += a * e;
24585
+ total += a;
24586
+ // Drag the next layers toward this one's crests, so the crests gather.
24587
+ p += d * e * cos(x) * a * 0.35 * uChoppiness;
24588
+ a *= 0.76;
24589
+ k *= 1.24;
24590
+ ang += 2.39996;
24591
+ }
24592
+ return h / total;
24593
+ }
24594
+
24595
+ float heightAt(vec2 p, int n) {
24596
+ return (sea(p, n) - 0.42) * AMP * uWaveHeight;
24597
+ }
24598
+
24599
+ vec3 sky(vec3 rd, vec3 sun) {
24600
+ float y = max(rd.y, 0.0);
24601
+ vec3 c = mix(uSkyHorizon, uSkyTop, pow(y, 0.5));
24602
+ float mu = max(dot(rd, sun), 0.0);
24603
+ c += uSun * (pow(mu, 8.0) * 0.35 + pow(mu, 90.0) * 0.6);
24604
+ c += uSun * smoothstep(0.9993, 0.9997, mu) * 3.0;
24605
+ return c;
24606
+ }
24607
+
24608
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
24609
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
24610
+ vec3 ro = vec3(0.0, 1.6 + 0.4 * uWaveHeight, iTime * uSpeed * 0.6);
24611
+ vec3 rd = normalize(vec3(p.x, p.y - 0.12, 1.2));
24612
+ vec3 sun = normalize(vec3(sin(uSunAzimuth), uSunHeight, cos(uSunAzimuth)));
24613
+ vec3 col = sky(rd, sun);
24614
+
24615
+ float top = AMP * uWaveHeight * 0.6;
24616
+ float bottom = -AMP * uWaveHeight * 0.45;
24617
+ if (rd.y < -0.0005) {
24618
+ // Between the planes of the highest and lowest water.
24619
+ float t0 = (top - ro.y) / rd.y;
24620
+ float t1 = min((bottom - ro.y) / rd.y, 400.0);
24621
+ float tPrev = t0;
24622
+ float gPrev = top - heightAt(ro.xz + rd.xz * t0, 7);
24623
+ float t = t0;
24624
+ float dt = (t1 - t0) / 16.0;
24625
+ float tHit = t1;
24626
+ for (int i = 1; i <= 16; i++) {
24627
+ t = t0 + dt * float(i);
24628
+ vec3 pos = ro + rd * t;
24629
+ float g = pos.y - heightAt(pos.xz, 7);
24630
+ if (g < 0.0) {
24631
+ // Secant steps between the last point above and this one below.
24632
+ float a = tPrev, b = t, ga = gPrev, gb = g;
24633
+ for (int j = 0; j < 4; j++) {
24634
+ float m = a + (b - a) * ga / (ga - gb);
24635
+ vec3 q = ro + rd * m;
24636
+ float gm = q.y - heightAt(q.xz, 7);
24637
+ if (gm > 0.0) {
24638
+ a = m;
24639
+ ga = gm;
24640
+ } else {
24641
+ b = m;
24642
+ gb = gm;
24643
+ }
24644
+ }
24645
+ tHit = a + (b - a) * ga / (ga - gb);
24646
+ break;
24647
+ }
24648
+ tPrev = t;
24649
+ gPrev = g;
24650
+ }
24651
+ vec3 pos = ro + rd * tHit;
24652
+ // The normal from more wave layers, finer up close.
24653
+ int fine = tHit < 30.0 ? 13 : 9;
24654
+ float e = 0.008 * tHit + 0.01;
24655
+ float h = heightAt(pos.xz, fine);
24656
+ vec3 n = normalize(vec3(h - heightAt(pos.xz + vec2(e, 0.0), fine), e, h - heightAt(pos.xz + vec2(0.0, e), fine)));
24657
+ // Far away the waves blur into a flatter surface.
24658
+ n = normalize(mix(n, vec3(0.0, 1.0, 0.0), smoothstep(30.0, 200.0, tHit)));
24659
+ float fres = 0.02 + 0.98 * pow(1.0 - max(dot(n, -rd), 0.0), 5.0);
24660
+ vec3 r = reflect(rd, n);
24661
+ r.y = abs(r.y);
24662
+ vec3 refl = sky(r, sun);
24663
+ float crest = clamp(h / (AMP * max(uWaveHeight, 0.05)) + 0.3, 0.0, 1.0);
24664
+ // Light through the crests, toward the sun.
24665
+ float through = pow(max(dot(rd, sun) * 0.5 + 0.5, 0.0), 3.0) * crest;
24666
+ vec3 water = uWater * (0.35 + 0.65 * max(dot(n, sun), 0.0)) + mix(uWater * 3.0, uSun * 0.6, 0.3) * through * 0.6;
24667
+ col = mix(water, refl, fres);
24668
+ // Glint.
24669
+ col += uSun * pow(max(dot(r, sun), 0.0), 700.0) * 6.0 * smoothstep(-0.1, 0.1, uSunHeight);
24670
+ // Foam on the crests, broken up by noise.
24671
+ float foamNoise = vnoise(pos.xz * 3.0 + iTime * 0.3) * vnoise(pos.xz * 7.0 - iTime * 0.2);
24672
+ float foam = smoothstep(0.55, 0.85, crest + foamNoise * 0.6 - (1.0 - uFoamAmount) * 0.5) * uFoamAmount;
24673
+ col = mix(col, uFoam * (0.5 + 0.5 * max(dot(n, sun), 0.3)) * mix(vec3(1.0), uSkyHorizon, 0.25), foam * smoothstep(80.0, 20.0, tHit));
24674
+ // Haze toward the horizon.
24675
+ col = mix(col, sky(vec3(rd.x, 0.0, rd.z), sun), smoothstep(0.0, 1.0, 1.0 - exp(-tHit * 0.006)));
24676
+ }
24677
+ fragColor = vec4(col, 1.0);
24678
+ }`
24679
+ ),
24680
+ presets: [
24681
+ { name: "Sunset", values: { uSunHeight: 0.06 } },
24682
+ {
24683
+ name: "Day",
24684
+ values: {
24685
+ uSkyTop: [0.22, 0.48, 0.85],
24686
+ uSkyHorizon: [0.8, 0.9, 0.97],
24687
+ uSun: [1, 0.97, 0.9],
24688
+ uWater: [0.03, 0.22, 0.32],
24689
+ uSunHeight: 0.55,
24690
+ uSunAzimuth: 0.4
24691
+ }
24692
+ },
24693
+ { name: "Stormy sea", values: { uSkyTop: [0.2, 0.23, 0.27], uSkyHorizon: [0.5, 0.53, 0.55], uSun: [0.6, 0.62, 0.65], uWater: [0.05, 0.1, 0.12], uSunHeight: 0.3, uWaveHeight: 2.2, uChoppiness: 1.6, uFoamAmount: 0.9, uSpeed: 1.4 } }
24694
+ ]
24695
+ };
24696
+ var underwaterScene = {
24697
+ kind: "shader",
24698
+ id: "underwater-scene",
24699
+ name: "Underwater scene",
24700
+ description: "Looking out under water: shafts of light from the rippling surface, caustics dancing on a sandy floor, drifting particles and deep blue fog.",
24701
+ category: "water",
24702
+ tags: ["water", "underwater", "ocean", "sea", "caustics", "light", "background"],
24703
+ source: glsl(
24704
+ `// Underwater scene: a sandy floor met analytically with caustic lines (ridges of warped noise), the surface
24705
+ // above shimmering inside its bright window, light shafts as streaks of noise fanning down, particles in layers.
24706
+ uniform vec3 uDeep; // @control color default=#021E33 label="Deep water"
24707
+ uniform vec3 uShallow; // @control color default=#13708A label="Shallow water"
24708
+ uniform vec3 uLight; // @control color default=#C8F6FF label="Light"
24709
+ uniform vec3 uSand; // @control color default=#C6B188 label="Sand"
24710
+ uniform float uShafts; // @control min=0 max=1.5 default=0.7 label="Light shafts"
24711
+ uniform float uCaustics; // @control min=0 max=1.5 default=0.8 label="Caustics"
24712
+ uniform float uParticles; // @control min=0 max=1 default=0.6 label="Particles"
24713
+ uniform float uFog; // @control min=0 max=2 default=1 label="Fog"
24714
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"`,
24715
+ HASH,
24716
+ NOISE,
24717
+ `// Bright lines where light gathers: the ridges of two layers of warped noise, multiplied.
24718
+ float caustic(vec2 p, float t) {
24719
+ vec2 w = vec2(vnoise(p * 0.7 + t * 0.3), vnoise(p * 0.7 - t * 0.25 + 5.0));
24720
+ vec2 q = p + w * 1.6;
24721
+ float a = 1.0 - abs(vnoise(q + t * 0.4) * 2.0 - 1.0);
24722
+ float b = 1.0 - abs(vnoise(q * 1.7 - t * 0.35 + 3.0) * 2.0 - 1.0);
24723
+ return pow(a, 6.0) + pow(b, 6.0) * 0.7 + pow(a * b, 4.0) * 1.5;
24724
+ }
24725
+
24726
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
24727
+ float px = 1.0 / iResolution.y;
24728
+ vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
24729
+ float t = iTime * uSpeed;
24730
+ vec3 ro = vec3(0.0, 0.0, t * 0.15);
24731
+ vec3 rd = normalize(vec3(p.x, p.y + 0.08, 1.0));
24732
+ // The water itself: lighter looking up.
24733
+ vec3 water = mix(uDeep, uShallow, smoothstep(-0.45, 0.7, rd.y));
24734
+ vec3 col = water;
24735
+ float fogK = 0.09 * uFog;
24736
+
24737
+ if (rd.y < 0.0) {
24738
+ // The floor: sand in low dunes, lit by caustics, fading into the blue.
24739
+ float tf = -3.0 / rd.y;
24740
+ vec3 pos = ro + rd * tf;
24741
+ float dunes = vnoise(pos.xz * 0.35) * 0.6 + vnoise(pos.xz * 1.3) * 0.25 + vnoise(pos.xz * 6.0) * 0.08;
24742
+ vec3 sand = uSand * (0.55 + 0.5 * dunes);
24743
+ float c = caustic(pos.xz * 0.9, t * 0.8);
24744
+ // Fine caustics turn to an even glow far away.
24745
+ c = mix(c, 0.5, smoothstep(10.0, 30.0, tf));
24746
+ sand += uLight * c * 0.55 * uCaustics;
24747
+ sand *= mix(uShallow * 1.5, vec3(1.0), 0.4);
24748
+ col = mix(water, sand, exp(-tf * fogK));
24749
+ } else {
24750
+ // The surface: a bright, rippling window overhead.
24751
+ float ts = 4.0 / max(rd.y, 0.05);
24752
+ vec2 sp = (ro + rd * ts).xz;
24753
+ float ripple = caustic(sp * 0.6, t * 1.2);
24754
+ float window = smoothstep(0.35, 0.8, rd.y);
24755
+ col += uLight * (0.25 + 0.6 * ripple) * window * exp(-ts * 0.04);
24756
+ }
24757
+
24758
+ // Light shafts fanning down from the surface, swaying.
24759
+ float ang = (p.x - 0.35 * p.y) / (1.3 - p.y);
24760
+ float shafts = vnoise(vec2(ang * 9.0 + t * 0.15, t * 0.1)) * vnoise(vec2(ang * 23.0 - t * 0.1, 3.0 + t * 0.12));
24761
+ shafts = smoothstep(0.12, 0.6, shafts);
24762
+ col += uLight * shafts * smoothstep(-0.55, 0.45, p.y) * 0.35 * uShafts;
24763
+
24764
+ // Drifting particles: far ones small and dim, near ones larger and soft.
24765
+ for (int i = 0; i < 3; i++) {
24766
+ float fi = float(i);
24767
+ float scale = mix(28.0, 8.0, fi / 2.0);
24768
+ vec2 q = p * scale + vec2(t * 0.05 * (fi + 1.0), t * (0.08 + 0.05 * fi)) + fi * 17.0;
24769
+ vec2 cell = floor(q);
24770
+ if (hash12(cell + fi) > uParticles * 0.5) continue;
24771
+ vec2 at = 0.2 + 0.6 * hash22(cell + 3.0 * fi);
24772
+ at += 0.15 * vec2(sin(t * 0.7 + hash12(cell) * 6.28), cos(t * 0.5 + hash12(cell + 1.0) * 6.28));
24773
+ float d = length(fract(q) - at) / (scale * px);
24774
+ float size = mix(0.8, 3.0, fi / 2.0) * (0.6 + 0.8 * hash12(cell + 5.0));
24775
+ col += uLight * (1.0 - smoothstep(size * 0.4, size + 0.8, d)) * mix(0.25, 0.45, fi / 2.0);
24776
+ }
24777
+ // Darker at the edges and bottom, like a lens in water.
24778
+ col *= 1.0 - 0.35 * dot(p, p);
24779
+ fragColor = vec4(col, 1.0);
24780
+ }`
24781
+ ),
24782
+ presets: [
24783
+ { name: "Tropical", values: { uDeep: [0, 0.25, 0.35], uShallow: [0.1, 0.65, 0.7], uSand: [0.95, 0.88, 0.7], uFog: 0.6, uCaustics: 1.1 } },
24784
+ { name: "Murky", values: { uDeep: [0.02, 0.07, 0.05], uShallow: [0.12, 0.25, 0.18], uLight: [0.75, 0.9, 0.7], uSand: [0.4, 0.38, 0.3], uFog: 1.8, uShafts: 1.1, uParticles: 1 } }
24785
+ ]
24786
+ };
24787
+ var pondRipples = {
24788
+ kind: "shader",
24789
+ id: "pond-ripples",
24790
+ name: "Pond ripples",
24791
+ description: "Raindrops landing on a picture as if it lay under a pond, each spreading rings that bend the picture beneath; clicks drop stones that ripple wider.",
24792
+ category: "water",
24793
+ video: "filter",
24794
+ tags: ["water", "ripples", "rain", "pond", "image", "distortion", "interactive", "click"],
24795
+ source: glsl(
24796
+ `// Pond ripples: drops on cycles in grid cells (a pixel sums its own and the eight around it), clicks as larger
24797
+ // drops (iClicks), each a damped ring whose slope bends the picture and catches a highlight.
24798
+ uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.landscape} label="Image"
24799
+ uniform float uRain; // @control min=0 max=1 default=0.55 label="Rain"
24800
+ uniform float uSize; // @control min=0.3 max=2 default=1 label="Ripple size"
24801
+ uniform float uStrength; // @control min=0 max=2 default=1 label="Refraction"
24802
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
24803
+ uniform vec3 uTint; // @control color default=#BFD8E4 label="Water tint"
24804
+ uniform float uTintAmount; // @control min=0 max=1 default=0.12 label="Tint amount"
24805
+ uniform bool uDemo; // @control default=true label="Demo when idle"
24806
+ ${BACKDROP_CONTROLS}`,
24807
+ HASH,
24808
+ PICTURE,
24809
+ `const float CELL = 0.22;
24810
+
24811
+ // The slope (gradient of the water's height) of one ring: centre c, age in seconds, size and strength.
24812
+ vec2 ring(vec2 p, vec2 c, float age, float speed, float life, float amp) {
24813
+ if (age < 0.0 || age > life) return vec2(0.0);
24814
+ vec2 d = p - c;
24815
+ float r = length(d);
24816
+ float front = age * speed;
24817
+ float x = r - front;
24818
+ float width = 0.025 * uSize + age * 0.012;
24819
+ float k = 6.28318 / (0.035 * uSize + age * 0.01);
24820
+ float env = exp(-x * x / (width * width)) * (1.0 - age / life) * smoothstep(0.0, 0.04, age);
24821
+ // d/dr of sin(k x)\xB7env, about k\xB7cos(k x)\xB7env for narrow rings.
24822
+ float slope = (k * cos(k * x) - 2.0 * x / (width * width) * sin(k * x)) * env * amp;
24823
+ return d / max(r, 1e-4) * slope;
24824
+ }
24825
+
24826
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
24827
+ vec2 uv = fragCoord / iResolution.xy;
24828
+ float px = 1.0 / iResolution.y;
24829
+ vec2 p = fragCoord * px;
24830
+ float t = iTime * uSpeed;
24831
+ vec2 grad = vec2(0.0);
24832
+
24833
+ // Rain: one drop per cell per cycle.
24834
+ if (uRain > 0.0) {
24835
+ vec2 base = floor(p / CELL);
24836
+ for (int j = -1; j <= 1; j++) {
24837
+ for (int i = -1; i <= 1; i++) {
24838
+ vec2 cell = base + vec2(float(i), float(j));
24839
+ float period = mix(3.5, 0.9, uRain) * (0.7 + 0.6 * hash12(cell));
24840
+ float time = t + hash12(cell + 4.2) * period;
24841
+ float cycle = floor(time / period);
24842
+ float age = time - cycle * period;
24843
+ if (hash12(cell + cycle * 3.7) > 0.35 + 0.65 * uRain) continue;
24844
+ vec2 c = (cell + 0.15 + 0.7 * hash22(cell + cycle)) * CELL;
24845
+ grad += ring(p, c, age, 0.12 * uSize, min(period, 1.6), 0.0012 * uSize);
24846
+ }
24847
+ }
24848
+ }
24849
+ // Clicks drop stones; while nobody has clicked, the demo drops one now and then.
24850
+ if (iClicks[0].w > 0.5 || !uDemo) {
24851
+ for (int i = 0; i < 8; i++) {
24852
+ vec4 c = iClicks[i];
24853
+ if (c.w < 0.5) continue;
24854
+ grad += ring(p, c.xy * px, (iTime - c.z) * uSpeed, 0.2 * uSize, 3.0, 0.004 * uSize);
24855
+ }
24856
+ } else {
24857
+ float k = floor(t / 3.0);
24858
+ vec2 at = vec2((0.2 + 0.6 * hash12(vec2(k, 1.0))) * iResolution.x * px, 0.25 + 0.5 * hash12(vec2(k, 2.0)));
24859
+ grad += ring(p, at, t - k * 3.0, 0.2 * uSize, 3.0, 0.004 * uSize);
24860
+ }
24861
+
24862
+ vec2 offset = grad * 0.6 * uStrength;
24863
+ offset.x *= iResolution.y / iResolution.x;
24864
+ vec4 pic = pictureRGBA(uImage, uv - offset);
24865
+ vec3 col = mix(pic.rgb, pic.rgb * uTint * 1.2, uTintAmount);
24866
+ // A highlight on the slopes facing the light, and a little shade on the others.
24867
+ vec3 n = normalize(vec3(-grad * 1.5, 1.0));
24868
+ vec3 l = normalize(vec3(-0.5, 0.6, 0.65));
24869
+ float lit = dot(n, l) - l.z;
24870
+ col += vec3(1.0) * pow(max(lit, 0.0), 1.5) * 0.9;
24871
+ col *= 1.0 + min(lit, 0.0) * 0.6;
24872
+ fragColor = vec4(clamp(col, 0.0, 1.0), pic.a);
24873
+ }`
24874
+ ),
24875
+ presets: [
24876
+ { name: "Drizzle", values: { uRain: 0.25, uSize: 0.7 } },
24877
+ { name: "Downpour", values: { uRain: 1, uSize: 0.8, uStrength: 1.3, uTintAmount: 0.2 } },
24878
+ { name: "Stones only", values: { uRain: 0 } }
24879
+ ]
24880
+ };
24881
+ var waterfall = {
24882
+ kind: "shader",
24883
+ id: "waterfall",
24884
+ name: "Waterfall",
24885
+ description: "A stylised waterfall pouring over a cliff between mossy rocks into a churning plunge pool, wrapped in rising mist.",
24886
+ category: "water",
24887
+ tags: ["water", "waterfall", "nature", "mist", "landscape", "background"],
24888
+ source: glsl(
24889
+ `// Waterfall: a curtain of streaks (noise stretched down, scrolling faster as it falls) between noise-edged
24890
+ // cliffs, foam where it lands in the pool, rings spreading across the pool and fbm mist rising around the foot.
24891
+ uniform vec3 uSky; // @control color default=#A9D3EE label="Sky"
24892
+ uniform vec3 uRock; // @control color default=#4A443E label="Rock"
24893
+ uniform vec3 uFoliage; // @control color default=#3E6B3C label="Foliage"
24894
+ uniform vec3 uWater; // @control color default=#CDEFFA label="Falling water"
24895
+ uniform vec3 uPool; // @control color default=#2D7591 label="Pool"
24896
+ uniform float uWidth; // @control min=0.1 max=0.8 default=0.32 label="Width"
24897
+ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
24898
+ uniform float uMist; // @control min=0 max=1.5 default=0.8 label="Mist"`,
24899
+ HASH,
24900
+ NOISE,
24901
+ FBM,
24902
+ `const float LIP = 0.74;
24903
+ const float POOL = 0.24;
24904
+
24905
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
24906
+ float px = 1.0 / iResolution.y;
24907
+ vec2 uv = fragCoord / iResolution.xy;
24908
+ float aspect = iResolution.x / iResolution.y;
24909
+ vec2 p = vec2((uv.x - 0.5) * aspect, uv.y);
24910
+ float t = iTime * uSpeed;
24911
+
24912
+ // Sky and distant hills.
24913
+ vec3 col = mix(uSky * 1.08, uSky * 0.85, uv.y);
24914
+ float hills = 0.83 + 0.05 * fbm(vec2(p.x * 2.0, 3.0), 4);
24915
+ col = mix(col, mix(uFoliage, uSky, 0.55), step(uv.y, hills));
24916
+
24917
+ // The cliff: rock on both sides of the fall, its top edge ragged with foliage.
24918
+ float half_ = uWidth * 0.5 * aspect * 0.5;
24919
+ float edgeNoise = (fbm(vec2(p.y * 8.0, 1.0), 3) - 0.5) * 0.06;
24920
+ float fallEdge = abs(p.x) - half_ - edgeNoise - (LIP - p.y) * 0.03;
24921
+ float cliffTop = LIP + 0.03 + 0.04 * fbm(vec2(p.x * 4.0, 7.0), 3);
24922
+ if (p.y < cliffTop) {
24923
+ float strata = fbm(vec2(p.x * 3.0, p.y * 14.0), 4);
24924
+ vec3 rock = uRock * (0.6 + 0.7 * strata) * (0.75 + 0.35 * smoothstep(0.0, 0.3, fallEdge));
24925
+ // Moss and leaves on ledges and along the top.
24926
+ float moss = smoothstep(0.55, 0.7, fbm(p * 9.0, 4)) + smoothstep(cliffTop - 0.08, cliffTop - 0.01, p.y);
24927
+ rock = mix(rock, uFoliage * (0.7 + 0.5 * fbm(p * 30.0, 2)), clamp(moss, 0.0, 1.0) * 0.85);
24928
+ col = rock;
24929
+ }
24930
+
24931
+ // The falling water, in front of the cliff between its edges.
24932
+ float inFall = smoothstep(px * 2.0, -px * 2.0, fallEdge) * step(POOL - 0.02, p.y) * step(p.y, LIP + 0.012);
24933
+ if (inFall > 0.0) {
24934
+ float drop = max(LIP - p.y, 0.0);
24935
+ // Water speeds up as it falls: the scroll stretches toward the bottom.
24936
+ float s = pow(drop, 0.7) * 6.0 - t * 1.6;
24937
+ float streaks = vnoise(vec2(p.x * 70.0, s * 3.0)) * 0.6 + vnoise(vec2(p.x * 160.0 + 3.0, s * 5.0)) * 0.4;
24938
+ float sheets = vnoise(vec2(p.x * 12.0, s * 1.2));
24939
+ vec3 w = mix(uWater * 0.65, uWater * 1.1, smoothstep(0.3, 0.8, streaks * 0.7 + sheets * 0.5));
24940
+ // The lip: water bulges over the edge, bright.
24941
+ w += vec3(0.25) * smoothstep(LIP - 0.04, LIP, p.y);
24942
+ // Darker gaps near the edges where the curtain thins.
24943
+ float thin = smoothstep(-0.06, 0.0, fallEdge + streaks * 0.03);
24944
+ w = mix(w, col, thin * 0.6);
24945
+ col = mix(col, w, inFall);
24946
+ }
24947
+
24948
+ // The plunge pool.
24949
+ if (p.y < POOL) {
24950
+ float depth = (POOL - p.y) / POOL;
24951
+ float z = 1.0 / (0.05 + depth);
24952
+ vec2 q = vec2(p.x * z * 0.6, z);
24953
+ float waves = vnoise(vec2(q.x * 5.0, q.y * 2.0 - t * 0.6)) * 0.5 + vnoise(vec2(q.x * 13.0, q.y * 5.0 + t * 0.3)) * 0.5;
24954
+ vec3 pool = uPool * (0.7 + 0.5 * waves) + mix(uSky, vec3(1.0), 0.3) * smoothstep(0.6, 0.85, waves) * 0.3;
24955
+ // Rings spreading from where the water lands.
24956
+ float rings = sin(length(vec2(p.x / max(half_ * 1.4, 0.05), (POOL - p.y) * 6.0)) * 9.0 - t * 4.0);
24957
+ pool += uWater * smoothstep(0.7, 1.0, rings) * 0.12 * (1.0 - depth);
24958
+ col = pool;
24959
+ }
24960
+
24961
+ // Foam where the curtain lands, churning.
24962
+ float foamZone = exp(-pow((p.y - POOL) / 0.05, 2.0)) * smoothstep(half_ * 1.6, half_ * 0.6, abs(p.x));
24963
+ float churn = fbm(vec2(p.x * 14.0, p.y * 18.0 - t * 1.5), 4);
24964
+ col = mix(col, uWater * 1.15, clamp(foamZone * (0.6 + churn), 0.0, 1.0));
24965
+
24966
+ // Mist rising from the foot.
24967
+ vec2 mp = vec2(p.x * 2.5, (p.y - POOL) * 3.0 - t * 0.25);
24968
+ float mist = fbm(mp + vec2(fbm(mp + t * 0.05, 3), 0.0), 5);
24969
+ float around = exp(-pow((p.y - POOL - 0.08) / 0.2, 2.0)) * smoothstep(half_ * 3.5, half_ * 0.5, abs(p.x));
24970
+ col = mix(col, vec3(0.95, 0.98, 1.0), clamp(smoothstep(0.35, 0.8, mist) * around * uMist, 0.0, 0.85));
24971
+ // Spray droplets thrown up.
24972
+ vec2 g = vec2(p.x * 90.0, (p.y + t * 0.05) * 90.0);
24973
+ vec2 cell = floor(g);
24974
+ float spray = step(0.93, hash12(cell)) * (1.0 - smoothstep(0.1, 0.35, length(fract(g) - 0.5)));
24975
+ col += spray * around * 0.5 * uMist;
24976
+ fragColor = vec4(col, 1.0);
24977
+ }`
24978
+ ),
24979
+ presets: [
24980
+ { name: "Jungle", values: { uSky: [0.75, 0.9, 0.85], uRock: [0.25, 0.27, 0.22], uFoliage: [0.18, 0.45, 0.2], uPool: [0.12, 0.45, 0.4], uMist: 1.2 } },
24981
+ { name: "Dusk", values: { uSky: [0.95, 0.6, 0.5], uRock: [0.22, 0.18, 0.2], uFoliage: [0.2, 0.22, 0.25], uWater: [0.95, 0.85, 0.85], uPool: [0.3, 0.25, 0.4] } }
24982
+ ]
24983
+ };
24984
+ var waterShaders = [oceanWaves, underwaterScene, pondRipples, waterfall];
24985
+
22802
24986
  // src/catalog/text.ts
22803
- var r112 = (n15) => Math.round(n15 * 10) / 10;
24987
+ var r112 = (n17) => Math.round(n17 * 10) / 10;
22804
24988
  function fit5(text, width, max, ems = textWidthEm(text)) {
22805
24989
  const e = Math.max(0.6, ems);
22806
24990
  const size = Math.min(max, width / e);
@@ -23264,7 +25448,7 @@ var glitchText = {
23264
25448
  };
23265
25449
  },
23266
25450
  motion: (() => {
23267
- const word = (paint) => `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" ${paint}>{{text}}</text>`;
25451
+ const word = (paint2) => `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" ${paint2}>{{text}}</text>`;
23268
25452
  const jumps = (id, value) => GLITCH.map((key, k) => ({ id, attrs: { transform: [value(k), value(k)] }, delay: key.at, ms: 0 }));
23269
25453
  return {
23270
25454
  svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='GLITCH' label='Text'}} {{background: color default=#07070B label='Background'}} {{ink: color default=#F5F5F5 label='Text colour'}} {{left: color default=#FF0040 label='Red copy'}} {{right: color default=#00F0FF label='Cyan copy'}} {{amount: float min=0 max=3 default=1 step=0.1 label='Intensity'}} ${typeface(HEAVY2, 900)} --><defs>${[0, 1, 2, 3].map((b) => `<clipPath id="slice${b}"><rect x="-5000" y="{{cut${b}}}" width="10400" height="{{band${b}}}"/></clipPath>`).join("")}</defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g id="red" style="mix-blend-mode:screen">${word(`fill="{{left}}"`)}</g><g id="cyan" style="mix-blend-mode:screen">${word(`fill="{{right}}"`)}</g>${[0, 1, 2, 3].map((b) => `<g clip-path="url(#slice${b})"><g id="s${b}">${word(`fill="{{ink}}"`)}</g></g>`).join("")}</svg>`,
@@ -23318,7 +25502,7 @@ var extrudedText = {
23318
25502
  };
23319
25503
  },
23320
25504
  motion: (() => {
23321
- const word = (paint) => `x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" stroke-linejoin="round" ${paint}`;
25505
+ const word = (paint2) => `x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" stroke-linejoin="round" ${paint2}`;
23322
25506
  const layers = Array.from({ length: LAYERS }, (_, k) => LAYERS - k);
23323
25507
  return {
23324
25508
  svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='RETRO' label='Text'}} {{background: color default=#FFE8C2 label='Background'}} {{face: color default=#FFD23F label='Face'}} {{outline: color default=#1A1423 label='Outline'}} {{side: color default=#EE4266 label='Extrude'}} {{shade: color default=#3B1F2B label='Shadow'}} {{depth: float min=0.2 max=2 default=1 step=0.1 label='Depth'}} ${typeface(HEAVY2, 900)} --><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/>${layers.map((i) => `<text id="e${i}" transform="translate(0 0)" ${word(`fill="{{fill${i}}}" stroke="{{fill${i}}}" stroke-width="2"`)}>{{text}}</text>`).join("")}<text ${word(`fill="{{face}}" stroke="{{outline}}" stroke-width="2.5" paint-order="stroke"`)}>{{text}}</text></svg>`,
@@ -24999,7 +27183,7 @@ var dripText = {
24999
27183
  const left = 200 - width / 2;
25000
27184
  const runs = Math.min(DRIP_RUNS, Math.max(1, Math.round(width / DRIP_RUN)));
25001
27185
  const run = width / runs;
25002
- const r114 = (n15) => Math.round(n15 * 10) / 10;
27186
+ const r116 = (n17) => Math.round(n17 * 10) / 10;
25003
27187
  const edge = Number(v.stroke) / 2 / DRIP_SIZE;
25004
27188
  const tip = (DRIP_TOP + Math.max(...DRIPS2.map((d) => d.length)) * Number(v.drip) + 13 * 0.72 - DRIP_BASELINE) / DRIP_SIZE;
25005
27189
  const overhang = [edge, edge, Math.max(0.05, tip) + edge, edge].map((e) => Math.round(e * 100) / 100).join(" ");
@@ -25013,15 +27197,15 @@ var dripText = {
25013
27197
  overhang,
25014
27198
  ...Object.fromEntries(
25015
27199
  DRIP_SLOTS.flatMap((d) => {
25016
- const x = r114(left + (d.run + d.at) * run);
25017
- const length = r114(d.length * Number(v.drip));
27200
+ const x = r116(left + (d.run + d.at) * run);
27201
+ const length = r116(d.length * Number(v.drip));
25018
27202
  return [
25019
27203
  [`show${d.i}`, d.run < runs ? "inline" : "none"],
25020
27204
  [`x${d.i}`, x],
25021
- [`left${d.i}`, r114(x - d.width / 2)],
25022
- [`shine${d.i}`, r114(x - d.width * 0.25)],
27205
+ [`left${d.i}`, r116(x - d.width / 2)],
27206
+ [`shine${d.i}`, r116(x - d.width * 0.25)],
25023
27207
  [`len${d.i}`, length],
25024
- [`tip${d.i}`, r114(DRIP_TOP + length)]
27208
+ [`tip${d.i}`, r116(DRIP_TOP + length)]
25025
27209
  ];
25026
27210
  })
25027
27211
  )
@@ -25029,8 +27213,8 @@ var dripText = {
25029
27213
  },
25030
27214
  motion: (() => {
25031
27215
  const word = `<text x="200" y="${DRIP_BASELINE}" text-anchor="middle" textLength="{{width}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="${DRIP_SIZE}" font-weight="{{weight}}" stroke-linejoin="round"`;
25032
- const drips = (layer, paint) => DRIP_SLOTS.map(
25033
- (d) => `<g display="{{show${d.i}}}"><rect id="${layer}r${d.i}" x="{{left${d.i}}}" y="${DRIP_TOP}" width="${d.width}" height="0" rx="${d.width / 2}" ${paint}/><circle id="${layer}c${d.i}" cx="{{x${d.i}}}" cy="${DRIP_TOP}" r="${d.width * 0.72}" ${paint}/></g>`
27216
+ const drips = (layer, paint2) => DRIP_SLOTS.map(
27217
+ (d) => `<g display="{{show${d.i}}}"><rect id="${layer}r${d.i}" x="{{left${d.i}}}" y="${DRIP_TOP}" width="${d.width}" height="0" rx="${d.width / 2}" ${paint2}/><circle id="${layer}c${d.i}" cx="{{x${d.i}}}" cy="${DRIP_TOP}" r="${d.width * 0.72}" ${paint2}/></g>`
25034
27218
  ).join("");
25035
27219
  const outline4 = `fill="{{outline}}" stroke="{{outline}}" stroke-width="{{stroke}}"`;
25036
27220
  const pop2 = (from, to) => [`translate(200 100) scale(${from}) translate(-200 -100)`, `translate(200 100) scale(${to}) translate(-200 -100)`];
@@ -25164,6 +27348,1038 @@ var vectorEffects = [
25164
27348
  uploadCard
25165
27349
  ];
25166
27350
 
27351
+ // src/catalog/data.ts
27352
+ var r114 = (v) => Math.round(v * 10) / 10;
27353
+ var r212 = (v) => Math.round(v * 100) / 100;
27354
+ var n15 = (name) => `{{${name}}}`;
27355
+ var SANS12 = `system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif`;
27356
+ function fit7(text, width, max) {
27357
+ const e = Math.max(0.6, textWidthEm(String(text)));
27358
+ const size = Math.min(max, width / e);
27359
+ return { size: r114(size), length: r114(e * size) };
27360
+ }
27361
+ function fitLabel(text, width, max, min) {
27362
+ const whole = String(text ?? "");
27363
+ const f = fit7(whole, width, max);
27364
+ if (f.size >= min) return { text: whole, ...f };
27365
+ const letters = Array.from(whole);
27366
+ while (letters.length > 1 && Math.max(0.6, textWidthEm(`${letters.join("").trimEnd()}\u2026`)) * min > width) letters.pop();
27367
+ const cut = `${letters.join("").trimEnd()}\u2026`;
27368
+ return { text: cut, ...fit7(cut, width, max) };
27369
+ }
27370
+ var paint = (v) => isCurrentColor(v) ? "currentColor" : Array.isArray(v) ? rgbToHex(v) : "#000000";
27371
+ var formatSafe = (v) => String(v ?? "").replace(/[{}]/g, "");
27372
+ var DATA_PALETTE = ["#6366F1", "#22C55E", "#F59E0B", "#EC4899", "#0EA5E9", "#A855F7"];
27373
+ var palette = (count = DATA_PALETTE.length) => DATA_PALETTE.slice(0, count).map((hex, i) => `{{c${i + 1}: color default=${hex} label='Colour ${i + 1}'}}`).join(" ");
27374
+ var PALETTE_PRESETS = [
27375
+ { name: "Ocean", values: { c1: [0.03, 0.4, 0.62], c2: [0.05, 0.65, 0.91], c3: [0.22, 0.74, 0.97], c4: [0.49, 0.83, 0.99], c5: [0.08, 0.72, 0.65], c6: [0.37, 0.92, 0.83] } },
27376
+ { name: "Sunset", values: { c1: [0.94, 0.27, 0.27], c2: [0.98, 0.45, 0.09], c3: [0.98, 0.75, 0.14], c4: [0.93, 0.29, 0.6], c5: [0.66, 0.33, 0.97], c6: [0.4, 0.4, 0.95] } }
27377
+ ];
27378
+ function parseNumberList(text, max) {
27379
+ return String(text ?? "").split(/[,;\s]+/).filter((t) => t !== "").slice(0, max).map((token) => {
27380
+ const cleaned = token.replace(/[^0-9.eE+-]/g, "");
27381
+ const value = /\d/.test(cleaned) ? Number(cleaned) : NaN;
27382
+ return Number.isFinite(value) ? Math.max(-1e9, Math.min(1e9, value)) : NaN;
27383
+ });
27384
+ }
27385
+ function parseLabels(text, max) {
27386
+ return String(text ?? "").split(/[,;\n]/).map((s) => s.trim()).slice(0, max);
27387
+ }
27388
+ function parseShares(text, max, fallback) {
27389
+ const values = parseNumberList(text, max).map((v) => Number.isFinite(v) && v > 0 ? v : 0);
27390
+ return values.some((v) => v > 0) ? values : [...fallback];
27391
+ }
27392
+ function decimalsOf(values) {
27393
+ let most = 0;
27394
+ for (const v of values) {
27395
+ if (!Number.isFinite(v) || Number.isInteger(v)) continue;
27396
+ const s = String(v);
27397
+ most = Math.max(most, s.includes("e") ? 2 : s.split(".")[1]?.length ?? 0);
27398
+ }
27399
+ return Math.min(2, most);
27400
+ }
27401
+ function parseRace(text, maxRows, maxStates, fallback) {
27402
+ const rows = [];
27403
+ for (const line of String(text ?? "").split(/[;\n]+/)) {
27404
+ if (!line.trim()) continue;
27405
+ const colon = line.indexOf(":");
27406
+ const name = colon >= 0 ? line.slice(0, colon).trim() : `#${rows.length + 1}`;
27407
+ const raw = parseNumberList(colon >= 0 ? line.slice(colon + 1) : line, maxStates);
27408
+ if (raw.some((v) => Number.isFinite(v))) rows.push({ name, raw });
27409
+ if (rows.length === maxRows) break;
27410
+ }
27411
+ if (!rows.length) return fallback.map((r) => ({ name: r.name, values: [...r.values] }));
27412
+ const states = Math.max(...rows.map((r) => r.raw.length));
27413
+ return rows.map(({ name, raw }) => {
27414
+ let last = raw.find((v) => Number.isFinite(v)) ?? 0;
27415
+ const values = Array.from({ length: states }, (_, k) => {
27416
+ const v = raw[k];
27417
+ if (v !== void 0 && Number.isFinite(v)) last = Math.max(0, v);
27418
+ return last;
27419
+ });
27420
+ return { name, values };
27421
+ });
27422
+ }
27423
+ function parseSeries(text, maxRows, maxPoints, fallback) {
27424
+ const rows = String(text ?? "").split(/[;\n]+/).map((line) => parseNumberList(line, maxPoints)).filter((row) => row.some((v) => Number.isFinite(v))).slice(0, maxRows).map((row) => row.map((v) => Number.isFinite(v) && v > 0 ? v : 0));
27425
+ if (!rows.length || rows.every((r) => r.every((v) => v === 0))) return fallback.map((r) => [...r]);
27426
+ const points = Math.max(2, ...rows.map((r) => r.length));
27427
+ return rows.map((r) => Array.from({ length: points }, (_, j) => r[Math.min(j, r.length - 1)]));
27428
+ }
27429
+ var SLOTS3 = 6;
27430
+ var LEGEND = { swatch: 158, label: 174, right: 250, pitch: 22 };
27431
+ function legendValues(labels, fractions, at4, cy) {
27432
+ const shown = fractions.map((f) => f > 0);
27433
+ const count = shown.filter(Boolean).length;
27434
+ const out = {};
27435
+ let row = 0;
27436
+ for (let i = 0; i < SLOTS3; i++) {
27437
+ const f = fractions[i] ?? 0;
27438
+ const label = labels[i] ?? "";
27439
+ const { text, size, length } = fitLabel(label, LEGEND.right - LEGEND.label - 32, 11, 8);
27440
+ out[`show${i}`] = f > 0 ? "inline" : "none";
27441
+ out[`ly${i}`] = r114(cy + ((f > 0 ? row : 0) - (count - 1) / 2) * LEGEND.pitch);
27442
+ out[`l${i}`] = text;
27443
+ out[`ls${i}`] = size;
27444
+ out[`ll${i}`] = length;
27445
+ out[`pct${i}`] = Math.round(f * 100);
27446
+ out[`at${i}`] = at4[i] ?? 0;
27447
+ if (f > 0) row++;
27448
+ }
27449
+ return out;
27450
+ }
27451
+ var legendMarkup = (prefix) => Array.from(
27452
+ { length: SLOTS3 },
27453
+ (_, i) => `<g id="${prefix}Row${i}" display="{{show${i}}}" opacity="0" transform="translate(8 {{ly${i}}})"><rect x="${LEGEND.swatch}" y="-5" width="10" height="10" rx="3" fill="{{c${i + 1}}}"/><text x="${LEGEND.label}" y="4" font-size="{{ls${i}}}" textLength="{{ll${i}}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}">{{l${i}}}</text><text id="${prefix}Pct${i}" x="${LEGEND.right}" y="4" text-anchor="end" font-size="11" font-weight="700" fill="{{ink}}">0%</text></g>`
27454
+ ).join("");
27455
+ var legendAnimations = (prefix) => Array.from({ length: SLOTS3 }, (_, i) => [
27456
+ { id: `${prefix}Row${i}`, attrs: { opacity: [0, 1], transform: [`translate(8 {{ly${i}}})`, `translate(0 {{ly${i}}})`] }, delay: n15(`at${i}`), ms: 320, ease: "out" },
27457
+ { id: `${prefix}Pct${i}`, count: [0, n15(`pct${i}`)], format: "{n}%", delay: n15(`at${i}`), ms: 600, ease: "out" }
27458
+ ]).flat();
27459
+ var DONUT = { cx: 80, cy: 80, r: 50, width: 22, start: 150, sweep: 1400 };
27460
+ var DONUT_DEFAULT = [42, 28, 18, 12];
27461
+ var donutChart = {
27462
+ kind: "vector",
27463
+ id: "donut-chart",
27464
+ name: "Donut chart",
27465
+ description: "Segments sweeping round a ring one after another, a legend giving each share, and the total counting up in the middle.",
27466
+ category: "data",
27467
+ tags: ["chart", "donut", "share", "dashboard", "report", "explainer"],
27468
+ hold: 1600,
27469
+ presets: [...PALETTE_PRESETS, { name: "Budget", values: { values: "1200,800,450,300,250", labels: "Rent,Food,Travel,Fun,Other", caption: "per month" } }],
27470
+ derived: (v) => {
27471
+ const values = parseShares(v.values, SLOTS3, DONUT_DEFAULT);
27472
+ const total = values.reduce((a, b) => a + b, 0);
27473
+ const fractions = values.map((x) => x / total);
27474
+ const gap = fractions.filter((f) => f > 0).length > 1 ? 5e-3 : 0;
27475
+ const out = {};
27476
+ const at4 = [];
27477
+ let cum = 0;
27478
+ for (let i = 0; i < SLOTS3; i++) {
27479
+ const f = fractions[i] ?? 0;
27480
+ at4.push(Math.round(DONUT.start + cum * DONUT.sweep));
27481
+ out[`rot${i}`] = r212(-90 + cum * 360);
27482
+ out[`frac${i}`] = Math.min(1, Math.max(1e-3, +(f - gap).toFixed(4)));
27483
+ out[`ms${i}`] = Math.max(60, Math.round(f * DONUT.sweep));
27484
+ cum += f;
27485
+ }
27486
+ const dec = decimalsOf(values);
27487
+ const totalSize = fit7(formatCount(total, { decimals: dec }), 64, 26).size;
27488
+ const caption = fitLabel(v.caption, 58, 9.5, 7.5);
27489
+ return {
27490
+ ...legendValues(parseLabels(v.labels, SLOTS3), fractions, at4, DONUT.cy),
27491
+ ...out,
27492
+ total: +total.toFixed(dec),
27493
+ dec,
27494
+ totalSize,
27495
+ totalY: r114(DONUT.cy + totalSize * 0.35 - (String(v.caption).trim() ? 5 : 0)),
27496
+ captionY: r114(DONUT.cy + totalSize * 0.35 + 9),
27497
+ captionText: caption.text,
27498
+ captionSize: caption.size,
27499
+ captionLength: caption.length,
27500
+ countMs: DONUT.start + DONUT.sweep
27501
+ };
27502
+ },
27503
+ motion: {
27504
+ svg: `<svg viewBox="0 0 260 160" font-family="${SANS12}"><!-- {{values: text default='42,28,18,12' label='Values (comma-separated)'}} {{labels: text default='Direct,Social,Search,Email' label='Labels (comma-separated)'}} {{caption: text default='visits' label='Caption'}} {{ink: color default=#1E293B label='Text'}} {{track: color default=#E2E8F0 label='Track'}} ${palette()} --><circle cx="${DONUT.cx}" cy="${DONUT.cy}" r="${DONUT.r}" fill="none" stroke="{{track}}" stroke-width="${DONUT.width}"/>${Array.from(
27505
+ { length: SLOTS3 },
27506
+ (_, i) => `<g display="{{show${i}}}"><circle id="donutChartSeg${i}" cx="${DONUT.cx}" cy="${DONUT.cy}" r="${DONUT.r}" fill="none" stroke="{{c${i + 1}}}" stroke-width="${DONUT.width}" transform="rotate({{rot${i}}} ${DONUT.cx} ${DONUT.cy})"/></g>`
27507
+ ).join(
27508
+ ""
27509
+ )}<text id="donutChartTotal" x="${DONUT.cx}" y="{{totalY}}" text-anchor="middle" font-size="{{totalSize}}" font-weight="800" fill="{{ink}}">0</text><text x="${DONUT.cx}" y="{{captionY}}" text-anchor="middle" font-size="{{captionSize}}" textLength="{{captionLength}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}" fill-opacity="0.6">{{captionText}}</text>${legendMarkup("donutChart")}</svg>`,
27510
+ animate: [
27511
+ ...Array.from({ length: SLOTS3 }, (_, i) => ({ id: `donutChartSeg${i}`, draw: [0, n15(`frac${i}`)], delay: n15(`at${i}`), ms: n15(`ms${i}`), ease: "linear" })),
27512
+ { id: "donutChartTotal", count: [0, n15("total")], decimals: n15("dec"), ms: n15("countMs"), ease: "out" },
27513
+ ...legendAnimations("donutChart")
27514
+ ]
27515
+ }
27516
+ };
27517
+ var PIE = { cx: 80, cy: 80, r: 58, pop: 7, start: 150, stagger: 230 };
27518
+ var PIE_DEFAULT = [38, 26, 20, 16];
27519
+ var pieMove = (dx, dy, s) => `translate(${dx} ${dy}) translate(${PIE.cx} ${PIE.cy}) scale(${s}) translate(${-PIE.cx} ${-PIE.cy})`;
27520
+ function wedge(a, b) {
27521
+ const { cx, cy, r } = PIE;
27522
+ if (b - a <= 0) return `M${cx} ${cy}Z`;
27523
+ if (b - a >= 0.9999) return `M${cx} ${cy - r} A${r} ${r} 0 1 1 ${cx} ${cy + r} A${r} ${r} 0 1 1 ${cx} ${cy - r}Z`;
27524
+ const at4 = (f) => `${r212(cx + r * Math.sin(f * 2 * Math.PI))} ${r212(cy - r * Math.cos(f * 2 * Math.PI))}`;
27525
+ return `M${cx} ${cy} L${at4(a)} A${r} ${r} 0 ${b - a > 0.5 ? 1 : 0} 1 ${at4(b)}Z`;
27526
+ }
27527
+ var pieChart = {
27528
+ kind: "vector",
27529
+ id: "pie-chart",
27530
+ name: "Pie chart",
27531
+ description: "Pie slices popping out one at a time and settling into place, each with its percentage, beside a legend.",
27532
+ category: "data",
27533
+ tags: ["chart", "pie", "share", "percentage", "dashboard", "report"],
27534
+ hold: 1600,
27535
+ presets: [...PALETTE_PRESETS, { name: "Survey", values: { values: "54,31,15", labels: "Yes,No,Not sure" } }],
27536
+ derived: (v) => {
27537
+ const values = parseShares(v.values, SLOTS3, PIE_DEFAULT);
27538
+ const total = values.reduce((a, b) => a + b, 0);
27539
+ const fractions = values.map((x) => x / total);
27540
+ const out = {};
27541
+ const at4 = [];
27542
+ const ink = paint(v.ink);
27543
+ let cum = 0;
27544
+ for (let i = 0; i < SLOTS3; i++) {
27545
+ const f = fractions[i] ?? 0;
27546
+ const mid = (cum + f / 2) * 2 * Math.PI;
27547
+ const inside = f >= 0.08;
27548
+ out[`tshow${i}`] = f >= 0.04 ? "inline" : "none";
27549
+ const radius = inside ? PIE.r * 0.62 : PIE.r + 14;
27550
+ at4.push(PIE.start + i * PIE.stagger);
27551
+ out[`d${i}`] = wedge(cum, cum + f);
27552
+ out[`ox${i}`] = r212(PIE.pop * Math.sin(mid));
27553
+ out[`oy${i}`] = r212(-PIE.pop * Math.cos(mid));
27554
+ out[`tx${i}`] = r114(PIE.cx + radius * Math.sin(mid));
27555
+ out[`ty${i}`] = r114(PIE.cy - radius * Math.cos(mid) + 4);
27556
+ out[`tf${i}`] = inside ? paint(v.onSlice) : ink;
27557
+ out[`tl${i}`] = PIE.start + i * PIE.stagger + 260;
27558
+ cum += f;
27559
+ }
27560
+ return { ...legendValues(parseLabels(v.labels, SLOTS3), fractions, at4, PIE.cy), ...out };
27561
+ },
27562
+ motion: {
27563
+ svg: `<svg viewBox="0 0 260 160" font-family="${SANS12}"><!-- {{values: text default='38,26,20,16' label='Values (comma-separated)'}} {{labels: text default='Product,Services,Support,Other' label='Labels (comma-separated)'}} {{ink: color default=#1E293B label='Text'}} {{onSlice: color default=#FFFFFF label='Labels on slices'}} {{edge: color default=#FFFFFF label='Slice edges'}} ${palette()} -->${Array.from(
27564
+ { length: SLOTS3 },
27565
+ (_, i) => `<g id="pieChartSlice${i}" display="{{show${i}}}" transform="${pieMove(0, 0, 0)}"><path d="{{d${i}}}" fill="{{c${i + 1}}}" stroke="{{edge}}" stroke-width="2" stroke-linejoin="round"/></g>`
27566
+ ).join("")}${Array.from(
27567
+ { length: SLOTS3 },
27568
+ (_, i) => `<text id="pieChartLabel${i}" display="{{tshow${i}}}" x="{{tx${i}}}" y="{{ty${i}}}" text-anchor="middle" font-size="11" font-weight="700" fill="{{tf${i}}}" opacity="0">0%</text>`
27569
+ ).join("")}${legendMarkup("pieChart")}</svg>`,
27570
+ animate: [
27571
+ ...Array.from({ length: SLOTS3 }, (_, i) => [
27572
+ { id: `pieChartSlice${i}`, attrs: { transform: [pieMove(0, 0, 0), pieMove(`{{ox${i}}}`, `{{oy${i}}}`, 1.06)] }, delay: n15(`at${i}`), ms: 260, ease: "out" },
27573
+ { id: `pieChartSlice${i}`, attrs: { transform: [pieMove(`{{ox${i}}}`, `{{oy${i}}}`, 1.06), pieMove(0, 0, 1)] }, delay: n15(`tl${i}`), ms: 300, ease: "in-out" },
27574
+ { id: `pieChartLabel${i}`, attrs: { opacity: [0, 1] }, delay: n15(`tl${i}`), ms: 200 },
27575
+ { id: `pieChartLabel${i}`, count: [0, n15(`pct${i}`)], format: "{n}%", delay: n15(`tl${i}`), ms: 500, ease: "out" }
27576
+ ]).flat(),
27577
+ ...legendAnimations("pieChart")
27578
+ ]
27579
+ }
27580
+ };
27581
+ var GAUGE = { cx: 120, cy: 112, r: 84, band: 14 };
27582
+ var gaugeAt = (f, r) => `${r212(GAUGE.cx - r * Math.cos(f * Math.PI))} ${r212(GAUGE.cy - r * Math.sin(f * Math.PI))}`;
27583
+ var gaugeArc = (a, b) => b - a <= 5e-4 ? `M${gaugeAt(a, GAUGE.r)}` : `M${gaugeAt(a, GAUGE.r)} A${GAUGE.r} ${GAUGE.r} 0 0 1 ${gaugeAt(b, GAUGE.r)}`;
27584
+ var needle = (deg) => `rotate(${deg} ${GAUGE.cx} ${GAUGE.cy})`;
27585
+ var gauge = {
27586
+ kind: "vector",
27587
+ id: "gauge",
27588
+ name: "Gauge",
27589
+ description: "A speedometer gauge: its coloured band draws on, the needle sweeps past the value and settles, and the reading counts up beneath.",
27590
+ category: "data",
27591
+ tags: ["gauge", "speedometer", "meter", "kpi", "dashboard", "score"],
27592
+ hold: 1600,
27593
+ presets: [
27594
+ { name: "Speed", values: { value: 88, max: 160, unit: " km/h", label: "Speed", warn: 70, danger: 85 } },
27595
+ { name: "Dark", values: { ink: [0.94, 0.95, 0.97], needleColor: [0.94, 0.95, 0.97], hub: [0.06, 0.07, 0.1] } }
27596
+ ],
27597
+ derived: (v) => {
27598
+ const max = Math.max(1e-3, Number(v.max) || 100);
27599
+ const value = Math.min(max, Math.max(0, Number(v.value) || 0));
27600
+ const f = value / max;
27601
+ const warn = Math.min(1, Math.max(0, Number(v.warn) / 100));
27602
+ const danger = Math.min(1, Math.max(warn, Number(v.danger) / 100));
27603
+ const dec = decimalsOf([value]);
27604
+ const unit = formatSafe(v.unit);
27605
+ const reading = fit7(`${formatCount(value, { decimals: dec })}${unit}`, 150, 26);
27606
+ const label = fitLabel(v.label, 150, 10, 8);
27607
+ const ends = [0, max].map((x) => fit7(formatCount(x, { decimals: decimalsOf([x]) }), 30, 10).size);
27608
+ return {
27609
+ band0: gaugeArc(0, warn),
27610
+ band1: gaugeArc(warn, danger),
27611
+ band2: gaugeArc(danger, 1),
27612
+ over: needle(r114(180 * Math.min(1, f + 0.06))),
27613
+ angle: needle(r114(180 * f)),
27614
+ reading: +value.toFixed(dec),
27615
+ dec,
27616
+ unit,
27617
+ readingSize: reading.size,
27618
+ labelText: label.text,
27619
+ labelSize: label.size,
27620
+ labelLength: label.length,
27621
+ maxText: formatCount(max, { decimals: decimalsOf([max]) }),
27622
+ maxSize: ends[1]
27623
+ };
27624
+ },
27625
+ motion: {
27626
+ svg: `<svg viewBox="0 0 240 170" font-family="${SANS12}"><!-- {{value: float min=0 max=10000 default=72 step=1 label='Value'}} {{max: float min=1 max=10000 default=100 step=1 label='Scale maximum'}} {{unit: text default='%' label='Unit'}} {{label: text default='Performance' label='Label'}} {{warn: float min=0 max=100 default=60 step=1 label='Amber from (% of scale)'}} {{danger: float min=0 max=100 default=85 step=1 label='Red from (% of scale)'}} {{low: color default=#22C55E label='Low band'}} {{mid: color default=#F59E0B label='Middle band'}} {{high: color default=#EF4444 label='High band'}} {{needleColor: color default=#0F172A label='Needle'}} {{hub: color default=#FFFFFF label='Hub centre'}} {{ink: color default=#1E293B label='Text'}} --><path id="gaugeBand0" d="{{band0}}" fill="none" stroke="{{low}}" stroke-width="${GAUGE.band}"/><path id="gaugeBand1" d="{{band1}}" fill="none" stroke="{{mid}}" stroke-width="${GAUGE.band}"/><path id="gaugeBand2" d="{{band2}}" fill="none" stroke="{{high}}" stroke-width="${GAUGE.band}"/><g stroke="{{ink}}" stroke-opacity="0.35" stroke-linecap="round">${Array.from({ length: 11 }, (_, k) => `<path d="M${gaugeAt(k / 10, 70)} L${gaugeAt(k / 10, k % 5 ? 65 : 61)}" stroke-width="${k % 5 ? 1.5 : 2.5}"/>`).join("")}</g><text x="${GAUGE.cx - GAUGE.r}" y="130" text-anchor="middle" font-size="10" fill="{{ink}}" fill-opacity="0.6">0</text><text x="${GAUGE.cx + GAUGE.r}" y="130" text-anchor="middle" font-size="{{maxSize}}" fill="{{ink}}" fill-opacity="0.6">{{maxText}}</text><g id="gaugeNeedle" transform="${needle(0)}"><path d="M${GAUGE.cx} ${GAUGE.cy - 4} L${GAUGE.cx - GAUGE.r + 12} ${GAUGE.cy} L${GAUGE.cx} ${GAUGE.cy + 4}Z" fill="{{needleColor}}"/></g><circle cx="${GAUGE.cx}" cy="${GAUGE.cy}" r="8" fill="{{needleColor}}"/><circle cx="${GAUGE.cx}" cy="${GAUGE.cy}" r="3" fill="{{hub}}"/><text id="gaugeValue" x="${GAUGE.cx}" y="148" text-anchor="middle" font-size="{{readingSize}}" font-weight="800" fill="{{ink}}">0</text><text x="${GAUGE.cx}" y="164" text-anchor="middle" font-size="{{labelSize}}" textLength="{{labelLength}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}" fill-opacity="0.6">{{labelText}}</text></svg>`,
27627
+ animate: [
27628
+ ...[0, 1, 2].map((k) => ({ id: `gaugeBand${k}`, draw: true, delay: k * 140, ms: 320, ease: k === 0 ? "out" : "linear" })),
27629
+ { id: "gaugeNeedle", attrs: { transform: [needle(0), "{{over}}"] }, delay: 300, ms: 900, ease: "out" },
27630
+ { id: "gaugeNeedle", attrs: { transform: ["{{over}}", "{{angle}}"] }, delay: 1200, ms: 450, ease: "in-out" },
27631
+ { id: "gaugeValue", count: [0, n15("reading")], decimals: n15("dec"), format: "{n}{{unit}}", delay: 300, ms: 1300, ease: "out" }
27632
+ ]
27633
+ }
27634
+ };
27635
+ var RACE_ROWS = 6;
27636
+ var RACE_STATES = 5;
27637
+ var RACE = { left: 74, top: 12, pitch: 28, height: 22, width: 320, value: 10, intro: 900, move: 1e3, rest: 700 };
27638
+ var RACE_DEFAULT = [
27639
+ { name: "Lumen", values: [12, 28, 41, 66] },
27640
+ { name: "Pixel", values: [30, 36, 48, 54] },
27641
+ { name: "Vertex", values: [22, 40, 39, 71] },
27642
+ { name: "Shader", values: [8, 20, 52, 60] },
27643
+ { name: "Raster", values: [18, 24, 30, 35] },
27644
+ { name: "Vector", values: [26, 30, 44, 49] }
27645
+ ];
27646
+ var raceTime = (k) => RACE.intro + RACE.rest + (k - 1) * (RACE.move + RACE.rest);
27647
+ var barRace = {
27648
+ kind: "vector",
27649
+ id: "bar-race",
27650
+ name: "Bar race",
27651
+ description: "A bar chart race: bars stretch and overtake each other, swapping places as the period label ticks through the years.",
27652
+ category: "data",
27653
+ tags: ["chart", "race", "ranking", "bars", "timeline", "explainer"],
27654
+ hold: 1800,
27655
+ presets: [
27656
+ ...PALETTE_PRESETS,
27657
+ { name: "Browsers", values: { rows: "Chrome:20,45,62,65;Firefox:28,30,22,8;Safari:5,8,15,19;Edge:0,2,4,5;Opera:3,2,2,2", periods: "2009,2012,2016,2020" } }
27658
+ ],
27659
+ derived: (v) => {
27660
+ const rows = parseRace(v.rows, RACE_ROWS, RACE_STATES, RACE_DEFAULT);
27661
+ const states = Math.max(1, rows[0].values.length);
27662
+ const all = rows.flatMap((r) => r.values);
27663
+ const dec = decimalsOf(all);
27664
+ const valueWidth = Math.max(...all.map((x) => textWidthEm(formatCount(x, { decimals: dec })))) * RACE.value;
27665
+ const barWidth = Math.max(40, RACE.width - RACE.left - 10 - valueWidth);
27666
+ const periods = parseLabels(v.periods, RACE_STATES);
27667
+ const end = states > 1 ? raceTime(states - 1) + RACE.move : RACE.intro;
27668
+ const out = { dec };
27669
+ const ranks = Array.from({ length: RACE_STATES }, (_, k) => {
27670
+ const s = Math.min(k, states - 1);
27671
+ const order = rows.map((r, i) => [i, r.values[s]]).sort((a, b) => b[1] - a[1] || a[0] - b[0]);
27672
+ const rank = new Map(order.map(([i], place) => [i, place]));
27673
+ const top = Math.max(1e-9, order[0][1]);
27674
+ return { s, rank, top };
27675
+ });
27676
+ for (let i = 0; i < RACE_ROWS; i++) {
27677
+ const row = rows[i];
27678
+ const name = fitLabel(row?.name ?? "", RACE.left - 12, 11, 8);
27679
+ out[`show${i}`] = row ? "inline" : "none";
27680
+ out[`nm${i}`] = name.text;
27681
+ out[`ns${i}`] = name.size;
27682
+ out[`nl${i}`] = name.length;
27683
+ out[`in${i}`] = i * 70;
27684
+ for (let k = 0; k < RACE_STATES; k++) {
27685
+ const { s, rank, top } = ranks[k];
27686
+ const value = row ? row.values[s] : 0;
27687
+ const w = r114(value / top * barWidth);
27688
+ out[`y${i}_${k}`] = RACE.top + (rank.get(i) ?? i) * RACE.pitch;
27689
+ out[`w${i}_${k}`] = w;
27690
+ out[`x${i}_${k}`] = r114(RACE.left + w + 5);
27691
+ out[`v${i}_${k}`] = value;
27692
+ }
27693
+ }
27694
+ for (let k = 1; k < RACE_STATES; k++) {
27695
+ out[`t${k}`] = k < states ? raceTime(k) : end;
27696
+ out[`m${k}`] = k < states ? RACE.move : 0;
27697
+ out[`tick${k}`] = k < states ? raceTime(k) + RACE.move / 2 : end;
27698
+ out[`solo${k}`] = Math.min(k, states - 1);
27699
+ }
27700
+ for (let k = 0; k < RACE_STATES; k++) {
27701
+ const p = fit7(periods[k] ?? "", 120, 30);
27702
+ out[`p${k}`] = periods[k] ?? "";
27703
+ out[`ps${k}`] = p.size;
27704
+ }
27705
+ return out;
27706
+ },
27707
+ motion: {
27708
+ svg: `<svg viewBox="0 0 ${RACE.width} 210" font-family="${SANS12}"><!-- {{rows: text default='Lumen:12,28,41,66;Pixel:30,36,48,54;Vertex:22,40,39,71;Shader:8,20,52,60;Raster:18,24,30,35;Vector:26,30,44,49' label='Rows (Name:v1,v2,\u2026; \u2026)'}} {{periods: text default='2021,2022,2023,2024' label='Periods (comma-separated)'}} {{ink: color default=#1E293B label='Text'}} ${palette()} --><g id="barRacePeriod" font-weight="800" fill="{{ink}}" fill-opacity="0.22" text-anchor="end">${Array.from(
27709
+ { length: RACE_STATES },
27710
+ (_, k) => `<text x="${RACE.width - 8}" y="202" font-size="{{ps${k}}}"${k ? ` display="none"` : ""}>{{p${k}}}</text>`
27711
+ ).join("")}</g>${Array.from(
27712
+ { length: RACE_ROWS },
27713
+ (_, i) => `<g id="barRaceRow${i}" display="{{show${i}}}" opacity="0" transform="translate(0 {{y${i}_0}})"><text x="${RACE.left - 6}" y="15" text-anchor="end" font-size="{{ns${i}}}" textLength="{{nl${i}}}" lengthAdjust="spacingAndGlyphs" font-weight="600" fill="{{ink}}">{{nm${i}}}</text><rect id="barRaceBar${i}" x="${RACE.left}" y="0" width="0" height="${RACE.height}" rx="4" fill="{{c${i + 1}}}"/><text id="barRaceValue${i}" x="${RACE.left + 5}" y="15" font-size="${RACE.value}" font-weight="700" fill="{{ink}}">0</text></g>`
27714
+ ).join("")}</svg>`,
27715
+ animate: [
27716
+ { id: "barRacePeriod", solo: 0 },
27717
+ ...Array.from({ length: RACE_STATES - 1 }, (_, j) => ({ id: "barRacePeriod", solo: n15(`solo${j + 1}`), delay: n15(`tick${j + 1}`) })),
27718
+ ...Array.from({ length: RACE_ROWS }, (_, i) => [
27719
+ { id: `barRaceRow${i}`, attrs: { opacity: [0, 1] }, delay: n15(`in${i}`), ms: 300 },
27720
+ { id: `barRaceBar${i}`, attrs: { width: [0, n15(`w${i}_0`)] }, delay: n15(`in${i}`), ms: 800, ease: "out" },
27721
+ { id: `barRaceValue${i}`, attrs: { x: [RACE.left + 5, n15(`x${i}_0`)] }, count: [0, n15(`v${i}_0`)], decimals: n15("dec"), delay: n15(`in${i}`), ms: 800, ease: "out" },
27722
+ ...Array.from({ length: RACE_STATES - 1 }, (_2, j) => {
27723
+ const [a, b] = [j, j + 1];
27724
+ const at4 = { delay: n15(`t${b}`), ms: n15(`m${b}`), ease: "in-out" };
27725
+ return [
27726
+ { id: `barRaceRow${i}`, attrs: { transform: [`translate(0 {{y${i}_${a}}})`, `translate(0 {{y${i}_${b}}})`] }, ...at4 },
27727
+ { id: `barRaceBar${i}`, attrs: { width: [n15(`w${i}_${a}`), n15(`w${i}_${b}`)] }, ...at4 },
27728
+ { id: `barRaceValue${i}`, attrs: { x: [n15(`x${i}_${a}`), n15(`x${i}_${b}`)] }, count: [n15(`v${i}_${a}`), n15(`v${i}_${b}`)], decimals: n15("dec"), ...at4 }
27729
+ ];
27730
+ }).flat()
27731
+ ]).flat()
27732
+ ]
27733
+ }
27734
+ };
27735
+ var SPARK_DEFAULT = [4, 6, 5, 8, 7, 10, 9, 13];
27736
+ var SPARK_DRAW = 1200;
27737
+ function linePath(values, x0, x1, top, bottom) {
27738
+ const lo = Math.min(...values);
27739
+ const hi = Math.max(...values);
27740
+ const span = hi - lo || 1;
27741
+ const pts = values.map((v, j) => [r114(x0 + j * (x1 - x0) / (values.length - 1)), r114(hi === lo ? (top + bottom) / 2 : bottom - (v - lo) / span * (bottom - top))]);
27742
+ const line = pts.map(([x, y], j) => `${j ? "L" : "M"}${x} ${y}`).join(" ");
27743
+ return { line, area: `${line} L${pts.at(-1)[0]} ${bottom + 3} L${pts[0][0]} ${bottom + 3}Z`, end: pts.at(-1) };
27744
+ }
27745
+ function series(text, max, fallback) {
27746
+ const values = parseNumberList(text, max).filter((x) => Number.isFinite(x));
27747
+ return values.length >= 2 ? values : values.length === 1 ? [values[0], values[0]] : fallback;
27748
+ }
27749
+ var sparkline = {
27750
+ kind: "vector",
27751
+ id: "sparkline",
27752
+ name: "Sparkline",
27753
+ description: "A tiny trend line drawing in with a dot riding its tip, which then pulses beside the latest value; small enough to sit in a line of text.",
27754
+ category: "data",
27755
+ tags: ["chart", "sparkline", "trend", "inline", "kpi", "dashboard"],
27756
+ hold: 600,
27757
+ presets: [
27758
+ { name: "Falling", values: { values: "14,13,11,12,9,8,9,6", accent: [0.86, 0.15, 0.15] } },
27759
+ { name: "Violet", values: { accent: [0.55, 0.36, 0.96] } }
27760
+ ],
27761
+ derived: (v) => {
27762
+ const values = series(v.values, 32, SPARK_DEFAULT);
27763
+ const dec = decimalsOf(values);
27764
+ const e = Math.max(0.6, textWidthEm(formatCount(values.at(-1), { decimals: dec })));
27765
+ const size = r114(Math.min(13, 44 / e));
27766
+ const right = r114(120 - 3 - e * size - 6);
27767
+ const { line, area } = linePath(values, 4, right, 6, 26);
27768
+ return { line, area, size, valueX: r114(right + 6), first: values[0], last: values.at(-1), dec };
27769
+ },
27770
+ motion: {
27771
+ svg: `<svg viewBox="0 0 120 32" font-family="${SANS12}"><!-- {{values: text default='4,6,5,8,7,10,9,13' label='Values (comma-separated)'}} {{accent: color default=#10B981 label='Line'}} {{ink: color default=#1E293B label='Value'}} --><defs><linearGradient id="sparklineFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{{accent}}" stop-opacity="0.3"/><stop offset="1" stop-color="{{accent}}" stop-opacity="0"/></linearGradient></defs><path id="sparklineArea" d="{{area}}" fill="url(#sparklineFill)" opacity="0"/><path id="sparklineLine" d="{{line}}" fill="none" stroke="{{accent}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><g id="sparklineDot"><circle id="sparklinePulse" r="2.6" fill="none" stroke="{{accent}}" stroke-width="1.5" opacity="0"/><circle r="2.8" fill="{{accent}}"/></g><text id="sparklineValue" x="{{valueX}}" y="20.5" font-size="{{size}}" font-weight="700" fill="{{ink}}">0</text></svg>`,
27772
+ animate: [
27773
+ { id: "sparklineLine", draw: true, ms: SPARK_DRAW, ease: "in-out" },
27774
+ { id: "sparklineDot", along: "sparklineLine", ms: SPARK_DRAW, ease: "in-out" },
27775
+ { id: "sparklineArea", attrs: { opacity: [0, 1] }, delay: 700, ms: 600 },
27776
+ { id: "sparklineValue", count: [n15("first"), n15("last")], decimals: n15("dec"), ms: SPARK_DRAW, ease: "in-out" },
27777
+ ...[0, 1, 2].map((k) => ({ id: "sparklinePulse", attrs: { r: [2.6, 9], opacity: [0.8, 0] }, delay: SPARK_DRAW + 50 + k * 750, ms: 700, ease: "out" }))
27778
+ ]
27779
+ }
27780
+ };
27781
+ var BOARD = { rows: 6, top: 8, pitch: 34, height: 30, left: 8, width: 244, slide: 130 };
27782
+ var leaderboard = {
27783
+ kind: "vector",
27784
+ id: "leaderboard",
27785
+ name: "Leaderboard",
27786
+ description: "Ranked rows sliding into place from the bottom up, each with an initials avatar and a score counting up, then a highlight on the winner.",
27787
+ category: "data",
27788
+ tags: ["leaderboard", "ranking", "scores", "game", "dashboard", "competition"],
27789
+ hold: 1800,
27790
+ presets: [
27791
+ { name: "Dark", values: { card: [0.12, 0.14, 0.19], ink: [0.94, 0.95, 0.97] } },
27792
+ { name: "Sales", values: { names: "North team,South team,East team,West team", scores: "128400,119250,98100,87600", accent: [0.13, 0.77, 0.37] } }
27793
+ ],
27794
+ derived: (v) => {
27795
+ const names = parseLabels(v.names, BOARD.rows);
27796
+ const scores = parseNumberList(v.scores, BOARD.rows).map((x) => Number.isFinite(x) ? x : 0);
27797
+ const count = Math.min(BOARD.rows, Math.max(1, names.filter((s) => s !== "").length, scores.length));
27798
+ const rows = Array.from({ length: count }, (_, i) => ({ name: names[i] || `Player ${i + 1}`, score: scores[i] ?? 0 }));
27799
+ const order = rows.map((r, i) => [i, r.score]).sort((a, b) => b[1] - a[1] || a[0] - b[0]);
27800
+ const rank = new Map(order.map(([i], place) => [i, place]));
27801
+ const dec = decimalsOf(rows.map((r) => r.score));
27802
+ const crown = (count - 1) * BOARD.slide + 450;
27803
+ const out = { dec, crown, settle: crown + 200 };
27804
+ for (let i = 0; i < BOARD.rows; i++) {
27805
+ const row = rows[i];
27806
+ const place = rank.get(i) ?? i;
27807
+ const wait = Math.max(0, count - 1 - place) * BOARD.slide;
27808
+ const scoreWidth = textWidthEm(formatCount(row?.score ?? 0, { decimals: dec })) * 12;
27809
+ const name = fitLabel(row?.name ?? "", Math.max(20, BOARD.left + BOARD.width - 12 - scoreWidth - 8 - 72), 12, 9);
27810
+ const initials = (row?.name ?? "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => Array.from(w)[0]).join("").toUpperCase() || "?";
27811
+ out[`show${i}`] = row ? "inline" : "none";
27812
+ out[`y${i}`] = BOARD.top + place * BOARD.pitch;
27813
+ out[`at${i}`] = wait;
27814
+ out[`land${i}`] = wait + 300;
27815
+ out[`rank${i}`] = place + 1;
27816
+ out[`nm${i}`] = name.text;
27817
+ out[`ns${i}`] = name.size;
27818
+ out[`nl${i}`] = name.length;
27819
+ out[`ini${i}`] = initials;
27820
+ out[`is${i}`] = r114(Math.min(10, 15 / Math.max(0.6, textWidthEm(initials))));
27821
+ out[`sc${i}`] = row?.score ?? 0;
27822
+ }
27823
+ return out;
27824
+ },
27825
+ motion: {
27826
+ svg: `<svg viewBox="0 0 260 214" font-family="${SANS12}"><!-- {{names: text default='Ava Stone,Ben Ito,Cleo Park,Dev Shah,Eli Moss' label='Names (comma-separated)'}} {{scores: text default='870,980,640,820,510' label='Scores (comma-separated)'}} {{card: color default=#F1F5F9 label='Rows'}} {{ink: color default=#0F172A label='Text'}} {{accent: color default=#F59E0B label='Winner highlight'}} ${palette()} -->${Array.from(
27827
+ { length: BOARD.rows },
27828
+ (_, i) => `<g id="leaderboardRow${i}" display="{{show${i}}}" opacity="0" transform="translate(280 {{y${i}}})"><rect x="${BOARD.left}" y="0" width="${BOARD.width}" height="${BOARD.height}" rx="8" fill="{{card}}"/><text x="26" y="19.5" text-anchor="middle" font-size="12" font-weight="700" fill="{{ink}}" fill-opacity="0.55">{{rank${i}}}</text><circle cx="52" cy="15" r="11" fill="{{c${i + 1}}}"/><text x="52" y="18.5" text-anchor="middle" font-size="{{is${i}}}" font-weight="700" fill="#FFFFFF">{{ini${i}}}</text><text x="70" y="19.5" font-size="{{ns${i}}}" textLength="{{nl${i}}}" lengthAdjust="spacingAndGlyphs" font-weight="600" fill="{{ink}}">{{nm${i}}}</text><text id="leaderboardScore${i}" x="${BOARD.left + BOARD.width - 12}" y="19.5" text-anchor="end" font-size="12" font-weight="700" fill="{{ink}}">0</text></g>`
27829
+ ).join(
27830
+ ""
27831
+ )}<rect id="leaderboardRing" x="${BOARD.left}" y="${BOARD.top}" width="${BOARD.width}" height="${BOARD.height}" rx="8" fill="none" stroke="{{accent}}" stroke-width="2" opacity="0"/><rect id="leaderboardTop" x="${BOARD.left}" y="${BOARD.top}" width="${BOARD.width}" height="${BOARD.height}" rx="8" fill="{{accent}}" fill-opacity="0.12" stroke="{{accent}}" stroke-width="2" opacity="0"/><g id="leaderboardBadge" transform="translate(26 23) scale(0)"><circle r="10" fill="{{accent}}"/><path d="M-5 -3.5 L-2.5 -0.5 L0 -5 L2.5 -0.5 L5 -3.5 L4 3.5 H-4Z" fill="#FFFFFF"/></g></svg>`,
27832
+ animate: [
27833
+ ...Array.from({ length: BOARD.rows }, (_, i) => [
27834
+ { id: `leaderboardRow${i}`, attrs: { transform: [`translate(280 {{y${i}}})`, `translate(0 {{y${i}}})`] }, delay: n15(`at${i}`), ms: 450, ease: "out" },
27835
+ { id: `leaderboardRow${i}`, attrs: { opacity: [0, 1] }, delay: n15(`at${i}`), ms: 200 },
27836
+ { id: `leaderboardScore${i}`, count: [0, n15(`sc${i}`)], decimals: n15("dec"), delay: n15(`land${i}`), ms: 800, ease: "out" }
27837
+ ]).flat(),
27838
+ { id: "leaderboardTop", attrs: { opacity: [0, 1] }, delay: n15("crown"), ms: 250 },
27839
+ { id: "leaderboardRing", attrs: { opacity: [0, 0.9] }, delay: n15("crown"), ms: 0 },
27840
+ { id: "leaderboardRing", attrs: { x: [BOARD.left, BOARD.left - 5], y: [BOARD.top, BOARD.top - 5], width: [BOARD.width, BOARD.width + 10], height: [BOARD.height, BOARD.height + 10], opacity: [0.9, 0] }, delay: n15("crown"), ms: 650, ease: "out" },
27841
+ { id: "leaderboardBadge", attrs: { transform: ["translate(26 23) scale(0)", "translate(26 23) scale(1.25)"] }, delay: n15("crown"), ms: 200, ease: "out" },
27842
+ { id: "leaderboardBadge", attrs: { transform: ["translate(26 23) scale(1.25)", "translate(26 23) scale(1)"] }, delay: n15("settle"), ms: 200, ease: "in-out" }
27843
+ ]
27844
+ }
27845
+ };
27846
+ var AREA = { series: 4, points: 12, x0: 34, x1: 268, top: 40, base: 150, sweep: 2400, rise: 1100 };
27847
+ var AREA_DEFAULT = [
27848
+ [12, 18, 15, 24, 28, 34],
27849
+ [8, 10, 14, 12, 18, 22],
27850
+ [5, 6, 9, 8, 11, 14]
27851
+ ];
27852
+ var areaChart = {
27853
+ kind: "vector",
27854
+ id: "area-chart",
27855
+ name: "Area chart",
27856
+ description: "A stacked area chart whose layers rise together from the baseline, then a tooltip line glides across reading the total.",
27857
+ category: "data",
27858
+ tags: ["chart", "area", "stacked", "trend", "dashboard", "report"],
27859
+ hold: 1400,
27860
+ presets: [...PALETTE_PRESETS.map((p) => ({ ...p, values: Object.fromEntries(Object.entries(p.values).slice(0, AREA.series)) })), { name: "Two series", values: { series: "5,9,7,12,15,14,18,22;3,4,6,5,8,9,8,11", labels: "Paid,Organic", points: "W1,W2,W3,W4,W5,W6,W7,W8" } }],
27861
+ derived: (v) => {
27862
+ const rows = parseSeries(v.series, AREA.series, AREA.points, AREA_DEFAULT);
27863
+ const points = rows[0].length;
27864
+ const cum = rows.map((_, s) => Array.from({ length: points }, (_2, j) => rows.slice(0, s + 1).reduce((a, r) => a + r[j], 0)));
27865
+ const top = Math.max(1e-9, ...cum.at(-1));
27866
+ const x = (j) => r114(AREA.x0 + j * (AREA.x1 - AREA.x0) / (points - 1));
27867
+ const y = (value) => r114(AREA.base - value / top * (AREA.base - AREA.top));
27868
+ const edge = (values, back) => {
27869
+ const pts = values.map((val, j) => `${x(j)} ${y(val)}`);
27870
+ return (back ? pts.reverse() : pts).join(" L");
27871
+ };
27872
+ const zero = new Array(points).fill(0);
27873
+ const labels = parseLabels(v.labels, AREA.series);
27874
+ const xLabels = parseLabels(v.points, AREA.points);
27875
+ const dec = decimalsOf(rows.flat());
27876
+ const totals = cum.at(-1);
27877
+ const spacing = (AREA.x1 - AREA.x0) / (points - 1);
27878
+ const seg = AREA.sweep / (points - 1);
27879
+ const tip = AREA.rise + 200;
27880
+ const end = tip + AREA.sweep;
27881
+ const tipSize = r114(Math.min(9, 34 / Math.max(0.6, ...totals.map((t) => textWidthEm(formatCount(t, { decimals: dec }))))));
27882
+ const out = { dec, tip, tipSize, tx0: x(0), total0: +totals[0].toFixed(dec) };
27883
+ for (let s = 0; s < AREA.series; s++) {
27884
+ const shown = s < rows.length;
27885
+ const below = s > 0 && shown ? cum[s - 1] : zero;
27886
+ const above = shown ? cum[s] : zero;
27887
+ out[`show${s}`] = shown ? "inline" : "none";
27888
+ out[`flat${s}`] = `M${edge(zero, false)} L${edge(zero, true)}Z`;
27889
+ out[`full${s}`] = `M${edge(above, false)} L${edge(below, true)}Z`;
27890
+ const lab = fitLabel(labels[s] ?? "", 50, 9, 7.5);
27891
+ out[`lab${s}`] = lab.text;
27892
+ out[`labs${s}`] = lab.size;
27893
+ out[`labl${s}`] = lab.length;
27894
+ out[`dy${s}_0`] = y(above[0]);
27895
+ }
27896
+ for (let j = 0; j < AREA.points; j++) {
27897
+ const shown = j < points;
27898
+ const label = fitLabel(xLabels[j] ?? "", spacing * 0.9, 9, 7);
27899
+ out[`xs${j}`] = shown ? "inline" : "none";
27900
+ out[`xx${j}`] = shown ? x(j) : AREA.x1;
27901
+ out[`xl${j}`] = label.text;
27902
+ out[`xz${j}`] = label.size;
27903
+ }
27904
+ for (let j = 0; j < AREA.points - 1; j++) {
27905
+ const real = j < points - 1;
27906
+ const [a, b] = real ? [j, j + 1] : [points - 1, points - 1];
27907
+ out[`sd${j}`] = real ? Math.round(tip + j * seg) : end;
27908
+ out[`sm${j}`] = real ? Math.round(seg) : 0;
27909
+ out[`ta${j}`] = x(a);
27910
+ out[`tb${j}`] = x(b);
27911
+ out[`va${j}`] = +totals[a].toFixed(dec);
27912
+ out[`vb${j}`] = +totals[b].toFixed(dec);
27913
+ for (let s = 0; s < AREA.series; s++) {
27914
+ const c = s < rows.length ? cum[s] : zero;
27915
+ out[`ya${s}_${j}`] = y(c[a]);
27916
+ out[`yb${s}_${j}`] = y(c[b]);
27917
+ }
27918
+ }
27919
+ return out;
27920
+ },
27921
+ motion: {
27922
+ svg: `<svg viewBox="0 0 300 176" font-family="${SANS12}"><!-- {{series: text default='12,18,15,24,28,34;8,10,14,12,18,22;5,6,9,8,11,14' label='Series (values, rows split by ;)'}} {{labels: text default='Web,Mobile,API' label='Series labels (comma-separated)'}} {{points: text default='Jan,Feb,Mar,Apr,May,Jun' label='Point labels (comma-separated)'}} {{ink: color default=#1E293B label='Text'}} {{grid: color default=#E2E8F0 label='Grid'}} ${palette(AREA.series)} --><g stroke="{{grid}}" stroke-width="1">${[0, 1, 2, 3].map((k) => `<line x1="${AREA.x0}" x2="${AREA.x1}" y1="${AREA.top + k * (AREA.base - AREA.top) / 3}" y2="${AREA.top + k * (AREA.base - AREA.top) / 3}"/>`).join("")}</g>${Array.from(
27923
+ { length: AREA.series },
27924
+ (_, s) => `<path id="areaChartArea${s}" display="{{show${s}}}" d="{{flat${s}}}" fill="{{c${s + 1}}}" fill-opacity="0.78" stroke="{{c${s + 1}}}" stroke-width="1.5" stroke-linejoin="round"/>`
27925
+ ).join("")}${Array.from(
27926
+ { length: AREA.series },
27927
+ (_, s) => `<g display="{{show${s}}}" transform="translate(${AREA.x0 + s * 64} 0)"><rect x="0" y="5" width="8" height="8" rx="2" fill="{{c${s + 1}}}"/><text x="12" y="12.5" font-size="{{labs${s}}}" textLength="{{labl${s}}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}">{{lab${s}}}</text></g>`
27928
+ ).join("")}<g font-size="9" fill="{{ink}}" fill-opacity="0.6" text-anchor="middle">${Array.from(
27929
+ { length: AREA.points },
27930
+ (_, j) => `<text display="{{xs${j}}}" x="{{xx${j}}}" y="166" font-size="{{xz${j}}}">{{xl${j}}}</text>`
27931
+ ).join("")}</g><g id="areaChartTip" opacity="0" transform="translate({{tx0}} 0)"><line x1="0" x2="0" y1="34" y2="${AREA.base}" stroke="{{ink}}" stroke-opacity="0.5" stroke-width="1.2" stroke-dasharray="3 2"/><rect x="-20" y="18" width="40" height="16" rx="4" fill="{{ink}}"/><text id="areaChartTipValue" x="0" y="29" text-anchor="middle" font-size="{{tipSize}}" font-weight="700" fill="#FFFFFF">0</text>${Array.from(
27932
+ { length: AREA.series },
27933
+ (_, s) => `<circle id="areaChartDot${s}" display="{{show${s}}}" cx="0" cy="{{dy${s}_0}}" r="3" fill="#FFFFFF" stroke="{{c${s + 1}}}" stroke-width="2"/>`
27934
+ ).join("")}</g></svg>`,
27935
+ animate: [
27936
+ ...Array.from({ length: AREA.series }, (_, s) => ({ id: `areaChartArea${s}`, attrs: { d: [`{{flat${s}}}`, `{{full${s}}}`] }, ms: AREA.rise, ease: "out" })),
27937
+ { id: "areaChartTip", attrs: { opacity: [0, 1] }, delay: n15("tip"), ms: 200 },
27938
+ { id: "areaChartTipValue", count: [n15("total0"), n15("total0")], decimals: n15("dec"), delay: n15("tip"), ms: 0 },
27939
+ ...Array.from({ length: AREA.points - 1 }, (_, j) => {
27940
+ const at4 = { delay: n15(`sd${j}`), ms: n15(`sm${j}`), ease: "linear" };
27941
+ return [
27942
+ { id: "areaChartTip", attrs: { transform: [`translate({{ta${j}}} 0)`, `translate({{tb${j}}} 0)`] }, ...at4 },
27943
+ { id: "areaChartTipValue", count: [n15(`va${j}`), n15(`vb${j}`)], decimals: n15("dec"), ...at4 },
27944
+ ...Array.from({ length: AREA.series }, (_2, s) => ({ id: `areaChartDot${s}`, attrs: { cy: [n15(`ya${s}_${j}`), n15(`yb${s}_${j}`)] }, ...at4 }))
27945
+ ];
27946
+ }).flat()
27947
+ ]
27948
+ }
27949
+ };
27950
+ var KPI_DEFAULT = [3, 4, 3.5, 5, 4.8, 6, 7.2];
27951
+ var kpiTile = {
27952
+ kind: "vector",
27953
+ id: "kpi-tile",
27954
+ name: "KPI tile",
27955
+ description: "A dashboard tile: the headline figure counts up, a delta arrow shows the change in green or red by its sign, and a mini sparkline draws in.",
27956
+ category: "data",
27957
+ tags: ["kpi", "dashboard", "metric", "card", "sparkline", "report"],
27958
+ hold: 1600,
27959
+ presets: [
27960
+ { name: "Dark", values: { card: [0.09, 0.11, 0.15], ink: [0.95, 0.96, 0.97] } },
27961
+ { name: "Churn", values: { label: "Churn rate", value: 2.4, prefix: "", suffix: "%", delta: -0.6, trend: "3.4,3.3,3.1,3.0,2.8,2.6,2.4", caption: "vs last quarter" } }
27962
+ ],
27963
+ derived: (v) => {
27964
+ const value = Number(v.value) || 0;
27965
+ const dec = Math.min(3, Math.max(0, Math.round(Number(v.decimals) || 0)));
27966
+ const pre = formatSafe(v.prefix);
27967
+ const suf = formatSafe(v.suffix);
27968
+ const big = fit7(`${pre}${formatCount(value, { decimals: dec })}${suf}`, 204, 34);
27969
+ const delta = Number(v.delta) || 0;
27970
+ const up = delta >= 0;
27971
+ const trend = series(v.trend, 24, KPI_DEFAULT);
27972
+ const { line, area, end } = linePath(trend, 148, 222, 90, 120);
27973
+ const label = fitLabel(v.label, 204, 12, 10);
27974
+ const caption = fitLabel(v.caption, 120, 10, 8);
27975
+ return {
27976
+ pre,
27977
+ suf,
27978
+ dec,
27979
+ value: +value.toFixed(dec),
27980
+ size: big.size,
27981
+ delta: +Math.abs(delta).toFixed(1),
27982
+ deltaFormat: up ? "+{n}%" : "\u2212{n}%",
27983
+ deltaColor: paint(up ? v.up : v.down),
27984
+ arrow: up ? "M24 92 L30 100 H18Z" : "M24 101 L30 93 H18Z",
27985
+ line,
27986
+ area,
27987
+ endX: end[0],
27988
+ endY: end[1],
27989
+ labelText: label.text,
27990
+ captionText: caption.text,
27991
+ labelSize: label.size,
27992
+ labelLength: label.length,
27993
+ captionSize: caption.size,
27994
+ captionLength: caption.length
27995
+ };
27996
+ },
27997
+ motion: {
27998
+ svg: `<svg viewBox="0 0 240 140" font-family="${SANS12}"><!-- {{label: text default='Monthly revenue' label='Label'}} {{value: float min=0 max=1000000 default=48.2 step=0.1 label='Value'}} {{decimals: int min=0 max=3 default=1 label='Decimals'}} {{prefix: text default='$' label='Prefix'}} {{suffix: text default='k' label='Suffix'}} {{delta: float min=-100 max=100 default=12.4 step=0.1 label='Change (%)'}} {{caption: text default='vs last month' label='Caption'}} {{trend: text default='3,4,3.5,5,4.8,6,7.2' label='Trend (comma-separated)'}} {{card: color default=#FFFFFF label='Card'}} {{ink: color default=#0F172A label='Text'}} {{accent: color default=#6366F1 label='Sparkline'}} {{up: color default=#16A34A label='Rise'}} {{down: color default=#DC2626 label='Fall'}} --><defs><linearGradient id="kpiTileFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{{accent}}" stop-opacity="0.28"/><stop offset="1" stop-color="{{accent}}" stop-opacity="0"/></linearGradient></defs><rect x="2" y="2" width="236" height="136" rx="14" fill="{{card}}" stroke="{{ink}}" stroke-opacity="0.1"/><text x="18" y="30" font-size="{{labelSize}}" textLength="{{labelLength}}" lengthAdjust="spacingAndGlyphs" font-weight="600" fill="{{ink}}" fill-opacity="0.6">{{labelText}}</text><text id="kpiTileValue" x="18" y="72" font-size="{{size}}" font-weight="800" fill="{{ink}}">0</text><g id="kpiTileDelta" opacity="0"><path d="{{arrow}}" fill="{{deltaColor}}"/><text id="kpiTileDeltaValue" x="34" y="101" font-size="12" font-weight="700" fill="{{deltaColor}}">0%</text></g><text id="kpiTileCaption" x="18" y="124" font-size="{{captionSize}}" textLength="{{captionLength}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}" fill-opacity="0.55" opacity="0">{{captionText}}</text><path id="kpiTileArea" d="{{area}}" fill="url(#kpiTileFill)" opacity="0"/><path id="kpiTileLine" d="{{line}}" fill="none" stroke="{{accent}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle id="kpiTileEnd" cx="{{endX}}" cy="{{endY}}" r="0" fill="{{accent}}"/></svg>`,
27999
+ animate: [
28000
+ { id: "kpiTileValue", count: [0, n15("value")], decimals: n15("dec"), format: "{{pre}}{n}{{suf}}", ms: 1400, ease: "out" },
28001
+ { id: "kpiTileDelta", attrs: { opacity: [0, 1], transform: ["translate(0 6)", "translate(0 0)"] }, delay: 500, ms: 400, ease: "out" },
28002
+ { id: "kpiTileDeltaValue", count: [0, n15("delta")], decimals: 1, format: "{{deltaFormat}}", delay: 500, ms: 900, ease: "out" },
28003
+ { id: "kpiTileCaption", attrs: { opacity: [0, 1] }, delay: 800, ms: 400 },
28004
+ { id: "kpiTileLine", draw: true, delay: 300, ms: 1100, ease: "in-out" },
28005
+ { id: "kpiTileArea", attrs: { opacity: [0, 1] }, delay: 900, ms: 500 },
28006
+ { id: "kpiTileEnd", attrs: { r: [0, 4.5] }, delay: 1400, ms: 180, ease: "out" },
28007
+ { id: "kpiTileEnd", attrs: { r: [4.5, 3] }, delay: 1580, ms: 200, ease: "in-out" }
28008
+ ]
28009
+ }
28010
+ };
28011
+ var dataEffects = [donutChart, pieChart, gauge, barRace, sparkline, leaderboard, areaChart, kpiTile];
28012
+
28013
+ // src/catalog/icons.ts
28014
+ var r115 = (v) => Math.round(v * 10) / 10;
28015
+ var r213 = (v) => Math.round(v * 100) / 100;
28016
+ var n16 = (name) => `{{${name}}}`;
28017
+ var SANS13 = `system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif`;
28018
+ var about6 = (cx, cy, s) => `translate(${cx} ${cy}) scale(${r213(s)}) translate(${-cx} ${-cy})`;
28019
+ function cycle(id, from, to, phase, period, ease = "linear") {
28020
+ const at4 = (u) => Object.fromEntries(Object.keys(from).map((k) => [k, lerpAttr(from[k], to[k], u)]));
28021
+ const mid = at4(phase);
28022
+ const pairs = (a, b) => Object.fromEntries(Object.keys(from).map((k) => [k, [a[k], b[k]]]));
28023
+ const first = Math.round((1 - phase) * period);
28024
+ const out = [{ id, attrs: pairs(mid, to), ms: first, ease }];
28025
+ if (phase > 0) out.push({ id, attrs: pairs(from, mid), delay: first, ms: period - first, ease });
28026
+ return out;
28027
+ }
28028
+ var swing = (id, attr, a, b, period, delay = 0) => [
28029
+ { id, attrs: { [attr]: [a, b] }, delay, ms: period / 2, ease: "in-out" },
28030
+ { id, attrs: { [attr]: [b, a] }, delay: delay + period / 2, ms: period / 2, ease: "in-out" }
28031
+ ];
28032
+ var picks = (name, count) => (v) => {
28033
+ const pick = Math.min(count - 1, Math.max(0, Math.round(Number(v[name]) || 0)));
28034
+ return Object.fromEntries(Array.from({ length: count }, (_, k) => [`show${k}`, k === pick ? "inline" : "none"]));
28035
+ };
28036
+ var WEATHER_MS = 2400;
28037
+ var CLOUD = "M-22 14 H22 A12 12 0 0 0 20 -9 A17 17 0 0 0 -12 -10 A12 12 0 0 0 -22 14Z";
28038
+ var DROP = "M0 -5 C2 -1 3 1 3 3 A3 3 0 0 1 -3 3 C-3 1 -2 -1 0 -5Z";
28039
+ var FLAKE = "M0 -4 V4 M-3.5 -2 L3.5 2 M-3.5 2 L3.5 -2";
28040
+ var sunRays = (cx, cy, from, to) => Array.from({ length: 8 }, (_, k) => {
28041
+ const a = k * Math.PI / 4;
28042
+ return `M${r115(cx + from * Math.cos(a))} ${r115(cy + from * Math.sin(a))} L${r115(cx + to * Math.cos(a))} ${r115(cy + to * Math.sin(a))}`;
28043
+ }).join(" ");
28044
+ var cloud2 = (id, x, y, s, fill, extra = "") => `<g id="${id}" transform="translate(${x} ${y}) scale(${s})"><path d="${CLOUD}" fill="${fill}"${extra}/></g>`;
28045
+ var falling = (prefix, shape, xs, top, bottom, spin = 0) => ({
28046
+ markup: xs.map((x, i) => `<g id="${prefix}${i}" transform="translate(${x} ${top}) rotate(0)" opacity="1">${shape(i)}</g>`).join(""),
28047
+ animate: xs.flatMap(
28048
+ (x, i) => cycle(`${prefix}${i}`, { transform: `translate(${x} ${top}) rotate(0)`, opacity: 1 }, { transform: `translate(${x + spin / 30} ${bottom}) rotate(${spin})`, opacity: 0 }, (i * 0.37 + 0.15) % 1, WEATHER_MS)
28049
+ )
28050
+ });
28051
+ var rain = falling("weatherIconsDrop", () => `<path d="${DROP}" fill="{{accent}}"/>`, [36, 50, 64, 43, 57], 50, 86);
28052
+ var storm = falling("weatherIconsStormDrop", () => `<path d="${DROP}" fill="{{accent}}"/>`, [34, 66], 50, 86);
28053
+ var snow = falling("weatherIconsFlake", () => `<path d="${FLAKE}" stroke="{{accent}}" stroke-width="1.8" stroke-linecap="round"/>`, [36, 50, 64, 43, 58], 50, 88, 120);
28054
+ var WIND = ["M14 38 H58 C68 38 70 26 62 24 C56 23 54 29 58 32", "M10 52 H76 C86 52 88 64 80 66 C74 67 72 61 76 58", "M20 66 H50"];
28055
+ var weatherIcons = {
28056
+ kind: "vector",
28057
+ id: "weather-icons",
28058
+ name: "Weather icons",
28059
+ description: "An animated weather icon: sun with turning rays, partly cloudy, drifting clouds, falling rain, a flashing thunderbolt, snow or gusting wind.",
28060
+ category: "icon",
28061
+ tags: ["weather", "forecast", "sun", "rain", "snow", "loop"],
28062
+ hold: 0,
28063
+ presets: [
28064
+ { name: "Rain", values: { kind: 3 } },
28065
+ { name: "Snow", values: { kind: 5, cloud: [0.8, 0.85, 0.92], accent: [0.55, 0.75, 0.95] } },
28066
+ { name: "Thunder", values: { kind: 4, cloud: [0.39, 0.45, 0.55] } }
28067
+ ],
28068
+ derived: picks("kind", 7),
28069
+ motion: {
28070
+ svg: `<svg viewBox="0 0 100 100"><!-- {{kind: int min=0 max=6 default=0 label='Weather (sun, partly cloudy, cloudy, rain, thunder, snow, wind)'}} {{sun: color default=#FBBF24 label='Sun'}} {{cloud: color default=#94A3B8 label='Cloud'}} {{accent: color default=#38BDF8 label='Rain, snow and wind'}} {{bolt: color default=#FACC15 label='Lightning'}} --><g display="{{show0}}"><g id="weatherIconsRays" transform="rotate(0 50 50)"><path d="${sunRays(50, 50, 26, 36)}" stroke="{{sun}}" stroke-width="5" stroke-linecap="round"/></g><circle id="weatherIconsSun" cx="50" cy="50" r="17" fill="{{sun}}"/></g><g display="{{show1}}"><g id="weatherIconsPartRays" transform="rotate(0 38 38)"><path d="${sunRays(38, 38, 19, 27)}" stroke="{{sun}}" stroke-width="4" stroke-linecap="round"/></g><circle cx="38" cy="38" r="13" fill="{{sun}}"/>${cloud2("weatherIconsPartCloud", 56, 60, 1, "{{cloud}}")}</g><g display="{{show2}}">${cloud2("weatherIconsBackCloud", 62, 40, 0.75, "{{cloud}}", ` opacity="0.55"`)}${cloud2("weatherIconsFrontCloud", 46, 56, 1.05, "{{cloud}}")}</g><g display="{{show3}}">${rain.markup}${cloud2("weatherIconsRainCloud", 50, 38, 1.1, "{{cloud}}")}</g><g display="{{show4}}">${storm.markup}<g transform="translate(50 36) scale(1.1)"><path d="${CLOUD}" fill="{{cloud}}"/><path d="${CLOUD}" fill="#000000" opacity="0.22"/></g><path id="weatherIconsBolt" d="M54 50 L42 70 H51 L45 90 L63 64 H54 L60 50Z" fill="{{bolt}}" opacity="0"/></g><g display="{{show5}}">${snow.markup}${cloud2("weatherIconsSnowCloud", 50, 38, 1.1, "{{cloud}}")}</g><g display="{{show6}}" fill="none" stroke="{{accent}}" stroke-width="5" stroke-linecap="round">${WIND.map((d, k) => `<path id="weatherIconsWind${k}" d="${d}"/>`).join("")}</g></svg>`,
28071
+ animate: [
28072
+ ...cycle("weatherIconsRays", { transform: "rotate(0 50 50)" }, { transform: "rotate(45 50 50)" }, 0, WEATHER_MS),
28073
+ ...swing("weatherIconsSun", "r", 17, 18.5, WEATHER_MS),
28074
+ ...cycle("weatherIconsPartRays", { transform: "rotate(0 38 38)" }, { transform: "rotate(45 38 38)" }, 0, WEATHER_MS),
28075
+ ...swing("weatherIconsPartCloud", "transform", "translate(54 60) scale(1)", "translate(60 60) scale(1)", WEATHER_MS),
28076
+ ...swing("weatherIconsBackCloud", "transform", "translate(66 40) scale(0.75)", "translate(58 40) scale(0.75)", WEATHER_MS),
28077
+ ...swing("weatherIconsFrontCloud", "transform", "translate(43 56) scale(1.05)", "translate(50 56) scale(1.05)", WEATHER_MS),
28078
+ ...swing("weatherIconsRainCloud", "transform", "translate(49 38) scale(1.1)", "translate(51 38) scale(1.1)", WEATHER_MS),
28079
+ ...swing("weatherIconsSnowCloud", "transform", "translate(49 38) scale(1.1)", "translate(51 38) scale(1.1)", WEATHER_MS),
28080
+ ...rain.animate,
28081
+ ...storm.animate,
28082
+ ...snow.animate,
28083
+ // Two quick flashes, then dark until the loop comes round.
28084
+ { id: "weatherIconsBolt", attrs: { opacity: [0, 1] }, delay: 700, ms: 60 },
28085
+ { id: "weatherIconsBolt", attrs: { opacity: [1, 0.2] }, delay: 760, ms: 90 },
28086
+ { id: "weatherIconsBolt", attrs: { opacity: [0.2, 1] }, delay: 850, ms: 60 },
28087
+ { id: "weatherIconsBolt", attrs: { opacity: [1, 0] }, delay: 1100, ms: 400 },
28088
+ // Gusts draw on in turn and fade; undrawn again by the time the loop restarts.
28089
+ ...WIND.flatMap((_, k) => [
28090
+ { id: `weatherIconsWind${k}`, draw: true, delay: k * 220, ms: 900, ease: "in-out" },
28091
+ { id: `weatherIconsWind${k}`, attrs: { opacity: [1, 0] }, delay: 1300 + k * 220, ms: 450 }
28092
+ ])
28093
+ ]
28094
+ }
28095
+ };
28096
+ var BELL_PIVOT = "50 16";
28097
+ var bellTurn = (deg) => `rotate(${deg} ${BELL_PIVOT})`;
28098
+ var BELL_SWING = [0, 18, -15, 11, -7, 3, 0];
28099
+ var CLAPPER_SWING = [0, -12, 14, -9, 6, -2, 0];
28100
+ var BADGE = [72, 26];
28101
+ var notificationBell = {
28102
+ kind: "vector",
28103
+ id: "notification-bell",
28104
+ name: "Notification bell",
28105
+ description: "A bell ringing with a damped swing and its clapper lagging behind, as a badge pops on with the unread count.",
28106
+ category: "icon",
28107
+ tags: ["notification", "bell", "alert", "badge", "ui", "loop"],
28108
+ hold: 300,
28109
+ presets: [
28110
+ { name: "Dark", values: { bell: [0.9, 0.91, 0.94], ring: [0.06, 0.07, 0.1] } },
28111
+ { name: "Many", values: { count: 128 } }
28112
+ ],
28113
+ derived: (v) => {
28114
+ const count = Math.max(0, Math.round(Number(v.count) || 0));
28115
+ const many = count > 99;
28116
+ const digits = many ? 3 : String(count).length;
28117
+ return {
28118
+ from: many ? 99 : Math.max(0, count - 1),
28119
+ to: many ? 99 : count,
28120
+ format: many ? "{n}+" : "{n}",
28121
+ size: digits >= 3 ? 9 : digits === 2 ? 11 : 13,
28122
+ badgeR: digits >= 3 ? 15 : 13
28123
+ };
28124
+ },
28125
+ motion: {
28126
+ svg: `<svg viewBox="0 0 100 100" font-family="${SANS13}"><!-- {{count: int min=0 max=999 default=3 label='Count'}} {{bell: color default=#334155 label='Bell'}} {{badge: color default=#EF4444 label='Badge'}} {{ring: color default=#FFFFFF label='Badge outline'}} --><g fill="none" stroke="{{bell}}" stroke-width="3.5" stroke-linecap="round"><path id="notificationBellWaveL" d="M20 30 C14 38 14 50 20 58" opacity="0"/><path id="notificationBellWaveR" d="M80 30 C86 38 86 50 80 58" opacity="0"/></g><g id="notificationBellClapper" transform="${bellTurn(0)}"><circle cx="50" cy="80" r="7" fill="{{bell}}"/></g><g id="notificationBellBody" transform="${bellTurn(0)}"><circle cx="50" cy="15" r="5" fill="{{bell}}"/><path d="M50 18 C36 18 30 30 30 44 V58 L23 68 V72 H77 V68 L70 58 V44 C70 30 64 18 50 18Z" fill="{{bell}}"/></g><g id="notificationBellBadge" transform="${about6(BADGE[0], BADGE[1], 0)}"><circle cx="${BADGE[0]}" cy="${BADGE[1]}" r="{{badgeR}}" fill="{{badge}}" stroke="{{ring}}" stroke-width="3"/><text id="notificationBellCount" x="${BADGE[0]}" y="${BADGE[1] + 4.5}" text-anchor="middle" font-size="{{size}}" font-weight="800" fill="#FFFFFF">0</text></g></svg>`,
28127
+ animate: [
28128
+ ...BELL_SWING.slice(1).map((deg, k) => ({ id: "notificationBellBody", attrs: { transform: [bellTurn(BELL_SWING[k]), bellTurn(deg)] }, delay: k * 120, ms: 120, ease: "in-out" })),
28129
+ ...CLAPPER_SWING.slice(1).map((deg, k) => ({ id: "notificationBellClapper", attrs: { transform: [bellTurn(CLAPPER_SWING[k]), bellTurn(deg)] }, delay: 50 + k * 120, ms: 120, ease: "in-out" })),
28130
+ ...["L", "R"].flatMap((side, k) => [
28131
+ { id: `notificationBellWave${side}`, attrs: { opacity: [0, 1] }, delay: 80 + k * 120, ms: 120 },
28132
+ { id: `notificationBellWave${side}`, attrs: { opacity: [1, 0] }, delay: 420 + k * 120, ms: 260 }
28133
+ ]),
28134
+ { id: "notificationBellBadge", attrs: { transform: [about6(BADGE[0], BADGE[1], 0), about6(BADGE[0], BADGE[1], 1.25)] }, delay: 300, ms: 180, ease: "out" },
28135
+ { id: "notificationBellBadge", attrs: { transform: [about6(BADGE[0], BADGE[1], 1.25), about6(BADGE[0], BADGE[1], 1)] }, delay: 480, ms: 200, ease: "in-out" },
28136
+ { id: "notificationBellCount", count: [n16("from"), n16("to")], format: "{{format}}", delay: 300, ms: 350, ease: "out" },
28137
+ // The badge goes before the loop starts over, so it can pop on again.
28138
+ { id: "notificationBellBadge", attrs: { transform: [about6(BADGE[0], BADGE[1], 1), about6(BADGE[0], BADGE[1], 0)] }, delay: 2400, ms: 220, ease: "in-out" }
28139
+ ]
28140
+ }
28141
+ };
28142
+ var ARROW_MS = 1200;
28143
+ var refreshArc = (() => {
28144
+ const r = 26;
28145
+ const end = -40 * Math.PI / 180;
28146
+ const [ex, ey] = [50 + r * Math.cos(end), 50 + r * Math.sin(end)];
28147
+ const [tx, ty] = [-Math.sin(end), Math.cos(end)];
28148
+ const [nx, ny] = [Math.cos(end), Math.sin(end)];
28149
+ const tip = `${r115(ex + tx * 7)} ${r115(ey + ty * 7)}`;
28150
+ const head = `M${r115(ex + nx * 7)} ${r115(ey + ny * 7)} L${tip} L${r115(ex - nx * 7)} ${r115(ey - ny * 7)}Z`;
28151
+ return { arc: `M24 50 A${r} ${r} 0 0 1 ${r115(ex)} ${r115(ey)}`, head };
28152
+ })();
28153
+ var animatedArrows = {
28154
+ kind: "vector",
28155
+ id: "animated-arrows",
28156
+ name: "Animated arrows",
28157
+ description: "An arrow icon set: an arrow nudging right, chevrons cascading down, refresh arrows turning, or an external-link arrow leaving its box.",
28158
+ category: "icon",
28159
+ tags: ["arrow", "chevron", "refresh", "link", "ui", "loop"],
28160
+ // Every arrow's motion is over by 1000 ms: each rests the same before it repeats.
28161
+ hold: ARROW_MS - 1e3,
28162
+ presets: [
28163
+ { name: "Scroll hint", values: { kind: 1, color: [0.39, 0.4, 0.95] } },
28164
+ { name: "Refresh", values: { kind: 2 } }
28165
+ ],
28166
+ derived: picks("kind", 4),
28167
+ motion: {
28168
+ svg: `<svg viewBox="0 0 100 100"><!-- {{kind: int min=0 max=3 default=0 label='Arrow (right, chevrons, refresh, external)'}} {{color: color default=#0F172A label='Colour'}} {{width: float min=2 max=12 default=7 step=0.5 label='Stroke width'}} --><g fill="none" stroke="{{color}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round"><g display="{{show0}}"><g id="animatedArrowsRight" transform="translate(0 0)"><path d="M20 50 H76 M56 30 L76 50 L56 70"/></g></g><g display="{{show1}}">${[0, 1, 2].map((k) => `<path id="animatedArrowsChevron${k}" d="M32 ${24 + k * 18} L50 ${38 + k * 18} L68 ${24 + k * 18}" opacity="0.2"/>`).join("")}</g><g display="{{show2}}"><g id="animatedArrowsRefresh" transform="rotate(0 50 50)">${[0, 180].map((deg) => `<g transform="rotate(${deg} 50 50)"><path d="${refreshArc.arc}"/><path d="${refreshArc.head}" fill="{{color}}" stroke-width="2"/></g>`).join("")}</g></g><g display="{{show3}}"><path d="M54 22 H32 C26 22 22 26 22 32 V68 C22 74 26 78 32 78 H68 C74 78 78 74 78 68 V46"/><g id="animatedArrowsExternal" transform="translate(0 0)"><path d="M46 54 L76 24 M56 24 H76 V44"/></g></g></g></svg>`,
28169
+ animate: [
28170
+ { id: "animatedArrowsRight", attrs: { transform: ["translate(0 0)", "translate(9 0)"] }, ms: 380, ease: "out" },
28171
+ { id: "animatedArrowsRight", attrs: { transform: ["translate(9 0)", "translate(0 0)"] }, delay: 380, ms: 520, ease: "in-out" },
28172
+ ...[0, 1, 2].flatMap((k) => [
28173
+ { id: `animatedArrowsChevron${k}`, attrs: { opacity: [0.2, 1], transform: ["translate(0 0)", "translate(0 3)"] }, delay: k * 180, ms: 260, ease: "out" },
28174
+ { id: `animatedArrowsChevron${k}`, attrs: { opacity: [1, 0.2], transform: ["translate(0 3)", "translate(0 0)"] }, delay: k * 180 + 300, ms: 420, ease: "in-out" }
28175
+ ]),
28176
+ // Two arrows round the circle: half a turn brings it back to how it started.
28177
+ { id: "animatedArrowsRefresh", attrs: { transform: ["rotate(0 50 50)", "rotate(180 50 50)"] }, ms: 1e3, ease: "in-out" },
28178
+ { id: "animatedArrowsExternal", attrs: { transform: ["translate(0 0)", "translate(10 -10)"], opacity: [1, 0] }, delay: 100, ms: 360, ease: "in-out" },
28179
+ { id: "animatedArrowsExternal", attrs: { transform: ["translate(-10 10)", "translate(0 0)"], opacity: [0, 1] }, delay: 520, ms: 420, ease: "out" }
28180
+ ]
28181
+ }
28182
+ };
28183
+ var EMOJI_Y = 40;
28184
+ var emojiX = (i) => 30 + i * 44;
28185
+ var emojiPose = (i, dy, s) => `translate(${emojiX(i)} ${EMOJI_Y}) translate(0 ${dy}) scale(${s})`;
28186
+ var FACES = [
28187
+ // Like: a thumb up on a blue disc.
28188
+ `<circle r="17" fill="{{like}}"/><path d="M-8 -1 H-4 V9 H-8Z M-2 -1 L2 -9 C3.5 -10.5 6.5 -9 5.8 -6.3 L4.8 -2.5 H8.5 C10.4 -2.5 11.4 -0.6 10.8 1.2 L8.8 7.5 C8.3 9 7.2 9.6 5.8 9.6 H-2Z" fill="#FFFFFF"/>`,
28189
+ // Love: a heart on a red disc.
28190
+ `<circle r="17" fill="{{love}}"/><path d="M0 9 C0 9 -9.5 3 -9.5 -2.8 C-9.5 -6 -7.3 -8.2 -4.6 -8.2 C-2.6 -8.2 -0.9 -7.1 0 -5.5 C0.9 -7.1 2.6 -8.2 4.6 -8.2 C7.3 -8.2 9.5 -6 9.5 -2.8 C9.5 3 0 9 0 9Z" fill="#FFFFFF"/>`,
28191
+ // Haha: shut eyes and a wide open laugh.
28192
+ `<circle r="17" fill="{{face}}"/><path d="M-10 -6 L-5 -3.5 L-10 -1 M10 -6 L5 -3.5 L10 -1" fill="none" stroke="{{features}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M-9 2 H9 C9 8.5 5 12 0 12 C-5 12 -9 8.5 -9 2Z" fill="{{features}}"/>`,
28193
+ // Wow: raised brows, round eyes and mouth.
28194
+ `<circle r="17" fill="{{face}}"/><path d="M-9 -9 Q-6 -11.5 -3 -9.5 M9 -9 Q6 -11.5 3 -9.5" fill="none" stroke="{{features}}" stroke-width="1.8" stroke-linecap="round"/><ellipse cx="-5.5" cy="-3.5" rx="2.2" ry="3" fill="{{features}}"/><ellipse cx="5.5" cy="-3.5" rx="2.2" ry="3" fill="{{features}}"/><ellipse cx="0" cy="7.5" rx="4" ry="5" fill="{{features}}"/>`,
28195
+ // Sad: a frown and a tear.
28196
+ `<circle r="17" fill="{{face}}"/><circle cx="-5.5" cy="-3" r="2.2" fill="{{features}}"/><circle cx="5.5" cy="-3" r="2.2" fill="{{features}}"/><path d="M-6 9.5 Q0 3.5 6 9.5" fill="none" stroke="{{features}}" stroke-width="2" stroke-linecap="round"/><path d="M-6 1 C-4.5 3.5 -4 4.8 -4 5.8 A2 2 0 0 1 -8 5.8 C-8 4.8 -7.5 3.5 -6 1Z" fill="#38BDF8"/>`,
28197
+ // Angry: a red-orange face with knitted brows.
28198
+ `<circle r="17" fill="{{angry}}"/><path d="M-10 -8 L-3 -4.5 M10 -8 L3 -4.5" stroke="{{features}}" stroke-width="2.2" stroke-linecap="round"/><circle cx="-5" cy="-1" r="2" fill="{{features}}"/><circle cx="5" cy="-1" r="2" fill="{{features}}"/><path d="M-6 10 Q0 5.5 6 10" fill="none" stroke="{{features}}" stroke-width="2" stroke-linecap="round"/>`
28199
+ ];
28200
+ var emojiReactions = {
28201
+ kind: "vector",
28202
+ id: "emoji-reactions",
28203
+ name: "Emoji reactions",
28204
+ description: "A reaction picker: on hover the bar opens and like, love, haha, wow, sad and angry faces bounce up one after another, drawn as vectors.",
28205
+ category: "icon",
28206
+ tags: ["interactive", "hover", "emoji", "reactions", "social", "ui"],
28207
+ trigger: "hover",
28208
+ hold: 600,
28209
+ presets: [{ name: "Dark", values: { pill: [0.13, 0.14, 0.19], button: [0.6, 0.63, 0.7] } }],
28210
+ motion: {
28211
+ svg: `<svg viewBox="0 0 280 80"><!-- {{pill: color default=#FFFFFF label='Bar'}} {{button: color default=#64748B label='Button icon'}} {{face: color default=#FBBF24 label='Faces'}} {{features: color default=#5B3A00 label='Eyes and mouths'}} {{like: color default=#2563EB label='Like'}} {{love: color default=#F43F5E label='Love'}} {{angry: color default=#F97316 label='Angry'}} --><rect id="emojiReactionsShadow" x="8" y="17" width="52" height="52" rx="26" fill="#000000" opacity="0.1"/><rect id="emojiReactionsPill" x="6" y="14" width="52" height="52" rx="26" fill="{{pill}}" stroke="#000000" stroke-opacity="0.08"/><g id="emojiReactionsButton" fill="none" stroke="{{button}}" stroke-width="2.2" stroke-linecap="round"><circle cx="30" cy="40" r="11"/><circle cx="26" cy="37" r="0.8" fill="{{button}}"/><circle cx="34" cy="37" r="0.8" fill="{{button}}"/><path d="M25.5 43.5 Q30 47 34.5 43.5"/></g>${FACES.map(
28212
+ (face, i) => `<g id="emojiReactionsFace${i}" transform="${emojiPose(i, 14, 0)}">${face}</g>`
28213
+ ).join("")}</svg>`,
28214
+ animate: [
28215
+ { id: "emojiReactionsPill", attrs: { width: [52, 268] }, ms: 280, ease: "out" },
28216
+ { id: "emojiReactionsShadow", attrs: { width: [52, 268] }, ms: 280, ease: "out" },
28217
+ { id: "emojiReactionsButton", attrs: { opacity: [1, 0] }, ms: 120 },
28218
+ ...FACES.flatMap((_, i) => [
28219
+ { id: `emojiReactionsFace${i}`, attrs: { transform: [emojiPose(i, 14, 0), emojiPose(i, -9, 1.15)] }, delay: 90 + i * 70, ms: 200, ease: "out" },
28220
+ { id: `emojiReactionsFace${i}`, attrs: { transform: [emojiPose(i, -9, 1.15), emojiPose(i, 0, 1)] }, delay: 290 + i * 70, ms: 200, ease: "in-out" }
28221
+ ])
28222
+ ]
28223
+ }
28224
+ };
28225
+ var STARS3 = 5;
28226
+ var STAR = { y: 32, r: 16, inner: 7, pitch: 38, first: 22 };
28227
+ var starX = (i) => STAR.first + i * STAR.pitch;
28228
+ var starPath2 = (cx) => `M${Array.from({ length: 10 }, (_, k) => {
28229
+ const a = k * Math.PI / 5 - Math.PI / 2;
28230
+ const r = k % 2 ? STAR.inner : STAR.r;
28231
+ return `${r115(cx + r * Math.cos(a))} ${r115(STAR.y + 1.5 + r * Math.sin(a))}`;
28232
+ }).join(" L")}Z`;
28233
+ var bookmarkStar = {
28234
+ kind: "vector",
28235
+ id: "bookmark-star",
28236
+ name: "Star rating",
28237
+ description: "Five stars that fill one after another on hover, each bouncing as it fills (halves too), while the rating counts up beside them.",
28238
+ category: "icon",
28239
+ tags: ["interactive", "hover", "rating", "stars", "review", "ui"],
28240
+ trigger: "hover",
28241
+ hold: 600,
28242
+ presets: [
28243
+ { name: "Half", values: { rating: 3.5 } },
28244
+ { name: "Dark", values: { empty: [0.28, 0.31, 0.37], ink: [0.94, 0.95, 0.97] } }
28245
+ ],
28246
+ derived: (v) => {
28247
+ const rating = Math.min(STARS3, Math.max(0, Math.round((Number(v.rating) || 0) * 2) / 2));
28248
+ return {
28249
+ rating,
28250
+ ...Object.fromEntries(
28251
+ Array.from({ length: STARS3 }, (_, i) => {
28252
+ const f = Math.min(1, Math.max(0, rating - i));
28253
+ return [
28254
+ [`w${i}`, r115(f * STAR.r * 2)],
28255
+ [`pop${i}`, about6(starX(i), STAR.y, f > 0 ? 1.22 : 1)]
28256
+ ];
28257
+ }).flat()
28258
+ )
28259
+ };
28260
+ },
28261
+ motion: {
28262
+ svg: `<svg viewBox="0 0 260 64" font-family="${SANS13}"><!-- {{rating: float min=0 max=5 default=4.5 step=0.5 label='Rating'}} {{star: color default=#FBBF24 label='Filled'}} {{empty: color default=#CBD5E1 label='Empty'}} {{ink: color default=#0F172A label='Text'}} --><defs>${Array.from(
28263
+ { length: STARS3 },
28264
+ (_, i) => `<clipPath id="bookmarkStarClip${i}"><rect id="bookmarkStarFill${i}" x="${starX(i) - STAR.r}" y="${STAR.y - STAR.r - 2}" width="0" height="${STAR.r * 2 + 6}"/></clipPath>`
28265
+ ).join("")}</defs>${Array.from(
28266
+ { length: STARS3 },
28267
+ (_, i) => `<g id="bookmarkStarStar${i}" transform="${about6(starX(i), STAR.y, 1)}"><path d="${starPath2(starX(i))}" fill="{{empty}}" stroke="{{empty}}" stroke-width="2" stroke-linejoin="round"/><path d="${starPath2(starX(i))}" fill="{{star}}" stroke="{{star}}" stroke-width="2" stroke-linejoin="round" clip-path="url(#bookmarkStarClip${i})"/></g>`
28268
+ ).join("")}<text id="bookmarkStarValue" x="202" y="38.5" font-size="16" font-weight="700" fill="{{ink}}" opacity="0">0.0</text></svg>`,
28269
+ animate: [
28270
+ ...Array.from({ length: STARS3 }, (_, i) => [
28271
+ { id: `bookmarkStarFill${i}`, attrs: { width: [0, n16(`w${i}`)] }, delay: i * 110, ms: 160, ease: "linear" },
28272
+ { id: `bookmarkStarStar${i}`, attrs: { transform: [about6(starX(i), STAR.y, 1), `{{pop${i}}}`] }, delay: i * 110 + 60, ms: 130, ease: "out" },
28273
+ { id: `bookmarkStarStar${i}`, attrs: { transform: [`{{pop${i}}}`, about6(starX(i), STAR.y, 1)] }, delay: i * 110 + 190, ms: 180, ease: "in-out" }
28274
+ ]).flat(),
28275
+ { id: "bookmarkStarValue", attrs: { opacity: [0, 1] }, ms: 200 },
28276
+ { id: "bookmarkStarValue", count: [0, n16("rating")], decimals: 1, format: "{n} / 5", ms: STARS3 * 110 + 100, ease: "out" }
28277
+ ]
28278
+ }
28279
+ };
28280
+ var LID_HINGE = "76 30";
28281
+ var lid = (deg) => `rotate(${deg} ${LID_HINGE})`;
28282
+ var paperAt = (x, y, deg) => `translate(${x} ${y}) rotate(${deg})`;
28283
+ var canTilt = (deg) => `rotate(${deg} 50 88)`;
28284
+ var trashDelete = {
28285
+ kind: "vector",
28286
+ id: "trash-delete",
28287
+ name: "Trash delete",
28288
+ description: "A trash can whose lid flips open as a crumpled paper drops in, then snaps shut with a little wobble; replays on every click.",
28289
+ category: "icon",
28290
+ tags: ["interactive", "click", "delete", "trash", "remove", "ui"],
28291
+ trigger: "click",
28292
+ hold: 600,
28293
+ presets: [
28294
+ { name: "Danger", values: { color: [0.86, 0.15, 0.15] } },
28295
+ { name: "Dark", values: { color: [0.9, 0.91, 0.94], paper: [0.6, 0.63, 0.7] } }
28296
+ ],
28297
+ motion: {
28298
+ svg: `<svg viewBox="0 0 100 100"><!-- {{color: color default=#334155 label='Can'}} {{paper: color default=#F8FAFC label='Paper'}} {{width: float min=2 max=8 default=5 step=0.5 label='Stroke width'}} --><g id="trashDeletePaper" transform="${paperAt(38, 12, 0)}" opacity="0"><path d="M-9 -4 L-4 -10 L3 -8 L9 -3 L8 5 L2 9 L-6 8 L-10 2Z" fill="{{paper}}" stroke="{{color}}" stroke-width="2" stroke-linejoin="round"/><path d="M-4 -3 L2 1 M-2 5 L4 -2" stroke="{{color}}" stroke-width="1.4" stroke-linecap="round"/></g><g id="trashDeleteCan" transform="${canTilt(0)}" fill="none" stroke="{{color}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round"><path d="M30 36 L34 82 C34.4 86 36 88 40 88 H60 C64 88 65.6 86 66 82 L70 36"/><path d="M43 46 V76 M57 46 V76"/><g id="trashDeleteLid" transform="${lid(0)}"><path d="M24 30 H76 M42 30 V23 H58 V30"/></g></g></svg>`,
28299
+ animate: [
28300
+ { id: "trashDeleteLid", step: 1, attrs: { transform: [lid(0), lid(32)] }, ms: 220, ease: "out" },
28301
+ { id: "trashDeletePaper", step: 1, attrs: { opacity: [0, 1] }, delay: 100, ms: 100 },
28302
+ { id: "trashDeletePaper", step: 1, attrs: { transform: [paperAt(38, 12, 0), paperAt(48, 62, 160)] }, delay: 120, ms: 420, ease: "in-out" },
28303
+ { id: "trashDeletePaper", step: 1, attrs: { opacity: [1, 0] }, delay: 430, ms: 110 },
28304
+ { id: "trashDeleteLid", step: 1, attrs: { transform: [lid(32), lid(0)] }, delay: 540, ms: 170, ease: "in-out" },
28305
+ { id: "trashDeleteLid", step: 1, attrs: { transform: [lid(0), lid(5)] }, delay: 710, ms: 80, ease: "out" },
28306
+ { id: "trashDeleteLid", step: 1, attrs: { transform: [lid(5), lid(0)] }, delay: 790, ms: 120, ease: "in-out" },
28307
+ { id: "trashDeleteCan", step: 1, attrs: { transform: [canTilt(0), canTilt(-5)] }, delay: 700, ms: 90, ease: "out" },
28308
+ { id: "trashDeleteCan", step: 1, attrs: { transform: [canTilt(-5), canTilt(3)] }, delay: 790, ms: 120, ease: "in-out" },
28309
+ { id: "trashDeleteCan", step: 1, attrs: { transform: [canTilt(3), canTilt(0)] }, delay: 910, ms: 140, ease: "in-out" },
28310
+ // The paper, gone, goes back to the top for the next click.
28311
+ { id: "trashDeletePaper", step: 1, attrs: { transform: [paperAt(38, 12, 0), paperAt(38, 12, 0)] }, delay: 1100, ms: 0 }
28312
+ ]
28313
+ }
28314
+ };
28315
+ var shackle = (y) => `translate(0 ${y})`;
28316
+ var keyPose = (deg, s) => `translate(50 66) rotate(${deg}) scale(${s})`;
28317
+ var lockUnlock = {
28318
+ kind: "vector",
28319
+ id: "lock-unlock",
28320
+ name: "Lock and unlock",
28321
+ description: "A padlock: a click turns a key and pops the shackle open as the lock turns green; the next click turns it back and snaps it shut.",
28322
+ category: "icon",
28323
+ tags: ["interactive", "click", "lock", "security", "toggle", "ui"],
28324
+ trigger: "click",
28325
+ hold: 600,
28326
+ presets: [{ name: "Dark", values: { locked: [0.85, 0.87, 0.9], hole: [0.06, 0.07, 0.1] } }],
28327
+ motion: {
28328
+ svg: `<svg viewBox="0 0 100 100"><!-- {{locked: color default=#334155 label='Locked'}} {{open: color default=#16A34A label='Unlocked'}} {{hole: color default=#FFFFFF label='Keyhole'}} {{key: color default=#F59E0B label='Key'}} --><g id="lockUnlockShackle" transform="${shackle(0)}"><path id="lockUnlockLoop" d="M34 48 V34 C34 25 41 18 50 18 C59 18 66 25 66 34 V48" fill="none" stroke="{{locked}}" stroke-width="8"/></g><rect id="lockUnlockBody" x="22" y="44" width="56" height="44" rx="9" fill="{{locked}}"/><g id="lockUnlockHole" transform="rotate(0 50 66)"><circle cx="50" cy="62" r="5.5" fill="{{hole}}"/><path d="M47.5 64 H52.5 L53.5 75 H46.5Z" fill="{{hole}}"/></g><g id="lockUnlockKey" transform="${keyPose(0, 0.4)}" opacity="0"><rect x="-2.5" y="-6" width="5" height="10" fill="{{key}}"/><circle cy="-13" r="8" fill="none" stroke="{{key}}" stroke-width="4.5"/></g></svg>`,
28329
+ animate: [
28330
+ // Step 1, unlock: the key goes in and turns, the shackle springs up, and the lock goes green.
28331
+ { id: "lockUnlockKey", step: 1, attrs: { opacity: [0, 1], transform: [keyPose(0, 0.4), keyPose(0, 1)] }, ms: 160, ease: "out" },
28332
+ { id: "lockUnlockKey", step: 1, attrs: { transform: [keyPose(0, 1), keyPose(90, 1)] }, delay: 180, ms: 200, ease: "in-out" },
28333
+ { id: "lockUnlockHole", step: 1, attrs: { transform: ["rotate(0 50 66)", "rotate(90 50 66)"] }, delay: 180, ms: 200, ease: "in-out" },
28334
+ { id: "lockUnlockShackle", step: 1, attrs: { transform: [shackle(0), shackle(-16)] }, delay: 380, ms: 160, ease: "out" },
28335
+ { id: "lockUnlockShackle", step: 1, attrs: { transform: [shackle(-16), shackle(-12)] }, delay: 540, ms: 180, ease: "in-out" },
28336
+ { id: "lockUnlockBody", step: 1, attrs: { fill: ["{{locked}}", "{{open}}"] }, delay: 380, ms: 220 },
28337
+ { id: "lockUnlockLoop", step: 1, attrs: { stroke: ["{{locked}}", "{{open}}"] }, delay: 380, ms: 220 },
28338
+ { id: "lockUnlockKey", step: 1, attrs: { opacity: [1, 0] }, delay: 650, ms: 200 },
28339
+ // Step 2, lock: the key turns back, the shackle drops shut with a bump.
28340
+ { id: "lockUnlockKey", step: 2, attrs: { opacity: [0, 1], transform: [keyPose(90, 0.4), keyPose(90, 1)] }, ms: 160, ease: "out" },
28341
+ { id: "lockUnlockKey", step: 2, attrs: { transform: [keyPose(90, 1), keyPose(0, 1)] }, delay: 180, ms: 200, ease: "in-out" },
28342
+ { id: "lockUnlockHole", step: 2, attrs: { transform: ["rotate(90 50 66)", "rotate(0 50 66)"] }, delay: 180, ms: 200, ease: "in-out" },
28343
+ { id: "lockUnlockShackle", step: 2, attrs: { transform: [shackle(-12), shackle(2)] }, delay: 380, ms: 150, ease: "in-out" },
28344
+ { id: "lockUnlockShackle", step: 2, attrs: { transform: [shackle(2), shackle(0)] }, delay: 530, ms: 120, ease: "out" },
28345
+ { id: "lockUnlockBody", step: 2, attrs: { fill: ["{{open}}", "{{locked}}"] }, delay: 400, ms: 220 },
28346
+ { id: "lockUnlockLoop", step: 2, attrs: { stroke: ["{{open}}", "{{locked}}"] }, delay: 400, ms: 220 },
28347
+ { id: "lockUnlockKey", step: 2, attrs: { opacity: [1, 0] }, delay: 650, ms: 200 },
28348
+ { id: "lockUnlockKey", step: 2, attrs: { transform: [keyPose(0, 0.4), keyPose(0, 0.4)] }, delay: 900, ms: 0 }
28349
+ ]
28350
+ }
28351
+ };
28352
+ var WIFI = { cx: 50, cy: 80 };
28353
+ var wifiArc = (r) => {
28354
+ const d = r * Math.SQRT1_2;
28355
+ return `M${r115(WIFI.cx - d)} ${r115(WIFI.cy - d)} A${r} ${r} 0 0 1 ${r115(WIFI.cx + d)} ${r115(WIFI.cy - d)}`;
28356
+ };
28357
+ var WIFI_ARCS = [20, 36, 52];
28358
+ var wifiSignal = {
28359
+ kind: "vector",
28360
+ id: "wifi-signal",
28361
+ name: "Wi-Fi signal",
28362
+ description: "Wi-Fi bars filling one after another from the dot outward, pulsing twice at full strength, then fading to start again.",
28363
+ category: "icon",
28364
+ tags: ["wifi", "signal", "connection", "network", "status", "loop"],
28365
+ hold: 200,
28366
+ presets: [
28367
+ { name: "Dark", values: { track: [0.2, 0.22, 0.27], color: [0.38, 0.65, 0.98] } }
28368
+ ],
28369
+ motion: {
28370
+ svg: `<svg viewBox="0 0 100 100"><!-- {{color: color default=#2563EB label='Signal'}} {{track: color default=#E2E8F0 label='Empty bars'}} {{width: float min=4 max=12 default=9 step=0.5 label='Bar width'}} --><g fill="none" stroke="{{track}}" stroke-width="{{width}}" stroke-linecap="round">${WIFI_ARCS.map((r) => `<path d="${wifiArc(r)}"/>`).join("")}</g><circle cx="${WIFI.cx}" cy="${WIFI.cy}" r="6.5" fill="{{track}}"/><g id="wifiSignalOn"><circle id="wifiSignalDot" cx="${WIFI.cx}" cy="${WIFI.cy}" r="6.5" fill="{{color}}" opacity="0"/><g fill="none" stroke="{{color}}" stroke-width="{{width}}" stroke-linecap="round">${WIFI_ARCS.map((r, k) => `<path id="wifiSignalArc${k}" d="${wifiArc(r)}" opacity="0"/>`).join("")}</g></g></svg>`,
28371
+ animate: [
28372
+ { id: "wifiSignalDot", attrs: { opacity: [0, 1], r: [3, 6.5] }, ms: 220, ease: "out" },
28373
+ ...WIFI_ARCS.map((_, k) => ({ id: `wifiSignalArc${k}`, attrs: { opacity: [0, 1] }, delay: 250 + k * 250, ms: 220, ease: "out" })),
28374
+ ...[0, 1].flatMap((k) => swing("wifiSignalOn", "opacity", 1, 0.45, 600, 1150 + k * 600)),
28375
+ // Fades out together, so the loop starts from empty again.
28376
+ { id: "wifiSignalDot", attrs: { opacity: [1, 0], r: [6.5, 3] }, delay: 2600, ms: 300 },
28377
+ ...WIFI_ARCS.map((_, k) => ({ id: `wifiSignalArc${k}`, attrs: { opacity: [1, 0] }, delay: 2600, ms: 300 }))
28378
+ ]
28379
+ }
28380
+ };
28381
+ var iconEffects = [weatherIcons, notificationBell, animatedArrows, emojiReactions, bookmarkStar, trashDelete, lockUnlock, wifiSignal];
28382
+
25167
28383
  // src/catalog/index.ts
25168
28384
  var SHADER_EFFECTS = [
25169
28385
  meshGradient,
@@ -25197,6 +28413,7 @@ var SHADER_EFFECTS = [
25197
28413
  starfield,
25198
28414
  nebula,
25199
28415
  blackHole,
28416
+ ...spaceShaders,
25200
28417
  synthwaveGrid,
25201
28418
  digitalRain,
25202
28419
  pixelSunset,
@@ -25234,9 +28451,12 @@ var SHADER_EFFECTS = [
25234
28451
  ...audioShaders,
25235
28452
  ...scrollShaders,
25236
28453
  ...uiShaders,
28454
+ ...threeDShaders,
28455
+ ...weatherShaders,
28456
+ ...waterShaders,
25237
28457
  ...transitions
25238
28458
  ];
25239
- var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects, ...lightingEffects, ...scrollEffects, ...broadcastEffects, ...annotationEffects, ...uiEffects, ...loaderEffects].sort(
28459
+ var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects, ...lightingEffects, ...scrollEffects, ...broadcastEffects, ...annotationEffects, ...uiEffects, ...loaderEffects, ...dataEffects, ...iconEffects].sort(
25240
28460
  (a, b) => EFFECT_CATEGORIES.indexOf(a.category) - EFFECT_CATEGORIES.indexOf(b.category)
25241
28461
  );
25242
28462
  var byId = new Map(EFFECTS.map((e) => [e.id, e]));
@@ -25312,4 +28532,4 @@ export {
25312
28532
  effectControls,
25313
28533
  effectDefaults
25314
28534
  };
25315
- //# sourceMappingURL=chunk-7F4ITXGK.js.map
28535
+ //# sourceMappingURL=chunk-4FFBIVPI.js.map