feat(ui): implement Display P3, Ultra HDR support, and perzeptive Oklab color interpolation
This commit is contained in:
parent
41a21c30c8
commit
57fa02bd6f
2 changed files with 123 additions and 0 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
package dev.kaizen.app
|
package dev.kaizen.app
|
||||||
|
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.SystemBarStyle
|
import androidx.activity.SystemBarStyle
|
||||||
|
|
@ -12,6 +14,16 @@ import dev.kaizen.app.ui.theme.KaizenTheme
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Enable High Dynamic Range (HDR) window color mode on Android 14+ (API 34+)
|
||||||
|
// and Wide Color Gamut (Display P3) on Android 8.0+ (API 26+)
|
||||||
|
// This leverages the hardware capabilities of high-end AMOLED/OLED displays (S25 Ultra, Pixel 10).
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
window.colorMode = ActivityInfo.COLOR_MODE_HDR
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
window.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
|
||||||
|
}
|
||||||
|
|
||||||
// auto() picks light/dark system-bar icons based on the system theme
|
// auto() picks light/dark system-bar icons based on the system theme
|
||||||
enableEdgeToEdge(
|
enableEdgeToEdge(
|
||||||
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||||
|
|
|
||||||
111
app/src/main/java/dev/kaizen/app/ui/theme/Oklab.kt
Normal file
111
app/src/main/java/dev/kaizen/app/ui/theme/Oklab.kt
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package dev.kaizen.app.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.colorspace.ColorSpaces
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-performance Oklab and Oklch (polar Oklab) color interpolation library.
|
||||||
|
*
|
||||||
|
* Pre-calculates linear perception color spaces to completely eliminate sRGB
|
||||||
|
* "muddy gray dead zones" in gradients, providing butter-smooth, luminous, and
|
||||||
|
* vibrant color transitions across all Android displays (from SDR to Display P3 / HDR).
|
||||||
|
*/
|
||||||
|
object Oklab {
|
||||||
|
|
||||||
|
private fun toLinear(c: Float): Float {
|
||||||
|
return if (c <= 0.04045f) c / 12.92f else Math.pow(((c + 0.055) / 1.055), 2.4).toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toSRGB(c: Float): Float {
|
||||||
|
val cl = c.coerceIn(0f, 1f)
|
||||||
|
return if (cl <= 0.0031308f) cl * 12.92f else (1.055f * Math.pow(cl.toDouble(), 1.0 / 2.4) - 0.055f).toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Lab(val l: Float, val a: Float, val b: Float)
|
||||||
|
|
||||||
|
/** Converts standard Compose Color (sRGB or P3) to Oklab space coordinates */
|
||||||
|
fun Color.toOklab(): Lab {
|
||||||
|
// Convert to linear RGB
|
||||||
|
val lr = toLinear(red)
|
||||||
|
val lg = toLinear(green)
|
||||||
|
val lb = toLinear(blue)
|
||||||
|
|
||||||
|
// Linear RGB to LMS
|
||||||
|
val l = 0.4122214708f * lr + 0.5363325363f * lg + 0.0514459929f * lb
|
||||||
|
val m = 0.1167116119f * lr + 0.6858151024f * lg + 0.1974732856f * lb
|
||||||
|
val s = 0.0883315616f * lr + 0.1117281988f * lg + 0.7999602417f * lb
|
||||||
|
|
||||||
|
// LMS to non-linear scaled LMS
|
||||||
|
val l_ = Math.cbrt(l.toDouble()).toFloat()
|
||||||
|
val m_ = Math.cbrt(m.toDouble()).toFloat()
|
||||||
|
val s_ = Math.cbrt(s.toDouble()).toFloat()
|
||||||
|
|
||||||
|
// Scaled LMS to Oklab (L, a, b)
|
||||||
|
val oklabL = 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_
|
||||||
|
val oklabA = 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_
|
||||||
|
val oklabB = 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_
|
||||||
|
|
||||||
|
return Lab(oklabL, oklabA, oklabB)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts Oklab coordinates back to standard Compose Color in DisplayP3 or sRGB space */
|
||||||
|
fun Lab.toColor(targetColorSpace: androidx.compose.ui.graphics.colorspace.ColorSpace = ColorSpaces.Srgb): Color {
|
||||||
|
// Oklab to scaled LMS
|
||||||
|
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
|
||||||
|
|
||||||
|
// Scaled LMS to LMS (cube)
|
||||||
|
val l = l_ * l_ * l_
|
||||||
|
val m = m_ * m_ * m_
|
||||||
|
val s = s_ * s_ * s_
|
||||||
|
|
||||||
|
// LMS to linear RGB
|
||||||
|
val lr = 4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s
|
||||||
|
val lg = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s
|
||||||
|
val lb = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s
|
||||||
|
|
||||||
|
// Linear RGB to non-linear standard sRGB
|
||||||
|
val r = toSRGB(lr)
|
||||||
|
val g = toSRGB(lg)
|
||||||
|
val b = toSRGB(lb)
|
||||||
|
|
||||||
|
return Color(red = r, green = g, blue = b, alpha = 1.0f, colorSpace = targetColorSpace)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Linearly interpolates two colors inside the perzeptive Oklab color space */
|
||||||
|
fun interpolate(from: Color, to: Color, fraction: Float): Color {
|
||||||
|
val oklabFrom = from.toOklab()
|
||||||
|
val oklabTo = to.toOklab()
|
||||||
|
|
||||||
|
val interpolatedL = oklabFrom.l + (oklabTo.l - oklabFrom.l) * fraction
|
||||||
|
val interpolatedA = oklabFrom.a + (oklabTo.a - oklabFrom.a) * fraction
|
||||||
|
val interpolatedB = oklabFrom.b + (oklabTo.b - oklabFrom.b) * fraction
|
||||||
|
|
||||||
|
val interpolatedAlpha = from.alpha + (to.alpha - from.alpha) * fraction
|
||||||
|
|
||||||
|
// Match the target color space (prefer DisplayP3 if either color is P3)
|
||||||
|
val targetSpace = if (from.colorSpace == ColorSpaces.DisplayP3 || to.colorSpace == ColorSpaces.DisplayP3) {
|
||||||
|
ColorSpaces.DisplayP3
|
||||||
|
} else {
|
||||||
|
ColorSpaces.Srgb
|
||||||
|
}
|
||||||
|
|
||||||
|
return Lab(interpolatedL, interpolatedA, interpolatedB).toColor(targetSpace).copy(alpha = interpolatedAlpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a list of interpolated Color stops in Oklab space.
|
||||||
|
*
|
||||||
|
* Perfect for constructing smooth Sweep, Radial, or Linear gradients that feel
|
||||||
|
* incredibly rich and high-fidelity on high-end device screens (S25 Ultra, Pixel 10).
|
||||||
|
*/
|
||||||
|
fun generateGradientStops(from: Color, to: Color, steps: Int = 12): List<Color> {
|
||||||
|
val list = ArrayList<Color>(steps)
|
||||||
|
for (i in 0 until steps) {
|
||||||
|
val fraction = i.toFloat() / (steps - 1).toFloat()
|
||||||
|
list.add(interpolate(from, to, fraction))
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue