From 75d990654b007fe4eb889aa10f9a668481e79fc2 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 22 Apr 2026 10:21:23 +0100 Subject: [PATCH] ui: OKLCH colors for gradients in onboarding cards (#6859) * ui: OKLCH colors for gradients in onboarding cards * add wide gamut to manifest --- .../Views/NewChat/OnboardingCards.swift | 16 ++--- apps/ios/SimpleXChat/Theme/Color.swift | 49 +++++++++++++++ .../android/src/main/AndroidManifest.xml | 1 + .../chat/simplex/common/ui/theme/Color.kt | 62 ++++++++++++++++--- .../common/views/newchat/OnboardingCards.kt | 16 ++--- 5 files changed, 121 insertions(+), 23 deletions(-) diff --git a/apps/ios/Shared/Views/NewChat/OnboardingCards.swift b/apps/ios/Shared/Views/NewChat/OnboardingCards.swift index 913fdf5577..dc0cf54afe 100644 --- a/apps/ios/Shared/Views/NewChat/OnboardingCards.swift +++ b/apps/ios/Shared/Views/NewChat/OnboardingCards.swift @@ -23,17 +23,17 @@ struct OnboardingCardView: View { let action: () -> Void static let lightStops: [Gradient.Stop] = [ - .init(color: Color(red: 0.824, green: 0.910, blue: 1.0), location: 0.0), - .init(color: Color(red: 0.800, green: 0.914, blue: 1.0), location: 0.5), - .init(color: Color(red: 0.875, green: 1.0, blue: 1.0), location: 0.9), - .init(color: Color(red: 1.0, green: 0.988, blue: 0.918), location: 1.0) + .init(color: oklch(0.9219, 0.0431, 249.4), location: 0.0), + .init(color: oklch(0.9198, 0.0471, 240.7), location: 0.5), + .init(color: oklch(0.9772, 0.0358, 196.6), location: 0.9), + .init(color: oklch(0.9886, 0.0272, 99.1), location: 1.0) ] static let darkStops: [Gradient.Stop] = [ - .init(color: Color(red: 0.016, green: 0.039, blue: 0.141), location: 0.4), - .init(color: Color(red: 0.220, green: 0.329, blue: 0.671), location: 0.72), - .init(color: Color(red: 0.659, green: 0.929, blue: 0.953), location: 0.9), - .init(color: Color(red: 1.0, green: 0.965, blue: 0.878), location: 1.0) + .init(color: oklch(0.1578, 0.0609, 267.3), location: 0.4), + .init(color: oklch(0.4729, 0.1574, 267.3), location: 0.72), + .init(color: oklch(0.9024, 0.0760, 202.8), location: 0.9), + .init(color: oklch(0.9744, 0.0370, 88.4), location: 1.0) ] static let gradientAngle: Double = 80.0 * .pi / 180.0 diff --git a/apps/ios/SimpleXChat/Theme/Color.swift b/apps/ios/SimpleXChat/Theme/Color.swift index f307eaa5aa..86eefa4482 100644 --- a/apps/ios/SimpleXChat/Theme/Color.swift +++ b/apps/ios/SimpleXChat/Theme/Color.swift @@ -33,6 +33,55 @@ let HighOrLowlight = Color(139, 135, 134, a: 255) //let FileLight = Color(183, 190, 199, a: 255) //let FileDark = Color(101, 101, 106, a: 255) +// Create a Display P3 Color from oklch components. H in degrees +public func oklch(_ L: Double, _ C: Double, _ H: Double, alpha: Double = 1.0) -> Color { + let hRad = H * .pi / 180.0 + let cosH = cos(hRad) + let sinH = sin(hRad) + + func linearP3(C: Double) -> (Double, Double, Double) { + let a = C * cosH + let b = C * sinH + // oklab → LMS (Ottosson 2021) + let l_ = L + 0.3963377774 * a + 0.2158037573 * b + let m_ = L - 0.1055613458 * a - 0.0638541728 * b + let s_ = L - 0.0894841775 * a - 1.2914855480 * b + let l = l_ * l_ * l_ + let m = m_ * m_ * m_ + let s = s_ * s_ * s_ + // LMS → linear Display P3 (direct, no sRGB clamping) + return ( + 3.1281105148 * l - 2.2570749853 * m + 0.1293047593 * s, + -1.0911282009 * l + 2.4132668169 * m - 0.3221681599 * s, + -0.0260136845 * l - 0.5080276339 * m + 1.5333166364 * s + ) + } + + func inGamut(_ r: Double, _ g: Double, _ b: Double) -> Bool { + r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1 + } + + // linear P3 → gamma-encoded P3 (same transfer function as sRGB) + func gammaEncode(_ x: Double) -> Double { + x >= 0.0031308 + ? 1.055 * pow(min(x, 1.0), 1.0 / 2.4) - 0.055 + : 12.92 * max(x, 0) + } + + var (r, g, b) = linearP3(C: C) + if !inGamut(r, g, b) { + var lo = 0.0, hi = C + while hi - lo > 1e-5 { + let mid = (lo + hi) / 2 + let (mr, mg, mb) = linearP3(C: mid) + if inGamut(mr, mg, mb) { lo = mid; r = mr; g = mg; b = mb } + else { hi = mid } + } + } + + return Color(.displayP3, red: gammaEncode(r), green: gammaEncode(g), blue: gammaEncode(b), opacity: alpha) +} + extension Color { public init(_ argb: Int64) { let a = Double((argb & 0xFF000000) >> 24) / 255.0 diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml index d6059896a5..9e059afa14 100644 --- a/apps/multiplatform/android/src/main/AndroidManifest.xml +++ b/apps/multiplatform/android/src/main/AndroidManifest.xml @@ -51,6 +51,7 @@ { + val a = c * cosH + val b = c * sinH + // oklab → LMS (Ottosson 2021) + val l_ = L + 0.3963377774f * a + 0.2158037573f * b + val m_ = L - 0.1055613458f * a - 0.0638541728f * b + val s_ = L - 0.0894841775f * a - 1.2914855480f * b + val l = l_ * l_ * l_ + val m = m_ * m_ * m_ + val s = s_ * s_ * s_ + // LMS → linear Display P3 + return Triple( + 3.1281105148f * l - 2.2570749853f * m + 0.1293047593f * s, + -1.0911282009f * l + 2.4132668169f * m - 0.3221681599f * s, + -0.0260136845f * l - 0.5080276339f * m + 1.5333166364f * s + ) + } + + fun inGamut(r: Float, g: Float, b: Float) = r in 0f..1f && g in 0f..1f && b in 0f..1f + + // linear P3 → gamma-encoded P3 (same transfer function as sRGB) + fun gammaEncode(x: Float): Float = + if (x >= 0.0031308f) 1.055f * min(x, 1f).pow(1f / 2.4f) - 0.055f + else 12.92f * max(x, 0f) + + var (r, g, b) = linearP3(C) + if (!inGamut(r, g, b)) { + var lo = 0f; var hi = C + while (hi - lo > 1e-5f) { + val mid = (lo + hi) / 2 + val (mr, mg, mb) = linearP3(mid) + if (inGamut(mr, mg, mb)) { lo = mid; r = mr; g = mg; b = mb } + else hi = mid + } + } + + return Color( + red = gammaEncode(r), + green = gammaEncode(g), + blue = gammaEncode(b), + alpha = alpha, + colorSpace = ColorSpaces.DisplayP3 + ) +} -val Purple200 = Color(0xFFBB86FC) -val Purple500 = Color(0xFF6200EE) -val Purple700 = Color(0xFF3700B3) -val Teal200 = Color(0xFF03DAC5) -val Gray = Color(0x22222222) val Indigo = Color(0xFF9966FF) val SimplexBlue = Color(0, 136, 255, 255) // If this value changes also need to update #0088ff in string resource files val SimplexGreen = Color(77, 218, 103, 255) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt index 98954eb74f..d287353ccc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/OnboardingCards.kt @@ -89,17 +89,17 @@ internal fun gradientPoints(aspectRatio: Float, scale: Float): GradientEndpoints } internal val lightStops = arrayOf( - 0.0f to Color(0xFFd2e8ff), - 0.5f to Color(0xFFcce9ff), - 0.9f to Color(0xFFdfffff), - 1.0f to Color(0xFFfffcea) + 0.0f to oklch(0.9219f, 0.0431f, 249.4f), + 0.5f to oklch(0.9198f, 0.0471f, 240.7f), + 0.9f to oklch(0.9772f, 0.0358f, 196.6f), + 1.0f to oklch(0.9886f, 0.0272f, 99.1f) ) internal val darkStops = arrayOf( - 0.4f to Color(0xFF040a24), - 0.72f to Color(0xFF3854ab), - 0.9f to Color(0xFFa8edf3), - 1.0f to Color(0xFFfff6e0) + 0.4f to oklch(0.1578f, 0.0609f, 267.3f), + 0.72f to oklch(0.4729f, 0.1574f, 267.3f), + 0.9f to oklch(0.9024f, 0.0760f, 202.8f), + 1.0f to oklch(0.9744f, 0.0370f, 88.4f) ) private fun Modifier.maxHeightByWidthRatio(ratio: Float) = layout { measurable, constraints ->