feat(engine): reduce-transparency + high-contrast support for glass chrome

New Settings::reduce_transparency_mode (serde-default, Accessibility
toggle row) swaps every GlassSurface's gradients for a flat opaque
BG_ELEVATED fill; high-contrast mode now also boosts the glass fill
opacity and rim luminance to BORDER_SUBTLE_HC levels. Both applied by
settings_plugin::update_glass_surfaces, which retargets the gradients
in place on toggle or on newly spawned glass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-07-15 15:16:32 -07:00
parent b405e54bb0
commit c2256582cd
6 changed files with 245 additions and 28 deletions
+161 -26
View File
@@ -30,10 +30,19 @@ use bevy::ui::{
BackgroundGradient, BorderGradient, BoxShadow, ColorStop, LinearGradient, ShadowStyle,
};
use crate::ui_theme::{BG_ELEVATED, BORDER_SUBTLE, BORDER_SUBTLE_HC};
/// Border width every glass surface must reserve in its `Node::border` so
/// the rim gradient has a strip to paint into.
pub const GLASS_BORDER_PX: f32 = 1.0;
/// Marker on every node that received [`glass_surface`]. The settings
/// plugin's `update_glass_surfaces` retargets these when the player
/// toggles reduce-transparency or high-contrast, swapping the gradients
/// in place via [`glass_decorations`].
#[derive(Component, Debug)]
pub struct GlassSurface;
/// Base fill at the *bottom* of the glass sheet — near-black at ~62%
/// opacity so the felt and cards remain visible through the bar while
/// keeping icon/label contrast comfortable.
@@ -58,6 +67,22 @@ const GLASS_RIM_BOTTOM: Color = Color::srgba(1.0, 1.0, 1.0, 0.16);
/// Drop shadow under the floating surface.
const GLASS_SHADOW: Color = Color::srgba(0.0, 0.0, 0.0, 0.35);
/// High-contrast fill — same glass, far less see-through, so text and
/// icons keep contrast over any card art. Per `design-system.md`
/// §Accessibility the HC toggle trades aesthetics for legibility.
const GLASS_FILL_TOP_HC: Color = Color::srgba(0.20, 0.21, 0.23, 0.90);
/// High-contrast fill at the bottom of the sheet.
const GLASS_FILL_BOTTOM_HC: Color = Color::srgba(0.055, 0.055, 0.063, 0.88);
/// High-contrast rim at the top edge — matches the luminance of
/// `BORDER_SUBTLE_HC` so the bar's outline reads as strongly as every
/// other HC-boosted border.
const GLASS_RIM_TOP_HC: Color = Color::srgba(1.0, 1.0, 1.0, 0.75);
/// High-contrast rim at the midpoint.
const GLASS_RIM_MID_HC: Color = Color::srgba(1.0, 1.0, 1.0, 0.35);
/// High-contrast rim at the bottom edge.
const GLASS_RIM_BOTTOM_HC: Color = Color::srgba(1.0, 1.0, 1.0, 0.50);
/// Visual components for a floating glass surface.
///
/// The caller owns the `Node` and must set two fields for the material to
@@ -69,33 +94,11 @@ const GLASS_SHADOW: Color = Color::srgba(0.0, 0.0, 0.0, 0.35);
/// Everything here is fragment-shader work inside Bevy's stock UI pipeline,
/// so it is safe on WebGL2 and adds no per-frame cost beyond ordinary nodes.
pub fn glass_surface() -> impl Bundle {
let (sheen, rim) = glass_decorations(false, false);
(
// Sheen: one top-to-bottom linear gradient carries both the fill and
// the lighting so there is a single source of truth for the surface
// colour (a separate `BackgroundColor` would just be painted over).
BackgroundGradient(vec![
LinearGradient::new(
LinearGradient::TO_BOTTOM,
vec![
ColorStop::new(GLASS_FILL_TOP, Val::Percent(0.0)),
ColorStop::new(GLASS_FILL_BOTTOM, Val::Percent(60.0)),
],
)
.into(),
]),
// Specular rim: bright at the top edge, fading out through the
// sides, with a faint return at the bottom.
BorderGradient(vec![
LinearGradient::new(
LinearGradient::TO_BOTTOM,
vec![
ColorStop::new(GLASS_RIM_TOP, Val::Percent(0.0)),
ColorStop::new(GLASS_RIM_MID, Val::Percent(55.0)),
ColorStop::new(GLASS_RIM_BOTTOM, Val::Percent(100.0)),
],
)
.into(),
]),
GlassSurface,
sheen,
rim,
// Soft shadow underneath sells the "floating above the table" read.
BoxShadow(vec![ShadowStyle {
color: GLASS_SHADOW,
@@ -107,9 +110,98 @@ pub fn glass_surface() -> impl Bundle {
)
}
/// The sheen + rim pair for the requested accessibility state. The
/// settings plugin overwrites every [`GlassSurface`]'s components with
/// these when the relevant toggles change.
///
/// - Default: translucent fill with a top-lit sheen and a specular rim.
/// - `high_contrast`: same shape, near-opaque fill, and a rim boosted to
/// `BORDER_SUBTLE_HC` luminance so the outline stays legible.
/// - `reduce_transparency`: flat opaque `BG_ELEVATED` fill and a flat
/// border — no see-through at all. Combined with `high_contrast` the
/// flat border brightens to `BORDER_SUBTLE_HC`.
pub fn glass_decorations(
reduce_transparency: bool,
high_contrast: bool,
) -> (BackgroundGradient, BorderGradient) {
if reduce_transparency {
// "Gradients" with a single stop render as flat fills — reusing the
// same component types means the settings toggle swaps values, not
// component sets.
let border = if high_contrast {
BORDER_SUBTLE_HC
} else {
BORDER_SUBTLE
};
return (
BackgroundGradient(vec![
LinearGradient::new(
LinearGradient::TO_BOTTOM,
vec![ColorStop::new(BG_ELEVATED, Val::Percent(0.0))],
)
.into(),
]),
BorderGradient(vec![
LinearGradient::new(
LinearGradient::TO_BOTTOM,
vec![ColorStop::new(border, Val::Percent(0.0))],
)
.into(),
]),
);
}
let (fill_top, fill_bottom, rim_top, rim_mid, rim_bottom) = if high_contrast {
(
GLASS_FILL_TOP_HC,
GLASS_FILL_BOTTOM_HC,
GLASS_RIM_TOP_HC,
GLASS_RIM_MID_HC,
GLASS_RIM_BOTTOM_HC,
)
} else {
(
GLASS_FILL_TOP,
GLASS_FILL_BOTTOM,
GLASS_RIM_TOP,
GLASS_RIM_MID,
GLASS_RIM_BOTTOM,
)
};
(
// Sheen: one top-to-bottom linear gradient carries both the fill and
// the lighting so there is a single source of truth for the surface
// colour (a separate `BackgroundColor` would just be painted over).
BackgroundGradient(vec![
LinearGradient::new(
LinearGradient::TO_BOTTOM,
vec![
ColorStop::new(fill_top, Val::Percent(0.0)),
ColorStop::new(fill_bottom, Val::Percent(60.0)),
],
)
.into(),
]),
// Specular rim: bright at the top edge, fading out through the
// sides, with a faint return at the bottom.
BorderGradient(vec![
LinearGradient::new(
LinearGradient::TO_BOTTOM,
vec![
ColorStop::new(rim_top, Val::Percent(0.0)),
ColorStop::new(rim_mid, Val::Percent(55.0)),
ColorStop::new(rim_bottom, Val::Percent(100.0)),
],
)
.into(),
]),
)
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::ui::Gradient;
/// The bundle must insert all three visual components — a regression
/// here (e.g. a refactor dropping the rim) would silently flatten the
@@ -142,5 +234,48 @@ mod tests {
fn rim_highlight_peaks_at_top() {
assert!(GLASS_RIM_TOP.alpha() > GLASS_RIM_BOTTOM.alpha());
assert!(GLASS_RIM_BOTTOM.alpha() > GLASS_RIM_MID.alpha());
assert!(GLASS_RIM_TOP_HC.alpha() > GLASS_RIM_BOTTOM_HC.alpha());
assert!(GLASS_RIM_BOTTOM_HC.alpha() > GLASS_RIM_MID_HC.alpha());
}
/// Reduce-transparency must produce fully opaque fills — the entire
/// point of the toggle is that nothing shows through.
#[test]
fn reduce_transparency_is_fully_opaque() {
for high_contrast in [false, true] {
let (sheen, _) = glass_decorations(true, high_contrast);
for gradient in &sheen.0 {
let Gradient::Linear(linear) = gradient else {
panic!("reduce-transparency sheen must stay linear");
};
for stop in &linear.stops {
assert_eq!(
stop.color.alpha(),
1.0,
"opaque variant leaked translucency (hc={high_contrast})"
);
}
}
}
}
/// High-contrast glass must be meaningfully less transparent than the
/// default, and its rim meaningfully brighter — otherwise the toggle
/// does nothing perceptible on this surface.
#[test]
fn high_contrast_boosts_fill_and_rim() {
assert!(GLASS_FILL_TOP_HC.alpha() >= GLASS_FILL_TOP.alpha() + 0.15);
assert!(GLASS_FILL_BOTTOM_HC.alpha() >= GLASS_FILL_BOTTOM.alpha() + 0.15);
assert!(GLASS_RIM_TOP_HC.alpha() >= GLASS_RIM_TOP.alpha() + 0.25);
}
/// `glass_surface()` must carry the marker the settings applier
/// retargets — without it the accessibility toggles silently skip
/// the bar.
#[test]
fn glass_surface_carries_retarget_marker() {
let mut world = World::new();
let e = world.spawn(glass_surface()).id();
assert!(world.get::<GlassSurface>(e).is_some());
}
}