Skip to content

Commit

Permalink
[orx-color] Add ColorSequence.toColorBuffer
Browse files Browse the repository at this point in the history
  • Loading branch information
edwinRNDR committed Jan 24, 2025
1 parent 2483520 commit 4e1349c
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 63 deletions.
143 changes: 143 additions & 0 deletions orx-color/src/commonMain/kotlin/palettes/ColorSequence.kt
Original file line number Diff line number Diff line change
@@ -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 <T> colorSequence(vararg offsets: Pair<Double, T>): 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<Pair<Double, ConvertibleToColorRGBa>>) {
infix fun blend(steps: Int): List<ColorRGBa> = 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)
63 changes: 0 additions & 63 deletions orx-color/src/commonMain/kotlin/palettes/Palettes.kt

This file was deleted.

71 changes: 71 additions & 0 deletions orx-color/src/jvmDemo/kotlin/DemoColorSequence01.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
}
}

0 comments on commit 4e1349c

Please sign in to comment.