diff --git a/README.md b/README.md index 0e8985f..630df05 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,31 @@ them. - **Production** — stable builds, published from `main`. - **Dev** — preview builds, published from `dev` on every green CI run. +## Branding + +The launcher icon is a single square of the Wallace tartan, and the app opens on +a tartan splash carrying the name in a Celtic uncial hand for two seconds before +fading into the menu. + +The tartan is drawn, not an image: `ui/theme/Tartan.kt` lays the sett down as +warp and then as weft at half opacity, which is what produces the blended +squares instead of a flat grid. The icon is the same construction as a vector +drawable, so it stays sharp at every density and needs no PNGs. + +Two honest caveats: + +- **The sett is an approximation.** `WallaceSett` reproduces what the Wallace + tartan reads as — a bold red and black check divided by a narrow gold + overstripe — but the registered threadcount could not be verified, so the + numbers were chosen to look right rather than to be authoritative. They are + plain data; replacing them with the registered counts rebrands the whole app + and needs no other change. +- **The typeface is bundled under a licence.** Uncial Antiqua, © 2011 Brian J. + Bonislawsky DBA Astigmatic, Reserved Font Name "Uncial Antiqua", used under + the SIL Open Font License 1.1. The full licence is at + [docs/licenses/UncialAntiqua-OFL.txt](docs/licenses/UncialAntiqua-OFL.txt) and + must stay with the font — the OFL requires it to be distributed alongside. + ## Screen sizes There are no fixed orientations or hard-coded card sizes. Layouts measure the diff --git a/app/src/main/java/org/prolibertate/games/ui/AppRoot.kt b/app/src/main/java/org/prolibertate/games/ui/AppRoot.kt index d886eae..8902894 100644 --- a/app/src/main/java/org/prolibertate/games/ui/AppRoot.kt +++ b/app/src/main/java/org/prolibertate/games/ui/AppRoot.kt @@ -82,11 +82,9 @@ fun AppRoot(env: AppEnv) { env.updater.installedChannel() == UpdateChannel.DEV } - fun push(route: Route) { - stack = stack + route - } + val push: (Route) -> Unit = { route -> stack = stack + route } - fun pop() { + val pop: () -> Unit = { if (stack.size > 1) { // Leaving a lobby or a table tears the networking down with it. when (stack.last()) { @@ -99,6 +97,35 @@ fun AppRoot(env: AppEnv) { BackHandler(enabled = stack.size > 1) { pop() } + var splashDone by remember { mutableStateOf(false) } + + Box(modifier = Modifier.fillMaxSize()) { + AppContent( + env = env, + settings = settings, + stack = stack, + showComingSoon = showComingSoon, + push = push, + pop = pop, + ) + + // Over the top, so the menu behind it is already composed when it fades. + if (!splashDone) { + SplashScreen(onFinished = { splashDone = true }) + } + } +} + +/** The navigation stack proper, kept separate so the splash can sit over it. */ +@Composable +private fun AppContent( + env: AppEnv, + settings: Settings, + stack: List, + showComingSoon: Boolean, + push: (Route) -> Unit, + pop: () -> Unit, +) { when (val route = stack.last()) { is Route.Menu -> MainMenuScreen( // Dev builds show the games that are not finished yet; production diff --git a/app/src/main/java/org/prolibertate/games/ui/SplashScreen.kt b/app/src/main/java/org/prolibertate/games/ui/SplashScreen.kt new file mode 100644 index 0000000..a7be877 --- /dev/null +++ b/app/src/main/java/org/prolibertate/games/ui/SplashScreen.kt @@ -0,0 +1,125 @@ +package org.prolibertate.games.ui + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import org.prolibertate.games.R +import org.prolibertate.games.ui.theme.TartanBackground + +/** How long the splash holds at full opacity before fading. */ +private const val SPLASH_HOLD_MILLIS = 2_000L + +private const val SPLASH_FADE_MILLIS = 600 + +/** + * Uncial Antiqua — a Celtic uncial face, bundled under the SIL Open Font + * License. See docs/licenses/UncialAntiqua-OFL.txt. + */ +val CelticFontFamily = FontFamily(Font(R.font.uncial_antiqua)) + +/** + * The launch screen: Wallace tartan under the name of the house. + * + * Drawn as an overlay rather than a separate destination, so the menu is + * already composed and ready behind it and the fade reveals a live screen + * instead of a blank one. + */ +@Composable +fun SplashScreen( + onFinished: () -> Unit, + modifier: Modifier = Modifier, +) { + var fading by remember { mutableStateOf(false) } + val alpha by animateFloatAsState( + targetValue = if (fading) 0f else 1f, + animationSpec = tween(durationMillis = SPLASH_FADE_MILLIS), + label = "splashFade", + ) + + LaunchedEffect(Unit) { + delay(SPLASH_HOLD_MILLIS) + fading = true + delay(SPLASH_FADE_MILLIS.toLong()) + onFinished() + } + + BoxWithConstraints(modifier = modifier.fillMaxSize().alpha(alpha)) { + // Weave scale follows the screen so the cloth reads the same on a phone + // and a tablet rather than turning into a fine grid on a large display. + val threadWidth = (minOf(maxWidth, maxHeight).value * 0.012f).dp + TartanBackground( + modifier = Modifier.fillMaxSize(), + threadWidth = threadWidth, + ) + + // A scrim keeps the lettering legible over the busiest part of the sett. + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.45f)) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Pro Libertate", + style = TextStyle( + fontFamily = CelticFontFamily, + fontSize = (minOf(maxWidth, maxHeight).value * 0.13f).sp, + color = Color(0xFFF7EFD8), + textAlign = TextAlign.Center, + shadow = Shadow( + color = Color.Black, + offset = Offset(0f, 3f), + blurRadius = 10f, + ), + ), + ) + Text( + text = "Games", + style = TextStyle( + fontFamily = CelticFontFamily, + fontSize = (minOf(maxWidth, maxHeight).value * 0.13f).sp, + color = Color(0xFFF2C200), + textAlign = TextAlign.Center, + shadow = Shadow( + color = Color.Black, + offset = Offset(0f, 3f), + blurRadius = 10f, + ), + ), + ) + } + } +} diff --git a/app/src/main/java/org/prolibertate/games/ui/theme/Tartan.kt b/app/src/main/java/org/prolibertate/games/ui/theme/Tartan.kt new file mode 100644 index 0000000..a5024ad --- /dev/null +++ b/app/src/main/java/org/prolibertate/games/ui/theme/Tartan.kt @@ -0,0 +1,97 @@ +package org.prolibertate.games.ui.theme + +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** One band of a tartan sett, measured in threads. */ +data class TartanStripe(val color: Color, val threads: Int) + +val WallaceRed = Color(0xFFB3121B) +val WallaceBlack = Color(0xFF111111) +val WallaceGold = Color(0xFFF2C200) + +/** + * The Wallace sett. + * + * NOTE: this is an approximation, not a certified threadcount. It reproduces + * what the Wallace tartan reads as — a bold red and black check divided by a + * narrow gold overstripe — but the exact thread counts held by the Scottish + * Register of Tartans could not be verified from here, so the numbers below + * are chosen to look right rather than to be authoritative. They are plain + * data: replacing them with the registered counts changes the whole app's + * branding and needs no other edit. + */ +val WallaceSett: List = listOf( + TartanStripe(WallaceRed, 40), + TartanStripe(WallaceBlack, 40), + TartanStripe(WallaceGold, 4), +) + +/** + * Draws a tartan by laying the sett down vertically as warp, then across as + * weft at half opacity. The overlap is what produces the blended squares and + * the sense of a woven cloth rather than a printed grid — red warp under black + * weft comes out a darker red, exactly as it does on the loom. + */ +fun DrawScope.drawTartan( + sett: List = WallaceSett, + threadSize: Float, +) { + if (sett.isEmpty() || threadSize <= 0f) return + val repeat = sett.sumOf { it.threads } * threadSize + if (repeat <= 0f) return + + // Warp: full-height bands. + var x = 0f + while (x < size.width) { + var offset = x + for (stripe in sett) { + val width = stripe.threads * threadSize + drawRect( + color = stripe.color, + topLeft = Offset(offset, 0f), + size = Size(width, size.height), + ) + offset += width + } + x += repeat + } + + // Weft: full-width bands, half opacity so the warp shows through. + var y = 0f + while (y < size.height) { + var offset = y + for (stripe in sett) { + val height = stripe.threads * threadSize + drawRect( + color = stripe.color.copy(alpha = 0.5f), + topLeft = Offset(0f, offset), + size = Size(size.width, height), + ) + offset += height + } + y += repeat + } +} + +/** + * A tartan fill. [threadWidth] sets the weave scale, so the same cloth can be + * a fine weave on a phone and a coarse one on a tablet without redrawing it. + */ +@Composable +fun TartanBackground( + modifier: Modifier = Modifier, + sett: List = WallaceSett, + threadWidth: Dp = 2.dp, +) { + Canvas(modifier = modifier) { + drawTartan(sett = sett, threadSize = threadWidth.toPx()) + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index a1cb143..09262d8 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,11 +1,59 @@ + - + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 5c8a39a..0e5a48e 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,14 +1,15 @@ + - - + android:fillColor="#00000000" + android:pathData="M0,0h108v108h-108z" /> diff --git a/app/src/main/res/font/uncial_antiqua.ttf b/app/src/main/res/font/uncial_antiqua.ttf new file mode 100644 index 0000000..05ded21 Binary files /dev/null and b/app/src/main/res/font/uncial_antiqua.ttf differ diff --git a/app/src/main/res/mipmap/ic_launcher.xml b/app/src/main/res/mipmap/ic_launcher.xml index 8ed1d4d..d1b3320 100644 --- a/app/src/main/res/mipmap/ic_launcher.xml +++ b/app/src/main/res/mipmap/ic_launcher.xml @@ -1,17 +1,21 @@ - + - - - + android:viewportWidth="84" + android:viewportHeight="84"> + + + + + + + + + + + diff --git a/app/src/main/res/mipmap/ic_launcher_round.xml b/app/src/main/res/mipmap/ic_launcher_round.xml index 69e9235..d1b3320 100644 --- a/app/src/main/res/mipmap/ic_launcher_round.xml +++ b/app/src/main/res/mipmap/ic_launcher_round.xml @@ -1,17 +1,21 @@ - + - - - + android:viewportWidth="84" + android:viewportHeight="84"> + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 48d3808..97c6599 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,9 @@ - #1B5E20 + #B3121B #FF000000 #FFFFFFFF + + #4A0B10 diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index bf1e235..492b38a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -5,5 +5,6 @@ diff --git a/docs/licenses/UncialAntiqua-OFL.txt b/docs/licenses/UncialAntiqua-OFL.txt new file mode 100644 index 0000000..d252554 --- /dev/null +++ b/docs/licenses/UncialAntiqua-OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2011 by Brian J. Bonislawsky DBA Astigmatic (AOETI) (astigma@astigmatic.com), with Reserved Font Name "Uncial Antiqua" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE.