[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-camera"))
|
||||||
implementation(project(":orx-mesh-generators"))
|
implementation(project(":orx-mesh-generators"))
|
||||||
implementation(project(":orx-color"))
|
implementation(project(":orx-color"))
|
||||||
|
implementation(project(":orx-jvm:orx-gui"))
|
||||||
|
|
||||||
implementation("org.openrndr:openrndr-application:$openrndrVersion")
|
implementation("org.openrndr:openrndr-application:$openrndrVersion")
|
||||||
implementation("org.openrndr:openrndr-extensions:$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 L = lab.l
|
||||||
val h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / PI;
|
val h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / PI;
|
||||||
|
|
||||||
val Cs = get_Cs(L, a_, b_)
|
val cs = get_Cs(L, a_, b_)
|
||||||
val C_0 = Cs[0];
|
val c0 = cs[0];
|
||||||
val C_mid = Cs[1];
|
val cMid = cs[1];
|
||||||
val C_max = Cs[2];
|
val cMax = cs[2];
|
||||||
|
|
||||||
|
|
||||||
val s = if (C < C_mid) {
|
val s = if (C < cMid) {
|
||||||
val k_0 = 0;
|
val k0 = 0;
|
||||||
val k_1 = 0.8 * C_0;
|
val k1 = 0.8 * c0;
|
||||||
val k_2 = (1 - k_1 / C_mid);
|
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;
|
t * 0.8;
|
||||||
} else {
|
} else {
|
||||||
val k_0 = C_mid;
|
val k0 = cMid;
|
||||||
val k_1 = 0.2 * C_mid * C_mid * 1.25 * 1.25 / C_0;
|
val k1 = 0.2 * cMid * cMid * 1.25 * 1.25 / c0;
|
||||||
val k_2 = (1 - (k_1) / (C_max - C_mid));
|
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;
|
0.8 + 0.2 * t;
|
||||||
}
|
}
|
||||||
val l = toe(L);
|
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) {
|
} else if (l == 0.0) {
|
||||||
ColorRGBa(0.0, 0.0, 0.0, a)
|
ColorRGBa(0.0, 0.0, 0.0, a)
|
||||||
}
|
}
|
||||||
val a_ = cos(2 * PI * h);
|
val a_ = cos(2 * PI * h / 360.0);
|
||||||
val b_ = sin(2 * PI * h);
|
val b_ = sin(2 * PI * h / 360.0);
|
||||||
val L = toe_inv(l);
|
val L = toeInv(l);
|
||||||
|
|
||||||
val Cs = get_Cs(L, a_, b_);
|
val Cs = get_Cs(L, a_, b_);
|
||||||
val C_0 = Cs[0];
|
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[1]),
|
||||||
// 255*srgb_transfer_function(rgb[2]),
|
// 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 {
|
override fun shiftHue(shiftInDegrees: Double): ColorOKHSLa {
|
||||||
val normalizedShift = shiftInDegrees / 360.0
|
return copy(h = h + shiftInDegrees)
|
||||||
return copy(h = h + normalizedShift)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun opacify(factor: Double): ColorOKHSLa {
|
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 {
|
override fun mix(other: ColorOKHSLa, factor: Double): ColorOKHSLa {
|
||||||
val sx = factor.coerceIn(0.0, 1.0)
|
val sx = factor.coerceIn(0.0, 1.0)
|
||||||
return ColorOKHSLa(
|
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) * s + sx * other.s,
|
||||||
(1.0 - sx) * l + sx * other.l,
|
(1.0 - sx) * l + sx * other.l,
|
||||||
(1.0 - sx) * a + sx * other.a
|
(1.0 - sx) * a + sx * other.a
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import org.openrndr.color.*
|
|||||||
import org.openrndr.math.mixAngle
|
import org.openrndr.math.mixAngle
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
|
|
||||||
data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Double = 1.0) :
|
data class ColorOKHSVa(val h: Double, val s: Double, val v: Double, val a: Double = 1.0) :
|
||||||
HueShiftableColor<ColorOKHSVa>,
|
HueShiftableColor<ColorOKHSVa>,
|
||||||
OpacifiableColor<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 {
|
fun fromColorRGBa(c: ColorRGBa): ColorOKHSVa {
|
||||||
val lab = c.toOKLABa()
|
val lab = c.toOKLABa()
|
||||||
var C = sqrt(lab.a * lab.a + lab.b * lab.b);
|
var C = sqrt(lab.a * lab.a + lab.b * lab.b);
|
||||||
val a_ = lab.a / C;
|
val a_ = if (C != 0.0) lab.a / C else 0.0
|
||||||
val b_ = lab.b / C;
|
val b_ = if (C != 0.0) lab.b / C else 0.0
|
||||||
|
|
||||||
var L = lab.l
|
var L = lab.l
|
||||||
val h = 0.5 + 0.5 * atan2(-lab.b, -lab.a) / PI;
|
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_max = ST_max[0];
|
||||||
val S_0 = 0.5;
|
val S_0 = 0.5;
|
||||||
val T = ST_max[1];
|
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 t = T / (C + L * T);
|
||||||
val L_v = t * L;
|
val L_v = t * L;
|
||||||
val C_v = t * C;
|
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 C_vt = C_v * L_vt / L_v;
|
||||||
|
|
||||||
val rgb_scale = ColorOKLABa(L_vt, a_ * C_vt, b_ * C_vt, c.a).toRGBa().toLinear()
|
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 v = L / L_v;
|
||||||
val s = (S_0 + T) * C_v / ((T * S_0) + T * k * C_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 {
|
override fun toRGBa(): ColorRGBa {
|
||||||
val a_ = cos(2 * PI * h)
|
val a_ = cos(2 * PI * h / 360.0)
|
||||||
val b_ = sin(2 * PI * h)
|
val b_ = sin(2 * PI * h / 360.0)
|
||||||
|
|
||||||
val ST_max = get_ST_max(a_, b_)
|
val ST_max = get_ST_max(a_, b_)
|
||||||
val S_max = ST_max[0];
|
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));
|
//L = v*(1 - s*S_max/(S_max+T));
|
||||||
//C = v*s*S_max*T/(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 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;
|
C = C * L_new / L;
|
||||||
L = L_new;
|
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;
|
L *= scale_L;
|
||||||
C *= 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 {
|
override fun shiftHue(shiftInDegrees: Double): ColorOKHSVa {
|
||||||
val normalizedShift = shiftInDegrees / 360.0
|
return copy(h = h + shiftInDegrees)
|
||||||
return copy(h = h + normalizedShift)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun opacify(factor: Double): ColorOKHSVa {
|
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 {
|
override fun mix(other: ColorOKHSVa, factor: Double): ColorOKHSVa {
|
||||||
val sx = factor.coerceIn(0.0, 1.0)
|
val sx = factor.coerceIn(0.0, 1.0)
|
||||||
return ColorOKHSVa(
|
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) * s + sx * other.s,
|
||||||
(1.0 - sx) * v + sx * other.v,
|
(1.0 - sx) * v + sx * other.v,
|
||||||
(1.0 - sx) * a + sx * other.a
|
(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))
|
return max(max(a, b), max(c, d))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun toe(x: Double): Double {
|
fun toe(x: Double): Double {
|
||||||
val k_1 = 0.206
|
val k1 = 0.206
|
||||||
val k_2 = 0.03
|
val k2 = 0.03
|
||||||
val k_3 = (1 + k_1) / (1 + k_2)
|
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 {
|
fun toeInv(x: Double): Double {
|
||||||
val k_1 = 0.206
|
val k1 = 0.206
|
||||||
val k_2 = 0.03
|
val k2 = 0.03
|
||||||
val k_3 = (1 + k_1) / (1 + k_2)
|
val k3 = (1 + k1) / (1 + k2)
|
||||||
return (x * x + k_1 * x) / (k_3 * (x + k_2))
|
return (x * x + k1 * x) / (k3 * (x + k2))
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun compute_max_saturation(a: Double, b: Double): Double {
|
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