From c9201a3920d16dab1c463fc2c59ae47be8bb6cba Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Mon, 22 Nov 2021 18:17:42 +0100 Subject: [PATCH] [orx-color] Fix NaN bugs in ColorOKHSLa, ColorOKHSVa --- orx-color/build.gradle.kts | 1 + .../commonMain/kotlin/spaces/ColorOKHSL.kt | 41 ++++++++-------- .../commonMain/kotlin/spaces/ColorOKHSV.kt | 29 +++++------ .../src/commonMain/kotlin/spaces/OKHelpers.kt | 21 ++++---- .../commonTest/kotlin/spaces/TestOKHSLa.kt | 43 ++++++++++++++++ .../commonTest/kotlin/spaces/TestOKHSVa.kt | 49 +++++++++++++++++++ 6 files changed, 139 insertions(+), 45 deletions(-) create mode 100644 orx-color/src/commonTest/kotlin/spaces/TestOKHSLa.kt create mode 100644 orx-color/src/commonTest/kotlin/spaces/TestOKHSVa.kt diff --git a/orx-color/build.gradle.kts b/orx-color/build.gradle.kts index 1ae6c2b3..9c3651d6 100644 --- a/orx-color/build.gradle.kts +++ b/orx-color/build.gradle.kts @@ -28,6 +28,7 @@ kotlin { implementation(project(":orx-camera")) implementation(project(":orx-mesh-generators")) implementation(project(":orx-color")) + implementation(project(":orx-jvm:orx-gui")) implementation("org.openrndr:openrndr-application:$openrndrVersion") implementation("org.openrndr:openrndr-extensions:$openrndrVersion") diff --git a/orx-color/src/commonMain/kotlin/spaces/ColorOKHSL.kt b/orx-color/src/commonMain/kotlin/spaces/ColorOKHSL.kt index 81e8c557..b5a5770a 100644 --- a/orx-color/src/commonMain/kotlin/spaces/ColorOKHSL.kt +++ b/orx-color/src/commonMain/kotlin/spaces/ColorOKHSL.kt @@ -22,29 +22,29 @@ data class ColorOKHSLa(val h: Double, val s: Double, val l: Double, val a: Doubl val L = lab.l val h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / PI; - val Cs = get_Cs(L, a_, b_) - val C_0 = Cs[0]; - val C_mid = Cs[1]; - val C_max = Cs[2]; + val cs = get_Cs(L, a_, b_) + val c0 = cs[0]; + val cMid = cs[1]; + val cMax = cs[2]; - val s = if (C < C_mid) { - val k_0 = 0; - val k_1 = 0.8 * C_0; - val k_2 = (1 - k_1 / C_mid); + val s = if (C < cMid) { + val k0 = 0; + val k1 = 0.8 * c0; + val k2 = (1 - k1 / cMid); - val t = (C - k_0) / (k_1 + k_2 * (C - k_0)); + val t = (C - k0) / (k1 + k2 * (C - k0)); t * 0.8; } else { - val k_0 = C_mid; - val k_1 = 0.2 * C_mid * C_mid * 1.25 * 1.25 / C_0; - val k_2 = (1 - (k_1) / (C_max - C_mid)); + val k0 = cMid; + val k1 = 0.2 * cMid * cMid * 1.25 * 1.25 / c0; + val k2 = (1 - (k1) / (cMax - cMid)); - val t = (C - k_0) / (k_1 + k_2 * (C - k_0)); + val t = (C - k0) / (k1 + k2 * (C - k0)); 0.8 + 0.2 * t; } val l = toe(L); - return ColorOKHSLa(h, s, l, c.a) + return ColorOKHSLa(h * 360.0, if (s == s) s else 0.0, if (l == l) l else 0.0, c.a) } } @@ -54,9 +54,9 @@ data class ColorOKHSLa(val h: Double, val s: Double, val l: Double, val a: Doubl } else if (l == 0.0) { ColorRGBa(0.0, 0.0, 0.0, a) } - val a_ = cos(2 * PI * h); - val b_ = sin(2 * PI * h); - val L = toe_inv(l); + val a_ = cos(2 * PI * h / 360.0); + val b_ = sin(2 * PI * h / 360.0); + val L = toeInv(l); val Cs = get_Cs(L, a_, b_); val C_0 = Cs[0]; @@ -94,12 +94,11 @@ data class ColorOKHSLa(val h: Double, val s: Double, val l: Double, val a: Doubl // 255*srgb_transfer_function(rgb[1]), // 255*srgb_transfer_function(rgb[2]), // ] - return ColorOKLABa(L, C * a_, C * b_).toRGBa().toSRGB() + return ColorOKLABa(if (L == L) L else 0.0, if (C == C) C * a_ else 0.0, if (C == C) C * b_ else 0.0).toRGBa().toSRGB() } override fun shiftHue(shiftInDegrees: Double): ColorOKHSLa { - val normalizedShift = shiftInDegrees / 360.0 - return copy(h = h + normalizedShift) + return copy(h = h + shiftInDegrees) } override fun opacify(factor: Double): ColorOKHSLa { @@ -125,7 +124,7 @@ data class ColorOKHSLa(val h: Double, val s: Double, val l: Double, val a: Doubl override fun mix(other: ColorOKHSLa, factor: Double): ColorOKHSLa { val sx = factor.coerceIn(0.0, 1.0) return ColorOKHSLa( - mixAngle(h * 360.0, other.h * 360.0, sx) / 360.0, + mixAngle(h, other.h, sx) / 360.0, (1.0 - sx) * s + sx * other.s, (1.0 - sx) * l + sx * other.l, (1.0 - sx) * a + sx * other.a diff --git a/orx-color/src/commonMain/kotlin/spaces/ColorOKHSV.kt b/orx-color/src/commonMain/kotlin/spaces/ColorOKHSV.kt index 1f806f10..91a9ca21 100644 --- a/orx-color/src/commonMain/kotlin/spaces/ColorOKHSV.kt +++ b/orx-color/src/commonMain/kotlin/spaces/ColorOKHSV.kt @@ -4,7 +4,6 @@ import org.openrndr.color.* import org.openrndr.math.mixAngle import kotlin.math.* - data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Double = 1.0) : HueShiftableColor, OpacifiableColor, @@ -17,8 +16,8 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl fun fromColorRGBa(c: ColorRGBa): ColorOKHSVa { val lab = c.toOKLABa() var C = sqrt(lab.a * lab.a + lab.b * lab.b); - val a_ = lab.a / C; - val b_ = lab.b / C; + val a_ = if (C != 0.0) lab.a / C else 0.0 + val b_ = if (C != 0.0) lab.b / C else 0.0 var L = lab.l val h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / PI; @@ -27,13 +26,13 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl val S_max = ST_max[0]; val S_0 = 0.5; val T = ST_max[1]; - val k = 1 - S_0 / S_max; + val k = if (S_max != 0.0) (1 - S_0 / S_max) else 0.0 val t = T / (C + L * T); val L_v = t * L; val C_v = t * C; - val L_vt = toe_inv(L_v); + val L_vt = toeInv(L_v); val C_vt = C_v * L_vt / L_v; val rgb_scale = ColorOKLABa(L_vt, a_ * C_vt, b_ * C_vt, c.a).toRGBa().toLinear() @@ -48,13 +47,13 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl val v = L / L_v; val s = (S_0 + T) * C_v / ((T * S_0) + T * k * C_v) - return ColorOKHSVa(h, s, v, c.a) + return ColorOKHSVa(h * 360.0, if (s == s) s else 0.0, if (v==v) v else 0.0, c.a) } } override fun toRGBa(): ColorRGBa { - val a_ = cos(2 * PI * h) - val b_ = sin(2 * PI * h) + val a_ = cos(2 * PI * h / 360.0) + val b_ = sin(2 * PI * h / 360.0) val ST_max = get_ST_max(a_, b_) val S_max = ST_max[0]; @@ -74,10 +73,10 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl //L = v*(1 - s*S_max/(S_max+T)); //C = v*s*S_max*T/(S_max+T); - val L_vt = toe_inv(L_v); + val L_vt = toeInv(L_v); val C_vt = C_v * L_vt / L_v; - val L_new = toe_inv(L); // * L_v/L_vt; + val L_new = toeInv(L); // * L_v/L_vt; C = C * L_new / L; L = L_new; @@ -89,12 +88,14 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl L *= scale_L; C *= scale_L; - return ColorOKLABa(L, C * a_, C * b_).toRGBa().toSRGB() + return ColorOKLABa( + if (L == L) L else 0.0, + if (C == C) C * a_ else 0.0, + if (C == C) C * b_ else 0.0).toRGBa().toSRGB() } override fun shiftHue(shiftInDegrees: Double): ColorOKHSVa { - val normalizedShift = shiftInDegrees / 360.0 - return copy(h = h + normalizedShift) + return copy(h = h + shiftInDegrees) } override fun opacify(factor: Double): ColorOKHSVa { @@ -120,7 +121,7 @@ data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Doubl override fun mix(other: ColorOKHSVa, factor: Double): ColorOKHSVa { val sx = factor.coerceIn(0.0, 1.0) return ColorOKHSVa( - mixAngle(h * 360.0, other.h * 360.0, sx) / 360.0, + mixAngle(h, other.h, sx), (1.0 - sx) * s + sx * other.s, (1.0 - sx) * v + sx * other.v, (1.0 - sx) * a + sx * other.a diff --git a/orx-color/src/commonMain/kotlin/spaces/OKHelpers.kt b/orx-color/src/commonMain/kotlin/spaces/OKHelpers.kt index 93d6cbff..2be2795c 100644 --- a/orx-color/src/commonMain/kotlin/spaces/OKHelpers.kt +++ b/orx-color/src/commonMain/kotlin/spaces/OKHelpers.kt @@ -12,20 +12,21 @@ internal fun max(a: Double, b: Double, c: Double, d: Double): Double { return max(max(a, b), max(c, d)) } - fun toe(x: Double): Double { - val k_1 = 0.206 - val k_2 = 0.03 - val k_3 = (1 + k_1) / (1 + k_2) + val k1 = 0.206 + val k2 = 0.03 + val k3 = (1 + k1) / (1 + k2) - return 0.5 * (k_3 * x - k_1 + sqrt((k_3 * x - k_1) * (k_3 * x - k_1) + 4 * k_2 * k_3 * x)) + val d = (k3 * x - k1) * (k3 * x - k1) + 4 * k2 * k3 * x + + return 0.5 * (k3 * x - k1 + sqrt(d.coerceAtLeast(0.0))) } -fun toe_inv(x: Double): Double { - val k_1 = 0.206 - val k_2 = 0.03 - val k_3 = (1 + k_1) / (1 + k_2) - return (x * x + k_1 * x) / (k_3 * (x + k_2)) +fun toeInv(x: Double): Double { + val k1 = 0.206 + val k2 = 0.03 + val k3 = (1 + k1) / (1 + k2) + return (x * x + k1 * x) / (k3 * (x + k2)) } internal fun compute_max_saturation(a: Double, b: Double): Double { diff --git a/orx-color/src/commonTest/kotlin/spaces/TestOKHSLa.kt b/orx-color/src/commonTest/kotlin/spaces/TestOKHSLa.kt new file mode 100644 index 00000000..65f7914b --- /dev/null +++ b/orx-color/src/commonTest/kotlin/spaces/TestOKHSLa.kt @@ -0,0 +1,43 @@ +package spaces + +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.color.spaces.toOKHSLa +import org.openrndr.extra.color.spaces.toOKHSVa +import kotlin.test.Test +import kotlin.test.assertTrue + +class TestOKHSLa { + @Test + fun testConversions() { + val testColors = listOf(ColorRGBa.RED, ColorRGBa.BLUE, ColorRGBa.GREEN, ColorRGBa.GRAY, ColorRGBa.YELLOW) + val error = (-1E-5 .. 1E-5) + testColors.forEach { + val testColor = it + val toColor = it.toOKHSLa() + val restoreColor = toColor.toRGBa() + assertTrue("color $testColor, $toColor, $restoreColor") { + testColor.r - restoreColor.r in error && testColor.g - restoreColor.g in error && testColor.b - restoreColor.b in error + } + } + } + + @Test + fun testSaturationPersistence() { + val black = ColorRGBa.BLACK.toOKHSLa() + + assertTrue("resulting OKHSLa $black contains no NaNs") { + black.h == black.h && black.s == black.s && black.l == black.l + } + + val rgbBlack = black.toRGBa() + val white = ColorRGBa.WHITE.toOKHSLa() + val rgbWhite = white.toRGBa() + val epsilon = 1E-6 + assertTrue("resulting color $rgbWhite is white") { + rgbWhite.r in (1.0 - epsilon .. 1.0 + epsilon) && rgbWhite.g in (1.0 - epsilon .. 1.0 + epsilon) && rgbWhite.b in (1.0 - epsilon .. 1.0 + epsilon) + } + assertTrue("resulting color $rgbBlack is black") { + rgbBlack.r in (0.0 - epsilon .. 0.0 + epsilon) && rgbBlack.g in (0.0 - epsilon .. 0.0 + epsilon) && rgbBlack.b in (0.0 - epsilon .. 0.0 + epsilon) + } + } +} \ No newline at end of file diff --git a/orx-color/src/commonTest/kotlin/spaces/TestOKHSVa.kt b/orx-color/src/commonTest/kotlin/spaces/TestOKHSVa.kt new file mode 100644 index 00000000..e9244348 --- /dev/null +++ b/orx-color/src/commonTest/kotlin/spaces/TestOKHSVa.kt @@ -0,0 +1,49 @@ +package spaces + +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.color.spaces.toOKHSLa +import org.openrndr.extra.color.spaces.toOKHSVa +import kotlin.test.Test +import kotlin.test.assertTrue + +class TestOKHSVa { + @Test + fun testConversions() { + val testColors = listOf(ColorRGBa.RED, ColorRGBa.BLUE, ColorRGBa.GREEN, ColorRGBa.GRAY, ColorRGBa.YELLOW) + val error = (-1E-5 .. 1E-5) + testColors.forEach { + val testColor = it + val toColor = it.toOKHSVa() + val restoreColor = toColor.toRGBa() + assertTrue("color $testColor, $toColor, $restoreColor") { + testColor.r - restoreColor.r in error && testColor.g - restoreColor.g in error && testColor.b - restoreColor.b in error + } + } + } + + @Test + fun testSaturationPersistence() { + val black = ColorRGBa.BLACK.toOKHSVa() + val rgbBlack = black.toRGBa() + + assertTrue("resulting OKHSVa $black contains no NaNs") { + black.h == black.h && black.s == black.s && black.v == black.v + } + + val white = ColorRGBa.WHITE.toOKHSVa() + val rgbWhite = white.toRGBa() + + assertTrue("resulting OKHSVa $white contains no NaNs") { + white.h == white.h && white.s == white.s && white.v == white.v + } + + val epsilon = 1E-6 + assertTrue("resulting color is white") { + rgbWhite.r in (1.0 - epsilon .. 1.0 + epsilon) && rgbWhite.g in (1.0 - epsilon .. 1.0 + epsilon) && rgbWhite.b in (1.0 - epsilon .. 1.0 + epsilon) + } + assertTrue("resulting color is black") { + rgbBlack.r in (0.0 - epsilon .. 0.0 + epsilon) && rgbBlack.g in (0.0 - epsilon .. 0.0 + epsilon) && rgbBlack.b in (0.0 - epsilon .. 0.0 + epsilon) + } + } + +} \ No newline at end of file