a292a7ead0
Project-wide palette shift at user request. Replaces the cyan primary accent everywhere it surfaces — splash boot screen, home menu glyphs, action chevrons, replay overlay banner + scrub fill + chip border, achievement checkmarks, leaderboard #1 indicator, radial menu fill, focus ring, card-back canonical badge, etc. — with `#a54242` from the same base16-eighties family as the existing pink suit colour. Knock-on changes that all land in this commit per the lockstep rule: - ui_theme.rs: ACCENT_PRIMARY (#a54242), ACCENT_PRIMARY_HOVER (#c25e5e brightened companion), FOCUS_RING (same hue, 0.85 alpha). Module-level palette comment + STOCK_BADGE_FG + CARD_SHADOW_ALPHA_DRAG doc strings updated to match. - card_plugin.rs: card_back_colour(0) now returns the brick-red ACCENT_PRIMARY (was cyan). RED_SUIT_COLOUR_CBM swapped from cyan to lime #acc267 — the CBM alternative needs to stay hue-distinct from the new red-family primary, lime is the next-best non-red base16-eighties accent. text_colour doc + CBM tests renamed cyan→lime in lockstep (text_colour_color_blind_mode_swaps_red_suits_to_lime). - card_face_svg.rs: BACK_ACCENTS[0] now "#a54242" (canonical Terminal back). - splash_plugin.rs / ui_modal.rs / replay_overlay.rs / selection_plugin.rs: descriptive "cyan" comments swapped to "accent" / "primary-accent" wording so the doc strings stay decoupled from any specific hue. Future palette tweaks won't require comment churn. - design-system.md: YAML token frontmatter updated (primary, surface-tint, suit-red-cb, primary-container, on-primary-container, inverse-primary). Palette table gains a project-specific `base08` slot for the new red. CTA / Selection / Card-back badge / Primary button / Bottom-bar active-icon / glow / CBM swap text all retuned. Historical references preserved (e.g. "Was cyan #6fc2ef before the 2026-05-08 swap") so the audit trail stays in the spec. - card_face_svg_pin.rs: rebaselined. Exactly one hash drift (back_0 — the canonical Terminal back's badge changed colour). Other 56 hashes identical (face SVGs don't reference the accent; back_1..4 use unchanged accents). The one-hash-drift signal confirms the change scope was surgical. Workspace clippy + cargo test --workspace clean, 1184 passing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
583 lines
26 KiB
Rust
583 lines
26 KiB
Rust
//! Centralised UI design tokens — colours, typography, spacing, radius,
|
||
//! z-index hierarchy, and motion durations.
|
||
//!
|
||
//! Every UI surface (HUD, modals, popovers, toasts) reads from these
|
||
//! tokens instead of hardcoding hex codes or magic numbers. The audit
|
||
//! that produced this module found 40+ scattered colour literals, 12+
|
||
//! distinct font sizes, and 8+ hardcoded z-index values across the
|
||
//! engine; collapsing them into one source of truth keeps the visual
|
||
//! system coherent and makes future palette swaps a single-file change.
|
||
//!
|
||
//! Palette is "Terminal" (base16-eighties) — see
|
||
//! `docs/ui-mockups/design-system.md` for the full token spec, mockup
|
||
//! library, and rationale. The tokens are exposed as `pub const` so
|
||
//! static contexts (default colours on Sprite components, etc.) can use
|
||
//! them; a future `UiTheme` resource can layer runtime switching on top
|
||
//! without changing the constant API.
|
||
|
||
use bevy::color::Color;
|
||
use bevy::math::Vec2;
|
||
use bevy::prelude::Val;
|
||
use solitaire_data::AnimSpeed;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Colours — Terminal (base16-eighties): near-black surface ramp with a brick-
|
||
// red primary accent and lime/lavender/gold/teal/pink semantic accents.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Window backstop and the default text colour on top of `ACCENT_PRIMARY`.
|
||
/// Near-black terminal background. `#151515`.
|
||
pub const BG_BASE: Color = Color::srgb(0.082, 0.082, 0.082);
|
||
|
||
/// Elevated surface — modal cards, popover panels, button backgrounds.
|
||
/// One step lighter than `BG_BASE` so cards read as a separate plane
|
||
/// without needing drop shadows. `#202020`.
|
||
pub const BG_ELEVATED: Color = Color::srgb(0.125, 0.125, 0.125);
|
||
|
||
/// Hovered/highlighted surface — used on button hover and on the
|
||
/// currently-active row of a popover. `#2a2a2a`.
|
||
pub const BG_ELEVATED_HI: Color = Color::srgb(0.165, 0.165, 0.165);
|
||
|
||
/// Top elevation step — Secondary button hover, popover currently-
|
||
/// hovered row. One rung above `BG_ELEVATED_HI`. `#353535`.
|
||
pub const BG_ELEVATED_TOP: Color = Color::srgb(0.208, 0.208, 0.208);
|
||
|
||
/// Pressed-button surface — sits below `BG_ELEVATED` so a press reads
|
||
/// as the surface receding rather than rising. `#1a1a1a`.
|
||
pub const BG_ELEVATED_PRESSED: Color = Color::srgb(0.102, 0.102, 0.102);
|
||
|
||
/// Uniform scrim under every modal. Per the Terminal design system,
|
||
/// modals dim the table aggressively (95% opacity) without blurring,
|
||
/// to maintain the crisp synthwave-flat aesthetic. `rgba(21, 21, 21, 0.95)`.
|
||
pub const SCRIM: Color = Color::srgba(0.082, 0.082, 0.082, 0.95);
|
||
|
||
/// Solid fill for the top-of-window HUD band painted by
|
||
/// `hud_plugin::spawn_hud_band`. Terminal HUD chips are opaque
|
||
/// `surface-container` panels — no transparency — so the chrome reads
|
||
/// as a status-line strip rather than a glassy overlay. `#202020`.
|
||
pub const BG_HUD_BAND: Color = Color::srgba(0.125, 0.125, 0.125, 1.0);
|
||
|
||
/// Primary text — warm off-white. The base16-eighties foreground.
|
||
/// `#d0d0d0`.
|
||
pub const TEXT_PRIMARY: Color = Color::srgb(0.816, 0.816, 0.816);
|
||
|
||
/// Secondary text — captions, hints, muted labels. `#a0a0a0`.
|
||
pub const TEXT_SECONDARY: Color = Color::srgb(0.627, 0.627, 0.627);
|
||
|
||
/// Disabled text — greyed-out buttons, locked items. `#505050`.
|
||
pub const TEXT_DISABLED: Color = Color::srgb(0.314, 0.314, 0.314);
|
||
|
||
/// Brick-red primary accent — the CTA colour of the system. Reserved
|
||
/// for primary actions (Play, Resume, Save), focus rings, and
|
||
/// selection. `#a54242` (base16-eighties `base08`). Pre-2026-05-08
|
||
/// this slot was cyan `#6fc2ef`; the swap was a project-wide
|
||
/// palette decision recorded in `design-system.md` and the
|
||
/// SESSION_HANDOFF entry that followed Option D.
|
||
pub const ACCENT_PRIMARY: Color = Color::srgb(0.647, 0.259, 0.259);
|
||
|
||
/// Brightened `ACCENT_PRIMARY` for hover states on primary buttons.
|
||
/// Picks up luminance while keeping the same hue. `#c25e5e`.
|
||
pub const ACCENT_PRIMARY_HOVER: Color = Color::srgb(0.761, 0.369, 0.369);
|
||
|
||
/// Lavender secondary accent — celebratory states (level-up,
|
||
/// achievement unlocked, streak milestones). Used sparingly so it stays
|
||
/// special. `#e1a3ee`.
|
||
pub const ACCENT_SECONDARY: Color = Color::srgb(0.882, 0.639, 0.933);
|
||
|
||
/// Success — foundation completion, valid drop tint, sync OK. Lime
|
||
/// from base16-eighties. `#acc267`.
|
||
pub const STATE_SUCCESS: Color = Color::srgb(0.675, 0.761, 0.404);
|
||
|
||
/// Warning — penalty signal, daily-seed expiry countdown, sync-pending
|
||
/// status. Gold from base16-eighties. **Both** Undo and Recycle
|
||
/// counters use this when non-zero. `#ddb26f`.
|
||
pub const STATE_WARNING: Color = Color::srgb(0.867, 0.698, 0.435);
|
||
|
||
/// Danger — rejection shake, illegal placement, sync error. Pink from
|
||
/// base16-eighties (also doubles as `suit-red` per the design.md
|
||
/// rationale). `#fb9fb1`.
|
||
pub const STATE_DANGER: Color = Color::srgb(0.984, 0.624, 0.694);
|
||
|
||
/// Info — neutral system toasts, sync-connected indicator. Teal from
|
||
/// base16-eighties. `#12cfc0`.
|
||
pub const STATE_INFO: Color = Color::srgb(0.071, 0.812, 0.753);
|
||
|
||
/// Soft fill colour for the drop-target overlay shown over every legal
|
||
/// destination pile while the player is dragging a card. Same lime hue
|
||
/// as `STATE_SUCCESS` (`#acc267`) so the visual language stays
|
||
/// consistent, but at 10 % alpha so the underlying card faces remain
|
||
/// fully readable through the wash.
|
||
pub const DROP_TARGET_FILL: Color = Color::srgba(0.675, 0.761, 0.404, 0.10);
|
||
|
||
/// Outline colour for the drop-target overlay. Matches the
|
||
/// `STATE_SUCCESS` hue at 75 % alpha so the rectangle border reads
|
||
/// unmistakably against both the felt and stacked card faces without
|
||
/// drowning the cards themselves.
|
||
pub const DROP_TARGET_OUTLINE: Color = Color::srgba(0.675, 0.761, 0.404, 0.75);
|
||
|
||
/// Thickness of the drop-target outline edges, in world-space pixels.
|
||
pub const DROP_TARGET_OUTLINE_PX: f32 = 3.0;
|
||
|
||
/// Sprite-space `Transform.z` for drop-target overlay entities. Sits
|
||
/// well above any static card (top stack z is `~1.04`) but well below
|
||
/// the lifted dragged stack (`DRAG_Z = 500.0` in `input_plugin`) so the
|
||
/// overlay never occludes the card the player is holding. Distinct from
|
||
/// the i32 `Z_*` UI-Node tokens above — those are `ZIndex` values for
|
||
/// `bevy::ui`, while this is a 2D `Sprite` z coordinate.
|
||
pub const Z_DROP_OVERLAY: f32 = 50.0;
|
||
|
||
/// Background colour of the stock-pile remaining-count chip.
|
||
///
|
||
/// Reuses `BG_ELEVATED_HI` so the chip reads as one rung above the
|
||
/// translucent stock pile marker without introducing a new palette
|
||
/// value. The badge sits on the stock corner so the player knows how
|
||
/// many cards remain before a recycle.
|
||
pub const STOCK_BADGE_BG: Color = BG_ELEVATED_HI;
|
||
|
||
/// Foreground (text) colour of the stock-pile remaining-count chip.
|
||
///
|
||
/// `ACCENT_PRIMARY` keeps the chip readable against the elevated
|
||
/// surface background and matches the primary accent already used
|
||
/// for other "look here" callouts.
|
||
pub const STOCK_BADGE_FG: Color = ACCENT_PRIMARY;
|
||
|
||
/// Sprite-space `Transform.z` for the stock-pile remaining-count chip.
|
||
///
|
||
/// Sits above the stock pile marker (`Z_PILE_MARKER` = `-1`) and any
|
||
/// face-down stock cards (which start at `0`), but well below
|
||
/// [`Z_DROP_OVERLAY`] (`50.0`) so the green drop-target wash always
|
||
/// renders on top while a card is being dragged. Like `Z_DROP_OVERLAY`,
|
||
/// this is a 2D `Sprite` z coordinate, not a `bevy::ui` `ZIndex`.
|
||
pub const Z_STOCK_BADGE: f32 = 30.0;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Card drop-shadow — the subtle dark halo painted beneath every card so the
|
||
// play surface reads as physical instead of a flat collage of stickers. Idle
|
||
// values are deliberately low-contrast (small offset, ~25% alpha) so resting
|
||
// cards feel grounded without competing with focus rings or drop overlays.
|
||
// Drag values are slightly stronger (further offset, ~40% alpha, larger
|
||
// halo) so the dragged stack visually "lifts" off the felt.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// RGB base for the per-card drop shadow. Always neutral black — never
|
||
/// suit-tinted — so the shadow never carries colour information that a
|
||
/// colour-blind player would rely on to identify a card. Alpha is applied
|
||
/// separately via [`CARD_SHADOW_ALPHA_IDLE`] / [`CARD_SHADOW_ALPHA_DRAG`].
|
||
pub const CARD_SHADOW_COLOR: Color = Color::srgb(0.0, 0.0, 0.0);
|
||
|
||
/// Alpha for the resting-state card shadow. Set to 0 under the Terminal
|
||
/// design system: depth is achieved through 1px suit-color borders and
|
||
/// tonal layering, not blur shadows ("no `box-shadow` anywhere" is a
|
||
/// hard constraint of `docs/ui-mockups/design-system.md`). The shadow
|
||
/// rendering code path is left in place so a future palette swap can
|
||
/// re-enable it without touching consumers.
|
||
pub const CARD_SHADOW_ALPHA_IDLE: f32 = 0.0;
|
||
|
||
/// Alpha for the lifted/dragged card shadow. Set to 0 for the same
|
||
/// reason as [`CARD_SHADOW_ALPHA_IDLE`]. Drag affordance under the
|
||
/// Terminal system is the primary-accent focus glow + z-index lift,
|
||
/// not a deeper shadow.
|
||
pub const CARD_SHADOW_ALPHA_DRAG: f32 = 0.0;
|
||
|
||
/// World-space pixel offset of the resting-state card shadow relative to
|
||
/// its parent card centre. Down-and-right matches a soft top-left light
|
||
/// source — the same convention used by the elevated-surface tones in the
|
||
/// rest of the palette.
|
||
pub const CARD_SHADOW_OFFSET_IDLE: Vec2 = Vec2::new(2.0, -3.0);
|
||
|
||
/// World-space pixel offset of the lifted/dragged card shadow. Roughly
|
||
/// double the idle offset so the parallax reads as "the card is further
|
||
/// from the table".
|
||
pub const CARD_SHADOW_OFFSET_DRAG: Vec2 = Vec2::new(4.0, -6.0);
|
||
|
||
/// Padding in pixels added to each axis of the card size when sizing the
|
||
/// resting-state shadow sprite. The shadow extends slightly past every
|
||
/// edge of the card so the dark border reads as a halo rather than a
|
||
/// matte rectangle behind the card.
|
||
pub const CARD_SHADOW_PADDING_IDLE: Vec2 = Vec2::new(4.0, 4.0);
|
||
|
||
/// Padding added to the card size when sizing the lifted/dragged shadow.
|
||
/// A slightly larger halo at the drag state reinforces the "lifted off
|
||
/// the felt" cue alongside the deeper offset and higher alpha.
|
||
pub const CARD_SHADOW_PADDING_DRAG: Vec2 = Vec2::new(8.0, 8.0);
|
||
|
||
/// Local `Transform.z` for the shadow child sprite, relative to its
|
||
/// parent `CardEntity`. Slightly negative so the shadow always renders
|
||
/// below the card itself even though it shares the parent's world z.
|
||
pub const CARD_SHADOW_LOCAL_Z: f32 = -0.05;
|
||
|
||
/// Subtle border — default popover, card, and idle button outline.
|
||
/// `outline-variant` from the design system at full alpha; the Terminal
|
||
/// aesthetic uses solid 1px borders rather than translucent washes.
|
||
/// `#353535`.
|
||
pub const BORDER_SUBTLE: Color = Color::srgba(0.208, 0.208, 0.208, 1.0);
|
||
|
||
/// Strong border — hover outline, focused button, active popover.
|
||
/// `outline` from the design system. `#505050`.
|
||
pub const BORDER_STRONG: Color = Color::srgba(0.314, 0.314, 0.314, 1.0);
|
||
|
||
/// 2 px ring drawn around the focused interactive element. Cyan
|
||
/// (matches `ACCENT_PRIMARY`) at 85% alpha so the ring stays legible
|
||
/// against both elevated surfaces and the modal scrim backdrop.
|
||
/// `rgba(111, 194, 239, 0.85)`.
|
||
pub const FOCUS_RING: Color = Color::srgba(0.647, 0.259, 0.259, 0.85);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Typography scale (px) — 5 rungs replace the prior
|
||
// 14/15/16/17/18/22/26/28/30/32/40/48 jungle. All UI uses FiraMono via
|
||
// `FontResource`; sizes carry the hierarchy.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Display titles — Home, Win Summary, Onboarding header. 40 px.
|
||
pub const TYPE_DISPLAY: f32 = 40.0;
|
||
|
||
/// Modal / overlay headers. 26 px.
|
||
pub const TYPE_HEADLINE: f32 = 26.0;
|
||
|
||
/// Primary HUD numbers, button labels, body copy that needs weight.
|
||
/// 18 px.
|
||
pub const TYPE_BODY_LG: f32 = 18.0;
|
||
|
||
/// Secondary HUD, body copy, list items. 14 px.
|
||
pub const TYPE_BODY: f32 = 14.0;
|
||
|
||
/// Hotkey-hint chips, microcopy, dates. 11 px.
|
||
pub const TYPE_CAPTION: f32 = 11.0;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Spacing scale (px) — 4-multiple rungs. Every padding, margin, and gap
|
||
// in the engine snaps to one of these.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// 4 px — inline padding, chip gap.
|
||
pub const SPACE_1: f32 = 4.0;
|
||
/// 8 px — default gap between row items.
|
||
pub const SPACE_2: f32 = 8.0;
|
||
/// 12 px — standard button padding-X, action row gap.
|
||
pub const SPACE_3: f32 = 12.0;
|
||
/// 16 px — section gap inside a modal.
|
||
pub const SPACE_4: f32 = 16.0;
|
||
/// 24 px — modal card outer padding.
|
||
pub const SPACE_5: f32 = 24.0;
|
||
/// 32 px — block separator inside large overlays.
|
||
pub const SPACE_6: f32 = 32.0;
|
||
/// 48 px — outer modal margin from the window edge.
|
||
pub const SPACE_7: f32 = 48.0;
|
||
|
||
/// `Val::Px` form of `SPACE_1`, for ergonomic Node construction.
|
||
pub const VAL_SPACE_1: Val = Val::Px(SPACE_1);
|
||
/// `Val::Px` form of `SPACE_2`.
|
||
pub const VAL_SPACE_2: Val = Val::Px(SPACE_2);
|
||
/// `Val::Px` form of `SPACE_3`.
|
||
pub const VAL_SPACE_3: Val = Val::Px(SPACE_3);
|
||
/// `Val::Px` form of `SPACE_4`.
|
||
pub const VAL_SPACE_4: Val = Val::Px(SPACE_4);
|
||
/// `Val::Px` form of `SPACE_5`.
|
||
pub const VAL_SPACE_5: Val = Val::Px(SPACE_5);
|
||
/// `Val::Px` form of `SPACE_6`.
|
||
pub const VAL_SPACE_6: Val = Val::Px(SPACE_6);
|
||
/// `Val::Px` form of `SPACE_7`.
|
||
pub const VAL_SPACE_7: Val = Val::Px(SPACE_7);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Border radius (px)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// 4 px — hotkey chip, inline pill.
|
||
pub const RADIUS_SM: f32 = 4.0;
|
||
/// 8 px — buttons, popover panels.
|
||
pub const RADIUS_MD: f32 = 8.0;
|
||
/// 16 px — modal cards.
|
||
pub const RADIUS_LG: f32 = 16.0;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Z-index hierarchy — replaces 8+ scattered magic numbers across plugins
|
||
// (background, pile markers, HUD, several overlay tiers, win cascade,
|
||
// toasts). Documented order:
|
||
//
|
||
// background → pile markers → cards → HUD → HUD popovers →
|
||
// modal scrim → modal panel → pause → onboarding →
|
||
// win cascade → toasts (always on top)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
pub const Z_BACKGROUND: i32 = -10;
|
||
pub const Z_PILE_MARKER: i32 = -1;
|
||
/// Base layer for HUD readouts (top-left).
|
||
pub const Z_HUD: i32 = 50;
|
||
/// Action bar + popovers — above HUD readouts so dropdowns can overlap.
|
||
pub const Z_HUD_TOP: i32 = 60;
|
||
pub const Z_MODAL_SCRIM: i32 = 200;
|
||
pub const Z_MODAL_PANEL: i32 = 210;
|
||
/// Pause overlay outranks normal modals — pausing should always be on top.
|
||
pub const Z_PAUSE: i32 = 220;
|
||
/// Confirmation dialog stacked on top of the pause overlay (e.g. the
|
||
/// forfeit-confirm modal launched from the pause modal). Sits above
|
||
/// `Z_PAUSE` so the dialog is always visible over the paused state.
|
||
pub const Z_PAUSE_DIALOG: i32 = 225;
|
||
pub const Z_ONBOARDING: i32 = 230;
|
||
/// Z-layer for the keyboard focus indicator. Sits one rung above the
|
||
/// topmost modal layer (`Z_ONBOARDING`) so the ring is never occluded by
|
||
/// a modal card's hover state, while staying below the win cascade and
|
||
/// transient toasts that are allowed to overlay everything else.
|
||
pub const Z_FOCUS_RING: i32 = 240;
|
||
/// Win cascade sits between modals and toasts so the celebration plays
|
||
/// over a paused / mid-modal screen.
|
||
pub const Z_WIN_CASCADE: i32 = 300;
|
||
/// Toasts always render above everything else.
|
||
pub const Z_TOAST: i32 = 400;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Motion — durations in seconds at `AnimSpeed::Normal`. `Fast` halves
|
||
// every value, `Instant` zeroes them. Use `scaled_duration` to apply.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Card slide during gameplay — tweened with `MotionCurve::SmoothSnap`
|
||
/// (ease-out-cubic). 180 ms; bumped from the old 150 ms because cubic
|
||
/// feels slower at endpoints — the perceived speed is unchanged.
|
||
pub const MOTION_SLIDE_SECS: f32 = 0.18;
|
||
|
||
/// Settle bounce on placement — only the moved card, not every top
|
||
/// card on every state change. 180 ms.
|
||
pub const MOTION_SETTLE_SECS: f32 = 0.18;
|
||
|
||
/// Shake on rejected drop — tightened from 300 ms; frequency drops to
|
||
/// 35 rad/s to match the new settle bounce so the two feedback signals
|
||
/// no longer feel discordant. 250 ms.
|
||
pub const MOTION_SHAKE_SECS: f32 = 0.25;
|
||
|
||
/// Shake angular frequency in rad/s.
|
||
pub const MOTION_SHAKE_OMEGA: f32 = 35.0;
|
||
|
||
/// Duration of the smooth return tween when a drag is rejected by an
|
||
/// invalid drop target. Short enough to feel snappy but long enough to
|
||
/// read as motion rather than a teleport.
|
||
pub const MOTION_DRAG_REJECT_SECS: f32 = 0.15;
|
||
|
||
/// Card flip — half-time per phase (squash + grow). 100 ms each =
|
||
/// 200 ms total. Pair with a ±8° Z-rotation at the midpoint for a 3D
|
||
/// feel without 3D rendering.
|
||
pub const MOTION_FLIP_HALF_SECS: f32 = 0.10;
|
||
|
||
/// Per-card stagger on the new-game deal animation — centre value;
|
||
/// each card gets ±10% jitter applied at deal time so the deal feels
|
||
/// organic instead of mechanical. 40 ms.
|
||
pub const MOTION_DEAL_STAGGER_SECS: f32 = 0.04;
|
||
|
||
/// Deal slide duration with an `ease-out` curve and a 40 ms
|
||
/// scale-pop on land so cards "arrive" instead of just stopping.
|
||
/// 280 ms.
|
||
pub const MOTION_DEAL_SLIDE_SECS: f32 = 0.28;
|
||
|
||
/// Win cascade per-card stagger — slightly slower than the prior
|
||
/// 50 ms for a more theatrical feel. 60 ms.
|
||
pub const MOTION_CASCADE_STAGGER_SECS: f32 = 0.06;
|
||
|
||
/// Win cascade per-card slide — uses `MotionCurve::Expressive`
|
||
/// (overshoot) plus ±15° Z-rotation. 500 ms.
|
||
pub const MOTION_CASCADE_SLIDE_SECS: f32 = 0.50;
|
||
|
||
/// Per-line stagger between score-breakdown rows during the win modal
|
||
/// reveal animation, in seconds.
|
||
pub const MOTION_SCORE_BREAKDOWN_STAGGER_SECS: f32 = 0.15;
|
||
|
||
/// Per-line fade-in duration during the win modal score reveal, in
|
||
/// seconds.
|
||
pub const MOTION_SCORE_BREAKDOWN_FADE_SECS: f32 = 0.12;
|
||
|
||
/// Screen shake on win — wider and longer than the old 0.6 s / 8 px.
|
||
/// 800 ms.
|
||
pub const MOTION_WIN_SHAKE_SECS: f32 = 0.80;
|
||
|
||
/// Peak displacement of the win screen shake. 12 px.
|
||
pub const MOTION_WIN_SHAKE_AMPLITUDE: f32 = 12.0;
|
||
|
||
/// Toast in — scale-from-0.92-to-1.0 fade-in. 200 ms.
|
||
pub const MOTION_TOAST_IN_SECS: f32 = 0.20;
|
||
|
||
/// Toast out — fade. 250 ms.
|
||
pub const MOTION_TOAST_OUT_SECS: f32 = 0.25;
|
||
|
||
/// Modal in/out — scale-from-0.96-to-1.0 + scrim fade. 220 ms.
|
||
pub const MOTION_MODAL_SECS: f32 = 0.22;
|
||
|
||
/// Button hover/press colour blend — short, snappy. 100 ms.
|
||
pub const MOTION_BUTTON_BLEND_SECS: f32 = 0.10;
|
||
|
||
/// Score-pulse — when score increases by ≥ 50, briefly scale the
|
||
/// readout 1.0 → 1.1 → 1.0. 250 ms.
|
||
pub const MOTION_SCORE_PULSE_SECS: f32 = 0.25;
|
||
|
||
/// Foundation-completion flourish — when a King lands on a foundation
|
||
/// pile (Ace → King, 13 cards), briefly scale the King card 1.0 →
|
||
/// [`FOUNDATION_FLOURISH_PEAK_SCALE`] → 1.0 and tint the matching
|
||
/// `PileMarker` gold. 400 ms.
|
||
pub const MOTION_FOUNDATION_FLOURISH_SECS: f32 = 0.4;
|
||
|
||
/// Peak scale magnification reached at the midpoint of the
|
||
/// foundation-completion flourish. The triangular curve climbs from
|
||
/// 1.0 at `t=0` to this value at `t=0.5` and back to 1.0 at `t=1.0`.
|
||
pub const FOUNDATION_FLOURISH_PEAK_SCALE: f32 = 1.15;
|
||
|
||
/// Total duration of the streak-milestone flourish on the HUD score
|
||
/// readout, in seconds. Mirrors the foundation flourish in feel — a
|
||
/// brief celebratory pulse that does not block subsequent gameplay.
|
||
pub const MOTION_STREAK_FLOURISH_SECS: f32 = 0.6;
|
||
|
||
/// Peak scale magnification reached at the midpoint of the streak
|
||
/// flourish (1.0 → this → 1.0). Larger than the foundation flourish
|
||
/// peak so the lifetime-streak celebration reads as a bigger deal than
|
||
/// the per-suit completion.
|
||
pub const STREAK_FLOURISH_PEAK_SCALE: f32 = 1.20;
|
||
|
||
/// Win-streak counts that trigger the flourish. The flourish fires
|
||
/// only when the streak crosses a threshold from below — never at
|
||
/// every win past the highest threshold. Static for now; could become
|
||
/// a `Settings`-tunable list later if play-testing surfaces it.
|
||
pub const STREAK_MILESTONES: &[u32] = &[3, 5, 10];
|
||
|
||
/// Loading-ellipsis cycle — `.`/`..`/`...` toggles every step.
|
||
/// 400 ms.
|
||
pub const MOTION_LOADING_TICK_SECS: f32 = 0.40;
|
||
|
||
/// Period of the focus-ring breathing pulse, in seconds.
|
||
///
|
||
/// The keyboard focus ring's alpha is modulated by a sin-curve over this
|
||
/// interval so the indicator gently "breathes" instead of presenting as
|
||
/// a flat outline. 1.4 s reads as a calm heartbeat — slow enough that
|
||
/// the motion is in the player's peripheral vision rather than competing
|
||
/// for attention, fast enough that a focus change still draws the eye.
|
||
/// Not run through [`scaled_duration`]: the pulse is an accessibility
|
||
/// affordance, not gameplay motion. `AnimSpeed::Instant` is honoured at
|
||
/// the system level by skipping the pulse entirely (see
|
||
/// `pulse_focus_overlay` in `ui_focus`).
|
||
pub const MOTION_FOCUS_PULSE_SECS: f32 = 1.4;
|
||
|
||
/// Hover delay before a tooltip appears, in seconds. Long enough that
|
||
/// players gliding the cursor across the HUD don't see flicker; short
|
||
/// enough that "stop and read" feels responsive. Not run through
|
||
/// [`scaled_duration`] — `AnimSpeed` controls gameplay motion, not the
|
||
/// hover-discoverability budget for help text.
|
||
pub const MOTION_TOOLTIP_DELAY_SECS: f32 = 0.5;
|
||
|
||
/// Total visible duration of the splash screen overlay, in seconds.
|
||
/// Composed of a fade-in, a hold, and a fade-out — see
|
||
/// [`MOTION_SPLASH_FADE_SECS`] for the per-edge fade budget. Not run
|
||
/// through [`scaled_duration`]: the splash is a one-shot brand beat at
|
||
/// app start, not gameplay motion that should track `AnimSpeed`.
|
||
pub const MOTION_SPLASH_TOTAL_SECS: f32 = 1.6;
|
||
|
||
/// Fade-in and fade-out duration of the splash overlay, in seconds.
|
||
/// The hold time is `MOTION_SPLASH_TOTAL_SECS - 2 * MOTION_SPLASH_FADE_SECS`.
|
||
/// Mirroring fade-in and fade-out keeps the curve symmetric so the brand
|
||
/// beat reads as a single dissolve instead of two separate animations.
|
||
pub const MOTION_SPLASH_FADE_SECS: f32 = 0.3;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Z-index — tooltip layer
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Z-layer for tooltips. Sits one rung above the focus ring so a
|
||
/// tooltip rendered over a focused button is never occluded by the
|
||
/// button's outline. Still below `Z_WIN_CASCADE` and `Z_TOAST` so the
|
||
/// celebration and notification layers stay on top.
|
||
pub const Z_TOOLTIP: i32 = Z_FOCUS_RING + 10;
|
||
|
||
/// Z-layer for the launch splash overlay. The splash owns the entire
|
||
/// viewport for ~1.6 s before fading out, so it sits above every other
|
||
/// UI rung — including `Z_TOAST` — to guarantee the brand beat is
|
||
/// never occluded by a stray toast or tooltip. Neither toasts nor the
|
||
/// win cascade can fire during the splash window in practice (no game
|
||
/// has run yet, no toast queue has dispatched), but the relative order
|
||
/// is kept tidy in case a future feature schedules either at startup.
|
||
pub const Z_SPLASH: i32 = Z_TOAST + 100;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Scales a `MOTION_*_SECS` value by the player's animation-speed
|
||
/// preference. `Normal` × 1.0, `Fast` × 0.5, `Instant` → 0.
|
||
///
|
||
/// Pass any duration constant from this module through this helper
|
||
/// before handing it to a tween. The audit found that only slide and
|
||
/// cascade respected `AnimSpeed`; toasts, deal stagger, shake, and
|
||
/// settle were hardcoded. Routing every duration through this function
|
||
/// fixes that.
|
||
pub fn scaled_duration(secs: f32, speed: AnimSpeed) -> f32 {
|
||
match speed {
|
||
AnimSpeed::Normal => secs,
|
||
AnimSpeed::Fast => secs * 0.5,
|
||
AnimSpeed::Instant => 0.0,
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// Every spacing rung is a positive multiple of 4 — keeps the scale
|
||
/// honest if someone tweaks values later.
|
||
#[test]
|
||
fn spacing_scale_is_a_4_multiple_geometric_progression() {
|
||
for v in [SPACE_1, SPACE_2, SPACE_3, SPACE_4, SPACE_5, SPACE_6, SPACE_7] {
|
||
assert!(v > 0.0, "spacing tokens must be positive");
|
||
assert!(
|
||
(v.rem_euclid(4.0)).abs() < f32::EPSILON,
|
||
"spacing token {v} must be a 4-multiple"
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Type scale is monotonically decreasing display → caption.
|
||
#[test]
|
||
fn type_scale_is_monotonically_decreasing() {
|
||
let scale = [TYPE_DISPLAY, TYPE_HEADLINE, TYPE_BODY_LG, TYPE_BODY, TYPE_CAPTION];
|
||
for window in scale.windows(2) {
|
||
assert!(
|
||
window[0] > window[1],
|
||
"type scale must be monotonically decreasing: {} should be > {}",
|
||
window[0],
|
||
window[1]
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Z-index hierarchy is monotonically increasing through documented
|
||
/// layers, so a future add-a-layer change can't accidentally land
|
||
/// in the wrong slot.
|
||
#[test]
|
||
fn z_index_hierarchy_is_monotonically_increasing() {
|
||
let layers = [
|
||
Z_BACKGROUND,
|
||
Z_PILE_MARKER,
|
||
Z_HUD,
|
||
Z_HUD_TOP,
|
||
Z_MODAL_SCRIM,
|
||
Z_MODAL_PANEL,
|
||
Z_PAUSE,
|
||
Z_PAUSE_DIALOG,
|
||
Z_ONBOARDING,
|
||
Z_FOCUS_RING,
|
||
Z_TOOLTIP,
|
||
Z_WIN_CASCADE,
|
||
Z_TOAST,
|
||
Z_SPLASH,
|
||
];
|
||
for window in layers.windows(2) {
|
||
assert!(
|
||
window[0] < window[1],
|
||
"z-index hierarchy must be monotonically increasing: {} should be < {}",
|
||
window[0],
|
||
window[1]
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn scaled_duration_matches_anim_speed() {
|
||
assert!((scaled_duration(0.18, AnimSpeed::Normal) - 0.18).abs() < f32::EPSILON);
|
||
assert!((scaled_duration(0.18, AnimSpeed::Fast) - 0.09).abs() < f32::EPSILON);
|
||
assert_eq!(scaled_duration(0.18, AnimSpeed::Instant), 0.0);
|
||
}
|
||
}
|