[orx-color] Add OKLab color space
This commit is contained in:
95
orx-color/src/demo/kotlin/DemoColorPlane01.kt
Normal file
95
orx-color/src/demo/kotlin/DemoColorPlane01.kt
Normal file
@@ -0,0 +1,95 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.*
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extras.camera.Orbital
|
||||
import org.openrndr.extras.meshgenerators.sphereMesh
|
||||
import org.openrndr.math.Vector3
|
||||
import spaces.ColorOKLCHa
|
||||
import kotlin.math.cos
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 800
|
||||
height = 800
|
||||
|
||||
}
|
||||
program {
|
||||
// -- this block is for automation purposes only
|
||||
if (System.getProperty("takeScreenshot") == "true") {
|
||||
extend(SingleScreenshot()) {
|
||||
this.outputFile = System.getProperty("screenshotPath")
|
||||
}
|
||||
}
|
||||
|
||||
val mesh = sphereMesh(8, 8, radius = 0.1)
|
||||
|
||||
val instanceData = vertexBuffer(
|
||||
vertexFormat {
|
||||
attribute("instanceColor", VertexElementType.VECTOR4_FLOAT32)
|
||||
attribute("instancePosition", VertexElementType.VECTOR3_FLOAT32)
|
||||
},
|
||||
90 * 100
|
||||
)
|
||||
|
||||
|
||||
extend(Orbital())
|
||||
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.WHITE)
|
||||
|
||||
drawer.stroke = null
|
||||
|
||||
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 16.0)
|
||||
|
||||
instanceData.put {
|
||||
for (hue in 0 until 360 step 4) {
|
||||
for (chroma in 0 until 100 step 1) {
|
||||
val lch = ColorOKLCHa(cos(seconds * 0.1) * 0.5 + 0.5, chroma / 100.0, hue.toDouble())
|
||||
val srgb = lch.toRGBa().toSRGB().saturated
|
||||
write(srgb)
|
||||
write(Vector3((srgb.r - 0.5) * 10.0, (srgb.g - 0.5) * 10.0, (srgb.b - 0.5) * 10.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
drawer.isolated {
|
||||
drawer.shadeStyle = shadeStyle {
|
||||
|
||||
vertexTransform = """
|
||||
x_position += i_instancePosition;
|
||||
""".trimIndent()
|
||||
fragmentTransform = """
|
||||
x_fill = vi_instanceColor;
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
drawer.vertexBufferInstances(listOf(mesh), listOf(instanceData), DrawPrimitive.TRIANGLES, 90 * 100)
|
||||
}
|
||||
|
||||
|
||||
drawer.stroke = ColorRGBa.BLACK.opacify(0.25)
|
||||
drawer.strokeWeight = 10.0
|
||||
drawer.lineSegments(
|
||||
listOf(
|
||||
Vector3(-5.0, -5.0, -5.0), Vector3(5.0, -5.0, -5.0),
|
||||
Vector3(-5.0, -5.0, 5.0), Vector3(5.0, -5.0, 5.0),
|
||||
Vector3(-5.0, 5.0, -5.0), Vector3(5.0, 5.0, -5.0),
|
||||
Vector3(-5.0, 5.0, 5.0), Vector3(5.0, 5.0, 5.0),
|
||||
|
||||
Vector3(-5.0, -5.0, -5.0), Vector3(-5.0, 5.0, -5.0),
|
||||
Vector3(5.0, -5.0, -5.0), Vector3(5.0, 5.0, -5.0),
|
||||
Vector3(-5.0, -5.0, 5.0), Vector3(-5.0, 5.0, 5.0),
|
||||
Vector3(5.0, -5.0, 5.0), Vector3(5.0, 5.0, 5.0),
|
||||
|
||||
Vector3(-5.0, -5.0, -5.0), Vector3(-5.0, -5.0, 5.0),
|
||||
Vector3(5.0, -5.0, -5.0), Vector3(5.0, -5.0, 5.0),
|
||||
Vector3(-5.0, 5.0, -5.0), Vector3(-5.0, 5.0, 5.0),
|
||||
Vector3(5.0, 5.0, -5.0), Vector3(5.0, 5.0, 5.0),
|
||||
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
orx-color/src/demo/kotlin/DemoColorPlane02.kt
Normal file
86
orx-color/src/demo/kotlin/DemoColorPlane02.kt
Normal file
@@ -0,0 +1,86 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.*
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extras.camera.Orbital
|
||||
import org.openrndr.extras.meshgenerators.sphereMesh
|
||||
import org.openrndr.math.Vector3
|
||||
import spaces.ColorOKLCHa
|
||||
import kotlin.math.cos
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 800
|
||||
height = 800
|
||||
}
|
||||
program {
|
||||
// -- this block is for automation purposes only
|
||||
if (System.getProperty("takeScreenshot") == "true") {
|
||||
extend(SingleScreenshot()) {
|
||||
this.outputFile = System.getProperty("screenshotPath")
|
||||
}
|
||||
}
|
||||
val mesh = sphereMesh(8, 8, radius = 0.1)
|
||||
|
||||
val instanceData = vertexBuffer(
|
||||
vertexFormat {
|
||||
attribute("instanceColor", VertexElementType.VECTOR4_FLOAT32)
|
||||
attribute("instancePosition", VertexElementType.VECTOR3_FLOAT32)
|
||||
},
|
||||
100 * 100
|
||||
)
|
||||
extend(Orbital())
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.WHITE)
|
||||
|
||||
drawer.stroke = null
|
||||
|
||||
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 16.0)
|
||||
|
||||
instanceData.put {
|
||||
for (lumo in 0 until 100 step 1) {
|
||||
for (chroma in 0 until 100 step 1) {
|
||||
val lch = ColorOKLCHa(lumo / 100.0, chroma / 100.0, cos(seconds * 0.1) * 360.0)
|
||||
val srgb = lch.toRGBa().toSRGB().saturated
|
||||
write(srgb)
|
||||
write(Vector3((srgb.r - 0.5) * 10.0, (srgb.g - 0.5) * 10.0, (srgb.b - 0.5) * 10.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
drawer.isolated {
|
||||
drawer.shadeStyle = shadeStyle {
|
||||
vertexTransform = """
|
||||
x_position += i_instancePosition;
|
||||
""".trimIndent()
|
||||
fragmentTransform = """
|
||||
x_fill = vi_instanceColor;
|
||||
""".trimIndent()
|
||||
}
|
||||
drawer.vertexBufferInstances(listOf(mesh), listOf(instanceData), DrawPrimitive.TRIANGLES, 90 * 100)
|
||||
}
|
||||
|
||||
drawer.stroke = ColorRGBa.BLACK.opacify(0.25)
|
||||
drawer.strokeWeight = 10.0
|
||||
drawer.lineSegments(
|
||||
listOf(
|
||||
Vector3(-5.0, -5.0, -5.0), Vector3(5.0, -5.0, -5.0),
|
||||
Vector3(-5.0, -5.0, 5.0), Vector3(5.0, -5.0, 5.0),
|
||||
Vector3(-5.0, 5.0, -5.0), Vector3(5.0, 5.0, -5.0),
|
||||
Vector3(-5.0, 5.0, 5.0), Vector3(5.0, 5.0, 5.0),
|
||||
|
||||
Vector3(-5.0, -5.0, -5.0), Vector3(-5.0, 5.0, -5.0),
|
||||
Vector3(5.0, -5.0, -5.0), Vector3(5.0, 5.0, -5.0),
|
||||
Vector3(-5.0, -5.0, 5.0), Vector3(-5.0, 5.0, 5.0),
|
||||
Vector3(5.0, -5.0, 5.0), Vector3(5.0, 5.0, 5.0),
|
||||
|
||||
Vector3(-5.0, -5.0, -5.0), Vector3(-5.0, -5.0, 5.0),
|
||||
Vector3(5.0, -5.0, -5.0), Vector3(5.0, -5.0, 5.0),
|
||||
Vector3(-5.0, 5.0, -5.0), Vector3(-5.0, 5.0, 5.0),
|
||||
Vector3(5.0, 5.0, -5.0), Vector3(5.0, 5.0, 5.0),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
orx-color/src/demo/kotlin/DemoColorRange03.kt
Normal file
57
orx-color/src/demo/kotlin/DemoColorRange03.kt
Normal file
@@ -0,0 +1,57 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.loadFont
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extras.color.palettes.rangeTo
|
||||
import org.openrndr.extras.color.spaces.toHSLUVa
|
||||
import org.openrndr.extras.color.spaces.toXSLUVa
|
||||
import spaces.toOKLABa
|
||||
import spaces.toOKLCHa
|
||||
|
||||
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.clear(ColorRGBa.WHITE)
|
||||
|
||||
val colorA = ColorRGBa.BLUE
|
||||
val colorB = ColorRGBa.PINK
|
||||
|
||||
val stepCount = 25
|
||||
|
||||
val allSteps = listOf(
|
||||
"RGB" to (colorA..colorB blend stepCount),
|
||||
"RGB linear" to (colorA.toLinear()..colorB.toLinear() blend stepCount),
|
||||
"HSV" to (colorA..colorB.toHSVa() blend stepCount),
|
||||
"Lab" to (colorA.toLABa()..colorB.toLABa() blend stepCount),
|
||||
"LCh(ab)" to (colorA.toLCHABa()..colorB.toLCHABa() blend stepCount),
|
||||
"OKLab" to (colorA.toOKLABa()..colorB.toOKLABa() blend stepCount),
|
||||
"OKLCh" to (colorA.toOKLCHa()..colorB.toOKLCHa() blend stepCount),
|
||||
"HSLUV" to (colorA.toHSLUVa()..colorB.toHSLUVa() blend stepCount),
|
||||
"XSLUV" to (colorA.toXSLUVa()..colorB.toXSLUVa() blend stepCount),
|
||||
)
|
||||
|
||||
drawer.stroke = null
|
||||
|
||||
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 16.0)
|
||||
drawer.translate(20.0, 20.0)
|
||||
for ((label, steps) in allSteps) {
|
||||
drawer.fill = ColorRGBa.GRAY.shade(0.25)
|
||||
drawer.text(label, 0.0, 24.0)
|
||||
|
||||
for (i in steps.indices) {
|
||||
drawer.fill = steps[i].toSRGB()
|
||||
drawer.rectangle(100.0 + i * 20.0, 0.0, 20.0, 40.0)
|
||||
}
|
||||
drawer.translate(0.0, 50.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
orx-color/src/demo/kotlin/DemoColorRange04.kt
Normal file
96
orx-color/src/demo/kotlin/DemoColorRange04.kt
Normal file
@@ -0,0 +1,96 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.DrawPrimitive
|
||||
import org.openrndr.draw.isolated
|
||||
import org.openrndr.draw.loadFont
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extras.camera.Orbital
|
||||
import org.openrndr.extras.color.palettes.rangeTo
|
||||
import org.openrndr.extras.color.spaces.toHSLUVa
|
||||
import org.openrndr.extras.color.spaces.toXSLUVa
|
||||
import org.openrndr.extras.meshgenerators.sphereMesh
|
||||
import org.openrndr.math.Vector3
|
||||
import spaces.toOKLABa
|
||||
import spaces.toOKLCHa
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 800
|
||||
height = 800
|
||||
|
||||
}
|
||||
program {
|
||||
// -- this block is for automation purposes only
|
||||
if (System.getProperty("takeScreenshot") == "true") {
|
||||
extend(SingleScreenshot()) {
|
||||
this.outputFile = System.getProperty("screenshotPath")
|
||||
}
|
||||
}
|
||||
|
||||
val mesh = sphereMesh(8, 8, radius = 0.1)
|
||||
|
||||
extend(Orbital())
|
||||
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.WHITE)
|
||||
|
||||
val colorA = ColorRGBa.BLUE.toHSVa().shiftHue(seconds * 40.0).toRGBa()
|
||||
val colorB = ColorRGBa.PINK.toHSVa().shiftHue(-seconds * 34.0).toRGBa()
|
||||
|
||||
val stepCount = 25
|
||||
|
||||
val allSteps = listOf(
|
||||
"RGB" to (colorA..colorB blend stepCount),
|
||||
"RGB linear" to (colorA.toLinear()..colorB.toLinear() blend stepCount),
|
||||
"HSV" to (colorA..colorB.toHSVa() blend stepCount),
|
||||
"Lab" to (colorA.toLABa()..colorB.toLABa() blend stepCount),
|
||||
"LCh(ab)" to (colorA.toLCHABa()..colorB.toLCHABa() blend stepCount),
|
||||
"OKLab" to (colorA.toOKLABa()..colorB.toOKLABa() blend stepCount),
|
||||
"OKLCh" to (colorA.toOKLCHa()..colorB.toOKLCHa() blend stepCount),
|
||||
"HSLUV" to (colorA.toHSLUVa()..colorB.toHSLUVa() blend stepCount),
|
||||
"XSLUV" to (colorA.toXSLUVa()..colorB.toXSLUVa() blend stepCount),
|
||||
)
|
||||
|
||||
drawer.stroke = null
|
||||
|
||||
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 16.0)
|
||||
for ((_, steps) in allSteps) {
|
||||
for (i in steps.indices) {
|
||||
val srgb = steps[i].toSRGB().saturated
|
||||
drawer.fill = srgb
|
||||
drawer.isolated {
|
||||
drawer.translate((srgb.r - 0.5) * 10.0, (srgb.g - 0.5) * 10.0, (srgb.b - 0.5) * 10.0)
|
||||
drawer.vertexBuffer(mesh, DrawPrimitive.TRIANGLES)
|
||||
}
|
||||
}
|
||||
val positions = steps.map {
|
||||
val l = it.toSRGB().saturated
|
||||
Vector3((l.r - 0.5) * 10.0, (l.g - 0.5) * 10.0, (l.b - 0.5) * 10.0)
|
||||
}
|
||||
drawer.stroke = ColorRGBa.BLACK.opacify(0.25)
|
||||
drawer.strokeWeight = 10.0
|
||||
drawer.lineStrip(positions)
|
||||
}
|
||||
drawer.lineSegments(
|
||||
listOf(
|
||||
Vector3(-5.0, -5.0, -5.0), Vector3(5.0, -5.0, -5.0),
|
||||
Vector3(-5.0, -5.0, 5.0), Vector3(5.0, -5.0, 5.0),
|
||||
Vector3(-5.0, 5.0, -5.0), Vector3(5.0, 5.0, -5.0),
|
||||
Vector3(-5.0, 5.0, 5.0), Vector3(5.0, 5.0, 5.0),
|
||||
|
||||
Vector3(-5.0, -5.0, -5.0), Vector3(-5.0, 5.0, -5.0),
|
||||
Vector3(5.0, -5.0, -5.0), Vector3(5.0, 5.0, -5.0),
|
||||
Vector3(-5.0, -5.0, 5.0), Vector3(-5.0, 5.0, 5.0),
|
||||
Vector3(5.0, -5.0, 5.0), Vector3(5.0, 5.0, 5.0),
|
||||
|
||||
Vector3(-5.0, -5.0, -5.0), Vector3(-5.0, -5.0, 5.0),
|
||||
Vector3(5.0, -5.0, -5.0), Vector3(5.0, -5.0, 5.0),
|
||||
Vector3(-5.0, 5.0, -5.0), Vector3(-5.0, 5.0, 5.0),
|
||||
Vector3(5.0, 5.0, -5.0), Vector3(5.0, 5.0, 5.0),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@ package org.openrndr.extras.color.palettes
|
||||
|
||||
import org.openrndr.color.*
|
||||
import org.openrndr.extras.color.spaces.*
|
||||
import spaces.ColorOKLABa
|
||||
import spaces.ColorOKLCHa
|
||||
import spaces.toOKLABa
|
||||
import spaces.toOKLCHa
|
||||
|
||||
|
||||
fun <T> colorSequence(vararg offsets: Pair<Double, T>): ColorSequence
|
||||
@@ -51,6 +55,8 @@ class ColorSequence(val colors: List<Pair<Double, ConvertibleToColorRGBa>>) {
|
||||
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()
|
||||
is ColorOKLABa -> right.second.toRGBa().toOKLABa().mix(l, nt).toRGBa()
|
||||
is ColorOKLCHa -> right.second.toRGBa().toOKLCHa().mix(l, nt).toRGBa()
|
||||
else -> error("unsupported color space: ${l::class}")
|
||||
}.toSRGB()
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@ fun maxChromaForLH(L100: Double, H: Double): Double {
|
||||
return min
|
||||
}
|
||||
|
||||
/**
|
||||
* HSLUV color space
|
||||
*/
|
||||
data class ColorHSLUVa(val h: Double, val s: Double, val l: Double, val a: Double = 1.0) :
|
||||
ConvertibleToColorRGBa,
|
||||
HueShiftableColor<ColorHSLUVa>,
|
||||
@@ -85,7 +88,6 @@ data class ColorHSLUVa(val h: Double, val s: Double, val l: Double, val a: Doubl
|
||||
OpacifiableColor<ColorHSLUVa>,
|
||||
AlgebraicColor<ColorHSLUVa> {
|
||||
|
||||
|
||||
fun toLCHUVa(): ColorLCHUVa {
|
||||
|
||||
val l100 = l * 100.0
|
||||
|
||||
62
orx-color/src/main/kotlin/spaces/ColorOKLABa.kt
Normal file
62
orx-color/src/main/kotlin/spaces/ColorOKLABa.kt
Normal file
@@ -0,0 +1,62 @@
|
||||
package spaces
|
||||
|
||||
import org.openrndr.color.*
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* Color in OKLab color space
|
||||
*/
|
||||
data class ColorOKLABa(val l: Double, val a: Double, val b: Double, val alpha: Double = 1.0) :
|
||||
ConvertibleToColorRGBa,
|
||||
ShadableColor<ColorOKLABa>,
|
||||
OpacifiableColor<ColorOKLABa>,
|
||||
AlgebraicColor<ColorOKLABa> {
|
||||
|
||||
companion object {
|
||||
fun fromRGBa(rgba: ColorRGBa): ColorOKLABa {
|
||||
// based on https://bottosson.github.io/posts/oklab/
|
||||
val c = rgba.toLinear()
|
||||
val l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929f * c.b
|
||||
val m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566f * c.b
|
||||
val s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005f * 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 L = 0.2104542553f * lnl + 0.7936177850f * mnl - 0.0040720468f * snl
|
||||
val a = 1.9779984951f * lnl - 2.4285922050f * mnl + 0.4505937099f * snl
|
||||
val b = 0.0259040371f * lnl + 0.7827717662f * mnl - 0.8086757660f * snl
|
||||
|
||||
return ColorOKLABa(L, a, b, alpha = c.a)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toRGBa(): ColorRGBa {
|
||||
// based on https://bottosson.github.io/posts/oklab/
|
||||
val lnl = l + 0.3963377774 * a + 0.2158037573 * b
|
||||
val mnl = l - 0.1055613458 * a - 0.0638541728 * b
|
||||
val snl = l - 0.0894841775 * a - 1.2914855480 * b
|
||||
|
||||
val l = lnl * lnl * lnl
|
||||
val m = mnl * mnl * mnl
|
||||
val s = snl * snl * snl
|
||||
|
||||
return ColorRGBa(
|
||||
4.0767416621 * l - 3.3077115913 * m + 0.2309699292f * s,
|
||||
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965f * s,
|
||||
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010f * s,
|
||||
alpha, linearity = Linearity.LINEAR
|
||||
)
|
||||
}
|
||||
|
||||
fun toOKLCHa() = ColorOKLCHa.fromColorOKLABa(this)
|
||||
|
||||
override fun shade(factor: Double) = ColorOKLABa(l * factor, a, b, alpha)
|
||||
override fun opacify(factor: Double) = ColorOKLABa(l, a, b, alpha * factor)
|
||||
override fun minus(right: ColorOKLABa) = ColorOKLABa(l - right.l, a - right.a, b - right.b, alpha - right.alpha)
|
||||
override fun plus(right: ColorOKLABa) = ColorOKLABa(l + right.l, a + right.a, b + right.b, alpha + right.alpha)
|
||||
override fun times(scale: Double) = ColorOKLABa(l * scale, a * scale, b * scale, alpha * scale)
|
||||
}
|
||||
|
||||
fun ColorRGBa.toOKLABa() = ColorOKLABa.fromRGBa(this)
|
||||
61
orx-color/src/main/kotlin/spaces/ColorOKLCHa.kt
Normal file
61
orx-color/src/main/kotlin/spaces/ColorOKLCHa.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package spaces
|
||||
|
||||
import org.openrndr.color.*
|
||||
import org.openrndr.math.mixAngle
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* Color in cylindrical OKLab space
|
||||
*/
|
||||
data class ColorOKLCHa(val l: Double, val c: Double, val h: Double, val a: Double = 1.0) : ConvertibleToColorRGBa,
|
||||
OpacifiableColor<ColorOKLCHa>,
|
||||
ShadableColor<ColorOKLCHa>,
|
||||
HueShiftableColor<ColorOKLCHa>,
|
||||
AlgebraicColor<ColorOKLCHa> {
|
||||
|
||||
companion object {
|
||||
fun fromColorOKLABa(oklaba: ColorOKLABa): ColorOKLCHa {
|
||||
val l = oklaba.l
|
||||
val c = sqrt(oklaba.a * oklaba.a + oklaba.b * oklaba.b)
|
||||
var h = atan2(oklaba.b, oklaba.a)
|
||||
|
||||
if (h < 0) {
|
||||
h += Math.PI * 2
|
||||
}
|
||||
h = Math.toDegrees(h)
|
||||
return ColorOKLCHa(l, c, h, oklaba.alpha)
|
||||
}
|
||||
}
|
||||
|
||||
override fun opacify(factor: Double) = copy(a = a * factor)
|
||||
override fun shade(factor: Double) = copy(l = l * factor)
|
||||
override fun shiftHue(shiftInDegrees: Double) = copy(h = h + shiftInDegrees)
|
||||
|
||||
override fun plus(right: ColorOKLCHa) = copy(l = l + right.l, c = c + right.c, h = h + right.h, a = a + right.a)
|
||||
override fun minus(right: ColorOKLCHa) = copy(l = l - right.l, c = c - right.c, h = h - right.h, a = a - right.a)
|
||||
override fun times(scale: Double) = copy(l = l * scale, c = c * scale, h = h * scale, a = a * scale)
|
||||
override fun mix(other: ColorOKLCHa, factor: Double) = mix(this, other, factor)
|
||||
|
||||
fun toOKLABa(): ColorOKLABa {
|
||||
val a = c * cos(Math.toRadians(h))
|
||||
val b = c * sin(Math.toRadians(h))
|
||||
return ColorOKLABa(l, a, b, alpha = this.a)
|
||||
}
|
||||
|
||||
override fun toRGBa() = toOKLABa().toRGBa()
|
||||
}
|
||||
|
||||
fun mix(left: ColorOKLCHa, right: ColorOKLCHa, x: Double): ColorOKLCHa {
|
||||
val sx = x.coerceIn(0.0, 1.0)
|
||||
return ColorOKLCHa(
|
||||
(1.0 - sx) * left.l + sx * right.l,
|
||||
(1.0 - sx) * left.c + sx * right.c,
|
||||
mixAngle(left.h, right.h, sx),
|
||||
(1.0 - sx) * left.a + sx * right.a
|
||||
)
|
||||
}
|
||||
|
||||
fun ColorRGBa.toOKLCHa() = ColorOKLABa.fromRGBa(this).toOKLCHa()
|
||||
42
orx-color/src/test/kotlin/TestMix.kt
Normal file
42
orx-color/src/test/kotlin/TestMix.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.color.Linearity
|
||||
import org.openrndr.extras.color.palettes.rangeTo
|
||||
import org.spekframework.spek2.Spek
|
||||
import org.spekframework.spek2.style.specification.describe
|
||||
|
||||
object TestMix : Spek({
|
||||
|
||||
describe("two srgb colors") {
|
||||
val a = ColorRGBa.BLUE
|
||||
val b = ColorRGBa.RED
|
||||
|
||||
a.linearity `should be equal to` Linearity.SRGB
|
||||
|
||||
it("should mix properly") {
|
||||
a.mix(b, 0.0) `should be equal to` a
|
||||
a.mix(b, 1.0) `should be equal to` b
|
||||
}
|
||||
}
|
||||
|
||||
describe("two linear rgb colors") {
|
||||
val a = ColorRGBa.BLUE.toLinear()
|
||||
val b = ColorRGBa.RED.toLinear()
|
||||
|
||||
it("should mix properly") {
|
||||
a.mix(b, 0.0) `should be equal to` a
|
||||
a.mix(b, 1.0) `should be equal to` b
|
||||
}
|
||||
}
|
||||
|
||||
describe("a 2-step range of colors") {
|
||||
val a = ColorRGBa.BLUE
|
||||
val b = ColorRGBa.RED
|
||||
|
||||
val blend = a..b blend 2
|
||||
blend.size `should be equal to` 2
|
||||
blend[0] `should be equal to` a
|
||||
blend[1] `should be equal to` b
|
||||
}
|
||||
|
||||
})
|
||||
Reference in New Issue
Block a user