diff --git a/build.gradle b/build.gradle index 6aa4ff60..c45e854b 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ def openrndrUseSnapshot = false apply plugin: 'org.jetbrains.dokka' project.ext { - openrndrVersion = openrndrUseSnapshot? "0.4.0-SNAPSHOT" : "0.3.44-rc.10" + openrndrVersion = openrndrUseSnapshot? "0.4.0-SNAPSHOT" : "0.3.44-rc.11" kotlinVersion = "1.4.0" spekVersion = "2.0.12" libfreenectVersion = "0.5.7-1.5.3" diff --git a/orx-color/README.md b/orx-color/README.md index 6a3e6157..12b2978a 100644 --- a/orx-color/README.md +++ b/orx-color/README.md @@ -15,6 +15,35 @@ val histogram = calculateHistogramRGB(image) val colors = histogram.sortedColors() ``` +## Color sequences + +Easy ways of creating blends between colors. + +Using the `rangeTo` operator: +``` +for (c in ColorRGBa.PINK..ColorRGBa.BLUE.toHSVa() blend 10) { + drawer.fill = c + drawer.rectangle(0.0, 0.0, 40.0, 40.0) + drawer.translate(0.0, 40.0) +} +``` + +Or blends for multiple color stops using `colorSequence`. Blending takes place in the colorspace of the input arguments. +``` +val cs = colorSequence(0.0 to ColorRGBa.PINK, + 0.5 to ColorRGBa.BLUE, + 1.0 to ColorRGBa.PINK.toHSLUVa()) // <-- note this one is in hsluv + +for (c in cs blend (width / 40)) { + drawer.fill = c + drawer.stroke = null + drawer.rectangle(0.0, 0.0, 40.0, height.toDouble()) + drawer.translate(40.0, 0.0) +} +``` + + + ## HSLUVa and HPLUVa colorspaces Two color spaces are added: `ColorHSLUVa` and `ColorHPLUVa`, they are an implementation of the colorspaces presented at [hsluv.org](http://www.hsluv.org) diff --git a/orx-color/src/demo/kotlin/DemoColorRange01.kt b/orx-color/src/demo/kotlin/DemoColorRange01.kt new file mode 100644 index 00000000..70f16d37 --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoColorRange01.kt @@ -0,0 +1,77 @@ +// Create a simple rectangle composition based on colors sampled from image + +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.isolated +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extras.color.palettes.rangeTo +import org.openrndr.extras.color.presets.CORAL +import org.openrndr.extras.color.spaces.toHSLUVa + +fun main() = application { + program { + // -- this block is for automation purposes only + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + extend { + + drawer.isolated { + for (c in ColorRGBa.PINK..ColorRGBa.BLUE.toHSVa() blend 10) { + drawer.fill = c + drawer.rectangle(0.0, 0.0, 40.0, 40.0) + drawer.translate(0.0, 40.0) + } + } + drawer.translate(50.0, 0.0) + drawer.isolated { + for (c in ColorRGBa.PINK..ColorRGBa.BLUE blend 10) { + drawer.fill = c + drawer.rectangle(0.0, 0.0, 40.0, 40.0) + drawer.translate(0.0, 40.0) + } + } + drawer.translate(50.0, 0.0) + + drawer.isolated { + for (c in ColorRGBa.PINK..ColorRGBa.BLUE.toHSLUVa() blend 10) { + drawer.fill = c.toSRGB() + drawer.rectangle(0.0, 0.0, 40.0, 40.0) + drawer.translate(0.0, 40.0) + } + } + + drawer.translate(50.0, 0.0) + + drawer.isolated { + for (c in ColorRGBa.PINK..ColorRGBa.BLUE.toXSVa() blend 10) { + drawer.fill = c.toSRGB() + drawer.rectangle(0.0, 0.0, 40.0, 40.0) + drawer.translate(0.0, 40.0) + } + } + + drawer.translate(50.0, 0.0) + + drawer.isolated { + for (c in ColorRGBa.PINK..ColorRGBa.BLUE.toLUVa() blend 10) { + drawer.fill = c.toSRGB() + drawer.rectangle(0.0, 0.0, 40.0, 40.0) + drawer.translate(0.0, 40.0) + } + } + + drawer.translate(50.0, 0.0) + + drawer.isolated { + for (c in ColorRGBa.PINK..ColorRGBa.BLUE.toLCHUVa() blend 10) { + drawer.fill = c.toSRGB() + drawer.rectangle(0.0, 0.0, 40.0, 40.0) + drawer.translate(0.0, 40.0) + } + } + } + } +} \ No newline at end of file diff --git a/orx-color/src/demo/kotlin/DemoColorRange02.kt b/orx-color/src/demo/kotlin/DemoColorRange02.kt new file mode 100644 index 00000000..a3927318 --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoColorRange02.kt @@ -0,0 +1,33 @@ +// Create a colorSequence with multiple color models + +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.isolated +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extras.color.palettes.colorSequence +import org.openrndr.extras.color.palettes.rangeTo +import org.openrndr.extras.color.presets.CORAL +import org.openrndr.extras.color.spaces.toHSLUVa + +fun main() = application { + program { + // -- this block is for automation purposes only + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + extend { + val cs = colorSequence(0.0 to ColorRGBa.PINK, + 0.5 to ColorRGBa.BLUE, + 1.0 to ColorRGBa.PINK.toHSLUVa()) // <-- note this one is in hsluv + + for (c in cs blend (width / 40)) { + drawer.fill = c + drawer.stroke = null + drawer.rectangle(0.0, 0.0, 40.0, height.toDouble()) + drawer.translate(40.0, 0.0) + } + } + } +} \ 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 new file mode 100644 index 00000000..3bdef105 --- /dev/null +++ b/orx-color/src/main/kotlin/palettes/Palettes.kt @@ -0,0 +1,77 @@ +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 + + +fun main() { + for (c in ColorRGBa.PINK..ColorRGBa.GRAY blend 10) { + + } + for (c in ColorRGBa.PINK..ColorHSVa(20.0, 0.5, 0.5) blend 10) { + println(c) + } + + colorSequence( + 0.0 to ColorRGBa.PINK, + 0.5 to ColorRGBa.RED, + 1.0 to hsv(360.0, 1.0, 1.0) + ).index(0.8) + +} + +fun colorSequence(vararg offsets: Pair): ColorSequence + where T : ConvertibleToColorRGBa { + return ColorSequence(offsets.sortedBy { it.first }) +} + +class ColorSequence(val colors: List>) { + infix fun blend(steps: Int): List = index(0.0, 1.0, steps) + + fun index(t0: Double, t1: Double, steps: Int) = (0 until steps).map { + val f = (it / (steps+0.0)) + val t = t0 * (1.0 - f) + t1 * f + index(t) + } + + fun index(t: Double): ColorRGBa { + if (colors.size == 1) { + return colors.first().second.toRGBa() + } + if (t < colors[0].first) { + return colors[0].second.toRGBa() + } + if (t >= colors.last().first) { + return colors.last().second.toRGBa() + } + val rightIndex = colors.indexOfLast { it.first <= t } + val leftIndex = (rightIndex + 1).coerceIn(0, colors.size - 1) + + val right = colors[rightIndex] + val left = colors[leftIndex] + + val rt = t - right.first + val dt = left.first - right.first + val nt = rt / dt + + return when (val l = left.second) { + is ColorRGBa -> right.second.toRGBa().mix(l, nt) + is ColorHSVa -> right.second.toRGBa().toHSVa().mix(l, nt).toRGBa() + is ColorHSLa -> right.second.toRGBa().toHSLa().mix(l, nt).toRGBa() + is ColorXSVa -> right.second.toRGBa().toXSVa().mix(l, nt).toRGBa() + is ColorXSLa -> right.second.toRGBa().toXSLa().mix(l, nt).toRGBa() + is ColorLABa -> right.second.toRGBa().toLABa().mix(l, nt).toRGBa() + 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 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}") + }.toSRGB() + } +} + +operator fun ConvertibleToColorRGBa.rangeTo(end: ConvertibleToColorRGBa) = colorSequence(0.0 to this, 1.0 to end) diff --git a/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt b/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt index ada7a737..735a8ba3 100644 --- a/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt +++ b/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt @@ -1,7 +1,7 @@ package org.openrndr.extras.color.spaces -import org.openrndr.color.ColorLCHUVa -import org.openrndr.color.ColorRGBa +import org.openrndr.color.* +import org.openrndr.math.mixAngle import java.util.* import kotlin.math.min import kotlin.math.pow @@ -17,7 +17,7 @@ private val epsilon = 0.0088564516 private fun getBounds(L: Double): List? { val result = ArrayList() - val sub1 = Math.pow(L + 16, 3.0) / 1560896 + val sub1 = (L + 16).pow(3.0) / 1560896 val sub2 = if (sub1 > epsilon) sub1 else L / kappa for (c in 0..2) { val m1 = m[c][0] @@ -77,35 +77,66 @@ fun maxChromaForLH(L: Double, H: Double): Double { return min } -data class ColorHSLUVa(val h: Double, val s: Double, val l: Double) { +data class ColorHSLUVa(val h: Double, val s: Double, val l: Double, val a: Double = 1.0) : + ConvertibleToColorRGBa, + HueShiftableColor, + SaturatableColor, + ShadableColor, + OpacifiableColor, + AlgebraicColor { fun toLCHUVa(): ColorLCHUVa { if (l > 99.9999999) { - ColorLCHUVa(100.0, 0.0, h) + ColorLCHUVa(100.0, 0.0, h, a) } if (l < 0.00000001) { - ColorLCHUVa(0.0, 0.0, h) + ColorLCHUVa(0.0, 0.0, h, a) } val max = maxChromaForLH(l, h) val c: Double = max / 100 * s - return ColorLCHUVa(l, c, h) + return ColorLCHUVa(l, c, h, a) } - fun shiftHue(shiftInDegrees: Double): ColorHSLUVa { - return copy(h = h + (shiftInDegrees)) - } + override fun shiftHue(shiftInDegrees: Double) = copy(h = h + (shiftInDegrees)) - fun shade(factor: Double): ColorHSLUVa = copy(l = l * factor) + override fun shade(factor: Double) = copy(l = l * factor) - fun saturate(factor: Double): ColorHSLUVa = copy(s = s * factor) + override fun saturate(factor: Double) = copy(s = s * factor) - fun toRGBa(): ColorRGBa { + override fun toRGBa(): ColorRGBa { return toLCHUVa().toRGBa() } + + override fun opacify(factor: Double) = copy(a = a * factor) + + override fun minus(other: ColorHSLUVa) = copy(h = h - other.h, s = s - other.s, l = l - other.l, a = a - other.a) + + override fun plus(other: ColorHSLUVa) = copy(h = h + other.h, s = s + other.s, l = l + other.l, a = a + other.a) + + override fun times(factor: Double) = copy(h = h * factor, s = s * factor, l = l * factor, a = a * factor) + + override fun mix(other: ColorHSLUVa, factor: Double) = mix(this, other, factor) + } -data class ColorHPLUVa(val h: Double, val s: Double, val l: Double) { +fun mix(left: ColorHSLUVa, right: ColorHSLUVa, x: Double): ColorHSLUVa { + val sx = x.coerceIn(0.0, 1.0) + return ColorHSLUVa( + mixAngle(left.h, right.h, 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) +} + + +data class ColorHPLUVa(val h: Double, val s: Double, val l: Double, val a: Double = 1.0) : + ConvertibleToColorRGBa, + HueShiftableColor, + SaturatableColor, + ShadableColor, + OpacifiableColor, + AlgebraicColor { fun toLCHUVa(): ColorLCHUVa { if (l > 99.9999999) { return ColorLCHUVa(100.0, 0.0, h) @@ -117,17 +148,39 @@ data class ColorHPLUVa(val h: Double, val s: Double, val l: Double) { val c = max / 100 * s return ColorLCHUVa(l, c, h) } - fun shiftHue(shiftInDegrees: Double): ColorHPLUVa { + + override fun shiftHue(shiftInDegrees: Double): ColorHPLUVa { return copy(h = h + (shiftInDegrees)) } - fun shade(factor: Double): ColorHPLUVa = copy(l = l * factor) + override fun shade(factor: Double): ColorHPLUVa = copy(l = l * factor) - fun saturate(factor: Double): ColorHPLUVa = copy(s = s * factor) + override fun saturate(factor: Double): ColorHPLUVa = copy(s = s * factor) + + override fun toRGBa(): ColorRGBa = toLCHUVa().toRGBa() + + override fun opacify(factor: Double) = copy(a = a * factor) + + override fun minus(other: ColorHPLUVa) = copy(h = h - other.h, s = s - other.s, l = l - other.l, a = a - other.a) + + override fun plus(other: ColorHPLUVa) = copy(h = h + other.h, s = s + other.s, l = l + other.l, a = a + other.a) + + override fun times(factor: Double) = copy(h = h * factor, s = s * factor, l = l * factor, a = a * factor) + + override fun mix(other: ColorHPLUVa, factor: Double) = mix(this, other, factor) - fun toRGBa(): ColorRGBa = toLCHUVa().toRGBa() } +fun mix(left: ColorHPLUVa, right: ColorHPLUVa, x: Double): ColorHPLUVa { + val sx = x.coerceIn(0.0, 1.0) + return ColorHPLUVa( + mixAngle(left.h, right.h, 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) +} + + fun ColorLCHUVa.toHPLUVa(): ColorHPLUVa { if (l > 99.9999999) { return ColorHPLUVa(h, 0.0, 100.0) @@ -150,7 +203,7 @@ fun ColorLCHUVa.toHSLUVa(): ColorHSLUVa { } val max = maxChromaForLH(l, h) val s = c / max * 100.0 - return ColorHSLUVa(h, s, l) + return ColorHSLUVa(h, s, l, alpha) } fun ColorRGBa.toHSLUVa(): ColorHSLUVa = toLCHUVa().toHSLUVa() diff --git a/orx-fx/src/demo/kotlin/DemoLaserBlur01.kt b/orx-fx/src/demo/kotlin/DemoLaserBlur01.kt index b14325e1..3aecefe7 100644 --- a/orx-fx/src/demo/kotlin/DemoLaserBlur01.kt +++ b/orx-fx/src/demo/kotlin/DemoLaserBlur01.kt @@ -39,7 +39,7 @@ fun main() = application { for (x in -1..1) { drawer.stroke = ColorRGBa.RED.toHSVa() .shiftHue(0.0 + simplex(500+x+y,seconds)*5.0) - .scaleValue(0.5 + 0.5 * simplex(300+x+y,seconds*4.0).absoluteValue) + .shade(0.5 + 0.5 * simplex(300+x+y,seconds*4.0).absoluteValue) .toRGBa() val r = simplex(400+x+y, seconds) * 150.0 + 150.0 drawer.circle(width / 2.0 + x * 100.0, height / 2.0 + y * 100.0, r) diff --git a/orx-shade-styles/src/demo/kotlin/DemoAllGradients01.kt b/orx-shade-styles/src/demo/kotlin/DemoAllGradients01.kt index dc76f954..1065a1a5 100644 --- a/orx-shade-styles/src/demo/kotlin/DemoAllGradients01.kt +++ b/orx-shade-styles/src/demo/kotlin/DemoAllGradients01.kt @@ -38,7 +38,7 @@ fun main() { gradients.forEachIndexed { gradientId, gradient -> for (column in 0 until 10) { val color1 = ColorRGBa.PINK.toHSVa().shiftHue(column * 12.0) - .scaleValue(0.5).toRGBa() + .shade(0.5).toRGBa() val w = width.toDouble() / 10.0 val h = height.toDouble() / gradients.size diff --git a/orx-shade-styles/src/demo/kotlin/DemoRadialGradient01.kt b/orx-shade-styles/src/demo/kotlin/DemoRadialGradient01.kt index 93378268..7aafcfa8 100644 --- a/orx-shade-styles/src/demo/kotlin/DemoRadialGradient01.kt +++ b/orx-shade-styles/src/demo/kotlin/DemoRadialGradient01.kt @@ -16,7 +16,7 @@ fun main() { extend { drawer.shadeStyle = radialGradient( ColorRGBa.PINK, - ColorRGBa.PINK.toHSVa().shiftHue(180.0).scaleValue(0.5).toRGBa(), + ColorRGBa.PINK.toHSVa().shiftHue(180.0).shade(0.5).toRGBa(), exponent = cos(seconds)*0.5+0.5 ) drawer.rectangle(120.0, 40.0, 400.0, 400.0)