[orx-color] Fix NaN bugs in ColorOKHSLa, ColorOKHSVa
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ColorOKHSVa>,
|
||||
OpacifiableColor<ColorOKHSVa>,
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
43
orx-color/src/commonTest/kotlin/spaces/TestOKHSLa.kt
Normal file
43
orx-color/src/commonTest/kotlin/spaces/TestOKHSLa.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
49
orx-color/src/commonTest/kotlin/spaces/TestOKHSVa.kt
Normal file
49
orx-color/src/commonTest/kotlin/spaces/TestOKHSVa.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user