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:
funman300
2026-05-08 11:23:22 -07:00
parent 716a025352
commit c5787c6953
4 changed files with 203 additions and 27 deletions
+20
View File
@@ -117,6 +117,24 @@ pub struct Settings {
/// solely on colour.
#[serde(default)]
pub color_blind_mode: bool,
/// When `true`, boost foreground text + suit-red glyphs to higher-
/// luminance variants for better legibility on low-quality displays
/// or for low-vision users. Per `design-system.md` §Accessibility:
/// on-surface `#d0d0d0` → `#f5f5f5`, suit-red `#fb9fb1` → `#ff8aa0`,
/// outline `#505050` → `#a0a0a0`. Older `settings.json` files
/// written before this field existed deserialize cleanly to
/// `false` thanks to `#[serde(default)]`.
#[serde(default)]
pub high_contrast_mode: bool,
/// When `true`, suppresses non-essential motion: card-lift slide
/// transitions become instant snaps, splash scanline / cursor pulse
/// animations are disabled, and the warning-chip pulse holds at
/// rest. Per `design-system.md` §Accessibility — the WCAG-required
/// reduce-motion mode. Older `settings.json` files written before
/// this field existed deserialize cleanly to `false` thanks to
/// `#[serde(default)]`.
#[serde(default)]
pub reduce_motion_mode: bool,
/// Window size and screen position to restore on next launch. `None`
/// means "use platform defaults" — set on first run, then populated
/// as the player resizes / moves the window. Older `settings.json`
@@ -314,6 +332,8 @@ impl Default for Settings {
selected_background: 0,
first_run_complete: false,
color_blind_mode: false,
high_contrast_mode: false,
reduce_motion_mode: false,
window_geometry: None,
selected_theme_id: default_theme_id(),
shown_achievement_onboarding: false,