Usage¶
Creating colors¶
Instances of a color are constructed by invoking their color model.
LAB(50, 75, 100)
For models with multiple color spaces, you can also invoke the specific color space, or create your own:
LAB50(50, 75, 100) // Uses the D50 illuminant
val LAB55 = LabColorSpace(Illuminant.D55)
LAB55(10, 20, 30)
You can optionally specify an alpha value.
LAB(l=0, a=0, b=0, alpha=0.5)
If you don’t specify an alpha value, it will default to 1, meaning fully opaque.
sRGB colors can also be constructed from hex strings or integers. All of the following are equivalent:
RGB(0.2, 0.4, 0.6)
SRGB(0.2, 0.4, 0.6)
RGB.from255(51, 102, 153)
RGB("#369")
RGB("#336699")
RGBInt(0x336699u).toSRGB()
You can find the full list of built-in color spaces here.
Converting colors¶
You can convert a color to another color space with any of the Color.to*() functions:
RGB("#111").toHSL()
XYZ(.1, .2, .3).toOklab()
You can also convert to a specific color space with convertTo:
RGB("#111").convertTo(LinearSRGB)
RGB("#222").convertTo(LAB)
RGB("#333").convertTo(LAB50)
If you need to convert multiple colors from one RGB color space to another, you can use an
RGBToRGBConverter, which is more efficient than using convertTo
multiple times:
val srgbColors: List<RGB> = listOf(/*...*/)
val converter = SRGB.converterTo(ACES)
val acesColors = srgbColors.map { converter.convert(it) }
Caution
When converting to polar spaces like HSL
, the hue is undefined for grayscale colors. When that’s the case,
the hue value will be NaN
. If you always want a non-NaN hue, you can use hueOr like hsl.hueOr(0)
.
Color transforms¶
You can create generic transforms for colors with Color.map. Colormath includes several built-in transforms.
Mix¶
Mix two colors based on a fraction of each, with a syntax that’s similar to the CSS color-mix function.
val purple = LCHab(29, 66, 327)
val plum = LCHab(73, 37, 324)
val mixed = LCHab.mix(purple, .8, plum, .3)
Note
If the amount of the two colors adds up to less than one, the resulting mix will be partially transparent.
Interpolate¶
You can also interpolate between two colors. This is similar to mix
, but takes a single parameter
t
indicating the amount to interpolate between the two colors, with t=0
returning the first
color, and t=1
returning the second.
val color1 = RGB("#000")
val color2 = RGB("#444")
color1.interpolate(color2, t=0.25)
// RGB("#111")
Premultiply alpha¶
Colormath colors aren’t normally stored with alpha premultiplied. You can do so with multiplyAlpha, and revert the operation with divideAlpha.
val color = RGB(1, 1, 1, alpha = 0.25)
color.multiplyAlpha()
// RGB(0.25, 0.25, 0.25, alpha=0.25)
Color calculations¶
Color gamut¶
You can check if a color is within the sRGB gamut with isInSRGBGamut.
XYZ(.5, .7, 1).isInSRGBGamut() // true
ICtCp(0, .5, .5).isInSRGBGamut() // false
Color contrast¶
You can calculate the relative luminance of a color, or the contrast between two colors according to the Web Content Accessibility Guidelines.
RGB("#f3a").wcagLuminance() // 0.26529932
RGB("#aaa").wcagContrastRatio(RGB("#fff")) // 2.323123
You can also select the most contrasting color from a list of colors, similar to the CSS color-contrast
function.
val wheat = RGB("#f5deb3")
val tan = RGB("#d2b48c")
val sienna = RGB("#a0522d")
val accent = RGB("#b22222")
wheat.mostContrasting(tan, sienna, accent) // returns accent
In addition to mostContrasting, you can use firstWithContrast or firstWithContrastOrNull, depending on your use case.
Color difference¶
Colormath includes several formulas for computing the relative perceptual difference between two colors.
Gradients and Interpolation¶
For gradients and advanced interpolation, you can use the interpolator builder.
// You can interpolate in any color space.
val interp = Oklab.interpolator {
// Color stops can be specified in a different space than the interpolator
stop(RGB("#00f"))
stop(RGB("#fff"))
stop(RGB("#000"))
}
// Get a single color
interp.interpolate(0.25)
// Or a sequence of colors to draw a gradient
for ((x, color) in interp.sequence(canvas.width).withIndex()) {
canvas.drawRect(x=x, y=0, w=1, h=canvas.height, color)
}
Interpolation method¶
Interpolators use linear interpolation by default. Colormath also includes an implementation of monotone spline interpolation, which produces smoother gradients.
LCHab.interpolator {
method = InterpolationMethods.monotoneSpline()
// ...
}
Easing functions¶
Where the interpolation method changes the path the gradient takes through a color space, an easing function changes the speed that the path is traversed.
EasingFunctions includes all the CSS easing functions, as well as an easing function to set the midpoint of the gradient between two stops.
LCHab.interpolator {
// Set the easing function for all components
easing = EasingFunctions.easeInOut()
// Override the easing function for a specific component
componentEasing("h", EasingFunctions.linear())
stop(RGB("#00f")) {
// Override the easing function between this stop and the next
easing = EasingFunctions.midpoint(.25)
}
stop(RGB("#fff"))
stop(RGB("#000"))
}
Component adjustment¶
You can make adjustments to the values of a component prior to interpolation. The CSS standard calls this a “fixup”.
Alpha adjustment¶
By default, if any color stops have an alpha value specified, any other stop with an unspecified alpha will have their alphas set to 1.
Hue adjustment¶
When interpolating in a cylindrical space like LCHab, there are multiple ways to interpolate the hue (do you travel clockwise or counterclockwise around the hue circle?). You can pick a strategy from HueAdjustments, which contains all the methods defined in the CSS standard. By default, HueAdjustments.shorter is used.
LCHab.interpolator {
// set the adjustment for the hue
componentAdjustment("h", HueAdjustments.longer)
// disable the default alpha adjustment
componentAdjustment("alpha") { it }
// ...
}
Chromatic adaptation¶
When converting between color spaces that use different white points, the color is automatically adapted using Von Kries’ method with the CIECAM02 CAT matrix. If you’d like to perform chromatic adaptation using a different matrix (such as Bradford’s), you can convert the color to XYZ and use adaptTo.
val bradfords = floatArrayOf(
0.8951f, 0.2664f, -0.1614f,
-0.7502f, 1.7135f, 0.0367f,
0.0389f, -0.0685f, 1.0296f,
)
// adapt this color to LAB with a D50 whitepoint using bradford's matrix
RGB("#f3a").toXYZ().adaptTo(XYZ50, bradfords).toLAB()
// LAB50(l=59.029217, a=79.97541, b=-14.047905)
If you want to adapt multiple colors at once based on a source white color, you can use createChromaticAdapter.
val sourceWhite : Color = bitmap.getWhitestPixel()
val adapter = RGBInt.createChromaticAdapter(sourceWhite)
val pixels : IntArray = bitmap.getArgbPixels()
adapter.adaptAll(pixels)
bitmap.setPixels(pixels)
Parsing color strings¶
You can create a Color
instance from any CSS color string using parse and parseOrNull.
Color.parse("red") // RGB(r=1.0, g=0.0, b=0.0)
Color.parse("rgb(51 102 51 / 40%)") // RGB(r=0.2, g=0.4, b=0.2, alpha=0.4)
Color.parse("hwb(200grad 20% 45%)") // HWB(h=180.0, w=0.2, b=0.45)
Rendering colors as strings¶
You can also render any Color
as a CSS color string with formatCssString. Colormath
supports more color spaces than CSS, so formatting formatCssString
will produce a color()
style
string with a dashed identifier name based on the color space.
You can also use formatCssStringOrNull which will return null when called on a color space that isn’t built in to CSS.
To render a color as a hex string, convert it to sRGB and use toHex.
RGB(.2, 0, 1, alpha = .5).formatCssString() // "rgb(51 0 255 / 0.5)"
LCHab50(50, 10, 180).formatCssString() // "lch(50% 10 180)"
ROMM_RGB(.1, .2, .4).formatCssString() // "color(prophoto-rgb 0.1 0.2 0.4)"
RGB(.2, .4, .6).toHex() // "#336699"
Caution
The CSS lab
, lch
, and xyz
functions specify colors with the D50 illuminant. Colormath’s
default constructors for those color spaces use D65, so they will be subject to chromatic
adaptation before rendering. To avoid this, use the D50 versions of the constructors: LAB50,
LCHab50, and XYZ50
Why can a hue be NaN
?¶
Cylindrical color spaces like HSL
and HSV
represent their hue as an angle in a hue circle.
But for monochrome colors like white and grey, the hue value is undefined. If the color is grey, we can use any value for the hue and the color will be unchanged.
HSV(0, 0, .5).toSRGB().toHex() // #808080
HSV(123, 0, .5).toSRGB().toHex() // #808080
So colormath sets the hue to NaN
when it’s undefined. Why not use a regular number like 0? Let’s say we want to make a
gradient from white to cyan.
0° hue is red, so if we use a value of 0 as the hue for white, the gradient will interpolate the hue from 0 to 180, sweeping through the hues from red to cyan:
HSV.interpolator {
stop(HSV(0, 0, 1))
stop(HSV(180, 1, 1))
}
If we instead use NaN
for the white hue, the gradient will keep the interpolated hue constant, resulting in the
gradient we expect:
HSV.interpolator {
stop(HSV(NaN, 0, 1))
stop(HSV(180, 1, 1))
}