diff --git a/colour.cpp b/colour.cpp index acc55084..22d4df07 100644 --- a/colour.cpp +++ b/colour.cpp @@ -3,13 +3,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * - * Copyright 2010-2017 Danny Robson + * Copyright 2010-2021 Danny Robson */ #include "colour.hpp" #include "ascii.hpp" #include "parse/value.hpp" +#include "debug/assert.hpp" + +using cruft::colour; +using cruft::srgba; /////////////////////////////////////////////////////////////////////////////// @@ -48,4 +52,156 @@ cruft::srgba4f cruft::parse::value (cruft::view &str) { return parse_hex (str); +} + + +/////////////////////////////////////////////////////////////////////////////// +/// Invert the sRGB gamma function +static constexpr float +linearise (float const u) +{ + if (u <= 0.04045f) + return u / 12.92f; + else + return std::pow ((u + 0.055f) / 1.055f, 2.4f); +} + + +///---------------------------------------------------------------------------- +/// Apply the sRGB gamma function +static constexpr float +exponentiate (float const u) +{ + if (u <= 0.0031308f) + return 12.92f * u; + else + return 1.055f * std::pow (u, 1/2.4f) - 0.055f; +} + + +///---------------------------------------------------------------------------- +/// Convert an sRGB colour to XYZ. +/// +/// Gamma will be removed during the conversion. Do not use with linear RGB. +static cruft::vector3f +sRGB_to_XYZ (cruft::vector3f sRGB) +{ + auto const [r_, g_, b_] = sRGB; + + float const r = linearise (r_); + float const g = linearise (g_); + float const b = linearise (b_); + + // D65 whitepoint, 2° observer. + // + // We use higher precision values than the specification dictates, from + // Bruce Lindbloom's reference, so as to minimise numerical errors when + // chaining conversions. + float const X = r * 0.4124564f + g * 0.3575761f + b * 0.1804375f; + float const Y = r * 0.2126729f + g * 0.7151522f + b * 0.0721750f; + float const Z = r * 0.0193339f + g * 0.1191920f + b * 0.9503041f; + + return {X, Y, Z}; +} + + +///---------------------------------------------------------------------------- +/// Convert an XYZ colour to sRGB. +/// +/// Gamma will be applied during the conversion. +/// +/// Out of range colours will be returned as is. It is the caller's +/// responsibility to process these as desired. +static cruft::vector3f +XYZ_to_sRGB (cruft::vector3f XYZ) +{ + auto const [X, Y, Z] = XYZ; + + float const r = +3.2404542f * X -1.5371385f * Y -0.4985314f * Z; + float const g = -0.9692660f * X +1.8760108f * Y +0.0415560f * Z; + float const b = +0.0556434f * X -0.2040259f * Y +1.0572252f * Z; + + return { + exponentiate (r), + exponentiate (g), + exponentiate (b), + }; +} + + +///---------------------------------------------------------------------------- +/// Convert an XYZ colour to xyY +static +cruft::vector3f +XYZ_to_xyY (cruft::vector3f XYZ) +{ + auto const [X, Y, Z] = XYZ; + + if (cruft::abs (X + Y + Z) < 1e-5f) { + // Use the D65 xy if we're at black. + return { + 0.31271f, + 0.32902f, + Y + }; + } else { + return { + X / (X + Y + Z), + Y / (X + Y + Z), + Y, + }; + } +} + + +///---------------------------------------------------------------------------- +/// Convert an xyY colour to XYZ +static cruft::vector3f +xyY_to_XYZ (cruft::vector3f xyY) +{ + auto const [x, y, Y] = xyY; + if (cruft::abs (y) < 1e-5f) { + return {0, 0, 0}; + } else { + return { + x * Y / y, + Y, + (1 - x - y) * Y / y, + }; + } +} + + +//----------------------------------------------------------------------------- +cruft::srgba3f +cruft::scale_luminance (cruft::srgba3f sRGB, float f) +{ + // We can't scale sRGB directly. Given we probably have to convert to other + // spaces via XYZ anyway we may as well scale the luminance value in xyY. + // + // The conversion chain is thus: + // * sRBB -> XYZ -> xyY -> scaling -> XYZ -> sRGB + + auto const XYZ0= sRGB_to_XYZ (sRGB.as ()); + auto const xyY0= XYZ_to_xyY (XYZ0); + + auto const xyY1 = xyY0 * cruft::vector3f{1,1,f}; + auto const XYZ1 = xyY_to_XYZ (xyY1); + return XYZ_to_sRGB (XYZ1).as (); +} + + +//----------------------------------------------------------------------------- +cruft::srgba4f +cruft::scale_luminance (cruft::srgba4f sRGBA, float f) +{ + auto const sRGB0 = sRGBA.indices<0,1,2> (); + auto const sRGB1 = scale_luminance (sRGB0, f); + + return { + sRGB1.r, + sRGB1.g, + sRGB1.b, + sRGBA.a, + }; } \ No newline at end of file diff --git a/colour.hpp b/colour.hpp index e1d2120e..ff916fbd 100644 --- a/colour.hpp +++ b/colour.hpp @@ -84,6 +84,9 @@ namespace cruft { using srgba3u = srgba<3,uint8_t>; using srgba4u = srgba<4,uint8_t>; + srgba3f scale_luminance (srgba3f, float factor); + srgba4f scale_luminance (srgba4f, float factor); + template struct hsva : colour> {}; template diff --git a/test/colour.cpp b/test/colour.cpp index 187d50c2..3afed11b 100644 --- a/test/colour.cpp +++ b/test/colour.cpp @@ -16,5 +16,15 @@ main (int, char**) tap.expect_eq (u.cast (), f, "cast u8 to float"); } + { + cruft::srgba3f white {1, 1, 1}; + cruft::srgba3f black {0, 0, 0}; + cruft::srgba3f percent_50 { 0.735359f, 0.735356f, 0.735357f }; + + tap.expect (max (scale_luminance (white, 1.f) - white) < 1e-5f, "unit luminance scaling for white"); + tap.expect (max (scale_luminance (white, 0.f) - black) < 1e-5f, "zero luminance scaling for white"); + tap.expect (max (scale_luminance (white, .5f) - percent_50) < 1e-5f, "half luminance scaling for white"); + } + return tap.status (); }