instantshader 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
+ ![InstantShader](https://raw.githubusercontent.com/ugolbck/instantshader/main/.github/assets/banner.jpg)
2
+
1
3
  # instantshader
2
4
 
3
5
  Animated WebGL gradient shaders with zero dependencies. Mount a live, resizable
4
6
  gradient into any DOM element, or render a single frame to a detached canvas
5
- for export pipelines. Built by [InstantGradient](https://instantgradient.com/shaders).
7
+ for export pipelines. Built by [InstantGradient](https://instantgradient.com/app).
6
8
 
7
9
  ## Install
8
10
 
@@ -23,6 +25,43 @@ const handle = mountGradient(document.getElementById("bg")!, {
23
25
  // handle.pause() / handle.resume() / handle.dispose() when done
24
26
  ```
25
27
 
28
+ ## Shaders
29
+
30
+ Each shader is a separate named export, so the ones you don't import
31
+ tree-shake away.
32
+
33
+ | Shader | Look | Params |
34
+ | --- | --- | --- |
35
+ | `flow` | Isotropic swirling currents. A curl-noise field advects the sample point before it hits fbm, so colour masses travel in continuous, fluid-like eddies with defined luminous edges and generous calm negative space. | `scale`, `curl`, `drift`, `openness`, `grain` |
36
+ | `beam` | One wide beam of soft light crossing a near-black frame. The palette walks the beam's length, thin brighter filaments crawl inside it, and most of the frame is dark negative space. | `scale`, `width`, `glow`, `angle`, `grain` |
37
+ | `bloom` | A fan of huge, ultra-soft petals radiating from the frame's bottom edge. One analytic rose-curve field, so the palette reads as concentric scalloped bands from a hot core out to a dark background, with thin dark creases between the petals. | `scale`, `petals`, `pinch`, `bend`, `sway`, `colorflow`, `grain` |
38
+ | `halo` | An eclipse: one dark disc with a razor-bright limb and a corona of streamers flowing outward, the palette wrapped around the ring. `x` / `y` place the disc; the default sinks it below the frame for a glowing horizon arc. | `radius`, `x`, `y`, `glow`, `crescent`, `flares`, `grain` |
39
+ | `strata` | Stacked cut-paper sheets, one per palette step, each casting a soft shadow on the one below. `ridges` and `stretch` reshape the relief itself, from round islands to branching spines to long agate bands. | `scale`, `layers`, `warp`, `ridges`, `stretch`, `depth`, `blend`, `angle`, `grain` |
40
+ | `dune` | Overlapping crests rolling across the frame, back to front. Each has one crisp top edge and airbrushes away below it; neighbouring crests slide against each other. | `layers`, `swell`, `waves`, `fade`, `soft`, `angle`, `grain` |
41
+ | `whorl` | A logarithmic spiral of curved blades, each with one sharp leading edge and a soft fade behind it. `x` / `y` place the centre anywhere, including off-frame. | `blades`, `twist`, `depth`, `scale`, `wobble`, `x`, `y`, `grain` |
42
+
43
+ Previews of every look, with full param ranges, are in the
44
+ [repository README](https://github.com/ugolbck/instantshader#readme).
45
+
46
+ Every shader takes the same mount options; `params` is where they differ:
47
+
48
+ ```ts
49
+ import { mountGradient, bloom } from "instantshader";
50
+
51
+ mountGradient(el, {
52
+ shader: bloom,
53
+ colors: ["#ffd9e8", "#ffb300", "#ff8fc0", "#3d7bff", "#0a2e14"],
54
+ // bloom has two independent motion modes — petals that breathe and lean,
55
+ // and colours that travel outward through a still pattern. Mix freely.
56
+ params: { sway: 0.5, colorflow: 0.4 },
57
+ });
58
+ ```
59
+
60
+ Ranges, defaults and labels are all discoverable at runtime — `shader.params`
61
+ is an array of `ParamDef`, and `shader.randomParams(rand)` produces a full,
62
+ sensible param set for "randomize" flows. `shaders` and `getShader(id)` expose
63
+ the whole registry (importing either pulls every shader).
64
+
26
65
  ## Seamless loops
27
66
 
28
67
  Set `loopSeconds` and the animation repeats exactly, with no visible seam at
@@ -68,5 +107,13 @@ Notes:
68
107
  - **`beam` freezes its width swell below ~29s.** Its natural cycle is ~57s and
69
108
  cannot be squeezed into a short loop without becoming a throb, so under that
70
109
  threshold the swell holds still instead. Everything else still animates.
110
+ - **`bloom` sheds parts of its sway on short loops.** Its `sway` is several
111
+ slow oscillations (petal breathing ~20s, fan lean ~30s, petal flex ~42s)
112
+ layered over a noise wander. Each oscillation holds still once the loop is
113
+ shorter than about half its own period, so below ~10s the wander is the only
114
+ thing left moving. `colorflow` is unaffected — it always fits at least one
115
+ full cycle into the loop, flowing faster on a short one.
116
+ - `halo`, `dune` and `whorl` always complete at least one full cycle of their
117
+ main motion per loop, so a short loop simply runs them faster.
71
118
  - Any loop necessarily revisits the same state every N seconds; a long period
72
119
  is what buys the impression of never repeating.
package/dist/index.d.ts CHANGED
@@ -126,6 +126,18 @@ declare const beam: ShaderDef;
126
126
  //#region src/shaders/bloom.d.ts
127
127
  declare const bloom: ShaderDef;
128
128
  //#endregion
129
+ //#region src/shaders/halo.d.ts
130
+ declare const halo: ShaderDef;
131
+ //#endregion
132
+ //#region src/shaders/strata.d.ts
133
+ declare const strata: ShaderDef;
134
+ //#endregion
135
+ //#region src/shaders/dune.d.ts
136
+ declare const dune: ShaderDef;
137
+ //#endregion
138
+ //#region src/shaders/whorl.d.ts
139
+ declare const whorl: ShaderDef;
140
+ //#endregion
129
141
  //#region src/registry.d.ts
130
142
  declare const shaders: readonly ShaderDef[];
131
143
  declare function getShader(id: string): ShaderDef | undefined;
@@ -191,4 +203,4 @@ declare function renderGradientFrame(opts: {
191
203
  */
192
204
  declare function buildPaletteRamp(colors: string[]): Uint8Array;
193
205
  //#endregion
194
- export { type MountHandle, type MountOptions, type ParamDef, type RenderFrameResult, type Renderer, type RendererOptions, type ShaderDef, beam, bloom, buildPaletteRamp, createRenderer, flow, getShader, mountGradient, renderGradientFrame, shaders };
206
+ export { type MountHandle, type MountOptions, type ParamDef, type RenderFrameResult, type Renderer, type RendererOptions, type ShaderDef, beam, bloom, buildPaletteRamp, createRenderer, dune, flow, getShader, halo, mountGradient, renderGradientFrame, shaders, strata, whorl };
package/dist/index.js CHANGED
@@ -187,10 +187,74 @@ float grain(vec2 uv, float time) {
187
187
  return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453);
188
188
  }
189
189
  `;
190
+ /**
191
+ * Isotropic frame helpers, shared by every look whose composition is a SHAPE
192
+ * rather than a frame-filling field. worldUv() is 0-1 on both axes of a 16:9
193
+ * world, so raw uv stretches x by 1.78 -- circles render as ellipses and a
194
+ * rotated stripe changes thickness with its angle. isoCoord() scales x by the
195
+ * world aspect so a unit circle is a screen circle (frame half-height = 0.5),
196
+ * and isoHalf() is the frame's visible half-extents in that space, mirroring
197
+ * worldUv()'s cover fit so a square tile and a 16:9 export agree on where the
198
+ * edges are.
199
+ */
200
+ const ISO = `
201
+ const float WORLD_ASPECT = 1000.0 / 562.5;
202
+ vec2 isoCoord(vec2 uv) { return (uv - 0.5) * vec2(WORLD_ASPECT, 1.0); }
203
+ vec2 isoHalf() {
204
+ float canvasAspect = u_resolution.x / u_resolution.y;
205
+ return vec2(0.5 * min(1.0, canvasAspect / WORLD_ASPECT),
206
+ 0.5 * min(1.0, WORLD_ASPECT / canvasAspect))
207
+ * vec2(WORLD_ASPECT, 1.0);
208
+ }
209
+ `;
210
+ /**
211
+ * A phase angle that advances at ~w rad/s and is exactly periodic over the
212
+ * loop -- for HEADLINE motions (a travelling wave, an orbiting colour) that
213
+ * must never freeze. This is the opposite trade to loopFreq, which rounds to
214
+ * zero on short loops to protect slow secondary swells from turning into a
215
+ * throb: here the cycle count is floored at one, so a short loop simply runs
216
+ * the motion faster.
217
+ *
218
+ * The count is also forced ODD. With an even count the phase at half-cycle is
219
+ * a whole number of turns, i.e. the frame at loop/2 would be identical to the
220
+ * frame at 0 for anything driven by this angle alone -- the loop would
221
+ * secretly be half as long as asked for.
222
+ *
223
+ * Not looping, w is snapped to a whole number of turns per 1000s, because
224
+ * u_time itself wraps there (see renderer.ts) and an unsnapped phase would
225
+ * jump once every ~16.7 minutes.
226
+ */
227
+ const LOOP_ANGLE = `
228
+ float loopAngle(float w) {
229
+ if (u_loop <= 0.0) return TAU * floor(w * 1000.0 / TAU + 0.5) * (u_time / 1000.0);
230
+ float n = 2.0 * floor(w * u_loop / TAU * 0.5) + 1.0;
231
+ return TAU * n * (u_time / u_loop);
232
+ }
233
+ `;
234
+ /**
235
+ * Seed decorrelation. Adding u_seed straight onto a noise coordinate makes
236
+ * the seed a PAN: neighbouring seeds show the same field slid sideways, so a
237
+ * seed slider reads as "moves the picture a bit" instead of "new picture".
238
+ * seedOffset() hashes the seed into an unrelated point of the noise plane, so
239
+ * any change of seed lands on a composition with nothing in common with the
240
+ * last one. seedHash(i) is the scalar version, one independent 0-1 value per
241
+ * channel i, for seeded angles and phases.
242
+ *
243
+ * The offset stays inside [8, 72), far below the ~1000 ceiling where snoise's
244
+ * float precision gives out (see the note at the top of this file).
245
+ */
246
+ const SEED = `
247
+ float seedHash(float i) {
248
+ return fract(sin(u_seed * 12.9898 + i * 78.233) * 43758.5453);
249
+ }
250
+ vec2 seedOffset() {
251
+ return vec2(seedHash(11.0), seedHash(23.0)) * 64.0 + 8.0;
252
+ }
253
+ `;
190
254
 
191
255
  //#endregion
192
256
  //#region src/shaders/flow.ts
193
- const FRAGMENT$2 = `
257
+ const FRAGMENT$6 = `
194
258
  uniform float u_scale;
195
259
  uniform float u_curl;
196
260
  uniform float u_drift;
@@ -327,7 +391,7 @@ void main() {
327
391
  const flow = {
328
392
  id: "flow",
329
393
  label: "Flow",
330
- fragment: FRAGMENT$2,
394
+ fragment: FRAGMENT$6,
331
395
  params: [
332
396
  {
333
397
  key: "scale",
@@ -383,7 +447,7 @@ const flow = {
383
447
 
384
448
  //#endregion
385
449
  //#region src/shaders/beam.ts
386
- const FRAGMENT$1 = `
450
+ const FRAGMENT$5 = `
387
451
  uniform float u_scale;
388
452
  uniform float u_width;
389
453
  uniform float u_glow;
@@ -637,7 +701,7 @@ void main() {
637
701
  const beam = {
638
702
  id: "beam",
639
703
  label: "Beam",
640
- fragment: FRAGMENT$1,
704
+ fragment: FRAGMENT$5,
641
705
  params: [
642
706
  {
643
707
  key: "scale",
@@ -693,7 +757,7 @@ const beam = {
693
757
 
694
758
  //#endregion
695
759
  //#region src/shaders/bloom.ts
696
- const FRAGMENT = `
760
+ const FRAGMENT$4 = `
697
761
  uniform float u_scale;
698
762
  uniform float u_petals;
699
763
  uniform float u_pinch;
@@ -852,7 +916,7 @@ void main() {
852
916
  const bloom = {
853
917
  id: "bloom",
854
918
  label: "Bloom",
855
- fragment: FRAGMENT,
919
+ fragment: FRAGMENT$4,
856
920
  params: [
857
921
  {
858
922
  key: "scale",
@@ -924,12 +988,688 @@ const bloom = {
924
988
  }
925
989
  };
926
990
 
991
+ //#endregion
992
+ //#region src/shaders/halo.ts
993
+ const FRAGMENT$3 = `
994
+ uniform float u_radius;
995
+ uniform float u_x;
996
+ uniform float u_y;
997
+ uniform float u_glow;
998
+ uniform float u_crescent;
999
+ uniform float u_flares;
1000
+ uniform float u_grain;
1001
+
1002
+ ${SIMPLEX_2D}
1003
+ ${PERIODIC_2D}
1004
+ ${GRAIN}
1005
+ ${ISO}
1006
+ ${LOOP_ANGLE}
1007
+ ${SEED}
1008
+
1009
+ const float PI = 3.14159265;
1010
+ const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);
1011
+
1012
+ void main() {
1013
+ vec2 uv = worldUv();
1014
+ vec2 p = isoCoord(uv);
1015
+ vec2 halfIso = isoHalf();
1016
+
1017
+ vec2 so = seedOffset();
1018
+ vec2 drift = loopDrift(0.04, vec2(1.0, 0.0));
1019
+
1020
+ // ---- the disc -----------------------------------------------------------
1021
+ float R = u_radius;
1022
+ vec2 c = vec2(u_x, u_y) * (halfIso + R);
1023
+
1024
+ vec2 rel = p - c;
1025
+ float r = length(rel);
1026
+ float theta = atan(rel.y, rel.x);
1027
+ float d = r - R;
1028
+
1029
+ // ---- streamers ----------------------------------------------------------
1030
+ // A = 14 angular periods; radial tile 4. The second octave doubles both
1031
+ // the frequency and the tile, and travels twice as far per loop, so it
1032
+ // lands on a whole tile too.
1033
+ float A = 14.0;
1034
+ // 0.7: radial features ~6x longer than they are wide at A = 14, which is
1035
+ // the anisotropy that reads as RAYS. At 2.2 the cells were near-square in
1036
+ // log-polar space and the corona rendered as cloud.
1037
+ float rho = log(max(r, 0.001) / R) * 0.7;
1038
+ float flowOut = loopTravel(0.04, vec2(0.0, 1.0), 4.0).y;
1039
+ vec2 sp = vec2((theta / TAU + 0.5) * A, rho - flowOut + so.x);
1040
+ float fl = pnoise(sp, vec2(A, 4.0)) + 0.5 * pnoise(sp * 2.0, vec2(2.0 * A, 8.0));
1041
+
1042
+ float len = (0.03 + 0.3 * u_glow) * max(1.0 + 1.5 * u_flares * fl, 0.15);
1043
+ float outside = max(d, 0.0);
1044
+ float corona = exp(-outside / len);
1045
+ float veil = 0.18 * exp(-outside / (3.5 * len));
1046
+ float inner = 0.4 * exp(min(d, 0.0) / (0.025 + 0.09 * u_glow));
1047
+ float limb = exp(-abs(d) / 0.007);
1048
+
1049
+ // One-pixel soft disc edge, in frame units.
1050
+ float pixel = 2.0 * halfIso.y / u_resolution.y;
1051
+ float isOut = smoothstep(-pixel, pixel, d);
1052
+
1053
+ // ---- crescent -----------------------------------------------------------
1054
+ // Light direction as a VECTOR blend (no angle wrap to worry about): the
1055
+ // seeded direction for a centred disc, handing over to "toward the frame
1056
+ // centre" as the disc moves off it, plus a slow sway.
1057
+ float aSeed = seedHash(1.0) * TAU;
1058
+ float off = length(c);
1059
+ float sway = 0.35 * snoise(vec2(so.y, 1.0) + drift);
1060
+ vec2 inward = -c / max(off, 0.0001);
1061
+ inward = vec2(inward.x * cos(sway) - inward.y * sin(sway),
1062
+ inward.x * sin(sway) + inward.y * cos(sway));
1063
+ vec2 seedDir = vec2(cos(aSeed + sway), sin(aSeed + sway));
1064
+ vec2 dirL = normalize(mix(seedDir, inward, smoothstep(0.05, 0.5, off)) + seedDir * 0.0001);
1065
+ float facing = 0.5 + 0.5 * dot(rel / max(r, 0.0001), dirL);
1066
+ float arc = mix(1.0, 0.03 + 0.97 * facing * facing * facing, u_crescent);
1067
+
1068
+ // ---- palette by azimuth -------------------------------------------------
1069
+ float theta0 = loopAngle(0.1) + seedHash(2.0) * TAU;
1070
+ float tA = 0.5 + 0.5 * cos(theta - theta0 + 0.7 * u_flares * fl);
1071
+ float t = clamp(tA * 0.85 + 0.15 * (1.0 - exp(-outside * 3.0)), 0.0, 1.0);
1072
+ vec3 col = palette(t);
1073
+ float lum = dot(col, LUMA);
1074
+
1075
+ // ---- background: the ramp's darkest stop, crushed (as in beam) ----------
1076
+ vec3 cA = palette(0.0);
1077
+ vec3 cB = palette(0.5);
1078
+ vec3 cC = palette(1.0);
1079
+ float lA = dot(cA, LUMA);
1080
+ float lB = dot(cB, LUMA);
1081
+ float lC = dot(cC, LUMA);
1082
+ vec3 dk = mix(cA, cB, step(lB, lA));
1083
+ dk = mix(dk, cC, step(lC, min(lA, lB)));
1084
+ vec3 color = dk * 0.1;
1085
+
1086
+ // ---- additive light -----------------------------------------------------
1087
+ // gain/boost: see beam.ts -- bright banks are divided down before they can
1088
+ // clip, dark banks are multiplied up (chroma-preserving) before they vanish.
1089
+ float gain = 1.0 / (1.0 + 1.2 * lum * lum);
1090
+ float boost = 1.0 + 2.0 * (1.0 - lum) * (1.0 - lum) * (1.0 - lum);
1091
+ float radiance = mix(inner, corona + veil, isOut);
1092
+ color += col * radiance * arc * gain * boost * 1.35;
1093
+ // The limb is the hottest thing in frame: palette colour plus a dark-gated
1094
+ // white lift, so it still reads as a filament on near-black banks.
1095
+ color += (col * 0.8 + vec3(0.35 * (1.0 - lum))) * limb * arc;
1096
+
1097
+ float g = grain(uv, u_time) - 0.5;
1098
+ color += g * u_grain;
1099
+
1100
+ gl_FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
1101
+ }
1102
+ `;
1103
+ const halo = {
1104
+ id: "halo",
1105
+ label: "Halo",
1106
+ fragment: FRAGMENT$3,
1107
+ params: [
1108
+ {
1109
+ key: "radius",
1110
+ label: "Radius",
1111
+ min: .12,
1112
+ max: 1.6,
1113
+ step: .01,
1114
+ default: 1
1115
+ },
1116
+ {
1117
+ key: "x",
1118
+ label: "X",
1119
+ min: -1,
1120
+ max: 1,
1121
+ step: .01,
1122
+ default: 0
1123
+ },
1124
+ {
1125
+ key: "y",
1126
+ label: "Y",
1127
+ min: -1,
1128
+ max: 1,
1129
+ step: .01,
1130
+ default: -.78
1131
+ },
1132
+ {
1133
+ key: "glow",
1134
+ label: "Glow",
1135
+ min: 0,
1136
+ max: 1,
1137
+ step: .01,
1138
+ default: .5
1139
+ },
1140
+ {
1141
+ key: "crescent",
1142
+ label: "Crescent",
1143
+ min: 0,
1144
+ max: 1,
1145
+ step: .01,
1146
+ default: .35
1147
+ },
1148
+ {
1149
+ key: "flares",
1150
+ label: "Flares",
1151
+ min: 0,
1152
+ max: 1,
1153
+ step: .01,
1154
+ default: .5
1155
+ },
1156
+ {
1157
+ key: "grain",
1158
+ label: "Grain",
1159
+ min: 0,
1160
+ max: .3,
1161
+ step: .01,
1162
+ default: .08
1163
+ }
1164
+ ],
1165
+ randomParams(rand) {
1166
+ const horizon = rand() < .6;
1167
+ return {
1168
+ radius: horizon ? .7 + rand() * .8 : .18 + rand() * .24,
1169
+ x: horizon ? -.3 + rand() * .6 : -.4 + rand() * .8,
1170
+ y: horizon ? -.86 + rand() * .16 : -.2 + rand() * .4,
1171
+ glow: .3 + rand() * .6000000000000001,
1172
+ crescent: rand() * .9,
1173
+ flares: .2 + rand() * .7,
1174
+ grain: .08
1175
+ };
1176
+ }
1177
+ };
1178
+
1179
+ //#endregion
1180
+ //#region src/shaders/strata.ts
1181
+ const FRAGMENT$2 = `
1182
+ uniform float u_scale;
1183
+ uniform float u_layers;
1184
+ uniform float u_warp;
1185
+ uniform float u_ridges;
1186
+ uniform float u_stretch;
1187
+ uniform float u_depth;
1188
+ uniform float u_blend;
1189
+ uniform float u_angle;
1190
+ uniform float u_grain;
1191
+
1192
+ ${SIMPLEX_2D}
1193
+ ${FBM}
1194
+ ${SHAPE}
1195
+ ${GRAIN}
1196
+ ${ISO}
1197
+ ${SEED}
1198
+
1199
+ const float PI = 3.14159265;
1200
+
1201
+ float field(vec2 q) {
1202
+ float n = fbm2(q);
1203
+ // Ridged transform of the SAME sample (free): constants chosen so its mean
1204
+ // and spread roughly match the fbm's, keeping spread()'s equal-area promise
1205
+ // approximately true at any mix.
1206
+ float ridge = 0.55 - 1.6 * abs(n);
1207
+ return spread(mix(n, ridge, u_ridges), 0.35); // 0.35 = fbm2's standard deviation
1208
+ }
1209
+
1210
+ // Frame-space vector -> field-space vector: rotate onto the seeded stretch
1211
+ // axis, then compress along it. Used for positions AND for the tap offsets,
1212
+ // so shadows keep their true on-screen length however stretched the field is.
1213
+ vec2 toField(vec2 v, vec2 axis) {
1214
+ return vec2(dot(v, axis) / (1.0 + 3.0 * u_stretch), dot(v, vec2(-axis.y, axis.x)));
1215
+ }
1216
+
1217
+ void main() {
1218
+ vec2 uv = worldUv();
1219
+ vec2 iso = isoCoord(uv);
1220
+
1221
+ vec2 drift = loopDrift(0.03, vec2(1.0, 0.0));
1222
+ float freq = 0.8 * u_scale;
1223
+
1224
+ // Domain warp, evaluated ONCE and shared by every tap below. The taps are
1225
+ // at most ~0.07 frame units apart, over which a 0.5x-frequency warp is
1226
+ // effectively constant -- re-evaluating it per tap would cost 6 more
1227
+ // snoise calls to move each tap by a fraction of a pixel.
1228
+ float axisAngle = seedHash(5.0) * PI;
1229
+ vec2 axis = vec2(cos(axisAngle), sin(axisAngle));
1230
+ vec2 fp = toField(iso, axis);
1231
+ vec2 so = seedOffset();
1232
+ vec2 wq = fp * (0.5 * u_scale) + so.yx + 40.0 - drift.yx;
1233
+ vec2 warp = u_warp * 0.45 * vec2(snoise(wq), snoise(wq + vec2(13.1, 7.7)));
1234
+ vec2 q = fp * freq + so + drift + warp;
1235
+
1236
+ float L = floor(u_layers + 0.5);
1237
+
1238
+ // Direction TOWARD the light, and the shadow length in frame units.
1239
+ float ang = u_angle * PI / 180.0;
1240
+ vec2 toLight = vec2(cos(ang), sin(ang));
1241
+ vec2 side = vec2(-toLight.y, toLight.x);
1242
+ float reach = 0.012 + 0.06 * u_depth;
1243
+ vec2 qLight = toField(toLight * reach, axis) * freq;
1244
+ vec2 qSide = toField(side * reach, axis) * freq;
1245
+
1246
+ float v = field(q) * L;
1247
+ float vFar = field(q + qLight) * L; // shadow tap
1248
+ float vNear = field(q + qLight * 0.3) * L; // rim tap
1249
+ float vSide = field(q + qSide) * L; // gradient only
1250
+
1251
+ float band = min(floor(v), L - 1.0);
1252
+ float f = v - band;
1253
+
1254
+ // ---- analytic edge AA ---------------------------------------------------
1255
+ // |grad v| per frame unit, from the two orthogonal taps, times the size of
1256
+ // a pixel in frame units (frame height is 2*isoHalf().y over resolution.y).
1257
+ float gradV = length(vec2(vFar - v, vSide - v)) / reach;
1258
+ float pixel = 2.0 * isoHalf().y / u_resolution.y;
1259
+ float aa = clamp(gradV * pixel * 1.5, 0.002, 0.5);
1260
+ float toNext = smoothstep(1.0 - aa, 1.0, f) * step(band, L - 1.5);
1261
+
1262
+ // ---- colour -------------------------------------------------------------
1263
+ float denom = max(L - 1.0, 1.0);
1264
+ float tHere = mix(band / denom, v / L, u_blend);
1265
+ float tNext = mix((band + 1.0) / denom, v / L, u_blend);
1266
+ vec3 color = mix(palette(tHere), palette(tNext), toNext);
1267
+
1268
+ // ---- shadow + rim -------------------------------------------------------
1269
+ // Both fade out across the AA band so the shadow ends exactly where the
1270
+ // upper sheet begins instead of printing a dark fringe onto its edge.
1271
+ float shadow = smoothstep(band + 0.85, band + 1.4, vFar) * (1.0 - toNext);
1272
+ color *= 1.0 - shadow * 0.6 * u_depth;
1273
+
1274
+ float rim = smoothstep(band, band - 0.45, vNear) * step(0.5, band);
1275
+ color += (vec3(1.0) - clamp(color, 0.0, 1.0)) * rim * 0.22 * u_depth;
1276
+
1277
+ float g = grain(uv, u_time) - 0.5;
1278
+ color += g * u_grain;
1279
+
1280
+ gl_FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
1281
+ }
1282
+ `;
1283
+ const strata = {
1284
+ id: "strata",
1285
+ label: "Strata",
1286
+ fragment: FRAGMENT$2,
1287
+ params: [
1288
+ {
1289
+ key: "scale",
1290
+ label: "Scale",
1291
+ min: .4,
1292
+ max: 2.5,
1293
+ step: .05,
1294
+ default: 1
1295
+ },
1296
+ {
1297
+ key: "layers",
1298
+ label: "Layers",
1299
+ min: 3,
1300
+ max: 16,
1301
+ step: 1,
1302
+ default: 7
1303
+ },
1304
+ {
1305
+ key: "warp",
1306
+ label: "Warp",
1307
+ min: 0,
1308
+ max: 1,
1309
+ step: .01,
1310
+ default: .5
1311
+ },
1312
+ {
1313
+ key: "ridges",
1314
+ label: "Ridges",
1315
+ min: 0,
1316
+ max: 1,
1317
+ step: .01,
1318
+ default: .25
1319
+ },
1320
+ {
1321
+ key: "stretch",
1322
+ label: "Stretch",
1323
+ min: 0,
1324
+ max: 1,
1325
+ step: .01,
1326
+ default: .2
1327
+ },
1328
+ {
1329
+ key: "depth",
1330
+ label: "Depth",
1331
+ min: 0,
1332
+ max: 1,
1333
+ step: .01,
1334
+ default: .6
1335
+ },
1336
+ {
1337
+ key: "blend",
1338
+ label: "Blend",
1339
+ min: 0,
1340
+ max: 1,
1341
+ step: .01,
1342
+ default: .15
1343
+ },
1344
+ {
1345
+ key: "angle",
1346
+ label: "Light",
1347
+ min: 0,
1348
+ max: 360,
1349
+ step: 1,
1350
+ default: 125
1351
+ },
1352
+ {
1353
+ key: "grain",
1354
+ label: "Grain",
1355
+ min: 0,
1356
+ max: .3,
1357
+ step: .01,
1358
+ default: .06
1359
+ }
1360
+ ],
1361
+ randomParams(rand) {
1362
+ return {
1363
+ scale: .6 + rand() * 1.2000000000000002,
1364
+ layers: Math.floor(4 + rand() * 9),
1365
+ warp: rand() * 1,
1366
+ ridges: rand() * 1,
1367
+ stretch: rand() * rand(),
1368
+ depth: .35 + rand() * .65,
1369
+ blend: rand() * .6,
1370
+ angle: Math.floor(rand() * 360),
1371
+ grain: .06
1372
+ };
1373
+ }
1374
+ };
1375
+
1376
+ //#endregion
1377
+ //#region src/shaders/dune.ts
1378
+ const FRAGMENT$1 = `
1379
+ uniform float u_layers;
1380
+ uniform float u_swell;
1381
+ uniform float u_waves;
1382
+ uniform float u_fade;
1383
+ uniform float u_soft;
1384
+ uniform float u_angle;
1385
+ uniform float u_grain;
1386
+
1387
+ ${GRAIN}
1388
+ ${ISO}
1389
+ ${LOOP_ANGLE}
1390
+ ${SEED}
1391
+
1392
+ const float PI = 3.14159265;
1393
+
1394
+ void main() {
1395
+ vec2 uv = worldUv();
1396
+ vec2 iso = isoCoord(uv);
1397
+ vec2 halfIso = isoHalf();
1398
+
1399
+ // Layer frame: x runs along the crests, y is "up", normalised so the
1400
+ // visible frame spans y = -1..1 at every angle and aspect.
1401
+ float ang = u_angle * PI / 180.0;
1402
+ vec2 ax = vec2(cos(ang), sin(ang));
1403
+ vec2 ay = vec2(-ax.y, ax.x);
1404
+ float spanY = halfIso.x * abs(ay.x) + halfIso.y * abs(ay.y);
1405
+ float x = dot(iso, ax);
1406
+ float y = dot(iso, ay) / spanY;
1407
+
1408
+ float n = floor(u_layers + 0.5);
1409
+ float a1 = loopAngle(0.22);
1410
+ float a2 = loopAngle(0.36);
1411
+ float k = PI * u_waves;
1412
+
1413
+ float pixel = 2.0 * halfIso.y / u_resolution.y / spanY;
1414
+ float w = pixel + u_soft * 0.22;
1415
+
1416
+ // Sky: the first stop, itself graded a little so it is not a flat fill.
1417
+ vec3 color = palette(0.04 * (1.0 - y));
1418
+
1419
+ for (int li = 0; li < 12; li++) {
1420
+ float i = float(li);
1421
+ if (i >= n) break;
1422
+
1423
+ float h1 = seedHash(i * 3.0 + 1.0);
1424
+ float h2 = seedHash(i * 3.0 + 2.0);
1425
+ float h3 = seedHash(i * 3.0 + 3.0);
1426
+
1427
+ // Crest baselines spread from near the top to near the bottom.
1428
+ float base = mix(0.72, -0.8, i / max(n - 1.0, 1.0));
1429
+ float fi = k * (0.75 + 0.5 * h1);
1430
+ float dir = h3 < 0.5 ? -1.0 : 1.0;
1431
+ float edge = base + u_swell * 0.5 * (0.6 * sin(fi * x + h2 * TAU + dir * a1)
1432
+ + 0.4 * sin(fi * 1.7 * x + h3 * TAU - a2));
1433
+
1434
+ float depth = edge - y;
1435
+ float coverage = smoothstep(-w, w, depth);
1436
+
1437
+ float ti = (i + 1.0) / n;
1438
+ float body = 1.0 - exp(-max(depth, 0.0) / 0.3);
1439
+ // Lengthwise drift: the crest colour wanders a third of a stop along x.
1440
+ float along = 0.35 * sin(x * 1.1 + h1 * TAU + dir * a2);
1441
+ float t = ti + (along - u_fade * 1.15 * body) / n;
1442
+
1443
+ color = mix(color, palette(clamp(t, 0.0, 1.0)), coverage);
1444
+ }
1445
+
1446
+ float g = grain(uv, u_time) - 0.5;
1447
+ color += g * u_grain;
1448
+
1449
+ gl_FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
1450
+ }
1451
+ `;
1452
+ const dune = {
1453
+ id: "dune",
1454
+ label: "Dune",
1455
+ fragment: FRAGMENT$1,
1456
+ params: [
1457
+ {
1458
+ key: "layers",
1459
+ label: "Layers",
1460
+ min: 2,
1461
+ max: 12,
1462
+ step: 1,
1463
+ default: 6
1464
+ },
1465
+ {
1466
+ key: "swell",
1467
+ label: "Swell",
1468
+ min: 0,
1469
+ max: 1.6,
1470
+ step: .01,
1471
+ default: .7
1472
+ },
1473
+ {
1474
+ key: "waves",
1475
+ label: "Waves",
1476
+ min: .3,
1477
+ max: 5,
1478
+ step: .05,
1479
+ default: 1.4
1480
+ },
1481
+ {
1482
+ key: "fade",
1483
+ label: "Fade",
1484
+ min: 0,
1485
+ max: 1.5,
1486
+ step: .01,
1487
+ default: .9
1488
+ },
1489
+ {
1490
+ key: "soft",
1491
+ label: "Soft",
1492
+ min: 0,
1493
+ max: 1,
1494
+ step: .01,
1495
+ default: .02
1496
+ },
1497
+ {
1498
+ key: "angle",
1499
+ label: "Angle",
1500
+ min: 0,
1501
+ max: 360,
1502
+ step: 1,
1503
+ default: 352
1504
+ },
1505
+ {
1506
+ key: "grain",
1507
+ label: "Grain",
1508
+ min: 0,
1509
+ max: .3,
1510
+ step: .01,
1511
+ default: .05
1512
+ }
1513
+ ],
1514
+ randomParams(rand) {
1515
+ return {
1516
+ layers: Math.floor(4 + rand() * 6),
1517
+ swell: .4 + rand() * .7999999999999999,
1518
+ waves: .7 + rand() * 2.0999999999999996,
1519
+ fade: .6 + rand() * .7000000000000001,
1520
+ soft: rand() < .75 ? rand() * .06 : .2 + rand() * .5,
1521
+ angle: Math.floor((rand() < .7 ? -25 + rand() * 50 : rand() * 360) + 360) % 360,
1522
+ grain: .05
1523
+ };
1524
+ }
1525
+ };
1526
+
1527
+ //#endregion
1528
+ //#region src/shaders/whorl.ts
1529
+ const FRAGMENT = `
1530
+ uniform float u_blades;
1531
+ uniform float u_twist;
1532
+ uniform float u_depth;
1533
+ uniform float u_scale;
1534
+ uniform float u_wobble;
1535
+ uniform float u_x;
1536
+ uniform float u_y;
1537
+ uniform float u_grain;
1538
+
1539
+ ${SIMPLEX_2D}
1540
+ ${GRAIN}
1541
+ ${ISO}
1542
+ ${LOOP_ANGLE}
1543
+ ${SEED}
1544
+
1545
+ void main() {
1546
+ vec2 uv = worldUv();
1547
+ vec2 p = isoCoord(uv);
1548
+ vec2 halfIso = isoHalf();
1549
+
1550
+ vec2 c = vec2(u_x, u_y) * halfIso;
1551
+ vec2 rel = p - c;
1552
+ float r = max(length(rel), 0.0001);
1553
+ float theta = atan(rel.y, rel.x);
1554
+
1555
+ float k = floor(u_blades + 0.5);
1556
+ float rot = loopAngle(0.5) / k + seedHash(1.0) * TAU;
1557
+
1558
+ vec2 drift = loopDrift(0.035, vec2(1.0, 0.0));
1559
+ float wob = u_wobble * 0.45 * snoise(p * 0.9 + seedOffset() + drift);
1560
+
1561
+ float u = k * ((theta - rot) + u_twist * log(r)) / TAU + wob;
1562
+ float s = u - floor(u);
1563
+
1564
+ // One-pixel blend across the sawtooth's drop (see header, point 3).
1565
+ float pixel = 2.0 * halfIso.y / u_resolution.y;
1566
+ float aa = clamp(k * sqrt(1.0 + u_twist * u_twist) / (TAU * r) * pixel * 1.5, 0.0, 0.5);
1567
+ s *= 1.0 - smoothstep(1.0 - aa, 1.0, s);
1568
+
1569
+ float core = smoothstep(0.0, 0.08, r);
1570
+ float t = (1.0 - exp(-r / (0.6 * u_scale))) + u_depth * 0.5 * (s - 0.5) * core;
1571
+ vec3 color = palette(clamp(t, 0.0, 1.0));
1572
+
1573
+ float g = grain(uv, u_time) - 0.5;
1574
+ color += g * u_grain;
1575
+
1576
+ gl_FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
1577
+ }
1578
+ `;
1579
+ const whorl = {
1580
+ id: "whorl",
1581
+ label: "Whorl",
1582
+ fragment: FRAGMENT,
1583
+ params: [
1584
+ {
1585
+ key: "blades",
1586
+ label: "Blades",
1587
+ min: 2,
1588
+ max: 24,
1589
+ step: 1,
1590
+ default: 9
1591
+ },
1592
+ {
1593
+ key: "twist",
1594
+ label: "Twist",
1595
+ min: -3,
1596
+ max: 3,
1597
+ step: .05,
1598
+ default: 1.1
1599
+ },
1600
+ {
1601
+ key: "depth",
1602
+ label: "Depth",
1603
+ min: 0,
1604
+ max: 1.5,
1605
+ step: .01,
1606
+ default: .8
1607
+ },
1608
+ {
1609
+ key: "scale",
1610
+ label: "Scale",
1611
+ min: .3,
1612
+ max: 3,
1613
+ step: .05,
1614
+ default: 1.2
1615
+ },
1616
+ {
1617
+ key: "wobble",
1618
+ label: "Wobble",
1619
+ min: 0,
1620
+ max: 1,
1621
+ step: .01,
1622
+ default: .4
1623
+ },
1624
+ {
1625
+ key: "x",
1626
+ label: "X",
1627
+ min: -1.5,
1628
+ max: 1.5,
1629
+ step: .01,
1630
+ default: -.55
1631
+ },
1632
+ {
1633
+ key: "y",
1634
+ label: "Y",
1635
+ min: -1.5,
1636
+ max: 1.5,
1637
+ step: .01,
1638
+ default: -.7
1639
+ },
1640
+ {
1641
+ key: "grain",
1642
+ label: "Grain",
1643
+ min: 0,
1644
+ max: .3,
1645
+ step: .01,
1646
+ default: .06
1647
+ }
1648
+ ],
1649
+ randomParams(rand) {
1650
+ return {
1651
+ blades: Math.floor(5 + rand() * 12),
1652
+ twist: (rand() < .5 ? -1 : 1) * (.5 + rand() * 2),
1653
+ depth: .5 + rand() * .7,
1654
+ scale: .8 + rand() * 1.2,
1655
+ wobble: rand() * .8,
1656
+ x: -1.1 + rand() * 2.2,
1657
+ y: -1.1 + rand() * 2.2,
1658
+ grain: .06
1659
+ };
1660
+ }
1661
+ };
1662
+
927
1663
  //#endregion
928
1664
  //#region src/registry.ts
929
1665
  const shaders = [
930
1666
  flow,
931
1667
  beam,
932
- bloom
1668
+ bloom,
1669
+ halo,
1670
+ strata,
1671
+ dune,
1672
+ whorl
933
1673
  ];
934
1674
  function getShader(id) {
935
1675
  return shaders.find((s) => s.id === id);
@@ -1580,4 +2320,4 @@ function renderGradientFrame(opts) {
1580
2320
  }
1581
2321
 
1582
2322
  //#endregion
1583
- export { beam, bloom, buildPaletteRamp, createRenderer, flow, getShader, mountGradient, renderGradientFrame, shaders };
2323
+ export { beam, bloom, buildPaletteRamp, createRenderer, dune, flow, getShader, halo, mountGradient, renderGradientFrame, shaders, strata, whorl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instantshader",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Animated WebGL gradient shaders. Zero dependencies.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/ugolbck/instantshader.git",
10
10
  "directory": "packages/core"
11
11
  },
12
- "homepage": "https://instantgradient.com/shaders",
12
+ "homepage": "https://instantgradient.com/app",
13
13
  "exports": {
14
14
  ".": {
15
15
  "types": "./dist/index.d.ts",