instantshader 0.2.0 → 0.4.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/dist/index.d.ts CHANGED
@@ -123,6 +123,9 @@ declare const flow: ShaderDef;
123
123
  //#region src/shaders/beam.d.ts
124
124
  declare const beam: ShaderDef;
125
125
  //#endregion
126
+ //#region src/shaders/bloom.d.ts
127
+ declare const bloom: ShaderDef;
128
+ //#endregion
126
129
  //#region src/registry.d.ts
127
130
  declare const shaders: readonly ShaderDef[];
128
131
  declare function getShader(id: string): ShaderDef | undefined;
@@ -188,4 +191,4 @@ declare function renderGradientFrame(opts: {
188
191
  */
189
192
  declare function buildPaletteRamp(colors: string[]): Uint8Array;
190
193
  //#endregion
191
- export { type MountHandle, type MountOptions, type ParamDef, type RenderFrameResult, type Renderer, type RendererOptions, type ShaderDef, beam, buildPaletteRamp, createRenderer, flow, getShader, mountGradient, renderGradientFrame, shaders };
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 };
package/dist/index.js CHANGED
@@ -190,8 +190,9 @@ float grain(vec2 uv, float time) {
190
190
 
191
191
  //#endregion
192
192
  //#region src/shaders/flow.ts
193
- const FRAGMENT$1 = `
193
+ const FRAGMENT$2 = `
194
194
  uniform float u_scale;
195
+ uniform float u_curl;
195
196
  uniform float u_drift;
196
197
  uniform float u_openness;
197
198
  uniform float u_grain;
@@ -236,11 +237,22 @@ void main() {
236
237
  // composition reorganized every ~3 seconds, measured as more pixel change
237
238
  // over 3.5s than the beam prototype showed over 7.5s.
238
239
  //
239
- // The curl field is sampled at 0.55x the fbm's frequency (see below), so
240
- // the visible frame spans curlScale units of noise. The tile has to be at
241
- // least twice that or the field repeats inside a single frame, which looks
242
- // like wallpaper; ceil keeps it integral, which pnoise requires.
243
- float curlScale = u_scale * 0.55;
240
+ // Curl field frequency, relative to the fbm's. Below 1.0 the currents are
241
+ // LARGER than the colour masses they carry, which is the look: sampled at
242
+ // the same frequency each mass sits inside its own little eddy, the
243
+ // advection only roughens mass edges, and the result is indistinguishable
244
+ // from a plain warped fbm.
245
+ //
246
+ // u_curl replaces what was a hardcoded 0.55, and its default is 1.05
247
+ // because the potential is pnoise now. Classic Perlin's lattice is a unit
248
+ // grid while simplex's skewed cells are roughly 0.7 units, so the same
249
+ // input scale yields visibly coarser features -- fewer closed eddies. 1.05
250
+ // was matched by eye against a reference render of the pre-pnoise look.
251
+ //
252
+ // The visible frame spans curlScale units of noise, so the tile has to be
253
+ // at least twice that or the field repeats inside a single frame, which
254
+ // looks like wallpaper; ceil keeps it integral, which pnoise requires.
255
+ float curlScale = u_scale * u_curl;
244
256
  float tile = max(2.0, ceil(curlScale * 2.0));
245
257
 
246
258
  // Straight-line travel through a tiling field: the direction never changes,
@@ -264,18 +276,18 @@ void main() {
264
276
  // the NUMBER of steps that turns advection into rotation: one step is a
265
277
  // plain directional shove, and at two the point still travels an almost
266
278
  // straight chord. The third is where it curves enough to close visible
267
- // eddies, which is the whole point of the look. Step gain is dropped from
268
- // 0.08 to 0.055 to keep the total travel about where it was.
279
+ // eddies, which is the whole point of the look.
269
280
  //
270
- // The curl field is sampled at 0.55x the fbm's frequency, i.e. the
271
- // currents are deliberately LARGER than the colour masses they carry.
272
- // Sampled at the same frequency (as it was) each mass sat inside its own
273
- // little eddy, so the advection only roughened mass edges and the result
274
- // was indistinguishable from a plain warped fbm.
281
+ // Step gain 0.19, up from the 0.055 that was tuned against a simplex
282
+ // potential. pnoise carries shallower gradients, so the old gain advected
283
+ // the point far too little and the eddies stopped closing -- the frame went
284
+ // to soft diagonal bands. The gain absorbs that difference rather than the
285
+ // default of u_drift, which stays at 0.5 in its published 0-1 range so the
286
+ // knob means the same thing it always did.
275
287
  vec2 advected = uv;
276
- advected += curl(advected * curlScale + u_seed + drift, tile) * u_drift * 0.055;
277
- advected += curl(advected * curlScale + u_seed + drift, tile) * u_drift * 0.055;
278
- advected += curl(advected * curlScale + u_seed + drift, tile) * u_drift * 0.055;
288
+ advected += curl(advected * curlScale + u_seed + drift, tile) * u_drift * 0.19;
289
+ advected += curl(advected * curlScale + u_seed + drift, tile) * u_drift * 0.19;
290
+ advected += curl(advected * curlScale + u_seed + drift, tile) * u_drift * 0.19;
279
291
 
280
292
  float t = fbm2(advected * u_scale + u_seed);
281
293
 
@@ -315,7 +327,7 @@ void main() {
315
327
  const flow = {
316
328
  id: "flow",
317
329
  label: "Flow",
318
- fragment: FRAGMENT$1,
330
+ fragment: FRAGMENT$2,
319
331
  params: [
320
332
  {
321
333
  key: "scale",
@@ -325,6 +337,14 @@ const flow = {
325
337
  step: .05,
326
338
  default: 1.7
327
339
  },
340
+ {
341
+ key: "curl",
342
+ label: "Curl",
343
+ min: .3,
344
+ max: 1.6,
345
+ step: .05,
346
+ default: 1.05
347
+ },
328
348
  {
329
349
  key: "drift",
330
350
  label: "Drift",
@@ -353,7 +373,8 @@ const flow = {
353
373
  randomParams(rand) {
354
374
  return {
355
375
  scale: .6 + rand() * 2,
356
- drift: rand() * 1,
376
+ curl: .6 + rand() * .7000000000000001,
377
+ drift: .25 + rand() * .75,
357
378
  openness: rand() * 1,
358
379
  grain: .08
359
380
  };
@@ -362,7 +383,7 @@ const flow = {
362
383
 
363
384
  //#endregion
364
385
  //#region src/shaders/beam.ts
365
- const FRAGMENT = `
386
+ const FRAGMENT$1 = `
366
387
  uniform float u_scale;
367
388
  uniform float u_width;
368
389
  uniform float u_glow;
@@ -616,7 +637,7 @@ void main() {
616
637
  const beam = {
617
638
  id: "beam",
618
639
  label: "Beam",
619
- fragment: FRAGMENT,
640
+ fragment: FRAGMENT$1,
620
641
  params: [
621
642
  {
622
643
  key: "scale",
@@ -670,9 +691,246 @@ const beam = {
670
691
  }
671
692
  };
672
693
 
694
+ //#endregion
695
+ //#region src/shaders/bloom.ts
696
+ const FRAGMENT = `
697
+ uniform float u_scale;
698
+ uniform float u_petals;
699
+ uniform float u_pinch;
700
+ uniform float u_bend;
701
+ uniform float u_sway;
702
+ uniform float u_colorflow;
703
+ uniform float u_grain;
704
+
705
+ ${SIMPLEX_2D}
706
+ ${GRAIN}
707
+
708
+ const float PI = 3.14159265;
709
+
710
+ // Triangle-wave ping-pong: period 2, range [0,1], IDENTITY on [0,1] when the
711
+ // phase is zero -- so colorflow=off renders the plain radial ramp, and any
712
+ // moving phase slides color bands through the pattern with no fract() seam
713
+ // anywhere (the wave reflects instead of jumping).
714
+ float tri(float x) {
715
+ return 1.0 - abs(1.0 - 2.0 * fract(0.5 * x));
716
+ }
717
+
718
+ void main() {
719
+ vec2 uv = worldUv();
720
+
721
+ // Isotropic frame, same construction as beam: worldUv() is 0-1 on both
722
+ // axes of a 16:9 world, so raw uv would render every petal 1.78x wider
723
+ // than tall and the fan would smear sideways. Scaling x by the world
724
+ // aspect makes a unit circle a screen circle; halfIso is the frame's
725
+ // visible half-extents in that space (cover-fit aware), used to place the
726
+ // origin against the actual frame edges at any canvas aspect.
727
+ float worldAspect = 1000.0 / 562.5;
728
+ float canvasAspect = u_resolution.x / u_resolution.y;
729
+ vec2 iso = (uv - 0.5) * vec2(worldAspect, 1.0);
730
+ vec2 halfIso = vec2(0.5 * min(1.0, canvasAspect / worldAspect),
731
+ 0.5 * min(1.0, worldAspect / canvasAspect))
732
+ * vec2(worldAspect, 1.0);
733
+
734
+ // ---- per-instance composition, all derived from u_seed ----------------
735
+ float seedRow = u_seed * 0.61 + 7.0;
736
+
737
+ // Origin: anywhere along the bottom edge (references use bottom-left and
738
+ // bottom-center), from slightly inside the frame (the hot core peeks in,
739
+ // as in all three references) to a bit below it.
740
+ float ox = snoise(vec2(seedRow, 1.3)) * 0.8 * halfIso.x;
741
+ // At or just below the edge -- never inside: an origin inside the frame
742
+ // shows petals radiating in every direction (a starburst), while the
743
+ // references always crop the fan at its base.
744
+ float oy = -halfIso.y * (1.02 + 0.11 * (1.0 + snoise(vec2(seedRow, 9.1))));
745
+ vec2 origin = vec2(ox, oy);
746
+
747
+ // Fan rotation. The rose envelope has k lobes evenly spaced around the
748
+ // full circle, so any rotation leaves several pointing into the frame;
749
+ // this just varies WHICH part of the fan the frame crops.
750
+ float theta0 = snoise(vec2(seedRow, 5.2)) * PI;
751
+
752
+ vec2 rel = iso - origin;
753
+ float r = length(rel);
754
+ float theta = atan(rel.y, rel.x);
755
+
756
+ // ---- sway: the shapes move ---------------------------------------------
757
+ // Lean: the whole fan pivots a few degrees around the origin. Shear pulse:
758
+ // the bend term below oscillates, so petals flex along their length rather
759
+ // than rotating rigidly. Both sinusoidal through loopFreq, so they are
760
+ // exactly periodic when looping (and freeze, rather than speed up, when
761
+ // the loop is too short for their tuned rates -- see loopFreq).
762
+ // The sines alone are not enough: loopFreq freezes them at loops shorter
763
+ // than ~half their period (its documented anti-throb choice), which left
764
+ // sway with NO motion at all at 4-8s loops. wdrift is the non-freezing
765
+ // half -- a loopDrift walk through the per-petal noise (below) and the
766
+ // lean, exactly periodic at ANY loop length by construction, same as the
767
+ // siblings' primary motion.
768
+ vec2 wdrift = loopDrift(0.05 * u_sway, vec2(1.0, 0.0));
769
+ float lean = u_sway * (0.06 * sin(loopFreq(0.21) * u_time + u_seed)
770
+ + 0.05 * snoise(vec2(seedRow * 0.47, 9.3) + wdrift));
771
+ float shearPulse = u_sway * 0.08 * sin(loopFreq(0.15) * u_time + 2.3 + u_seed);
772
+
773
+ // Bend: angular shear proportional to r curves each petal progressively
774
+ // toward its tip -- the gentle bow the reference petals have -- instead of
775
+ // rotating the fan rigidly.
776
+ float th = theta - theta0 - lean + (u_bend + shearPulse) * r;
777
+
778
+ // ---- rose-curve envelope ------------------------------------------------
779
+ // k is snapped to an integer: |cos| has period pi, so integer k makes the
780
+ // envelope exactly 2pi-periodic and the atan branch cut at theta = +-pi is
781
+ // invisible. A fractional k would draw a hard radial seam there.
782
+ float k = floor(u_petals + 0.5);
783
+ float a = 0.5 * k * th;
784
+ float e = pow(abs(cos(a)), u_pinch);
785
+
786
+ // Per-petal identity: which lobe this pixel belongs to, expressed as the
787
+ // lobe's center ANGLE and hashed through cos/sin so it is periodic around
788
+ // the circle -- a raw lobe index would jump by k across the branch cut and
789
+ // tear the per-petal variation along that ray. The discontinuity between
790
+ // ADJACENT lobes is harmless: it sits exactly in the crease, where e ~ 0
791
+ // pushes both sides to the background color anyway.
792
+ float lobe = floor(a / PI + 0.5);
793
+ float cAngle = theta0 + 2.0 * PI * lobe / k;
794
+ float perLobe = 0.28 * snoise(vec2(cos(cAngle), sin(cAngle)) * 1.9
795
+ + vec2(seedRow * 0.13, seedRow * 0.29) + wdrift);
796
+
797
+ // Breathing: each petal's length swells ~10%, phase-offset around the fan
798
+ // by 2*cAngle (integer multiple of the angle, so it survives the branch
799
+ // cut) -- the petals ripple in sequence instead of pulsing in lockstep.
800
+ float breathe = u_sway * 0.10 * sin(loopFreq(0.32) * u_time + 2.0 * cAngle + u_seed * 0.7);
801
+ float len = 1.0 + perLobe + breathe;
802
+
803
+ // ---- the field ----------------------------------------------------------
804
+ // eps = 0.075 is the crease floor (see header). R = 1.4 at scale 1 puts
805
+ // the default tips just past the frame's top edge with the origin at the
806
+ // bottom, matching the references' crop.
807
+ float R = 1.4 * u_scale;
808
+ float t = r / (R * len * (e + 0.075));
809
+
810
+ // Radial shaping: pow < 1 compresses the ramp's early stops into a small
811
+ // hot core at the origin and hands most of each petal's length to the
812
+ // later stops -- in the references the petals are dominated by ONE body
813
+ // color (the second-to-last stop) for ~70% of their length, with the core
814
+ // colors confined to the base. Linear t spread the early stops across half
815
+ // the petal and the body color only appeared at the rim.
816
+ t = pow(t, 0.5);
817
+
818
+ // ---- colorflow: the colors move ----------------------------------------
819
+ // Phase in tri-units (period 2 = one full out-and-back palette cycle).
820
+ // Not looping: linear crawl; the +37 base offset means the knob has a
821
+ // visible effect even at t=0 (and 37 is deliberately not a multiple of the
822
+ // period). Looping: exactly n cycles per loop, floored at ONE -- unlike
823
+ // the sway sines, colorflow is a headline motion, and freezing it at
824
+ // common loop lengths (loopFreq's choice) would read as the feature being
825
+ // broken rather than as restraint. One cycle over a short loop is simply a
826
+ // faster flow, which is what a short loop asks for.
827
+ float pRate = 0.05 * u_colorflow;
828
+ float p;
829
+ if (u_loop <= 0.0) {
830
+ p = pRate * (u_time + 37.0);
831
+ } else {
832
+ p = 2.0 * max(1.0, floor(pRate * u_loop * 0.5 + 0.5)) * (u_time / u_loop);
833
+ }
834
+
835
+ // The flow lives INSIDE the silhouette: the mask fades it out across the
836
+ // tips so the background (t >= 1) stays pinned to the last stop and the
837
+ // negative space never pulses. Gated on u_colorflow so 0 is exactly the
838
+ // static field even while looping (where p above moves regardless).
839
+ float flowMix = (1.0 - smoothstep(0.82, 1.0, t)) * smoothstep(0.0, 0.08, u_colorflow);
840
+ float tc = mix(min(t, 1.0), tri(t - p), flowMix);
841
+
842
+ vec3 color = palette(tc);
843
+
844
+ // Grain: cheap per-pixel dither, centered at 0 so it can darken or
845
+ // lighten symmetrically instead of just brightening the whole frame.
846
+ float g = grain(uv, u_time) - 0.5;
847
+ color += g * u_grain;
848
+
849
+ gl_FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
850
+ }
851
+ `;
852
+ const bloom = {
853
+ id: "bloom",
854
+ label: "Bloom",
855
+ fragment: FRAGMENT,
856
+ params: [
857
+ {
858
+ key: "scale",
859
+ label: "Scale",
860
+ min: .5,
861
+ max: 2,
862
+ step: .05,
863
+ default: .9
864
+ },
865
+ {
866
+ key: "petals",
867
+ label: "Petals",
868
+ min: 6,
869
+ max: 16,
870
+ step: 1,
871
+ default: 11
872
+ },
873
+ {
874
+ key: "pinch",
875
+ label: "Pinch",
876
+ min: .35,
877
+ max: 2.5,
878
+ step: .05,
879
+ default: .6
880
+ },
881
+ {
882
+ key: "bend",
883
+ label: "Bend",
884
+ min: -.9,
885
+ max: .9,
886
+ step: .05,
887
+ default: .25
888
+ },
889
+ {
890
+ key: "sway",
891
+ label: "Sway",
892
+ min: 0,
893
+ max: 1,
894
+ step: .01,
895
+ default: .5
896
+ },
897
+ {
898
+ key: "colorflow",
899
+ label: "Color flow",
900
+ min: 0,
901
+ max: 1,
902
+ step: .01,
903
+ default: 0
904
+ },
905
+ {
906
+ key: "grain",
907
+ label: "Grain",
908
+ min: 0,
909
+ max: .3,
910
+ step: .01,
911
+ default: .08
912
+ }
913
+ ],
914
+ randomParams(rand) {
915
+ return {
916
+ scale: .7 + rand() * .9000000000000001,
917
+ petals: Math.floor(8 + rand() * 7),
918
+ pinch: .5 + rand() * 1.1,
919
+ bend: -.6 + rand() * 1.2,
920
+ sway: .2 + rand() * .7,
921
+ colorflow: rand() * .8,
922
+ grain: .08
923
+ };
924
+ }
925
+ };
926
+
673
927
  //#endregion
674
928
  //#region src/registry.ts
675
- const shaders = [flow, beam];
929
+ const shaders = [
930
+ flow,
931
+ beam,
932
+ bloom
933
+ ];
676
934
  function getShader(id) {
677
935
  return shaders.find((s) => s.id === id);
678
936
  }
@@ -1322,4 +1580,4 @@ function renderGradientFrame(opts) {
1322
1580
  }
1323
1581
 
1324
1582
  //#endregion
1325
- export { beam, buildPaletteRamp, createRenderer, flow, getShader, mountGradient, renderGradientFrame, shaders };
1583
+ export { beam, bloom, buildPaletteRamp, createRenderer, flow, getShader, mountGradient, renderGradientFrame, shaders };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instantshader",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Animated WebGL gradient shaders. Zero dependencies.",
5
5
  "type": "module",
6
6
  "license": "MIT",