fix(engine): raise dynamic tableau fan cap to fill tall viewports

MAX_DYNAMIC_FAN_FRAC 0.6 -> 0.9 so the dynamic tableau fill spreads
further on very tall / narrow viewports (e.g. a foldable cover screen),
which were left ~40% empty at the cap. Fills the unfolded near-square
screen to ~100% and lets the cover screen fill further (and the rest fills
as columns deepen during play). Normal phones are unaffected — their fill
fraction is already below the cap. apply_dynamic_tableau_fan still floors
at TABLEAU_FAN_FRAC and deeper columns drive the fraction down, so nothing
overflows and hit-testing stays in sync.

Vertical centring of the residual was investigated but dropped: a 21:9
phone is aspect-identical to the cover screen, so centring can't be
targeted to foldables without also disconnecting the board from the HUD on
tall phones.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-26 13:56:31 -07:00
parent c00656ec8f
commit 94a6feb5db
2 changed files with 12 additions and 14 deletions
+3 -1
View File
@@ -41,7 +41,9 @@ project follows [Semantic Versioning](https://semver.org/).
each column's total depth (face-down cards included) and refills on the each column's total depth (face-down cards included) and refills on the
cold-start deal, every move, and on resize (incl. the Android safe-area-inset cold-start deal, every move, and on resize (incl. the Android safe-area-inset
resize and fold/unfold), so a near-square viewport such as an unfolded Galaxy resize and fold/unfold), so a near-square viewport such as an unfolded Galaxy
Fold no longer leaves the bottom of the screen empty. Fold no longer leaves the bottom of the screen empty. The fan spread cap was
raised so very tall / narrow viewports (e.g. a foldable cover screen) fill
further.
- **Card-move animation jank.** A move now rebuilds a card's child visuals only - **Card-move animation jank.** A move now rebuilds a card's child visuals only
when its appearance changes (flip / resize / accessibility) instead of when its appearance changes (flip / resize / accessibility) instead of
despawning and respawning every card's children each `StateChangedEvent`, despawning and respawning every card's children each `StateChangedEvent`,
+9 -13
View File
@@ -99,7 +99,7 @@ const MAX_TABLEAU_CARDS: f32 = 13.0;
/// clear overlap (a readable stack) while still filling most of a near-square / /// clear overlap (a readable stack) while still filling most of a near-square /
/// unfolded-foldable screen. Tunable purely for feel — no effect on correctness /// unfolded-foldable screen. Tunable purely for feel — no effect on correctness
/// or hit-testing. /// or hit-testing.
pub(crate) const MAX_DYNAMIC_FAN_FRAC: f32 = 0.6; pub(crate) const MAX_DYNAMIC_FAN_FRAC: f32 = 0.9;
/// Vertical pixel band reserved at the top of the play area for the HUD /// Vertical pixel band reserved at the top of the play area for the HUD
/// (action buttons, Score / Moves / Timer readouts). The card grid starts /// (action buttons, Score / Moves / Timer readouts). The card grid starts
@@ -285,13 +285,12 @@ pub fn compute_layout(
); );
} }
// Adaptive tableau fan fraction. On height-limited (desktop) windows the // Adaptive tableau fan fraction. On height-limited windows the height-based
// height-based sizing already ensures a worst-case 13-card column fits at // sizing already ensures a worst-case 13-card column fits at TABLEAU_FAN_FRAC,
// TABLEAU_FAN_FRAC (0.25), so the formula returns ≈0.25 and the clamp // so the formula returns the minimum and the clamp keeps it there. On
// keeps it there — no change from prior behaviour. On width-limited // width-limited (portrait phone) windows card_size is small and lots of
// (portrait phone) windows card_size is small and lots of vertical space // vertical space is unused; solve for the fraction that fills the available
// is unused; we solve for the fraction that exactly fills the available // space. `apply_dynamic_tableau_fan` later refines this for the actual deal.
// space to the bottom margin.
// //
// avail = distance from the top of the first tableau card to the bottom // avail = distance from the top of the first tableau card to the bottom
// margin — i.e. the space available for 12 fan steps. // margin — i.e. the space available for 12 fan steps.
@@ -302,20 +301,17 @@ pub fn compute_layout(
} else { } else {
TABLEAU_FAN_FRAC TABLEAU_FAN_FRAC
}; };
// Never go below the desktop minimum — avoids shrinking the fan on
// degenerate near-square windows where the formula might undershoot.
let tableau_fan_frac = ideal_fan_frac.max(TABLEAU_FAN_FRAC); let tableau_fan_frac = ideal_fan_frac.max(TABLEAU_FAN_FRAC);
// Scale the face-down fraction proportionally so rendering and hit-testing
// stay in sync (TABLEAU_FACEDOWN_FAN_FRAC / TABLEAU_FAN_FRAC = 0.48 ratio).
let facedown_scale = TABLEAU_FACEDOWN_FAN_FRAC / TABLEAU_FAN_FRAC; let facedown_scale = TABLEAU_FACEDOWN_FAN_FRAC / TABLEAU_FAN_FRAC;
let tableau_facedown_fan_frac = tableau_fan_frac * facedown_scale; let tableau_facedown_fan_frac = tableau_fan_frac * facedown_scale;
let available_tableau_height = avail;
Layout { Layout {
card_size, card_size,
pile_positions, pile_positions,
tableau_fan_frac, tableau_fan_frac,
tableau_facedown_fan_frac, tableau_facedown_fan_frac,
available_tableau_height: avail, available_tableau_height,
} }
} }