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.
- package/lib/client.js +16 -2626
- 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
|
|
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
|
-
|
|
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
|
-
①(修正)
|
|
5442
|
-
|
|
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 背景方框 + 虚化 + 蓝色文字
|