From 5aed3dab08840c2290f3a674dd2f40102312b8dd Mon Sep 17 00:00:00 2001 From: AJ Date: Wed, 17 Apr 2024 11:13:02 -0700 Subject: [PATCH] Normalize hue angles instead of clamping --- .../com/github/ajalt/colormath/Color.kt | 5 +- .../colormath/internal/ColorSpaceUtils.kt | 54 +++++++++++++++++-- .../com/github/ajalt/colormath/model/HPLuv.kt | 4 +- .../com/github/ajalt/colormath/model/HSL.kt | 4 +- .../com/github/ajalt/colormath/model/HSLuv.kt | 2 +- .../com/github/ajalt/colormath/model/HSV.kt | 4 +- .../com/github/ajalt/colormath/model/HWB.kt | 5 +- .../com/github/ajalt/colormath/model/LCHab.kt | 2 +- .../com/github/ajalt/colormath/model/LCHuv.kt | 2 +- .../com/github/ajalt/colormath/model/Oklch.kt | 4 +- .../github/ajalt/colormath/model/HSLTest.kt | 18 +++++++ .../github/ajalt/colormath/model/OklchTest.kt | 16 ++++++ 12 files changed, 101 insertions(+), 19 deletions(-) diff --git a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/Color.kt b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/Color.kt index e7088bc1..1895a377 100644 --- a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/Color.kt +++ b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/Color.kt @@ -95,7 +95,10 @@ interface Color { val info = space.components[i] if (values[i] !in info.min..info.max) { clamped = true - values[i] = values[i].coerceIn(info.min, info.max) + values[i] = when { + info.isPolar -> values[i] % 360 + else -> values[i].coerceIn(info.min, info.max) + } } } return if (clamped) space.create(values) else this diff --git a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/internal/ColorSpaceUtils.kt b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/internal/ColorSpaceUtils.kt index 04e662cf..09799443 100644 --- a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/internal/ColorSpaceUtils.kt +++ b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/internal/ColorSpaceUtils.kt @@ -72,7 +72,7 @@ internal fun polarComponentInfo( name = it.toString(), isPolar = it == 'H', min = if (it == 'H') 0f else l, - max = if (it == 'H') 1f else r + max = if (it == 'H') 360f else r ) } add(alphaInfo) @@ -88,9 +88,9 @@ internal inline fun T.clamp3( ): T { val (c1, c2, c3) = space.components return when { - v1 >= c1.min && v1 <= c1.max - && v2 >= c2.min && v2 <= c2.max - && v3 >= c3.min && v3 <= c3.max + v1 in c1.min..c1.max + && v2 in c2.min..c2.max + && v3 in c3.min..c3.max && alpha in 0f..1f -> this else -> copy( @@ -101,3 +101,49 @@ internal inline fun T.clamp3( ) } } + +internal inline fun T.clampLeadingHue( + v1: Float, + v2: Float, + v3: Float, + alpha: Float, + copy: (v1: Float, v2: Float, v3: Float, alpha: Float) -> T, +): T { + val (c1, c2, c3) = space.components + return when { + v1 in c1.min..c1.max + && v2 in c2.min..c2.max + && v3 in c3.min..c3.max + && alpha in 0f..1f -> this + + else -> copy( + v1 % 360, + v2.coerceIn(c2.min, c2.max), + v3.coerceIn(c3.min, c3.max), + alpha.coerceIn(0f, 1f) + ) + } +} + +internal inline fun T.clampTrailingHue( + v1: Float, + v2: Float, + v3: Float, + alpha: Float, + copy: (v1: Float, v2: Float, v3: Float, alpha: Float) -> T, +): T { + val (c1, c2, c3) = space.components + return when { + v1 in c1.min..c1.max + && v2 in c2.min..c2.max + && v3 in c3.min..c3.max + && alpha in 0f..1f -> this + + else -> copy( + v1.coerceIn(c1.min, c1.max), + v2.coerceIn(c2.min, c2.max), + v3 % 360, + alpha.coerceIn(0f, 1f) + ) + } +} diff --git a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HPLuv.kt b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HPLuv.kt index 87b159e2..ee9339fd 100644 --- a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HPLuv.kt +++ b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HPLuv.kt @@ -4,7 +4,7 @@ import com.github.ajalt.colormath.Color import com.github.ajalt.colormath.ColorComponentInfo import com.github.ajalt.colormath.ColorSpace import com.github.ajalt.colormath.HueColor -import com.github.ajalt.colormath.internal.clamp3 +import com.github.ajalt.colormath.internal.clampLeadingHue import com.github.ajalt.colormath.internal.doCreate import com.github.ajalt.colormath.internal.polarComponentInfo @@ -52,5 +52,5 @@ data class HPLuv( override fun toXYZ(): XYZ = toLCHuv().toXYZ() override fun toHPLuv(): HPLuv = this override fun toArray(): FloatArray = floatArrayOf(h, p, l, alpha) - override fun clamp(): HPLuv = clamp3(h, p, l, alpha, ::copy) + override fun clamp(): HPLuv = clampLeadingHue(h, p, l, alpha, ::copy) } diff --git a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSL.kt b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSL.kt index 01e7149a..18e113ec 100644 --- a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSL.kt +++ b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSL.kt @@ -1,7 +1,7 @@ package com.github.ajalt.colormath.model import com.github.ajalt.colormath.* -import com.github.ajalt.colormath.internal.clamp3 +import com.github.ajalt.colormath.internal.clampLeadingHue import com.github.ajalt.colormath.internal.doCreate import com.github.ajalt.colormath.internal.normalizeDeg import com.github.ajalt.colormath.internal.polarComponentInfo @@ -66,5 +66,5 @@ data class HSL(override val h: Float, val s: Float, val l: Float, override val a override fun toHSL() = this override fun toArray(): FloatArray = floatArrayOf(h, s, l, alpha) - override fun clamp(): HSL = clamp3(h, s, l, alpha, ::copy) + override fun clamp(): HSL = clampLeadingHue(h, s, l, alpha, ::copy) } diff --git a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSLuv.kt b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSLuv.kt index 54f8cd91..3b995157 100644 --- a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSLuv.kt +++ b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSLuv.kt @@ -52,7 +52,7 @@ data class HSLuv( override fun toXYZ(): XYZ = toLCHuv().toXYZ() override fun toHSLuv(): HSLuv = this override fun toArray(): FloatArray = floatArrayOf(h, s, l, alpha) - override fun clamp(): HSLuv = clamp3(h, s, l, alpha, ::copy) + override fun clamp(): HSLuv = clampLeadingHue(h, s, l, alpha, ::copy) } diff --git a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSV.kt b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSV.kt index 7f7d9df1..ea1936a2 100644 --- a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSV.kt +++ b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HSV.kt @@ -4,7 +4,7 @@ import com.github.ajalt.colormath.Color import com.github.ajalt.colormath.ColorComponentInfo import com.github.ajalt.colormath.ColorSpace import com.github.ajalt.colormath.HueColor -import com.github.ajalt.colormath.internal.clamp3 +import com.github.ajalt.colormath.internal.clampLeadingHue import com.github.ajalt.colormath.internal.doCreate import com.github.ajalt.colormath.internal.normalizeDeg import com.github.ajalt.colormath.internal.polarComponentInfo @@ -59,5 +59,5 @@ data class HSV(override val h: Float, val s: Float, val v: Float, override val a override fun toHSV() = this override fun toArray(): FloatArray = floatArrayOf(h, s, v, alpha) - override fun clamp(): HSV = clamp3(h, s, v, alpha, ::copy) + override fun clamp(): HSV = clampLeadingHue(h, s, v, alpha, ::copy) } diff --git a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HWB.kt b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HWB.kt index 3df1f4b3..3698ff9b 100644 --- a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HWB.kt +++ b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/HWB.kt @@ -4,10 +4,9 @@ import com.github.ajalt.colormath.Color import com.github.ajalt.colormath.ColorComponentInfo import com.github.ajalt.colormath.ColorSpace import com.github.ajalt.colormath.HueColor -import com.github.ajalt.colormath.internal.clamp3 +import com.github.ajalt.colormath.internal.clampLeadingHue import com.github.ajalt.colormath.internal.doCreate import com.github.ajalt.colormath.internal.polarComponentInfo -import kotlin.math.roundToInt /** * A color model represented with Hue, Whiteness, and Blackness. @@ -79,5 +78,5 @@ data class HWB(override val h: Float, val w: Float, val b: Float, override val a override fun toHWB(): HWB = this override fun toArray(): FloatArray = floatArrayOf(h, w, b, alpha) - override fun clamp(): HWB = clamp3(h, w, b, alpha, ::copy) + override fun clamp(): HWB = clampLeadingHue(h, w, b, alpha, ::copy) } diff --git a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LCHab.kt b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LCHab.kt index ed1f0340..2d58fab4 100644 --- a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LCHab.kt +++ b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LCHab.kt @@ -78,5 +78,5 @@ data class LCHab internal constructor( override fun toLCHab(): LCHab = this override fun toArray(): FloatArray = floatArrayOf(l, c, h, alpha) - override fun clamp(): LCHab = clamp3(l, c, h, alpha, ::copy) + override fun clamp(): LCHab = clampTrailingHue(l, c, h, alpha, ::copy) } diff --git a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LCHuv.kt b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LCHuv.kt index c084fa69..65eab015 100644 --- a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LCHuv.kt +++ b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/LCHuv.kt @@ -88,5 +88,5 @@ data class LCHuv internal constructor( override fun toLCHuv(): LCHuv = this override fun toArray(): FloatArray = floatArrayOf(l, c, h, alpha) - override fun clamp(): LCHuv = clamp3(l, c, h, alpha, ::copy) + override fun clamp(): LCHuv = clampTrailingHue(l, c, h, alpha, ::copy) } diff --git a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Oklch.kt b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Oklch.kt index 723884d5..c1fbaadd 100644 --- a/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Oklch.kt +++ b/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Oklch.kt @@ -4,7 +4,7 @@ import com.github.ajalt.colormath.Color import com.github.ajalt.colormath.ColorComponentInfo import com.github.ajalt.colormath.ColorSpace import com.github.ajalt.colormath.HueColor -import com.github.ajalt.colormath.internal.clamp3 +import com.github.ajalt.colormath.internal.* import com.github.ajalt.colormath.internal.componentInfoList import com.github.ajalt.colormath.internal.doCreate import com.github.ajalt.colormath.internal.fromPolarModel @@ -45,5 +45,5 @@ data class Oklch( override fun toOklab(): Oklab = fromPolarModel(c, h) { a, b -> Oklab(l, a, b, alpha) } override fun toOklch(): Oklch = this override fun toArray(): FloatArray = floatArrayOf(l, c, h, alpha) - override fun clamp(): Oklch = clamp3(l, c, h, alpha, ::copy) + override fun clamp(): Oklch = clampTrailingHue(l, c, h, alpha, ::copy) } diff --git a/test/src/commonTest/kotlin/com/github/ajalt/colormath/model/HSLTest.kt b/test/src/commonTest/kotlin/com/github/ajalt/colormath/model/HSLTest.kt index a6e34e4c..486752a1 100644 --- a/test/src/commonTest/kotlin/com/github/ajalt/colormath/model/HSLTest.kt +++ b/test/src/commonTest/kotlin/com/github/ajalt/colormath/model/HSLTest.kt @@ -1,7 +1,11 @@ package com.github.ajalt.colormath.model import com.github.ajalt.colormath.roundtripTest +import com.github.ajalt.colormath.shouldEqualColor import com.github.ajalt.colormath.testColorConversions +import io.kotest.data.blocking.forAll +import io.kotest.data.row +import io.kotest.matchers.types.shouldBeSameInstanceAs import kotlin.js.JsName import kotlin.test.Test @@ -26,4 +30,18 @@ class HSLTest { HSL(144.00, 0.50, 0.60) to HSV(144.0, 0.5, 0.8), HSL(0.00, 0.00, 1.00) to HSV(0.0, 0.0, 1.0), ) + + @Test + fun clamp() { + forAll( + row(HSL(0.0, 0.0, 0.0), HSL(0.0, 0.0, 0.0)), + row(HSL(359, 1.0, 1.0), HSL(359, 1.0, 1.0)), + row(HSL(361, 1.0, 1.0), HSL(1, 1.0, 1.0)), + row(HSL(180, 2, 2), HSL(180, 1.0, 1.0)), + ) { hsl, ex -> + hsl.clamp().shouldEqualColor(ex) + } + val hsl = HSL(359, .9, .9, .9) + hsl.clamp().shouldBeSameInstanceAs(hsl) + } } diff --git a/test/src/commonTest/kotlin/com/github/ajalt/colormath/model/OklchTest.kt b/test/src/commonTest/kotlin/com/github/ajalt/colormath/model/OklchTest.kt index aaa99b59..b851ebcc 100644 --- a/test/src/commonTest/kotlin/com/github/ajalt/colormath/model/OklchTest.kt +++ b/test/src/commonTest/kotlin/com/github/ajalt/colormath/model/OklchTest.kt @@ -1,7 +1,11 @@ package com.github.ajalt.colormath.model import com.github.ajalt.colormath.roundtripTest +import com.github.ajalt.colormath.shouldEqualColor import com.github.ajalt.colormath.testColorConversions +import io.kotest.data.blocking.forAll +import io.kotest.data.row +import io.kotest.matchers.types.shouldBeSameInstanceAs import kotlin.js.JsName import kotlin.test.Test @@ -18,4 +22,16 @@ class OklchTest { Oklab(0.25, 0.5, 0.75) to Oklch(0.25, 0.90138782, 56.30993247), Oklab(1.0, 1.0, 1.0) to Oklch(1.0, 1.41421356, 45.0), ) + + @Test + fun clamp() { + forAll( + row(Oklch(0.0, 0.0, 0.0), Oklch(0.0, 0.0, 0.0)), + row(Oklch(-1, -1, 361, 3), Oklch(0.0, 0.0, 1)), + ) { color, ex -> + color.clamp().shouldEqualColor(ex) + } + val oklch = Oklch(.9, .2, 359, .9) + oklch.clamp().shouldBeSameInstanceAs(oklch) + } }