diff --git a/orx-color/src/commonMain/kotlin/palettes/ColorSequence.kt b/orx-color/src/commonMain/kotlin/palettes/ColorSequence.kt new file mode 100644 index 00000000..835ded5b --- /dev/null +++ b/orx-color/src/commonMain/kotlin/palettes/ColorSequence.kt @@ -0,0 +1,143 @@ +package org.openrndr.extra.color.palettes + +import org.openrndr.color.* +import org.openrndr.draw.* +import org.openrndr.extra.color.spaces.* + + +/** + * Creates a `ColorSequence` by accepting a variable number of pairs, where each pair consists of + * a position (Double) and a color (T). The positions represent the normalized range `[0.0, 1.0]`. + * The resulting `ColorSequence` can be used for creating interpolated colors between the specified positions. + * + * @param offsets Vararg parameter of pairs, where each pair includes a position (Double) and a color (of type T). + * The position defines the location along a normalized sequence `[0.0, 1.0]`, and the color must implement `ConvertibleToColorRGBa`. + * Typically, positions must be sorted, but the function will sort them internally based on their position values. + * @return A `ColorSequence` containing the sorted sequence of colors and positions. + */ +fun colorSequence(vararg offsets: Pair): ColorSequence + where T : ConvertibleToColorRGBa { + return ColorSequence(offsets.sortedBy { it.first }) +} + +/** + * Represents a sequence of colors along with their corresponding positions in a normalized range [0.0, 1.0]. + * The `ColorSequence` allows for creating interpolated colors between the specified color points. + * + * @property colors A list of pairs where the first value is a position (ranging from 0.0 to 1.0) + * and the second value is a color that implements `ConvertibleToColorRGBa`. + */ +class ColorSequence(val colors: List>) { + infix fun blend(steps: Int): List = color(0.0, 1.0, steps) + + /** + * Converts a color sequence into a color buffer with a gradient representation. + * + * @param drawer The Drawer used to render the gradient into the color buffer. + * @param width The width of the resulting color buffer in pixels. Defaults to 256. + * @param height The height of the resulting color buffer in pixels. Defaults to 16. + * @param type The ColorType of the resulting color buffer. Defaults to UINT8_SRGB. + * @param format The ColorFormat of the resulting color buffer. Defaults to RGBa. + * @return A ColorBuffer containing the rendered color gradient. + */ + fun toColorBuffer( + drawer: Drawer, + width: Int = 256, + height: Int = 16, + type: ColorType = ColorType.UINT8_SRGB, + format: ColorFormat = ColorFormat.RGBa + ): ColorBuffer { + val cb = colorBuffer(width, height, type = type, format = format) + val rt = renderTarget(width, height) { + colorBuffer(cb) + } + + drawer.isolatedWithTarget(rt) { + defaults() + ortho(rt) + drawer.rectangles { + for (i in 0 until width) { + fill = color(i / (width.toDouble() - 1.0)) + stroke = null + rectangle(i * 1.0, 0.0, 1.0, height.toDouble()) + } + } + } + + rt.destroy() + return cb + } + + /** + * Generates a sequence of interpolated colors between two specified values. + * + * @param t0 A Double representing the start value for interpolation. + * @param t1 A Double representing the end value for interpolation. + * @param steps An Int representing the number of colors to generate in the sequence. + * @return A List of interpolated colors. + */ + fun color(t0: Double, t1: Double, steps: Int) = (0 until steps).map { + val f = (it / (steps - 1.0)) + val t = t0 * (1.0 - f) + t1 * f + color(t) + } + + /** + * Calculates a color using interpolation based on the provided parameter `t`. + * + * @param t A Double representing the position along the color sequence, typically ranging from 0.0 to 1.0. + * It indicates how far between the sequence colors the interpolation should occur, + * with 0.0 being the start of the sequence and 1.0 being the end. + * @return A ColorRGBa instance representing the interpolated color in the sRGB color space. + * If the provided `t` is outside the range of the sequence, the color at the nearest boundary will be returned. + */ + fun color(t: Double): ColorRGBa { + if (colors.size == 1) { + return colors.first().second.toRGBa().toSRGB() + } + if (t < colors[0].first) { + return colors[0].second.toRGBa().toSRGB() + } + if (t >= colors.last().first) { + return colors.last().second.toRGBa().toSRGB() + } + val rightIndex = colors.binarySearch { it.first.compareTo(t) }.let { if (it < 0) -it - 2 else it } + 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 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() + is ColorOKHSLa -> right.second.toRGBa().toOKHSLa().mix(l, nt).toRGBa() + is ColorOKHSVa -> right.second.toRGBa().toOKHSVa().mix(l, nt).toRGBa() + else -> error("unsupported color space: ${l::class}") + }.toSRGB() + } +} + +/** + * Defines a range between two colors by creating a sequence of colors + * that transition smoothly from the start color to the end color. + * + * @param end The end color of the range. Both start and end colors must implement `ConvertibleToColorRGBa`. + * The start color is implicitly the color on which this operator is called. + */ +operator fun ConvertibleToColorRGBa.rangeTo(end: ConvertibleToColorRGBa) = colorSequence(0.0 to this, 1.0 to end) diff --git a/orx-color/src/commonMain/kotlin/palettes/Palettes.kt b/orx-color/src/commonMain/kotlin/palettes/Palettes.kt deleted file mode 100644 index 80041580..00000000 --- a/orx-color/src/commonMain/kotlin/palettes/Palettes.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.openrndr.extra.color.palettes - -import org.openrndr.color.* -import org.openrndr.extra.color.spaces.* - - -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 - 1.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().toSRGB() - } - if (t < colors[0].first) { - return colors[0].second.toRGBa().toSRGB() - } - if (t >= colors.last().first) { - return colors.last().second.toRGBa().toSRGB() - } - 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 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() - is ColorOKHSLa -> right.second.toRGBa().toOKHSLa().mix(l, nt).toRGBa() - is ColorOKHSVa -> right.second.toRGBa().toOKHSVa().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/jvmDemo/kotlin/DemoColorSequence01.kt b/orx-color/src/jvmDemo/kotlin/DemoColorSequence01.kt new file mode 100644 index 00000000..26c268a0 --- /dev/null +++ b/orx-color/src/jvmDemo/kotlin/DemoColorSequence01.kt @@ -0,0 +1,71 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.DrawPrimitive +import org.openrndr.draw.isolated +import org.openrndr.draw.shadeStyle +import org.openrndr.extra.camera.Orbital +import org.openrndr.extra.color.palettes.ColorSequence +import org.openrndr.extra.color.presets.MEDIUM_AQUAMARINE +import org.openrndr.extra.color.presets.ORANGE +import org.openrndr.extra.color.spaces.toOKLABa +import org.openrndr.extra.meshgenerators.sphereMesh +import org.openrndr.math.Vector3 + +/** + * A demo that demonstrates 3D objects with custom shading and color gradients. + * + * The application setup involves: + * - Configuring the application window dimensions. + * - Creating a color gradient using `ColorSequence` and converting it to a `ColorBuffer` for shading purposes. + * - Defining a 3D sphere mesh with specified resolution. + * + * The rendering process includes: + * - Setting up an orbital camera extension to provide an interactive 3D view. + * - Applying a custom fragment shader with a palette-based shading style. + * - Rendering a grid of 3D spheres, each transformed and rotated to create a dynamic pattern. + */ +fun main() { + application { + configure { + width = 720 + height = 720 + } + program { + val cs = ColorSequence( + listOf( + 0.0 to ColorRGBa.PINK, + 0.25 to ColorRGBa.ORANGE.toOKLABa(), + 0.27 to ColorRGBa.WHITE.toOKLABa(), + 0.32 to ColorRGBa.BLUE, + 1.0 to ColorRGBa.MEDIUM_AQUAMARINE + ) + ) + val palette = cs.toColorBuffer(drawer, 256, 16) + val sphere = sphereMesh(sides = 48, segments = 48) + + extend(Orbital()) { + fov = 50.0 + eye = Vector3(0.0, 0.0, 13.0) + } + extend { + drawer.shadeStyle = shadeStyle { + fragmentTransform = """ + float d = normalize(va_normal).z; + x_fill = texture(p_palette, vec2(1.0-d, 0.0)); + """.trimIndent() + parameter("palette", palette) + } + for (j in -2..2) { + for (i in -2..2) { + drawer.isolated { + drawer.translate(i * 2.0, j * 2.0, 0.0) + drawer.rotate(Vector3.UNIT_Y, j * 30.0) + drawer.rotate(Vector3.UNIT_X, i * 30.0) + drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES) + } + } + } + } + } + } +} \ No newline at end of file