Dynamically scale the cards & everything else

Currently, most things were scaled based on the card dimensions,
though some things were hard-coded. This commit moves away from all
hard-coded sizes in favor of everything relying on card heights.

Additionally, it makes the card height itself no longer hard-coded,
passing it as a property to most custom structures.

The card height itself is now calculated from Main.qml to make sure
everything fits within the screen and scales as the window is resized.
This commit is contained in:
ItsDrike 2024-12-08 11:41:28 +01:00
parent 511ca3744a
commit 57958ed853
Signed by: ItsDrike
GPG key ID: FA2745890B7048C0
5 changed files with 111 additions and 35 deletions

View file

@ -3,13 +3,17 @@ import Solitare
Item { Item {
id: drawPile id: drawPile
width: drawPileCard.width
height: drawPileCard.height required property real cardWidth
required property real cardHeight
width: cardWidth
height: cardHeight
CardModel { CardModel {
id: drawPileCard id: drawPileCard
anchors.fill: drawPile
anchors.fill: parent
visible: GameState.drawPile.length > 0 visible: GameState.drawPile.length > 0
card: GameState.drawPile.length > 0 ? GameState.drawPile[GameState.drawPile.length - 1] : null card: GameState.drawPile.length > 0 ? GameState.drawPile[GameState.drawPile.length - 1] : null
@ -24,9 +28,9 @@ Item {
color: "gray" color: "gray"
border.color: "white" border.color: "white"
border.width: 3 border.width: width * 0.03
opacity: 0.4 opacity: 0.4
radius: 10 radius: width * 0.125
Image { Image {
id: flipIcon id: flipIcon
@ -46,14 +50,16 @@ Item {
color: "black" color: "black"
opacity: 0.7 opacity: 0.7
width: cardCountText.width + 8 // 8px padding property real padding: Math.max(cardCountText.width, cardCountText.height) * 0.3
height: cardCountText.height + 8 // ^^
width: cardCountText.width + padding
height: cardCountText.height + padding
visible: drawPileCard.visible visible: drawPileCard.visible
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.margins: 4 anchors.margins: parent.width * 0.05
radius: 4 radius: parent.width * 0.1
z: 1 // Behind the text, but above the card z: 1 // Behind the text, but above the card
} }
@ -62,7 +68,7 @@ Item {
text: GameState.drawPile.length text: GameState.drawPile.length
color: "white" color: "white"
font.pixelSize: 16 font.pixelSize: parent.width * 0.2
font.bold: true font.bold: true
visible: drawPileCard.visible visible: drawPileCard.visible

View file

@ -2,15 +2,20 @@ import QtQuick
import Solitare import Solitare
Row { Row {
spacing: 15 id: foundationRow
required property real cardWidth
required property real cardHeight
spacing: cardWidth * 0.2
Repeater { Repeater {
model: 4 // Each of the 4 suits model: 4 // Each of the 4 suits
Item { Item {
id: foundationPile id: foundationPile
width: foundationCard.width width: foundationRow.cardWidth
height: foundationCard.height height: foundationRow.cardHeight
required property int index // passed from repeater required property int index // passed from repeater
@ -32,14 +37,14 @@ Row {
color: "gray" color: "gray"
border.color: "white" border.color: "white"
border.width: 3 border.width: width * 0.03
opacity: 0.4 opacity: 0.4
radius: 10 radius: width * 0.125
Text { Text {
text: "A" text: "A"
color: "white" color: "white"
font.pixelSize: 40 font.pixelSize: parent.width * 0.5
font.bold: true font.bold: true
anchors.centerIn: parent anchors.centerIn: parent

View file

@ -4,16 +4,56 @@ import QtQuick.Layouts
import Solitare import Solitare
ApplicationWindow { ApplicationWindow {
id: app
width: 750 width: 750
height: 650 height: 650
visible: true visible: true
title: qsTr("Solitare") title: qsTr("Solitare")
color: "#019934" color: "#019934"
// Card dimensions calculation:
//
// Horizontal space
// ================
// The tableau columns take up at most 7*(w*1.125) = 7.875 widths.
//
// The first row:
// * foundation piles: 4*w*1.2 = 4.8 widths.
// * throwaway pile: 3*(w-w*0.75)+w = 1.75 widths.
// * draw pile: 1 width
// * spacing between throwaway & draw pile: 0.25 widths.
// * spacing between foundations & throwaway pile: 0.25 widths
//
// Totalling at: 8.05 card widths.
//
// Vertical space
// ==============
// * The score bar takes up min(8% app height, 50).
// * The first row takes up 1.35 card heights.
// * Margin between first row & tableau: 0.5 card heights.
// * The tableau height depends on the max amount of cards that can be in a single column.
// Generally, the column will take up: ((maxCards - 1) * (h-h*0.8))+h = h(0.2 * maxCards + 0.8).
//
// A good base assumption is 14 cards, though the cards should dynamically rescale
// if this amount is surpassed, which is very possible.
//
// Totalling at: (0.2 * maxCards + 2.65) card heights, filling up 100% - min(8%, 50) of screen height.
//
//
// Aspect Ratio
// ============
// Finally, we need to make sure that the card proportions are preserved,
// with the height being 1.4x the width.
property int maxCardsPerColumn: Math.max(GameState.columns.reduce((max, col) => Math.max(max, col.length), 0), 14)
property real cardHeight: Math.min((app.height - Math.min(app.height * 0.08, 50)) / (maxCardsPerColumn * 0.2 + 2.65), (app.width / 8.05) * 1.4)
property real cardWidth: cardHeight / 1.4
ScoreBar { ScoreBar {
id: scoreBar id: scoreBar
height: 50 height: Math.max(parent.height * 0.08, 50)
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -23,7 +63,7 @@ ApplicationWindow {
Rectangle { Rectangle {
id: firstRow id: firstRow
height: 135 height: app.cardHeight * 1.35
anchors.top: scoreBar.bottom anchors.top: scoreBar.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@ -31,21 +71,34 @@ ApplicationWindow {
Item { Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: 10
anchors.leftMargin: app.cardWidth * 0.125
anchors.rightMargin: app.cardWidth * 0.125
anchors.topMargin: app.cardHeight * 0.2
anchors.bottomMargin: app.cardHeight * 0.2
// Left side // Left side
FoundationPiles { FoundationPiles {
anchors.left: parent.left anchors.left: parent.left
cardWidth: app.cardWidth
cardHeight: app.cardHeight
} }
// Right side // Right side
Row { Row {
spacing: 20 spacing: app.cardWidth * 0.25
anchors.right: parent.right anchors.right: parent.right
ThrowawayPile {} ThrowawayPile {
cardWidth: app.cardWidth
cardHeight: app.cardHeight
}
DrawPile {} DrawPile {
cardWidth: app.cardWidth
cardHeight: app.cardHeight
}
} }
} }
} }
@ -53,6 +106,9 @@ ApplicationWindow {
Tableau { Tableau {
anchors.top: firstRow.bottom anchors.top: firstRow.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 50 anchors.topMargin: app.cardHeight * 0.5
cardWidth: app.cardWidth
cardHeight: app.cardHeight
} }
} }

View file

@ -4,7 +4,11 @@ import Solitare
// In solitare, Tableau refers to the part of the board where all of the columns are placed // In solitare, Tableau refers to the part of the board where all of the columns are placed
Row { Row {
id: tableau id: tableau
spacing: 10
required property real cardWidth
required property real cardHeight
spacing: cardWidth * 0.125
Repeater { Repeater {
id: colRepeater id: colRepeater
@ -13,9 +17,7 @@ Row {
delegate: Column { delegate: Column {
required property int index // passed from repeater required property int index // passed from repeater
onChildrenChanged: { spacing: -tableau.cardHeight * 0.8 // Overlap
spacing = -(slotRepeater.itemAt(0).height * 0.8); // Overlap
}
Repeater { Repeater {
id: slotRepeater id: slotRepeater
@ -25,6 +27,9 @@ Row {
required property int index // passed from repeater required property int index // passed from repeater
cardId: index cardId: index
columnId: parent.index // this is actually passed from the Column, not the Repeater columnId: parent.index // this is actually passed from the Column, not the Repeater
width: tableau.cardWidth
height: tableau.cardHeight
} }
} }
} }
@ -35,9 +40,6 @@ Row {
required property int cardId required property int cardId
property ColumnSlot colSlot: GameState.columns[columnId].length > 0 ? GameState.columns[columnId][cardId] : null property ColumnSlot colSlot: GameState.columns[columnId].length > 0 ? GameState.columns[columnId][cardId] : null
height: colCard.height
width: colCard.width
CardModel { CardModel {
id: colCard id: colCard
@ -67,9 +69,9 @@ Row {
color: "gray" color: "gray"
border.color: "white" border.color: "white"
border.width: 2 border.width: width * 0.025
opacity: 0.2 opacity: 0.2
radius: 5 radius: width * 0.05
} }
} }
} }

View file

@ -3,8 +3,12 @@ import Solitare
// The throwaway pile (shows last 3 cards) // The throwaway pile (shows last 3 cards)
Row { Row {
id: throwawayRow
required property real cardWidth
required property real cardHeight
// This allows makes the cards overlap // This allows makes the cards overlap
spacing: -60 spacing: -cardWidth * 0.75
Repeater { Repeater {
model: Math.min(GameState.throwawayPile.length, 3) model: Math.min(GameState.throwawayPile.length, 3)
@ -13,6 +17,9 @@ Row {
required property int index // passed from repeater required property int index // passed from repeater
property int reversedIndex: Math.min(GameState.throwawayPile.length, 3) - 1 - index property int reversedIndex: Math.min(GameState.throwawayPile.length, 3) - 1 - index
width: throwawayRow.cardWidth
height: throwawayRow.cardHeight
card: GameState.throwawayPile[GameState.throwawayPile.length - 1 - reversedIndex] card: GameState.throwawayPile[GameState.throwawayPile.length - 1 - reversedIndex]
isFaceDown: false isFaceDown: false
onClicked: { onClicked: {
@ -21,9 +28,9 @@ Row {
if (reversedIndex == 0) { if (reversedIndex == 0) {
if (GameState.autoMoveThrownCard()) { if (GameState.autoMoveThrownCard()) {
if (GameState.isWinnable()) { if (GameState.isWinnable()) {
console.log("Still winnable") console.log("Still winnable");
} else { } else {
console.log("Game is lost") console.log("Game is lost");
} }
} }
} }