From 57405bd0a53bd96ccdade6aca6a6062ef837ef1b Mon Sep 17 00:00:00 2001 From: Hawkwood Glazier Date: Tue, 21 Mar 2023 22:05:09 +0000 Subject: [PATCH] Additional Color Palettes Bug: 274647325 Test: Checked new tonal specs Change-Id: Iae852a80567e2f0420009eaac7022d3e8c194f48 --- .../com/android/systemui/monet/ColorScheme.kt | 394 ++++++++++++------ .../systemui/plugins/ClockProviderPlugin.kt | 11 +- 2 files changed, 275 insertions(+), 130 deletions(-) diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt index db88b593e432a..204bac88bc0dc 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt @@ -23,6 +23,8 @@ import com.android.internal.graphics.ColorUtils import com.android.internal.graphics.cam.Cam import com.android.internal.graphics.cam.CamUtils import kotlin.math.absoluteValue +import kotlin.math.max +import kotlin.math.min import kotlin.math.roundToInt const val TAG = "ColorScheme" @@ -35,12 +37,12 @@ internal interface Hue { fun get(sourceColor: Cam): Double /** - * Given a hue, and a mapping of hues to hue rotations, find which hues in the mapping the - * hue fall betweens, and use the hue rotation of the lower hue. + * Given a hue, and a mapping of hues to hue rotations, find which hues in the mapping the hue + * fall betweens, and use the hue rotation of the lower hue. * * @param sourceHue hue of source color - * @param hueAndRotations list of pairs, where the first item in a pair is a hue, and the - * second item in the pair is a hue rotation that should be applied + * @param hueAndRotations list of pairs, where the first item in a pair is a hue, and the second + * item in the pair is a hue rotation that should be applied */ fun getHueRotation(sourceHue: Float, hueAndRotations: List>): Double { val sanitizedSourceHue = (if (sourceHue < 0 || sourceHue >= 360) 0 else sourceHue).toFloat() @@ -48,8 +50,9 @@ internal interface Hue { val thisHue = hueAndRotations[i].first.toFloat() val nextHue = hueAndRotations[i + 1].first.toFloat() if (thisHue <= sanitizedSourceHue && sanitizedSourceHue < nextHue) { - return ColorScheme.wrapDegreesDouble(sanitizedSourceHue.toDouble() + - hueAndRotations[i].second) + return ColorScheme.wrapDegreesDouble( + sanitizedSourceHue.toDouble() + hueAndRotations[i].second + ) } } @@ -78,8 +81,18 @@ internal class HueSubtract(val amountDegrees: Double) : Hue { } internal class HueVibrantSecondary() : Hue { - val hueToRotations = listOf(Pair(0, 18), Pair(41, 15), Pair(61, 10), Pair(101, 12), - Pair(131, 15), Pair(181, 18), Pair(251, 15), Pair(301, 12), Pair(360, 12)) + val hueToRotations = + listOf( + Pair(0, 18), + Pair(41, 15), + Pair(61, 10), + Pair(101, 12), + Pair(131, 15), + Pair(181, 18), + Pair(251, 15), + Pair(301, 12), + Pair(360, 12) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -87,8 +100,18 @@ internal class HueVibrantSecondary() : Hue { } internal class HueVibrantTertiary() : Hue { - val hueToRotations = listOf(Pair(0, 35), Pair(41, 30), Pair(61, 20), Pair(101, 25), - Pair(131, 30), Pair(181, 35), Pair(251, 30), Pair(301, 25), Pair(360, 25)) + val hueToRotations = + listOf( + Pair(0, 35), + Pair(41, 30), + Pair(61, 20), + Pair(101, 25), + Pair(131, 30), + Pair(181, 35), + Pair(251, 30), + Pair(301, 25), + Pair(360, 25) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -96,8 +119,18 @@ internal class HueVibrantTertiary() : Hue { } internal class HueExpressiveSecondary() : Hue { - val hueToRotations = listOf(Pair(0, 45), Pair(21, 95), Pair(51, 45), Pair(121, 20), - Pair(151, 45), Pair(191, 90), Pair(271, 45), Pair(321, 45), Pair(360, 45)) + val hueToRotations = + listOf( + Pair(0, 45), + Pair(21, 95), + Pair(51, 45), + Pair(121, 20), + Pair(151, 45), + Pair(191, 90), + Pair(271, 45), + Pair(321, 45), + Pair(360, 45) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -105,8 +138,18 @@ internal class HueExpressiveSecondary() : Hue { } internal class HueExpressiveTertiary() : Hue { - val hueToRotations = listOf(Pair(0, 120), Pair(21, 120), Pair(51, 20), Pair(121, 45), - Pair(151, 20), Pair(191, 15), Pair(271, 20), Pair(321, 120), Pair(360, 120)) + val hueToRotations = + listOf( + Pair(0, 120), + Pair(21, 120), + Pair(51, 20), + Pair(121, 45), + Pair(151, 20), + Pair(191, 15), + Pair(271, 20), + Pair(321, 120), + Pair(360, 120) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -115,13 +158,18 @@ internal class HueExpressiveTertiary() : Hue { internal interface Chroma { fun get(sourceColor: Cam): Double + + companion object { + val MAX_VALUE = 120.0 + val MIN_VALUE = 0.0 + } } internal class ChromaMaxOut : Chroma { override fun get(sourceColor: Cam): Double { // Intentionally high. Gamut mapping from impossible HCT to sRGB will ensure that // the maximum chroma is reached, even if lower than this constant. - return 130.0 + return Chroma.MAX_VALUE + 10.0 } } @@ -131,6 +179,23 @@ internal class ChromaMultiple(val multiple: Double) : Chroma { } } +internal class ChromaAdd(val amount: Double) : Chroma { + override fun get(sourceColor: Cam): Double { + return sourceColor.chroma + amount + } +} + +internal class ChromaBound( + val baseChroma: Chroma, + val minVal: Double, + val maxVal: Double, +) : Chroma { + override fun get(sourceColor: Cam): Double { + val result = baseChroma.get(sourceColor) + return min(max(result, minVal), maxVal) + } +} + internal class ChromaConstant(val chroma: Double) : Chroma { override fun get(sourceColor: Cam): Double { return chroma @@ -149,109 +214,173 @@ internal class TonalSpec(val hue: Hue = HueSource(), val chroma: Chroma) { val chroma = chroma.get(sourceColor) return Shades.of(hue.toFloat(), chroma.toFloat()).toList() } + + fun getAtTone(sourceColor: Cam, tone: Float): Int { + val hue = hue.get(sourceColor) + val chroma = chroma.get(sourceColor) + return ColorUtils.CAMToColor(hue.toFloat(), chroma.toFloat(), (1000f - tone) / 10f) + } } internal class CoreSpec( - val a1: TonalSpec, - val a2: TonalSpec, - val a3: TonalSpec, - val n1: TonalSpec, - val n2: TonalSpec + val a1: TonalSpec, + val a2: TonalSpec, + val a3: TonalSpec, + val n1: TonalSpec, + val n2: TonalSpec ) enum class Style(internal val coreSpec: CoreSpec) { - SPRITZ(CoreSpec( + SPRITZ( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(12.0)), a2 = TonalSpec(HueSource(), ChromaConstant(8.0)), a3 = TonalSpec(HueSource(), ChromaConstant(16.0)), n1 = TonalSpec(HueSource(), ChromaConstant(2.0)), n2 = TonalSpec(HueSource(), ChromaConstant(2.0)) - )), - TONAL_SPOT(CoreSpec( + ) + ), + TONAL_SPOT( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(36.0)), a2 = TonalSpec(HueSource(), ChromaConstant(16.0)), a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)), n1 = TonalSpec(HueSource(), ChromaConstant(6.0)), n2 = TonalSpec(HueSource(), ChromaConstant(8.0)) - )), - VIBRANT(CoreSpec( + ) + ), + VIBRANT( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaMaxOut()), a2 = TonalSpec(HueVibrantSecondary(), ChromaConstant(24.0)), a3 = TonalSpec(HueVibrantTertiary(), ChromaConstant(32.0)), n1 = TonalSpec(HueSource(), ChromaConstant(10.0)), n2 = TonalSpec(HueSource(), ChromaConstant(12.0)) - )), - EXPRESSIVE(CoreSpec( + ) + ), + EXPRESSIVE( + CoreSpec( a1 = TonalSpec(HueAdd(240.0), ChromaConstant(40.0)), a2 = TonalSpec(HueExpressiveSecondary(), ChromaConstant(24.0)), a3 = TonalSpec(HueExpressiveTertiary(), ChromaConstant(32.0)), n1 = TonalSpec(HueAdd(15.0), ChromaConstant(8.0)), n2 = TonalSpec(HueAdd(15.0), ChromaConstant(12.0)) - )), - RAINBOW(CoreSpec( + ) + ), + RAINBOW( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(48.0)), a2 = TonalSpec(HueSource(), ChromaConstant(16.0)), a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)), n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) - )), - FRUIT_SALAD(CoreSpec( + ) + ), + FRUIT_SALAD( + CoreSpec( a1 = TonalSpec(HueSubtract(50.0), ChromaConstant(48.0)), a2 = TonalSpec(HueSubtract(50.0), ChromaConstant(36.0)), a3 = TonalSpec(HueSource(), ChromaConstant(36.0)), n1 = TonalSpec(HueSource(), ChromaConstant(10.0)), n2 = TonalSpec(HueSource(), ChromaConstant(16.0)) - )), - CONTENT(CoreSpec( + ) + ), + CONTENT( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaSource()), a2 = TonalSpec(HueSource(), ChromaMultiple(0.33)), a3 = TonalSpec(HueSource(), ChromaMultiple(0.66)), n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)), n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666)) - )), - MONOCHROMATIC(CoreSpec( + ) + ), + MONOCHROMATIC( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(.0)), a2 = TonalSpec(HueSource(), ChromaConstant(.0)), a3 = TonalSpec(HueSource(), ChromaConstant(.0)), n1 = TonalSpec(HueSource(), ChromaConstant(.0)), n2 = TonalSpec(HueSource(), ChromaConstant(.0)) - )), + ) + ), + CLOCK( + CoreSpec( + a1 = TonalSpec(HueSource(), ChromaBound(ChromaSource(), 20.0, Chroma.MAX_VALUE)), + a2 = TonalSpec(HueAdd(10.0), ChromaBound(ChromaMultiple(0.85), 17.0, 40.0)), + a3 = TonalSpec(HueAdd(20.0), ChromaBound(ChromaAdd(20.0), 50.0, Chroma.MAX_VALUE)), + + // Not Used + n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) + ) + ), + CLOCK_VIBRANT( + CoreSpec( + a1 = TonalSpec(HueSource(), ChromaBound(ChromaSource(), 30.0, Chroma.MAX_VALUE)), + a2 = TonalSpec(HueAdd(20.0), ChromaBound(ChromaSource(), 30.0, Chroma.MAX_VALUE)), + a3 = TonalSpec(HueAdd(60.0), ChromaBound(ChromaSource(), 30.0, Chroma.MAX_VALUE)), + + // Not Used + n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) + ) + ) } -class TonalPalette { - val shadeKeys = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) - val allShades: List - val allShadesMapped: Map +class TonalPalette +internal constructor( + private val spec: TonalSpec, + seedColor: Int, +) { + val seedCam: Cam = Cam.fromInt(seedColor) + val allShades: List = spec.shades(seedCam) + val allShadesMapped: Map = SHADE_KEYS.zip(allShades).toMap() val baseColor: Int - internal constructor(spec: TonalSpec, seedColor: Int) { - val seedCam = Cam.fromInt(seedColor) - allShades = spec.shades(seedCam) - allShadesMapped = shadeKeys.zip(allShades).toMap() - + init { val h = spec.hue.get(seedCam).toFloat() val c = spec.chroma.get(seedCam).toFloat() baseColor = ColorUtils.CAMToColor(h, c, CamUtils.lstarFromInt(seedColor)) } - val s10: Int get() = this.allShades[0] - val s50: Int get() = this.allShades[1] - val s100: Int get() = this.allShades[2] - val s200: Int get() = this.allShades[3] - val s300: Int get() = this.allShades[4] - val s400: Int get() = this.allShades[5] - val s500: Int get() = this.allShades[6] - val s600: Int get() = this.allShades[7] - val s700: Int get() = this.allShades[8] - val s800: Int get() = this.allShades[9] - val s900: Int get() = this.allShades[10] - val s1000: Int get() = this.allShades[11] + // Dynamically computed tones across the full range from 0 to 1000 + fun getAtTone(tone: Float) = spec.getAtTone(seedCam, tone) + + // Predefined & precomputed tones + val s10: Int + get() = this.allShades[0] + val s50: Int + get() = this.allShades[1] + val s100: Int + get() = this.allShades[2] + val s200: Int + get() = this.allShades[3] + val s300: Int + get() = this.allShades[4] + val s400: Int + get() = this.allShades[5] + val s500: Int + get() = this.allShades[6] + val s600: Int + get() = this.allShades[7] + val s700: Int + get() = this.allShades[8] + val s800: Int + get() = this.allShades[9] + val s900: Int + get() = this.allShades[10] + val s1000: Int + get() = this.allShades[11] + + companion object { + val SHADE_KEYS = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) + } } class ColorScheme( - @ColorInt val seed: Int, - val darkTheme: Boolean, - val style: Style = Style.TONAL_SPOT + @ColorInt val seed: Int, + val darkTheme: Boolean, + val style: Style = Style.TONAL_SPOT ) { val accent1: TonalPalette @@ -260,16 +389,14 @@ class ColorScheme( val neutral1: TonalPalette val neutral2: TonalPalette - constructor(@ColorInt seed: Int, darkTheme: Boolean) : - this(seed, darkTheme, Style.TONAL_SPOT) + constructor(@ColorInt seed: Int, darkTheme: Boolean) : this(seed, darkTheme, Style.TONAL_SPOT) @JvmOverloads constructor( - wallpaperColors: WallpaperColors, - darkTheme: Boolean, - style: Style = Style.TONAL_SPOT - ) : - this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) + wallpaperColors: WallpaperColors, + darkTheme: Boolean, + style: Style = Style.TONAL_SPOT + ) : this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) val allHues: List get() { @@ -301,13 +428,14 @@ class ColorScheme( init { val proposedSeedCam = Cam.fromInt(seed) - val seedArgb = if (seed == Color.TRANSPARENT) { - GOOGLE_BLUE - } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) { - GOOGLE_BLUE - } else { - seed - } + val seedArgb = + if (seed == Color.TRANSPARENT) { + GOOGLE_BLUE + } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) { + GOOGLE_BLUE + } else { + seed + } accent1 = TonalPalette(style.coreSpec.a1, seedArgb) accent2 = TonalPalette(style.coreSpec.a2, seedArgb) @@ -316,19 +444,23 @@ class ColorScheme( neutral2 = TonalPalette(style.coreSpec.n2, seedArgb) } - val shadeCount get() = this.accent1.allShades.size + val shadeCount + get() = this.accent1.allShades.size + + val seedTone: Float + get() = 1000f - CamUtils.lstarFromInt(seed) * 10f override fun toString(): String { return "ColorScheme {\n" + - " seed color: ${stringForColor(seed)}\n" + - " style: $style\n" + - " palettes: \n" + - " ${humanReadable("PRIMARY", accent1.allShades)}\n" + - " ${humanReadable("SECONDARY", accent2.allShades)}\n" + - " ${humanReadable("TERTIARY", accent3.allShades)}\n" + - " ${humanReadable("NEUTRAL", neutral1.allShades)}\n" + - " ${humanReadable("NEUTRAL VARIANT", neutral2.allShades)}\n" + - "}" + " seed color: ${stringForColor(seed)}\n" + + " style: $style\n" + + " palettes: \n" + + " ${humanReadable("PRIMARY", accent1.allShades)}\n" + + " ${humanReadable("SECONDARY", accent2.allShades)}\n" + + " ${humanReadable("TERTIARY", accent3.allShades)}\n" + + " ${humanReadable("NEUTRAL", neutral1.allShades)}\n" + + " ${humanReadable("NEUTRAL VARIANT", neutral2.allShades)}\n" + + "}" } companion object { @@ -356,8 +488,8 @@ class ColorScheme( @JvmStatic @JvmOverloads fun getSeedColors(wallpaperColors: WallpaperColors, filter: Boolean = true): List { - val totalPopulation = wallpaperColors.allColors.values.reduce { a, b -> a + b } - .toDouble() + val totalPopulation = + wallpaperColors.allColors.values.reduce { a, b -> a + b }.toDouble() val totalPopulationMeaningless = (totalPopulation == 0.0) if (totalPopulationMeaningless) { // WallpaperColors with a population of 0 indicate the colors didn't come from @@ -365,51 +497,56 @@ class ColorScheme( // secondary/tertiary colors. // // In this case, the colors are usually from a Live Wallpaper. - val distinctColors = wallpaperColors.mainColors.map { - it.toArgb() - }.distinct().filter { - if (!filter) { - true - } else { - Cam.fromInt(it).chroma >= MIN_CHROMA - } - }.toList() + val distinctColors = + wallpaperColors.mainColors + .map { it.toArgb() } + .distinct() + .filter { + if (!filter) { + true + } else { + Cam.fromInt(it).chroma >= MIN_CHROMA + } + } + .toList() if (distinctColors.isEmpty()) { return listOf(GOOGLE_BLUE) } return distinctColors } - val intToProportion = wallpaperColors.allColors.mapValues { - it.value.toDouble() / totalPopulation - } + val intToProportion = + wallpaperColors.allColors.mapValues { it.value.toDouble() / totalPopulation } val intToCam = wallpaperColors.allColors.mapValues { Cam.fromInt(it.key) } // Get an array with 360 slots. A slot contains the percentage of colors with that hue. val hueProportions = huePopulations(intToCam, intToProportion, filter) // Map each color to the percentage of the image with its hue. - val intToHueProportion = wallpaperColors.allColors.mapValues { - val cam = intToCam[it.key]!! - val hue = cam.hue.roundToInt() - var proportion = 0.0 - for (i in hue - 15..hue + 15) { - proportion += hueProportions[wrapDegrees(i)] + val intToHueProportion = + wallpaperColors.allColors.mapValues { + val cam = intToCam[it.key]!! + val hue = cam.hue.roundToInt() + var proportion = 0.0 + for (i in hue - 15..hue + 15) { + proportion += hueProportions[wrapDegrees(i)] + } + proportion } - proportion - } // Remove any inappropriate seed colors. For example, low chroma colors look grayscale // raising their chroma will turn them to a much louder color that may not have been // in the image. - val filteredIntToCam = if (!filter) intToCam else (intToCam.filter { - val cam = it.value - val proportion = intToHueProportion[it.key]!! - cam.chroma >= MIN_CHROMA && - (totalPopulationMeaningless || proportion > 0.01) - }) + val filteredIntToCam = + if (!filter) intToCam + else + (intToCam.filter { + val cam = it.value + val proportion = intToHueProportion[it.key]!! + cam.chroma >= MIN_CHROMA && + (totalPopulationMeaningless || proportion > 0.01) + }) // Sort the colors by score, from high to low. - val intToScoreIntermediate = filteredIntToCam.mapValues { - score(it.value, intToHueProportion[it.key]!!) - } + val intToScoreIntermediate = + filteredIntToCam.mapValues { score(it.value, intToHueProportion[it.key]!!) } val intToScore = intToScoreIntermediate.entries.toMutableList() intToScore.sortByDescending { it.value } @@ -423,11 +560,12 @@ class ColorScheme( seeds.clear() for (entry in intToScore) { val int = entry.key - val existingSeedNearby = seeds.find { - val hueA = intToCam[int]!!.hue - val hueB = intToCam[it]!!.hue - hueDiff(hueA, hueB) < i - } != null + val existingSeedNearby = + seeds.find { + val hueA = intToCam[int]!!.hue + val hueB = intToCam[it]!!.hue + hueDiff(hueA, hueB) < i + } != null if (existingSeedNearby) { continue } @@ -489,22 +627,22 @@ class ColorScheme( } private fun humanReadable(paletteName: String, colors: List): String { - return "$paletteName\n" + colors.map { - stringForColor(it) - }.joinToString(separator = "\n") { it } + return "$paletteName\n" + + colors.map { stringForColor(it) }.joinToString(separator = "\n") { it } } private fun score(cam: Cam, proportion: Double): Double { val proportionScore = 0.7 * 100.0 * proportion - val chromaScore = if (cam.chroma < ACCENT1_CHROMA) 0.1 * (cam.chroma - ACCENT1_CHROMA) - else 0.3 * (cam.chroma - ACCENT1_CHROMA) + val chromaScore = + if (cam.chroma < ACCENT1_CHROMA) 0.1 * (cam.chroma - ACCENT1_CHROMA) + else 0.3 * (cam.chroma - ACCENT1_CHROMA) return chromaScore + proportionScore } private fun huePopulations( - camByColor: Map, - populationByColor: Map, - filter: Boolean = true + camByColor: Map, + populationByColor: Map, + filter: Boolean = true ): List { val huePopulation = List(size = 360, init = { 0.0 }).toMutableList() diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 0080517a722b7..c279053e6daf9 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -109,6 +109,9 @@ interface ClockEvents { /** Call whenever the locale changes */ fun onLocaleChanged(locale: Locale) {} + val isReactiveToTone + get() = true + /** Call whenever the color palette should update */ fun onColorPaletteChanged(resources: Resources) {} @@ -164,8 +167,12 @@ interface ClockFaceEvents { val hasCustomWeatherDataDisplay: Boolean get() = false - /** Region Darkness specific to the clock face */ - fun onRegionDarknessChanged(isDark: Boolean) {} + /** + * Region Darkness specific to the clock face. + * - isRegionDark = dark theme -> clock should be light + * - !isRegionDark = light theme -> clock should be dark + */ + fun onRegionDarknessChanged(isRegionDark: Boolean) {} /** * Call whenever font settings change. Pass in a target font size in pixels. The specific clock