1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.monet 18 19 import android.annotation.ColorInt 20 import android.app.WallpaperColors 21 import android.graphics.Color 22 import com.android.internal.graphics.ColorUtils 23 import com.android.internal.graphics.cam.Cam 24 import com.android.internal.graphics.cam.CamUtils 25 import kotlin.math.absoluteValue 26 import kotlin.math.max 27 import kotlin.math.min 28 import kotlin.math.roundToInt 29 30 const val TAG = "ColorScheme" 31 32 const val ACCENT1_CHROMA = 48.0f 33 const val GOOGLE_BLUE = 0xFF1b6ef3.toInt() 34 const val MIN_CHROMA = 5 35 36 internal interface Hue { 37 fun get(sourceColor: Cam): Double 38 39 /** 40 * Given a hue, and a mapping of hues to hue rotations, find which hues in the mapping the hue 41 * fall betweens, and use the hue rotation of the lower hue. 42 * 43 * @param sourceHue hue of source color 44 * @param hueAndRotations list of pairs, where the first item in a pair is a hue, and the second 45 * item in the pair is a hue rotation that should be applied 46 */ 47 fun getHueRotation(sourceHue: Float, hueAndRotations: List<Pair<Int, Int>>): Double { 48 val sanitizedSourceHue = (if (sourceHue < 0 || sourceHue >= 360) 0 else sourceHue).toFloat() 49 for (i in 0..hueAndRotations.size - 2) { 50 val thisHue = hueAndRotations[i].first.toFloat() 51 val nextHue = hueAndRotations[i + 1].first.toFloat() 52 if (thisHue <= sanitizedSourceHue && sanitizedSourceHue < nextHue) { 53 return ColorScheme.wrapDegreesDouble( 54 sanitizedSourceHue.toDouble() + hueAndRotations[i].second 55 ) 56 } 57 } 58 59 // If this statement executes, something is wrong, there should have been a rotation 60 // found using the arrays. 61 return sourceHue.toDouble() 62 } 63 } 64 65 internal class HueSource : Hue { 66 override fun get(sourceColor: Cam): Double { 67 return sourceColor.hue.toDouble() 68 } 69 } 70 71 internal class HueAdd(val amountDegrees: Double) : Hue { 72 override fun get(sourceColor: Cam): Double { 73 return ColorScheme.wrapDegreesDouble(sourceColor.hue.toDouble() + amountDegrees) 74 } 75 } 76 77 internal class HueSubtract(val amountDegrees: Double) : Hue { 78 override fun get(sourceColor: Cam): Double { 79 return ColorScheme.wrapDegreesDouble(sourceColor.hue.toDouble() - amountDegrees) 80 } 81 } 82 83 internal class HueVibrantSecondary() : Hue { 84 val hueToRotations = 85 listOf( 86 Pair(0, 18), 87 Pair(41, 15), 88 Pair(61, 10), 89 Pair(101, 12), 90 Pair(131, 15), 91 Pair(181, 18), 92 Pair(251, 15), 93 Pair(301, 12), 94 Pair(360, 12) 95 ) 96 97 override fun get(sourceColor: Cam): Double { 98 return getHueRotation(sourceColor.hue, hueToRotations) 99 } 100 } 101 102 internal class HueVibrantTertiary() : Hue { 103 val hueToRotations = 104 listOf( 105 Pair(0, 35), 106 Pair(41, 30), 107 Pair(61, 20), 108 Pair(101, 25), 109 Pair(131, 30), 110 Pair(181, 35), 111 Pair(251, 30), 112 Pair(301, 25), 113 Pair(360, 25) 114 ) 115 116 override fun get(sourceColor: Cam): Double { 117 return getHueRotation(sourceColor.hue, hueToRotations) 118 } 119 } 120 121 internal class HueExpressiveSecondary() : Hue { 122 val hueToRotations = 123 listOf( 124 Pair(0, 45), 125 Pair(21, 95), 126 Pair(51, 45), 127 Pair(121, 20), 128 Pair(151, 45), 129 Pair(191, 90), 130 Pair(271, 45), 131 Pair(321, 45), 132 Pair(360, 45) 133 ) 134 135 override fun get(sourceColor: Cam): Double { 136 return getHueRotation(sourceColor.hue, hueToRotations) 137 } 138 } 139 140 internal class HueExpressiveTertiary() : Hue { 141 val hueToRotations = 142 listOf( 143 Pair(0, 120), 144 Pair(21, 120), 145 Pair(51, 20), 146 Pair(121, 45), 147 Pair(151, 20), 148 Pair(191, 15), 149 Pair(271, 20), 150 Pair(321, 120), 151 Pair(360, 120) 152 ) 153 154 override fun get(sourceColor: Cam): Double { 155 return getHueRotation(sourceColor.hue, hueToRotations) 156 } 157 } 158 159 internal interface Chroma { 160 fun get(sourceColor: Cam): Double 161 162 companion object { 163 val MAX_VALUE = 120.0 164 val MIN_VALUE = 0.0 165 } 166 } 167 168 internal class ChromaMaxOut : Chroma { 169 override fun get(sourceColor: Cam): Double { 170 // Intentionally high. Gamut mapping from impossible HCT to sRGB will ensure that 171 // the maximum chroma is reached, even if lower than this constant. 172 return Chroma.MAX_VALUE + 10.0 173 } 174 } 175 176 internal class ChromaMultiple(val multiple: Double) : Chroma { 177 override fun get(sourceColor: Cam): Double { 178 return sourceColor.chroma * multiple 179 } 180 } 181 182 internal class ChromaAdd(val amount: Double) : Chroma { 183 override fun get(sourceColor: Cam): Double { 184 return sourceColor.chroma + amount 185 } 186 } 187 188 internal class ChromaBound( 189 val baseChroma: Chroma, 190 val minVal: Double, 191 val maxVal: Double, 192 ) : Chroma { 193 override fun get(sourceColor: Cam): Double { 194 val result = baseChroma.get(sourceColor) 195 return min(max(result, minVal), maxVal) 196 } 197 } 198 199 internal class ChromaConstant(val chroma: Double) : Chroma { 200 override fun get(sourceColor: Cam): Double { 201 return chroma 202 } 203 } 204 205 internal class ChromaSource : Chroma { 206 override fun get(sourceColor: Cam): Double { 207 return sourceColor.chroma.toDouble() 208 } 209 } 210 211 internal class TonalSpec(val hue: Hue = HueSource(), val chroma: Chroma) { 212 fun shades(sourceColor: Cam): List<Int> { 213 val hue = hue.get(sourceColor) 214 val chroma = chroma.get(sourceColor) 215 return Shades.of(hue.toFloat(), chroma.toFloat()).toList() 216 } 217 218 fun getAtTone(sourceColor: Cam, tone: Float): Int { 219 val hue = hue.get(sourceColor) 220 val chroma = chroma.get(sourceColor) 221 return ColorUtils.CAMToColor(hue.toFloat(), chroma.toFloat(), (1000f - tone) / 10f) 222 } 223 } 224 225 internal class CoreSpec( 226 val a1: TonalSpec, 227 val a2: TonalSpec, 228 val a3: TonalSpec, 229 val n1: TonalSpec, 230 val n2: TonalSpec 231 ) 232 233 enum class Style(internal val coreSpec: CoreSpec) { 234 SPRITZ( 235 CoreSpec( 236 a1 = TonalSpec(HueSource(), ChromaConstant(12.0)), 237 a2 = TonalSpec(HueSource(), ChromaConstant(8.0)), 238 a3 = TonalSpec(HueSource(), ChromaConstant(16.0)), 239 n1 = TonalSpec(HueSource(), ChromaConstant(2.0)), 240 n2 = TonalSpec(HueSource(), ChromaConstant(2.0)) 241 ) 242 ), 243 TONAL_SPOT( 244 CoreSpec( 245 a1 = TonalSpec(HueSource(), ChromaConstant(36.0)), 246 a2 = TonalSpec(HueSource(), ChromaConstant(16.0)), 247 a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)), 248 n1 = TonalSpec(HueSource(), ChromaConstant(6.0)), 249 n2 = TonalSpec(HueSource(), ChromaConstant(8.0)) 250 ) 251 ), 252 VIBRANT( 253 CoreSpec( 254 a1 = TonalSpec(HueSource(), ChromaMaxOut()), 255 a2 = TonalSpec(HueVibrantSecondary(), ChromaConstant(24.0)), 256 a3 = TonalSpec(HueVibrantTertiary(), ChromaConstant(32.0)), 257 n1 = TonalSpec(HueSource(), ChromaConstant(10.0)), 258 n2 = TonalSpec(HueSource(), ChromaConstant(12.0)) 259 ) 260 ), 261 EXPRESSIVE( 262 CoreSpec( 263 a1 = TonalSpec(HueAdd(240.0), ChromaConstant(40.0)), 264 a2 = TonalSpec(HueExpressiveSecondary(), ChromaConstant(24.0)), 265 a3 = TonalSpec(HueExpressiveTertiary(), ChromaConstant(32.0)), 266 n1 = TonalSpec(HueAdd(15.0), ChromaConstant(8.0)), 267 n2 = TonalSpec(HueAdd(15.0), ChromaConstant(12.0)) 268 ) 269 ), 270 RAINBOW( 271 CoreSpec( 272 a1 = TonalSpec(HueSource(), ChromaConstant(48.0)), 273 a2 = TonalSpec(HueSource(), ChromaConstant(16.0)), 274 a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)), 275 n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), 276 n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) 277 ) 278 ), 279 FRUIT_SALAD( 280 CoreSpec( 281 a1 = TonalSpec(HueSubtract(50.0), ChromaConstant(48.0)), 282 a2 = TonalSpec(HueSubtract(50.0), ChromaConstant(36.0)), 283 a3 = TonalSpec(HueSource(), ChromaConstant(36.0)), 284 n1 = TonalSpec(HueSource(), ChromaConstant(10.0)), 285 n2 = TonalSpec(HueSource(), ChromaConstant(16.0)) 286 ) 287 ), 288 CONTENT( 289 CoreSpec( 290 a1 = TonalSpec(HueSource(), ChromaSource()), 291 a2 = TonalSpec(HueSource(), ChromaMultiple(0.33)), 292 a3 = TonalSpec(HueSource(), ChromaMultiple(0.66)), 293 n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)), 294 n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666)) 295 ) 296 ), 297 MONOCHROMATIC( 298 CoreSpec( 299 a1 = TonalSpec(HueSource(), ChromaConstant(.0)), 300 a2 = TonalSpec(HueSource(), ChromaConstant(.0)), 301 a3 = TonalSpec(HueSource(), ChromaConstant(.0)), 302 n1 = TonalSpec(HueSource(), ChromaConstant(.0)), 303 n2 = TonalSpec(HueSource(), ChromaConstant(.0)) 304 ) 305 ), 306 CLOCK( 307 CoreSpec( 308 a1 = TonalSpec(HueSource(), ChromaBound(ChromaSource(), 20.0, Chroma.MAX_VALUE)), 309 a2 = TonalSpec(HueAdd(10.0), ChromaBound(ChromaMultiple(0.85), 17.0, 40.0)), 310 a3 = TonalSpec(HueAdd(20.0), ChromaBound(ChromaAdd(20.0), 50.0, Chroma.MAX_VALUE)), 311 312 // Not Used 313 n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), 314 n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) 315 ) 316 ), 317 CLOCK_VIBRANT( 318 CoreSpec( 319 a1 = TonalSpec(HueSource(), ChromaBound(ChromaSource(), 70.0, Chroma.MAX_VALUE)), 320 a2 = TonalSpec(HueAdd(20.0), ChromaBound(ChromaSource(), 70.0, Chroma.MAX_VALUE)), 321 a3 = TonalSpec(HueAdd(60.0), ChromaBound(ChromaSource(), 70.0, Chroma.MAX_VALUE)), 322 323 // Not Used 324 n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), 325 n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) 326 ) 327 ) 328 } 329 330 class TonalPalette 331 internal constructor( 332 private val spec: TonalSpec, 333 seedColor: Int, 334 ) { 335 val seedCam: Cam = Cam.fromInt(seedColor) 336 val allShades: List<Int> = spec.shades(seedCam) 337 val allShadesMapped: Map<Int, Int> = SHADE_KEYS.zip(allShades).toMap() 338 val baseColor: Int 339 340 init { 341 val h = spec.hue.get(seedCam).toFloat() 342 val c = spec.chroma.get(seedCam).toFloat() 343 baseColor = ColorUtils.CAMToColor(h, c, CamUtils.lstarFromInt(seedColor)) 344 } 345 346 // Dynamically computed tones across the full range from 0 to 1000 347 fun getAtTone(tone: Float) = spec.getAtTone(seedCam, tone) 348 349 // Predefined & precomputed tones 350 val s10: Int 351 get() = this.allShades[0] 352 val s50: Int 353 get() = this.allShades[1] 354 val s100: Int 355 get() = this.allShades[2] 356 val s200: Int 357 get() = this.allShades[3] 358 val s300: Int 359 get() = this.allShades[4] 360 val s400: Int 361 get() = this.allShades[5] 362 val s500: Int 363 get() = this.allShades[6] 364 val s600: Int 365 get() = this.allShades[7] 366 val s700: Int 367 get() = this.allShades[8] 368 val s800: Int 369 get() = this.allShades[9] 370 val s900: Int 371 get() = this.allShades[10] 372 val s1000: Int 373 get() = this.allShades[11] 374 375 companion object { 376 val SHADE_KEYS = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) 377 } 378 } 379 380 @Deprecated("Please use com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors " + 381 "instead") 382 class ColorScheme( 383 @ColorInt val seed: Int, 384 val darkTheme: Boolean, 385 val style: Style = Style.TONAL_SPOT 386 ) { 387 388 val accent1: TonalPalette 389 val accent2: TonalPalette 390 val accent3: TonalPalette 391 val neutral1: TonalPalette 392 val neutral2: TonalPalette 393 394 constructor(@ColorInt seed: Int, darkTheme: Boolean) : this(seed, darkTheme, Style.TONAL_SPOT) 395 396 @JvmOverloads 397 constructor( 398 wallpaperColors: WallpaperColors, 399 darkTheme: Boolean, 400 style: Style = Style.TONAL_SPOT 401 ) : this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) 402 403 val allHues: List<TonalPalette> 404 get() { 405 return listOf(accent1, accent2, accent3, neutral1, neutral2) 406 } 407 408 val allAccentColors: List<Int> 409 get() { 410 val allColors = mutableListOf<Int>() 411 allColors.addAll(accent1.allShades) 412 allColors.addAll(accent2.allShades) 413 allColors.addAll(accent3.allShades) 414 return allColors 415 } 416 417 val allNeutralColors: List<Int> 418 get() { 419 val allColors = mutableListOf<Int>() 420 allColors.addAll(neutral1.allShades) 421 allColors.addAll(neutral2.allShades) 422 return allColors 423 } 424 425 val backgroundColor 426 get() = ColorUtils.setAlphaComponent(if (darkTheme) neutral1.s700 else neutral1.s10, 0xFF) 427 428 val accentColor 429 get() = ColorUtils.setAlphaComponent(if (darkTheme) accent1.s100 else accent1.s500, 0xFF) 430 431 init { 432 val proposedSeedCam = Cam.fromInt(seed) 433 val seedArgb = 434 if (seed == Color.TRANSPARENT) { 435 GOOGLE_BLUE 436 } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) { 437 GOOGLE_BLUE 438 } else { 439 seed 440 } 441 442 accent1 = TonalPalette(style.coreSpec.a1, seedArgb) 443 accent2 = TonalPalette(style.coreSpec.a2, seedArgb) 444 accent3 = TonalPalette(style.coreSpec.a3, seedArgb) 445 neutral1 = TonalPalette(style.coreSpec.n1, seedArgb) 446 neutral2 = TonalPalette(style.coreSpec.n2, seedArgb) 447 } 448 449 val shadeCount 450 get() = this.accent1.allShades.size 451 452 val seedTone: Float 453 get() = 1000f - CamUtils.lstarFromInt(seed) * 10f 454 455 override fun toString(): String { 456 return "ColorScheme {\n" + 457 " seed color: ${stringForColor(seed)}\n" + 458 " style: $style\n" + 459 " palettes: \n" + 460 " ${humanReadable("PRIMARY", accent1.allShades)}\n" + 461 " ${humanReadable("SECONDARY", accent2.allShades)}\n" + 462 " ${humanReadable("TERTIARY", accent3.allShades)}\n" + 463 " ${humanReadable("NEUTRAL", neutral1.allShades)}\n" + 464 " ${humanReadable("NEUTRAL VARIANT", neutral2.allShades)}\n" + 465 "}" 466 } 467 468 companion object { 469 /** 470 * Identifies a color to create a color scheme from. 471 * 472 * @param wallpaperColors Colors extracted from an image via quantization. 473 * @param filter If false, allow colors that have low chroma, creating grayscale themes. 474 * @return ARGB int representing the color 475 */ 476 @JvmStatic 477 @JvmOverloads 478 @ColorInt 479 fun getSeedColor(wallpaperColors: WallpaperColors, filter: Boolean = true): Int { 480 return getSeedColors(wallpaperColors, filter).first() 481 } 482 483 /** 484 * Filters and ranks colors from WallpaperColors. 485 * 486 * @param wallpaperColors Colors extracted from an image via quantization. 487 * @param filter If false, allow colors that have low chroma, creating grayscale themes. 488 * @return List of ARGB ints, ordered from highest scoring to lowest. 489 */ 490 @JvmStatic 491 @JvmOverloads 492 fun getSeedColors(wallpaperColors: WallpaperColors, filter: Boolean = true): List<Int> { 493 val totalPopulation = 494 wallpaperColors.allColors.values.reduce { a, b -> a + b }.toDouble() 495 val totalPopulationMeaningless = (totalPopulation == 0.0) 496 if (totalPopulationMeaningless) { 497 // WallpaperColors with a population of 0 indicate the colors didn't come from 498 // quantization. Instead of scoring, trust the ordering of the provided primary 499 // secondary/tertiary colors. 500 // 501 // In this case, the colors are usually from a Live Wallpaper. 502 val distinctColors = 503 wallpaperColors.mainColors 504 .map { it.toArgb() } 505 .distinct() 506 .filter { 507 if (!filter) { 508 true 509 } else { 510 Cam.fromInt(it).chroma >= MIN_CHROMA 511 } 512 } 513 .toList() 514 if (distinctColors.isEmpty()) { 515 return listOf(GOOGLE_BLUE) 516 } 517 return distinctColors 518 } 519 520 val intToProportion = 521 wallpaperColors.allColors.mapValues { it.value.toDouble() / totalPopulation } 522 val intToCam = wallpaperColors.allColors.mapValues { Cam.fromInt(it.key) } 523 524 // Get an array with 360 slots. A slot contains the percentage of colors with that hue. 525 val hueProportions = huePopulations(intToCam, intToProportion, filter) 526 // Map each color to the percentage of the image with its hue. 527 val intToHueProportion = 528 wallpaperColors.allColors.mapValues { 529 val cam = intToCam[it.key]!! 530 val hue = cam.hue.roundToInt() 531 var proportion = 0.0 532 for (i in hue - 15..hue + 15) { 533 proportion += hueProportions[wrapDegrees(i)] 534 } 535 proportion 536 } 537 // Remove any inappropriate seed colors. For example, low chroma colors look grayscale 538 // raising their chroma will turn them to a much louder color that may not have been 539 // in the image. 540 val filteredIntToCam = 541 if (!filter) intToCam 542 else 543 (intToCam.filter { 544 val cam = it.value 545 val proportion = intToHueProportion[it.key]!! 546 cam.chroma >= MIN_CHROMA && 547 (totalPopulationMeaningless || proportion > 0.01) 548 }) 549 // Sort the colors by score, from high to low. 550 val intToScoreIntermediate = 551 filteredIntToCam.mapValues { score(it.value, intToHueProportion[it.key]!!) } 552 val intToScore = intToScoreIntermediate.entries.toMutableList() 553 intToScore.sortByDescending { it.value } 554 555 // Go through the colors, from high score to low score. 556 // If the color is distinct in hue from colors picked so far, pick the color. 557 // Iteratively decrease the amount of hue distinctness required, thus ensuring we 558 // maximize difference between colors. 559 val minimumHueDistance = 15 560 val seeds = mutableListOf<Int>() 561 maximizeHueDistance@ for (i in 90 downTo minimumHueDistance step 1) { 562 seeds.clear() 563 for (entry in intToScore) { 564 val int = entry.key 565 val existingSeedNearby = 566 seeds.find { 567 val hueA = intToCam[int]!!.hue 568 val hueB = intToCam[it]!!.hue 569 hueDiff(hueA, hueB) < i 570 } != null 571 if (existingSeedNearby) { 572 continue 573 } 574 seeds.add(int) 575 if (seeds.size >= 4) { 576 break@maximizeHueDistance 577 } 578 } 579 } 580 581 if (seeds.isEmpty()) { 582 // Use gBlue 500 if there are 0 colors 583 seeds.add(GOOGLE_BLUE) 584 } 585 586 return seeds 587 } 588 589 private fun wrapDegrees(degrees: Int): Int { 590 return when { 591 degrees < 0 -> { 592 (degrees % 360) + 360 593 } 594 degrees >= 360 -> { 595 degrees % 360 596 } 597 else -> { 598 degrees 599 } 600 } 601 } 602 603 public fun wrapDegreesDouble(degrees: Double): Double { 604 return when { 605 degrees < 0 -> { 606 (degrees % 360) + 360 607 } 608 degrees >= 360 -> { 609 degrees % 360 610 } 611 else -> { 612 degrees 613 } 614 } 615 } 616 617 private fun hueDiff(a: Float, b: Float): Float { 618 return 180f - ((a - b).absoluteValue - 180f).absoluteValue 619 } 620 621 private fun stringForColor(color: Int): String { 622 val width = 4 623 val hct = Cam.fromInt(color) 624 val h = "H${hct.hue.roundToInt().toString().padEnd(width)}" 625 val c = "C${hct.chroma.roundToInt().toString().padEnd(width)}" 626 val t = "T${CamUtils.lstarFromInt(color).roundToInt().toString().padEnd(width)}" 627 val hex = Integer.toHexString(color and 0xffffff).padStart(6, '0').uppercase() 628 return "$h$c$t = #$hex" 629 } 630 631 private fun humanReadable(paletteName: String, colors: List<Int>): String { 632 return "$paletteName\n" + 633 colors.map { stringForColor(it) }.joinToString(separator = "\n") { it } 634 } 635 636 private fun score(cam: Cam, proportion: Double): Double { 637 val proportionScore = 0.7 * 100.0 * proportion 638 val chromaScore = 639 if (cam.chroma < ACCENT1_CHROMA) 0.1 * (cam.chroma - ACCENT1_CHROMA) 640 else 0.3 * (cam.chroma - ACCENT1_CHROMA) 641 return chromaScore + proportionScore 642 } 643 644 private fun huePopulations( 645 camByColor: Map<Int, Cam>, 646 populationByColor: Map<Int, Double>, 647 filter: Boolean = true 648 ): List<Double> { 649 val huePopulation = List(size = 360, init = { 0.0 }).toMutableList() 650 651 for (entry in populationByColor.entries) { 652 val population = populationByColor[entry.key]!! 653 val cam = camByColor[entry.key]!! 654 val hue = cam.hue.roundToInt() % 360 655 if (filter && cam.chroma <= MIN_CHROMA) { 656 continue 657 } 658 huePopulation[hue] = huePopulation[hue] + population 659 } 660 661 return huePopulation 662 } 663 } 664 } 665