instantshader 0.7.0 → 0.8.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.js CHANGED
@@ -254,7 +254,7 @@ vec2 seedOffset() {
254
254
 
255
255
  //#endregion
256
256
  //#region src/shaders/flow.ts
257
- const FRAGMENT$10 = `
257
+ const FRAGMENT$11 = `
258
258
  uniform float u_scale;
259
259
  uniform float u_curl;
260
260
  uniform float u_drift;
@@ -391,7 +391,7 @@ void main() {
391
391
  const flow = {
392
392
  id: "flow",
393
393
  label: "Flow",
394
- fragment: FRAGMENT$10,
394
+ fragment: FRAGMENT$11,
395
395
  params: [
396
396
  {
397
397
  key: "scale",
@@ -447,7 +447,7 @@ const flow = {
447
447
 
448
448
  //#endregion
449
449
  //#region src/shaders/beam.ts
450
- const FRAGMENT$9 = `
450
+ const FRAGMENT$10 = `
451
451
  uniform float u_scale;
452
452
  uniform float u_width;
453
453
  uniform float u_glow;
@@ -701,7 +701,7 @@ void main() {
701
701
  const beam = {
702
702
  id: "beam",
703
703
  label: "Beam",
704
- fragment: FRAGMENT$9,
704
+ fragment: FRAGMENT$10,
705
705
  params: [
706
706
  {
707
707
  key: "scale",
@@ -709,7 +709,7 @@ const beam = {
709
709
  min: .5,
710
710
  max: 2,
711
711
  step: .05,
712
- default: 1
712
+ default: .75
713
713
  },
714
714
  {
715
715
  key: "width",
@@ -717,7 +717,7 @@ const beam = {
717
717
  min: .04,
718
718
  max: .6,
719
719
  step: .005,
720
- default: .14
720
+ default: .4
721
721
  },
722
722
  {
723
723
  key: "glow",
@@ -757,7 +757,7 @@ const beam = {
757
757
 
758
758
  //#endregion
759
759
  //#region src/shaders/bloom.ts
760
- const FRAGMENT$8 = `
760
+ const FRAGMENT$9 = `
761
761
  uniform float u_scale;
762
762
  uniform float u_petals;
763
763
  uniform float u_pinch;
@@ -916,7 +916,7 @@ void main() {
916
916
  const bloom = {
917
917
  id: "bloom",
918
918
  label: "Bloom",
919
- fragment: FRAGMENT$8,
919
+ fragment: FRAGMENT$9,
920
920
  params: [
921
921
  {
922
922
  key: "scale",
@@ -990,7 +990,7 @@ const bloom = {
990
990
 
991
991
  //#endregion
992
992
  //#region src/shaders/halo.ts
993
- const FRAGMENT$7 = `
993
+ const FRAGMENT$8 = `
994
994
  uniform float u_radius;
995
995
  uniform float u_x;
996
996
  uniform float u_y;
@@ -1103,7 +1103,7 @@ void main() {
1103
1103
  const halo = {
1104
1104
  id: "halo",
1105
1105
  label: "Halo",
1106
- fragment: FRAGMENT$7,
1106
+ fragment: FRAGMENT$8,
1107
1107
  params: [
1108
1108
  {
1109
1109
  key: "radius",
@@ -1178,7 +1178,7 @@ const halo = {
1178
1178
 
1179
1179
  //#endregion
1180
1180
  //#region src/shaders/strata.ts
1181
- const FRAGMENT$6 = `
1181
+ const FRAGMENT$7 = `
1182
1182
  uniform float u_scale;
1183
1183
  uniform float u_layers;
1184
1184
  uniform float u_warp;
@@ -1283,7 +1283,7 @@ void main() {
1283
1283
  const strata = {
1284
1284
  id: "strata",
1285
1285
  label: "Strata",
1286
- fragment: FRAGMENT$6,
1286
+ fragment: FRAGMENT$7,
1287
1287
  params: [
1288
1288
  {
1289
1289
  key: "scale",
@@ -1375,7 +1375,7 @@ const strata = {
1375
1375
 
1376
1376
  //#endregion
1377
1377
  //#region src/shaders/dune.ts
1378
- const FRAGMENT$5 = `
1378
+ const FRAGMENT$6 = `
1379
1379
  uniform float u_layers;
1380
1380
  uniform float u_swell;
1381
1381
  uniform float u_waves;
@@ -1452,7 +1452,7 @@ void main() {
1452
1452
  const dune = {
1453
1453
  id: "dune",
1454
1454
  label: "Dune",
1455
- fragment: FRAGMENT$5,
1455
+ fragment: FRAGMENT$6,
1456
1456
  params: [
1457
1457
  {
1458
1458
  key: "layers",
@@ -1526,7 +1526,7 @@ const dune = {
1526
1526
 
1527
1527
  //#endregion
1528
1528
  //#region src/shaders/whorl.ts
1529
- const FRAGMENT$4 = `
1529
+ const FRAGMENT$5 = `
1530
1530
  uniform float u_blades;
1531
1531
  uniform float u_twist;
1532
1532
  uniform float u_depth;
@@ -1579,7 +1579,7 @@ void main() {
1579
1579
  const whorl = {
1580
1580
  id: "whorl",
1581
1581
  label: "Whorl",
1582
- fragment: FRAGMENT$4,
1582
+ fragment: FRAGMENT$5,
1583
1583
  params: [
1584
1584
  {
1585
1585
  key: "blades",
@@ -1619,7 +1619,7 @@ const whorl = {
1619
1619
  min: 0,
1620
1620
  max: 1,
1621
1621
  step: .01,
1622
- default: .4
1622
+ default: 0
1623
1623
  },
1624
1624
  {
1625
1625
  key: "x",
@@ -1662,7 +1662,7 @@ const whorl = {
1662
1662
 
1663
1663
  //#endregion
1664
1664
  //#region src/shaders/silk.ts
1665
- const FRAGMENT$3 = `
1665
+ const FRAGMENT$4 = `
1666
1666
  uniform float u_scale;
1667
1667
  uniform float u_folds;
1668
1668
  uniform float u_depth;
@@ -1751,7 +1751,7 @@ void main() {
1751
1751
  const silk = {
1752
1752
  id: "silk",
1753
1753
  label: "Silk",
1754
- fragment: FRAGMENT$3,
1754
+ fragment: FRAGMENT$4,
1755
1755
  params: [
1756
1756
  {
1757
1757
  key: "scale",
@@ -1825,7 +1825,7 @@ const silk = {
1825
1825
 
1826
1826
  //#endregion
1827
1827
  //#region src/shaders/nacre.ts
1828
- const FRAGMENT$2 = `
1828
+ const FRAGMENT$3 = `
1829
1829
  uniform float u_scale;
1830
1830
  uniform float u_flow;
1831
1831
  uniform float u_crease;
@@ -1902,7 +1902,7 @@ void main() {
1902
1902
  const nacre = {
1903
1903
  id: "nacre",
1904
1904
  label: "Nacre",
1905
- fragment: FRAGMENT$2,
1905
+ fragment: FRAGMENT$3,
1906
1906
  params: [
1907
1907
  {
1908
1908
  key: "scale",
@@ -1976,7 +1976,7 @@ const nacre = {
1976
1976
 
1977
1977
  //#endregion
1978
1978
  //#region src/shaders/burst.ts
1979
- const FRAGMENT$1 = `
1979
+ const FRAGMENT$2 = `
1980
1980
  uniform float u_rays;
1981
1981
  uniform float u_sharp;
1982
1982
  uniform float u_glow;
@@ -2040,7 +2040,7 @@ void main() {
2040
2040
  const burst = {
2041
2041
  id: "burst",
2042
2042
  label: "Burst",
2043
- fragment: FRAGMENT$1,
2043
+ fragment: FRAGMENT$2,
2044
2044
  params: [
2045
2045
  {
2046
2046
  key: "rays",
@@ -2114,7 +2114,7 @@ const burst = {
2114
2114
 
2115
2115
  //#endregion
2116
2116
  //#region src/shaders/glint.ts
2117
- const FRAGMENT = `
2117
+ const FRAGMENT$1 = `
2118
2118
  uniform float u_size;
2119
2119
  uniform float u_density;
2120
2120
  uniform float u_bend;
@@ -2237,7 +2237,7 @@ void main() {
2237
2237
  const glint = {
2238
2238
  id: "glint",
2239
2239
  label: "Glint",
2240
- fragment: FRAGMENT,
2240
+ fragment: FRAGMENT$1,
2241
2241
  params: [
2242
2242
  {
2243
2243
  key: "size",
@@ -2300,94 +2300,6 @@ const glint = {
2300
2300
  }
2301
2301
  };
2302
2302
 
2303
- //#endregion
2304
- //#region src/effects/pixelate.ts
2305
- const CELL_FRAGMENT$3 = `
2306
- uniform float u_levels;
2307
-
2308
- void main() {
2309
- vec3 c = cellSource().rgb;
2310
- // Posterize per channel. Rounds to the nearest level, so black and white
2311
- // stay put. levels = 1 would divide by zero and means nothing; 0 is "off".
2312
- if (u_levels >= 2.0) {
2313
- float n = u_levels - 1.0;
2314
- c = floor(c * n + 0.5) / n;
2315
- }
2316
- gl_FragColor = vec4(c, 1.0);
2317
- }
2318
- `;
2319
- const DRAW_FRAGMENT$2 = `
2320
- uniform float u_gap;
2321
- uniform vec3 u_gapColor;
2322
-
2323
- void main() {
2324
- vec3 color = coverage();
2325
- if (u_gap > 0.0) {
2326
- // A gap line is u_gap of a cell wide, centred on every cell boundary.
2327
- // Its weight is the exact share of this output pixel it covers, and the
2328
- // blend happens in linear light, for the same reason coverage() works
2329
- // that way: a smoothstep edge in gamma space draws sub-pixel lines far
2330
- // too heavy, so a small preview would look darker than its own export.
2331
- vec2 px = cellPx();
2332
- vec2 d = min(cellLocal(), 1.0 - cellLocal()) * px; // px to nearest boundary
2333
- vec2 hw = 0.5 * u_gap * px; // line half-width in px
2334
- vec2 cov = clamp(min(hw, d + 0.5) - max(-hw, d - 0.5), 0.0, 1.0);
2335
- float m = 1.0 - (1.0 - cov.x) * (1.0 - cov.y);
2336
- color = toSrgb(mix(toLinear(color), toLinear(u_gapColor), m));
2337
- }
2338
- gl_FragColor = vec4(color, 1.0);
2339
- }
2340
- `;
2341
- const pixelate = {
2342
- id: "pixelate",
2343
- label: "Pixelate",
2344
- fragment: DRAW_FRAGMENT$2,
2345
- grid: {
2346
- cell: (p) => [p.size, p.size],
2347
- fragment: CELL_FRAGMENT$3
2348
- },
2349
- params: [
2350
- {
2351
- key: "size",
2352
- label: "Size",
2353
- min: 2,
2354
- max: 160,
2355
- step: 1,
2356
- default: 24
2357
- },
2358
- {
2359
- key: "levels",
2360
- label: "Levels",
2361
- min: 0,
2362
- max: 16,
2363
- step: 1,
2364
- default: 0
2365
- },
2366
- {
2367
- key: "gap",
2368
- label: "Gap",
2369
- min: 0,
2370
- max: .4,
2371
- step: .01,
2372
- default: 0
2373
- },
2374
- {
2375
- key: "gapColor",
2376
- label: "Gap color",
2377
- type: "color",
2378
- default: "#000000"
2379
- }
2380
- ],
2381
- randomParams(rand) {
2382
- return {
2383
- size: Math.round(10 + rand() * 50),
2384
- levels: rand() < .5 ? 0 : Math.round(3 + rand() * 5),
2385
- gap: rand() < .6 ? 0 : .05 + rand() * .15,
2386
- gapColor: "#000000"
2387
- };
2388
- }
2389
- };
2390
-
2391
2303
  //#endregion
2392
2304
  //#region src/effects/chunks.ts
2393
2305
  /** sRGB transfer functions. Blending and averaging must happen in linear
@@ -2608,8 +2520,7 @@ vec3 colorize(float tone, vec3 src) {
2608
2520
  return palette(tone);
2609
2521
  }
2610
2522
  `;
2611
- /** The param defs that go with COLOR_MODE. `paperAlways` is for effects
2612
- * where paper is a visible ground in every mode (ASCII, Halftone). */
2523
+ /** The param defs that go with COLOR_MODE. */
2613
2524
  function colorModeParams(opts) {
2614
2525
  return [
2615
2526
  {
@@ -2623,7 +2534,7 @@ function colorModeParams(opts) {
2623
2534
  },
2624
2535
  {
2625
2536
  value: "duotone",
2626
- label: "Duotone"
2537
+ label: opts.singleColour ? "Single colour" : "Duotone"
2627
2538
  },
2628
2539
  {
2629
2540
  value: "palette",
@@ -2634,7 +2545,7 @@ function colorModeParams(opts) {
2634
2545
  },
2635
2546
  {
2636
2547
  key: "ink",
2637
- label: "Ink",
2548
+ label: opts.singleColour ? "Colour" : "Ink",
2638
2549
  type: "color",
2639
2550
  default: opts.ink ?? "#111111",
2640
2551
  when: {
@@ -2647,10 +2558,10 @@ function colorModeParams(opts) {
2647
2558
  label: "Paper",
2648
2559
  type: "color",
2649
2560
  default: opts.paper ?? "#f4f1ea",
2650
- ...opts.paperAlways ? {} : { when: {
2561
+ when: opts.paperWhen ?? {
2651
2562
  key: "colorMode",
2652
2563
  in: ["duotone"]
2653
- } }
2564
+ }
2654
2565
  },
2655
2566
  {
2656
2567
  key: "invert",
@@ -2660,6 +2571,341 @@ function colorModeParams(opts) {
2660
2571
  }
2661
2572
  ];
2662
2573
  }
2574
+ /**
2575
+ * Blend modes, W3C Compositing and Blending Level 1, computed on
2576
+ * gamma-encoded sRGB like CSS and every image editor. b is the backdrop, s
2577
+ * the source (the mark or the filter's colour). Overlay is hard light with
2578
+ * the roles swapped. Color dodge: 0 where b is 0, else min(1, b / (1 - s)).
2579
+ */
2580
+ const BLEND = `
2581
+ uniform float u_blend;
2582
+ vec3 blendScreen(vec3 b, vec3 s) { return b + s - b * s; }
2583
+ vec3 blendHardLight(vec3 b, vec3 s) {
2584
+ return mix(b * 2.0 * s, blendScreen(b, 2.0 * s - 1.0), step(0.5, s));
2585
+ }
2586
+ vec3 blendSoftLight(vec3 b, vec3 s) {
2587
+ vec3 d = mix(sqrt(b), ((16.0 * b - 12.0) * b + 4.0) * b, step(b, vec3(0.25)));
2588
+ return mix(b - (1.0 - 2.0 * s) * b * (1.0 - b), b + (2.0 * s - 1.0) * (d - b), step(0.5, s));
2589
+ }
2590
+ vec3 blendColorDodge(vec3 b, vec3 s) {
2591
+ return min(vec3(1.0), b / max(1.0 - s, vec3(1e-5))) * step(1e-5, b);
2592
+ }
2593
+ vec3 blendOf(vec3 b, vec3 s) {
2594
+ if (u_blend < 0.5) return s;
2595
+ if (u_blend < 1.5) return b * s;
2596
+ if (u_blend < 2.5) return blendScreen(b, s);
2597
+ if (u_blend < 3.5) return blendHardLight(s, b);
2598
+ if (u_blend < 4.5) return blendSoftLight(b, s);
2599
+ return blendColorDodge(b, s);
2600
+ }
2601
+ `;
2602
+ /** Draw stage: the layer below and the blend-then-fade composite. The
2603
+ * blended colour is faded in by coverage x opacity in linear light, so an
2604
+ * antialiased edge keeps its true weight. */
2605
+ const COMPOSITE = `
2606
+ uniform float u_opacity;
2607
+ uniform sampler2D u_picture;
2608
+ vec3 pictureAt(vec2 fc) { return texture2D(u_picture, fc / u_resolution).rgb; }
2609
+ vec3 compositeLinear(vec3 b, vec3 s, float a) {
2610
+ return mix(toLinear(b), toLinear(blendOf(b, s)), clamp(a * u_opacity, 0.0, 1.0));
2611
+ }
2612
+ `;
2613
+ /** Mark effects: what the marks are drawn on. */
2614
+ const GROUND = `
2615
+ uniform float u_ground;
2616
+ vec3 backdropAt(vec2 fc) { return u_ground < 0.5 ? pictureAt(fc) : u_paper; }
2617
+ `;
2618
+ /** Filter effects (a whole replacement picture per cell) over the layer
2619
+ * below. Normal at full opacity hands the colour back untouched: no linear
2620
+ * round trip, so whole-pixel cells stay exact at 1080p and 4K. */
2621
+ const FILTER_COMPOSITE = `
2622
+ vec3 overPicture(vec3 s) {
2623
+ if (u_blend < 0.5 && u_opacity >= 1.0) return s;
2624
+ return toSrgb(compositeLinear(pictureAt(gl_FragCoord.xy), s, 1.0));
2625
+ }
2626
+ `;
2627
+ /**
2628
+ * Cell stage of a mark effect: the cell's tone becomes a mark strength in
2629
+ * 0..1. See SPEC-marks.md, Response.
2630
+ *
2631
+ * Direction: marks grow where they show. On an image ground a darkening
2632
+ * blend (multiply) grows them in the darks and every other blend in the
2633
+ * brights; on paper they grow away from the paper. Invert flips it.
2634
+ *
2635
+ * Motion multiplies the strength, so blank cells stay blank. Its angular
2636
+ * speed is snapped to whole cycles per loop and floored at one: it is the
2637
+ * effect's own motion, and freezing it on a short loop would read as broken.
2638
+ *
2639
+ * No shipped effect declares motion, motionSpeed or motionAmount yet (see
2640
+ * motionParams() below), so these uniforms are unset and read as 0: motion
2641
+ * is "none" and the strength above is unmodulated. The GLSL stays wired up
2642
+ * because motion ships later, once effects get a clock independent of the
2643
+ * source's own speed and pause; see SPEC-marks.md.
2644
+ */
2645
+ const RESPONSE = `
2646
+ uniform float u_style;
2647
+ uniform float u_exposure;
2648
+ uniform float u_contrast;
2649
+ uniform float u_density;
2650
+ uniform float u_ground;
2651
+ uniform float u_blend;
2652
+ uniform float u_motion;
2653
+ uniform float u_motionSpeed;
2654
+ uniform float u_motionAmount;
2655
+
2656
+ bool growsInBrights() {
2657
+ if (u_ground > 0.5) return luma(u_paper) < 0.5;
2658
+ return abs(u_blend - 1.0) > 0.5;
2659
+ }
2660
+
2661
+ float motionAt(vec2 cell) {
2662
+ if (u_motion < 0.5) return 0.0;
2663
+ float w = TAU * u_motionSpeed;
2664
+ if (u_loop > 0.0) w = TAU * max(1.0, floor(u_motionSpeed * u_loop + 0.5)) / u_loop;
2665
+ float ph = w * u_time;
2666
+ if (u_motion < 1.5) return sin(ph);
2667
+ if (u_motion < 2.5) {
2668
+ // 1.5 waves per frame height, travelling along a fixed diagonal.
2669
+ vec2 pos = cell * u_cellRef / u_refSize.y;
2670
+ return sin(ph - TAU * 1.5 * dot(pos, vec2(0.857, 0.514)));
2671
+ }
2672
+ return sin(ph + TAU * hash12(cell + 13.1));
2673
+ }
2674
+
2675
+ float markStrength(vec3 c) {
2676
+ float tone = u_style < 0.5 ? luma(c) : 0.5;
2677
+ tone = clamp(tone * exp2(2.0 * u_exposure), 0.0, 1.0);
2678
+ float s = growsInBrights() ? tone : 1.0 - tone;
2679
+ if (u_invert > 0.5) s = 1.0 - s;
2680
+ s = clamp((s - 0.5) * u_contrast + 0.5, 0.0, 1.0);
2681
+ s = clamp(s * (1.0 + u_motionAmount * motionAt(cellIndex())), 0.0, 1.0);
2682
+ if (hash12(cellIndex() + vec2(71.3, 19.7)) >= u_density) s = 0.0;
2683
+ return s;
2684
+ }
2685
+ `;
2686
+ /** Append only: the uniform is the option's index. */
2687
+ const BLEND_OPTIONS = [
2688
+ {
2689
+ value: "normal",
2690
+ label: "Normal"
2691
+ },
2692
+ {
2693
+ value: "multiply",
2694
+ label: "Multiply"
2695
+ },
2696
+ {
2697
+ value: "screen",
2698
+ label: "Screen"
2699
+ },
2700
+ {
2701
+ value: "overlay",
2702
+ label: "Overlay"
2703
+ },
2704
+ {
2705
+ value: "softLight",
2706
+ label: "Soft light"
2707
+ },
2708
+ {
2709
+ value: "colorDodge",
2710
+ label: "Color dodge"
2711
+ }
2712
+ ];
2713
+ function blendParams(d) {
2714
+ return [{
2715
+ key: "blend",
2716
+ label: "Blend",
2717
+ type: "enum",
2718
+ options: BLEND_OPTIONS,
2719
+ default: d.blend
2720
+ }, {
2721
+ key: "opacity",
2722
+ label: "Opacity",
2723
+ min: 0,
2724
+ max: 1,
2725
+ step: .01,
2726
+ default: d.opacity
2727
+ }];
2728
+ }
2729
+ function groundParams(d) {
2730
+ return [
2731
+ {
2732
+ key: "ground",
2733
+ label: "Ground",
2734
+ type: "enum",
2735
+ options: [{
2736
+ value: "image",
2737
+ label: "Image"
2738
+ }, {
2739
+ value: "paper",
2740
+ label: "Paper"
2741
+ }],
2742
+ default: d.ground
2743
+ },
2744
+ ...blendParams(d),
2745
+ {
2746
+ key: "blur",
2747
+ label: "Blur",
2748
+ min: 0,
2749
+ max: 40,
2750
+ step: .5,
2751
+ default: 0,
2752
+ when: {
2753
+ key: "ground",
2754
+ in: ["image"]
2755
+ }
2756
+ }
2757
+ ];
2758
+ }
2759
+ function responseParams(d) {
2760
+ return [
2761
+ {
2762
+ key: "style",
2763
+ label: "Style",
2764
+ type: "enum",
2765
+ options: [{
2766
+ value: "filled",
2767
+ label: "Filled"
2768
+ }, {
2769
+ value: "uniform",
2770
+ label: "Uniform"
2771
+ }],
2772
+ default: "filled"
2773
+ },
2774
+ {
2775
+ key: "exposure",
2776
+ label: "Exposure",
2777
+ min: -1,
2778
+ max: 1,
2779
+ step: .01,
2780
+ default: 0
2781
+ },
2782
+ {
2783
+ key: "contrast",
2784
+ label: "Contrast",
2785
+ min: 0,
2786
+ max: 2,
2787
+ step: .01,
2788
+ default: d.contrast
2789
+ },
2790
+ {
2791
+ key: "density",
2792
+ label: "Density",
2793
+ min: 0,
2794
+ max: 1,
2795
+ step: .01,
2796
+ default: 1
2797
+ }
2798
+ ];
2799
+ }
2800
+ /** grid.picture for mark effects. A paper ground never reads the picture,
2801
+ * so it gets none; blur only applies to an image ground. */
2802
+ function groundPicture(p) {
2803
+ return p.ground === "image" ? { blur: p.blur } : null;
2804
+ }
2805
+ /** grid.picture for filter effects (pixelate, dither). Normal at full
2806
+ * opacity replaces the picture outright (FILTER_COMPOSITE), so it is not
2807
+ * read and the stack skips rendering it. */
2808
+ function filterPicture(p) {
2809
+ return p.blend === "normal" && p.opacity >= 1 ? null : { blur: 0 };
2810
+ }
2811
+
2812
+ //#endregion
2813
+ //#region src/effects/pixelate.ts
2814
+ const CELL_FRAGMENT$3 = `
2815
+ uniform float u_levels;
2816
+
2817
+ void main() {
2818
+ vec3 c = cellSource().rgb;
2819
+ // Posterize per channel. Rounds to the nearest level, so black and white
2820
+ // stay put. levels = 1 would divide by zero and means nothing; 0 is "off".
2821
+ if (u_levels >= 2.0) {
2822
+ float n = u_levels - 1.0;
2823
+ c = floor(c * n + 0.5) / n;
2824
+ }
2825
+ gl_FragColor = vec4(c, 1.0);
2826
+ }
2827
+ `;
2828
+ const DRAW_FRAGMENT$3 = `
2829
+ uniform float u_gap;
2830
+ uniform vec3 u_gapColor;
2831
+ ${BLEND}
2832
+ ${COMPOSITE}
2833
+ ${FILTER_COMPOSITE}
2834
+
2835
+ void main() {
2836
+ vec3 color = coverage();
2837
+ if (u_gap > 0.0) {
2838
+ // A gap line is u_gap of a cell wide, centred on every cell boundary.
2839
+ // Its weight is the exact share of this output pixel it covers, and the
2840
+ // blend happens in linear light, for the same reason coverage() works
2841
+ // that way: a smoothstep edge in gamma space draws sub-pixel lines far
2842
+ // too heavy, so a small preview would look darker than its own export.
2843
+ vec2 px = cellPx();
2844
+ vec2 d = min(cellLocal(), 1.0 - cellLocal()) * px; // px to nearest boundary
2845
+ vec2 hw = 0.5 * u_gap * px; // line half-width in px
2846
+ vec2 cov = clamp(min(hw, d + 0.5) - max(-hw, d - 0.5), 0.0, 1.0);
2847
+ float m = 1.0 - (1.0 - cov.x) * (1.0 - cov.y);
2848
+ color = toSrgb(mix(toLinear(color), toLinear(u_gapColor), m));
2849
+ }
2850
+ gl_FragColor = vec4(overPicture(color), 1.0);
2851
+ }
2852
+ `;
2853
+ const pixelate = {
2854
+ id: "pixelate",
2855
+ label: "Pixelate",
2856
+ fragment: DRAW_FRAGMENT$3,
2857
+ grid: {
2858
+ cell: (p) => [p.size, p.size],
2859
+ fragment: CELL_FRAGMENT$3,
2860
+ picture: filterPicture
2861
+ },
2862
+ params: [
2863
+ {
2864
+ key: "size",
2865
+ label: "Size",
2866
+ min: 2,
2867
+ max: 160,
2868
+ step: 1,
2869
+ default: 13
2870
+ },
2871
+ {
2872
+ key: "levels",
2873
+ label: "Levels",
2874
+ min: 0,
2875
+ max: 16,
2876
+ step: 1,
2877
+ default: 0
2878
+ },
2879
+ {
2880
+ key: "gap",
2881
+ label: "Gap",
2882
+ min: 0,
2883
+ max: .4,
2884
+ step: .01,
2885
+ default: .08
2886
+ },
2887
+ {
2888
+ key: "gapColor",
2889
+ label: "Gap color",
2890
+ type: "color",
2891
+ default: "#000000"
2892
+ },
2893
+ ...blendParams({
2894
+ blend: "normal",
2895
+ opacity: 1
2896
+ })
2897
+ ],
2898
+ randomParams(rand) {
2899
+ return {
2900
+ size: Math.round(10 + rand() * 50),
2901
+ levels: rand() < .5 ? 0 : Math.round(3 + rand() * 5),
2902
+ gap: rand() < .6 ? 0 : .05 + rand() * .15,
2903
+ gapColor: "#000000",
2904
+ blend: "normal",
2905
+ opacity: 1
2906
+ };
2907
+ }
2908
+ };
2663
2909
 
2664
2910
  //#endregion
2665
2911
  //#region src/effects/blueNoise.ts
@@ -2750,12 +2996,23 @@ void main() {
2750
2996
  gl_FragColor = vec4(color, 1.0);
2751
2997
  }
2752
2998
  `;
2999
+ /** Coverage upscale, then over the picture (FILTER_COMPOSITE). */
3000
+ const DRAW_FRAGMENT$2 = `
3001
+ ${BLEND}
3002
+ ${COMPOSITE}
3003
+ ${FILTER_COMPOSITE}
3004
+ void main() {
3005
+ gl_FragColor = vec4(overPicture(coverage()), 1.0);
3006
+ }
3007
+ `;
2753
3008
  const dither = {
2754
3009
  id: "dither",
2755
3010
  label: "Dither",
3011
+ fragment: DRAW_FRAGMENT$2,
2756
3012
  grid: {
2757
3013
  cell: (p) => [p.size, p.size],
2758
- fragment: CELL_FRAGMENT$2
3014
+ fragment: CELL_FRAGMENT$2,
3015
+ picture: filterPicture
2759
3016
  },
2760
3017
  textures: [{
2761
3018
  uniform: "u_noise",
@@ -2833,7 +3090,15 @@ const dither = {
2833
3090
  step: 1,
2834
3091
  default: 0
2835
3092
  },
2836
- ...colorModeParams({ mode: "source" })
3093
+ ...colorModeParams({
3094
+ mode: "duotone",
3095
+ ink: "#000000",
3096
+ paper: "#ffffff"
3097
+ }),
3098
+ ...blendParams({
3099
+ blend: "screen",
3100
+ opacity: .6
3101
+ })
2837
3102
  ],
2838
3103
  randomParams(rand) {
2839
3104
  const patterns = [
@@ -2847,7 +3112,13 @@ const dither = {
2847
3112
  "duotone",
2848
3113
  "palette"
2849
3114
  ];
2850
- return {
3115
+ const blends = [
3116
+ "normal",
3117
+ "screen",
3118
+ "overlay",
3119
+ "softLight"
3120
+ ];
3121
+ const p = {
2851
3122
  pattern: patterns[Math.floor(rand() * patterns.length)],
2852
3123
  size: Math.round(2 + rand() * 6),
2853
3124
  levels: Math.round(2 + rand() * 3),
@@ -2857,8 +3128,15 @@ const dither = {
2857
3128
  colorMode: modes[Math.floor(rand() * modes.length)],
2858
3129
  ink: "#111111",
2859
3130
  paper: "#f4f1ea",
2860
- invert: false
3131
+ invert: false,
3132
+ blend: blends[Math.floor(rand() * blends.length)],
3133
+ opacity: .6 + rand() * .4
2861
3134
  };
3135
+ if (p.blend === "screen" || p.blend === "colorDodge") {
3136
+ p.ink = "#000000";
3137
+ p.paper = "#ffffff";
3138
+ }
3139
+ return p;
2862
3140
  }
2863
3141
  };
2864
3142
 
@@ -2866,26 +3144,27 @@ const dither = {
2866
3144
  //#region src/effects/halftone.ts
2867
3145
  const ROW = .8660254;
2868
3146
  const CELL_FRAGMENT$1 = `
2869
- uniform float u_contrast;
2870
3147
  ${COLOR_MODE}
3148
+ ${RESPONSE}
2871
3149
 
2872
3150
  void main() {
2873
3151
  vec3 sc = cellSource().rgb;
3152
+ float ink = markStrength(sc);
2874
3153
 
2875
- // Ink amount. Halftone sizes its shapes by brightness in every color mode,
2876
- // so this is luma even where toneOf() would return a ramp position.
2877
- float tone = luma(sc);
2878
- if (u_invert > 0.5) tone = 1.0 - tone;
2879
- float ink = clamp((1.0 - tone - 0.5) * u_contrast + 0.5, 0.0, 1.0);
2880
-
2881
- // Ink color. Duotone inks every shape with u_ink (colorize() would mix
2882
- // toward paper by tone, which is dither's meaning of duotone). Palette
2883
- // inks each dot with the nearest palette STOP to the un-inverted tone, so
2884
- // Invert flips sizes and leaves colors, and the dots are flat palette
2885
- // colors rather than a copy of the source.
3154
+ // Ink color. Duotone inks every shape with u_ink. Palette inks each dot
3155
+ // with the nearest palette STOP to the un-inverted tone, so Invert flips
3156
+ // sizes and leaves colors, and the dots are flat palette colors rather
3157
+ // than a copy of the source.
2886
3158
  vec3 color = sc;
2887
3159
  if (u_colorMode > 1.5) color = paletteStop(toneIsRamp() ? rampPosition(sc) : luma(sc));
2888
3160
  else if (u_colorMode > 0.5) color = u_ink;
3161
+ else if (u_ground < 0.5) {
3162
+ // Over the picture, a shape in the picture's own colour blends into
3163
+ // itself and vanishes. Lift it most of the way to full brightness, as
3164
+ // ASCII does, so it reads as a vivid version of what is under it.
3165
+ float peak = max(max(sc.r, sc.g), max(sc.b, 1e-3));
3166
+ color = mix(sc, sc / peak, 0.75);
3167
+ }
2889
3168
 
2890
3169
  gl_FragColor = vec4(color, ink);
2891
3170
  }
@@ -2895,8 +3174,10 @@ uniform float u_grid;
2895
3174
  uniform float u_shape;
2896
3175
  uniform float u_radius;
2897
3176
  uniform float u_softness;
2898
- uniform float u_pulse;
2899
3177
  ${COLOR_MODE}
3178
+ ${BLEND}
3179
+ ${COMPOSITE}
3180
+ ${GROUND}
2900
3181
 
2901
3182
  const float ROW = ${ROW};
2902
3183
 
@@ -2905,9 +3186,10 @@ vec4 lattice(vec2 k) {
2905
3186
  return texture2D(u_cells, (kk + 0.5) / u_gridSize);
2906
3187
  }
2907
3188
 
2908
- // The shape covering lattice coordinate g most, blended over paper, in
2909
- // linear light. \`aa\` is the width, in lattice units, edges are ramped over.
2910
- vec3 shade(vec2 g, float aa, float w) {
3189
+ // The shape covering lattice coordinate g most, composited onto backdrop b
3190
+ // (sRGB), returned in linear light. \`aa\` is the width, in lattice units,
3191
+ // edges are ramped over.
3192
+ vec3 shade(vec2 g, float aa, vec3 b) {
2911
3193
  bool hex = u_grid > 0.5;
2912
3194
  bool line = u_shape > 0.5 && u_shape < 1.5;
2913
3195
  // Texel units -> lattice units (1 = the pitch the user set as Size).
@@ -2930,8 +3212,7 @@ vec3 shade(vec2 g, float aa, float w) {
2930
3212
  float kl = stride * floor((g.x - 0.5 - par) / stride) + par;
2931
3213
  float f = (g.x - 0.5 - kl) / stride;
2932
3214
  vec4 v = mix(lattice(vec2(kl, j)), lattice(vec2(kl + stride, j)), f);
2933
- float swell = 1.0 + 0.25 * u_pulse * sin(w * u_time + 0.22 * (j - 0.5 * u_gridSize.y) + u_seed);
2934
- float extent = min(0.5 * v.a * unit.y * u_radius * swell, 1.0);
3215
+ float extent = min(0.5 * v.a * unit.y * u_radius, 1.0);
2935
3216
  float ramp = aa + u_softness * extent;
2936
3217
  float mask = extent <= 0.0 ? 0.0 : clamp((extent - abs(dy)) / ramp + 0.5, 0.0, 1.0);
2937
3218
  if (mask > bestMask) { bestMask = mask; bestInk = v.rgb; }
@@ -2943,20 +3224,23 @@ vec3 shade(vec2 g, float aa, float w) {
2943
3224
  vec2 k = vec2(k0 + stride * float(di), j);
2944
3225
  vec4 v = lattice(k);
2945
3226
  vec2 d = vec2((g.x - (k.x + 0.5)) * unit.x, dy);
2946
- vec2 id = k - 0.5 * u_gridSize;
2947
- float swell = 1.0 + 0.25 * u_pulse * sin(w * u_time + dot(id * unit, vec2(0.35, 0.22)) + u_seed);
2948
-
2949
3227
  float dist;
2950
3228
  float extent;
3229
+ // On paper, shape AREA follows ink: the print rule that reproduces
3230
+ // tone. Over the picture there is no tone to reproduce, and area
3231
+ // sizing squeezes the visible size range (half the ink is still 71%
3232
+ // of the full width), so there the width follows strength directly:
3233
+ // empty cells stay empty and full ones fill.
3234
+ float grow = u_ground < 0.5 ? v.a : sqrt(v.a);
2951
3235
  if (u_shape < 0.5) {
2952
3236
  // Dot: area pi * rho^2 out of a cell of area 1 (square) or ROW (hex).
2953
3237
  dist = length(d);
2954
- extent = sqrt(v.a * (hex ? ROW : 1.0) / 3.14159265);
3238
+ extent = grow * sqrt((hex ? ROW : 1.0) / 3.14159265);
2955
3239
  } else {
2956
3240
  dist = max(abs(d.x), abs(d.y));
2957
- extent = 0.5 * sqrt(v.a);
3241
+ extent = 0.5 * grow;
2958
3242
  }
2959
- extent = min(extent * u_radius * swell, 1.0);
3243
+ extent = min(extent * u_radius, 1.0);
2960
3244
 
2961
3245
  // Coverage of a straight edge by a box is linear in the distance to
2962
3246
  // it. Softness widens that ramp into a blur.
@@ -2965,22 +3249,12 @@ vec3 shade(vec2 g, float aa, float w) {
2965
3249
  if (mask > bestMask) { bestMask = mask; bestInk = v.rgb; }
2966
3250
  }
2967
3251
  }
2968
- return mix(toLinear(u_paper), toLinear(bestInk), bestMask);
3252
+ return compositeLinear(b, bestInk, bestMask);
2969
3253
  }
2970
3254
 
2971
3255
  void main() {
2972
3256
  float pitchRef = u_grid > 0.5 ? u_cellRef.x * 2.0 : u_cellRef.x;
2973
- // One output pixel, in lattice units.
2974
3257
  float px = 1.0 / (pitchRef * u_outputScale);
2975
-
2976
- // Pulse: every shape swells and shrinks, phase-shifted across the lattice
2977
- // so the swell travels through the frame instead of throbbing in lockstep.
2978
- // Snapped to whole cycles per loop and floored at one: unlike a generator's
2979
- // slow secondary swell, this is the effect's only motion, so freezing it at
2980
- // short loops (loopFreq's choice) would read as the knob being broken.
2981
- float w = 0.8;
2982
- if (u_loop > 0.0) w = TAU * max(1.0, floor(w * u_loop / TAU + 0.5)) / u_loop;
2983
-
2984
3258
  // Supersampled: n x n samples per pixel, each ramping its edges over 1/n
2985
3259
  // of a pixel. One sample with a one-pixel linear ramp is exact only for a
2986
3260
  // straight edge parallel to the pixel grid; on a 5px preview dot every
@@ -2995,8 +3269,8 @@ void main() {
2995
3269
  for (int sy = 0; sy < 4; sy++) {
2996
3270
  for (int sx = 0; sx < 4; sx++) {
2997
3271
  if (float(sx) >= n || float(sy) >= n) continue;
2998
- vec2 o = (vec2(float(sx), float(sy)) + 0.5) / n - 0.5;
2999
- acc += shade(cellCoordAt(gl_FragCoord.xy + o), px / n, w);
3272
+ vec2 fc = gl_FragCoord.xy + (vec2(float(sx), float(sy)) + 0.5) / n - 0.5;
3273
+ acc += shade(cellCoordAt(fc), px / n, backdropAt(fc));
3000
3274
  }
3001
3275
  }
3002
3276
  gl_FragColor = vec4(toSrgb(acc / (n * n)), 1.0);
@@ -3013,9 +3287,15 @@ const halftone = {
3013
3287
  grid: {
3014
3288
  cell: cellOf$1,
3015
3289
  lattice: (p) => ({ angle: p.angle }),
3290
+ picture: groundPicture,
3016
3291
  fragment: CELL_FRAGMENT$1
3017
3292
  },
3018
3293
  params: [
3294
+ ...groundParams({
3295
+ ground: "image",
3296
+ blend: "screen",
3297
+ opacity: 1
3298
+ }),
3019
3299
  {
3020
3300
  key: "grid",
3021
3301
  label: "Grid",
@@ -3047,7 +3327,7 @@ const halftone = {
3047
3327
  label: "Square"
3048
3328
  }
3049
3329
  ],
3050
- default: "dot"
3330
+ default: "square"
3051
3331
  },
3052
3332
  {
3053
3333
  key: "size",
@@ -3055,7 +3335,7 @@ const halftone = {
3055
3335
  min: 6,
3056
3336
  max: 160,
3057
3337
  step: .5,
3058
- default: 10
3338
+ default: 20
3059
3339
  },
3060
3340
  {
3061
3341
  key: "angle",
@@ -3071,7 +3351,7 @@ const halftone = {
3071
3351
  min: .2,
3072
3352
  max: 1.5,
3073
3353
  step: .01,
3074
- default: 1.4
3354
+ default: .75
3075
3355
  },
3076
3356
  {
3077
3357
  key: "softness",
@@ -3081,25 +3361,14 @@ const halftone = {
3081
3361
  step: .01,
3082
3362
  default: .1
3083
3363
  },
3084
- {
3085
- key: "contrast",
3086
- label: "Contrast",
3087
- min: 0,
3088
- max: 2,
3089
- step: .01,
3090
- default: 1.15
3091
- },
3092
- {
3093
- key: "pulse",
3094
- label: "Pulse",
3095
- min: 0,
3096
- max: 1,
3097
- step: .01,
3098
- default: 0
3099
- },
3364
+ ...responseParams({ contrast: 1.15 }),
3100
3365
  ...colorModeParams({
3101
- mode: "palette",
3102
- paperAlways: true
3366
+ mode: "source",
3367
+ paperWhen: {
3368
+ key: "ground",
3369
+ in: ["paper"]
3370
+ },
3371
+ singleColour: true
3103
3372
  })
3104
3373
  ],
3105
3374
  randomParams(rand) {
@@ -3114,17 +3383,34 @@ const halftone = {
3114
3383
  "duotone",
3115
3384
  "palette"
3116
3385
  ];
3386
+ const ground = rand() < .8 ? "image" : "paper";
3387
+ const blends = ground === "paper" ? ["normal", "multiply"] : [
3388
+ "normal",
3389
+ "multiply",
3390
+ "screen",
3391
+ "screen",
3392
+ "overlay",
3393
+ "softLight",
3394
+ "colorDodge"
3395
+ ];
3396
+ const blend = blends[Math.floor(rand() * blends.length)];
3117
3397
  return {
3398
+ ground,
3399
+ blend,
3400
+ opacity: .6 + rand() * .4,
3401
+ blur: 0,
3118
3402
  grid: rand() < .5 ? "square" : "hex",
3119
3403
  shape: shapes[Math.floor(rand() * shapes.length)],
3120
3404
  size: Math.round(8 + rand() * 30),
3121
3405
  angle: Math.round(rand() * 180),
3122
- radius: 1.2 + rand() * .3,
3406
+ radius: .7 + rand() * .7,
3123
3407
  softness: rand() < .7 ? .1 : rand() * .5,
3408
+ style: "filled",
3409
+ exposure: 0,
3124
3410
  contrast: 1.15,
3125
- pulse: 0,
3411
+ density: 1,
3126
3412
  colorMode: modes[Math.floor(rand() * modes.length)],
3127
- ink: "#111111",
3413
+ ink: blend === "screen" || blend === "colorDodge" ? "#ffffff" : "#111111",
3128
3414
  paper: "#f4f1ea",
3129
3415
  invert: false
3130
3416
  };
@@ -3362,11 +3648,13 @@ uniform float u_cycleAmount;
3362
3648
  uniform float u_glyphCount;
3363
3649
  uniform sampler2D u_lut;
3364
3650
  ${COLOR_MODE}
3651
+ ${RESPONSE}
3365
3652
 
3366
3653
  void main() {
3367
3654
  vec3 sc = cellSource().rgb;
3368
- float tone = luma(sc);
3369
- if (u_invert > 0.5) tone = 1.0 - tone;
3655
+ // Glyph weight is the shared mark strength: a stronger mark is a denser
3656
+ // glyph, and a cell the density knob empties gets the blank glyph.
3657
+ float tone = markStrength(sc);
3370
3658
 
3371
3659
  // The lookup places glyphs by measured ink coverage: R = the glyph just
3372
3660
  // below this tone, G = the one just above, B = how far between them.
@@ -3380,13 +3668,22 @@ void main() {
3380
3668
  float t = u_smooth > 0.5 ? 0.02 + 0.96 * hash12(cellIndex() + 0.5) : 0.5;
3381
3669
  float glyph = l.b > t ? hi : lo;
3382
3670
 
3383
- // Cycle: every step each cell re-rolls its glyph among neighbours of
3384
- // similar weight, so the picture flickers but stays readable. Blank cells
3671
+ // Cycle: every step a share of the cells re-rolls its glyph among
3672
+ // neighbours of similar weight, so the picture flickers but stays
3673
+ // readable. cycleAmount is that share, and also how far a cell may move
3674
+ // (one glyph at small amounts, up to a quarter of the set at 1): a small
3675
+ // amount must flicker a few cells, not round every move down to nothing,
3676
+ // which is what a pure distance scale did on a 10-glyph set. Blank cells
3385
3677
  // stay blank, or dark areas would fill with sparks.
3386
3678
  if (u_cycle > 0.0 && glyph > 0.5) {
3387
- float h = hash12(cellIndex() + vec2(37.0, 17.0) * loopStep(u_cycle) + u_seed);
3388
- float span = u_cycleAmount * u_glyphCount * 0.25;
3389
- glyph = clamp(glyph + floor((h * 2.0 - 1.0) * span + 0.5), 1.0, u_glyphCount - 1.0);
3679
+ float st = loopStep(u_cycle);
3680
+ float pick = hash12(cellIndex() + vec2(11.0, 53.0) * st + u_seed + 3.1);
3681
+ if (pick < u_cycleAmount) {
3682
+ float h = hash12(cellIndex() + vec2(37.0, 17.0) * st + u_seed);
3683
+ float reach = max(1.0, floor(u_cycleAmount * u_glyphCount * 0.25));
3684
+ float move = (h < 0.5 ? -1.0 : 1.0) * (1.0 + floor(fract(h * 2.0) * reach));
3685
+ glyph = clamp(glyph + move, 1.0, u_glyphCount - 1.0);
3686
+ }
3390
3687
  }
3391
3688
 
3392
3689
  // Glyph color. Tone already lives in the glyph's density, so source colors
@@ -3412,6 +3709,9 @@ uniform vec2 u_atlasGrid; // glyph columns, rows
3412
3709
  uniform vec2 u_atlasSize; // atlas size in texels
3413
3710
  uniform vec2 u_glyphPx; // one glyph's size in atlas texels (without gutter)
3414
3711
  ${COLOR_MODE}
3712
+ ${BLEND}
3713
+ ${COMPOSITE}
3714
+ ${GROUND}
3415
3715
 
3416
3716
  void main() {
3417
3717
  vec4 cell = cellValue(cellIndexAt());
@@ -3440,8 +3740,7 @@ void main() {
3440
3740
  }
3441
3741
  a /= n * n;
3442
3742
 
3443
- vec3 color = toSrgb(mix(toLinear(u_paper), toLinear(cell.rgb), a));
3444
- gl_FragColor = vec4(color, 1.0);
3743
+ gl_FragColor = vec4(toSrgb(compositeLinear(backdropAt(gl_FragCoord.xy), cell.rgb, a)), 1.0);
3445
3744
  }
3446
3745
  `;
3447
3746
  function cellOf(p) {
@@ -3457,7 +3756,8 @@ const ascii = {
3457
3756
  fragment: DRAW_FRAGMENT,
3458
3757
  grid: {
3459
3758
  cell: cellOf,
3460
- fragment: CELL_FRAGMENT
3759
+ fragment: CELL_FRAGMENT,
3760
+ picture: groundPicture
3461
3761
  },
3462
3762
  textures: [{
3463
3763
  uniform: "u_atlas",
@@ -3477,6 +3777,11 @@ const ascii = {
3477
3777
  })
3478
3778
  }],
3479
3779
  params: [
3780
+ ...groundParams({
3781
+ ground: "image",
3782
+ blend: "colorDodge",
3783
+ opacity: 1
3784
+ }),
3480
3785
  {
3481
3786
  key: "charset",
3482
3787
  label: "Characters",
@@ -3515,7 +3820,7 @@ const ascii = {
3515
3820
  min: 8,
3516
3821
  max: 96,
3517
3822
  step: 1,
3518
- default: 24
3823
+ default: 10
3519
3824
  },
3520
3825
  {
3521
3826
  key: "smooth",
@@ -3529,7 +3834,7 @@ const ascii = {
3529
3834
  min: 0,
3530
3835
  max: 12,
3531
3836
  step: 1,
3532
- default: 0
3837
+ default: 2
3533
3838
  },
3534
3839
  {
3535
3840
  key: "cycleAmount",
@@ -3537,13 +3842,18 @@ const ascii = {
3537
3842
  min: 0,
3538
3843
  max: 1,
3539
3844
  step: .01,
3540
- default: .3
3845
+ default: .2
3541
3846
  },
3847
+ ...responseParams({ contrast: 1.2 }),
3542
3848
  ...colorModeParams({
3543
3849
  mode: "source",
3544
3850
  ink: "#e8ffe8",
3545
3851
  paper: "#000000",
3546
- paperAlways: true
3852
+ paperWhen: {
3853
+ key: "ground",
3854
+ in: ["paper"]
3855
+ },
3856
+ singleColour: true
3547
3857
  })
3548
3858
  ],
3549
3859
  randomParams(rand) {
@@ -3559,12 +3869,28 @@ const ascii = {
3559
3869
  "duotone",
3560
3870
  "palette"
3561
3871
  ];
3872
+ const ground = rand() < .8 ? "image" : "paper";
3873
+ const blends = ground === "paper" ? ["normal"] : [
3874
+ "normal",
3875
+ "screen",
3876
+ "screen",
3877
+ "overlay",
3878
+ "colorDodge"
3879
+ ];
3562
3880
  return {
3881
+ ground,
3882
+ blend: blends[Math.floor(rand() * blends.length)],
3883
+ opacity: .7 + rand() * .3,
3884
+ blur: 0,
3563
3885
  charset: sets[Math.floor(rand() * sets.length)],
3564
3886
  size: Math.round(14 + rand() * 30),
3565
3887
  smooth: true,
3566
- cycle: 0,
3567
- cycleAmount: .3,
3888
+ cycle: 2,
3889
+ cycleAmount: .2,
3890
+ style: "filled",
3891
+ exposure: 0,
3892
+ contrast: 1,
3893
+ density: 1,
3568
3894
  colorMode: modes[Math.floor(rand() * modes.length)],
3569
3895
  ink: "#e8ffe8",
3570
3896
  paper: "#000000",
@@ -3573,6 +3899,77 @@ const ascii = {
3573
3899
  }
3574
3900
  };
3575
3901
 
3902
+ //#endregion
3903
+ //#region src/effects/tint.ts
3904
+ const FRAGMENT = `
3905
+ uniform float u_mode;
3906
+ uniform vec3 u_dark;
3907
+ uniform vec3 u_light;
3908
+ uniform float u_amount;
3909
+ void main() {
3910
+ vec3 c = source(v_uv).rgb;
3911
+ float t = luma(c);
3912
+ vec3 mapped = u_mode < 0.5 ? mix(u_dark, u_light, t) : palette(t);
3913
+ gl_FragColor = vec4(toSrgb(mix(toLinear(c), toLinear(mapped), u_amount)), 1.0);
3914
+ }
3915
+ `;
3916
+ const tint = {
3917
+ id: "tint",
3918
+ label: "Tint",
3919
+ fragment: FRAGMENT,
3920
+ params: [
3921
+ {
3922
+ key: "mode",
3923
+ label: "Mode",
3924
+ type: "enum",
3925
+ options: [{
3926
+ value: "duotone",
3927
+ label: "Duotone"
3928
+ }, {
3929
+ value: "palette",
3930
+ label: "Palette"
3931
+ }],
3932
+ default: "duotone"
3933
+ },
3934
+ {
3935
+ key: "dark",
3936
+ label: "Dark",
3937
+ type: "color",
3938
+ default: "#1b1a4a",
3939
+ when: {
3940
+ key: "mode",
3941
+ in: ["duotone"]
3942
+ }
3943
+ },
3944
+ {
3945
+ key: "light",
3946
+ label: "Light",
3947
+ type: "color",
3948
+ default: "#f2c6a0",
3949
+ when: {
3950
+ key: "mode",
3951
+ in: ["duotone"]
3952
+ }
3953
+ },
3954
+ {
3955
+ key: "amount",
3956
+ label: "Amount",
3957
+ min: 0,
3958
+ max: 1,
3959
+ step: .01,
3960
+ default: 1
3961
+ }
3962
+ ],
3963
+ randomParams(rand) {
3964
+ return {
3965
+ mode: rand() < .5 ? "duotone" : "palette",
3966
+ dark: "#1b1a4a",
3967
+ light: "#f2c6a0",
3968
+ amount: .6 + rand() * .4
3969
+ };
3970
+ }
3971
+ };
3972
+
3576
3973
  //#endregion
3577
3974
  //#region src/registry.ts
3578
3975
  const shaders = [
@@ -3598,7 +3995,8 @@ const effects = [
3598
3995
  pixelate,
3599
3996
  dither,
3600
3997
  halftone,
3601
- ascii
3998
+ ascii,
3999
+ tint
3602
4000
  ];
3603
4001
  function getEffect(id) {
3604
4002
  return effects.find((e) => e.id === id);
@@ -4196,6 +4594,41 @@ function resolveParams(def, overrides) {
4196
4594
 
4197
4595
  //#endregion
4198
4596
  //#region src/stack.ts
4597
+ /** Backdrop blur, pass 1: box-average the frame down by k (4x4 LINEAR taps
4598
+ * spread over one small texel). */
4599
+ const BLUR_DOWN_FRAGMENT = `
4600
+ uniform sampler2D u_source;
4601
+ uniform vec2 u_span;
4602
+ void main() {
4603
+ vec3 acc = vec3(0.0);
4604
+ for (int j = 0; j < 4; j++) {
4605
+ for (int i = 0; i < 4; i++) {
4606
+ vec2 o = ((vec2(float(i), float(j)) + 0.5) / 4.0 - 0.5) * u_span;
4607
+ acc += toLinear(texture2D(u_source, v_uv + o).rgb);
4608
+ }
4609
+ }
4610
+ gl_FragColor = vec4(toSrgb(acc / 16.0), 1.0);
4611
+ }
4612
+ `;
4613
+ /** Backdrop blur, passes 2 and 3: one axis of a 25-tap Gaussian, in linear
4614
+ * light. Sigma is at most 4 small texels (below 2 only when blurPx < 2),
4615
+ * so +-12 taps reach 3 sigma. */
4616
+ const BLUR_PASS_FRAGMENT = `
4617
+ uniform sampler2D u_source;
4618
+ uniform vec2 u_step;
4619
+ uniform float u_sigma;
4620
+ void main() {
4621
+ vec3 acc = vec3(0.0);
4622
+ float wsum = 0.0;
4623
+ for (int i = -12; i <= 12; i++) {
4624
+ float x = float(i);
4625
+ float w = exp(-0.5 * x * x / (u_sigma * u_sigma));
4626
+ acc += w * toLinear(texture2D(u_source, v_uv + x * u_step).rgb);
4627
+ wsum += w;
4628
+ }
4629
+ gl_FragColor = vec4(toSrgb(acc / wsum), 1.0);
4630
+ }
4631
+ `;
4199
4632
  const QUAD_VERTICES = new Float32Array([
4200
4633
  -1,
4201
4634
  -1,
@@ -4447,6 +4880,69 @@ function createStackRenderer(opts) {
4447
4880
  }
4448
4881
  cellTargetsUsed.clear();
4449
4882
  }
4883
+ const blurTargets = /* @__PURE__ */ new Map();
4884
+ const blurTargetsUsed = /* @__PURE__ */ new Set();
4885
+ let blurDownProgram = null;
4886
+ let blurPassProgram = null;
4887
+ /**
4888
+ * `input` blurred by `blurRef` reference px. Done at 1/k resolution with
4889
+ * k chosen so sigma is at most 4 small texels (below 2 only when blurPx
4890
+ * < 2, where k floors to 1): the cost is the same at every output size,
4891
+ * and the small target is ~2 * ref / blur texels whatever the output
4892
+ * scale, so preview and export blur alike.
4893
+ */
4894
+ function blurred(input, blurRef) {
4895
+ const blurPx = blurRef * outputScale(canvas.width, canvas.height);
4896
+ const k = Math.max(1, Math.floor(blurPx / 2));
4897
+ const w = Math.max(1, Math.round(canvas.width / k));
4898
+ const h = Math.max(1, Math.round(canvas.height / k));
4899
+ const key = `${w}x${h}`;
4900
+ blurTargetsUsed.add(key);
4901
+ let pair = blurTargets.get(key);
4902
+ if (!pair) {
4903
+ pair = [createTarget(gl, w, h, "linear"), createTarget(gl, w, h, "linear")];
4904
+ blurTargets.set(key, pair);
4905
+ }
4906
+ const [a, b] = pair;
4907
+ blurDownProgram ??= createProgram(gl, VERTEX_SHADER, BASE_UNIFORMS + COLOR_MATH + BLUR_DOWN_FRAGMENT);
4908
+ blurPassProgram ??= createProgram(gl, VERTEX_SHADER, BASE_UNIFORMS + COLOR_MATH + BLUR_PASS_FRAGMENT);
4909
+ bindOutput(a);
4910
+ useProgram(blurDownProgram, [w, h]);
4911
+ bindTexture(1, input.texture);
4912
+ gl.uniform1i(blurDownProgram.loc("u_source"), 1);
4913
+ gl.uniform2f(blurDownProgram.loc("u_span"), k / canvas.width, k / canvas.height);
4914
+ draw();
4915
+ const passes = [[
4916
+ a,
4917
+ b,
4918
+ 1 / w,
4919
+ 0
4920
+ ], [
4921
+ b,
4922
+ a,
4923
+ 0,
4924
+ 1 / h
4925
+ ]];
4926
+ for (const [src, dst, dx, dy] of passes) {
4927
+ bindOutput(dst);
4928
+ useProgram(blurPassProgram, [w, h]);
4929
+ bindTexture(1, src.texture);
4930
+ gl.uniform1i(blurPassProgram.loc("u_source"), 1);
4931
+ gl.uniform2f(blurPassProgram.loc("u_step"), dx, dy);
4932
+ gl.uniform1f(blurPassProgram.loc("u_sigma"), blurPx / k);
4933
+ draw();
4934
+ }
4935
+ return a;
4936
+ }
4937
+ function pruneBlurTargets() {
4938
+ for (const [key, pair] of blurTargets) {
4939
+ if (blurTargetsUsed.has(key)) continue;
4940
+ deleteTarget(gl, pair[0]);
4941
+ deleteTarget(gl, pair[1]);
4942
+ blurTargets.delete(key);
4943
+ }
4944
+ blurTargetsUsed.clear();
4945
+ }
4450
4946
  let timeSec = 0;
4451
4947
  function bindOutput(target) {
4452
4948
  gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.framebuffer : null);
@@ -4639,7 +5135,6 @@ function createStackRenderer(opts) {
4639
5135
  let input = null;
4640
5136
  active.forEach((state, i) => {
4641
5137
  const last = i === active.length - 1;
4642
- const outIndex = input === frameTargets[0] ? 1 : 0;
4643
5138
  const overGenerator = input === null && source.kind === "generator";
4644
5139
  syncTextures(state);
4645
5140
  if (state.def.grid) {
@@ -4647,6 +5142,15 @@ function createStackRenderer(opts) {
4647
5142
  const [cellIn, cellOut] = cellTargetPair(grid);
4648
5143
  if (input === null) renderSourceToCells(grid, cellIn);
4649
5144
  else downsampleToCells(input, grid, cellIn);
5145
+ const pic = state.def.grid.picture;
5146
+ const want = pic === true ? { blur: 0 } : pic ? pic(state.params) : null;
5147
+ if (want && input === null) {
5148
+ input = frameTarget(0);
5149
+ renderSourceToFrame(input);
5150
+ }
5151
+ let picture = want ? input : null;
5152
+ if (picture && want && want.blur > 0) picture = blurred(picture, want.blur);
5153
+ const outIndex = input === frameTargets[0] ? 1 : 0;
4650
5154
  const cellProgram = state.cell;
4651
5155
  bindOutput(cellOut);
4652
5156
  useProgram(cellProgram, frameSize);
@@ -4661,6 +5165,10 @@ function createStackRenderer(opts) {
4661
5165
  applyEffectUniforms(drawProgram, state, grid, overGenerator);
4662
5166
  bindTexture(2, cellOut.texture);
4663
5167
  gl.uniform1i(drawProgram.loc("u_cells"), 2);
5168
+ if (picture) {
5169
+ bindTexture(1, picture.texture);
5170
+ gl.uniform1i(drawProgram.loc("u_picture"), 1);
5171
+ }
4664
5172
  draw();
4665
5173
  input = out;
4666
5174
  } else {
@@ -4680,6 +5188,7 @@ function createStackRenderer(opts) {
4680
5188
  }
4681
5189
  });
4682
5190
  pruneCellTargets();
5191
+ pruneBlurTargets();
4683
5192
  }
4684
5193
  function dispose() {
4685
5194
  for (const state of layers) disposeLayer(state);
@@ -4688,6 +5197,8 @@ function createStackRenderer(opts) {
4688
5197
  mediaFrameProgram?.dispose();
4689
5198
  mediaCellProgram?.dispose();
4690
5199
  downsampleProgram?.dispose();
5200
+ blurDownProgram?.dispose();
5201
+ blurPassProgram?.dispose();
4691
5202
  deleteTarget(gl, frameTargets[0]);
4692
5203
  deleteTarget(gl, frameTargets[1]);
4693
5204
  frameTargets = [null, null];
@@ -4696,6 +5207,11 @@ function createStackRenderer(opts) {
4696
5207
  deleteTarget(gl, pair[1]);
4697
5208
  }
4698
5209
  cellTargets.clear();
5210
+ for (const pair of blurTargets.values()) {
5211
+ deleteTarget(gl, pair[0]);
5212
+ deleteTarget(gl, pair[1]);
5213
+ }
5214
+ blurTargets.clear();
4699
5215
  gl.deleteTexture(mediaTexture);
4700
5216
  gl.deleteTexture(paletteTexture);
4701
5217
  gl.deleteBuffer(quadBuffer);
@@ -5024,4 +5540,4 @@ function createRenderer(opts) {
5024
5540
  }
5025
5541
 
5026
5542
  //#endregion
5027
- export { ascii, beam, bloom, buildPaletteRamp, burst, createRenderer, createStackRenderer, dither, dune, effects, flow, getEffect, getShader, glint, gridFor, halftone, halo, mountGradient, mountStack, nacre, pixelate, renderGradientFrame, renderStackFrame, shaders, silk, strata, whorl };
5543
+ export { ascii, beam, bloom, buildPaletteRamp, burst, createRenderer, createStackRenderer, dither, dune, effects, flow, getEffect, getShader, glint, gridFor, halftone, halo, mountGradient, mountStack, nacre, pixelate, renderGradientFrame, renderStackFrame, shaders, silk, strata, tint, whorl };