diff --git a/orx-color/src/demo/kotlin/DemoXSLUV01.kt b/orx-color/src/demo/kotlin/DemoXSLUV01.kt new file mode 100644 index 00000000..cdb6c21b --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoXSLUV01.kt @@ -0,0 +1,68 @@ +// Visualize XSLUV color space by drawing a recursively subdivided arcs + +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extras.color.spaces.ColorXSLUVa +import org.openrndr.extras.color.spaces.toHSLUVa +import org.openrndr.math.Polar +import org.openrndr.shape.contour + +fun main() { + class Arc(val start: Double, val radius: Double, val length: Double, val height: Double) { + fun split(offset: Double = 0.0): List { + val hl = length / 2.0 + return listOf(Arc(start, radius + offset, hl, height), Arc(start + hl, radius + offset, hl, height)) + } + + val contour + get() = contour { + moveTo(Polar(start, radius).cartesian) + arcTo(radius, radius, length, false, true, Polar(start + length, radius).cartesian) + lineTo(Polar(start + length, radius + height).cartesian) + arcTo(radius + height, radius + height, length, false, false, Polar(start, radius + height).cartesian) + lineTo(anchor) + close() + } + } + + fun List.split(depth: Int): List = if (depth == 0) { + this + } else { + this + flatMap { it.split(it.height) }.split(depth - 1) + } + + application { + configure { + width = 720 + height = 720 + } + + program { + // -- this block is for automation purposes only + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + + val arcs = (0..4).map { Arc(it * 90.0 - 45.0, 50.0, 90.0, 50.0) }.split(5) + + extend { + drawer.clear(ColorRGBa.GRAY) + val color = ColorRGBa.RED + val hc = color.toHSLUVa() + drawer.stroke = ColorRGBa.BLACK + drawer.strokeWeight = 1.0 + drawer.translate(drawer.bounds.center) + val l = if (System.getProperty("takeScreenshot") == "true") 0.7 else mouse.position.y / height + val s = if (System.getProperty("takeScreenshot") == "true") 1.0 else mouse.position.x / width + for (arc in arcs) { + val xsluv = ColorXSLUVa(arc.start + arc.length / 2.0, s, l, 1.0) + drawer.fill = xsluv.toRGBa() + drawer.contour(arc.contour) + } + } + } + } +} \ No newline at end of file diff --git a/orx-color/src/main/kotlin/palettes/Palettes.kt b/orx-color/src/main/kotlin/palettes/Palettes.kt index 8f5c2439..417e3624 100644 --- a/orx-color/src/main/kotlin/palettes/Palettes.kt +++ b/orx-color/src/main/kotlin/palettes/Palettes.kt @@ -1,11 +1,7 @@ package org.openrndr.extras.color.palettes import org.openrndr.color.* -import org.openrndr.extras.color.spaces.ColorHPLUVa -import org.openrndr.extras.color.spaces.ColorHSLUVa -import org.openrndr.extras.color.spaces.toHPLUVa -import org.openrndr.extras.color.spaces.toHSLUVa - +import org.openrndr.extras.color.spaces.* fun colorSequence(vararg offsets: Pair): ColorSequence @@ -52,6 +48,7 @@ class ColorSequence(val colors: List>) { is ColorLUVa -> right.second.toRGBa().toLUVa().mix(l, nt).toRGBa() is ColorHSLUVa -> right.second.toRGBa().toHSLUVa().mix(l, nt).toRGBa() is ColorHPLUVa -> right.second.toRGBa().toHPLUVa().mix(l, nt).toRGBa() + is ColorXSLUVa -> right.second.toRGBa().toXSLUVa().mix(l, nt).toRGBa() is ColorLCHUVa -> right.second.toRGBa().toLCHUVa().mix(l, nt).toRGBa() is ColorLCHABa -> right.second.toRGBa().toLCHABa().mix(l, nt).toRGBa() else -> error("unsupported color space: ${l::class}") diff --git a/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt b/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt index 36939676..a34cffef 100644 --- a/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt +++ b/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt @@ -105,6 +105,10 @@ data class ColorHSLUVa(val h: Double, val s: Double, val l: Double, val a: Doubl return ColorLCHUVa(l100, c, h, a) } + fun toXSLUVa() : ColorXSLUVa { + return ColorXSLUVa(hueToX(h), s, l, a) + } + override fun shiftHue(shiftInDegrees: Double) = copy(h = h + (shiftInDegrees)) override fun shade(factor: Double) = copy(l = l * factor) @@ -136,6 +140,84 @@ fun mix(left: ColorHSLUVa, right: ColorHSLUVa, x: Double): ColorHSLUVa { (1.0 - sx) * left.a + sx * right.a) } +data class ColorXSLUVa(val x: Double, val s: Double, val l: Double, val a: Double): + ConvertibleToColorRGBa, + HueShiftableColor, + SaturatableColor, + ShadableColor, + OpacifiableColor, + AlgebraicColor { + override fun shiftHue(shiftInDegrees: Double) = copy(x = x + (shiftInDegrees)) + + override fun shade(factor: Double) = copy(l = l * factor) + + override fun saturate(factor: Double) = copy(s = s * factor) + + override fun toRGBa(): ColorRGBa { + return toHSLUVa().toRGBa() + } + + fun toHSLUVa(): ColorHSLUVa = ColorHSLUVa(xToHue(x), s, l, a) + + override fun opacify(factor: Double) = copy(a = a * factor) + + override fun minus(other: ColorXSLUVa) = copy(x = x - other.x, s = s - other.s, l = l - other.l, a = a - other.a) + + override fun plus(other: ColorXSLUVa) = copy(x = x + other.x, s = s + other.s, l = l + other.l, a = a + other.a) + + override fun times(factor: Double) = copy(x = x * factor, s = s * factor, l = l * factor, a = a * factor) + + override fun mix(other: ColorXSLUVa, factor: Double) = mix(this, other, factor) +} + +fun ColorRGBa.toXSLUVa() = toHSLUVa().toXSLUVa() + +fun mix(left: ColorXSLUVa, right: ColorXSLUVa, x: Double): ColorXSLUVa { + val sx = x.coerceIn(0.0, 1.0) + return ColorXSLUVa( + mixAngle(left.x, right.x, sx), + (1.0 - sx) * left.s + sx * right.s, + (1.0 - sx) * left.l + sx * right.l, + (1.0 - sx) * left.a + sx * right.a) +} + +private fun map(x: Double, a: Double, b: Double, c: Double, d: Double): Double { + return ((x - a) / (b - a)) * (d - c) + c +} + +private fun hueToX(hue:Double): Double { + val h = ((hue % 360.0) + 360.0) % 360.0 + return if (0 <= h && h < 35) { + map(h, 0.0, 35.0, 0.0, 60.0) + } else if (35 <= h && h < 60) { + map(h, 35.0, 60.0, 60.0, 120.0) + } else if (60 <= h && h < 135.0) { + map(h, 60.0, 135.0, 120.0, 180.0) + } else if (135.0 <= h && h < 225.0) { + map(h, 135.0, 225.0, 180.0, 240.0) + } else if (225.0 <= h && h < 275.0) { + map(h, 225.0, 275.0, 240.0, 300.0) + } else { + map(h, 276.0, 360.0, 300.0, 360.0) + } +} + +private fun xToHue(x:Double) : Double { + val x = x % 360.0 + return if (0.0 <= x && x < 60.0) { + map(x, 0.0, 60.0, 0.0, 35.0) + } else if (60.0 <= x && x < 120.0) { + map(x, 60.0, 120.0, 35.0, 60.0) + } else if (120.0 <= x && x < 180.0) { + map(x, 120.0, 180.0, 60.0, 135.0) + } else if (180.0 <= x && x < 240.0) { + map(x, 180.0, 240.0, 135.0, 225.0) + } else if (240.0 <= x && x < 300.0) { + map(x, 240.0, 300.0, 225.0, 275.0) + } else { + map(x, 300.0, 360.0, 276.0, 360.0) + } +} data class ColorHPLUVa(val h: Double, val s: Double, val l: Double, val a: Double = 1.0) : ConvertibleToColorRGBa,