[orx-color] Add ColorHSLUVa and ColorHPLUVa
This commit is contained in:
@@ -14,6 +14,10 @@ orx-color comes with tools to calculate color histograms for images.
|
|||||||
val histogram = calculateHistogramRGB(image)
|
val histogram = calculateHistogramRGB(image)
|
||||||
val colors = histogram.sortedColors()
|
val colors = histogram.sortedColors()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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)
|
||||||
<!-- __demos__ -->
|
<!-- __demos__ -->
|
||||||
## Demos
|
## Demos
|
||||||
### DemoHistogram01
|
### DemoHistogram01
|
||||||
|
|||||||
30
orx-color/src/demo/kotlin/DemoHSLUV01.kt
Normal file
30
orx-color/src/demo/kotlin/DemoHSLUV01.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Draw rectangles shaded in RGB and HSLUV space
|
||||||
|
|
||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extensions.SingleScreenshot
|
||||||
|
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 color = ColorRGBa.PINK
|
||||||
|
drawer.stroke = null
|
||||||
|
for (i in 0 until 10) {
|
||||||
|
drawer.fill = color.shade(1.0 - i / 10.0)
|
||||||
|
drawer.rectangle(100.0, 100.0 + i * 20.0, 100.0, 20.0)
|
||||||
|
|
||||||
|
drawer.fill = color.toHSLUVa().shade(1.0 - i / 10.0).toRGBa().toSRGB()
|
||||||
|
drawer.rectangle(200.0, 100.0 + i * 20.0, 100.0, 20.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
orx-color/src/demo/kotlin/DemoHSLUV02.kt
Normal file
37
orx-color/src/demo/kotlin/DemoHSLUV02.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Visualize HSLUV color space
|
||||||
|
|
||||||
|
import org.openrndr.application
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import org.openrndr.extensions.SingleScreenshot
|
||||||
|
import org.openrndr.extras.color.spaces.toHSLUVa
|
||||||
|
import org.openrndr.math.Polar
|
||||||
|
import org.openrndr.math.Vector2
|
||||||
|
|
||||||
|
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 color = ColorRGBa.RED
|
||||||
|
val hc = color.toHSLUVa()
|
||||||
|
drawer.stroke = null
|
||||||
|
drawer.strokeWeight = 0.0
|
||||||
|
for (h in 0 until 360 step 10) {
|
||||||
|
for (s in 0 until 10) {
|
||||||
|
for (l in 9 downTo 0) {
|
||||||
|
val position = Polar(h.toDouble(), s * 25.0).cartesian + Vector2(width/ 2.0, height / 2.0)
|
||||||
|
drawer.fill = hc.shiftHue(h.toDouble()).saturate(s/9.0).shade((9-l)/4.5).toRGBa().toSRGB()
|
||||||
|
drawer.circle(position, kotlin.math.sqrt(s/10.0)*25.0 * l/9.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt
Normal file
157
orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package org.openrndr.extras.color.spaces
|
||||||
|
|
||||||
|
import org.openrndr.color.ColorLCHUVa
|
||||||
|
import org.openrndr.color.ColorRGBa
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
private val m = arrayOf(
|
||||||
|
doubleArrayOf(3.240969941904521, -1.537383177570093, -0.498610760293),
|
||||||
|
doubleArrayOf(-0.96924363628087, 1.87596750150772, 0.041555057407175),
|
||||||
|
doubleArrayOf(0.055630079696993, -0.20397695888897, 1.056971514242878))
|
||||||
|
|
||||||
|
private val kappa = 903.2962962
|
||||||
|
private val epsilon = 0.0088564516
|
||||||
|
|
||||||
|
private fun getBounds(L: Double): List<DoubleArray>? {
|
||||||
|
val result = ArrayList<DoubleArray>()
|
||||||
|
val sub1 = Math.pow(L + 16, 3.0) / 1560896
|
||||||
|
val sub2 = if (sub1 > epsilon) sub1 else L / kappa
|
||||||
|
for (c in 0..2) {
|
||||||
|
val m1 = m[c][0]
|
||||||
|
val m2 = m[c][1]
|
||||||
|
val m3 = m[c][2]
|
||||||
|
for (t in 0..1) {
|
||||||
|
val top1 = (284517 * m1 - 94839 * m3) * sub2
|
||||||
|
val top2 = (838422 * m3 + 769860 * m2 + 731718 * m1) * L * sub2 - 769860 * t * L
|
||||||
|
val bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t
|
||||||
|
result.add(doubleArrayOf(top1 / bottom, top2 / bottom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun intersectLineLine(lineA: DoubleArray, lineB: DoubleArray): Double {
|
||||||
|
return (lineA[1] - lineB[1]) / (lineB[0] - lineA[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun distanceFromPole(point: DoubleArray): Double {
|
||||||
|
return sqrt(point[0].pow(2.0) + point[1].pow(2.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lengthOfRayUntilIntersect(theta: Double, line: DoubleArray): Length {
|
||||||
|
val length = line[1] / (Math.sin(theta) - line[0] * Math.cos(theta))
|
||||||
|
return Length(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Length(val length: Double) {
|
||||||
|
val greaterEqualZero: Boolean = length >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maxSafeChromaForL(L: Double): Double {
|
||||||
|
val bounds = getBounds(L)
|
||||||
|
var min = Double.MAX_VALUE
|
||||||
|
for (i in 0..1) {
|
||||||
|
val m1 = bounds!![i][0]
|
||||||
|
val b1 = bounds[i][1]
|
||||||
|
val line = doubleArrayOf(m1, b1)
|
||||||
|
val x = intersectLineLine(line, doubleArrayOf(-1 / m1, 0.0))
|
||||||
|
val length = distanceFromPole(doubleArrayOf(x, b1 + x * m1))
|
||||||
|
min = min(min, length)
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
fun maxChromaForLH(L: Double, H: Double): Double {
|
||||||
|
val hrad = H / 360 * Math.PI * 2
|
||||||
|
val bounds = getBounds(L)
|
||||||
|
var min = Double.MAX_VALUE
|
||||||
|
for (bound in bounds!!) {
|
||||||
|
val length: Length = lengthOfRayUntilIntersect(hrad, bound)
|
||||||
|
if (length.greaterEqualZero) {
|
||||||
|
min = min(min, length.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ColorHSLUVa(val h: Double, val s: Double, val l: Double) {
|
||||||
|
fun toLCHUVa(): ColorLCHUVa {
|
||||||
|
if (l > 99.9999999) {
|
||||||
|
ColorLCHUVa(100.0, 0.0, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (l < 0.00000001) {
|
||||||
|
ColorLCHUVa(0.0, 0.0, h)
|
||||||
|
}
|
||||||
|
val max = maxChromaForLH(l, h)
|
||||||
|
val c: Double = max / 100 * s
|
||||||
|
|
||||||
|
return ColorLCHUVa(l, c, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shiftHue(shiftInDegrees: Double): ColorHSLUVa {
|
||||||
|
return copy(h = h + (shiftInDegrees))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shade(factor: Double): ColorHSLUVa = copy(l = l * factor)
|
||||||
|
|
||||||
|
fun saturate(factor: Double): ColorHSLUVa = copy(s = s * factor)
|
||||||
|
|
||||||
|
fun toRGBa(): ColorRGBa {
|
||||||
|
return toLCHUVa().toRGBa()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ColorHPLUVa(val h: Double, val s: Double, val l: Double) {
|
||||||
|
fun toLCHUVa(): ColorLCHUVa {
|
||||||
|
if (l > 99.9999999) {
|
||||||
|
return ColorLCHUVa(100.0, 0.0, h)
|
||||||
|
}
|
||||||
|
if (l < 0.00000001) {
|
||||||
|
return ColorLCHUVa(0.0, 0.0, h)
|
||||||
|
}
|
||||||
|
val max = maxSafeChromaForL(l)
|
||||||
|
val c = max / 100 * s
|
||||||
|
return ColorLCHUVa(l, c, h)
|
||||||
|
}
|
||||||
|
fun shiftHue(shiftInDegrees: Double): ColorHPLUVa {
|
||||||
|
return copy(h = h + (shiftInDegrees))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shade(factor: Double): ColorHPLUVa = copy(l = l * factor)
|
||||||
|
|
||||||
|
fun saturate(factor: Double): ColorHPLUVa = copy(s = s * factor)
|
||||||
|
|
||||||
|
fun toRGBa(): ColorRGBa = toLCHUVa().toRGBa()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ColorLCHUVa.toHPLUVa(): ColorHPLUVa {
|
||||||
|
if (l > 99.9999999) {
|
||||||
|
return ColorHPLUVa(h, 0.0, 100.0)
|
||||||
|
}
|
||||||
|
if (l < 0.00000001) {
|
||||||
|
return ColorHPLUVa(h, 0.0, 0.0)
|
||||||
|
|
||||||
|
}
|
||||||
|
val max = maxSafeChromaForL(l)
|
||||||
|
val s = c / max * 100
|
||||||
|
return ColorHPLUVa(h, s, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ColorLCHUVa.toHSLUVa(): ColorHSLUVa {
|
||||||
|
if (l > 99.99999) {
|
||||||
|
return ColorHSLUVa(h, 0.0, 100.0)
|
||||||
|
}
|
||||||
|
if (l < 0.000001) {
|
||||||
|
return ColorHSLUVa(h, 0.0, 0.0)
|
||||||
|
}
|
||||||
|
val max = maxChromaForLH(l, h)
|
||||||
|
val s = c / max * 100.0
|
||||||
|
return ColorHSLUVa(h, s, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ColorRGBa.toHSLUVa(): ColorHSLUVa = toLCHUVa().toHSLUVa()
|
||||||
|
fun ColorRGBa.toHPLUVa(): ColorHPLUVa = toLCHUVa().toHPLUVa()
|
||||||
Reference in New Issue
Block a user