Spread the seat arc and fan the hole cards
Two issues visible on the real Galaxy S24+ screenshot. Shoulder seats crowded the crown. Lanes were 0.30 / 0.06 / 0 / 0.06 / 0.30, so Bruno and Dex sat within 0.06 of Cleo and the three read as one packed row while Ada and Enzo were stranded far below. Shoulders now sit at 0.15, the midpoint between the crown and the side seats, so the row reads as a curve. A test pins the shoulder near that midpoint rather than merely below the crown, since "below" was already true at 0.06. Hole cards looked like one red slab. Two 18x27dp cards overlapped by 5dp with no rotation merged into a single block. They are now a proper fan: opposing rotation, real offset, and tucked behind the avatar as in the reference design. Fanning first pushed the rightmost seats off-screen, because a fixed 48dp avatar plus the fan exceeded the 69.6dp slot a 384dp screen allows. Avatar and fan now scale with the slot and share one slot-width box, so the pair can never overflow the seat it was allotted. Verified: zero bright pixels in the right margin. 260 tests, 0 failures. Lint 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -142,7 +143,7 @@ internal fun opponentSeatPlacements(
|
||||
availableWidthDp: Float,
|
||||
seatWidthDp: Float = opponentSeatWidthDp(availableWidthDp),
|
||||
): List<SeatPlacement> {
|
||||
val lanes = listOf(0.30f, 0.06f, 0f, 0.06f, 0.30f)
|
||||
val lanes = listOf(0.30f, 0.15f, 0f, 0.15f, 0.30f)
|
||||
val span = availableWidthDp - 2f * SEAT_EDGE_MARGIN_DP - seatWidthDp
|
||||
val step = span / 4f
|
||||
return List(5) { index ->
|
||||
@@ -419,18 +420,19 @@ private fun OpponentSeat(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
||||
// One slot-width box: the fan is drawn first so the avatar
|
||||
// sits on top of it, and neither can push past the seat.
|
||||
Box(
|
||||
modifier = Modifier.width(width - 6.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
OpponentAvatar(seat)
|
||||
if (!seat.folded) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy((-5).dp)) {
|
||||
repeat(2) {
|
||||
CardImage(null, Modifier.size(18.dp, 27.dp))
|
||||
}
|
||||
}
|
||||
HoleCardFan(
|
||||
cardWidth = (width * 0.27f).coerceIn(18.dp, 27.dp),
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
)
|
||||
}
|
||||
OpponentAvatar(seat, size = (width * 0.58f).coerceIn(34.dp, 48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,11 +488,44 @@ private fun OpponentSeat(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Two face-down cards fanned like a real hand.
|
||||
*
|
||||
* Drawn flat and barely overlapped they merged into one red slab; opposing
|
||||
* rotation and a real offset make the pair legible at seat scale. The left card
|
||||
* tucks behind the avatar, which is why this is drawn after it in the row.
|
||||
*/
|
||||
@Composable
|
||||
private fun OpponentAvatar(seat: SeatSnapshot) {
|
||||
private fun HoleCardFan(cardWidth: Dp, modifier: Modifier = Modifier) {
|
||||
val cardHeight = cardWidth * 1.5f
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(cardWidth * 1.40f)
|
||||
.height(cardHeight * 1.16f),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CardImage(
|
||||
null,
|
||||
Modifier
|
||||
.size(cardWidth, cardHeight)
|
||||
.offset(x = (-4).dp)
|
||||
.rotate(-11f),
|
||||
)
|
||||
CardImage(
|
||||
null,
|
||||
Modifier
|
||||
.size(cardWidth, cardHeight)
|
||||
.offset(x = 5.dp)
|
||||
.rotate(10f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OpponentAvatar(seat: SeatSnapshot, size: Dp = 48.dp) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
|
||||
@@ -66,6 +66,26 @@ class TableOrbitTest {
|
||||
assertEquals("the middle seat crowns the arc", 0f, lanes[2], 0.001f)
|
||||
}
|
||||
|
||||
/**
|
||||
* The shoulder seats sat at 0.06 against a crown of 0.0, so three seats read
|
||||
* as one crowded row instead of a curve. They belong between the crown and
|
||||
* the side seats.
|
||||
*/
|
||||
@Test
|
||||
fun `shoulder seats sit between the crown and the side seats`() {
|
||||
val lanes = opponentSeatPlacements(384f).map { it.topFraction }
|
||||
val crown = lanes[2]
|
||||
val side = lanes[0]
|
||||
val shoulder = lanes[1]
|
||||
assertTrue("shoulder must be below the crown", shoulder > crown)
|
||||
assertTrue("shoulder must be above the side seats", shoulder < side)
|
||||
val midpoint = (crown + side) / 2f
|
||||
assertTrue(
|
||||
"shoulder $shoulder should be near the midpoint $midpoint, not hugging the crown",
|
||||
kotlin.math.abs(shoulder - midpoint) <= 0.05f,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `spacing is even so the row reads as a deliberate arrangement`() {
|
||||
val width = 384f
|
||||
|
||||
Reference in New Issue
Block a user