feat(accessibility): wire high-contrast + reduce-motion modes through engine
Resume-prompt Option F, part 1 of 2. Adds two accessibility flags to Settings and threads each through the engine surfaces that react to them. Settings UI toggle rows follow in a separate commit; players who want to test today can edit `settings.json` manually. Spec at `docs/ui-mockups/design-system.md` §Accessibility (#2 and #3). ### High-contrast mode `Settings::high_contrast_mode: bool` (defaults to false; serde- default for back-compat). When on: - Red-suit text colour boosts from `RED_SUIT_COLOUR` (`#fb9fb1`) to a new `RED_SUIT_COLOUR_HC` (`#ff8aa0`). - Black-suit text colour boosts from `BLACK_SUIT_COLOUR` (`#d0d0d0`) to a new `TEXT_PRIMARY_HC` (`#f5f5f5`). - New `BORDER_SUBTLE_HC` (`#a0a0a0`) constant available for future chrome-side wiring (this commit only routes HC through card text rendering — chrome border boost is a separable follow-up). The HC and CBM flags compose. CBM red→lime wins over HC on red suits when both are on (lime is itself a high-luminance accent, so the HC boost has nothing further to do). HC still applies to black suits when both flags are on (CBM doesn't touch black). Four new `text_colour` tests pin the truth table. ### Reduce-motion mode `Settings::reduce_motion_mode: bool` (defaults to false; serde- default for back-compat). When on: - Card-slide animation duration is forced to `0.0` regardless of the player's `AnimSpeed` selection — cards snap instantly to their target position. Implemented by extracting a new `effective_slide_secs(&Settings)` helper that wraps `anim_speed_to_secs` with the reduce-motion gate. - Future scaffolding hooks (splash scanline, warning-chip pulse, card-lift z-bump animation) follow the same `if settings.reduce_motion_mode { skip }` pattern when wired — stays out of scope for this commit since each motion path needs its own per-system gate. Two new tests cover the gate behaviour and the fall-through-to- AnimSpeed pass-through path. ### Threading `text_colour` signature extended with a `high_contrast: bool` parameter; `sync_cards` / `sync_cards_startup` / `sync_cards_on_change` / `sync_cards` core / `spawn_card_entity` / `update_card_entity` all gain a parallel parameter mirroring the existing `color_blind: bool` plumbing. Verbose but matches the established pattern; a future refactor could pack both into an `AccessibilityView` struct, but bigger blast radius. ### Stats 1191 passing / 0 failing across the workspace (net +6 from v0.21.0's 1185 baseline once the icon-pin test landed): - 4 new `text_colour` HC tests in `card_plugin` (red-suit boost, black-suit boost, CBM-wins-on-red, black-suits-with-CBM+HC-still-boost). - 2 new `effective_slide_secs` tests in `animation_plugin` (zero-out under reduce-motion, fall-through to AnimSpeed when off). `cargo clippy --workspace --all-targets -- -D warnings` clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use solitaire_data::AnimSpeed;
|
||||
use solitaire_data::{AnimSpeed, Settings};
|
||||
|
||||
use crate::achievement_plugin::display_name_for;
|
||||
use crate::auto_complete_plugin::AutoCompleteState;
|
||||
@@ -196,7 +196,7 @@ fn init_slide_duration(
|
||||
mut dur: ResMut<EffectiveSlideDuration>,
|
||||
) {
|
||||
if let Some(s) = settings {
|
||||
dur.slide_secs = anim_speed_to_secs(&s.0.animation_speed);
|
||||
dur.slide_secs = effective_slide_secs(&s.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,20 @@ fn sync_slide_duration(
|
||||
mut dur: ResMut<EffectiveSlideDuration>,
|
||||
) {
|
||||
for ev in events.read() {
|
||||
dur.slide_secs = anim_speed_to_secs(&ev.0.animation_speed);
|
||||
dur.slide_secs = effective_slide_secs(&ev.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the player's effective per-slide animation duration —
|
||||
/// pre-Settings the same as `anim_speed_to_secs(&settings.animation_speed)`,
|
||||
/// but reduce-motion mode forces it to `0.0` regardless of the
|
||||
/// `AnimSpeed` selection so cards snap instantly to their target
|
||||
/// position. Spec at `design-system.md` §Accessibility (#3).
|
||||
fn effective_slide_secs(settings: &Settings) -> f32 {
|
||||
if settings.reduce_motion_mode {
|
||||
0.0
|
||||
} else {
|
||||
anim_speed_to_secs(&settings.animation_speed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,6 +746,45 @@ mod tests {
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_slide_secs_zeros_out_under_reduce_motion() {
|
||||
// Reduce-motion forces slide_secs to 0.0 regardless of the
|
||||
// AnimSpeed selection — cards snap instantly. Spec at
|
||||
// `design-system.md` §Accessibility (#3).
|
||||
let s = Settings {
|
||||
animation_speed: AnimSpeed::Normal,
|
||||
reduce_motion_mode: true,
|
||||
..Settings::default()
|
||||
};
|
||||
assert_eq!(effective_slide_secs(&s), 0.0);
|
||||
|
||||
let s = Settings {
|
||||
animation_speed: AnimSpeed::Fast,
|
||||
reduce_motion_mode: true,
|
||||
..Settings::default()
|
||||
};
|
||||
assert_eq!(effective_slide_secs(&s), 0.0, "Fast + reduce-motion still 0.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_slide_secs_falls_through_to_anim_speed_when_motion_unchecked() {
|
||||
// With reduce-motion off, the resolver is just a pass-through
|
||||
// to anim_speed_to_secs — preserves the existing AnimSpeed
|
||||
// ladder for players who aren't using the accessibility flag.
|
||||
for speed in [AnimSpeed::Normal, AnimSpeed::Fast, AnimSpeed::Instant] {
|
||||
let s = Settings {
|
||||
animation_speed: speed,
|
||||
reduce_motion_mode: false,
|
||||
..Settings::default()
|
||||
};
|
||||
assert_eq!(
|
||||
effective_slide_secs(&s),
|
||||
anim_speed_to_secs(&speed),
|
||||
"without reduce-motion the effective duration must equal the AnimSpeed mapping",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn card_anim_at_half_elapsed_passes_geometric_midpoint() {
|
||||
let mut app = App::new();
|
||||
|
||||
Reference in New Issue
Block a user