A glass design after Apple's Liquid Glass (appearance.surface.design = glass, appearance.glass.*): glass.wgsl draws the shadow and a thin tint under the content and a lens-like lit edge, specular rim and faint fringe over it
This commit is contained in:
@@ -132,7 +132,7 @@ The config window edits the same file: Settings at the foot of the control centr
|
|||||||
|
|
||||||
### Glass
|
### Glass
|
||||||
|
|
||||||
`appearance.surface.design` picks what the island, the pills, the tray menu, the control centre and its popups are made of: `solid` (the wallpaper's gradient with an accent glow), `frosted` (a thin tint, a hairline white rim, a sheen along the top, a soft shadow) or `liquid` (clear glass, denser toward the bottom right, a light-catching edge, the accent's glow). Each design's opacity, rim and light are settings of their own (`appearance.solid|frosted|liquid.*`); the look itself is `src/surface.rs`, drawn by one shader (`src/surface.wgsl`: the shadow, the fill and the sheen or lit edge in one pass; the rim stays a border). On see-through glass a face fades with `.opacity()` instead of a veil, a GPU layer only while it fades.
|
`appearance.surface.design` picks what the island, the pills, the tray menu, the control centre and its popups are made of: `solid` (the wallpaper's gradient with an accent glow), `frosted` (a thin tint, a hairline white rim, a sheen along the top, a soft shadow), `liquid` (clear glass, denser toward the bottom right, a light-catching edge, the accent's glow) or `glass` (after Apple's Liquid Glass: a thin tint and an edge like the rim of a lens, brightest where the light falls in at the top left, a glint where it leaves at the bottom right, a thin specular line and a faint warm and cool fringe). Each design's opacity, rim and light are settings of their own (`appearance.solid|frosted|liquid|glass.*`); the look itself is `src/surface.rs`, drawn by one shader (`src/surface.wgsl`: the shadow, the fill and the sheen or lit edge in one pass; the rim stays a border). `glass` has its own, `src/glass.wgsl`, drawn twice: the shadow and tint under the content, the lit edge over it, so a photo on a card lies behind the glass. Unlike Apple's, it doesn't bend the desktop at the rim: a shell's shader never sees what's behind its window, Hyprland's blur is all it has. On see-through glass a face fades with `.opacity()` instead of a veil, a GPU layer only while it fades.
|
||||||
|
|
||||||
Screenshots of each design, from when they were made: `docs/design/` (`base-solid-card.png`, `base-glass-0.5-card.png` for solid below 1, `frosted-0.62-card.png`, `liquid-0.4-card.png`, and a board for each glass design: the card three times side by side, with the pills below).
|
Screenshots of each design, from when they were made: `docs/design/` (`base-solid-card.png`, `base-glass-0.5-card.png` for solid below 1, `frosted-0.62-card.png`, `liquid-0.4-card.png`, and a board for each glass design: the card three times side by side, with the pills below).
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ Amane draws rectangles, borders, letters and pictures as cheap quads, and gradie
|
|||||||
|
|
||||||
`tools/perf.sh PID [SECONDS]` (on the desktop) prints a process's CPU (and its children's), GPU per engine (DRM fdinfo) and memory.
|
`tools/perf.sh PID [SECONDS]` (on the desktop) prints a process's CPU (and its children's), GPU per engine (DRM fdinfo) and memory.
|
||||||
|
|
||||||
Amane can only blur what the shell draws, so the frosting comes from Hyprland: a layer rule for the shell's windows (the namespace match is a full match). `ignore_alpha` leaves pixels at or below that opacity unblurred, which keeps the clear margin around the pills sharp; keep it below the design's thinnest part, or that part shows the desktop unblurred (liquid at its default 0.4 is 0.22 in its top left corner).
|
Amane can only blur what the shell draws, so the frosting comes from Hyprland: a layer rule for the shell's windows (the namespace match is a full match). `ignore_alpha` leaves pixels at or below that opacity unblurred, which keeps the clear margin around the pills sharp; keep it below the design's thinnest part, or that part shows the desktop unblurred (liquid at its default 0.4 is 0.22 in its top left corner; glass is its `opacity` all over, 0.32 by default).
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
hl.layer_rule({
|
hl.layer_rule({
|
||||||
@@ -185,7 +185,7 @@ src/png.rs a stdlib png writer, for tray icons sent as pixels
|
|||||||
src/system.rs what the control centre shows: iwd, wired cards, openvpn, power, the external GPU
|
src/system.rs what the control centre shows: iwd, wired cards, openvpn, power, the external GPU
|
||||||
src/glyph.rs every drawn icon, as a Canvas or as shapes for a shared one
|
src/glyph.rs every drawn icon, as a Canvas or as shapes for a shared one
|
||||||
src/theme.rs the shell's colours: the matugen theme file, the cross-fade; watches config.toml
|
src/theme.rs the shell's colours: the matugen theme file, the cross-fade; watches config.toml
|
||||||
src/surface.rs what the island, pills, menus and popups are made of: the solid, frosted and liquid designs as values for surface.wgsl
|
src/surface.rs what the island, pills, menus and popups are made of: the solid, frosted and liquid designs as values for surface.wgsl, glass for glass.wgsl
|
||||||
src/*.wgsl shaders: surface (every surface), ring (the candles), curve (the equalizer's line)
|
src/*.wgsl shaders: surface (every surface), ring (the candles), curve (the equalizer's line)
|
||||||
src/config.rs every setting, declared once (typed accessors + the window's list), config.toml read and written
|
src/config.rs every setting, declared once (typed accessors + the window's list), config.toml read and written
|
||||||
src/hypr.rs Hyprland's options for the config window: hyprctl descriptions/getoption, hl.config, amane.lua
|
src/hypr.rs Hyprland's options for the config window: hyprctl descriptions/getoption, hl.config, amane.lua
|
||||||
|
|||||||
+9
-1
@@ -232,7 +232,7 @@ settings! {
|
|||||||
hover_ms: u64 = 220, 0..=2000; "How long hover highlights take to fade in and out"
|
hover_ms: u64 = 220, 0..=2000; "How long hover highlights take to fade in and out"
|
||||||
}
|
}
|
||||||
surface {
|
surface {
|
||||||
design: String ["solid", "frosted", "liquid"] = String::from("solid"); "What the island, the pills, the menus, the control centre and its popups are made of: solid (the wallpaper's gradient with an accent glow), frosted (a thin tint, a hairline rim and a sheen) or liquid (clear glass with a light-catching edge). See-through ones need Hyprland's blur behind the shell: README, Glass"
|
design: String ["solid", "frosted", "liquid", "glass"] = String::from("solid"); "What the island, the pills, the menus, the control centre and its popups are made of: solid (the wallpaper's gradient with an accent glow), frosted (a thin tint, a hairline rim and a sheen), liquid (clear glass with a light-catching edge) or glass (after Apple's Liquid Glass: a thin tint with a lens-like rim). See-through ones need Hyprland's blur behind the shell: README, Glass"
|
||||||
}
|
}
|
||||||
solid {
|
solid {
|
||||||
opacity: f32 = 1.0, 0.05..=1.0; "How opaque the solid design is; below 1 the desktop shows through"
|
opacity: f32 = 1.0, 0.05..=1.0; "How opaque the solid design is; below 1 the desktop shows through"
|
||||||
@@ -252,6 +252,14 @@ settings! {
|
|||||||
light: f32 = 0.235, 0.0..=1.0; "Opacity of the light caught at the top left edge"
|
light: f32 = 0.235, 0.0..=1.0; "Opacity of the light caught at the top left edge"
|
||||||
glow: f32 = 0.22, 0.0..=1.0; "Strength of the accent glow around each surface"
|
glow: f32 = 0.22, 0.0..=1.0; "Strength of the accent glow around each surface"
|
||||||
}
|
}
|
||||||
|
glass {
|
||||||
|
opacity: f32 = 0.32, 0.05..=1.0; "How strongly the glass is tinted in its middle. Keep it above the ignore_alpha of Hyprland's layer rule, or the desktop behind isn't blurred (README, Glass)"
|
||||||
|
edge: f32 = 16.0, 1..=60; "Width of the band along the rim that catches the light like the edge of a lens, in pixels"
|
||||||
|
light: f32 = 0.55, 0.0..=1.0; "Strength of the light caught where it falls in (top left) and where it leaves (bottom right)"
|
||||||
|
rim: f32 = 0.5, 0.0..=1.0; "Strength of the thin bright line on the rim"
|
||||||
|
dispersion: f32 = 0.3, 0.0..=1.0; "Strength of the warm and cool fringe along the rim, as glass splits light"
|
||||||
|
shadow: f32 = 0.18, 0.0..=1.0; "Strength of the soft shadow under each surface"
|
||||||
|
}
|
||||||
fallback {
|
fallback {
|
||||||
accent: Color = Color::rgb(247, 168, 200); "Accent, used until matugen writes ~/.config/amane/theme from the wallpaper"
|
accent: Color = Color::rgb(247, 168, 200); "Accent, used until matugen writes ~/.config/amane/theme from the wallpaper"
|
||||||
on_accent: Color = Color::rgb(40, 18, 32); "Text and icons on the accent colour, without a theme file"
|
on_accent: Color = Color::rgb(40, 18, 32); "Text and icons on the accent colour, without a theme file"
|
||||||
|
|||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
// glass after Apple's Liquid Glass, as far as a surface that can't see what's behind it
|
||||||
|
// gets: a thin, lightly tinted middle over Hyprland's blur, and an edge drawn like the rim
|
||||||
|
// of a lens: a band that brightens toward the rim where the light falls in (top left) and
|
||||||
|
// where it leaves again (bottom right), a thin specular line on the rim itself, and a faint
|
||||||
|
// warm and cool fringe. surface.rs fills the values:
|
||||||
|
// 0: shape width, height, corner radius, margin (room around the shape for the shadow)
|
||||||
|
// 1: shadow colour, its alpha already scaled by the shadow's strength
|
||||||
|
// 2: shadow deviation, its x and y offset, lift (0 to 1, hovered or raised)
|
||||||
|
// 3: width of the edge band in pixels, light, rim and fringe strengths (0 to 1)
|
||||||
|
// 4-6: the tint, top left to bottom right, plain colours with the middle's opacity as alpha
|
||||||
|
// 7.x: the layer: 1 the shadow and the tint, under the content; 2 the lit edge and the rim,
|
||||||
|
// over it (a photo on the card then lies behind the glass); 0 both
|
||||||
|
|
||||||
|
fn erf7(x: f32) -> f32 {
|
||||||
|
let y = clamp(x * 1.1283791671, -100.0, 100.0);
|
||||||
|
let yy = y * y;
|
||||||
|
let z = y + (0.24295 + (0.03395 + 0.0104 * yy) * yy) * (y * yy);
|
||||||
|
|
||||||
|
return z / sqrt(1.0 + z * z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// vello's blurred rounded rectangle at p, from its centre, as in surface.wgsl
|
||||||
|
fn shadow(p: vec2<f32>, w: f32, h: f32, radius: f32, deviation: f32) -> f32 {
|
||||||
|
let s = max(deviation, 1e-5);
|
||||||
|
let inv = 1.0 / s;
|
||||||
|
let min_edge = min(w, h);
|
||||||
|
let radius_max = 0.5 * min_edge;
|
||||||
|
let r0 = min(length(vec2<f32>(radius, s * 1.15)), radius_max);
|
||||||
|
let r1 = min(length(vec2<f32>(radius, s * 2.0)), radius_max);
|
||||||
|
let exponent = 2.0 * r1 / r0;
|
||||||
|
|
||||||
|
let delta = 1.25 * s * (exp(-pow(0.5 * inv * w, 2.0)) - exp(-pow(0.5 * inv * h, 2.0)));
|
||||||
|
let width = w + min(delta, 0.0);
|
||||||
|
let height = h - max(delta, 0.0);
|
||||||
|
|
||||||
|
let scale = 0.5 * erf7(inv * 0.5 * (max(width, height) - 0.5 * radius));
|
||||||
|
|
||||||
|
let y0 = abs(p.y) - (height * 0.5 - r1);
|
||||||
|
let x0 = abs(p.x) - (width * 0.5 - r1);
|
||||||
|
let d_pos = pow(pow(max(x0, 0.0), exponent) + pow(max(y0, 0.0), exponent), 1.0 / exponent);
|
||||||
|
let d = d_pos + min(max(x0, y0), 0.0) - r1;
|
||||||
|
|
||||||
|
return scale * (erf7(inv * (min_edge + d)) - erf7(inv * d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// signed distance to a rounded rectangle centred on 0, negative inside
|
||||||
|
fn rounded(p: vec2<f32>, half: vec2<f32>, radius: f32) -> f32 {
|
||||||
|
let r = min(radius, min(half.x, half.y));
|
||||||
|
let q = abs(p) - half + vec2<f32>(r);
|
||||||
|
|
||||||
|
return length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the way out of the shape at p: across a side, or out from a corner's centre
|
||||||
|
fn outward(p: vec2<f32>, half: vec2<f32>, radius: f32) -> vec2<f32> {
|
||||||
|
let r = min(radius, min(half.x, half.y));
|
||||||
|
let q = abs(p) - half + vec2<f32>(r);
|
||||||
|
let side = select(vec2<f32>(-1.0), vec2<f32>(1.0), p >= vec2<f32>(0.0));
|
||||||
|
|
||||||
|
if q.x > 0.0 && q.y > 0.0 {
|
||||||
|
return normalize(q) * side;
|
||||||
|
}
|
||||||
|
|
||||||
|
return select(vec2<f32>(0.0, side.y), vec2<f32>(side.x, 0.0), q.x > q.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// premultiplied colours, one over the other
|
||||||
|
fn over(top: vec4<f32>, under: vec4<f32>) -> vec4<f32> {
|
||||||
|
return top + under * (1.0 - top.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// amane blends a plain colour over the window
|
||||||
|
fn plain(color: vec4<f32>) -> vec4<f32> {
|
||||||
|
if color.a <= 0.0 {
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec4<f32>(color.rgb / color.a, color.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
|
||||||
|
let shape = values[0];
|
||||||
|
let extent = shape.xy;
|
||||||
|
let centre = extent / 2.0;
|
||||||
|
let lift = values[2].w;
|
||||||
|
let edge = max(values[3].x, 1.0);
|
||||||
|
let light = values[3].y * (1.0 + 0.35 * lift);
|
||||||
|
let rim = values[3].z * (1.0 + 0.35 * lift);
|
||||||
|
let fringe = values[3].w;
|
||||||
|
|
||||||
|
let layer = u32(values[7].x);
|
||||||
|
|
||||||
|
let p = uv * size - vec2<f32>(shape.w);
|
||||||
|
let pixel = max(fwidth(p.x), 1e-4);
|
||||||
|
let from_centre = p - centre;
|
||||||
|
|
||||||
|
let shade = select(shadow(from_centre - values[2].yz, extent.x, extent.y, shape.z, values[2].x) * values[1].a, 0.0, layer == 2u);
|
||||||
|
let under = vec4<f32>(values[1].rgb * shade, shade);
|
||||||
|
|
||||||
|
let d = rounded(from_centre, centre, shape.z);
|
||||||
|
let inside = clamp(0.5 - d / pixel, 0.0, 1.0);
|
||||||
|
|
||||||
|
if inside <= 0.0 {
|
||||||
|
return plain(under);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the tint, a gradient over the shape from its top left to its bottom right
|
||||||
|
let t = clamp(dot(p / max(extent, vec2<f32>(1.0)), vec2<f32>(0.5)), 0.0, 1.0);
|
||||||
|
let tint = select(mix(values[5], values[6], (t - 0.5) * 2.0), mix(values[4], values[5], t * 2.0), t < 0.5);
|
||||||
|
|
||||||
|
var glass = select(vec4<f32>(tint.rgb * tint.a, tint.a), vec4<f32>(0.0), layer == 2u);
|
||||||
|
|
||||||
|
if layer == 1u {
|
||||||
|
return plain(over(glass * inside, under));
|
||||||
|
}
|
||||||
|
|
||||||
|
// the lens: 1 on the rim, 0 deeper in than `edge`, steepest at the rim like a bevel
|
||||||
|
let band = pow(1.0 - smoothstep(0.0, edge, max(-d, 0.0)), 2.0);
|
||||||
|
|
||||||
|
// which way this part of the rim faces: toward the light (top left) is 1, away is -1
|
||||||
|
let facing = dot(outward(from_centre, centre, shape.z), vec2<f32>(-0.7071, -0.7071));
|
||||||
|
|
||||||
|
// a faint warm fringe toward the light and a cool one away from it
|
||||||
|
let hue = mix(vec3<f32>(0.75, 0.85, 1.0), vec3<f32>(1.0, 0.9, 0.78), facing * 0.5 + 0.5);
|
||||||
|
let tone = mix(vec3<f32>(1.0), hue, fringe);
|
||||||
|
|
||||||
|
// brightest where the light comes in, a weaker glint where it goes out, a little all round
|
||||||
|
let caught = clamp(light * band * (0.9 * max(facing, 0.0) + 0.45 * max(-facing, 0.0) + 0.12), 0.0, 1.0);
|
||||||
|
|
||||||
|
glass = over(vec4<f32>(tone * caught, caught), glass);
|
||||||
|
|
||||||
|
// the specular line on the rim itself, about 1.5 px wide
|
||||||
|
let line = clamp(clamp((d + 1.5) / pixel, 0.0, 1.0) * rim * (0.35 + 0.65 * abs(facing)), 0.0, 1.0);
|
||||||
|
|
||||||
|
glass = over(vec4<f32>(tone * line, line), glass);
|
||||||
|
|
||||||
|
return plain(over(glass * inside, under));
|
||||||
|
}
|
||||||
+48
-1
@@ -3,7 +3,7 @@ use std::sync::LazyLock;
|
|||||||
|
|
||||||
use amane::{Blend, Color, Rectangle, Stack, children};
|
use amane::{Blend, Color, Rectangle, Stack, children};
|
||||||
|
|
||||||
use crate::config::appearance::{frosted, liquid, solid, surface};
|
use crate::config::appearance::{frosted, glass, liquid, solid, surface};
|
||||||
use crate::island::faded;
|
use crate::island::faded;
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
@@ -22,6 +22,10 @@ use crate::theme;
|
|||||||
* the rim stays a border, which amane draws as a quad
|
* the rim stays a border, which amane draws as a quad
|
||||||
*/
|
*/
|
||||||
pub fn shell(width: f32, height: f32, radius: f32, colors: &[Color; 3], lift: f32, content: Rectangle) -> Rectangle {
|
pub fn shell(width: f32, height: f32, radius: f32, colors: &[Color; 3], lift: f32, content: Rectangle) -> Rectangle {
|
||||||
|
if surface::design() == "glass" {
|
||||||
|
return glass_shell(width, height, radius, colors, lift, content);
|
||||||
|
}
|
||||||
|
|
||||||
let theme = theme::now();
|
let theme = theme::now();
|
||||||
|
|
||||||
let look = match surface::design().as_str() {
|
let look = match surface::design().as_str() {
|
||||||
@@ -89,6 +93,49 @@ pub fn clear() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static SHADER: LazyLock<PathBuf> = LazyLock::new(|| shader_file("surface.wgsl", include_str!("surface.wgsl")));
|
static SHADER: LazyLock<PathBuf> = LazyLock::new(|| shader_file("surface.wgsl", include_str!("surface.wgsl")));
|
||||||
|
static GLASS: LazyLock<PathBuf> = LazyLock::new(|| shader_file("glass.wgsl", include_str!("glass.wgsl")));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* after Apple's Liquid Glass: glass.wgsl draws the shadow and the thin tint in the
|
||||||
|
* wallpaper's colours under the content, and the lens-like edge with its rim over it, so a
|
||||||
|
* photo on a card lies behind the glass; there's no border. the frosting behind is
|
||||||
|
* Hyprland's blur; the desktop isn't bent at the rim the way Apple's glass bends it, since
|
||||||
|
* a shell's shader can't see what's behind its window
|
||||||
|
*/
|
||||||
|
fn glass_shell(width: f32, height: f32, radius: f32, colors: &[Color; 3], lift: f32, content: Rectangle) -> Rectangle {
|
||||||
|
let (blur, drop): (f32, f32) = (22.0, 6.0);
|
||||||
|
let margin = (1.25 * blur + drop).ceil();
|
||||||
|
|
||||||
|
let mut shadow = plain(Color::BLACK);
|
||||||
|
|
||||||
|
shadow[3] = (glass::shadow() + 0.1 * lift).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
let tint = |color: Color| {
|
||||||
|
let mut tint = plain(color);
|
||||||
|
|
||||||
|
tint[3] = glass::opacity().clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
tint
|
||||||
|
};
|
||||||
|
|
||||||
|
// the under layer covers the shadow's room around the shape too, the edge only the shape
|
||||||
|
let layer = |which: f32, margin: f32| {
|
||||||
|
let values = vec![
|
||||||
|
[width, height, radius, margin],
|
||||||
|
shadow,
|
||||||
|
[blur / 2.0, 0.0, drop, lift],
|
||||||
|
[glass::edge(), glass::light(), glass::rim(), glass::dispersion()],
|
||||||
|
tint(colors[0]),
|
||||||
|
tint(colors[1]),
|
||||||
|
tint(colors[2]),
|
||||||
|
[which, 0.0, 0.0, 0.0],
|
||||||
|
];
|
||||||
|
|
||||||
|
Rectangle::new().width(width + 2.0 * margin).height(height + 2.0 * margin).translate(-margin, -margin).shader(GLASS.as_path()).shader_values(values)
|
||||||
|
};
|
||||||
|
|
||||||
|
Rectangle::new().width(width).height(height).child(Stack::new(children![layer(1.0, margin), content, layer(2.0, 0.0)]).width(width).height(height))
|
||||||
|
}
|
||||||
|
|
||||||
// a shader's source is part of the shell; amane wants a file, so it goes beside cava's config
|
// a shader's source is part of the shell; amane wants a file, so it goes beside cava's config
|
||||||
pub fn shader_file(name: &str, source: &str) -> PathBuf {
|
pub fn shader_file(name: &str, source: &str) -> PathBuf {
|
||||||
|
|||||||
Reference in New Issue
Block a user