diff --git a/orx-color/src/commonMain/kotlin/spaces/ColorOKLABa.kt b/orx-color/src/commonMain/kotlin/spaces/ColorOKLABa.kt index 225bf8c0..ff020fbb 100644 --- a/orx-color/src/commonMain/kotlin/spaces/ColorOKLABa.kt +++ b/orx-color/src/commonMain/kotlin/spaces/ColorOKLABa.kt @@ -3,7 +3,9 @@ package org.openrndr.extra.color.spaces import kotlinx.serialization.Serializable import org.openrndr.color.* import org.openrndr.math.Vector4 +import kotlin.math.abs import kotlin.math.pow +import kotlin.math.sign /** * Color in OKLab color space. @@ -27,9 +29,10 @@ data class ColorOKLABa(val l: Double, val a: Double, val b: Double, override val val m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b val s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b - val lnl = l.pow(1.0 / 3.0) - val mnl = m.pow(1.0 / 3.0) - val snl = s.pow(1.0 / 3.0) + val lnl = abs(l).pow(1.0 / 3.0) * sign(l) + val mnl = abs(m).pow(1.0 / 3.0) * sign(m) + val snl = abs(s).pow(1.0 / 3.0) * sign(s) + val L = 0.2104542553 * lnl + 0.7936177850 * mnl - 0.0040720468 * snl val a = 1.9779984951 * lnl - 2.4285922050 * mnl + 0.4505937099 * snl @@ -68,9 +71,9 @@ data class ColorOKLABa(val l: Double, val a: Double, val b: Double, override val override fun toVector4() = Vector4(l, a, b, alpha) override val luminosity: Double - get() = l + get() = l * 100.0 - override fun withLuminosity(luminosity: Double): ColorOKLABa = copy(l = luminosity) + override fun withLuminosity(luminosity: Double): ColorOKLABa = copy(l = luminosity / 100.0) } fun ColorRGBa.toOKLABa() = ColorOKLABa.fromRGBa(this) \ No newline at end of file diff --git a/orx-color/src/commonMain/kotlin/spaces/ColorOKLCHa.kt b/orx-color/src/commonMain/kotlin/spaces/ColorOKLCHa.kt index 8d47541d..2eea1614 100644 --- a/orx-color/src/commonMain/kotlin/spaces/ColorOKLCHa.kt +++ b/orx-color/src/commonMain/kotlin/spaces/ColorOKLCHa.kt @@ -51,16 +51,16 @@ data class ColorOKLCHa(val l: Double, val c: Double, val h: Double, override val override fun toRGBa(): ColorRGBa = toOKLABa().toRGBa() override fun toVector4(): Vector4 = Vector4(l, c, h, alpha) override val chroma: Double - get() = c - override fun withChroma(chroma: Double): ColorOKLCHa = copy(c = chroma) + get() = c * 100.0 + override fun withChroma(chroma: Double): ColorOKLCHa = copy(c = chroma / 100.0) override val hue: Double get() = h override fun withHue(hue: Double): ColorOKLCHa = copy(h = hue) override val luminosity: Double - get() = l + get() = l * 100.0 - override fun withLuminosity(luminosity: Double): ColorOKLCHa = copy(l = luminosity) + override fun withLuminosity(luminosity: Double): ColorOKLCHa = copy(l = luminosity / 100.0) } fun mix(left: ColorOKLCHa, right: ColorOKLCHa, x: Double): ColorOKLCHa { diff --git a/orx-color/src/commonMain/kotlin/tools/ChromaColorExtensions.kt b/orx-color/src/commonMain/kotlin/tools/ChromaColorExtensions.kt new file mode 100644 index 00000000..ea344b8e --- /dev/null +++ b/orx-color/src/commonMain/kotlin/tools/ChromaColorExtensions.kt @@ -0,0 +1,47 @@ +package org.openrndr.extra.color.tools + +import org.openrndr.color.ChromaColor +import org.openrndr.color.ConvertibleToColorRGBa + + +private fun binarySearchMax(min: Double, max: Double, start: Double, threshold: Double = 1E-5, f: (Double) -> Boolean): Double { + var low = min + var high = max + + var best = min + var mid = start + while (low <= high) { + + val res = f(mid) + + if (res) { + best = mid + low = mid + } else { + high = mid + } + + if (high - low < threshold) { + return best + } + mid = (low + high) / 2.0 + } + return best +} + +fun T.findMaxChroma(): Double + where T : ChromaColor, + T : ConvertibleToColorRGBa { + return binarySearchMax(0.0, 200.0, chroma, 1E-5) { + val c = withChroma(it).toRGBa() + !c.isOutOfGamut + } +} + +fun T.clipChroma(): T + where T : ChromaColor, + T : ConvertibleToColorRGBa { + + val maxChroma = findMaxChroma() + return withChroma(maxChroma) +} \ No newline at end of file diff --git a/orx-color/src/commonMain/kotlin/tools/ColorRGBaExtensions.kt b/orx-color/src/commonMain/kotlin/tools/ColorRGBaExtensions.kt index 31c7184b..32bae9d2 100644 --- a/orx-color/src/commonMain/kotlin/tools/ColorRGBaExtensions.kt +++ b/orx-color/src/commonMain/kotlin/tools/ColorRGBaExtensions.kt @@ -3,6 +3,10 @@ package org.openrndr.extra.color.tools import org.openrndr.color.* import org.openrndr.extra.color.spaces.* +val ColorRGBa.isOutOfGamut: Boolean + get() { + return (r !in -1E-3..1.0) || (g !in -1E-3..1.0) || (b !in -1E-3..1.0) || (alpha !in 0.0..1.0) + } fun ColorRGBa.matchLinearity(other: ColorRGBa): ColorRGBa { return if (other.linearity.isEquivalent(linearity)) { this @@ -100,6 +104,31 @@ inline fun ColorRGBa.mixedWith(other: ColorRGBa, factor: Double): Co return source.mix(target, factor).toRGBa().matchLinearity(this) } +inline fun ColorRGBa.mixChroma(chroma: Double, factor: Double): ColorRGBa + where T : ChromaColor, + T : ColorModel, + T : ConvertibleToColorRGBa = + convertTo().mixChroma(chroma, factor).toRGBa().matchLinearity(this) + +inline fun ColorRGBa.withChroma(chroma: Double): ColorRGBa + where T : ChromaColor, + T : ColorModel, + T : ConvertibleToColorRGBa = + convertTo().withChroma(chroma).toRGBa().matchLinearity(this) + +inline fun ColorRGBa.chroma(): Double + where T : ChromaColor, + T : ColorModel, + T : ConvertibleToColorRGBa = + convertTo().chroma + +inline fun ColorRGBa.modulateChroma(factor: Double): ColorRGBa + where T : ChromaColor, + T : ColorModel, + T : ConvertibleToColorRGBa = + convertTo().modulateChroma(factor).toRGBa().matchLinearity(this) + + inline fun ColorRGBa.saturate(factor: Double): ColorRGBa where T : SaturatableColor, T : ColorModel, @@ -110,3 +139,13 @@ inline fun ColorRGBa.shiftHue(degrees: Double): ColorRGBa where T : ColorModel, T : ConvertibleToColorRGBa = convertTo().shiftHue(degrees).toRGBa().matchLinearity(this) +inline fun ColorRGBa.clipChroma(): ColorRGBa + where T : ChromaColor, + T : ColorModel, + T : ConvertibleToColorRGBa = + + if (isOutOfGamut) { + convertTo().clipChroma().toRGBa().matchLinearity(this).clip() + } else { + this + } diff --git a/orx-color/src/commonTest/kotlin/spaces/TestOKLCHa.kt b/orx-color/src/commonTest/kotlin/spaces/TestOKLCHa.kt new file mode 100644 index 00000000..6fd6bb34 --- /dev/null +++ b/orx-color/src/commonTest/kotlin/spaces/TestOKLCHa.kt @@ -0,0 +1,26 @@ +package spaces + +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.color.spaces.OKLCH +import org.openrndr.extra.color.spaces.toOKLCHa +import org.openrndr.extra.color.tools.chroma +import org.openrndr.extra.color.tools.withLuminosity +import kotlin.test.Test + +class TestOKLCHa { + @Test + fun testLowLuminosity() { + val c = ColorRGBa.GREEN.withLuminosity(10.0) + println(c) + println(c.chroma()) + + + val g = ColorRGBa.GREEN.toOKLCHa() + val gl10 = g.withLuminosity(10.0) + val rgb10 = gl10.toRGBa() + println(gl10) + println(rgb10) + println(rgb10.toOKLCHa()) + + } +} \ No newline at end of file diff --git a/orx-color/src/commonTest/kotlin/tools/TestChromaColorExtensions.kt b/orx-color/src/commonTest/kotlin/tools/TestChromaColorExtensions.kt new file mode 100644 index 00000000..9ac07291 --- /dev/null +++ b/orx-color/src/commonTest/kotlin/tools/TestChromaColorExtensions.kt @@ -0,0 +1,31 @@ +package tools + +import org.openrndr.color.ColorRGBa +import org.openrndr.extra.color.spaces.toOKLCHa +import org.openrndr.extra.color.tools.findMaxChroma +import org.openrndr.extra.color.tools.isOutOfGamut +import kotlin.test.Test + +class TestChromaColorExtensions { + @Test + fun testFindMaxChroma() { + + run { + val i = ColorRGBa.BLUE.toLCHABa()//.withLuminosity(50.0) + val maxChroma = i.findMaxChroma() + println(i.chroma) + println(maxChroma) + } + run { + val i = ColorRGBa.BLUE.toOKLCHa()//.withLuminosity(50.0) + val maxChroma = i.findMaxChroma() + println(ColorRGBa.BLUE.isOutOfGamut) + println(i.chroma) + println(maxChroma) + println(i.toRGBa()) + } + + } + + +} \ No newline at end of file