From d6dbd50abc669520d6a7908ba5030aec8433d581 Mon Sep 17 00:00:00 2001 From: James O'Leary Date: Thu, 10 Mar 2022 20:43:16 +0000 Subject: [PATCH] Update styles to match latest specs A) Refactor hue/chroma specification to support specification of hue rotation and chroma depending on source color hue. Update vibrant & expressive specs to use granular specification of hue rotations for secondary and tertiary palettes for both, and expressive's neutral uses specification of chroma for the neutral palette. B) Use chroma equal to seed color for all palettes, replacing some usage of >= C) Remove chroma 40 cap at tone 90 Test: atest ColorSchemeTest Bug: N/A Change-Id: Ib9fff483443ccae888faf636d4388b9658ff3dfe --- .../com/android/systemui/monet/ColorScheme.kt | 208 +++++++++++++----- .../com/android/systemui/monet/Shades.java | 3 - .../systemui/monet/ColorSchemeTest.java | 4 +- 3 files changed, 158 insertions(+), 57 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 d8b050aa8e2c3..46dad02ddb450 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt @@ -31,37 +31,134 @@ const val ACCENT1_CHROMA = 48.0f const val GOOGLE_BLUE = 0xFF1b6ef3.toInt() const val MIN_CHROMA = 5 -internal enum class ChromaStrategy { - EQ, GTE -} +internal interface Hue { + fun get(sourceColor: Cam): Double -internal enum class HueStrategy { - SOURCE, ADD, SUBTRACT -} - -internal class Chroma(val strategy: ChromaStrategy, val value: Double) { - fun get(sourceChroma: Double): Double { - return when (strategy) { - ChromaStrategy.EQ -> value - ChromaStrategy.GTE -> sourceChroma.coerceAtLeast(value) + /** + * 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 + */ + fun getHueRotation(sourceHue: Float, hueAndRotations: List>): Double { + for (i in 0..hueAndRotations.size) { + val previousIndex = if (i == 0) hueAndRotations.size - 1 else i - 1 + val thisHue = hueAndRotations[i].first + val previousHue = hueAndRotations[previousIndex].first + if (ColorScheme.angleIsBetween(sourceHue, thisHue, previousHue)) { + return ColorScheme.wrapDegreesDouble(sourceHue.toDouble() + + hueAndRotations[previousIndex].first) + } } + + // If this statement executes, something is wrong, there should have been a rotation + // found using the arrays. + return sourceHue.toDouble() } } -internal class Hue(val strategy: HueStrategy = HueStrategy.SOURCE, val value: Double = 0.0) { - fun get(sourceHue: Double): Double { - return when (strategy) { - HueStrategy.SOURCE -> sourceHue - HueStrategy.ADD -> ColorScheme.wrapDegreesDouble(sourceHue + value) - HueStrategy.SUBTRACT -> ColorScheme.wrapDegreesDouble(sourceHue - value) - } +internal class HueSource : Hue { + override fun get(sourceColor: Cam): Double { + return sourceColor.hue.toDouble() } } -internal class TonalSpec(val hue: Hue = Hue(), val chroma: Chroma) { +internal class HueAdd(val amountDegrees: Double) : Hue { + override fun get(sourceColor: Cam): Double { + return ColorScheme.wrapDegreesDouble(sourceColor.hue.toDouble() + amountDegrees) + } +} + +internal class HueSubtract(val amountDegrees: Double) : Hue { + override fun get(sourceColor: Cam): Double { + return ColorScheme.wrapDegreesDouble(sourceColor.hue.toDouble() - amountDegrees) + } +} + +internal class HueVibrantSecondary() : Hue { + val hueToRotations = listOf(Pair(24, 15), Pair(53, 15), Pair(91, 15), Pair(123, 15), + Pair(141, 15), Pair(172, 15), Pair(198, 15), Pair(234, 18), Pair(272, 18), + Pair(302, 18), Pair(329, 30), Pair(354, 15)) + override fun get(sourceColor: Cam): Double { + return getHueRotation(sourceColor.hue, hueToRotations) + } +} + +internal class HueVibrantTertiary() : Hue { + val hueToRotations = listOf(Pair(24, 30), Pair(53, 30), Pair(91, 15), Pair(123, 30), + Pair(141, 27), Pair(172, 27), Pair(198, 30), Pair(234, 35), Pair(272, 30), + Pair(302, 30), Pair(329, 60), Pair(354, 30)) + override fun get(sourceColor: Cam): Double { + return getHueRotation(sourceColor.hue, hueToRotations) + } +} + +internal class HueExpressiveSecondary() : Hue { + val hueToRotations = listOf(Pair(24, 95), Pair(53, 45), Pair(91, 45), Pair(123, 20), + Pair(141, 45), Pair(172, 45), Pair(198, 15), Pair(234, 15), + Pair(272, 45), Pair(302, 45), Pair(329, 45), Pair(354, 45)) + override fun get(sourceColor: Cam): Double { + return getHueRotation(sourceColor.hue, hueToRotations) + } +} + +internal class HueExpressiveTertiary() : Hue { + val hueToRotations = listOf(Pair(24, 20), Pair(53, 20), Pair(91, 20), Pair(123, 45), + Pair(141, 20), Pair(172, 20), Pair(198, 90), Pair(234, 90), Pair(272, 20), + Pair(302, 20), Pair(329, 120), Pair(354, 120)) + override fun get(sourceColor: Cam): Double { + return getHueRotation(sourceColor.hue, hueToRotations) + } +} + +internal interface Chroma { + 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. + * + * @param sourceHue hue of source color + * @param hueAndChromas list of pairs, where the first item in a pair is a hue, and the + * second item in the pair is a chroma that should be applied + */ + fun getSpecifiedChroma(sourceHue: Float, hueAndChromas: List>): Double { + for (i in 0..hueAndChromas.size) { + val previousIndex = if (i == 0) hueAndChromas.size - 1 else i - 1 + val thisHue = hueAndChromas[i].first + val previousHue = hueAndChromas[previousIndex].first + if (ColorScheme.angleIsBetween(sourceHue, thisHue, previousHue)) { + return hueAndChromas[i].second.toDouble() + } + } + + // If this statement executes, something is wrong, there should have been a rotation + // found using the arrays. + return sourceHue.toDouble() + } +} + +internal class ChromaConstant(val chroma: Double) : Chroma { + override fun get(sourceColor: Cam): Double { + return chroma + } +} + +internal class ChromaExpressiveNeutral() : Chroma { + val hueToChromas = listOf(Pair(24, 8), Pair(53, 8), Pair(91, 8), Pair(123, 8), + Pair(141, 6), Pair(172, 6), Pair(198, 8), Pair(234, 8), Pair(272, 8), + Pair(302, 8), Pair(329, 8), Pair(354, 8)) + override fun get(sourceColor: Cam): Double { + return getSpecifiedChroma(sourceColor.hue, hueToChromas) + } +} + +internal class TonalSpec(val hue: Hue = HueSource(), val chroma: Chroma) { fun shades(sourceColor: Cam): List { - val hue = hue.get(sourceColor.hue.toDouble()) - val chroma = chroma.get(sourceColor.chroma.toDouble()) + val hue = hue.get(sourceColor) + val chroma = chroma.get(sourceColor) return Shades.of(hue.toFloat(), chroma.toFloat()).toList() } } @@ -76,46 +173,46 @@ internal class CoreSpec( enum class Style(internal val coreSpec: CoreSpec) { SPRITZ(CoreSpec( - a1 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 12.0)), - a2 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 8.0)), - a3 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 16.0)), - n1 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 4.0)), - n2 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 8.0)) + 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( - a1 = TonalSpec(chroma = Chroma(ChromaStrategy.GTE, 32.0)), - a2 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 16.0)), - a3 = TonalSpec(Hue(HueStrategy.ADD, 60.0), Chroma(ChromaStrategy.EQ, 24.0)), - n1 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 4.0)), - n2 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 8.0)) + a1 = TonalSpec(HueSource(), ChromaConstant(36.0)), + a2 = TonalSpec(HueSource(), ChromaConstant(16.0)), + a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)), + n1 = TonalSpec(HueSource(), ChromaConstant(4.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(8.0)) )), VIBRANT(CoreSpec( - a1 = TonalSpec(chroma = Chroma(ChromaStrategy.GTE, 48.0)), - a2 = TonalSpec(Hue(HueStrategy.ADD, 15.0), Chroma(ChromaStrategy.EQ, 24.0)), - a3 = TonalSpec(Hue(HueStrategy.ADD, 30.0), Chroma(ChromaStrategy.GTE, 32.0)), - n1 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 8.0)), - n2 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 16.0)) + a1 = TonalSpec(HueSource(), ChromaConstant(48.0)), + a2 = TonalSpec(HueVibrantSecondary(), ChromaConstant(24.0)), + a3 = TonalSpec(HueVibrantTertiary(), ChromaConstant(32.0)), + n1 = TonalSpec(HueSource(), ChromaConstant(6.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(12.0)) )), EXPRESSIVE(CoreSpec( - a1 = TonalSpec(Hue(HueStrategy.SUBTRACT, 60.0), Chroma(ChromaStrategy.GTE, 64.0)), - a2 = TonalSpec(Hue(HueStrategy.SUBTRACT, 30.0), Chroma(ChromaStrategy.EQ, 24.0)), - a3 = TonalSpec(chroma = Chroma(ChromaStrategy.GTE, 48.0)), - n1 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 12.0)), - n2 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 16.0)) + a1 = TonalSpec(HueAdd(240.0), ChromaConstant(40.0)), + a2 = TonalSpec(HueExpressiveSecondary(), ChromaConstant(24.0)), + a3 = TonalSpec(HueExpressiveTertiary(), ChromaConstant(40.0)), + n1 = TonalSpec(HueAdd(15.0), ChromaExpressiveNeutral()), + n2 = TonalSpec(HueAdd(15.0), ChromaConstant(12.0)) )), RAINBOW(CoreSpec( - a1 = TonalSpec(chroma = Chroma(ChromaStrategy.GTE, 48.0)), - a2 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 16.0)), - a3 = TonalSpec(Hue(HueStrategy.ADD, 60.0), Chroma(ChromaStrategy.EQ, 24.0)), - n1 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 0.0)), - n2 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 0.0)) + 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( - a1 = TonalSpec(Hue(HueStrategy.SUBTRACT, 50.0), Chroma(ChromaStrategy.GTE, 48.0)), - a2 = TonalSpec(Hue(HueStrategy.SUBTRACT, 50.0), Chroma(ChromaStrategy.EQ, 36.0)), - a3 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 36.0)), - n1 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 10.0)), - n2 = TonalSpec(chroma = Chroma(ChromaStrategy.EQ, 16.0)) + 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)) )), } @@ -296,6 +393,13 @@ class ColorScheme( return seeds } + internal fun angleIsBetween(angle: Float, a: Int, b: Int): Boolean { + if (a < b) { + return a <= angle && angle <= b + } + return a <= angle || angle <= b + } + private fun wrapDegrees(degrees: Int): Int { return when { degrees < 0 -> { diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java b/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java index aab3538e3c4c1..c97b9601f7435 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/Shades.java @@ -59,9 +59,6 @@ public class Shades { shades[1] = ColorUtils.CAMToColor(hue, Math.min(40f, chroma), 95); for (int i = 2; i < 12; i++) { float lStar = (i == 6) ? MIDDLE_LSTAR : 100 - 10 * (i - 1); - if (lStar >= 90) { - chroma = Math.min(40f, chroma); - } shades[i] = ColorUtils.CAMToColor(hue, chroma, lStar); } return shades; diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java index 6df56e98783c1..51088b1d929ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java @@ -123,7 +123,7 @@ public class ColorSchemeTest extends SysuiTestCase { Style.VIBRANT /* style */); int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2); Cam cam = Cam.fromInt(neutralMid); - Assert.assertEquals(cam.getChroma(), 8.0, 1.0); + Assert.assertTrue(cam.getChroma() <= 8.0); } @Test @@ -133,7 +133,7 @@ public class ColorSchemeTest extends SysuiTestCase { Style.EXPRESSIVE /* style */); int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2); Cam cam = Cam.fromInt(neutralMid); - Assert.assertEquals(cam.getChroma(), 12.0, 1.0); + Assert.assertTrue(cam.getChroma() <= 8.0); } /**