[orx-color] Add generated and verified documentation

This commit is contained in:
Edwin Jakobs
2025-01-19 00:12:55 +01:00
parent 8d0df407d0
commit 8ef7264c63
12 changed files with 312 additions and 20 deletions

View File

@@ -13,6 +13,14 @@ import kotlin.math.sqrt
Direct port of https://github.com/rvanwijnen/spectral.js
*/
/**
* Represents spectral power distribution (SPD) coefficients for the cyan channel,
* spanning 38 wavelength samples. This array is used as part of the spectral upsampling
* process to convert a linear RGB color into a reflectance spectrum.
*
* The coefficients are predefined values that define the contribution of the cyan channel
* at specific wavelengths to the resulting reflectance spectrum.
*/
private val SPD_C = doubleArrayOf(
0.96853629,
0.96855103,
@@ -54,6 +62,11 @@ private val SPD_C = doubleArrayOf(
0.01435408
)
/**
* Represents the spectral power distribution (SPD) data for the magenta color component.
* This array contains 38 predefined values corresponding to specific wavelengths.
* Used in the process of spectral upsampling to compute reflectance spectra from RGB color information.
*/
private val SPD_M = doubleArrayOf(
0.51567122,
0.5401552,
@@ -95,6 +108,11 @@ private val SPD_M = doubleArrayOf(
0.50083021
)
/**
* Spectral power distribution (SPD) values for the Yellow primary in the spectral upsampling process.
* This array contains 38 precomputed reflectance values corresponding to specific wavelength samples.
* It is used to calculate the reflectance spectrum when converting a linear RGB color to its spectral representation.
*/
private val SPD_Y = doubleArrayOf(
0.02055257,
0.02059936,
@@ -136,6 +154,17 @@ private val SPD_Y = doubleArrayOf(
0.98350852
)
/**
* A predefined spectral power distribution (SPD) array for the red channel,
* used in the spectral upsampling process to convert linear RGB colors into reflectance spectra.
*
* This array contains 38 values corresponding to specific wavelengths and represents
* the relative spectral contribution of the red channel in the conversion process.
*
* The SPD_R array is utilized in conjunction with other SPDs (e.g., SPD_C, SPD_M, SPD_Y, SPD_G, SPD_B)
* and the weights derived from spectral upsampling to calculate the reflectance spectrum
* for a given color.
*/
private val SPD_R = doubleArrayOf(
0.03147571,
0.03146636,
@@ -177,6 +206,16 @@ private val SPD_R = doubleArrayOf(
0.98551547
)
/**
* Represents the predefined spectral power distribution (SPD) values for the green component
* used in spectral upsampling of linear RGB colors.
*
* This array contains 38 reflectance values corresponding to specific wavelengths and
* reflects the spectral characteristics of the green primary in the color model.
*
* It is utilized as one of the SPD datasets in the linearToReflectance function, which
* converts linear RGB colors into reflectance spectra.
*/
private val SPD_G = doubleArrayOf(
0.49108579,
0.46944057,
@@ -218,6 +257,12 @@ private val SPD_G = doubleArrayOf(
0.49889859
)
/**
* Represents the spectral power distribution (SPD) values corresponding to the blue component
* of a linear RGB color. The array contains 38 precomputed reflectance values
* that span a specific range of wavelengths. These values are utilized in the spectral
* upsampling process to map an RGB color to its equivalent spectral reflectance distribution.
*/
private val SPD_B = doubleArrayOf(
0.97901834,
0.97901649,
@@ -259,6 +304,14 @@ private val SPD_B = doubleArrayOf(
0.0157002
)
/**
* A pre-defined array representing the CIE 1931 Standard Observer's color matching function values for the X component.
* This data is used in color science calculations to transform spectral reflectance data into the CIE XYZ color space.
* The array contains 38 discrete samples corresponding to wavelengths within the visible spectrum.
*
* This constant is specifically used during computations involving spectral data to calculate the X component
* of the CIE XYZ color space via multiplication with a corresponding reflectance spectrum array.
*/
private val CIE_CMF_X = doubleArrayOf(
0.00006469,
0.00021941,
@@ -300,6 +353,17 @@ private val CIE_CMF_X = doubleArrayOf(
0.00002
)
/**
* Represents the Y-component of the CIE 1931 color matching functions.
*
* The CIE 1931 color matching functions are used to convert spectral power distributions into
* CIE XYZ tristimulus values, which represent a color in a perceptually-uniform color space.
* These functions are defined over 38 discrete wavelength samples, typically covering
* the visible spectrum.
*
* The Y-component corresponds to the luminous efficiency function,
* which describes the sensitivity of human vision to different wavelengths of light.
*/
private val CIE_CMF_Y = doubleArrayOf(
0.00000184,
0.00000621,
@@ -341,6 +405,15 @@ private val CIE_CMF_Y = doubleArrayOf(
0.00000722
)
/**
* Represents the Z component of the CIE 1931 2° Standard Observer Color Matching Function (CMF).
*
* This array contains precomputed values representing the spectral sensitivity of the human eye's Z cone.
* It is used in color science to convert spectral reflectance data into the Z component of the CIE XYZ color space.
*
* The values in this array correspond to sampling points across the visible light spectrum
* and are used in conjunction with `CIE_CMF_X` and `CIE_CMF_Y` to perform reflectance spectrum to XYZ color conversions.
*/
private val CIE_CMF_Z = doubleArrayOf(
0.00030502,
0.00103681,
@@ -382,6 +455,12 @@ private val CIE_CMF_Z = doubleArrayOf(
0.0
)
/**
* Converts RGB color values into a form that represents spectral power distribution weights.
*
* @param rgb The RGB color in the form of a `ColorRGBa` object, which is to be converted to spectral weights.
* @return A `DoubleArray` representing the decomposed weights for white, cyan, magenta, yellow, red, green, and blue.
*/
private fun spectralUpsampling(rgb: ColorRGBa): DoubleArray {
var lrgb = rgb.toLinear()
val w = min(min(lrgb.r, lrgb.g), lrgb.b)
@@ -399,6 +478,14 @@ private fun spectralUpsampling(rgb: ColorRGBa): DoubleArray {
}
/**
* Converts a linear RGB color into a reflectance spectrum represented as a `DoubleArray`.
* The resulting reflectance spectrum spans 38 wavelength samples
* and is computed using spectral upsampling based on predefined spectral distributions.
*
* @param rgb The linear RGB color to be converted, represented as a `ColorRGBa` object.
* @return A `DoubleArray` containing 38 reflectance values corresponding to the wavelengths.
*/
internal fun linearToReflectance(rgb: ColorRGBa): DoubleArray {
val eps = 0.00000001
val weights = spectralUpsampling(rgb)
@@ -419,6 +506,20 @@ internal fun linearToReflectance(rgb: ColorRGBa): DoubleArray {
return reflectance
}
/**
* Computes the concentration of a component in a linear interpolation based on the specified parameters.
*
* The method calculates a concentration factor `c` using two linear values `l1` and `l2`,
* as well as a blend factor `t`. This can be used in scenarios such as spectral mixing
* or interpolation tasks where weights are dynamically computed.
*
* @param l1 The first linear value, typically derived from reflectance or intensity.
* @param l2 The second linear value, typically derived from reflectance or intensity.
* @param t The blending factor in the range [0.0, 1.0], where 0.0 represents full influence of `l1`
* and 1.0 represents full influence of `l2`.
* @return The computed concentration factor as a `Double`, representing the relative contribution
* of the components based on the blending factor.
*/
private fun linearToConcentration(l1: Double, l2: Double, t: Double): Double {
val t1 = l1 * (1 - t).pow(2.0)
val t2 = l2 * t.pow(2.0)
@@ -434,6 +535,16 @@ private fun DoubleArray.dot(other: DoubleArray): Double {
return d
}
/**
* Converts a reflectance spectrum represented as a `DoubleArray` to a color in the CIE XYZ color space.
*
* This method calculates the XYZ color by performing a dot product operation between the reflectance spectrum
* and the CIE color matching functions (CIE_CMF_X, CIE_CMF_Y, CIE_CMF_Z). The result is returned as a `ColorXYZa` object.
*
* @param reflectance An array of reflectance spectrum values, typically spanning the visible spectrum.
* This is represented as a `DoubleArray` with 38 wavelength samples.
* @return A `ColorXYZa` object representing the corresponding color in the CIE XYZ color space.
*/
internal fun reflectanceToXYZ(reflectance: DoubleArray): ColorXYZa {
val x = reflectance.dot(CIE_CMF_X)
val y = reflectance.dot(CIE_CMF_Y)
@@ -443,10 +554,34 @@ internal fun reflectanceToXYZ(reflectance: DoubleArray): ColorXYZa {
private fun pow(x: Double, y: Double): Double = x.pow(y)
/**
* Applies the Saunderson correction to a reflectance value to account for surface reflectance effects.
*
* Saunderson correction adjusts the measured reflectance by considering front surface reflection
* and internal reflections in the material. The correction is based on two coefficients, `k1` and `k2`.
*
* @param rInf The measured reflectance value to be corrected.
* @param k1 The first Saunderson coefficient representing front surface reflection.
* @param k2 The second Saunderson coefficient representing internal reflection effects.
* @return The corrected reflectance value as a `Double`.
*/
internal fun saundersonCorrection(rInf: Double, k1: Double, k2: Double): Double {
return k1 + ((1 - k1) * (1 - k2) * rInf) / (1 - (k2 * rInf))
}
/**
* Blends two colors spectrally by interpolating their reflectance spectra and returns the resulting color.
* This method uses spectral upsampling, Saunderson correction, and concentration factors to compute
* the resulting color in the RGB color space.
*
* @param color1 The first color to be mixed, represented as a `ColorRGBa`.
* @param color2 The second color to be mixed, represented as a `ColorRGBa`.
* @param t The blending factor in the range [0.0, 1.0], where 0.0 represents full influence of `color1`
* and 1.0 represents full influence of `color2`.
* @param k1 The first Saunderson correction coefficient for surface reflection. Default is 0.0.
* @param k2 The second Saunderson correction coefficient for internal reflections. Default is 0.0.
* @return The resulting blended color as a `ColorRGBa`, maintaining the linearity of the first input color (`color1`).
*/
fun mixSpectral(color1: ColorRGBa, color2: ColorRGBa, t: Double, k1: Double = 0.0, k2: Double = 0.0): ColorRGBa {
val lrgb1 = color1.toLinear()
val lrgb2 = color2.toLinear()

View File

@@ -6,10 +6,18 @@ import org.openrndr.color.HueShiftableColor
import org.openrndr.extra.color.tools.shiftHue
/**
* Generate an analogous palette
* @param T the color model to use
* @param hueShift Hue degrees between the first and the last color
* @param steps Number of colors to create
* Generates an analogous color palette based on the current color.
*
* This function creates a sequence of colors by shifting the hue of the current color
* gradually across a specified range of steps, using a particular color model that supports hue shifting.
*
* @param T The color model used for hue shifting.
* Must extend both `HueShiftableColor` and `ColorModel`.
* @param hueShift The total degree shift in hue between the first color and the last color.
* The hue shift is divided among the specified number of steps.
* @param steps The number of colors to include in the palette, including the starting color.
* Defaults to 5.
* @return A list of `ColorRGBa` instances forming the analogous palette.
*/
inline fun <reified T> ColorRGBa.analogous(hueShift: Double, steps: Int = 5): List<ColorRGBa>
where T : HueShiftableColor<T>,
@@ -18,10 +26,21 @@ inline fun <reified T> ColorRGBa.analogous(hueShift: Double, steps: Int = 5): Li
}
/**
* Generate a split complementary palette in which the receiver is the seed color
* @param T the color model to use
* @param splitFactor a value between 0 and 1 that indicates how much the complementary color should be split
* @param double should a double complementary palette be generated
* Generates a split complementary color palette based on the current `ColorRGBa`.
*
* The method calculates complementary colors that are spread around the complementary
* hue axis of the original color. Depending on the parameters, the result may include
* two or four additional colors in addition to the original color.
*
* @param T The color model and hue shifting capability of the colors to generate.
* @param splitFactor A value between 0.0 and 1.0 that controls the spread of the complementary colors
* around the complementary hue. A higher value increases the angle between
* the colors on the hue wheel, while a lower value decreases it.
* @param double If `true`, the method will generate two additional colors derived by more granular
* shifts within the complementary range. If `false`, a simpler complementary palette
* is returned.
* @return A list of `ColorRGBa` objects representing the split complementary palette, with the original
* color as the first element in the list.
*/
inline fun <reified T> ColorRGBa.splitComplementary(splitFactor: Double, double: Boolean = false): List<ColorRGBa>
where T : HueShiftableColor<T>,
@@ -38,9 +57,16 @@ inline fun <reified T> ColorRGBa.splitComplementary(splitFactor: Double, double:
}
}
/**
* Generate a triadic palette in which the receiver is the seed color.
* @param T the color model to use
* Generates a triadic color palette based on the current `ColorRGBa`.
*
* Triadic colors are evenly spaced on the color wheel, forming a triangle.
* This method generates two additional colors by evenly shifting the hue of the given color
* at 120° intervals around the hue circle.
*
* @param T The color model and hue shifting capability of the colors to generate.
* @return A list of `ColorRGBa` objects representing the triadic color palette.
*/
inline fun <reified T> ColorRGBa.triadic(): List<ColorRGBa>
where T : HueShiftableColor<T>,
@@ -48,9 +74,14 @@ inline fun <reified T> ColorRGBa.triadic(): List<ColorRGBa>
/**
* Generate a tetradic palette in which the receiver is the seed color.
* @param T the color model to use
* @param aspectRatio the aspect ratio between even and odd sides
* Generates a tetradic color scheme based on the current color.
* A tetradic color scheme consists of four colors that are equidistant on the color wheel.
*
* @param aspectRatio A double value representing the aspect ratio of the tetradic scheme.
* The aspect ratio determines the angular separation between the colors in the scheme.
* Default is 1.0, resulting in equidistant colors.
* @return A list of `ColorRGBa` instances representing the tetradic color scheme.
* The list includes the original color and three additional colors derived by shifting the hue.
*/
inline fun <reified T> ColorRGBa.tetradic(aspectRatio: Double = 1.0): List<ColorRGBa>
where T : HueShiftableColor<T>,

View File

@@ -6,6 +6,16 @@ import org.openrndr.math.Vector4
import org.openrndr.math.mixAngle
import kotlin.jvm.JvmRecord
/**
* Represents a color in the HPLUVa (Hue, Perceptual Lightness, Saturation, Alpha) color space.
* This color space is based on perceptual uniformity, making it suitable for operations
* like interpolation, shading, and manipulation of hue, saturation, and lightness values.
*
* @property h The hue component of the color, representing the angle on the color wheel in degrees [0, 360).
* @property s The saturation component of the color, representing the intensity of the color [0.0, 1.0].
* @property l The lightness component of the color, representing the relative brightness [0.0, 1.0].
* @property alpha The alpha (opacity) component of the color, ranging from fully transparent (0.0) to fully opaque (1.0).
*/
@Serializable
@JvmRecord
data class ColorHPLUVa(val h: Double, val s: Double, val l: Double, override val alpha: Double = 1.0) :

View File

@@ -78,8 +78,16 @@ private fun maxChromaForLH(L100: Double, H: Double): Double {
return min
}
/**
* HSLUV color space
* Represents a color in the HSLuv color space with an alpha transparency component.
* HSLuv is a perceptually uniform color space, where hues are uniformly distributed
* and the perception of color is consistent across the spectrum.
*
* @property h The hue of the color in degrees, ranging from 0.0 to 360.0.
* @property s The saturation of the color, ranging from 0.0 to 1.0.
* @property l The luminance of the color, ranging from 0.0 to 1.0.
* @property alpha The alpha transparency value, ranging from 0.0 (fully transparent) to 1.0 (fully opaque).
*/
@Serializable
@JvmRecord

View File

@@ -7,6 +7,16 @@ import org.openrndr.math.mixAngle
import kotlin.jvm.JvmRecord
import kotlin.math.*
/**
* Represents a color in the OKHSL (hue, saturation, lightness) color space with an alpha channel.
* This color model is based on perceptual uniformity and is useful for hue, saturation, and
* lightness manipulations while maintaining consistency with human vision.
*
* @property h The hue of the color, represented as a value in degrees [0.0, 360.0).
* @property s The saturation of the color, where 0.0 is fully desaturated (gray) and 1.0 is fully saturated.
* @property l The lightness of the color, where 0.0 is completely dark and 1.0 is completely light.
* @property alpha The opacity of the color, where 0.0 is fully transparent and 1.0 is fully opaque.
*/
@Suppress("LocalVariableName")
@Serializable
@JvmRecord

View File

@@ -7,6 +7,22 @@ import org.openrndr.math.mixAngle
import kotlin.jvm.JvmRecord
import kotlin.math.*
/**
* Represents a color in the OKHSVa color model.
*
* The OKHSVa color model is derived from OKLABa and provides a perceptually uniform representation
* of colors using hue (h), saturation (s), value (v), and alpha (opacity).
*
* This class supports operations and transformations such as conversion to and from RGBa,
* hue shifting, saturation adjustment, shading, and algebraic operations like addition, subtraction,
* and scaling. It is ideal for working with colors in contexts requiring accurate color mixing
* and perceptual results.
*
* @property h Hue value in degrees (0.0 - 360.0), representing the color's angle on the color wheel.
* @property s Saturation value (0.0 - 1.0), representing the intensity or purity of the color.
* @property v Value (0.0 - 1.0), representing the color's brightness.
* @property alpha Opacity value (0.0 - 1.0), with 1.0 being fully opaque.
*/
@Suppress("LocalVariableName")
@Serializable
@JvmRecord

View File

@@ -8,11 +8,16 @@ import kotlin.math.abs
import kotlin.math.pow
import kotlin.math.sign
/**
* Color in OKLab color space.
* [l] = lightness: black (0.0) to white (1.0),
* [a] = red (-1.0) to green (1.0),
* [b] = yellow (-1.0) to blue (1.0).
* Represents a color in the OKLAB color space with an optional alpha (transparency) value.
* OKLAB is a perceptual color space designed to represent colors in a way that aligns
* with human visual perception, offering an alternative to traditional color spaces like RGB.
*
* @property l The lightness component of the color. A value between 0 (black) and 1 (white).
* @property a The 'a' component in the OKLAB color space, representing the first chromatic axis.
* @property b The 'b' component in the OKLAB color space, representing the second chromatic axis.
* @property alpha The alpha (opacity) value of the color. A value between 0.0 (completely transparent) and 1.0 (completely opaque).
*/
@Suppress("LocalVariableName")
@Serializable

View File

@@ -6,8 +6,14 @@ import org.openrndr.math.*
import kotlin.jvm.JvmRecord
import kotlin.math.*
/**
* Color in cylindrical OKLab space
* Represents a color in the OKLCH color space, which is based on the OKLab color space with added polar coordinates for chroma and hue.
*
* @property l Lightness of the color. Range: 0.0 (black) to 1.0 (white).
* @property c Chroma, representing color intensity. Typically non-negative.
* @property h Hue angle in degrees. Range: 0.0 to 360.0.
* @property alpha Opacity of the color. Range: 0.0 (fully transparent) to 1.0 (fully opaque). Default is 1.0.
*/
@Serializable
@JvmRecord

View File

@@ -7,6 +7,16 @@ import org.openrndr.math.map
import org.openrndr.math.mixAngle
import kotlin.jvm.JvmRecord
/**
* Represents a color in the XSLUV color space with an optional alpha transparency value.
* XSLUV is a cylindrical representation of the HSLUV color space using an alternative X coordinate
* instead of the standard hue. It provides a perceptually uniform color representation.
*
* @property x The X coordinate representing the hue variation in the XSLUV color space.
* @property s The saturation of the color in the XSLUV color space.
* @property l The luminance of the color in the XSLUV color space.
* @property alpha The alpha transparency value of the color, ranging from 0.0 to 1.0. Defaults to 1.0.
*/
@Serializable
@JvmRecord
data class ColorXSLUVa(val x: Double, val s: Double, val l: Double, override val alpha: Double = 1.0) :

View File

@@ -14,6 +14,12 @@ internal fun max(a: Double, b: Double, c: Double, d: Double): Double {
return max(max(a, b), max(c, d))
}
/**
* Computes a modified version of the Toe mapping function, used in color space transformations.
*
* @param x The input value for the function, representing a parameter in the transformation process.
* @return The transformed value after applying the Toe mapping function.
*/
fun toe(x: Double): Double {
val k1 = 0.206
val k2 = 0.03
@@ -24,6 +30,14 @@ fun toe(x: Double): Double {
return 0.5 * (k3 * x - k1 + sqrt(d.coerceAtLeast(0.0)))
}
/**
* Computes the toe inverse transformation of a given input value based on specific constants.
* This function is typically used for calculations involving perceptual transformations
* or non-linear scaling in color spaces or other mathematical models.
*
* @param x The input value for which the toe inverse transformation is calculated.
* @return A transformed value based on the toe inverse function.
*/
fun toeInv(x: Double): Double {
val k1 = 0.206
val k2 = 0.03

View File

@@ -5,7 +5,12 @@ import org.openrndr.color.ConvertibleToColorRGBa
import org.openrndr.math.Vector3
/**
* Computes delta E between two colors.
* Computes the CIE76 color difference (ΔE*76) between this color and another color.
* The method calculates the Euclidean distance between the two colors in the LAB color space.
* If either of the colors is not in LAB format, it will be converted to LAB before computation.
*
* @param other The second color to compare, which should implement the ConvertibleToColorRGBa interface.
* @return The calculated CIE76 color difference as a Double.
*/
fun <T: ConvertibleToColorRGBa> T.deltaE76(other: T): Double {
return if (this is ColorLABa && other is ColorLABa) {

View File

@@ -3,10 +3,30 @@ package org.openrndr.extra.color.tools
import org.openrndr.color.*
import org.openrndr.extra.color.spaces.*
/**
* Indicates whether the color is out of the RGB gamut.
*
* This property evaluates if the color's red, green, or blue components are outside
* the valid range of [0.0, 1.0], accounting for a slight tolerance in the negative range (-1E-3).
* Additionally, it checks whether the alpha component is outside the range [0.0, 1.0].
*
* This property is commonly used in color manipulation functions to detect and handle
* out-of-gamut colors, which may require adjustments (e.g., clipping or chroma adjustment)
* to fit within a valid color space.
*/
val ColorRGBa.isOutOfGamut: Boolean
get() {
return (r !in -1E-3..1.0) || (g !in -1E-3..1.0) || (b !in -1E-3..1.0) || (alpha !in 0.0..1.0)
}
/**
* Matches the linearity of the current `ColorRGBa` instance with another `ColorRGBa` instance.
* If the linearity of `other` matches that of the current instance, the current instance is returned.
* Otherwise, it converts the current instance to match the linearity of `other`.
*
* @param other The `ColorRGBa` instance whose linearity is to be matched.
* @return A `ColorRGBa` instance with the same linearity as the `other` color.
*/
fun ColorRGBa.matchLinearity(other: ColorRGBa): ColorRGBa {
return if (other.linearity.isEquivalent(linearity)) {
this
@@ -36,6 +56,12 @@ inline fun <reified T> ColorRGBa.blendWith(other: ColorRGBa, steps: Int): Sequen
}
/**
* Converts the current `ColorRGBa` instance to the specified color model type `T`.
*
* @return An instance of the specified color model type `T` after conversion.
* @throws IllegalStateException if the specified color model is not supported.
*/
inline fun <reified T : ColorModel<T>> ColorRGBa.convertTo(): T {
val converted = when (T::class) {
ColorHSLa::class -> this.toHSLa()
@@ -134,11 +160,27 @@ inline fun <reified T> ColorRGBa.modulateChroma(factor: Double): ColorRGBa
convertTo<T>().modulateChroma(factor).toRGBa().matchLinearity(this)
/**
* Adjusts the saturation of the current `ColorRGBa` based on a given factor.
*
* @param T The target color model type that supports saturation adjustments.
* @param factor The saturation adjustment factor. A value of 1.0 keeps the saturation unchanged,
* values less than 1.0 decrease saturation, and values greater than 1.0 increase it.
* @return A new `ColorRGBa` instance with the adjusted saturation, maintaining the linearity of
* the original color.
*/
inline fun <reified T> ColorRGBa.saturate(factor: Double): ColorRGBa
where T : SaturatableColor<T>,
T : ColorModel<T>,
T : ConvertibleToColorRGBa = convertTo<T>().saturate(factor).toRGBa().matchLinearity(this)
/**
* Shifts the hue of the current `ColorRGBa` by the specified number of degrees.
* The method is only applicable to color models that support hue shifting and can be converted to `ColorRGBa`.
*
* @param degrees The amount of hue adjustment in degrees. Positive values shift the hue clockwise, while negative values shift it counterclockwise.
* @return A new `ColorRGBa` instance with the hue shifted by the specified degree, maintaining the same linearity as the input color.
*/
inline fun <reified T> ColorRGBa.shiftHue(degrees: Double): ColorRGBa where
T : HueShiftableColor<T>,
T : ColorModel<T>,