dsh-mpkg-wallpaper 3.5.0 → 3.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/client.js +16 -2626
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -1609,2631 +1609,16 @@ window.__ModuleLoader__.load({
1609
1609
  let lgResizeObs = null;
1610
1610
  let lgBgVideoHooked = false;
1611
1611
  const LG_EL_KEYS = [['lgComposer', '.wSkVaW_scrollBody, [data-slot*="composer"] [class*="card"], [data-composer-card]'], ['lgSidebar', '[class*="sidebarCol"], [data-dsh-better-sidebar] [class*="_panel"], [data-dsh-better-sidebar] [class*="_bottomPanel"]'], ['lgHeader', '.wSkVaW_header, [class*="wSkVaW_header"]']];
1612
- // ①(重做) 液态玻璃模块**内联**(不依赖 host 静态路由):源码打包成单一 ESM
1613
- // bundle(tools/build-lg-bundle.mjs),运行时用 Blob URL 动态 import——
1614
- // 任何设备/安装方式(npm/市场/git)都能用,无需重启 dsh 注册路由,
1615
- // 跨 Windows/macOS/WSL/Linux 稳定。LG_BUNDLE_SRC 由 tools/inline-lg-bundle.mjs
1616
- // 生成(勿手改)。
1617
- const LG_BUNDLE_SRC = `// 自动生成:液态玻璃单一 bundle(build-lg-bundle.mjs),勿手改
1618
-
1619
- // ===== shaders.js =====
1620
- // GLSL sources for the Liquid Glass replica.
1621
-
1622
- const VS_FULLSCREEN = \`#version 300 es
1623
- in vec2 aPos;
1624
- out vec2 vUV;
1625
- void main() {
1626
- vUV = aPos;
1627
- gl_Position = vec4(aPos * 2.0 - 1.0, 0.0, 1.0);
1628
- }\`;
1629
-
1630
- // Vertex shader for one glass element: expands the unit quad to the element's
1631
- // bounding box (plus padding for the drop shadow) in pixel space.
1632
- const VS_GLASS = \`#version 300 es
1633
- in vec2 aPos;
1634
- uniform vec2 uRes;
1635
- uniform vec2 uCenter;
1636
- uniform vec2 uHalf;
1637
- uniform float uPad;
1638
- out vec2 vUV;
1639
- void main() {
1640
- vec2 half2 = uHalf + uPad;
1641
- vec2 px = uCenter + (aPos * 2.0 - 1.0) * half2;
1642
- vUV = px / uRes;
1643
- gl_Position = vec4(px / uRes * 2.0 - 1.0, 0.0, 1.0);
1644
- }\`;
1645
-
1646
- const FS_BLIT = \`#version 300 es
1647
- precision highp float;
1648
- in vec2 vUV;
1649
- uniform sampler2D uTex;
1650
- out vec4 outColor;
1651
- vec3 linearToSrgb(vec3 c) {
1652
- c = max(c, 0.0);
1653
- return mix(1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055,
1654
- 12.92 * c,
1655
- lessThanEqual(c, vec3(0.0031308)));
1656
- }
1657
- void main() { outColor = vec4(linearToSrgb(texture(uTex, vUV).rgb), 1.0); }\`;
1658
-
1659
- // Dual-filter downsample (13 tap) used to build a progressively blurred mip
1660
- // chain. Sampling that chain with textureLod() gives a cheap variable blur,
1661
- // which is the "frosted"/scattering part of the material.
1662
- const FS_DOWN = \`#version 300 es
1663
- precision highp float;
1664
- in vec2 vUV;
1665
- uniform sampler2D uTex;
1666
- uniform vec2 uTexel; // texel size of the SOURCE level
1667
- out vec4 outColor;
1668
- void main() {
1669
- vec2 t = uTexel;
1670
- vec4 a = texture(uTex, vUV) * 0.125;
1671
- vec4 b = (texture(uTex, vUV + vec2(-t.x, -t.y)) +
1672
- texture(uTex, vUV + vec2( t.x, -t.y)) +
1673
- texture(uTex, vUV + vec2(-t.x, t.y)) +
1674
- texture(uTex, vUV + vec2( t.x, t.y))) * 0.125;
1675
- vec4 c = (texture(uTex, vUV + vec2(-2.0 * t.x, 0.0)) +
1676
- texture(uTex, vUV + vec2( 2.0 * t.x, 0.0)) +
1677
- texture(uTex, vUV + vec2(0.0, -2.0 * t.y)) +
1678
- texture(uTex, vUV + vec2(0.0, 2.0 * t.y))) * 0.0625;
1679
- vec4 d = (texture(uTex, vUV + vec2(-2.0 * t.x, -2.0 * t.y)) +
1680
- texture(uTex, vUV + vec2( 2.0 * t.x, -2.0 * t.y)) +
1681
- texture(uTex, vUV + vec2(-2.0 * t.x, 2.0 * t.y)) +
1682
- texture(uTex, vUV + vec2( 2.0 * t.x, 2.0 * t.y))) * 0.03125;
1683
- // RGB stores radiance. Alpha stores normalized optical density, so the mip
1684
- // chain can blur both representations with exactly the same footprint.
1685
- outColor = a + b + c + d;
1686
- }\`;
1687
-
1688
- // Bloom-style tent reconstruction. Each level combines its own downsampled
1689
- // detail with a tent-filtered version of the next coarser reconstructed level.
1690
- // The resulting chain removes the block boundaries that a downsample-only mip
1691
- // pyramid exposes when wide blur moves over high-contrast content.
1692
- const FS_UP = \`#version 300 es
1693
- precision highp float;
1694
- in vec2 vUV;
1695
- uniform sampler2D uLow;
1696
- uniform sampler2D uHigh;
1697
- uniform vec2 uLowTexel;
1698
- out vec4 outColor;
1699
- void main() {
1700
- vec2 t = uLowTexel;
1701
- vec4 low = texture(uLow, vUV) * 4.0;
1702
- low += (texture(uLow, vUV + vec2( t.x, 0.0)) +
1703
- texture(uLow, vUV + vec2(-t.x, 0.0)) +
1704
- texture(uLow, vUV + vec2(0.0, t.y)) +
1705
- texture(uLow, vUV + vec2(0.0, -t.y))) * 2.0;
1706
- low += texture(uLow, vUV + vec2( t.x, t.y)) +
1707
- texture(uLow, vUV + vec2(-t.x, t.y)) +
1708
- texture(uLow, vUV + vec2( t.x, -t.y)) +
1709
- texture(uLow, vUV + vec2(-t.x, -t.y));
1710
- low *= 1.0 / 16.0;
1711
- vec4 high = texture(uHigh, vUV);
1712
- outColor = mix(high, low, 0.65);
1713
- }\`;
1714
-
1715
- // Procedural wallpapers. They only exist to give the glass something with hard,
1716
- // high contrast edges to bend -- exactly what the reference screenshots have.
1717
- const FS_WALLPAPER = \`#version 300 es
1718
- precision highp float;
1719
- in vec2 vUV;
1720
- uniform vec2 uRes;
1721
- uniform int uScene;
1722
- uniform float uZoom;
1723
- uniform sampler2D uWallpaper;
1724
- uniform int uUseImage;
1725
- out vec4 outColor;
1726
-
1727
- float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
1728
- vec3 srgbToLinear(vec3 c) {
1729
- bvec3 cutoff = lessThanEqual(c, vec3(0.04045));
1730
- vec3 low = c / 12.92;
1731
- vec3 high = pow((c + 0.055) / 1.055, vec3(2.4));
1732
- return mix(high, low, cutoff);
1733
- }
1734
- float noise(vec2 p) {
1735
- vec2 i = floor(p), f = fract(p);
1736
- f = f * f * (3.0 - 2.0 * f);
1737
- return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x),
1738
- mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);
1739
- }
1740
- float fbm(vec2 p) {
1741
- float s = 0.0, a = 0.5;
1742
- for (int i = 0; i < 5; i++) { s += a * noise(p); p *= 2.02; a *= 0.5; }
1743
- return s;
1744
- }
1745
- // distance to a quadratic bezier (iterative, good enough for a backdrop)
1746
- float sdBezier(vec2 p, vec2 a, vec2 b, vec2 c) {
1747
- float best = 1e9;
1748
- vec2 prev = a;
1749
- for (int i = 1; i <= 40; i++) {
1750
- float t = float(i) / 40.0;
1751
- vec2 q = mix(mix(a, b, t), mix(b, c, t), t);
1752
- vec2 pa = p - prev, ba = q - prev;
1753
- float u = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-9), 0.0, 1.0);
1754
- best = min(best, length(pa - ba * u));
1755
- prev = q;
1756
- }
1757
- return best;
1758
- }
1759
-
1760
- vec3 sunsetBranches(vec2 uv) {
1761
- // dusk gradient: cool grey-mauve at the top, warm amber near the horizon
1762
- vec3 top = vec3(0.62, 0.55, 0.55);
1763
- vec3 mid = vec3(0.85, 0.63, 0.53);
1764
- vec3 low = vec3(0.94, 0.70, 0.52);
1765
- vec3 col = mix(mid, top, smoothstep(0.45, 1.0, uv.y));
1766
- col = mix(col, low, smoothstep(0.45, 0.0, uv.y));
1767
- col += (fbm(uv * 3.0) - 0.5) * 0.05;
1768
-
1769
- vec2 p = uv * vec2(uRes.x / uRes.y, 1.0);
1770
- float sc = uRes.x / uRes.y;
1771
- vec3 bark = vec3(0.17, 0.10, 0.09);
1772
- // main trunk + a few branches, thick and dark like the reference photo
1773
- float d = sdBezier(p, vec2(0.42 * sc, -0.1), vec2(0.52 * sc, 0.45), vec2(0.36 * sc, 1.1));
1774
- float m = smoothstep(0.060, 0.040, d);
1775
- d = sdBezier(p, vec2(0.40 * sc, 0.30), vec2(0.62 * sc, 0.44), vec2(0.95 * sc, 0.26));
1776
- m = max(m, smoothstep(0.034, 0.020, d));
1777
- d = sdBezier(p, vec2(0.44 * sc, 0.62), vec2(0.25 * sc, 0.80), vec2(0.05 * sc, 0.72));
1778
- m = max(m, smoothstep(0.022, 0.010, d));
1779
- d = sdBezier(p, vec2(0.46 * sc, 0.80), vec2(0.72 * sc, 0.95), vec2(1.05 * sc, 0.78));
1780
- m = max(m, smoothstep(0.016, 0.007, d));
1781
- d = sdBezier(p, vec2(0.12 * sc, -0.05), vec2(0.18 * sc, 0.5), vec2(0.06 * sc, 1.05));
1782
- m = max(m, smoothstep(0.038, 0.022, d));
1783
- // seed pods
1784
- for (int i = 0; i < 3; i++) {
1785
- float fi = float(i);
1786
- vec2 c = vec2((0.14 + 0.02 * fi) * sc, 0.30 + 0.26 * fi);
1787
- m = max(m, smoothstep(0.035, 0.022, length((p - c) * vec2(1.0, 0.8))));
1788
- }
1789
- return mix(col, bark, m * 0.94);
1790
- }
1791
-
1792
- vec3 deepBlueCity(vec2 uv) {
1793
- vec3 col = mix(vec3(0.06, 0.14, 0.55), vec3(0.02, 0.06, 0.34), smoothstep(0.0, 1.0, uv.y));
1794
- col += (fbm(uv * vec2(90.0, 90.0)) - 0.5) * 0.05; // fabric-like dither
1795
- // bright vertical tower strip
1796
- float x = abs(uv.x - 0.5);
1797
- float tower = smoothstep(0.035, 0.012, x) * smoothstep(0.02, 0.25, uv.y);
1798
- col = mix(col, vec3(0.72, 0.58, 0.52), tower * 0.85);
1799
- float glow = smoothstep(0.16, 0.0, x) * smoothstep(0.0, 0.5, uv.y) * 0.18;
1800
- col += vec3(0.5, 0.42, 0.36) * glow;
1801
- col = mix(col, vec3(0.10, 0.14, 0.26), smoothstep(0.16, 0.02, uv.y));
1802
- return col;
1803
- }
1804
-
1805
- vec3 islandOcean(vec2 uv) {
1806
- float ar = uRes.x / uRes.y;
1807
- vec3 deep = vec3(0.03, 0.26, 0.52);
1808
- vec3 shallow = vec3(0.20, 0.74, 0.82);
1809
- float waves = fbm(vec2(uv.x * ar * 6.0, uv.y * 22.0) + 3.0);
1810
- vec3 col = mix(deep, shallow, smoothstep(0.30, 0.78, waves * 0.7 + uv.y * 0.5));
1811
-
1812
- vec2 p = (uv - vec2(0.5, 0.40)) * vec2(ar, 1.0);
1813
- float isl = (fbm(p * 2.2 + 11.0) - 0.5) * 0.34;
1814
- float d = length(p * vec2(0.62, 1.25)) - (0.42 + isl);
1815
- // shallow reef ring around the land
1816
- col = mix(col, vec3(0.42, 0.86, 0.86), smoothstep(0.14, 0.03, d) * 0.75);
1817
- col = mix(col, vec3(0.94, 0.90, 0.72), smoothstep(0.035, 0.0, d)); // beach
1818
- vec3 jungle = mix(vec3(0.04, 0.26, 0.09), vec3(0.24, 0.55, 0.20),
1819
- fbm(p * 9.0 + 5.0));
1820
- col = mix(col, jungle, smoothstep(0.005, -0.02, d)); // jungle
1821
- return col;
1822
- }
1823
-
1824
- vec2 coverUV(vec2 uv) {
1825
- vec2 imageSize = vec2(textureSize(uWallpaper, 0));
1826
- float imageAspect = imageSize.x / max(imageSize.y, 1.0);
1827
- float viewportAspect = uRes.x / max(uRes.y, 1.0);
1828
- vec2 p = uv;
1829
- if (imageAspect > viewportAspect) {
1830
- float crop = (imageAspect / viewportAspect - 1.0) * 0.5;
1831
- p.x = p.x * (1.0 - 2.0 * crop) + crop;
1832
- } else {
1833
- float crop = (viewportAspect / imageAspect - 1.0) * 0.5;
1834
- p.y = p.y * (1.0 - 2.0 * crop) + crop;
1835
- }
1836
- return clamp(p, vec2(0.001), vec2(0.999));
1837
- }
1838
-
1839
- void main() {
1840
- vec2 uv = (vUV - 0.5) / max(uZoom, 0.01) + 0.5;
1841
- vec3 col;
1842
- if (uUseImage == 1) {
1843
- // Wallpaper textures use SRGB8_ALPHA8, so texture() already returns linear
1844
- // radiance here.
1845
- col = texture(uWallpaper, coverUV(uv)).rgb;
1846
- } else {
1847
- // Procedural palette constants are authored as display/sRGB colours. The
1848
- // SRGB render target expects linear shader output and encodes it on write.
1849
- col = srgbToLinear(uScene == 0 ? sunsetBranches(uv)
1850
- : uScene == 1 ? deepBlueCity(uv)
1851
- : islandOcean(uv));
1852
- }
1853
- // Beer-Lambert representation of backdrop darkness. A value of 4 optical
1854
- // density units already corresponds to ~1.8% transmission, enough for the
1855
- // near-black branches while retaining useful precision in RGBA8.
1856
- float lum = max(dot(col, vec3(0.2126, 0.7152, 0.0722)), 0.018);
1857
- float density = clamp(-log(lum) / 4.0, 0.0, 1.0);
1858
- outColor = vec4(col, density);
1859
- }\`;
1860
-
1861
- // ---------------------------------------------------------------------------
1862
- // The material itself.
1863
- //
1864
- // 1. shape : square/rect folder / exact capsule / exact circle SDF
1865
- // 2. thickness : t = clamp(-d / bevel), height h(t) = a convex bevel
1866
- // profile -> flat plateau in the middle, steep rim
1867
- // 3. normal : n = normalize(vec3(s * H/bevel * dh/dt * grad(d), 1))
1868
- // s = -1 -> MENISCUS rim (concave, like a liquid climbing
1869
- // the wall of a glass): normals lean inward, refraction
1870
- // pushes the sample point OUTWARD, so the surroundings get
1871
- // squeezed into the rim. This is the Apple signature.
1872
- // s = +1 -> convex lens rim: magnifies the interior instead.
1873
- // 4. refraction : Snell (refract()) through that surface, screen-space
1874
- // displacement = R.xy / -R.z * optical path length
1875
- // 5. dispersion : R/G/B refracted with slightly different IOR
1876
- // 6. scattering : variable-radius blur (multi-tap disc on the blurred mip
1877
- // chain), strong on the plateau, weak on the rim
1878
- // 7. reflection : Schlick-Fresnel environment + 2 specular lobes on the
1879
- // bevel -> the bright glass rim
1880
- // 8. shading : saturation boost / tint / soft contact shadow
1881
- // ---------------------------------------------------------------------------
1882
- const FS_GLASS = \`#version 300 es
1883
- precision highp float;
1884
- in vec2 vUV;
1885
- out vec4 outColor;
1886
-
1887
- uniform sampler2D uSrc; // blurred mip chain of the backdrop
1888
- uniform sampler2D uBlurSrc; // tent-upsampled reconstruction chain
1889
- uniform vec2 uRes;
1890
- uniform vec2 uCenter; // draw-group bounds centre, px (vertex quad only)
1891
- uniform vec2 uHalf; // element half size, px
1892
- const int MAX_SHAPES = 16;
1893
- uniform int uShapeCount;
1894
- uniform vec2 uShapeCenters[MAX_SHAPES];
1895
- uniform vec2 uShapeHalves[MAX_SHAPES];
1896
- uniform int uShapeTypes[MAX_SHAPES]; // 0 square/rect, 1 capsule, 2 circle
1897
- uniform float uShapeRadii[MAX_SHAPES];
1898
- uniform float uMergeRadius; // smooth-union reach, px
1899
- uniform float uSquircle; // superellipse exponent (2 = circular corners)
1900
- uniform float uBevel; // max width of the refracting rim, px
1901
- uniform float uHeight; // max glass height / optical thickness, px
1902
- uniform float uSizeAdaptation;// 0 = absolute material lengths, 1 = fit small UI
1903
- uniform float uIOR;
1904
- uniform float uDispersion;
1905
- uniform float uBlurPlateau; // blur radius in the middle, px
1906
- uniform float uBlurRim; // blur radius at the rim, px
1907
- uniform float uOpticalDensity;// dark-detail preservation; 0 = linear radiance
1908
- uniform float uMips; // number of levels in the blurred chain
1909
- uniform float uSpecular;
1910
- uniform float uSpecPower;
1911
- uniform float uHighlightAdapt;
1912
- uniform float uHighlightWidth;
1913
- uniform float uHighlightSharpness;
1914
- uniform float uHighlightBase;
1915
- uniform float uFresnel;
1916
- uniform float uSat;
1917
- uniform float uBright;
1918
- uniform float uTintAmount;
1919
- uniform vec3 uTintColor;
1920
- uniform float uTintAdapt; // content-aware light/dark material polarity
1921
- uniform float uShadow;
1922
- uniform float uShadowSize;
1923
- uniform float uShadowOffset;
1924
- uniform vec2 uLightDir;
1925
- uniform float uEdgeLine;
1926
- uniform float uEdgeWidth;
1927
- uniform float uEdgeDark;
1928
- uniform float uRefractScale;
1929
- uniform float uMeniscus; // 1 = concave meniscus rim, 0 = convex lens rim
1930
- uniform int uDebug; // 0 final, 1 thickness, 2 normals, 3 displacement
1931
-
1932
- float sdSquircle(vec2 p, vec2 b, float r, float n) {
1933
- vec2 q = abs(p) - b + r;
1934
- vec2 m = max(q, 0.0) + 1e-5;
1935
- float e = pow(pow(m.x, n) + pow(m.y, n), 1.0 / n);
1936
- return min(max(q.x, q.y), 0.0) + e - r;
1937
- }
1938
-
1939
- float sdPrimitive(vec2 p, vec2 halfSize, int shapeType, float radius) {
1940
- if (shapeType == 2) {
1941
- // Circle is invariant: layout cannot turn it into an ellipse.
1942
- return length(p) - min(halfSize.x, halfSize.y);
1943
- }
1944
- if (shapeType == 1) {
1945
- // Apple's capsule rule: end-cap radius is exactly half the short side.
1946
- return sdSquircle(p, halfSize, min(halfSize.x, halfSize.y), 2.0);
1947
- }
1948
- // Square and rectangular folders share the same fixed-radius corner model;
1949
- // only their bounding boxes differ. The default exponent is 2 per reference.
1950
- return sdSquircle(p, halfSize, radius, max(uSquircle, 2.0));
1951
- }
1952
-
1953
- // One distance field represents the complete component group. Because the
1954
- // normal is derived from this same field below, the meniscus, refraction and
1955
- // highlight bend continuously through the bridge instead of exposing two
1956
- // composited glass layers.
1957
- float sdAppleShape(vec2 px) {
1958
- float nearest = 1e8;
1959
- for (int i = 0; i < MAX_SHAPES; i++) {
1960
- if (i >= uShapeCount) break;
1961
- float next = sdPrimitive(px - uShapeCenters[i], uShapeHalves[i],
1962
- uShapeTypes[i], uShapeRadii[i]);
1963
- nearest = min(nearest, next);
1964
- }
1965
-
1966
- if (uMergeRadius < 0.01) return nearest;
1967
-
1968
- // Global exponential smooth-min is associative and C-infinity. Pairwise
1969
- // polynomial unions are only C1 and become order-dependent with 3+ shapes;
1970
- // their curvature boundaries show up as diagonal tears under sharp glass
1971
- // highlights. 0.36 matches the polynomial union's depth at equal distances.
1972
- float scale = max(uMergeRadius * 0.36, 0.01);
1973
- float sum = 0.0;
1974
- for (int i = 0; i < MAX_SHAPES; i++) {
1975
- if (i >= uShapeCount) break;
1976
- float next = sdPrimitive(px - uShapeCenters[i], uShapeHalves[i],
1977
- uShapeTypes[i], uShapeRadii[i]);
1978
- sum += exp(-(next - nearest) / scale);
1979
- }
1980
- return nearest - scale * log(max(sum, 1e-6));
1981
- }
1982
-
1983
- // Quintic tangent transition. Besides value and slope, its second derivative
1984
- // matches at both ends: f(0/1)=0/1, f'(0/1)=0/1, f''(0/1)=0. This lets a
1985
- // circular corner leave a straight side with zero curvature instead of the
1986
- // rounded-box SDF's abrupt 0 -> 1/r curvature jump.
1987
- float tangentTransition(float u) {
1988
- return u * u * u * (6.0 - 8.0 * u + 3.0 * u * u);
1989
- }
1990
-
1991
- vec2 primitiveOpticalGradient(vec2 p, vec2 halfSize,
1992
- int shapeType, float radius) {
1993
- if (shapeType == 2) return normalize(p + 1e-6);
1994
-
1995
- float exponent = shapeType == 1 ? 2.0 : max(uSquircle, 2.0);
1996
- float resolvedRadius = shapeType == 1
1997
- ? min(halfSize.x, halfSize.y) : radius;
1998
- vec2 q = abs(p) - halfSize + resolvedRadius;
1999
- vec2 direction;
2000
-
2001
- if (q.x > 0.0 && q.y > 0.0) {
2002
- // Analytic Lp-corner normal. Exponents above 2 already approach the side
2003
- // with zero curvature; blend out the circular-corner correction by n=3.
2004
- vec2 lp = normalize(pow(q, vec2(exponent - 1.0)) + 1e-6);
2005
- const float HALF_PI = 1.57079632679;
2006
- const float TRANSITION_ANGLE = 0.43633231299; // 25 degrees at each tangent
2007
- float angle = atan(q.y, q.x);
2008
- float easedAngle = angle;
2009
- if (angle < TRANSITION_ANGLE) {
2010
- easedAngle = TRANSITION_ANGLE *
2011
- tangentTransition(angle / TRANSITION_ANGLE);
2012
- } else if (angle > HALF_PI - TRANSITION_ANGLE) {
2013
- float fromTop = (HALF_PI - angle) / TRANSITION_ANGLE;
2014
- easedAngle = HALF_PI - TRANSITION_ANGLE *
2015
- tangentTransition(fromTop);
2016
- }
2017
- vec2 continuousCorner = vec2(cos(easedAngle), sin(easedAngle));
2018
- float circularCorner = 1.0 - smoothstep(2.0, 3.0, exponent);
2019
- direction = normalize(mix(lp, continuousCorner, circularCorner));
2020
- } else if (q.x > q.y) {
2021
- direction = vec2(1.0, 0.0);
2022
- } else {
2023
- direction = vec2(0.0, 1.0);
2024
- }
2025
-
2026
- return direction * sign(p);
2027
- }
2028
-
2029
- vec2 opticalGradient(vec2 px) {
2030
- float nearest = 1e8;
2031
- int nearestIndex = 0;
2032
- for (int i = 0; i < MAX_SHAPES; i++) {
2033
- if (i >= uShapeCount) break;
2034
- float next = sdPrimitive(px - uShapeCenters[i], uShapeHalves[i],
2035
- uShapeTypes[i], uShapeRadii[i]);
2036
- if (next < nearest) {
2037
- nearest = next;
2038
- nearestIndex = i;
2039
- }
2040
- }
2041
-
2042
- if (uMergeRadius < 0.01 || uShapeCount == 1) {
2043
- return primitiveOpticalGradient(
2044
- px - uShapeCenters[nearestIndex], uShapeHalves[nearestIndex],
2045
- uShapeTypes[nearestIndex], uShapeRadii[nearestIndex]
2046
- );
2047
- }
2048
-
2049
- // The derivative of exponential smooth-min is the same weighted average of
2050
- // the primitive derivatives. Reusing those weights keeps fused normals C2
2051
- // through both a primitive's tangent and the union bridge.
2052
- float scale = max(uMergeRadius * 0.36, 0.01);
2053
- vec2 gradientSum = vec2(0.0);
2054
- float weightSum = 0.0;
2055
- for (int i = 0; i < MAX_SHAPES; i++) {
2056
- if (i >= uShapeCount) break;
2057
- vec2 local = px - uShapeCenters[i];
2058
- float next = sdPrimitive(local, uShapeHalves[i],
2059
- uShapeTypes[i], uShapeRadii[i]);
2060
- float weight = exp(-(next - nearest) / scale);
2061
- gradientSum += primitiveOpticalGradient(
2062
- local, uShapeHalves[i], uShapeTypes[i], uShapeRadii[i]
2063
- ) * weight;
2064
- weightSum += weight;
2065
- }
2066
- return gradientSum / max(weightSum, 1e-6);
2067
- }
2068
-
2069
- // Shading adaptation belongs to the primitive under this fragment, not to the
2070
- // bounds of the whole draw group. The latter changes whenever any component in
2071
- // a connected fusion group moves, making every highlight pulse in sympathy.
2072
- //
2073
- // Around a genuine smooth-union bridge, use the same exponential influence as
2074
- // the distance field so the material centre crosses continuously from one
2075
- // primitive to the next. Contributions too weak to affect the visible bridge
2076
- // are smoothly discarded; a nearby-but-separate component then has exactly no
2077
- // influence on this component's highlight or light/dark tint.
2078
- void localComponentMetrics(vec2 px, out vec2 componentCenter,
2079
- out float componentShortSide) {
2080
- float nearest = 1e8;
2081
- vec2 nearestCenter = uShapeCenters[0];
2082
- float nearestShortSide = 1.0;
2083
- for (int i = 0; i < MAX_SHAPES; i++) {
2084
- if (i >= uShapeCount) break;
2085
- float next = sdPrimitive(px - uShapeCenters[i], uShapeHalves[i],
2086
- uShapeTypes[i], uShapeRadii[i]);
2087
- if (next < nearest) {
2088
- nearest = next;
2089
- nearestCenter = uShapeCenters[i];
2090
- nearestShortSide = 2.0 * min(uShapeHalves[i].x, uShapeHalves[i].y);
2091
- }
2092
- }
2093
-
2094
- componentCenter = nearestCenter;
2095
- componentShortSide = nearestShortSide;
2096
- if (uMergeRadius < 0.01 || uShapeCount == 1) return;
2097
-
2098
- float scale = max(uMergeRadius * 0.36, 0.01);
2099
- vec2 centreSum = vec2(0.0);
2100
- float shortSideSum = 0.0;
2101
- float weightSum = 0.0;
2102
- for (int i = 0; i < MAX_SHAPES; i++) {
2103
- if (i >= uShapeCount) break;
2104
- float next = sdPrimitive(px - uShapeCenters[i], uShapeHalves[i],
2105
- uShapeTypes[i], uShapeRadii[i]);
2106
- float weight = exp(-(next - nearest) / scale);
2107
- weight *= smoothstep(0.04, 0.20, weight);
2108
- centreSum += uShapeCenters[i] * weight;
2109
- shortSideSum += 2.0 * min(uShapeHalves[i].x, uShapeHalves[i].y) * weight;
2110
- weightSum += weight;
2111
- }
2112
- if (weightSum > 0.0) {
2113
- componentCenter = centreSum / weightSum;
2114
- componentShortSide = shortSideSum / weightSum;
2115
- }
2116
- }
2117
-
2118
- vec4 sampleBg(vec2 px, float lod) {
2119
- vec2 uv = clamp(px / uRes, vec2(0.001), vec2(0.999));
2120
- return textureLod(uSrc, uv, lod);
2121
- }
2122
-
2123
- vec4 sampleReconstructedBg(vec2 px, float lod) {
2124
- vec2 uv = clamp(px / uRes, vec2(0.001), vec2(0.999));
2125
- return textureLod(uBlurSrc, uv, lod);
2126
- }
2127
-
2128
- float luminance(vec3 c) {
2129
- return dot(c, vec3(0.2126, 0.7152, 0.0722));
2130
- }
2131
-
2132
- float hash12(vec2 p) {
2133
- vec3 p3 = fract(vec3(p.xyx) * 0.1031);
2134
- p3 += dot(p3, p3.yzx + 33.33);
2135
- return fract((p3.x + p3.y) * p3.z);
2136
- }
2137
-
2138
- vec3 linearToSrgb(vec3 c) {
2139
- c = max(c, 0.0);
2140
- return mix(1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055,
2141
- 12.92 * c,
2142
- lessThanEqual(c, vec3(0.0031308)));
2143
- }
2144
-
2145
- vec2 softLimitOffset(vec2 offset, float limit) {
2146
- float magnitude = length(offset);
2147
- if (magnitude < 1e-4) return offset;
2148
- float limited = tanh(magnitude / max(limit, 1.0)) * limit;
2149
- return offset * (limited / magnitude);
2150
- }
2151
-
2152
- // Variable-radius blur, radius in device px.
2153
- //
2154
- // A single textureLod() tap on the mip chain is not enough. The chain only
2155
- // offers radii in powers of two, and on the top levels one texel is tens of
2156
- // pixels wide, so a lone bilinear tap (a) averages in a huge slab of the
2157
- // screen, which drags the colour toward the frame mean -> washed out, and
2158
- // (b) reconstructs as a handful of big diamonds -> the "too few samples" mush.
2159
- // Instead take the level whose own radius is about a third of what we want and
2160
- // spread TAPS samples over the remainder on a golden-angle spiral. Neighbouring
2161
- // taps then land roughly one texel apart at that level, which is exactly the
2162
- // spacing at which the level's own filtering makes the disc continuous, so the
2163
- // result is a real wide Gaussian that keeps its local colour.
2164
- const int TAPS = 12;
2165
- const float GOLDEN_ANGLE = 2.39996323;
2166
-
2167
- vec3 blurBg(vec2 px, float radius) {
2168
- if (radius < 1.0) return sampleBg(px, 0.0).rgb;
2169
- float lod = clamp(log2(radius) - 1.585, 0.0, uMips - 1.0); // 2^lod ~ r/3
2170
- vec3 acc = vec3(0.0);
2171
- float densityAcc = 0.0;
2172
- float wsum = 0.0;
2173
- for (int i = 0; i < TAPS; i++) {
2174
- float fi = float(i) + 0.5;
2175
- float r = sqrt(fi / float(TAPS)); // equal-area spacing over the disc
2176
- float a = fi * GOLDEN_ANGLE;
2177
- float w = exp(-1.8 * r * r);
2178
- vec2 samplePx = px + vec2(cos(a), sin(a)) * r * radius;
2179
- // Keep narrow blur faithful to the original downsample chain, then lean on
2180
- // the reconstructed chain where coarse mip blocks and temporal breathing
2181
- // become visible. Both samplers return linear radiance from sRGB textures.
2182
- float reconstruction = 0.78 * smoothstep(10.0, 52.0, radius);
2183
- vec4 s = mix(sampleBg(samplePx, lod),
2184
- sampleReconstructedBg(samplePx, lod), reconstruction);
2185
- acc += s.rgb * w;
2186
- densityAcc += s.a * w;
2187
- wsum += w;
2188
- }
2189
- vec3 linearCol = acc / wsum;
2190
-
2191
- // A pure radiance average spreads a dark branch but also dilutes it toward
2192
- // the pale sky. The density channel averages -log(luminance), equivalent to
2193
- // geometrically averaging transmission. That preserves the visual weight of
2194
- // dark occluders while keeping uniform light regions unchanged. We retain
2195
- // the linear RGB hue and only restore the missing luminance contrast.
2196
- float linearLum = max(dot(linearCol, vec3(0.2126, 0.7152, 0.0722)), 0.001);
2197
- float densityLum = exp(-4.0 * densityAcc / wsum);
2198
- float densityGap = max(linearLum - densityLum, 0.0);
2199
- float radiusGate = smoothstep(1.0, 8.0, radius);
2200
- float targetLum = max(linearLum * 0.22,
2201
- linearLum - densityGap * uOpticalDensity * radiusGate);
2202
- return linearCol * (targetLum / linearLum);
2203
- }
2204
-
2205
- void main() {
2206
- vec2 px = vUV * uRes;
2207
-
2208
- float d = sdAppleShape(px);
2209
- float aa = smoothstep(0.8, -0.8, d);
2210
- vec2 adaptCenter;
2211
- float localShortSide;
2212
- localComponentMetrics(px, adaptCenter, localShortSide);
2213
- // Material lengths are authored against the large demo components. Treat
2214
- // them as maxima and fit the complete optical system to 30% of a smaller
2215
- // primitive's short side. The shared metric call above also keeps this scale
2216
- // continuous when differently sized primitives form one fused surface.
2217
- float fittedScale = clamp(0.30 * localShortSide / max(uBevel, 1.0), 0.05, 1.0);
2218
- float opticalScale = mix(1.0, fittedScale,
2219
- clamp(uSizeAdaptation, 0.0, 1.0));
2220
- float bevel = max(uBevel * opticalScale, 1.0);
2221
- float opticalHeight = uHeight * opticalScale;
2222
-
2223
- // ---- gradient of the SDF = outward direction of the surface -------------
2224
- vec2 g = normalize(opticalGradient(px) + 1e-6);
2225
-
2226
- // ---- thickness field / bevel profile -----------------------------------
2227
- float t = clamp(-d / bevel, 0.0, 1.0); // 0 at the edge, 1 on the plateau
2228
- float ct = 1.0 - t;
2229
- float h = sqrt(max(1.0 - ct * ct, 0.0)); // convex (circular) bevel
2230
- float dhdt = ct / max(h, 0.10); // slope, clamped at the silhouette
2231
- float slope = (opticalHeight / bevel) * dhdt;
2232
-
2233
- // 0 = convex lens, 1 = the default Apple-like concave rim. Values above 1
2234
- // deliberately exaggerate the inward normal for exploratory tuning.
2235
- float curveSign = 1.0 - 2.0 * uMeniscus;
2236
- vec3 n = normalize(vec3(curveSign * g * slope, 1.0));
2237
- vec3 I = vec3(0.0, 0.0, -1.0);
2238
-
2239
- // ---- refraction (Snell) + dispersion -----------------------------------
2240
- float path = opticalHeight * mix(0.25, 1.0, h) * uRefractScale;
2241
- vec2 dR, dG, dB;
2242
- {
2243
- float e = 1.0 / max(uIOR - uDispersion, 1.0);
2244
- vec3 R = refract(I, n, e);
2245
- dR = (R == vec3(0.0)) ? vec2(0.0) : R.xy / max(-R.z, 0.25) * path;
2246
- e = 1.0 / max(uIOR, 1.0);
2247
- R = refract(I, n, e);
2248
- dG = (R == vec3(0.0)) ? vec2(0.0) : R.xy / max(-R.z, 0.25) * path;
2249
- e = 1.0 / max(uIOR + uDispersion, 1.0);
2250
- R = refract(I, n, e);
2251
- dB = (R == vec3(0.0)) ? vec2(0.0) : R.xy / max(-R.z, 0.25) * path;
2252
- }
2253
-
2254
- // Strong concave meniscus normals can make the screen-space mapping fold
2255
- // over itself at multi-shape junctions. On hard-edged wallpapers that reads
2256
- // as triangular tearing rather than refraction. Compress only the extreme
2257
- // tail; ordinary offsets remain almost linear while caustic spikes stay
2258
- // within a bevel-sized optical footprint.
2259
- float maxDisplacement = max(1.15 * bevel, max(12.0 * opticalScale, 3.0));
2260
- dR = softLimitOffset(dR, maxDisplacement);
2261
- dG = softLimitOffset(dG, maxDisplacement);
2262
- dB = softLimitOffset(dB, maxDisplacement);
2263
-
2264
- // ---- scattering: rim stays readable, plateau is frosted ----------------
2265
- float radius = mix(uBlurRim, uBlurPlateau, smoothstep(0.0, 0.85, t))
2266
- * opticalScale;
2267
-
2268
- vec3 col;
2269
- col.r = blurBg(px + dR, radius).r;
2270
- col.g = blurBg(px + dG, radius).g;
2271
- col.b = blurBg(px + dB, radius).b;
2272
-
2273
- // Saturation is boosted on the TRANSMITTED backdrop only (this is what
2274
- // UIVisualEffectView's saturationDeltaFactor does). Any wide blur averages
2275
- // colours toward grey; without this the frosted panel reads pale even though
2276
- // the wallpaper behind it is saturated. Doing it before the reflections keeps
2277
- // the specular/Fresnel highlights neutral.
2278
- col = mix(vec3(dot(col, vec3(0.2126, 0.7152, 0.0722))), col, uSat);
2279
-
2280
- // ---- reflection: backdrop environment + narrow specular lobes -----------
2281
- // Schlick: F0 for glass is ~4%, and the (1-cos)^5 falloff keeps the mirror
2282
- // term confined to the steepest part of the bevel. A softer exponent smears
2283
- // a grey wash across the whole rim and bleaches the refracted image there.
2284
- float fres = 0.04 + 0.96 * pow(1.0 - n.z, 5.0);
2285
- vec2 nn = normalize(n.xy + 1e-6);
2286
-
2287
- // Keep the reflected environment local to the fragment. Stabilise the lobe
2288
- // controls around the owning primitive below so hard wallpaper edges do not
2289
- // chop one rim into unrelated bright and dark pieces.
2290
- float probeLod = clamp(3.5 + log2(max(opticalScale, 0.05)),
2291
- 0.0, uMips - 1.0);
2292
- float probeRadius = max(1.35 * bevel, max(18.0 * opticalScale, 3.0));
2293
- vec3 envL = sampleBg(px + vec2(-probeRadius, 0.0), probeLod).rgb;
2294
- vec3 envR = sampleBg(px + vec2( probeRadius, 0.0), probeLod).rgb;
2295
- vec3 envB = sampleBg(px + vec2(0.0, -probeRadius), probeLod).rgb;
2296
- vec3 envT = sampleBg(px + vec2(0.0, probeRadius), probeLod).rgb;
2297
- vec2 fallbackLight = normalize(uLightDir + vec2(1e-5));
2298
- // Highlight adaptation must be stable across one component. Driving its
2299
- // strength and colour from each fragment's probe makes a mountain ridge or
2300
- // tree line cut the rim into bright and dark pieces. Probe around the local
2301
- // component centre while retaining per-fragment environment reflection.
2302
- vec3 adaptL = sampleBg(adaptCenter + vec2(-probeRadius, 0.0), probeLod).rgb;
2303
- vec3 adaptR = sampleBg(adaptCenter + vec2( probeRadius, 0.0), probeLod).rgb;
2304
- vec3 adaptB = sampleBg(adaptCenter + vec2(0.0, -probeRadius), probeLod).rgb;
2305
- vec3 adaptT = sampleBg(adaptCenter + vec2(0.0, probeRadius), probeLod).rgb;
2306
- vec2 adaptGradient = vec2(luminance(adaptR) - luminance(adaptL),
2307
- luminance(adaptT) - luminance(adaptB));
2308
- float stableContrast = length(adaptGradient);
2309
- float lightAdapt = clamp(uHighlightAdapt, 0.0, 1.0) *
2310
- smoothstep(0.025, 0.22, stableContrast);
2311
- // Keep the lobe direction material-local. Steering it with the wallpaper
2312
- // gradient creates a rapidly rotating direction field around hard colour
2313
- // edges, which appears as diagonal tears and lets unrelated components alter
2314
- // each other's highlights. The environment still adapts strength and colour.
2315
- vec2 lightDir = fallbackLight;
2316
-
2317
- // Reflect the colour seen in the surface-normal direction. A local sample
2318
- // keeps small bright structures (tower lights, clouds, coastlines) attached
2319
- // to the nearby rim instead of turning every frame into the same white ring.
2320
- float wx = clamp(0.5 + 0.5 * nn.x, 0.0, 1.0);
2321
- float wy = clamp(0.5 + 0.5 * nn.y, 0.0, 1.0);
2322
- vec3 envX = mix(envL, envR, wx);
2323
- vec3 envY = mix(envB, envT, wy);
2324
- vec3 ringEnv = (envX * abs(nn.x) + envY * abs(nn.y)) /
2325
- max(abs(nn.x) + abs(nn.y), 1e-3);
2326
- float localProbeLod = clamp(2.0 + log2(max(opticalScale, 0.05)),
2327
- 0.0, uMips - 1.0);
2328
- vec3 localEnv = sampleBg(px + g * max(0.55 * bevel,
2329
- max(6.0 * opticalScale, 2.0)),
2330
- localProbeLod).rgb;
2331
- vec3 env = mix(ringEnv, localEnv, 0.58);
2332
- float envLum = luminance(env);
2333
- env = mix(env, vec3(envLum), 0.10); // retain wallpaper hue, tame neon spikes
2334
- float envStrength = mix(0.58, 1.0, smoothstep(0.08, 0.75, envLum));
2335
- col = mix(col, env, clamp(fres * uFresnel * envStrength, 0.0, 0.82));
2336
-
2337
- vec3 L1 = normalize(vec3(lightDir, 0.58));
2338
- vec3 L2 = normalize(vec3(-lightDir, 0.48));
2339
- float sharpness = max(uHighlightSharpness, 0.1);
2340
- float s1 = pow(max(dot(n, L1), 0.0),
2341
- max(uSpecPower * sharpness, 1.0));
2342
- float s2 = pow(max(dot(n, L2), 0.0),
2343
- max(uSpecPower * sharpness * 0.78, 1.0)) * 0.18;
2344
- float highlightWidth = clamp(uHighlightWidth, 0.16, 1.0);
2345
- float riseEnd = min(0.10, 0.25 * highlightWidth);
2346
- float specBand = smoothstep(0.015, riseEnd, t) *
2347
- (1.0 - smoothstep(0.61 * highlightWidth,
2348
- highlightWidth, t));
2349
- float baseHighlight = clamp(uHighlightBase, 0.0, 1.0);
2350
- float sourceStrength = baseHighlight + (1.0 - baseHighlight) * lightAdapt;
2351
- vec3 sourceEnv = mix(mix(adaptL, adaptR, 0.5 + 0.5 * lightDir.x),
2352
- mix(adaptB, adaptT, 0.5 + 0.5 * lightDir.y), 0.5);
2353
- float sourceLum = max(luminance(sourceEnv), 0.08);
2354
- vec3 specColor = clamp(mix(vec3(1.0), sourceEnv / sourceLum, 0.42),
2355
- vec3(0.45), vec3(2.2));
2356
- col += uSpecular * (s1 + s2) * specBand * sourceStrength * specColor;
2357
-
2358
- // Dark contour right at the silhouette: at grazing angles the rim reflects
2359
- // the surroundings instead of transmitting, so real glass edges read dark.
2360
- float w = max(uEdgeWidth, 0.5);
2361
- float contour = smoothstep(w, 0.0, abs(d + 0.55 * w));
2362
- col *= 1.0 - uEdgeDark * contour;
2363
-
2364
- // Crisp inner highlight line; direction and colour follow the local probe.
2365
- float line = smoothstep(1.35 * w, 0.0, abs(d + 2.2 * w));
2366
- float lit = 0.26 + 0.74 * max(dot(g, lightDir), 0.0);
2367
- vec3 stableEnv = (adaptL + adaptR + adaptB + adaptT) * 0.25;
2368
- float stableEnvLum = max(luminance(stableEnv), 0.12);
2369
- vec3 lineColor = mix(vec3(1.0), stableEnv / stableEnvLum, 0.28);
2370
- col += uEdgeLine * line * lit * lineColor;
2371
-
2372
- // ---- tint --------------------------------------------------------------
2373
- // Tint polarity adapts once per local component, not per draw group.
2374
- // This captures the system-material light/dark switch without letting a hard
2375
- // background edge split one surface into visibly different materials.
2376
- float materialLum = luminance(sampleBg(adaptCenter, uMips - 1.0).rgb);
2377
- float useDarkMaterial = smoothstep(0.38, 0.68, materialLum);
2378
- vec3 automaticTint = mix(vec3(0.97, 0.985, 1.0),
2379
- vec3(0.035, 0.055, 0.080), useDarkMaterial);
2380
- vec3 resolvedTint = mix(uTintColor, automaticTint, clamp(uTintAdapt, 0.0, 1.0));
2381
- float adaptiveAmount = uTintAmount * mix(1.10, 0.92, useDarkMaterial);
2382
- col = mix(col, resolvedTint, clamp(adaptiveAmount, 0.0, 1.0));
2383
- col += uBright;
2384
-
2385
- // ---- soft contact shadow ----------------------------------------------
2386
- float ds = sdAppleShape(px + vec2(0.0, uShadowOffset));
2387
- float sh = exp(-max(ds, 0.0) / max(uShadowSize, 0.5)) * uShadow;
2388
-
2389
- if (uDebug == 1) col = vec3(h);
2390
- if (uDebug == 2) col = vec3(0.5 + 0.5 * n.xy, n.z);
2391
- if (uDebug == 3) col = vec3(length(dG) / max(opticalHeight, 1.0),
2392
- length(dR - dB) / max(opticalHeight, 1.0) * 6.0, 0.0);
2393
-
2394
- // The browser drawing buffer stores display/sRGB values, unlike the explicit
2395
- // SRGB8_ALPHA8 offscreen attachments. Encode the final linear material here,
2396
- // then add triangular-distribution noise smaller than one display-space LSB.
2397
- if (uDebug == 0) {
2398
- float n0 = hash12(gl_FragCoord.xy + vec2(17.0, 59.0));
2399
- float n1 = hash12(gl_FragCoord.yx + vec2(83.0, 11.0));
2400
- float noise = (n0 - n1) * (0.85 / 255.0);
2401
- col = clamp(linearToSrgb(col) + noise, 0.0, 1.0);
2402
- }
2403
-
2404
- float a = aa + sh * (1.0 - aa);
2405
- outColor = vec4(col * aa, a); // premultiplied; shadow contributes black
2406
- }\`;
2407
-
2408
-
2409
- // ===== v2-shaders.js =====
2410
- // The V2 clear/transparent optical model. This is intentionally a separate
2411
- // shader rather than a branch inside FS_GLASS: its controls have different
2412
- // units, profiles and compositing rules even where a public name looks alike.
2413
- const FS_GLASS_V2 = \`#version 300 es
2414
- precision highp float;
2415
- in vec2 vUV;
2416
- out vec4 outColor;
2417
-
2418
- uniform sampler2D uSrc;
2419
- uniform vec2 uRes;
2420
- uniform float uDpr;
2421
- uniform float uMips;
2422
- const int MAX_SHAPES = 16;
2423
- uniform int uShapeCount;
2424
- uniform vec2 uShapeCenters[MAX_SHAPES];
2425
- uniform vec2 uShapeHalves[MAX_SHAPES];
2426
- uniform int uShapeTypes[MAX_SHAPES];
2427
- uniform float uShapeRadii[MAX_SHAPES];
2428
- uniform float uShapeTints[MAX_SHAPES];
2429
- uniform float uShapeTintLights[MAX_SHAPES];
2430
- uniform float uShapeFrosts[MAX_SHAPES];
2431
- uniform float uShapeOpacities[MAX_SHAPES];
2432
- uniform vec2 uLightDirs[MAX_SHAPES];
2433
- uniform float uRefraction;
2434
- uniform float uEdgeReach;
2435
- uniform float uEdgeWidth;
2436
- uniform float uDispersion;
2437
- uniform float uBody;
2438
- uniform float uAbsorption;
2439
- uniform float uRim;
2440
- uniform float uReflection;
2441
- uniform float uHighlight;
2442
- uniform float uEcho;
2443
- uniform float uHairline;
2444
- uniform float uHairWidth;
2445
-
2446
- vec3 linearToSrgb(vec3 c) {
2447
- c = max(c, 0.0);
2448
- return mix(1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055,
2449
- 12.92 * c,
2450
- lessThanEqual(c, vec3(0.0031308)));
2451
- }
2452
-
2453
- float sdRoundBox(vec2 p, vec2 b, float r) {
2454
- vec2 q = abs(p) - b + r;
2455
- return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
2456
- }
2457
-
2458
- float smoothUnion(float d1, float d2, float k) {
2459
- float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
2460
- return mix(d2, d1, h) - k * h * (1.0 - h);
2461
- }
2462
-
2463
- float shapeSdf(int index, vec2 point) {
2464
- vec2 p = point - uShapeCenters[index];
2465
- vec2 halfSize = uShapeHalves[index];
2466
- int kind = uShapeTypes[index];
2467
- float radius = min(uShapeRadii[index], min(halfSize.x, halfSize.y));
2468
- if (kind == 0) return sdRoundBox(p, halfSize, radius);
2469
- if (kind == 1) return sdRoundBox(p, halfSize, min(halfSize.x, halfSize.y));
2470
- return length(p) - min(halfSize.x, halfSize.y);
2471
- }
2472
-
2473
- vec2 opticalNormal(int index, vec2 point, vec2 sdfNormal) {
2474
- vec2 p = point - uShapeCenters[index];
2475
- vec2 halfSize = max(uShapeHalves[index], vec2(1.0));
2476
- int kind = uShapeTypes[index];
2477
-
2478
- if (kind == 0) {
2479
- // The sixth-order superellipse is the optical field, while roundness only
2480
- // controls the silhouette. This separation is part of the V2 model.
2481
- vec2 q = p / halfSize;
2482
- vec2 g = vec2(sign(q.x) * pow(abs(q.x), 5.0) / halfSize.x,
2483
- sign(q.y) * pow(abs(q.y), 5.0) / halfSize.y);
2484
- return normalize(g + sdfNormal * 0.0001);
2485
- }
2486
- if (kind == 1) {
2487
- vec2 closest;
2488
- if (halfSize.x >= halfSize.y) {
2489
- float segment = max(halfSize.x - halfSize.y, 0.0);
2490
- closest = vec2(clamp(p.x, -segment, segment), 0.0);
2491
- } else {
2492
- float segment = max(halfSize.y - halfSize.x, 0.0);
2493
- closest = vec2(0.0, clamp(p.y, -segment, segment));
2494
- }
2495
- return normalize(p - closest + sdfNormal * 0.0001);
2496
- }
2497
- return normalize(p + sdfNormal * 0.0001);
2498
- }
2499
-
2500
- vec3 backdrop(vec2 uv) {
2501
- // The shared backdrop pipeline stores linear radiance in SRGB8_ALPHA8.
2502
- // V2's optical constants were authored in display space, so convert each
2503
- // sample back before applying the V2 equations.
2504
- return linearToSrgb(texture(uSrc, clamp(uv, vec2(0.001), vec2(0.999))).rgb);
2505
- }
2506
-
2507
- vec3 softBackdrop(vec2 uv, float radius) {
2508
- float lod = clamp(log2(max(radius * 0.9, 1.0)), 0.0, max(uMips - 1.0, 0.0));
2509
- vec2 r = vec2(max(radius * 0.42, 0.35)) / uRes;
2510
- vec2 center = clamp(uv, vec2(0.001), vec2(0.999));
2511
- vec3 c = linearToSrgb(textureLod(uSrc, center, lod).rgb) * 0.44;
2512
- c += linearToSrgb(textureLod(uSrc, clamp(center + vec2(r.x, 0.0), vec2(0.001), vec2(0.999)), lod).rgb) * 0.14;
2513
- c += linearToSrgb(textureLod(uSrc, clamp(center - vec2(r.x, 0.0), vec2(0.001), vec2(0.999)), lod).rgb) * 0.14;
2514
- c += linearToSrgb(textureLod(uSrc, clamp(center + vec2(0.0, r.y), vec2(0.001), vec2(0.999)), lod).rgb) * 0.14;
2515
- c += linearToSrgb(textureLod(uSrc, clamp(center - vec2(0.0, r.y), vec2(0.001), vec2(0.999)), lod).rgb) * 0.14;
2516
- return c;
2517
- }
2518
-
2519
- float luminance(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
2520
-
2521
- vec3 interfaceColor(vec2 point, vec2 normal) {
2522
- vec3 outsideColor = softBackdrop((point + normal * 1.8) / uRes, 2.0);
2523
- vec3 insideColor = softBackdrop((point - normal * 1.8) / uRes, 2.0);
2524
- // Apple's outer interface is a neutral contrast line rather than a copy of
2525
- // the wallpaper colour. Include display-space value as well as luminance so
2526
- // saturated blue/purple fields select a dark line even though their formal
2527
- // luminance is modest. The outside carries more weight because that is the
2528
- // field the silhouette must remain legible against.
2529
- float outsideValue = max(outsideColor.r, max(outsideColor.g, outsideColor.b));
2530
- float insideValue = max(insideColor.r, max(insideColor.g, insideColor.b));
2531
- float outsideLight = max(luminance(outsideColor), outsideValue * 0.72);
2532
- float insideLight = max(luminance(insideColor), insideValue * 0.72);
2533
- float interfaceLight = outsideLight * 0.68 + insideLight * 0.32;
2534
- float darkLine = smoothstep(0.40, 0.61, interfaceLight);
2535
- return mix(vec3(0.92, 0.93, 0.96), vec3(0.014, 0.013, 0.018), darkLine);
2536
- }
2537
-
2538
- void main() {
2539
- vec2 point = vUV * uRes;
2540
- int chosen = -1;
2541
- float chosenD = 1e6;
2542
- for (int i = 0; i < MAX_SHAPES; i++) {
2543
- if (i >= uShapeCount) break;
2544
- float d = shapeSdf(i, point);
2545
- if (d <= 2.1) { chosen = i; chosenD = d; }
2546
- }
2547
-
2548
- if (chosen < 0) {
2549
- outColor = vec4(0.0);
2550
- return;
2551
- }
2552
-
2553
- vec2 center = uShapeCenters[chosen];
2554
- vec2 halfSize = uShapeHalves[chosen];
2555
- float minHalf = min(halfSize.x, halfSize.y);
2556
- float e = 1.35;
2557
- float dx = shapeSdf(chosen, point + vec2(e, 0.0)) - shapeSdf(chosen, point - vec2(e, 0.0));
2558
- float dy = shapeSdf(chosen, point + vec2(0.0, e)) - shapeSdf(chosen, point - vec2(0.0, e));
2559
- vec2 normal = normalize(vec2(dx, dy) + vec2(0.0001));
2560
- float depth = clamp(-chosenD / max(12.0, minHalf * 0.62), 0.0, 1.0);
2561
- float refractionSupport = max(14.0, minHalf * 0.50);
2562
- float edgeCurve = pow(1.0 - smoothstep(0.0, refractionSupport, -chosenD), 2.2);
2563
- vec2 local = (point - center) / max(halfSize, vec2(1.0));
2564
-
2565
- float edgeDepth = max(-chosenD, 0.0);
2566
- float causticSupport = max(8.0, minHalf * uEdgeWidth);
2567
- float captureX = clamp(edgeDepth / causticSupport, 0.0, 1.0);
2568
- float causticT = 1.0 - smoothstep(0.0, causticSupport, edgeDepth);
2569
- float causticShade = causticT * causticT * (3.0 - 2.0 * causticT);
2570
- // The old double-smoothstep displacement flattened at the visible contour.
2571
- // Its source-coordinate derivative therefore changed sign twice, making a
2572
- // captured line turn back just before it touched the edge. A one-sided exit
2573
- // profile keeps a finite slope at the contour and relaxes to zero only on
2574
- // the inner side of the capture band, leaving a single optical fold.
2575
- float captureProfile = pow(1.0 - captureX, 1.64);
2576
- float refractionX = clamp(edgeDepth / refractionSupport, 0.0, 1.0);
2577
- float refractionProfile = pow(1.0 - refractionX, 2.2);
2578
- // The silhouette and optical superellipse deliberately differ in V2, but
2579
- // the visible contour must still exit along the silhouette normal. Blend to
2580
- // the broader optical field only after leaving the outer edge pixels.
2581
- float opticalNormalMix = smoothstep(0.12, 0.55, captureX);
2582
- vec2 bendNormal = normalize(mix(normal, opticalNormal(chosen, point, normal), opticalNormalMix));
2583
- vec2 inward = -bendNormal;
2584
- // Edge pull used to multiply Capture reach as a second public control. Keep
2585
- // its original default as an internal calibration so the default material
2586
- // retains the same displacement with one unambiguous capture parameter.
2587
- const float CAPTURE_REACH_SCALE = 1.24;
2588
- float captureDistance = uEdgeReach * CAPTURE_REACH_SCALE * captureProfile;
2589
- float shallowRefraction = uRefraction * refractionProfile * 0.32;
2590
- vec2 lensShift = inward * (shallowRefraction + captureDistance);
2591
- lensShift += -local * (uRefraction * 0.035) * smoothstep(0.16, 0.92, depth);
2592
- vec2 chromaShift = bendNormal * uDispersion * (0.32 + edgeCurve * 0.95);
2593
- vec2 uvR = (point + lensShift * (1.0 + uDispersion * 0.009) + chromaShift) / uRes;
2594
- vec2 uvG = (point + lensShift) / uRes;
2595
- vec2 uvB = (point + lensShift * (1.0 - uDispersion * 0.011) - chromaShift) / uRes;
2596
- // Reach controls where the sample comes from, not how fat a captured line
2597
- // becomes. Preserve the tuned reach=35 softness while preventing larger
2598
- // reaches from silently doubling the blur radius.
2599
- float causticBlur = min(0.7 + 1.13 * uDpr, 0.7 + captureDistance * 0.026);
2600
- // Frost is a ratio, not a fixed pixel radius. Resolve it against the
2601
- // component's short side so a small icon stays clear while a larger card
2602
- // naturally becomes denser and more opaque at the same material setting.
2603
- float shapeFrost = clamp(uShapeFrosts[chosen], 0.0, 1.0);
2604
- float shortSideCss = minHalf * 2.0 / max(uDpr, 1.0);
2605
- float sizeRatio = clamp(shortSideCss / 96.0, 0.28, 2.25);
2606
- float blurRadius = max(shapeFrost * shortSideCss
2607
- * (0.22 + depth * 0.55) * uDpr,
2608
- causticBlur);
2609
- float blurMix = clamp(shapeFrost * (0.56 + sizeRatio * 0.34)
2610
- + causticShade * 0.18, 0.0, 0.88);
2611
- vec3 sr = mix(backdrop(uvR), softBackdrop(uvR, blurRadius), blurMix);
2612
- vec3 sg = mix(backdrop(uvG), softBackdrop(uvG, blurRadius), blurMix);
2613
- vec3 sb = mix(backdrop(uvB), softBackdrop(uvB, blurRadius), blurMix);
2614
- vec3 transmitted = vec3(sr.r, sg.g, sb.b);
2615
-
2616
- float transmittedLum = luminance(transmitted);
2617
- vec3 bodyTarget = mix(vec3(0.030, 0.031, 0.038), vec3(0.94, 0.95, 0.97),
2618
- smoothstep(0.58, 0.82, transmittedLum));
2619
- transmitted = mix(transmitted, bodyTarget,
2620
- clamp(uBody * (0.034 + edgeCurve * 0.012), 0.0, 0.11));
2621
- transmitted = mix(vec3(luminance(transmitted)), transmitted, 1.0 - uBody * 0.045);
2622
-
2623
- float opticalPath = 0.26 + sqrt(depth) * 0.74;
2624
- transmitted *= exp(-vec3(0.018, 0.011, 0.004) * opticalPath * 2.4 * uAbsorption);
2625
- // Tinted Liquid Glass chooses one light/dark material for the whole
2626
- // component. Choosing per fragment lets high-contrast content punch a
2627
- // checkerboard through the surface instead of producing the coherent milky
2628
- // veil used by notifications and other legibility-first controls.
2629
- vec3 tintTarget = mix(vec3(0.055, 0.057, 0.066), vec3(0.975, 0.970, 0.955),
2630
- clamp(uShapeTintLights[chosen], 0.0, 1.0));
2631
- float tintOpacity = smoothstep(0.0, 1.5, uShapeTints[chosen]) * 0.78;
2632
- transmitted = mix(transmitted, tintTarget, tintOpacity * (0.88 + depth * 0.12));
2633
-
2634
- float mask = 1.0 - smoothstep(0.0, 1.35, chosenD);
2635
- float thinRim = exp(-pow((chosenD + 0.65) / 1.4, 2.0));
2636
- float innerRim = exp(-pow((chosenD + 6.2) / 3.8, 2.0));
2637
- float fresnel = pow(clamp(edgeCurve, 0.0, 1.0), 0.72);
2638
- // A softened environment probe keeps moving video/feed edges from turning
2639
- // into one-frame white flashes while preserving the local colour response.
2640
- vec3 reflected = softBackdrop((point + normal * (8.0 + uRefraction * 0.17)) / uRes, 5.2);
2641
- vec3 adaptiveRim = reflected * 1.45 + vec3(0.06, 0.035, 0.08);
2642
- adaptiveRim = mix(adaptiveRim, vec3(0.96, 0.97, 1.0), 0.24);
2643
- adaptiveRim = mix(adaptiveRim, vec3(0.035, 0.025, 0.045),
2644
- smoothstep(0.78, 0.98, luminance(reflected)) * 0.48);
2645
-
2646
- vec2 lightDir = normalize(uLightDirs[chosen] + vec2(0.0001));
2647
- float key = pow(max(dot(normal, lightDir), 0.0), 7.0) * fresnel;
2648
- float opposite = pow(max(dot(normal, -lightDir), 0.0), 5.0) * innerRim;
2649
- vec3 color = transmitted;
2650
- color = mix(color, adaptiveRim,
2651
- clamp((thinRim * 0.42 + innerRim * 0.18 + fresnel * 0.10)
2652
- * uRim * uReflection, 0.0, 0.72));
2653
- color += vec3(1.0, 0.82, 0.92) * key * 0.30 * uRim * uHighlight;
2654
- color *= 1.0 - opposite * 0.12 * uRim;
2655
- float echo = exp(-pow((chosenD + 11.0) / 5.5, 2.0));
2656
- vec3 echoColor = backdrop((point - normal * 11.0) / uRes);
2657
- color = mix(color, echoColor * 1.12, echo * 0.075 * uRim * uEcho);
2658
-
2659
- float edgeAA = max(fwidth(chosenD), 0.72);
2660
- float lineWidth = mix(0.34, 1.08, clamp(uHairWidth, 0.0, 1.0));
2661
- float strokeDistance = abs(chosenD + 0.10) - lineWidth * 0.5;
2662
- float hairline = 1.0 - smoothstep(-edgeAA * 0.72, edgeAA * 0.72, strokeDistance);
2663
- vec3 hairColor = interfaceColor(point, normal);
2664
- // The contrast line is the default interface. On the light-facing arc the
2665
- // specular key replaces it with the thin white highlight visible in the
2666
- // native material instead of merely brightening the black line underneath.
2667
- float hairHighlight = clamp(key * uHighlight * 2.5 * (0.65 + uRim * 0.60), 0.0, 0.96);
2668
- hairColor = mix(hairColor, vec3(0.985, 0.99, 1.0), hairHighlight);
2669
-
2670
- // Premultiplied layer composition exactly reproduces the prototype's two
2671
- // sequential mixes when drawn over the supplied backdrop, and also allows
2672
- // the same shader to work in overlay mode over a DOM/canvas backdrop.
2673
- float hairAlpha = clamp(hairline * uHairline * (0.22 + uRim * 0.20), 0.0, 1.0);
2674
- float alpha = hairAlpha + mask * (1.0 - hairAlpha);
2675
- vec3 premultiplied = hairColor * hairAlpha + color * mask * (1.0 - hairAlpha);
2676
- float surfaceOpacity = clamp(uShapeOpacities[chosen], 0.0, 1.0);
2677
- outColor = vec4(premultiplied * surfaceOpacity, alpha * surfaceOpacity);
2678
- }\`;
2679
-
2680
-
2681
- // ===== geometry.js =====
2682
- // CPU mirror of the shape maths in FS_GLASS.
2683
- //
2684
- // The shader is the source of truth for what the glass looks like, but callers
2685
- // also need the same geometry on the CPU: to know which component the pointer
2686
- // is over, and to split a large element list into groups that cannot influence
2687
- // each other. Keeping both in this module - free of any WebGL or DOM
2688
- // dependency - is what makes those rules unit-testable.
2689
- //
2690
- // Every length here is in the same unit as the element coordinates (CSS pixels
2691
- // for the public API). The renderer applies \`dpr\` separately.
2692
-
2693
- // The glass shader carries the group in uniform arrays of this length.
2694
- const MAX_GLASS_SHAPES = 16;
2695
-
2696
- // \`sdGroup\` weights each shape by exp(-(d - nearest) / scale). Past this many
2697
- // multiples of \`scale\` the contribution is below 1/3000 of a pixel of distance,
2698
- // which is far under the quantisation of the RGBA8 output. Shapes separated by
2699
- // more than that can be shaded in different draw calls without a visible seam.
2700
- const MERGE_INFLUENCE_SCALES = 8;
2701
-
2702
- // Matches the shader: scale = max(mergeRadius * 0.36, 0.01).
2703
- const MERGE_SCALE_RATIO = 0.36;
2704
-
2705
- // Apple's folder corner is capped at 23.5% of the short side.
2706
- const MAX_CORNER_RATIO = 0.235;
2707
-
2708
- const SHAPE_TYPES = Object.freeze({ rect: 0, folder: 0, pill: 1, circle: 2 });
2709
-
2710
- function shapeTypeOf(shape) {
2711
- return SHAPE_TYPES[shape] ?? 0;
2712
- }
2713
-
2714
- /** Corner radius the renderer will use for an element, before \`dpr\`. */
2715
- function cornerRadiusOf(element, materialRadius = 0) {
2716
- const short = Math.min(element.w ?? element.width ?? 0, element.h ?? element.height ?? 0);
2717
- return Math.min(element.radius ?? materialRadius, short * MAX_CORNER_RATIO);
2718
- }
2719
-
2720
- /** Superellipse rounded box, mirroring \`sdSquircle\` in the glass shader. */
2721
- function sdSquircle(px, py, halfX, halfY, radius, exponent) {
2722
- const qx = Math.abs(px) - halfX + radius;
2723
- const qy = Math.abs(py) - halfY + radius;
2724
- const mx = Math.max(qx, 0) + 1e-5;
2725
- const my = Math.max(qy, 0) + 1e-5;
2726
- const e = (mx ** exponent + my ** exponent) ** (1 / exponent);
2727
- return Math.min(Math.max(qx, qy), 0) + e - radius;
2728
- }
2729
-
2730
- /** Mirrors \`sdPrimitive\`: exact circle, exact capsule, or squircle folder. */
2731
- function sdPrimitive(px, py, halfX, halfY, shapeType, radius, squircle = 2) {
2732
- if (shapeType === 2) return Math.hypot(px, py) - Math.min(halfX, halfY);
2733
- if (shapeType === 1) return sdSquircle(px, py, halfX, halfY, Math.min(halfX, halfY), 2);
2734
- return sdSquircle(px, py, halfX, halfY, radius, Math.max(squircle, 2));
2735
- }
2736
-
2737
- function toShape(element, material) {
2738
- const w = element.w ?? element.width ?? element.size ?? 0;
2739
- const h = element.h ?? element.height ?? element.size ?? w;
2740
- return {
2741
- cx: (element.x ?? 0) + w / 2,
2742
- cy: (element.y ?? 0) + h / 2,
2743
- halfX: w / 2,
2744
- halfY: h / 2,
2745
- type: shapeTypeOf(element.shape),
2746
- radius: cornerRadiusOf({ ...element, w, h }, material.radius ?? 0),
2747
- };
2748
- }
2749
-
2750
- /**
2751
- * Signed distance to the fused silhouette of \`elements\`, in element
2752
- * coordinates. Mirrors \`sdAppleShape\`, including the global exponential
2753
- * smooth-min, so a hit test agrees with the pixels the shader produced.
2754
- *
2755
- * The smooth-min pulls the surface inward by at most \`scale * ln(2)\`, which is
2756
- * a quarter of \`mergeRadius\`. A gap between two components therefore only
2757
- * closes into a bridge while it is narrower than about \`mergeRadius / 2\`;
2758
- * beyond that the fusion distance only softens the approach.
2759
- */
2760
- function sdGroup(x, y, elements, material = {}, mergeRadius = material.mergeRadius ?? 0) {
2761
- if (!elements.length) return Infinity;
2762
- const shapes = elements.map((element) => toShape(element, material));
2763
- const squircle = material.squircle ?? 2;
2764
-
2765
- let nearest = Infinity;
2766
- const distances = shapes.map((shape) => {
2767
- const d = sdPrimitive(x - shape.cx, y - shape.cy, shape.halfX, shape.halfY,
2768
- shape.type, shape.radius, squircle);
2769
- if (d < nearest) nearest = d;
2770
- return d;
2771
- });
2772
-
2773
- if (!(mergeRadius >= 0.01) || shapes.length === 1) return nearest;
2774
-
2775
- const scale = Math.max(mergeRadius * MERGE_SCALE_RATIO, 0.01);
2776
- let sum = 0;
2777
- for (const d of distances) sum += Math.exp(-(d - nearest) / scale);
2778
- return nearest - scale * Math.log(Math.max(sum, 1e-6));
2779
- }
2780
-
2781
- /**
2782
- * The element whose own primitive is nearest to the point, or \`null\` when the
2783
- * point is outside the fused surface. \`tolerance\` grows the hit area, which is
2784
- * what a coarse pointer (touch) wants.
2785
- */
2786
- function hitTestElements(x, y, elements, material = {}, options = {}) {
2787
- const mergeRadius = options.fusion === false
2788
- ? 0
2789
- : (options.mergeRadius ?? material.mergeRadius ?? 0);
2790
- const tolerance = options.tolerance ?? 0;
2791
- const squircle = material.squircle ?? 2;
2792
-
2793
- // Separate elements are separate draw calls. The last one painted owns every
2794
- // overlapping pixel, regardless of how deeply the point lies inside an older
2795
- // element. Picking the most-negative distance here would make a large card
2796
- // steal clicks from a smaller button drawn on top of it.
2797
- if (options.fusion === false) {
2798
- for (let i = elements.length - 1; i >= 0; i--) {
2799
- const shape = toShape(elements[i], material);
2800
- const d = sdPrimitive(x - shape.cx, y - shape.cy, shape.halfX, shape.halfY,
2801
- shape.type, shape.radius, squircle);
2802
- if (d <= tolerance) return elements[i];
2803
- }
2804
- return null;
2805
- }
2806
-
2807
- // Mirror the renderer's connected-component splitting and 16-shape chunks.
2808
- // In particular, do not invent a smooth-union bridge across a chunk boundary
2809
- // that the GPU cannot draw. Iterate backwards because later passes composite
2810
- // over earlier ones when separately rendered groups overlap.
2811
- const groups = groupElements(elements, mergeRadius, MAX_GLASS_SHAPES).groups;
2812
- for (let groupIndex = groups.length - 1; groupIndex >= 0; groupIndex--) {
2813
- const group = groups[groupIndex];
2814
- if (sdGroup(x, y, group, material, mergeRadius) > tolerance) continue;
2815
-
2816
- let best = null;
2817
- let bestDistance = Infinity;
2818
- for (let i = group.length - 1; i >= 0; i--) {
2819
- const shape = toShape(group[i], material);
2820
- const d = sdPrimitive(x - shape.cx, y - shape.cy, shape.halfX, shape.halfY,
2821
- shape.type, shape.radius, squircle);
2822
- if (d < bestDistance) {
2823
- bestDistance = d;
2824
- best = group[i];
2825
- }
2826
- }
2827
- return best;
2828
- }
2829
- return null;
2830
- }
2831
-
2832
- /** Signed distance to the exact silhouette produced by the renderer's passes. */
2833
- function sdRenderedGroups(
2834
- x, y, elements, material = {}, mergeRadius = material.mergeRadius ?? 0,
2835
- ) {
2836
- const groups = groupElements(elements, mergeRadius, MAX_GLASS_SHAPES).groups;
2837
- if (!groups.length) return Infinity;
2838
- let bestDistance = Infinity;
2839
- for (const group of groups) {
2840
- const d = sdGroup(x, y, group, material, mergeRadius);
2841
- if (d < bestDistance) bestDistance = d;
2842
- }
2843
- return bestDistance;
2844
- }
2845
-
2846
- /** Axis-aligned gap between two element boxes; 0 when they overlap. */
2847
- function boxGap(a, b) {
2848
- const box = (element) => {
2849
- const w = Number(element.w ?? element.width ?? element.size ?? 0);
2850
- const h = Number(element.h ?? element.height ?? element.size ?? w);
2851
- return { x: Number(element.x ?? 0), y: Number(element.y ?? 0), w, h };
2852
- };
2853
- const aa = box(a);
2854
- const bb = box(b);
2855
- const dx = Math.max(0, Math.max(aa.x - (bb.x + bb.w), bb.x - (aa.x + aa.w)));
2856
- const dy = Math.max(0, Math.max(aa.y - (bb.y + bb.h), bb.y - (aa.y + aa.h)));
2857
- return Math.hypot(dx, dy);
2858
- }
2859
-
2860
- /**
2861
- * Split elements into sets that can be shaded independently.
2862
- *
2863
- * Two elements land in the same set when their boxes are close enough for the
2864
- * smooth-min to bridge them. Elements further apart than the influence radius
2865
- * contribute nothing measurable to each other's distance field, so drawing
2866
- * them in separate passes is visually identical to one fused pass.
2867
- */
2868
- function connectedElementGroups(elements, mergeRadius = 0) {
2869
- if (elements.length <= 1) return elements.length ? [elements.slice()] : [];
2870
- const reach = Math.max(0, mergeRadius) * MERGE_SCALE_RATIO * MERGE_INFLUENCE_SCALES;
2871
-
2872
- const parent = elements.map((_, i) => i);
2873
- const find = (i) => {
2874
- while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; }
2875
- return i;
2876
- };
2877
- for (let i = 0; i < elements.length; i++) {
2878
- for (let j = i + 1; j < elements.length; j++) {
2879
- if (boxGap(elements[i], elements[j]) <= reach) parent[find(i)] = find(j);
2880
- }
2881
- }
2882
-
2883
- const byRoot = new Map();
2884
- elements.forEach((element, i) => {
2885
- const root = find(i);
2886
- if (!byRoot.has(root)) byRoot.set(root, []);
2887
- byRoot.get(root).push(element);
2888
- });
2889
-
2890
- return [...byRoot.values()];
2891
- }
2892
-
2893
- /**
2894
- * Connected sets, further chunked so no group exceeds the shader's uniform
2895
- * arrays. A chunked set is the only lossy case: shapes that really do influence
2896
- * each other end up in different passes, so callers should surface a warning
2897
- * when \`groupElements\` reports it.
2898
- */
2899
- function groupElements(elements, mergeRadius = 0, maxPerGroup = MAX_GLASS_SHAPES) {
2900
- const connected = connectedElementGroups(elements, mergeRadius);
2901
- const groups = [];
2902
- let truncated = false;
2903
- for (const group of connected) {
2904
- if (group.length > maxPerGroup) truncated = true;
2905
- for (let i = 0; i < group.length; i += maxPerGroup) {
2906
- groups.push(group.slice(i, i + maxPerGroup));
2907
- }
2908
- }
2909
- return { groups, truncated };
2910
- }
2911
-
2912
-
2913
- // ===== v2-geometry.js =====
2914
- // CPU mirror of the V2 transparent shader's geometry. V2 keeps its independent
2915
- // optical material and non-fusing passes, but uses the same three visual
2916
- // primitives as V1: folder/rect, capsule and circle.
2917
-
2918
- const SHAPE_TYPES_V2 = Object.freeze({ rect: 0, folder: 0, pill: 1, circle: 2 });
2919
-
2920
- function shapeTypeOfV2(shape) {
2921
- return SHAPE_TYPES_V2[shape] ?? 0;
2922
- }
2923
-
2924
- function sdRoundBoxV2(px, py, halfX, halfY, radius) {
2925
- const r = Math.min(radius, halfX, halfY);
2926
- const qx = Math.abs(px) - halfX + r;
2927
- const qy = Math.abs(py) - halfY + r;
2928
- return Math.min(Math.max(qx, qy), 0) + Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) - r;
2929
- }
2930
-
2931
- function smoothUnionV2(d1, d2, radius) {
2932
- if (!(radius > 0)) return Math.min(d1, d2);
2933
- const h = Math.max(0, Math.min(1, 0.5 + 0.5 * (d2 - d1) / radius));
2934
- return d2 * (1 - h) + d1 * h - radius * h * (1 - h);
2935
- }
2936
-
2937
- function cornerRadiusV2(element, roundness = 0.47) {
2938
- const width = Number(element.w ?? element.width ?? element.size ?? 0);
2939
- const height = Number(element.h ?? element.height ?? element.size ?? width);
2940
- // A caller may provide a CSS-pixel radius for surfaces that must register
2941
- // to an external frame (for example the iPhone screen mask). Keep the V2
2942
- // material ratio as the default for authored components, but honour an
2943
- // explicit radius when exact geometry matters.
2944
- if (Number.isFinite(element.radius)) {
2945
- return Math.max(0, Math.min(Number(element.radius), Math.min(width, height) * 0.5));
2946
- }
2947
- return Math.min(width, height) * 0.5 * roundness;
2948
- }
2949
-
2950
- function sdElementV2(x, y, element, material = {}) {
2951
- const width = Number(element.w ?? element.width ?? element.size ?? 0);
2952
- const height = Number(element.h ?? element.height ?? element.size ?? width);
2953
- const halfX = width / 2;
2954
- const halfY = height / 2;
2955
- const px = x - Number(element.x ?? 0) - halfX;
2956
- const py = y - Number(element.y ?? 0) - halfY;
2957
- const kind = shapeTypeOfV2(element.shape);
2958
- const radius = cornerRadiusV2({ ...element, w: width, h: height }, material.roundness ?? 0.47);
2959
-
2960
- if (kind === 1) return sdRoundBoxV2(px, py, halfX, halfY, Math.min(halfX, halfY));
2961
- if (kind === 2) return Math.hypot(px, py) - Math.min(halfX, halfY);
2962
- return sdRoundBoxV2(px, py, halfX, halfY, radius);
2963
- }
2964
-
2965
- function distanceToElementsV2(x, y, elements, material = {}) {
2966
- let nearest = Infinity;
2967
- for (const element of elements) nearest = Math.min(nearest, sdElementV2(x, y, element, material));
2968
- return nearest;
2969
- }
2970
-
2971
- function hitTestElementsV2(x, y, elements, material = {}, options = {}) {
2972
- const tolerance = options.tolerance ?? 0;
2973
- // The V2 shader resolves overlap by taking the last matching surface.
2974
- for (let i = elements.length - 1; i >= 0; i--) {
2975
- if (sdElementV2(x, y, elements[i], material) <= tolerance) return elements[i];
2976
- }
2977
- return null;
2978
- }
2979
-
2980
-
2981
- // ===== v2-material.js =====
2982
- // V2 is the clear optical material from the transparent renderer. These
2983
- // values deliberately live outside material.js: similarly named V1 controls
2984
- // (notably dispersion and edgeWidth) use different units and shader maths.
2985
- const DEFAULT_MATERIAL_V2 = Object.freeze({
2986
- refraction: 90,
2987
- edgeReach: 0,
2988
- edgeWidth: 0,
2989
- dispersion: 2.0,
2990
- // Dimensionless softness ratio. The shader multiplies this by each
2991
- // component's short side, so the same value stays delicate on small icons
2992
- // and becomes denser on larger cards.
2993
- frost: 0.18,
2994
- body: 0.72,
2995
- absorption: 0.58,
2996
- tint: 0,
2997
- rim: 0.72,
2998
- reflection: 0.68,
2999
- highlight: 0.38,
3000
- lightAngle: 136,
3001
- echo: 0.28,
3002
- hairline: 0.92,
3003
- hairWidth: 0.52,
3004
- roundness: 0.47,
3005
- });
3006
-
3007
- const REDUCED_TRANSPARENCY_MATERIAL_V2 = Object.freeze({
3008
- refraction: 0,
3009
- edgeReach: 0,
3010
- dispersion: 0,
3011
- frost: 0,
3012
- body: 1.5,
3013
- tint: 1.35,
3014
- reflection: 0.18,
3015
- highlight: 0.12,
3016
- echo: 0,
3017
- });
3018
-
3019
- const SLIDERS_V2 = Object.freeze([
3020
- ['refraction', 0, 110, 1],
3021
- ['edgeReach', 0, 160, 1],
3022
- ['edgeWidth', 0, 0.55, 0.01],
3023
- ['dispersion', 0, 7, 0.1],
3024
- ['frost', 0, 1, 0.01],
3025
- ['body', 0, 1.5, 0.01],
3026
- ['absorption', 0, 2, 0.01],
3027
- ['tint', 0, 1.5, 0.01],
3028
- ['rim', 0, 1, 0.01],
3029
- ['reflection', 0, 1.5, 0.01],
3030
- ['highlight', 0, 1.5, 0.01],
3031
- ['lightAngle', -180, 180, 1],
3032
- ['echo', 0, 1.5, 0.01],
3033
- ['hairline', 0, 1.5, 0.01],
3034
- ['hairWidth', 0, 1, 0.01],
3035
- ['roundness', 0.05, 0.6, 0.01],
3036
- ]);
3037
-
3038
- /** Return a fresh V2 material. No V1 preset or parameter conversion is used. */
3039
- function getDefaultMaterialV2() {
3040
- return { ...DEFAULT_MATERIAL_V2 };
3041
- }
3042
-
3043
- function makeMaterialV2(overrides = {}) {
3044
- if (typeof overrides === 'string') {
3045
- throw new TypeError('Liquid Glass V2 does not use V1 preset names.');
3046
- }
3047
- const unknown = Object.keys(overrides || {}).filter((key) => !(key in DEFAULT_MATERIAL_V2));
3048
- if (unknown.length) {
3049
- throw new TypeError(\`Unknown Liquid Glass V2 material parameter\${unknown.length === 1 ? '' : 's'}: \${unknown.join(', ')}\`);
3050
- }
3051
- return { ...getDefaultMaterialV2(), ...(overrides || {}) };
3052
- }
3053
-
3054
-
3055
- // ===== renderer.js =====
3056
-
3057
-
3058
-
3059
-
3060
- function compile(gl, type, src) {
3061
- const s = gl.createShader(type);
3062
- gl.shaderSource(s, src);
3063
- gl.compileShader(s);
3064
- if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
3065
- const log = gl.getShaderInfoLog(s);
3066
- gl.deleteShader(s);
3067
- throw new Error(log + '\\n' + src);
3068
- }
3069
- return s;
3070
- }
3071
-
3072
- function program(gl, vs, fs) {
3073
- const p = gl.createProgram();
3074
- const vertex = compile(gl, gl.VERTEX_SHADER, vs);
3075
- let fragment;
3076
- try {
3077
- fragment = compile(gl, gl.FRAGMENT_SHADER, fs);
3078
- } catch (error) {
3079
- gl.deleteShader(vertex);
3080
- gl.deleteProgram(p);
3081
- throw error;
3082
- }
3083
- gl.attachShader(p, vertex);
3084
- gl.attachShader(p, fragment);
3085
- gl.linkProgram(p);
3086
- // The shader objects only exist to build the program; keeping them alive
3087
- // holds on to driver memory for the lifetime of the renderer.
3088
- gl.detachShader(p, vertex);
3089
- gl.detachShader(p, fragment);
3090
- gl.deleteShader(vertex);
3091
- gl.deleteShader(fragment);
3092
- if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
3093
- const log = gl.getProgramInfoLog(p);
3094
- gl.deleteProgram(p);
3095
- throw new Error(log);
3096
- }
3097
- const loc = {};
3098
- const n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS);
3099
- for (let i = 0; i < n; i++) {
3100
- const name = gl.getActiveUniform(p, i).name.replace('[0]', '');
3101
- loc[name] = gl.getUniformLocation(p, name);
3102
- }
3103
- return { p, loc };
3104
- }
3105
-
3106
- const MIPS = 7;
3107
-
3108
- class GlassRenderer {
3109
- constructor(canvas, options = {}) {
3110
- const gl = canvas.getContext('webgl2', {
3111
- alpha: Boolean(options.alpha), antialias: false, premultipliedAlpha: true,
3112
- // Reading the canvas back (screenshots, toDataURL) needs the drawing
3113
- // buffer preserved, but it also stops the driver from discarding it
3114
- // between frames. Off by default; the tooling turns it on explicitly.
3115
- preserveDrawingBuffer: Boolean(options.preserveDrawingBuffer),
3116
- });
3117
- if (!gl) throw new Error('WebGL2 unavailable');
3118
- this.gl = gl;
3119
- this.canvas = canvas;
3120
- this.materialVersion = options.materialVersion === 2 ? 2 : 1;
3121
- // Set while the GPU context is gone. Every GL call in this class is a no-op
3122
- // until \`restore()\` rebuilds the resources, so a lost context degrades to a
3123
- // frozen surface instead of an exception storm.
3124
- this.lost = false;
3125
-
3126
- this.tex = null;
3127
- this.blurTex = null;
3128
- this.wallpapers = [];
3129
- this.fbos = [];
3130
- this.blurFbos = [];
3131
- this.mipLevels = 0;
3132
- this.w = 0;
3133
- this.h = 0;
3134
- this.createResources();
3135
- }
3136
-
3137
- createResources() {
3138
- const gl = this.gl;
3139
- this.quad = gl.createVertexArray();
3140
- gl.bindVertexArray(this.quad);
3141
- this.quadBuffer = gl.createBuffer();
3142
- gl.bindBuffer(gl.ARRAY_BUFFER, this.quadBuffer);
3143
- gl.bufferData(gl.ARRAY_BUFFER,
3144
- new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW);
3145
- gl.enableVertexAttribArray(0);
3146
- gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
3147
- gl.bindVertexArray(null);
3148
-
3149
- this.progWall = program(gl, VS_FULLSCREEN, FS_WALLPAPER);
3150
- this.progDown = program(gl, VS_FULLSCREEN, FS_DOWN);
3151
- this.progUp = program(gl, VS_FULLSCREEN, FS_UP);
3152
- this.progBlit = program(gl, VS_FULLSCREEN, FS_BLIT);
3153
- this.progGlass = program(gl, VS_GLASS,
3154
- this.materialVersion === 2 ? FS_GLASS_V2 : FS_GLASS);
3155
-
3156
- // FS_WALLPAPER always has a sampler, even for its procedural path. Binding
3157
- // the backdrop mip texture while rendering into that same texture is an
3158
- // illegal feedback loop, so keep a complete inert texture for uUseImage=0.
3159
- this.fallbackTexture = gl.createTexture();
3160
- gl.bindTexture(gl.TEXTURE_2D, this.fallbackTexture);
3161
- gl.texImage2D(
3162
- gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
3163
- new Uint8Array([0, 0, 0, 255]),
3164
- );
3165
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
3166
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
3167
- gl.bindTexture(gl.TEXTURE_2D, null);
3168
- }
3169
-
3170
- releaseResources() {
3171
- const gl = this.gl;
3172
- for (const entry of [
3173
- this.progWall, this.progDown, this.progUp, this.progBlit, this.progGlass,
3174
- ]) {
3175
- if (entry) gl.deleteProgram(entry.p);
3176
- }
3177
- this.progWall = null;
3178
- this.progDown = null;
3179
- this.progUp = null;
3180
- this.progBlit = null;
3181
- this.progGlass = null;
3182
- if (this.quad) gl.deleteVertexArray(this.quad);
3183
- if (this.quadBuffer) gl.deleteBuffer(this.quadBuffer);
3184
- if (this.fallbackTexture) gl.deleteTexture(this.fallbackTexture);
3185
- this.quad = null;
3186
- this.quadBuffer = null;
3187
- this.fallbackTexture = null;
3188
- }
3189
-
3190
- releaseTargets() {
3191
- const gl = this.gl;
3192
- if (this.tex) gl.deleteTexture(this.tex);
3193
- if (this.blurTex) gl.deleteTexture(this.blurTex);
3194
- this.fbos.forEach((framebuffer) => gl.deleteFramebuffer(framebuffer));
3195
- this.blurFbos.forEach((framebuffer) => gl.deleteFramebuffer(framebuffer));
3196
- this.tex = null;
3197
- this.blurTex = null;
3198
- this.fbos = [];
3199
- this.blurFbos = [];
3200
- this.mipLevels = 0;
3201
- }
3202
-
3203
- // Drops every handle without touching the GPU: after a context loss the ids
3204
- // are already invalid and deleting them is meaningless.
3205
- handleContextLost() {
3206
- this.lost = true;
3207
- this.progWall = null;
3208
- this.progDown = null;
3209
- this.progUp = null;
3210
- this.progBlit = null;
3211
- this.progGlass = null;
3212
- this.quad = null;
3213
- this.quadBuffer = null;
3214
- this.fallbackTexture = null;
3215
- this.tex = null;
3216
- this.blurTex = null;
3217
- this.fbos = [];
3218
- this.blurFbos = [];
3219
- for (const entry of this.wallpapers) {
3220
- entry.texture = null;
3221
- entry.ready = false;
3222
- entry.width = 0;
3223
- entry.height = 0;
3224
- }
3225
- }
3226
-
3227
- // Rebuilds programs, render targets and backdrop textures after the browser
3228
- // restores the context. The WebGL2 context object itself is reused per spec,
3229
- // so only the resources have to be recreated.
3230
- restore() {
3231
- if (!this.lost) return this;
3232
- this.lost = false;
3233
- this.createResources();
3234
- const { w, h } = this;
3235
- this.w = 0;
3236
- this.h = 0;
3237
- if (w > 0 && h > 0) this.resize(w, h);
3238
- this.createWallpaperTextures();
3239
- return this;
3240
- }
3241
-
3242
- hasLiveBackdrop() {
3243
- return this.wallpapers.some((entry) => entry.update === 'live');
3244
- }
3245
-
3246
- sourceSize(source) {
3247
- return [
3248
- Number(source?.videoWidth || source?.naturalWidth || source?.width || 0),
3249
- Number(source?.videoHeight || source?.naturalHeight || source?.height || 0),
3250
- ];
3251
- }
3252
-
3253
- uploadWallpaper(entry, forceAllocation = false) {
3254
- if (this.lost || !entry.texture) return false;
3255
- const [width, height] = this.sourceSize(entry.source);
3256
- if (!(width > 0) || !(height > 0)) return false;
3257
-
3258
- const gl = this.gl;
3259
- gl.bindTexture(gl.TEXTURE_2D, entry.texture);
3260
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
3261
- if (!forceAllocation && entry.ready && entry.width === width && entry.height === height) {
3262
- gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, entry.source);
3263
- } else {
3264
- gl.texImage2D(
3265
- gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, gl.RGBA, gl.UNSIGNED_BYTE, entry.source,
3266
- );
3267
- }
3268
- gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
3269
- entry.width = width;
3270
- entry.height = height;
3271
- entry.ready = true;
3272
- return true;
3273
- }
3274
-
3275
- resize(w, h) {
3276
- w = Math.max(1, Math.round(w));
3277
- h = Math.max(1, Math.round(h));
3278
- if (this.lost) { this.w = w; this.h = h; return; }
3279
- if (w === this.w && h === this.h) return;
3280
- const gl = this.gl;
3281
- this.w = w; this.h = h;
3282
- this.canvas.width = w; this.canvas.height = h;
3283
-
3284
- this.releaseTargets();
3285
-
3286
- // texStorage2D rejects a level count larger than the size can represent.
3287
- // Seven levels are useful on a full-screen surface, but a 32px icon only
3288
- // has six (32, 16, 8, 4, 2, 1).
3289
- this.mipLevels = Math.min(MIPS, Math.floor(Math.log2(Math.max(w, h))) + 1);
3290
-
3291
- const createMipTexture = () => {
3292
- const texture = gl.createTexture();
3293
- gl.bindTexture(gl.TEXTURE_2D, texture);
3294
- // Sampling an sRGB texture decodes RGB to linear; writing to the sRGB
3295
- // attachment encodes it again. Alpha remains linear, preserving the
3296
- // optical-density side channel.
3297
- gl.texStorage2D(gl.TEXTURE_2D, this.mipLevels, gl.SRGB8_ALPHA8, w, h);
3298
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
3299
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3300
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3301
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3302
- return texture;
3303
- };
3304
-
3305
- this.tex = createMipTexture();
3306
- this.blurTex = createMipTexture();
3307
- this.fbos = [];
3308
- this.blurFbos = [];
3309
- for (let i = 0; i < this.mipLevels; i++) {
3310
- const f = gl.createFramebuffer();
3311
- gl.bindFramebuffer(gl.FRAMEBUFFER, f);
3312
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.tex, i);
3313
- this.fbos.push(f);
3314
-
3315
- const blurFbo = gl.createFramebuffer();
3316
- gl.bindFramebuffer(gl.FRAMEBUFFER, blurFbo);
3317
- gl.framebufferTexture2D(
3318
- gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.blurTex, i,
3319
- );
3320
- this.blurFbos.push(blurFbo);
3321
- }
3322
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
3323
- }
3324
-
3325
- // (Re)allocates a GL texture per backdrop entry and uploads its first frame.
3326
- createWallpaperTextures() {
3327
- if (this.lost) return;
3328
- const gl = this.gl;
3329
- for (const entry of this.wallpapers) {
3330
- if (entry.texture) gl.deleteTexture(entry.texture);
3331
- entry.texture = gl.createTexture();
3332
- entry.ready = false;
3333
- entry.width = 0;
3334
- entry.height = 0;
3335
- gl.bindTexture(gl.TEXTURE_2D, entry.texture);
3336
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
3337
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3338
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3339
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3340
- this.uploadWallpaper(entry, true);
3341
- }
3342
- gl.bindTexture(gl.TEXTURE_2D, null);
3343
- }
3344
-
3345
- setWallpapers(images, options = {}) {
3346
- const gl = this.gl;
3347
- const update = options.update === 'live' ? 'live' : 'static';
3348
- if (!this.lost) this.wallpapers.forEach((entry) => gl.deleteTexture(entry.texture));
3349
- this.wallpapers = images.map((source) => ({
3350
- texture: null,
3351
- source,
3352
- update,
3353
- ready: false,
3354
- width: 0,
3355
- height: 0,
3356
- }));
3357
- this.createWallpaperTextures();
3358
- }
3359
-
3360
- refreshWallpapers(force = false) {
3361
- if (this.lost) return;
3362
- for (const entry of this.wallpapers) {
3363
- if (force || entry.update === 'live') this.uploadWallpaper(entry);
3364
- }
3365
- this.gl.bindTexture(this.gl.TEXTURE_2D, null);
3366
- }
3367
-
3368
- mipSize(level) {
3369
- return [Math.max(1, this.w >> level), Math.max(1, this.h >> level)];
3370
- }
3371
-
3372
- // Renders the backdrop into mip 0 and builds the progressively blurred chain.
3373
- buildBackdrop(scene, zoom = 1) {
3374
- if (this.lost || !this.fbos.length) return;
3375
- const gl = this.gl;
3376
- this.refreshWallpapers();
3377
- gl.bindVertexArray(this.quad);
3378
- gl.disable(gl.BLEND);
3379
-
3380
- gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbos[0]);
3381
- gl.viewport(0, 0, this.w, this.h);
3382
- gl.useProgram(this.progWall.p);
3383
- gl.uniform2f(this.progWall.loc.uRes, this.w, this.h);
3384
- gl.uniform1i(this.progWall.loc.uScene, scene);
3385
- gl.uniform1f(this.progWall.loc.uZoom, zoom);
3386
- const wallpaper = this.wallpapers[scene];
3387
- gl.activeTexture(gl.TEXTURE1);
3388
- gl.bindTexture(gl.TEXTURE_2D, wallpaper?.ready ? wallpaper.texture : this.fallbackTexture);
3389
- gl.uniform1i(this.progWall.loc.uWallpaper, 1);
3390
- gl.uniform1i(this.progWall.loc.uUseImage, wallpaper?.ready ? 1 : 0);
3391
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
3392
-
3393
- gl.useProgram(this.progDown.p);
3394
- gl.uniform1i(this.progDown.loc.uTex, 0);
3395
- gl.activeTexture(gl.TEXTURE0);
3396
- gl.bindTexture(gl.TEXTURE_2D, this.tex);
3397
- for (let i = 1; i < this.mipLevels; i++) {
3398
- const [sw, sh] = this.mipSize(i - 1);
3399
- const [dw, dh] = this.mipSize(i);
3400
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, i - 1);
3401
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, i - 1);
3402
- gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbos[i]);
3403
- gl.viewport(0, 0, dw, dh);
3404
- gl.uniform2f(this.progDown.loc.uTexel, 1 / sw, 1 / sh);
3405
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
3406
- }
3407
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, 0);
3408
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, this.mipLevels - 1);
3409
-
3410
- // Seed the coarsest reconstructed level, then walk back toward full
3411
- // resolution with a tent filter. Restricting BASE/MAX_LEVEL keeps sampling
3412
- // a different mip image from the one attached for drawing, avoiding a
3413
- // framebuffer feedback loop while retaining one filterable texture chain.
3414
- const last = this.mipLevels - 1;
3415
- const [lastW, lastH] = this.mipSize(last);
3416
- gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.fbos[last]);
3417
- gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.blurFbos[last]);
3418
- gl.blitFramebuffer(
3419
- 0, 0, lastW, lastH, 0, 0, lastW, lastH, gl.COLOR_BUFFER_BIT, gl.NEAREST,
3420
- );
3421
-
3422
- gl.bindVertexArray(this.quad);
3423
- gl.useProgram(this.progUp.p);
3424
- gl.uniform1i(this.progUp.loc.uLow, 0);
3425
- gl.uniform1i(this.progUp.loc.uHigh, 1);
3426
- for (let i = last - 1; i >= 0; i--) {
3427
- const [lowW, lowH] = this.mipSize(i + 1);
3428
- const [dw, dh] = this.mipSize(i);
3429
-
3430
- gl.activeTexture(gl.TEXTURE0);
3431
- gl.bindTexture(gl.TEXTURE_2D, this.blurTex);
3432
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, i + 1);
3433
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, i + 1);
3434
-
3435
- gl.activeTexture(gl.TEXTURE1);
3436
- gl.bindTexture(gl.TEXTURE_2D, this.tex);
3437
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, i);
3438
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, i);
3439
-
3440
- gl.bindFramebuffer(gl.FRAMEBUFFER, this.blurFbos[i]);
3441
- gl.viewport(0, 0, dw, dh);
3442
- gl.uniform2f(this.progUp.loc.uLowTexel, 1 / lowW, 1 / lowH);
3443
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
3444
- }
3445
-
3446
- gl.activeTexture(gl.TEXTURE0);
3447
- gl.bindTexture(gl.TEXTURE_2D, this.blurTex);
3448
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, 0);
3449
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, this.mipLevels - 1);
3450
- gl.activeTexture(gl.TEXTURE1);
3451
- gl.bindTexture(gl.TEXTURE_2D, this.tex);
3452
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, 0);
3453
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, this.mipLevels - 1);
3454
- }
3455
-
3456
- // Draws the sharp backdrop to the screen.
3457
- drawBackdrop() {
3458
- if (this.lost || !this.tex) return;
3459
- const gl = this.gl;
3460
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
3461
- gl.viewport(0, 0, this.w, this.h);
3462
- gl.disable(gl.BLEND);
3463
- gl.bindVertexArray(this.quad);
3464
- gl.useProgram(this.progBlit.p);
3465
- gl.activeTexture(gl.TEXTURE0);
3466
- gl.bindTexture(gl.TEXTURE_2D, this.tex);
3467
- gl.uniform1i(this.progBlit.loc.uTex, 0);
3468
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
3469
- }
3470
-
3471
- // Clears the visible framebuffer while preserving the offscreen backdrop
3472
- // texture. Used when the canvas overlays an existing DOM/canvas backdrop.
3473
- clearOutput() {
3474
- if (this.lost) return;
3475
- const gl = this.gl;
3476
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
3477
- gl.viewport(0, 0, this.w, this.h);
3478
- gl.disable(gl.BLEND);
3479
- gl.clearColor(0, 0, 0, 0);
3480
- gl.clear(gl.COLOR_BUFFER_BIT);
3481
- }
3482
-
3483
- // elements: {x, y, w, h, shape} in CSS pixels, y measured from the TOP.
3484
- // The group is evaluated as a single smooth-union SDF. This is important:
3485
- // compositing independent glass draws can overlap, but can never produce the
3486
- // shared silhouette and continuous normals of one fused liquid surface.
3487
- drawGlassGroup(elements, m, dpr, mergeRadius = m.mergeRadius ?? 0) {
3488
- if (!elements.length || this.lost || !this.tex) return;
3489
-
3490
- const gl = this.gl;
3491
- const { loc, p } = this.progGlass;
3492
- const shapes = elements.slice(0, MAX_GLASS_SHAPES);
3493
-
3494
- const minX = Math.min(...shapes.map((element) => element.x));
3495
- const minY = Math.min(...shapes.map((element) => element.y));
3496
- const maxX = Math.max(...shapes.map((element) => element.x + element.w));
3497
- const maxY = Math.max(...shapes.map((element) => element.y + element.h));
3498
- const groupWidth = maxX - minX;
3499
- const groupHeight = maxY - minY;
3500
-
3501
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
3502
- gl.viewport(0, 0, this.w, this.h);
3503
- gl.enable(gl.BLEND);
3504
- gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
3505
- gl.bindVertexArray(this.quad);
3506
- gl.useProgram(p);
3507
-
3508
- const cx = (minX + groupWidth / 2) * dpr;
3509
- const cy = this.h - (minY + groupHeight / 2) * dpr;
3510
- const hw = (groupWidth / 2) * dpr;
3511
- const hh = (groupHeight / 2) * dpr;
3512
- const centers = new Float32Array(MAX_GLASS_SHAPES * 2);
3513
- const halves = new Float32Array(MAX_GLASS_SHAPES * 2);
3514
- const radii = new Float32Array(MAX_GLASS_SHAPES);
3515
- const types = new Int32Array(MAX_GLASS_SHAPES);
3516
-
3517
- shapes.forEach((element, i) => {
3518
- const short = Math.min(element.w, element.h);
3519
- centers[i * 2] = (element.x + element.w / 2) * dpr;
3520
- centers[i * 2 + 1] = this.h - (element.y + element.h / 2) * dpr;
3521
- halves[i * 2] = element.w / 2 * dpr;
3522
- halves[i * 2 + 1] = element.h / 2 * dpr;
3523
- radii[i] = Math.min(element.radius ?? m.radius, short * 0.235) * dpr;
3524
- types[i] = element.shape === 'pill' ? 1 : element.shape === 'circle' ? 2 : 0;
3525
- });
3526
-
3527
- gl.activeTexture(gl.TEXTURE0);
3528
- gl.bindTexture(gl.TEXTURE_2D, this.tex);
3529
- gl.uniform1i(loc.uSrc, 0);
3530
- gl.activeTexture(gl.TEXTURE2);
3531
- gl.bindTexture(gl.TEXTURE_2D, this.blurTex);
3532
- gl.uniform1i(loc.uBlurSrc, 2);
3533
- gl.uniform2f(loc.uRes, this.w, this.h);
3534
- gl.uniform2f(loc.uCenter, cx, cy);
3535
- gl.uniform2f(loc.uHalf, hw, hh);
3536
- gl.uniform1i(loc.uShapeCount, shapes.length);
3537
- gl.uniform2fv(loc.uShapeCenters, centers);
3538
- gl.uniform2fv(loc.uShapeHalves, halves);
3539
- gl.uniform1iv(loc.uShapeTypes, types);
3540
- gl.uniform1fv(loc.uShapeRadii, radii);
3541
- gl.uniform1f(loc.uMergeRadius, Math.max(0, mergeRadius) * dpr);
3542
- gl.uniform1f(loc.uPad, (m.shadowSize * 4 + Math.max(mergeRadius, 0) * 0.3 + 8) * dpr);
3543
- gl.uniform1f(loc.uSquircle, m.squircle);
3544
- gl.uniform1f(loc.uBevel, m.bevel * dpr);
3545
- gl.uniform1f(loc.uHeight, m.height * dpr);
3546
- gl.uniform1f(loc.uSizeAdaptation, m.sizeAdaptation ?? 1);
3547
- gl.uniform1f(loc.uIOR, m.ior);
3548
- gl.uniform1f(loc.uDispersion, m.dispersion);
3549
- gl.uniform1f(loc.uBlurPlateau, m.blurPlateau * dpr);
3550
- gl.uniform1f(loc.uBlurRim, m.blurRim * dpr);
3551
- gl.uniform1f(loc.uOpticalDensity, m.opticalDensity);
3552
- gl.uniform1f(loc.uMips, this.mipLevels);
3553
- gl.uniform1f(loc.uSpecular, m.specular);
3554
- gl.uniform1f(loc.uSpecPower, m.specPower);
3555
- gl.uniform1f(loc.uHighlightAdapt, m.highlightAdapt);
3556
- gl.uniform1f(loc.uHighlightWidth, m.highlightWidth);
3557
- gl.uniform1f(loc.uHighlightSharpness, m.highlightSharpness);
3558
- gl.uniform1f(loc.uHighlightBase, m.highlightBase);
3559
- gl.uniform1f(loc.uFresnel, m.fresnel);
3560
- gl.uniform1f(loc.uSat, m.saturation);
3561
- gl.uniform1f(loc.uBright, m.brightness);
3562
- gl.uniform1f(loc.uTintAmount, m.tintAmount);
3563
- gl.uniform3f(loc.uTintColor, ...m.tintColor);
3564
- gl.uniform1f(loc.uTintAdapt, m.tintAdapt ?? 0);
3565
- gl.uniform1f(loc.uShadow, m.shadow);
3566
- gl.uniform1f(loc.uShadowSize, m.shadowSize * dpr);
3567
- gl.uniform1f(loc.uShadowOffset, m.shadowOffset * dpr);
3568
- gl.uniform2f(loc.uLightDir, m.lightX, m.lightY);
3569
- gl.uniform1f(loc.uEdgeLine, m.edgeLine);
3570
- gl.uniform1f(loc.uEdgeWidth, m.edgeWidth * dpr);
3571
- gl.uniform1f(loc.uEdgeDark, m.edgeDark);
3572
- gl.uniform1f(loc.uRefractScale, m.refractScale);
3573
- gl.uniform1f(loc.uMeniscus, m.meniscus);
3574
- gl.uniform1i(loc.uDebug, m.debug | 0);
3575
-
3576
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
3577
- gl.disable(gl.BLEND);
3578
- }
3579
-
3580
- drawGlass(element, m, dpr) {
3581
- this.drawGlassGroup([element], m, dpr, 0);
3582
- }
3583
-
3584
- // V2 surfaces share V1's public silhouettes and backdrop/mip pipeline, but
3585
- // nothing from the material calculation. In particular, similarly named
3586
- // uniforms are filled using V2's own units: edgeWidth is a fraction,
3587
- // dispersion is a pixel split, and roundness is a short-half ratio.
3588
- drawGlassV2Group(elements, m, dpr, lightDirections = [], tintLights = []) {
3589
- if (!elements.length || this.lost || !this.tex) return;
3590
-
3591
- const gl = this.gl;
3592
- const { loc, p } = this.progGlass;
3593
- const shapes = elements.slice(0, MAX_GLASS_SHAPES);
3594
- const minX = Math.min(...shapes.map((element) => element.x));
3595
- const minY = Math.min(...shapes.map((element) => element.y));
3596
- const maxX = Math.max(...shapes.map((element) => element.x + element.w));
3597
- const maxY = Math.max(...shapes.map((element) => element.y + element.h));
3598
- const groupWidth = maxX - minX;
3599
- const groupHeight = maxY - minY;
3600
-
3601
- const centers = new Float32Array(MAX_GLASS_SHAPES * 2);
3602
- const halves = new Float32Array(MAX_GLASS_SHAPES * 2);
3603
- const radii = new Float32Array(MAX_GLASS_SHAPES);
3604
- const types = new Int32Array(MAX_GLASS_SHAPES);
3605
- const lights = new Float32Array(MAX_GLASS_SHAPES * 2);
3606
- const tints = new Float32Array(MAX_GLASS_SHAPES);
3607
- const tintTones = new Float32Array(MAX_GLASS_SHAPES);
3608
- const frosts = new Float32Array(MAX_GLASS_SHAPES);
3609
- const opacities = new Float32Array(MAX_GLASS_SHAPES);
3610
- shapes.forEach((element, i) => {
3611
- const short = Math.min(element.w, element.h);
3612
- centers[i * 2] = (element.x + element.w / 2) * dpr;
3613
- centers[i * 2 + 1] = this.h - (element.y + element.h / 2) * dpr;
3614
- halves[i * 2] = element.w / 2 * dpr;
3615
- halves[i * 2 + 1] = element.h / 2 * dpr;
3616
- // V2 normally derives the corner from its dimensionless roundness
3617
- // ratio. An explicit element radius keeps surfaces such as a phone
3618
- // screen exactly aligned with their external clip path.
3619
- radii[i] = Math.min(
3620
- element.radius ?? short * 0.5 * m.roundness,
3621
- short * 0.5,
3622
- ) * dpr;
3623
- types[i] = element.shape === 'pill' ? 1
3624
- : element.shape === 'circle' ? 2 : 0;
3625
- const direction = lightDirections[i] ?? [Math.SQRT1_2, Math.SQRT1_2];
3626
- lights[i * 2] = direction[0];
3627
- lights[i * 2 + 1] = direction[1];
3628
- tints[i] = element.tint ?? m.tint;
3629
- tintTones[i] = tintLights[i] ?? 1;
3630
- // V2 frost is a dimensionless ratio resolved in the shader against the
3631
- // component short side. V1 keeps its authored CSS-pixel blur lengths.
3632
- frosts[i] = element.frost ?? m.frost;
3633
- opacities[i] = element.opacity ?? 1;
3634
- });
3635
-
3636
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
3637
- gl.viewport(0, 0, this.w, this.h);
3638
- gl.enable(gl.BLEND);
3639
- gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
3640
- gl.bindVertexArray(this.quad);
3641
- gl.useProgram(p);
3642
-
3643
- gl.activeTexture(gl.TEXTURE0);
3644
- gl.bindTexture(gl.TEXTURE_2D, this.tex);
3645
- gl.uniform1i(loc.uSrc, 0);
3646
- gl.uniform2f(loc.uRes, this.w, this.h);
3647
- gl.uniform1f(loc.uDpr, dpr);
3648
- gl.uniform2f(loc.uCenter,
3649
- (minX + groupWidth / 2) * dpr,
3650
- this.h - (minY + groupHeight / 2) * dpr);
3651
- gl.uniform2f(loc.uHalf, groupWidth / 2 * dpr, groupHeight / 2 * dpr);
3652
- gl.uniform1f(loc.uPad, 4 * dpr);
3653
- gl.uniform1f(loc.uMips, this.mipLevels);
3654
- gl.uniform1i(loc.uShapeCount, shapes.length);
3655
- gl.uniform2fv(loc.uShapeCenters, centers);
3656
- gl.uniform2fv(loc.uShapeHalves, halves);
3657
- gl.uniform1iv(loc.uShapeTypes, types);
3658
- gl.uniform1fv(loc.uShapeRadii, radii);
3659
- gl.uniform1fv(loc.uShapeTints, tints);
3660
- gl.uniform1fv(loc.uShapeTintLights, tintTones);
3661
- gl.uniform1fv(loc.uShapeFrosts, frosts);
3662
- gl.uniform1fv(loc.uShapeOpacities, opacities);
3663
- gl.uniform2fv(loc.uLightDirs, lights);
3664
- gl.uniform1f(loc.uRefraction, m.refraction * dpr);
3665
- gl.uniform1f(loc.uEdgeReach, m.edgeReach * dpr);
3666
- gl.uniform1f(loc.uEdgeWidth, m.edgeWidth);
3667
- gl.uniform1f(loc.uDispersion, m.dispersion);
3668
- gl.uniform1f(loc.uBody, m.body);
3669
- gl.uniform1f(loc.uAbsorption, m.absorption);
3670
- gl.uniform1f(loc.uRim, m.rim);
3671
- gl.uniform1f(loc.uReflection, m.reflection);
3672
- gl.uniform1f(loc.uHighlight, m.highlight);
3673
- gl.uniform1f(loc.uEcho, m.echo);
3674
- gl.uniform1f(loc.uHairline, m.hairline);
3675
- gl.uniform1f(loc.uHairWidth, m.hairWidth);
3676
-
3677
- gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
3678
- gl.disable(gl.BLEND);
3679
- }
3680
-
3681
- destroy() {
3682
- if (this.lost) {
3683
- this.wallpapers = [];
3684
- return;
3685
- }
3686
- const gl = this.gl;
3687
- this.releaseTargets();
3688
- this.releaseResources();
3689
- this.wallpapers.forEach((entry) => gl.deleteTexture(entry.texture));
3690
- this.wallpapers = [];
3691
- }
3692
- }
3693
-
3694
-
3695
- // ===== v2.js =====
3696
-
3697
-
3698
-
3699
-
3700
-
3701
- const SHAPES_V2 = new Set(['folder', 'rect', 'pill', 'circle']);
3702
- const COMPOSITE_MODES = new Set(['replace', 'overlay']);
3703
- const BACKDROP_UPDATES = new Set(['auto', 'static', 'live']);
3704
- const REDUCED_TRANSPARENCY_QUERY = '(prefers-reduced-transparency: reduce)';
3705
-
3706
- function normalizeCompositeMode(mode) {
3707
- if (!COMPOSITE_MODES.has(mode)) throw new TypeError(\`Unknown liquid glass V2 composite mode: \${mode}\`);
3708
- return mode;
3709
- }
3710
-
3711
- function normalizeShape(shape) {
3712
- const normalized = shape === 'folderRect' ? 'rect' : shape;
3713
- if (!SHAPES_V2.has(normalized)) throw new TypeError(\`Unknown liquid glass V2 shape: \${shape}\`);
3714
- return normalized;
3715
- }
3716
-
3717
- function normalizeElement(input, index) {
3718
- const width = Number(input.w ?? input.width ?? input.size ?? 0);
3719
- const height = Number(input.h ?? input.height ?? input.size ?? width);
3720
- if (!(width > 0) || !(height > 0)) {
3721
- throw new TypeError('Liquid glass V2 elements need a positive width and height.');
3722
- }
3723
- const tint = input.tint == null ? undefined : Number(input.tint);
3724
- if (tint !== undefined && !Number.isFinite(tint)) {
3725
- throw new TypeError('Liquid glass V2 element tint must be a finite number.');
3726
- }
3727
- const frost = input.frost == null ? undefined : Number(input.frost);
3728
- if (frost !== undefined && !Number.isFinite(frost)) {
3729
- throw new TypeError('Liquid glass V2 element frost must be a finite number.');
3730
- }
3731
- const opacity = input.opacity == null ? undefined : Number(input.opacity);
3732
- if (opacity !== undefined && !Number.isFinite(opacity)) {
3733
- throw new TypeError('Liquid glass V2 element opacity must be a finite number.');
3734
- }
3735
- const tintTone = input.tintTone ?? 'auto';
3736
- if (!['auto', 'light', 'dark'].includes(tintTone)) {
3737
- throw new TypeError(\`Unknown liquid glass V2 tint tone: \${tintTone}\`);
3738
- }
3739
- return {
3740
- ...input,
3741
- id: input.id ?? \`glass-v2-\${index + 1}\`,
3742
- shape: normalizeShape(input.shape ?? 'rect'),
3743
- x: Number(input.x ?? 0),
3744
- y: Number(input.y ?? 0),
3745
- w: width,
3746
- h: height,
3747
- ...(tint === undefined ? {} : { tint }),
3748
- ...(frost === undefined ? {} : { frost }),
3749
- ...(opacity === undefined ? {} : { opacity }),
3750
- ...(input.tintTone == null ? {} : { tintTone }),
3751
- };
3752
- }
3753
-
3754
- function resolveImage(source) {
3755
- if (typeof source !== 'string') return Promise.resolve(source);
3756
- return new Promise((resolve, reject) => {
3757
- const image = new Image();
3758
- image.onload = () => resolve(image);
3759
- image.onerror = () => reject(new Error(\`Unable to load liquid glass V2 wallpaper: \${source}\`));
3760
- image.src = source;
3761
- });
3762
- }
3763
-
3764
- function isLiveBackdropSource(source) {
3765
- const tagName = source?.tagName?.toUpperCase();
3766
- return tagName === 'CANVAS' || tagName === 'VIDEO'
3767
- || source?.constructor?.name === 'OffscreenCanvas'
3768
- || source?.constructor?.name === 'VideoFrame';
3769
- }
3770
-
3771
- function resolveBackdropUpdate(source, update = 'auto') {
3772
- if (!BACKDROP_UPDATES.has(update)) {
3773
- throw new TypeError(\`Unknown liquid glass V2 backdrop update mode: \${update}\`);
3774
- }
3775
- return update === 'auto' ? (isLiveBackdropSource(source) ? 'live' : 'static') : update;
3776
- }
3777
-
3778
- function matchMediaSafe(query) {
3779
- return typeof globalThis.matchMedia === 'function' ? globalThis.matchMedia(query) : null;
3780
- }
3781
-
3782
- /**
3783
- * Clear optical Liquid Glass V2.
3784
- *
3785
- * This is a separate public class, not a mode on LiquidGlassWebGL. Its material
3786
- * values are never converted from V1. It intentionally shares V1's public
3787
- * shape silhouettes while refraction, chromatic split, tint and interface
3788
- * lighting continue to follow the independent V2 equations.
3789
- */
3790
- class LiquidGlassWebGLV2 {
3791
- static isSupported() {
3792
- if (typeof document === 'undefined') return false;
3793
- try {
3794
- const probe = document.createElement('canvas');
3795
- const gl = probe.getContext('webgl2');
3796
- if (!gl) return false;
3797
- gl.getExtension('WEBGL_lose_context')?.loseContext();
3798
- return true;
3799
- } catch {
3800
- return false;
3801
- }
3802
- }
3803
-
3804
- constructor(canvas, options = {}) {
3805
- if (!canvas || typeof canvas.getContext !== 'function') {
3806
- throw new TypeError('LiquidGlassWebGLV2 needs an HTMLCanvasElement.');
3807
- }
3808
- this.canvas = canvas;
3809
- this.version = 'v2';
3810
- this.compositeMode = normalizeCompositeMode(options.compositeMode ?? 'replace');
3811
- this.renderer = new GlassRenderer(canvas, {
3812
- alpha: this.compositeMode === 'overlay',
3813
- preserveDrawingBuffer: Boolean(options.preserveDrawingBuffer),
3814
- materialVersion: 2,
3815
- });
3816
- this.material = makeMaterialV2(options.material);
3817
- this.elements = [];
3818
- this.backdrops = [];
3819
- this.wallpaperIndex = 0;
3820
- this.wallpaperZoom = options.wallpaperZoom ?? 1;
3821
- this.running = false;
3822
- this.animationFrame = 0;
3823
- this.dirty = true;
3824
- this.backdropDirty = true;
3825
- this.lightFieldDirty = true;
3826
- this.lastFrame = { width: 0, height: 0, dpr: 0 };
3827
- this.warnedShapeLimit = false;
3828
- this.lightCanvas = null;
3829
- this.lightPixels = null;
3830
- this.lightSampleSize = 64;
3831
- this.smoothedLightDirections = new Map();
3832
- this.lastLightFieldUpdate = 0;
3833
- this.lastLightBlendTime = 0;
3834
- this.onContextLost = options.onContextLost ?? null;
3835
- this.onContextRestored = options.onContextRestored ?? null;
3836
-
3837
- this.respectReducedTransparency = options.respectReducedTransparency ?? true;
3838
- this.reducedTransparencyQuery = this.respectReducedTransparency
3839
- ? matchMediaSafe(REDUCED_TRANSPARENCY_QUERY) : null;
3840
- this.handleReducedTransparencyChange = () => {
3841
- this.markDirty();
3842
- this.render();
3843
- };
3844
- this.reducedTransparencyQuery?.addEventListener?.('change', this.handleReducedTransparencyChange);
3845
-
3846
- this.handleContextLost = (event) => {
3847
- event.preventDefault();
3848
- this.renderer.handleContextLost();
3849
- this.markBackdropDirty();
3850
- this.onContextLost?.(event);
3851
- };
3852
- this.handleContextRestored = (event) => {
3853
- this.renderer.restore();
3854
- this.markBackdropDirty();
3855
- this.lastFrame = { width: 0, height: 0, dpr: 0 };
3856
- this.onContextRestored?.(event);
3857
- this.render();
3858
- };
3859
- canvas.addEventListener('webglcontextlost', this.handleContextLost, false);
3860
- canvas.addEventListener('webglcontextrestored', this.handleContextRestored, false);
3861
-
3862
- this.resizeObserver = null;
3863
- if ((options.autoResize ?? true) && typeof globalThis.ResizeObserver === 'function') {
3864
- this.resizeObserver = new globalThis.ResizeObserver(() => {
3865
- this.lightFieldDirty = true;
3866
- this.markDirty();
3867
- this.render();
3868
- });
3869
- this.resizeObserver.observe(canvas);
3870
- }
3871
-
3872
- if (options.elements) this.setElements(options.elements, false);
3873
- if (options.wallpapers) this.setWallpapers(options.wallpapers, false);
3874
- if (options.backdrop) {
3875
- this.setBackdrop(options.backdrop, {
3876
- update: options.backdropUpdate,
3877
- autoStart: options.autoStart,
3878
- shouldRender: false,
3879
- });
3880
- }
3881
- }
3882
-
3883
- get contextLost() { return this.renderer.lost; }
3884
- get reducedTransparency() { return Boolean(this.reducedTransparencyQuery?.matches); }
3885
- get effectiveMaterial() {
3886
- return this.reducedTransparency
3887
- ? { ...this.material, ...REDUCED_TRANSPARENCY_MATERIAL_V2 }
3888
- : this.material;
3889
- }
3890
-
3891
- markDirty() {
3892
- this.dirty = true;
3893
- return this;
3894
- }
3895
-
3896
- markBackdropDirty() {
3897
- this.backdropDirty = true;
3898
- this.lightFieldDirty = true;
3899
- this.lastLightFieldUpdate = 0;
3900
- this.smoothedLightDirections.clear();
3901
- return this.markDirty();
3902
- }
3903
-
3904
- setElements(elements, shouldRender = true) {
3905
- this.elements = elements.map((element, index) => normalizeElement(element, index));
3906
- this.markDirty();
3907
- if (shouldRender) this.render();
3908
- return this;
3909
- }
3910
-
3911
- addElement(element, shouldRender = true) {
3912
- const normalized = normalizeElement(element, this.elements.length);
3913
- this.elements.push(normalized);
3914
- this.markDirty();
3915
- if (shouldRender) this.render();
3916
- return normalized.id;
3917
- }
3918
-
3919
- updateElement(id, patch, shouldRender = true) {
3920
- const index = this.elements.findIndex((element) => element.id === id);
3921
- if (index === -1) return this;
3922
- this.elements[index] = normalizeElement({ ...this.elements[index], ...patch }, index);
3923
- this.markDirty();
3924
- if (shouldRender) this.render();
3925
- return this;
3926
- }
3927
-
3928
- removeElement(id, shouldRender = true) {
3929
- this.elements = this.elements.filter((element) => element.id !== id);
3930
- this.markDirty();
3931
- if (shouldRender) this.render();
3932
- return this;
3933
- }
3934
-
3935
- setMaterial(material, shouldRender = true) {
3936
- if (typeof material === 'string') {
3937
- throw new TypeError('Liquid Glass V2 does not convert V1 preset names. Pass a V2 material object.');
3938
- }
3939
- this.material = makeMaterialV2({ ...this.material, ...(material || {}) });
3940
- this.markDirty();
3941
- if (shouldRender) this.render();
3942
- return this;
3943
- }
3944
-
3945
- setWallpapers(images, shouldRender = true) {
3946
- this.backdrops = images.slice();
3947
- this.renderer.setWallpapers(images, { update: 'static' });
3948
- this.markBackdropDirty();
3949
- if (shouldRender) this.render();
3950
- return this;
3951
- }
3952
-
3953
- async loadWallpapers(sources, shouldRender = true) {
3954
- const images = await Promise.all(sources.map(resolveImage));
3955
- return this.setWallpapers(images, shouldRender);
3956
- }
3957
-
3958
- async setWallpaper(source, shouldRender = true) {
3959
- return this.loadWallpapers([source], shouldRender);
3960
- }
3961
-
3962
- setBackdrop(source, options = {}) {
3963
- if (!source || typeof source === 'string') {
3964
- throw new TypeError('setBackdrop needs a CanvasImageSource. Use loadBackdrop for a URL.');
3965
- }
3966
- const update = resolveBackdropUpdate(source, options.update);
3967
- this.backdrops = [source];
3968
- this.renderer.setWallpapers([source], { update });
3969
- this.wallpaperIndex = 0;
3970
- this.markBackdropDirty();
3971
- if (options.autoStart ?? update === 'live') this.start();
3972
- if (options.shouldRender ?? true) this.render();
3973
- return this;
3974
- }
3975
-
3976
- async loadBackdrop(source, options = {}) {
3977
- const image = await resolveImage(source);
3978
- return this.setBackdrop(image, { ...options, update: options.update ?? 'static' });
3979
- }
3980
-
3981
- updateBackdrop(shouldRender = true) {
3982
- this.renderer.refreshWallpapers(true);
3983
- this.markBackdropDirty();
3984
- if (shouldRender) this.render();
3985
- return this;
3986
- }
3987
-
3988
- setWallpaperIndex(index, shouldRender = true) {
3989
- this.wallpaperIndex = Math.max(0, Math.floor(index));
3990
- this.markBackdropDirty();
3991
- if (shouldRender) this.render();
3992
- return this;
3993
- }
3994
-
3995
- distanceAt(x, y) {
3996
- return distanceToElementsV2(x, y, this.elements, this.material);
3997
- }
3998
-
3999
- hitTest(x, y, options = {}) {
4000
- return hitTestElementsV2(x, y, this.elements, this.material, options);
4001
- }
4002
-
4003
- pointerPosition(event) {
4004
- const rect = this.canvas.getBoundingClientRect();
4005
- const source = event.touches?.[0] ?? event.changedTouches?.[0] ?? event;
4006
- return { x: source.clientX - rect.left, y: source.clientY - rect.top };
4007
- }
4008
-
4009
- hitTestEvent(event, options = {}) {
4010
- const { x, y } = this.pointerPosition(event);
4011
- const tolerance = options.tolerance
4012
- ?? (event.pointerType && event.pointerType !== 'mouse' ? 8 : 0);
4013
- return this.hitTest(x, y, { ...options, tolerance });
4014
- }
4015
-
4016
- start() {
4017
- if (this.running) return this;
4018
- if (typeof globalThis.requestAnimationFrame !== 'function') {
4019
- throw new Error('LiquidGlassWebGLV2.start() requires requestAnimationFrame.');
4020
- }
4021
- this.running = true;
4022
- const tick = () => {
4023
- if (!this.running) return;
4024
- this.render();
4025
- this.animationFrame = globalThis.requestAnimationFrame(tick);
4026
- };
4027
- this.animationFrame = globalThis.requestAnimationFrame(tick);
4028
- return this;
4029
- }
4030
-
4031
- stop() {
4032
- this.running = false;
4033
- if (this.animationFrame && typeof globalThis.cancelAnimationFrame === 'function') {
4034
- globalThis.cancelAnimationFrame(this.animationFrame);
4035
- }
4036
- this.animationFrame = 0;
4037
- return this;
4038
- }
4039
-
4040
- resize(width = this.canvas.clientWidth || this.canvas.width || 1,
4041
- height = this.canvas.clientHeight || this.canvas.height || 1,
4042
- dpr = Math.min(globalThis.devicePixelRatio || 1, 2)) {
4043
- this.renderer.resize(Math.round(width * dpr), Math.round(height * dpr));
4044
- return { width, height, dpr };
4045
- }
4046
-
4047
- updateLightField() {
4048
- this.lightFieldDirty = false;
4049
- const source = this.backdrops[this.wallpaperIndex];
4050
- const sourceWidth = Number(source?.videoWidth || source?.naturalWidth || source?.width || 0);
4051
- const sourceHeight = Number(source?.videoHeight || source?.naturalHeight || source?.height || 0);
4052
- if (!(sourceWidth > 0) || !(sourceHeight > 0) || typeof document === 'undefined') {
4053
- this.lightPixels = null;
4054
- return;
4055
- }
4056
- if (!this.lightCanvas) this.lightCanvas = document.createElement('canvas');
4057
- const size = this.lightSampleSize;
4058
- this.lightCanvas.width = size;
4059
- this.lightCanvas.height = size;
4060
- const context = this.lightCanvas.getContext('2d', { willReadFrequently: true });
4061
- if (!context) { this.lightPixels = null; return; }
4062
- context.clearRect(0, 0, size, size);
4063
- const scale = Math.max(size / sourceWidth, size / sourceHeight) * this.wallpaperZoom;
4064
- const drawWidth = sourceWidth * scale;
4065
- const drawHeight = sourceHeight * scale;
4066
- try {
4067
- context.drawImage(source, (size - drawWidth) / 2, (size - drawHeight) / 2,
4068
- drawWidth, drawHeight);
4069
- this.lightPixels = context.getImageData(0, 0, size, size).data;
4070
- } catch {
4071
- // A cross-origin source can still be WebGL-sampleable with CORS while a
4072
- // browser refuses Canvas2D readback. The deterministic angle remains a
4073
- // complete fallback in that case.
4074
- this.lightPixels = null;
4075
- }
4076
- }
4077
-
4078
- sampleLuminance(x, y) {
4079
- if (!this.lightPixels) return 0.5;
4080
- const size = this.lightSampleSize;
4081
- const px = Math.max(0, Math.min(size - 1, Math.round(x * (size - 1))));
4082
- const py = Math.max(0, Math.min(size - 1, Math.round(y * (size - 1))));
4083
- const index = (py * size + px) * 4;
4084
- return (this.lightPixels[index] * 0.2126
4085
- + this.lightPixels[index + 1] * 0.7152
4086
- + this.lightPixels[index + 2] * 0.0722) / 255;
4087
- }
4088
-
4089
- lightDirection(element, width, height, fallbackAngle) {
4090
- const positions = [-0.34, 0, 0.34];
4091
- let gradientX = 0;
4092
- let gradientY = 0;
4093
- for (const sampleY of positions) {
4094
- for (const sampleX of positions) {
4095
- const x = (element.x + element.w * (0.5 + sampleX)) / Math.max(width, 1);
4096
- const y = (element.y + element.h * (0.5 + sampleY)) / Math.max(height, 1);
4097
- const light = this.sampleLuminance(x, y);
4098
- gradientX += sampleX * light;
4099
- gradientY += sampleY * light;
4100
- }
4101
- }
4102
- const radians = fallbackAngle * Math.PI / 180;
4103
- const fallbackX = Math.cos(radians);
4104
- const fallbackY = Math.sin(radians);
4105
- const contrast = Math.hypot(gradientX, gradientY) / positions.length;
4106
- if (contrast < 0.008) return [fallbackX, fallbackY];
4107
-
4108
- const length = Math.hypot(gradientX, gradientY) || 1;
4109
- const autoX = -gradientX / length;
4110
- const autoY = gradientY / length;
4111
- const rawStrength = Math.max(0, Math.min(1, (contrast - 0.015) / 0.13));
4112
- // Keep the environment influential without allowing a moving high-contrast
4113
- // edge to rotate the key light almost 180 degrees from one sample to the next.
4114
- const strength = rawStrength * rawStrength * (3 - 2 * rawStrength) * 0.58;
4115
- const mixedX = fallbackX * (1 - strength) + autoX * strength;
4116
- const mixedY = fallbackY * (1 - strength) + autoY * strength;
4117
- const mixedLength = Math.hypot(mixedX, mixedY) || 1;
4118
- return [mixedX / mixedLength, mixedY / mixedLength];
4119
- }
4120
-
4121
- tintLightForElement(element, width, height) {
4122
- if (element.tintTone === 'light') return 1;
4123
- if (element.tintTone === 'dark') return 0;
4124
- const positions = [-0.34, 0, 0.34];
4125
- let luminance = 0;
4126
- for (const sampleY of positions) {
4127
- for (const sampleX of positions) {
4128
- const x = (element.x + element.w * (0.5 + sampleX)) / Math.max(width, 1);
4129
- const y = (element.y + element.h * (0.5 + sampleY)) / Math.max(height, 1);
4130
- luminance += this.sampleLuminance(x, y);
4131
- }
4132
- }
4133
- const average = luminance / (positions.length * positions.length);
4134
- const t = Math.max(0, Math.min(1, (average - 0.22) / (0.50 - 0.22)));
4135
- return t * t * (3 - 2 * t);
4136
- }
4137
-
4138
- render(options = {}) {
4139
- if (this.renderer.lost) return this;
4140
- const width = this.canvas.clientWidth || this.canvas.width || 1;
4141
- const height = this.canvas.clientHeight || this.canvas.height || 1;
4142
- const dpr = Math.min(globalThis.devicePixelRatio || 1, 2);
4143
- const resized = width !== this.lastFrame.width || height !== this.lastFrame.height
4144
- || dpr !== this.lastFrame.dpr;
4145
- const liveBackdrop = this.renderer.hasLiveBackdrop();
4146
- if (!options.force && !this.dirty && !resized && !liveBackdrop) return this;
4147
-
4148
- this.resize(width, height, dpr);
4149
- const now = globalThis.performance?.now?.() ?? Date.now();
4150
- if (this.backdropDirty || resized || liveBackdrop) {
4151
- this.renderer.buildBackdrop(this.wallpaperIndex, this.wallpaperZoom);
4152
- // The optical backdrop remains fully live, but the low-resolution light
4153
- // probe runs at a steadier cadence. This decouples moving content from the
4154
- // white key highlight and removes single-frame direction spikes.
4155
- const refreshLiveLight = liveBackdrop && now - this.lastLightFieldUpdate >= 84;
4156
- if (this.lightFieldDirty || resized || refreshLiveLight) {
4157
- this.updateLightField();
4158
- this.lastLightFieldUpdate = now;
4159
- }
4160
- this.backdropDirty = false;
4161
- }
4162
- if (this.compositeMode === 'overlay') this.renderer.clearOutput();
4163
- else this.renderer.drawBackdrop();
4164
-
4165
- const material = this.effectiveMaterial;
4166
- const elapsed = this.lastLightBlendTime ? Math.min(100, now - this.lastLightBlendTime) : 100;
4167
- const blend = liveBackdrop ? 1 - Math.exp(-elapsed / 280) : 1;
4168
- const activeLightIds = new Set(this.elements.map((element) => element.id));
4169
- for (const id of this.smoothedLightDirections.keys()) {
4170
- if (!activeLightIds.has(id)) this.smoothedLightDirections.delete(id);
4171
- }
4172
- const lightDirections = this.elements.map((element) => {
4173
- const target = this.lightDirection(element, width, height, material.lightAngle);
4174
- const previous = this.smoothedLightDirections.get(element.id);
4175
- if (!previous || blend >= 1) {
4176
- this.smoothedLightDirections.set(element.id, target);
4177
- return target;
4178
- }
4179
- const mixedX = previous[0] * (1 - blend) + target[0] * blend;
4180
- const mixedY = previous[1] * (1 - blend) + target[1] * blend;
4181
- const length = Math.hypot(mixedX, mixedY) || 1;
4182
- const direction = [mixedX / length, mixedY / length];
4183
- this.smoothedLightDirections.set(element.id, direction);
4184
- return direction;
4185
- });
4186
- const tintLights = this.elements.map((element) => (
4187
- this.tintLightForElement(element, width, height)
4188
- ));
4189
- this.lastLightBlendTime = now;
4190
- if (this.elements.length > MAX_GLASS_SHAPES && !this.warnedShapeLimit) {
4191
- this.warnedShapeLimit = true;
4192
- console.warn(\`LiquidGlassWebGLV2: more than \${MAX_GLASS_SHAPES} shapes require multiple passes; overlapping shapes across a pass boundary may composite differently.\`);
4193
- }
4194
- for (let i = 0; i < this.elements.length; i += MAX_GLASS_SHAPES) {
4195
- this.renderer.drawGlassV2Group(
4196
- this.elements.slice(i, i + MAX_GLASS_SHAPES),
4197
- material,
4198
- dpr,
4199
- lightDirections.slice(i, i + MAX_GLASS_SHAPES),
4200
- tintLights.slice(i, i + MAX_GLASS_SHAPES),
4201
- );
4202
- }
4203
-
4204
- this.dirty = false;
4205
- this.lastFrame = { width, height, dpr };
4206
- return this;
4207
- }
4208
-
4209
- destroy() {
4210
- this.stop();
4211
- this.canvas.removeEventListener('webglcontextlost', this.handleContextLost, false);
4212
- this.canvas.removeEventListener('webglcontextrestored', this.handleContextRestored, false);
4213
- this.reducedTransparencyQuery?.removeEventListener?.('change', this.handleReducedTransparencyChange);
4214
- this.resizeObserver?.disconnect();
4215
- this.resizeObserver = null;
4216
- this.renderer.destroy();
4217
- this.elements = [];
4218
- this.backdrops = [];
4219
- this.lightPixels = null;
4220
- this.lightCanvas = null;
4221
- this.smoothedLightDirections.clear();
4222
- }
4223
- }
4224
-
4225
-
4226
-
4227
- export { VS_FULLSCREEN, VS_GLASS, FS_BLIT, FS_DOWN, FS_UP, FS_WALLPAPER, FS_GLASS, FS_GLASS_V2, MAX_GLASS_SHAPES, SHAPE_TYPES, shapeTypeOf, cornerRadiusOf, sdSquircle, sdPrimitive, sdGroup, hitTestElements, sdRenderedGroups, connectedElementGroups, groupElements, SHAPE_TYPES_V2, shapeTypeOfV2, sdRoundBoxV2, smoothUnionV2, cornerRadiusV2, sdElementV2, distanceToElementsV2, hitTestElementsV2, DEFAULT_MATERIAL_V2, REDUCED_TRANSPARENCY_MATERIAL_V2, SLIDERS_V2, getDefaultMaterialV2, makeMaterialV2, MIPS, GlassRenderer, LiquidGlassWebGLV2 };
4228
- `; // LG_BUNDLE_END
4229
1612
  async function ensureLgModule() {
4230
1613
  if (lgModule) return lgModule;
1614
+ // ①(修正) 用 host 路由动态加载(不再内联 107KB 源码——内联的模板字符串
1615
+ // 含反引号/${,会触发 DSH client-modules 打包器异常 → 插件 bundle 损坏
1616
+ // → loadSection is not defined + 连带官方插件 bundle 失败,dsh 启动崩溃)。
1617
+ // host 路由(/lg)在 index.js 注册,加载失败仅 console 警告,不影响主功能。
4231
1618
  try {
4232
- const url = URL.createObjectURL(new Blob([LG_BUNDLE_SRC], { type: "text/javascript" }));
4233
- const mod = await import(url);
4234
- setTimeout(() => { try { URL.revokeObjectURL(url); } catch {} }, 30000);
1619
+ const mod = await import(HOST_BASE + "/lg/v2.js");
4235
1620
  lgModule = mod;
4236
- setLgStatus("✓ 模块加载成功(内联)");
1621
+ setLgStatus("✓ 模块加载成功");
4237
1622
  return mod;
4238
1623
  } catch (err) { console.warn("[dsh-mpkg-wallpaper] 液态玻璃模块加载失败:", err); setLgStatus("✗ 模块加载失败: " + (err && err.message || err)); return null; }
4239
1624
  }
@@ -4921,12 +2306,16 @@ body:not([data-ds-dark-theme]) .mpw_dialog {
4921
2306
  命中 DSH 原生设置面板的 full-viewport overlay(渲染在 sidebarCol 内,类名含 mask),
4922
2307
  backdrop-filter 使它成为 fixed 弹窗的包含块 → 设置被"困"在侧边栏宽度里无法居中
4923
2308
  (用户实测:Termux X11 + Firefox 下设置被挤进侧边栏成窄长条)。 */
4924
- .mpw_mask {
2309
+ /* ①(修正) 遮罩虚化作用于 DSH 弹窗遮罩([class$="_mask"],如 BInVoG_mask/fNh4Da_mask/
2310
+ VOzbGW_mask——session 导出弹窗的遮罩) + 插件自己的 .mpw_mask。原收窄成 .mpw_mask
2311
+ 后 DSH 遮罩失去模糊 → session 导出弹窗遮罩效果变差(用户实测)。用 $= 结尾匹配
2312
+ 只命中真正的遮罩类,不会误伤设置 overlay(其类名中间含 mask 但不以 _mask 结尾)。 */
2313
+ [class$="_mask"], .mpw_mask {
4925
2314
  backdrop-filter: ${blurNoWall} !important;
4926
2315
  -webkit-backdrop-filter: ${blurNoWall} !important;
4927
2316
  }
4928
2317
  /* 遮罩背景加深(防字叠字) */
4929
- .mpw_mask { background: rgba(0,0,0,0.55) !important; }
2318
+ [class$="_mask"], .mpw_mask { background: rgba(0,0,0,0.55) !important; }
4930
2319
  `;
4931
2320
  return css + buildUiCss(section, false);
4932
2321
  }
@@ -5438,13 +2827,14 @@ ${unifyTint ? `body:not([data-ds-dark-theme]) [data-cordis-panel] [class*="row"]
5438
2827
  // 否则遮罩只有灰底无模糊、弹窗边框透明(用户实测))
5439
2828
  css += `
5440
2829
  /* ②(重做) 遮罩虚化独立(maskBlur/maskAmount):设置/弹层打开时全屏背景遮罩的虚化
5441
- ①(修正) 同无壁纸分支:只作用于 .mpw_mask,避免宽匹配误伤 DSH 原生设置 overlay */
5442
- .mpw_mask {
2830
+ ①(修正) 同无壁纸分支:作用于 DSH 弹窗遮罩 [class$="_mask"] + 插件 .mpw_mask。
2831
+ $= 结尾匹配只命中真正的遮罩类(session 导出弹窗遮罩等),不误伤设置 overlay */
2832
+ [class$="_mask"], .mpw_mask {
5443
2833
  backdrop-filter: ${(maskBlur && maskAmount > 0 && bdSupported) ? `blur(${maskAmount}px)` : "none"} !important;
5444
2834
  -webkit-backdrop-filter: ${(maskBlur && maskAmount > 0 && bdSupported) ? `blur(${maskAmount}px)` : "none"} !important;
5445
2835
  }
5446
2836
  /* ①(修正) 遮罩背景加深一点(防字叠字;:has 兼容性差不用) */
5447
- .mpw_mask { background: rgba(0,0,0,0.55) !important; }
2837
+ [class$="_mask"], .mpw_mask { background: rgba(0,0,0,0.55) !important; }
5448
2838
  `;
5449
2839
  if (thinkBg) {
5450
2840
  // 开关开:恢复 Deep diving 背景方框 + 虚化 + 蓝色文字