instantshader 0.6.1 → 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$6 = `
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$6,
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$5 = `
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$5,
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$4 = `
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$4,
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$3 = `
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$3,
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$2 = `
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$2,
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$1 = `
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$1,
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 = `
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,
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",
@@ -1661,89 +1661,641 @@ const whorl = {
1661
1661
  };
1662
1662
 
1663
1663
  //#endregion
1664
- //#region src/effects/pixelate.ts
1665
- const CELL_FRAGMENT$3 = `
1666
- uniform float u_levels;
1664
+ //#region src/shaders/silk.ts
1665
+ const FRAGMENT$4 = `
1666
+ uniform float u_scale;
1667
+ uniform float u_folds;
1668
+ uniform float u_depth;
1669
+ uniform float u_warp;
1670
+ uniform float u_sheen;
1671
+ uniform float u_light;
1672
+ uniform float u_grain;
1673
+
1674
+ ${SIMPLEX_2D}
1675
+ ${FBM}
1676
+ ${SHAPE}
1677
+ ${GRAIN}
1678
+ ${ISO}
1679
+ ${SEED}
1680
+
1681
+ const float PI = 3.14159265;
1682
+
1683
+ vec2 toField(vec2 v, vec2 axis) {
1684
+ return vec2(dot(v, axis) / (1.0 + 4.0 * u_folds), dot(v, vec2(-axis.y, axis.x)));
1685
+ }
1686
+
1687
+ // Fold height at frame point p. The warp is passed in, computed once at the
1688
+ // pixel: it varies over ~2 frame units, the taps are 0.006 apart.
1689
+ float height(vec2 p, vec2 axis, float freq, vec2 so, vec2 drift, float warp) {
1690
+ vec2 q = toField(p, axis) * freq + so + drift;
1691
+ q.y += warp;
1692
+ return fbm2(q);
1693
+ }
1667
1694
 
1668
1695
  void main() {
1669
- vec3 c = cellSource().rgb;
1670
- // Posterize per channel. Rounds to the nearest level, so black and white
1671
- // stay put. levels = 1 would divide by zero and means nothing; 0 is "off".
1672
- if (u_levels >= 2.0) {
1673
- float n = u_levels - 1.0;
1674
- c = floor(c * n + 0.5) / n;
1675
- }
1676
- gl_FragColor = vec4(c, 1.0);
1696
+ vec2 uv = worldUv();
1697
+ vec2 p = isoCoord(uv);
1698
+
1699
+ vec2 so = seedOffset();
1700
+ float axisAngle = seedHash(3.0) * PI;
1701
+ vec2 axis = vec2(cos(axisAngle), sin(axisAngle));
1702
+ float freq = 1.6 * u_scale;
1703
+
1704
+ vec2 drift = loopDrift(0.025, vec2(1.0, 0.0));
1705
+ // Warp: one coarse octave, shifts the across-fold coordinate so ridges
1706
+ // bend, converge and fork.
1707
+ float warp = u_warp * 1.4 * snoise(toField(p, axis) * (0.45 * freq) + so.yx + 31.0 + drift.yx);
1708
+
1709
+ float eps = 0.006;
1710
+ float hx1 = height(p + vec2(eps, 0.0), axis, freq, so, drift, warp);
1711
+ float hx0 = height(p - vec2(eps, 0.0), axis, freq, so, drift, warp);
1712
+ float hy1 = height(p + vec2(0.0, eps), axis, freq, so, drift, warp);
1713
+ float hy0 = height(p - vec2(0.0, eps), axis, freq, so, drift, warp);
1714
+ float h = 0.25 * (hx1 + hx0 + hy1 + hy0);
1715
+
1716
+ // Slope scale: fbm2 spans ~[-1, 1] over ~1/freq frame units, so its raw
1717
+ // gradient is O(freq); 0.35 * depth brings the default to slopes near 1.
1718
+ float amp = 0.35 * u_depth / eps;
1719
+ vec3 n = normalize(vec3(-(hx1 - hx0) * amp * 0.5, -(hy1 - hy0) * amp * 0.5, 1.0));
1720
+
1721
+ // Light: azimuth measured from the ACROSS-fold direction, so u_light = 0
1722
+ // always rakes across the folds whatever axis the seed picked (a light
1723
+ // along the folds shows no relief at all). Slow sway; elevation 40.
1724
+ float az = axisAngle + 0.5 * PI + u_light * PI / 180.0 + 0.14 * sin(loopFreq(0.12) * u_time + u_seed);
1725
+ vec3 L = vec3(cos(az) * 0.766, sin(az) * 0.766, 0.643);
1726
+ vec3 V = vec3(0.0, 0.0, 1.0);
1727
+ vec3 H = normalize(L + V);
1728
+ vec3 T = normalize(vec3(axis, 0.0) - n * dot(vec3(axis, 0.0), n));
1729
+
1730
+ float ndl = dot(n, L);
1731
+ float diff = 0.5 + 0.5 * ndl;
1732
+ float th = sqrt(max(1.0 - dot(T, H) * dot(T, H), 0.0));
1733
+ float sheen = (0.55 * pow(th, 48.0) + 0.3 * pow(th, 7.0)) * max(ndl, 0.0);
1734
+ // Valleys sit in shadow from the folds above them.
1735
+ float cav = 0.75 + 0.25 * (0.5 + 0.5 * h);
1736
+
1737
+ // Colour: its own coarse field, drifting on its own path.
1738
+ vec2 drift2 = loopDrift(0.02, vec2(0.0, 1.0));
1739
+ float t = spread(fbm2(p * (0.55 * u_scale) + so + 17.0 + drift2), 0.35);
1740
+ vec3 cloth = palette(t);
1741
+
1742
+ vec3 color = cloth * (0.35 + 0.65 * diff) * cav;
1743
+ color += mix(cloth, vec3(1.0), 0.5) * sheen * u_sheen;
1744
+
1745
+ float g = grain(uv, u_time) - 0.5;
1746
+ color += g * u_grain;
1747
+
1748
+ gl_FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
1677
1749
  }
1678
1750
  `;
1679
- const DRAW_FRAGMENT$2 = `
1680
- uniform float u_gap;
1681
- uniform vec3 u_gapColor;
1751
+ const silk = {
1752
+ id: "silk",
1753
+ label: "Silk",
1754
+ fragment: FRAGMENT$4,
1755
+ params: [
1756
+ {
1757
+ key: "scale",
1758
+ label: "Scale",
1759
+ min: .3,
1760
+ max: 2.5,
1761
+ step: .05,
1762
+ default: 1
1763
+ },
1764
+ {
1765
+ key: "folds",
1766
+ label: "Folds",
1767
+ min: 0,
1768
+ max: 1,
1769
+ step: .01,
1770
+ default: .6
1771
+ },
1772
+ {
1773
+ key: "depth",
1774
+ label: "Depth",
1775
+ min: .2,
1776
+ max: 2,
1777
+ step: .01,
1778
+ default: 1
1779
+ },
1780
+ {
1781
+ key: "warp",
1782
+ label: "Warp",
1783
+ min: 0,
1784
+ max: 1,
1785
+ step: .01,
1786
+ default: .5
1787
+ },
1788
+ {
1789
+ key: "sheen",
1790
+ label: "Sheen",
1791
+ min: 0,
1792
+ max: 1,
1793
+ step: .01,
1794
+ default: .6
1795
+ },
1796
+ {
1797
+ key: "light",
1798
+ label: "Light",
1799
+ min: 0,
1800
+ max: 360,
1801
+ step: 1,
1802
+ default: 20
1803
+ },
1804
+ {
1805
+ key: "grain",
1806
+ label: "Grain",
1807
+ min: 0,
1808
+ max: .5,
1809
+ step: .01,
1810
+ default: .06
1811
+ }
1812
+ ],
1813
+ randomParams(rand) {
1814
+ return {
1815
+ scale: .7 + rand() * 1.1,
1816
+ folds: .3 + rand() * .7,
1817
+ depth: .6 + rand() * 1,
1818
+ warp: rand(),
1819
+ sheen: .3 + rand() * .6000000000000001,
1820
+ light: Math.floor((rand() < .5 ? 0 : 180) + (rand() - .5) * 100 + 360) % 360,
1821
+ grain: .06
1822
+ };
1823
+ }
1824
+ };
1825
+
1826
+ //#endregion
1827
+ //#region src/shaders/nacre.ts
1828
+ const FRAGMENT$3 = `
1829
+ uniform float u_scale;
1830
+ uniform float u_flow;
1831
+ uniform float u_crease;
1832
+ uniform float u_depth;
1833
+ uniform float u_iridescence;
1834
+ uniform float u_light;
1835
+ uniform float u_grain;
1836
+
1837
+ ${SIMPLEX_2D}
1838
+ ${FBM}
1839
+ ${SHAPE}
1840
+ ${GRAIN}
1841
+ ${ISO}
1842
+ ${SEED}
1843
+
1844
+ const float PI = 3.14159265;
1845
+
1846
+ float tri(float x) {
1847
+ return 1.0 - abs(1.0 - 2.0 * fract(0.5 * x));
1848
+ }
1849
+
1850
+ // One smooth octave with a faint second: pooled lobes, not texture. The
1851
+ // creases are narrow ridges where the base field crosses a level, so they
1852
+ // are contours of the lobes themselves: they hug each lobe's edge and, as
1853
+ // level sets of one field, never cross each other or cut through a lobe.
1854
+ float height(vec2 p, float f, vec2 so, vec2 drift, vec2 warp) {
1855
+ vec2 q = (p + warp) * f + so + drift;
1856
+ float base = snoise(q) + 0.18 * snoise(q * 2.1 + 5.0);
1857
+ float d0 = base;
1858
+ float d1 = abs(base) - 0.6;
1859
+ float crease = exp(-(d0 * d0) / 0.012) + 0.6 * exp(-(d1 * d1) / 0.01);
1860
+ return base * 0.7 + crease * u_crease * 0.3;
1861
+ }
1682
1862
 
1683
1863
  void main() {
1684
- vec3 color = coverage();
1685
- if (u_gap > 0.0) {
1686
- // A gap line is u_gap of a cell wide, centred on every cell boundary.
1687
- // Its weight is the exact share of this output pixel it covers, and the
1688
- // blend happens in linear light, for the same reason coverage() works
1689
- // that way: a smoothstep edge in gamma space draws sub-pixel lines far
1690
- // too heavy, so a small preview would look darker than its own export.
1691
- vec2 px = cellPx();
1692
- vec2 d = min(cellLocal(), 1.0 - cellLocal()) * px; // px to nearest boundary
1693
- vec2 hw = 0.5 * u_gap * px; // line half-width in px
1694
- vec2 cov = clamp(min(hw, d + 0.5) - max(-hw, d - 0.5), 0.0, 1.0);
1695
- float m = 1.0 - (1.0 - cov.x) * (1.0 - cov.y);
1696
- color = toSrgb(mix(toLinear(color), toLinear(u_gapColor), m));
1864
+ vec2 uv = worldUv();
1865
+ vec2 p = isoCoord(uv);
1866
+
1867
+ vec2 so = seedOffset();
1868
+ float f = 0.55 * u_scale;
1869
+ vec2 drift = loopDrift(0.02, vec2(1.0, 0.0));
1870
+
1871
+ // One warp for every tap: it varies over ~2 frame units, the taps are
1872
+ // 0.006 apart.
1873
+ vec2 wq = p * (0.45 * f) + so.yx + 23.0 + drift.yx;
1874
+ vec2 warp = u_flow * 0.5 * vec2(snoise(wq), snoise(wq + vec2(9.2, 4.1)));
1875
+
1876
+ float eps = 0.006;
1877
+ float hx1 = height(p + vec2(eps, 0.0), f, so, drift, warp);
1878
+ float hx0 = height(p - vec2(eps, 0.0), f, so, drift, warp);
1879
+ float hy1 = height(p + vec2(0.0, eps), f, so, drift, warp);
1880
+ float hy0 = height(p - vec2(0.0, eps), f, so, drift, warp);
1881
+
1882
+ float amp = 0.5 * u_depth / eps;
1883
+ vec3 n = normalize(vec3(-(hx1 - hx0) * amp * 0.5, -(hy1 - hy0) * amp * 0.5, 1.0));
1884
+ float tilt = 1.0 - n.z;
1885
+
1886
+ float az = u_light * PI / 180.0 + 0.12 * sin(loopFreq(0.1) * u_time + u_seed);
1887
+ vec3 L = vec3(cos(az) * 0.643, sin(az) * 0.643, 0.766);
1888
+ float diff = 0.7 + 0.3 * dot(n, L);
1889
+ float rim = pow(tilt, 6.0) * 0.35;
1890
+
1891
+ vec2 drift2 = loopDrift(0.015, vec2(0.0, 1.0));
1892
+ float t0 = spread(fbm2(p * (0.4 * f) + so + 17.0 + drift2), 0.35);
1893
+ float t = tri(t0 + u_iridescence * 1.5 * tilt);
1894
+ vec3 color = palette(t) * diff + vec3(rim);
1895
+
1896
+ float g = grain(uv, u_time) - 0.5;
1897
+ color += g * u_grain;
1898
+
1899
+ gl_FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
1900
+ }
1901
+ `;
1902
+ const nacre = {
1903
+ id: "nacre",
1904
+ label: "Nacre",
1905
+ fragment: FRAGMENT$3,
1906
+ params: [
1907
+ {
1908
+ key: "scale",
1909
+ label: "Scale",
1910
+ min: .4,
1911
+ max: 2,
1912
+ step: .05,
1913
+ default: .9
1914
+ },
1915
+ {
1916
+ key: "flow",
1917
+ label: "Flow",
1918
+ min: 0,
1919
+ max: 1,
1920
+ step: .01,
1921
+ default: .5
1922
+ },
1923
+ {
1924
+ key: "crease",
1925
+ label: "Crease",
1926
+ min: 0,
1927
+ max: 1,
1928
+ step: .01,
1929
+ default: .6
1930
+ },
1931
+ {
1932
+ key: "depth",
1933
+ label: "Depth",
1934
+ min: .2,
1935
+ max: 2,
1936
+ step: .01,
1937
+ default: 1
1938
+ },
1939
+ {
1940
+ key: "iridescence",
1941
+ label: "Iridescence",
1942
+ min: 0,
1943
+ max: 1,
1944
+ step: .01,
1945
+ default: .6
1946
+ },
1947
+ {
1948
+ key: "light",
1949
+ label: "Light",
1950
+ min: 0,
1951
+ max: 360,
1952
+ step: 1,
1953
+ default: 40
1954
+ },
1955
+ {
1956
+ key: "grain",
1957
+ label: "Grain",
1958
+ min: 0,
1959
+ max: .3,
1960
+ step: .01,
1961
+ default: .04
1962
+ }
1963
+ ],
1964
+ randomParams(rand) {
1965
+ return {
1966
+ scale: .5 + rand() * 1.1,
1967
+ flow: rand(),
1968
+ crease: .2 + rand() * .8,
1969
+ depth: .6 + rand() * 1,
1970
+ iridescence: .3 + rand() * .7,
1971
+ light: Math.floor(rand() * 360),
1972
+ grain: .04
1973
+ };
1974
+ }
1975
+ };
1976
+
1977
+ //#endregion
1978
+ //#region src/shaders/burst.ts
1979
+ const FRAGMENT$2 = `
1980
+ uniform float u_rays;
1981
+ uniform float u_sharp;
1982
+ uniform float u_glow;
1983
+ uniform float u_twist;
1984
+ uniform float u_x;
1985
+ uniform float u_y;
1986
+ uniform float u_grain;
1987
+
1988
+ ${PERIODIC_2D}
1989
+ ${GRAIN}
1990
+ ${ISO}
1991
+ ${SEED}
1992
+
1993
+ void main() {
1994
+ vec2 uv = worldUv();
1995
+ vec2 p = isoCoord(uv);
1996
+ vec2 halfIso = isoHalf();
1997
+
1998
+ vec2 c = vec2(u_x, u_y) * halfIso;
1999
+ vec2 rel = p - c;
2000
+ float r = max(length(rel), 0.0005);
2001
+ float theta = atan(rel.y, rel.x);
2002
+ float rho = log(r / 0.1) * 0.6;
2003
+ float th = theta + u_twist * 1.2 * rho;
2004
+
2005
+ // Ray count: the angular period of the noise, integral so the field
2006
+ // closes on itself around the centre.
2007
+ float A = floor(8.0 + 72.0 * u_rays);
2008
+ vec2 so = seedOffset();
2009
+ float flowOut = loopTravel(0.05, vec2(0.0, 1.0), 4.0).y;
2010
+ float flowPk = loopTravel(0.2, vec2(0.0, 1.0), 12.0).y;
2011
+
2012
+ vec2 sp = vec2(th / TAU * A, rho - flowOut + so.x);
2013
+ float rays = pnoise(sp, vec2(A, 4.0)) + 0.5 * pnoise(sp * 2.0 + vec2(0.0, so.y), vec2(2.0 * A, 8.0));
2014
+ // Packets: shorter along the ray and streaming out four times faster.
2015
+ float pk = pnoise(vec2(sp.x, rho * 3.0 - flowPk + so.y), vec2(A, 12.0));
2016
+ // Sector field: a few broad angular sectors that keep neighbouring rays
2017
+ // in one family, and the mask's contrast.
2018
+ float sector = pnoise(vec2(th / TAU * 6.0 + seedHash(3.0) * 6.0, 0.5), vec2(6.0, 1024.0));
2019
+ // Softer toward the centre, where the rays converge.
2020
+ float edge = mix(0.8, 0.2, u_sharp) + 0.4 * exp(-r / 0.3);
2021
+ float mask = smoothstep(-edge, edge, rays + 0.4 * pk + 0.3 * sector);
2022
+
2023
+ float hole = 1.0 - exp(-(r * r) / (0.25 * 0.25));
2024
+ float env = mix(hole, 1.0 + 0.6 * exp(-r / 0.35), u_glow) * exp(-0.3 * r);
2025
+ float hot = u_glow * exp(-(r * r) / (0.16 * 0.16));
2026
+
2027
+ float sway = 0.35 * sin(loopFreq(0.08) * u_time + u_seed);
2028
+ float theta0 = seedHash(2.0) * TAU + sway;
2029
+ float t = 0.5 + 0.5 * cos(th - theta0 + 2.0 * rays + 1.5 * sector);
2030
+ vec3 tint = palette(t);
2031
+ vec3 color = tint * (0.15 + 0.85 * mask * env);
2032
+ color = mix(color, vec3(1.0), hot * 0.85);
2033
+
2034
+ float g = grain(uv, u_time) - 0.5;
2035
+ color += g * u_grain;
2036
+
2037
+ gl_FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
2038
+ }
2039
+ `;
2040
+ const burst = {
2041
+ id: "burst",
2042
+ label: "Burst",
2043
+ fragment: FRAGMENT$2,
2044
+ params: [
2045
+ {
2046
+ key: "rays",
2047
+ label: "Rays",
2048
+ min: 0,
2049
+ max: 1,
2050
+ step: .01,
2051
+ default: .45
2052
+ },
2053
+ {
2054
+ key: "sharp",
2055
+ label: "Sharp",
2056
+ min: 0,
2057
+ max: 1,
2058
+ step: .01,
2059
+ default: .6
2060
+ },
2061
+ {
2062
+ key: "glow",
2063
+ label: "Glow",
2064
+ min: 0,
2065
+ max: 1,
2066
+ step: .01,
2067
+ default: .7
2068
+ },
2069
+ {
2070
+ key: "twist",
2071
+ label: "Twist",
2072
+ min: 0,
2073
+ max: 1,
2074
+ step: .01,
2075
+ default: .2
2076
+ },
2077
+ {
2078
+ key: "x",
2079
+ label: "X",
2080
+ min: -1,
2081
+ max: 1,
2082
+ step: .01,
2083
+ default: .1
2084
+ },
2085
+ {
2086
+ key: "y",
2087
+ label: "Y",
2088
+ min: -1,
2089
+ max: 1,
2090
+ step: .01,
2091
+ default: .05
2092
+ },
2093
+ {
2094
+ key: "grain",
2095
+ label: "Grain",
2096
+ min: 0,
2097
+ max: .3,
2098
+ step: .01,
2099
+ default: .14
2100
+ }
2101
+ ],
2102
+ randomParams(rand) {
2103
+ return {
2104
+ rays: .2 + rand() * .7,
2105
+ sharp: .3 + rand() * .7,
2106
+ glow: .3 + rand() * .7,
2107
+ twist: rand() * .7,
2108
+ x: -.7 + rand() * 1.4,
2109
+ y: -.7 + rand() * 1.4,
2110
+ grain: .14
2111
+ };
2112
+ }
2113
+ };
2114
+
2115
+ //#endregion
2116
+ //#region src/shaders/glint.ts
2117
+ const FRAGMENT$1 = `
2118
+ uniform float u_size;
2119
+ uniform float u_density;
2120
+ uniform float u_bend;
2121
+ uniform float u_angle;
2122
+ uniform float u_speed;
2123
+ uniform float u_grain;
2124
+
2125
+ ${SIMPLEX_2D}
2126
+ ${GRAIN}
2127
+ ${ISO}
2128
+ ${LOOP_ANGLE}
2129
+ ${SEED}
2130
+
2131
+ const float PI = 3.14159265;
2132
+ const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);
2133
+
2134
+ float hash21(vec2 c) {
2135
+ return fract(sin(dot(c, vec2(127.1, 311.7)) + u_seed * 0.37) * 43758.5453);
2136
+ }
2137
+
2138
+ // Rhombus coverage at local offset d for half-extents a (along) and b
2139
+ // (across), edge ramped over aa.
2140
+ float rhombus(vec2 d, float a, float b, float aa) {
2141
+ float s = abs(d.x) / a + abs(d.y) / b - 1.0;
2142
+ return 1.0 - smoothstep(-aa, aa, s);
2143
+ }
2144
+
2145
+ void main() {
2146
+ vec2 uv = worldUv();
2147
+ vec2 p = isoCoord(uv);
2148
+ vec2 halfIso = isoHalf();
2149
+ float pixel = 2.0 * halfIso.y / u_resolution.y;
2150
+
2151
+ float ang = u_angle * PI / 180.0;
2152
+ vec2 q = vec2(p.x * cos(ang) + p.y * sin(ang), -p.x * sin(ang) + p.y * cos(ang));
2153
+ vec2 so = seedOffset();
2154
+ // The current: one slow bend, so the shoal arcs across the frame.
2155
+ float bend = 0.35 * u_bend * snoise(vec2(q.x * 0.9 + so.x, so.y));
2156
+ float across = q.y - bend;
2157
+
2158
+ // Background and haze.
2159
+ vec3 c0 = palette(0.0);
2160
+ vec3 c1 = palette(0.5);
2161
+ vec3 c2 = palette(1.0);
2162
+ float l0 = dot(c0, LUMA);
2163
+ float l1 = dot(c1, LUMA);
2164
+ float l2 = dot(c2, LUMA);
2165
+ vec3 dk = mix(c0, c1, step(l1, l0));
2166
+ dk = mix(dk, c2, step(l2, min(l0, l1)));
2167
+ vec3 color = dk * 0.1;
2168
+ color += palette(0.5) * 0.05 * exp(-(across * across) / 0.1);
2169
+
2170
+ float tw = loopFreq(1.2) * u_time;
2171
+ // Hash period along the stream, in cells. The lattice travels one period
2172
+ // per loop, so a short loop would race; halving the period halves that at
2173
+ // the cost of the pattern repeating every 8 cells instead of 16.
2174
+ float TILE = (u_loop > 0.0 && u_loop < 12.0) ? 8.0 : 16.0;
2175
+
2176
+ for (int layer = 0; layer < 3; layer++) {
2177
+ float fl = float(layer);
2178
+ float sizeMul = fl < 0.5 ? 0.5 : (fl < 1.5 ? 0.85 : 1.5);
2179
+ float speedMul = fl < 0.5 ? 0.5 : (fl < 1.5 ? 0.8 : 1.2);
2180
+ float bright = fl < 0.5 ? 0.45 : (fl < 1.5 ? 0.7 : 1.0);
2181
+ float soft = fl < 0.5 ? 2.5 : (fl < 1.5 ? 1.5 : 1.0);
2182
+
2183
+ float shard = u_size * sizeMul;
2184
+ float cs = shard * 3.2;
2185
+ // The static u_speed term gives the knob a visible effect at t = 0.
2186
+ float travel = loopTravel(0.08 * u_speed * speedMul, vec2(1.0, 0.0), cs * TILE).x + 0.3 * u_speed;
2187
+ vec2 l = vec2(q.x + travel, across) / cs;
2188
+ vec2 cell0 = floor(l);
2189
+
2190
+ for (int j = -1; j <= 1; j++) {
2191
+ for (int i = -1; i <= 1; i++) {
2192
+ vec2 cell = cell0 + vec2(float(i), float(j));
2193
+ vec2 key = vec2(mod(cell.x, TILE), cell.y + fl * 100.0);
2194
+ float h1 = hash21(key);
2195
+ float h2 = hash21(key + 7.0);
2196
+ float h3 = hash21(key + 19.0);
2197
+ float h4 = hash21(key + 31.0);
2198
+ float h5 = hash21(key + 47.0);
2199
+
2200
+ // Denser along the current, sparser off it.
2201
+ float rowY = (cell.y + 0.5) * cs;
2202
+ float present = step(h1, u_density * (0.12 + 0.88 * exp(-(rowY * rowY) / 0.05)));
2203
+ if (present < 0.5) continue;
2204
+
2205
+ vec2 centre = (cell + vec2(0.15 + 0.7 * h2, 0.15 + 0.7 * h3)) * cs;
2206
+ float sz = shard * (0.4 + 0.6 * h4);
2207
+ float dir = h5 < 0.5 ? -1.0 : 1.0;
2208
+ float ori = h5 * TAU + dir * loopAngle(0.25 + 0.5 * h4);
2209
+ float ca = cos(ori);
2210
+ float sa = sin(ori);
2211
+
2212
+ vec2 d = vec2(l.x * cs, across) - centre;
2213
+ vec2 dl = vec2(d.x * ca + d.y * sa, -d.x * sa + d.y * ca);
2214
+ float aa = pixel * soft / sz;
2215
+ float fr = 0.08 * sz;
2216
+ float covR = rhombus(vec2((d.x - fr) * ca + d.y * sa, -(d.x - fr) * sa + d.y * ca) / sz, 1.0, 0.35, aa);
2217
+ float covG = rhombus(dl / sz, 1.0, 0.35, aa);
2218
+ float covB = rhombus(vec2((d.x + fr) * ca + d.y * sa, -(d.x + fr) * sa + d.y * ca) / sz, 1.0, 0.35, aa);
2219
+
2220
+ float twinkle = 0.5 + 0.5 * sin(tw + h2 * TAU + ori * 2.0);
2221
+ float flash = twinkle * twinkle * twinkle;
2222
+ vec3 col = palette(fract(h3 + 0.13 * fl));
2223
+ float lum = dot(col, LUMA);
2224
+ col = col * (1.0 + 2.0 * (1.0 - lum) * (1.0 - lum)) * (0.5 + 0.5 * twinkle) + vec3(0.6) * flash;
2225
+ col *= bright;
2226
+ color += vec3(covR, covG, covB) * col;
2227
+ }
2228
+ }
1697
2229
  }
1698
- gl_FragColor = vec4(color, 1.0);
2230
+
2231
+ float g = grain(uv, u_time) - 0.5;
2232
+ color += g * u_grain;
2233
+
2234
+ gl_FragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
1699
2235
  }
1700
2236
  `;
1701
- const pixelate = {
1702
- id: "pixelate",
1703
- label: "Pixelate",
1704
- fragment: DRAW_FRAGMENT$2,
1705
- grid: {
1706
- cell: (p) => [p.size, p.size],
1707
- fragment: CELL_FRAGMENT$3
1708
- },
2237
+ const glint = {
2238
+ id: "glint",
2239
+ label: "Glint",
2240
+ fragment: FRAGMENT$1,
1709
2241
  params: [
1710
2242
  {
1711
2243
  key: "size",
1712
2244
  label: "Size",
1713
- min: 2,
1714
- max: 160,
1715
- step: 1,
1716
- default: 24
2245
+ min: .01,
2246
+ max: .06,
2247
+ step: .001,
2248
+ default: .03
1717
2249
  },
1718
2250
  {
1719
- key: "levels",
1720
- label: "Levels",
2251
+ key: "density",
2252
+ label: "Density",
2253
+ min: .2,
2254
+ max: 1,
2255
+ step: .01,
2256
+ default: .6
2257
+ },
2258
+ {
2259
+ key: "bend",
2260
+ label: "Bend",
1721
2261
  min: 0,
1722
- max: 16,
1723
- step: 1,
1724
- default: 0
2262
+ max: 1,
2263
+ step: .01,
2264
+ default: .5
1725
2265
  },
1726
2266
  {
1727
- key: "gap",
1728
- label: "Gap",
2267
+ key: "angle",
2268
+ label: "Angle",
1729
2269
  min: 0,
1730
- max: .4,
2270
+ max: 360,
2271
+ step: 1,
2272
+ default: 10
2273
+ },
2274
+ {
2275
+ key: "speed",
2276
+ label: "Speed",
2277
+ min: .3,
2278
+ max: 2,
1731
2279
  step: .01,
1732
- default: 0
2280
+ default: 1
1733
2281
  },
1734
2282
  {
1735
- key: "gapColor",
1736
- label: "Gap color",
1737
- type: "color",
1738
- default: "#000000"
2283
+ key: "grain",
2284
+ label: "Grain",
2285
+ min: 0,
2286
+ max: .3,
2287
+ step: .01,
2288
+ default: .06
1739
2289
  }
1740
2290
  ],
1741
2291
  randomParams(rand) {
1742
2292
  return {
1743
- size: Math.round(10 + rand() * 50),
1744
- levels: rand() < .5 ? 0 : Math.round(3 + rand() * 5),
1745
- gap: rand() < .6 ? 0 : .05 + rand() * .15,
1746
- gapColor: "#000000"
2293
+ size: .015 + rand() * .03,
2294
+ density: .4 + rand() * .6,
2295
+ bend: rand(),
2296
+ angle: Math.floor(rand() * 360),
2297
+ speed: .6 + rand() * 1,
2298
+ grain: .06
1747
2299
  };
1748
2300
  }
1749
2301
  };
@@ -1913,6 +2465,13 @@ void main() {
1913
2465
  * over another effect, there is no ramp coordinate to recover and tone stays
1914
2466
  * luma (a gradient map).
1915
2467
  *
2468
+ * paletteStop() snaps a tone to the nearest of the palette's stops
2469
+ * (u_paletteStops is the stop count). Halftone and ASCII use it in palette
2470
+ * mode: they do not quantize tone the way Dither's `levels` does, so
2471
+ * without the snap, palette mode over a generator handed back the exact
2472
+ * source color and looked identical to source mode. Snapped, each cell is
2473
+ * one of the palette's own colors, flat.
2474
+ *
1916
2475
  * The search is 33 coarse + 17 fine ramp samples. That is affordable
1917
2476
  * because tone is evaluated per cell or per lattice point, not per pixel.
1918
2477
  * The ends snap to exactly 0 and 1: generators clamp their coordinate, so
@@ -1925,6 +2484,7 @@ uniform vec3 u_ink;
1925
2484
  uniform vec3 u_paper;
1926
2485
  uniform float u_invert;
1927
2486
  uniform float u_sourceIsRamp; // 1 when this layer reads a generator directly
2487
+ uniform float u_paletteStops; // number of colors in the palette
1928
2488
  float rampPosition(vec3 c) {
1929
2489
  float best = 0.0;
1930
2490
  float bd = 1e9;
@@ -1950,14 +2510,17 @@ float toneOf(vec3 c) {
1950
2510
  float t = toneIsRamp() ? rampPosition(c) : luma(c);
1951
2511
  return u_invert > 0.5 ? 1.0 - t : t;
1952
2512
  }
2513
+ vec3 paletteStop(float tone) {
2514
+ float n = max(u_paletteStops - 1.0, 1.0);
2515
+ return palette(floor(tone * n + 0.5) / n);
2516
+ }
1953
2517
  vec3 colorize(float tone, vec3 src) {
1954
2518
  if (u_colorMode < 0.5) return src;
1955
2519
  if (u_colorMode < 1.5) return mix(u_ink, u_paper, tone);
1956
2520
  return palette(tone);
1957
2521
  }
1958
2522
  `;
1959
- /** The param defs that go with COLOR_MODE. `paperAlways` is for effects
1960
- * where paper is a visible ground in every mode (ASCII, Halftone). */
2523
+ /** The param defs that go with COLOR_MODE. */
1961
2524
  function colorModeParams(opts) {
1962
2525
  return [
1963
2526
  {
@@ -1971,7 +2534,7 @@ function colorModeParams(opts) {
1971
2534
  },
1972
2535
  {
1973
2536
  value: "duotone",
1974
- label: "Duotone"
2537
+ label: opts.singleColour ? "Single colour" : "Duotone"
1975
2538
  },
1976
2539
  {
1977
2540
  value: "palette",
@@ -1981,33 +2544,368 @@ function colorModeParams(opts) {
1981
2544
  default: opts.mode
1982
2545
  },
1983
2546
  {
1984
- key: "ink",
1985
- label: "Ink",
1986
- type: "color",
1987
- default: opts.ink ?? "#111111",
2547
+ key: "ink",
2548
+ label: opts.singleColour ? "Colour" : "Ink",
2549
+ type: "color",
2550
+ default: opts.ink ?? "#111111",
2551
+ when: {
2552
+ key: "colorMode",
2553
+ in: ["duotone"]
2554
+ }
2555
+ },
2556
+ {
2557
+ key: "paper",
2558
+ label: "Paper",
2559
+ type: "color",
2560
+ default: opts.paper ?? "#f4f1ea",
2561
+ when: opts.paperWhen ?? {
2562
+ key: "colorMode",
2563
+ in: ["duotone"]
2564
+ }
2565
+ },
2566
+ {
2567
+ key: "invert",
2568
+ label: "Invert",
2569
+ type: "bool",
2570
+ default: false
2571
+ }
2572
+ ];
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,
1988
2752
  when: {
1989
- key: "colorMode",
1990
- in: ["duotone"]
2753
+ key: "ground",
2754
+ in: ["image"]
1991
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"
1992
2773
  },
1993
2774
  {
1994
- key: "paper",
1995
- label: "Paper",
1996
- type: "color",
1997
- default: opts.paper ?? "#f4f1ea",
1998
- ...opts.paperAlways ? {} : { when: {
1999
- key: "colorMode",
2000
- in: ["duotone"]
2001
- } }
2775
+ key: "exposure",
2776
+ label: "Exposure",
2777
+ min: -1,
2778
+ max: 1,
2779
+ step: .01,
2780
+ default: 0
2002
2781
  },
2003
2782
  {
2004
- key: "invert",
2005
- label: "Invert",
2006
- type: "bool",
2007
- default: false
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
2008
2797
  }
2009
2798
  ];
2010
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
+ };
2011
2909
 
2012
2910
  //#endregion
2013
2911
  //#region src/effects/blueNoise.ts
@@ -2098,12 +2996,23 @@ void main() {
2098
2996
  gl_FragColor = vec4(color, 1.0);
2099
2997
  }
2100
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
+ `;
2101
3008
  const dither = {
2102
3009
  id: "dither",
2103
3010
  label: "Dither",
3011
+ fragment: DRAW_FRAGMENT$2,
2104
3012
  grid: {
2105
3013
  cell: (p) => [p.size, p.size],
2106
- fragment: CELL_FRAGMENT$2
3014
+ fragment: CELL_FRAGMENT$2,
3015
+ picture: filterPicture
2107
3016
  },
2108
3017
  textures: [{
2109
3018
  uniform: "u_noise",
@@ -2181,7 +3090,15 @@ const dither = {
2181
3090
  step: 1,
2182
3091
  default: 0
2183
3092
  },
2184
- ...colorModeParams({ mode: "source" })
3093
+ ...colorModeParams({
3094
+ mode: "duotone",
3095
+ ink: "#000000",
3096
+ paper: "#ffffff"
3097
+ }),
3098
+ ...blendParams({
3099
+ blend: "screen",
3100
+ opacity: .6
3101
+ })
2185
3102
  ],
2186
3103
  randomParams(rand) {
2187
3104
  const patterns = [
@@ -2195,7 +3112,13 @@ const dither = {
2195
3112
  "duotone",
2196
3113
  "palette"
2197
3114
  ];
2198
- return {
3115
+ const blends = [
3116
+ "normal",
3117
+ "screen",
3118
+ "overlay",
3119
+ "softLight"
3120
+ ];
3121
+ const p = {
2199
3122
  pattern: patterns[Math.floor(rand() * patterns.length)],
2200
3123
  size: Math.round(2 + rand() * 6),
2201
3124
  levels: Math.round(2 + rand() * 3),
@@ -2205,8 +3128,15 @@ const dither = {
2205
3128
  colorMode: modes[Math.floor(rand() * modes.length)],
2206
3129
  ink: "#111111",
2207
3130
  paper: "#f4f1ea",
2208
- invert: false
3131
+ invert: false,
3132
+ blend: blends[Math.floor(rand() * blends.length)],
3133
+ opacity: .6 + rand() * .4
2209
3134
  };
3135
+ if (p.blend === "screen" || p.blend === "colorDodge") {
3136
+ p.ink = "#000000";
3137
+ p.paper = "#ffffff";
3138
+ }
3139
+ return p;
2210
3140
  }
2211
3141
  };
2212
3142
 
@@ -2214,24 +3144,27 @@ const dither = {
2214
3144
  //#region src/effects/halftone.ts
2215
3145
  const ROW = .8660254;
2216
3146
  const CELL_FRAGMENT$1 = `
2217
- uniform float u_contrast;
2218
3147
  ${COLOR_MODE}
3148
+ ${RESPONSE}
2219
3149
 
2220
3150
  void main() {
2221
3151
  vec3 sc = cellSource().rgb;
3152
+ float ink = markStrength(sc);
2222
3153
 
2223
- // Ink amount. Halftone sizes its shapes by brightness in every color mode,
2224
- // so this is luma even where toneOf() would return a ramp position.
2225
- float tone = luma(sc);
2226
- if (u_invert > 0.5) tone = 1.0 - tone;
2227
- float ink = clamp((1.0 - tone - 0.5) * u_contrast + 0.5, 0.0, 1.0);
2228
-
2229
- // Ink color. Duotone inks every shape with u_ink (colorize() would mix
2230
- // toward paper by tone, which is dither's meaning of duotone). Palette
2231
- // colors by the un-inverted tone, so Invert flips sizes and leaves colors.
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.
2232
3158
  vec3 color = sc;
2233
- if (u_colorMode > 1.5) color = palette(toneIsRamp() ? rampPosition(sc) : luma(sc));
3159
+ if (u_colorMode > 1.5) color = paletteStop(toneIsRamp() ? rampPosition(sc) : luma(sc));
2234
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
+ }
2235
3168
 
2236
3169
  gl_FragColor = vec4(color, ink);
2237
3170
  }
@@ -2241,8 +3174,10 @@ uniform float u_grid;
2241
3174
  uniform float u_shape;
2242
3175
  uniform float u_radius;
2243
3176
  uniform float u_softness;
2244
- uniform float u_pulse;
2245
3177
  ${COLOR_MODE}
3178
+ ${BLEND}
3179
+ ${COMPOSITE}
3180
+ ${GROUND}
2246
3181
 
2247
3182
  const float ROW = ${ROW};
2248
3183
 
@@ -2251,9 +3186,10 @@ vec4 lattice(vec2 k) {
2251
3186
  return texture2D(u_cells, (kk + 0.5) / u_gridSize);
2252
3187
  }
2253
3188
 
2254
- // The shape covering lattice coordinate g most, blended over paper, in
2255
- // linear light. \`aa\` is the width, in lattice units, edges are ramped over.
2256
- 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) {
2257
3193
  bool hex = u_grid > 0.5;
2258
3194
  bool line = u_shape > 0.5 && u_shape < 1.5;
2259
3195
  // Texel units -> lattice units (1 = the pitch the user set as Size).
@@ -2276,8 +3212,7 @@ vec3 shade(vec2 g, float aa, float w) {
2276
3212
  float kl = stride * floor((g.x - 0.5 - par) / stride) + par;
2277
3213
  float f = (g.x - 0.5 - kl) / stride;
2278
3214
  vec4 v = mix(lattice(vec2(kl, j)), lattice(vec2(kl + stride, j)), f);
2279
- float swell = 1.0 + 0.25 * u_pulse * sin(w * u_time + 0.22 * (j - 0.5 * u_gridSize.y) + u_seed);
2280
- 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);
2281
3216
  float ramp = aa + u_softness * extent;
2282
3217
  float mask = extent <= 0.0 ? 0.0 : clamp((extent - abs(dy)) / ramp + 0.5, 0.0, 1.0);
2283
3218
  if (mask > bestMask) { bestMask = mask; bestInk = v.rgb; }
@@ -2289,20 +3224,23 @@ vec3 shade(vec2 g, float aa, float w) {
2289
3224
  vec2 k = vec2(k0 + stride * float(di), j);
2290
3225
  vec4 v = lattice(k);
2291
3226
  vec2 d = vec2((g.x - (k.x + 0.5)) * unit.x, dy);
2292
- vec2 id = k - 0.5 * u_gridSize;
2293
- float swell = 1.0 + 0.25 * u_pulse * sin(w * u_time + dot(id * unit, vec2(0.35, 0.22)) + u_seed);
2294
-
2295
3227
  float dist;
2296
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);
2297
3235
  if (u_shape < 0.5) {
2298
3236
  // Dot: area pi * rho^2 out of a cell of area 1 (square) or ROW (hex).
2299
3237
  dist = length(d);
2300
- extent = sqrt(v.a * (hex ? ROW : 1.0) / 3.14159265);
3238
+ extent = grow * sqrt((hex ? ROW : 1.0) / 3.14159265);
2301
3239
  } else {
2302
3240
  dist = max(abs(d.x), abs(d.y));
2303
- extent = 0.5 * sqrt(v.a);
3241
+ extent = 0.5 * grow;
2304
3242
  }
2305
- extent = min(extent * u_radius * swell, 1.0);
3243
+ extent = min(extent * u_radius, 1.0);
2306
3244
 
2307
3245
  // Coverage of a straight edge by a box is linear in the distance to
2308
3246
  // it. Softness widens that ramp into a blur.
@@ -2311,22 +3249,12 @@ vec3 shade(vec2 g, float aa, float w) {
2311
3249
  if (mask > bestMask) { bestMask = mask; bestInk = v.rgb; }
2312
3250
  }
2313
3251
  }
2314
- return mix(toLinear(u_paper), toLinear(bestInk), bestMask);
3252
+ return compositeLinear(b, bestInk, bestMask);
2315
3253
  }
2316
3254
 
2317
3255
  void main() {
2318
3256
  float pitchRef = u_grid > 0.5 ? u_cellRef.x * 2.0 : u_cellRef.x;
2319
- // One output pixel, in lattice units.
2320
3257
  float px = 1.0 / (pitchRef * u_outputScale);
2321
-
2322
- // Pulse: every shape swells and shrinks, phase-shifted across the lattice
2323
- // so the swell travels through the frame instead of throbbing in lockstep.
2324
- // Snapped to whole cycles per loop and floored at one: unlike a generator's
2325
- // slow secondary swell, this is the effect's only motion, so freezing it at
2326
- // short loops (loopFreq's choice) would read as the knob being broken.
2327
- float w = 0.8;
2328
- if (u_loop > 0.0) w = TAU * max(1.0, floor(w * u_loop / TAU + 0.5)) / u_loop;
2329
-
2330
3258
  // Supersampled: n x n samples per pixel, each ramping its edges over 1/n
2331
3259
  // of a pixel. One sample with a one-pixel linear ramp is exact only for a
2332
3260
  // straight edge parallel to the pixel grid; on a 5px preview dot every
@@ -2341,8 +3269,8 @@ void main() {
2341
3269
  for (int sy = 0; sy < 4; sy++) {
2342
3270
  for (int sx = 0; sx < 4; sx++) {
2343
3271
  if (float(sx) >= n || float(sy) >= n) continue;
2344
- vec2 o = (vec2(float(sx), float(sy)) + 0.5) / n - 0.5;
2345
- 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));
2346
3274
  }
2347
3275
  }
2348
3276
  gl_FragColor = vec4(toSrgb(acc / (n * n)), 1.0);
@@ -2359,9 +3287,15 @@ const halftone = {
2359
3287
  grid: {
2360
3288
  cell: cellOf$1,
2361
3289
  lattice: (p) => ({ angle: p.angle }),
3290
+ picture: groundPicture,
2362
3291
  fragment: CELL_FRAGMENT$1
2363
3292
  },
2364
3293
  params: [
3294
+ ...groundParams({
3295
+ ground: "image",
3296
+ blend: "screen",
3297
+ opacity: 1
3298
+ }),
2365
3299
  {
2366
3300
  key: "grid",
2367
3301
  label: "Grid",
@@ -2393,7 +3327,7 @@ const halftone = {
2393
3327
  label: "Square"
2394
3328
  }
2395
3329
  ],
2396
- default: "dot"
3330
+ default: "square"
2397
3331
  },
2398
3332
  {
2399
3333
  key: "size",
@@ -2401,7 +3335,7 @@ const halftone = {
2401
3335
  min: 6,
2402
3336
  max: 160,
2403
3337
  step: .5,
2404
- default: 28
3338
+ default: 20
2405
3339
  },
2406
3340
  {
2407
3341
  key: "angle",
@@ -2417,7 +3351,7 @@ const halftone = {
2417
3351
  min: .2,
2418
3352
  max: 1.5,
2419
3353
  step: .01,
2420
- default: 1
3354
+ default: .75
2421
3355
  },
2422
3356
  {
2423
3357
  key: "softness",
@@ -2425,27 +3359,16 @@ const halftone = {
2425
3359
  min: 0,
2426
3360
  max: 1,
2427
3361
  step: .01,
2428
- default: 0
2429
- },
2430
- {
2431
- key: "contrast",
2432
- label: "Contrast",
2433
- min: 0,
2434
- max: 2,
2435
- step: .01,
2436
- default: 1
2437
- },
2438
- {
2439
- key: "pulse",
2440
- label: "Pulse",
2441
- min: 0,
2442
- max: 1,
2443
- step: .01,
2444
- default: 0
3362
+ default: .1
2445
3363
  },
3364
+ ...responseParams({ contrast: 1.15 }),
2446
3365
  ...colorModeParams({
2447
- mode: "duotone",
2448
- paperAlways: true
3366
+ mode: "source",
3367
+ paperWhen: {
3368
+ key: "ground",
3369
+ in: ["paper"]
3370
+ },
3371
+ singleColour: true
2449
3372
  })
2450
3373
  ],
2451
3374
  randomParams(rand) {
@@ -2460,17 +3383,34 @@ const halftone = {
2460
3383
  "duotone",
2461
3384
  "palette"
2462
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)];
2463
3397
  return {
3398
+ ground,
3399
+ blend,
3400
+ opacity: .6 + rand() * .4,
3401
+ blur: 0,
2464
3402
  grid: rand() < .5 ? "square" : "hex",
2465
3403
  shape: shapes[Math.floor(rand() * shapes.length)],
2466
- size: Math.round(14 + rand() * 50),
3404
+ size: Math.round(8 + rand() * 30),
2467
3405
  angle: Math.round(rand() * 180),
2468
- radius: .8 + rand() * .5,
2469
- softness: rand() < .7 ? 0 : rand() * .5,
2470
- contrast: 1,
2471
- pulse: 0,
3406
+ radius: .7 + rand() * .7,
3407
+ softness: rand() < .7 ? .1 : rand() * .5,
3408
+ style: "filled",
3409
+ exposure: 0,
3410
+ contrast: 1.15,
3411
+ density: 1,
2472
3412
  colorMode: modes[Math.floor(rand() * modes.length)],
2473
- ink: "#111111",
3413
+ ink: blend === "screen" || blend === "colorDodge" ? "#ffffff" : "#111111",
2474
3414
  paper: "#f4f1ea",
2475
3415
  invert: false
2476
3416
  };
@@ -2708,11 +3648,13 @@ uniform float u_cycleAmount;
2708
3648
  uniform float u_glyphCount;
2709
3649
  uniform sampler2D u_lut;
2710
3650
  ${COLOR_MODE}
3651
+ ${RESPONSE}
2711
3652
 
2712
3653
  void main() {
2713
3654
  vec3 sc = cellSource().rgb;
2714
- float tone = luma(sc);
2715
- 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);
2716
3658
 
2717
3659
  // The lookup places glyphs by measured ink coverage: R = the glyph just
2718
3660
  // below this tone, G = the one just above, B = how far between them.
@@ -2726,13 +3668,22 @@ void main() {
2726
3668
  float t = u_smooth > 0.5 ? 0.02 + 0.96 * hash12(cellIndex() + 0.5) : 0.5;
2727
3669
  float glyph = l.b > t ? hi : lo;
2728
3670
 
2729
- // Cycle: every step each cell re-rolls its glyph among neighbours of
2730
- // 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
2731
3677
  // stay blank, or dark areas would fill with sparks.
2732
3678
  if (u_cycle > 0.0 && glyph > 0.5) {
2733
- float h = hash12(cellIndex() + vec2(37.0, 17.0) * loopStep(u_cycle) + u_seed);
2734
- float span = u_cycleAmount * u_glyphCount * 0.25;
2735
- 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
+ }
2736
3687
  }
2737
3688
 
2738
3689
  // Glyph color. Tone already lives in the glyph's density, so source colors
@@ -2745,7 +3696,8 @@ void main() {
2745
3696
  } else if (u_colorMode < 1.5) {
2746
3697
  color = u_ink;
2747
3698
  } else {
2748
- color = palette(toneIsRamp() ? rampPosition(sc) : luma(sc));
3699
+ // Nearest palette stop, flat: see paletteStop() in chunks.ts.
3700
+ color = paletteStop(toneIsRamp() ? rampPosition(sc) : luma(sc));
2749
3701
  }
2750
3702
 
2751
3703
  gl_FragColor = vec4(color, glyph / 255.0);
@@ -2757,6 +3709,9 @@ uniform vec2 u_atlasGrid; // glyph columns, rows
2757
3709
  uniform vec2 u_atlasSize; // atlas size in texels
2758
3710
  uniform vec2 u_glyphPx; // one glyph's size in atlas texels (without gutter)
2759
3711
  ${COLOR_MODE}
3712
+ ${BLEND}
3713
+ ${COMPOSITE}
3714
+ ${GROUND}
2760
3715
 
2761
3716
  void main() {
2762
3717
  vec4 cell = cellValue(cellIndexAt());
@@ -2785,8 +3740,7 @@ void main() {
2785
3740
  }
2786
3741
  a /= n * n;
2787
3742
 
2788
- vec3 color = toSrgb(mix(toLinear(u_paper), toLinear(cell.rgb), a));
2789
- gl_FragColor = vec4(color, 1.0);
3743
+ gl_FragColor = vec4(toSrgb(compositeLinear(backdropAt(gl_FragCoord.xy), cell.rgb, a)), 1.0);
2790
3744
  }
2791
3745
  `;
2792
3746
  function cellOf(p) {
@@ -2802,7 +3756,8 @@ const ascii = {
2802
3756
  fragment: DRAW_FRAGMENT,
2803
3757
  grid: {
2804
3758
  cell: cellOf,
2805
- fragment: CELL_FRAGMENT
3759
+ fragment: CELL_FRAGMENT,
3760
+ picture: groundPicture
2806
3761
  },
2807
3762
  textures: [{
2808
3763
  uniform: "u_atlas",
@@ -2822,6 +3777,11 @@ const ascii = {
2822
3777
  })
2823
3778
  }],
2824
3779
  params: [
3780
+ ...groundParams({
3781
+ ground: "image",
3782
+ blend: "colorDodge",
3783
+ opacity: 1
3784
+ }),
2825
3785
  {
2826
3786
  key: "charset",
2827
3787
  label: "Characters",
@@ -2860,7 +3820,7 @@ const ascii = {
2860
3820
  min: 8,
2861
3821
  max: 96,
2862
3822
  step: 1,
2863
- default: 24
3823
+ default: 10
2864
3824
  },
2865
3825
  {
2866
3826
  key: "smooth",
@@ -2874,7 +3834,7 @@ const ascii = {
2874
3834
  min: 0,
2875
3835
  max: 12,
2876
3836
  step: 1,
2877
- default: 0
3837
+ default: 2
2878
3838
  },
2879
3839
  {
2880
3840
  key: "cycleAmount",
@@ -2882,13 +3842,18 @@ const ascii = {
2882
3842
  min: 0,
2883
3843
  max: 1,
2884
3844
  step: .01,
2885
- default: .3
3845
+ default: .2
2886
3846
  },
3847
+ ...responseParams({ contrast: 1.2 }),
2887
3848
  ...colorModeParams({
2888
3849
  mode: "source",
2889
3850
  ink: "#e8ffe8",
2890
3851
  paper: "#000000",
2891
- paperAlways: true
3852
+ paperWhen: {
3853
+ key: "ground",
3854
+ in: ["paper"]
3855
+ },
3856
+ singleColour: true
2892
3857
  })
2893
3858
  ],
2894
3859
  randomParams(rand) {
@@ -2904,12 +3869,28 @@ const ascii = {
2904
3869
  "duotone",
2905
3870
  "palette"
2906
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
+ ];
2907
3880
  return {
3881
+ ground,
3882
+ blend: blends[Math.floor(rand() * blends.length)],
3883
+ opacity: .7 + rand() * .3,
3884
+ blur: 0,
2908
3885
  charset: sets[Math.floor(rand() * sets.length)],
2909
3886
  size: Math.round(14 + rand() * 30),
2910
3887
  smooth: true,
2911
- cycle: 0,
2912
- cycleAmount: .3,
3888
+ cycle: 2,
3889
+ cycleAmount: .2,
3890
+ style: "filled",
3891
+ exposure: 0,
3892
+ contrast: 1,
3893
+ density: 1,
2913
3894
  colorMode: modes[Math.floor(rand() * modes.length)],
2914
3895
  ink: "#e8ffe8",
2915
3896
  paper: "#000000",
@@ -2918,6 +3899,77 @@ const ascii = {
2918
3899
  }
2919
3900
  };
2920
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
+
2921
3973
  //#endregion
2922
3974
  //#region src/registry.ts
2923
3975
  const shaders = [
@@ -2927,7 +3979,11 @@ const shaders = [
2927
3979
  halo,
2928
3980
  strata,
2929
3981
  dune,
2930
- whorl
3982
+ whorl,
3983
+ silk,
3984
+ nacre,
3985
+ burst,
3986
+ glint
2931
3987
  ];
2932
3988
  function getShader(id) {
2933
3989
  return shaders.find((s) => s.id === id);
@@ -2939,7 +3995,8 @@ const effects = [
2939
3995
  pixelate,
2940
3996
  dither,
2941
3997
  halftone,
2942
- ascii
3998
+ ascii,
3999
+ tint
2943
4000
  ];
2944
4001
  function getEffect(id) {
2945
4002
  return effects.find((e) => e.id === id);
@@ -3537,6 +4594,41 @@ function resolveParams(def, overrides) {
3537
4594
 
3538
4595
  //#endregion
3539
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
+ `;
3540
4632
  const QUAD_VERTICES = new Float32Array([
3541
4633
  -1,
3542
4634
  -1,
@@ -3788,6 +4880,69 @@ function createStackRenderer(opts) {
3788
4880
  }
3789
4881
  cellTargetsUsed.clear();
3790
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
+ }
3791
4946
  let timeSec = 0;
3792
4947
  function bindOutput(target) {
3793
4948
  gl.bindFramebuffer(gl.FRAMEBUFFER, target ? target.framebuffer : null);
@@ -3941,6 +5096,7 @@ function createStackRenderer(opts) {
3941
5096
  function applyEffectUniforms(p, state, grid, overGenerator) {
3942
5097
  const ref = refSize(canvas.width, canvas.height);
3943
5098
  gl.uniform1f(p.loc("u_sourceIsRamp"), overGenerator ? 1 : 0);
5099
+ gl.uniform1f(p.loc("u_paletteStops"), colors.length);
3944
5100
  gl.uniform2f(p.loc("u_refSize"), ref[0], ref[1]);
3945
5101
  gl.uniform1f(p.loc("u_outputScale"), outputScale(canvas.width, canvas.height));
3946
5102
  if (grid) {
@@ -3979,7 +5135,6 @@ function createStackRenderer(opts) {
3979
5135
  let input = null;
3980
5136
  active.forEach((state, i) => {
3981
5137
  const last = i === active.length - 1;
3982
- const outIndex = input === frameTargets[0] ? 1 : 0;
3983
5138
  const overGenerator = input === null && source.kind === "generator";
3984
5139
  syncTextures(state);
3985
5140
  if (state.def.grid) {
@@ -3987,6 +5142,15 @@ function createStackRenderer(opts) {
3987
5142
  const [cellIn, cellOut] = cellTargetPair(grid);
3988
5143
  if (input === null) renderSourceToCells(grid, cellIn);
3989
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;
3990
5154
  const cellProgram = state.cell;
3991
5155
  bindOutput(cellOut);
3992
5156
  useProgram(cellProgram, frameSize);
@@ -4001,6 +5165,10 @@ function createStackRenderer(opts) {
4001
5165
  applyEffectUniforms(drawProgram, state, grid, overGenerator);
4002
5166
  bindTexture(2, cellOut.texture);
4003
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
+ }
4004
5172
  draw();
4005
5173
  input = out;
4006
5174
  } else {
@@ -4020,6 +5188,7 @@ function createStackRenderer(opts) {
4020
5188
  }
4021
5189
  });
4022
5190
  pruneCellTargets();
5191
+ pruneBlurTargets();
4023
5192
  }
4024
5193
  function dispose() {
4025
5194
  for (const state of layers) disposeLayer(state);
@@ -4028,6 +5197,8 @@ function createStackRenderer(opts) {
4028
5197
  mediaFrameProgram?.dispose();
4029
5198
  mediaCellProgram?.dispose();
4030
5199
  downsampleProgram?.dispose();
5200
+ blurDownProgram?.dispose();
5201
+ blurPassProgram?.dispose();
4031
5202
  deleteTarget(gl, frameTargets[0]);
4032
5203
  deleteTarget(gl, frameTargets[1]);
4033
5204
  frameTargets = [null, null];
@@ -4036,6 +5207,11 @@ function createStackRenderer(opts) {
4036
5207
  deleteTarget(gl, pair[1]);
4037
5208
  }
4038
5209
  cellTargets.clear();
5210
+ for (const pair of blurTargets.values()) {
5211
+ deleteTarget(gl, pair[0]);
5212
+ deleteTarget(gl, pair[1]);
5213
+ }
5214
+ blurTargets.clear();
4039
5215
  gl.deleteTexture(mediaTexture);
4040
5216
  gl.deleteTexture(paletteTexture);
4041
5217
  gl.deleteBuffer(quadBuffer);
@@ -4364,4 +5540,4 @@ function createRenderer(opts) {
4364
5540
  }
4365
5541
 
4366
5542
  //#endregion
4367
- export { ascii, beam, bloom, buildPaletteRamp, createRenderer, createStackRenderer, dither, dune, effects, flow, getEffect, getShader, gridFor, halftone, halo, mountGradient, mountStack, pixelate, renderGradientFrame, renderStackFrame, shaders, 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 };