diff --git a/build.gradle b/build.gradle index ec48c6a3..3dccabee 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.45" + openrndrVersion = openrndrUseSnapshot? "0.4.0-SNAPSHOT" : "0.3.46" kotlinVersion = "1.4.30" spekVersion = "2.0.15" libfreenectVersion = "0.5.7-1.5.4" diff --git a/orx-color/build.gradle b/orx-color/build.gradle index dec3965b..a3dfde8c 100644 --- a/orx-color/build.gradle +++ b/orx-color/build.gradle @@ -9,6 +9,9 @@ sourceSets { } dependencies { + demoImplementation(project(":orx-camera")) + demoImplementation(project(":orx-mesh-generators")) + demoImplementation("org.openrndr:openrndr-core:$openrndrVersion") demoImplementation("org.openrndr:openrndr-core:$openrndrVersion") demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion") demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion") diff --git a/orx-color/src/demo/kotlin/DemoColorPlane01.kt b/orx-color/src/demo/kotlin/DemoColorPlane01.kt new file mode 100644 index 00000000..c88f0b25 --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoColorPlane01.kt @@ -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), + + ) + ) + } + } + } +} \ No newline at end of file diff --git a/orx-color/src/demo/kotlin/DemoColorPlane02.kt b/orx-color/src/demo/kotlin/DemoColorPlane02.kt new file mode 100644 index 00000000..08b6a9a6 --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoColorPlane02.kt @@ -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), + ) + ) + } + } + } +} \ No newline at end of file diff --git a/orx-color/src/demo/kotlin/DemoColorRange03.kt b/orx-color/src/demo/kotlin/DemoColorRange03.kt new file mode 100644 index 00000000..3a49176f --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoColorRange03.kt @@ -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) + } + } + } + } +} \ No newline at end of file diff --git a/orx-color/src/demo/kotlin/DemoColorRange04.kt b/orx-color/src/demo/kotlin/DemoColorRange04.kt new file mode 100644 index 00000000..7b449701 --- /dev/null +++ b/orx-color/src/demo/kotlin/DemoColorRange04.kt @@ -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), + ) + ) + } + } + } +} \ 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 417e3624..47d29f36 100644 --- a/orx-color/src/main/kotlin/palettes/Palettes.kt +++ b/orx-color/src/main/kotlin/palettes/Palettes.kt @@ -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 colorSequence(vararg offsets: Pair): ColorSequence @@ -51,6 +55,8 @@ class ColorSequence(val colors: List>) { 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() } diff --git a/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt b/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt index a34cffef..e5fbea11 100644 --- a/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt +++ b/orx-color/src/main/kotlin/spaces/ColorHSLUVa.kt @@ -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, @@ -85,7 +88,6 @@ data class ColorHSLUVa(val h: Double, val s: Double, val l: Double, val a: Doubl OpacifiableColor, AlgebraicColor { - fun toLCHUVa(): ColorLCHUVa { val l100 = l * 100.0 diff --git a/orx-color/src/main/kotlin/spaces/ColorOKLABa.kt b/orx-color/src/main/kotlin/spaces/ColorOKLABa.kt new file mode 100644 index 00000000..3ef51fe6 --- /dev/null +++ b/orx-color/src/main/kotlin/spaces/ColorOKLABa.kt @@ -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, + OpacifiableColor, + AlgebraicColor { + + 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) \ No newline at end of file diff --git a/orx-color/src/main/kotlin/spaces/ColorOKLCHa.kt b/orx-color/src/main/kotlin/spaces/ColorOKLCHa.kt new file mode 100644 index 00000000..b2bd581e --- /dev/null +++ b/orx-color/src/main/kotlin/spaces/ColorOKLCHa.kt @@ -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, + ShadableColor, + HueShiftableColor, + AlgebraicColor { + + 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() diff --git a/orx-color/src/test/kotlin/TestMix.kt b/orx-color/src/test/kotlin/TestMix.kt new file mode 100644 index 00000000..2c807573 --- /dev/null +++ b/orx-color/src/test/kotlin/TestMix.kt @@ -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 + } + +}) \ No newline at end of file