1#include "QskHctColor.h"
33 constexpr XYZ() noexcept = default;
35 constexpr XYZ(
double x,
double y,
double z ) noexcept
42 double value(
int axis )
const
62 class ViewingConditions
66 const double backgroundYTowhitePointY = 0.18418651851244416;
68 const double aw = 29.980997194447337;
69 const double nbb = 1.0169191804458755;
70 const double z = 1.909169568483652;
71 const double fl = 0.38848145378003529;
73 const XYZ rgbD = { 1.02117770275752, 0.98630772942801237, 0.93396050828022992 };
77static const XYZ Y_FROM_LINRGB = { 0.2126, 0.7152, 0.0722 };
79static double planes[] =
338static inline int delinearized(
double rgbComponent )
340 const double normalized = rgbComponent / 100.0;
344 if ( normalized <= 0.0031308 )
345 v = normalized * 12.92;
347 v = 1.055 * pow( normalized, 1.0 / 2.4 ) - 0.055;
349 return qBound( 0, qRound( v * 255 ), 255 );
352static inline double labF(
double t )
354 constexpr double e = 216.0 / 24389.0;
358 return pow( t, 1.0 / 3.0 );
362 constexpr double kappa = 24389.0 / 27.0;
363 return ( kappa * t + 16 ) / 116;
367static inline double labInvf(
double ft )
369 const double e = 216.0 / 24389.0;
370 const double kappa = 24389.0 / 27.0;
371 const double ft3 = ft * ft * ft;
376 return ( 116 * ft - 16 ) / kappa;
379static inline double sanitizeDegreesDouble(
double degrees )
381 degrees = fmod( degrees, 360.0 );
384 degrees = degrees + 360.0;
389static inline double yFromLstar(
double lstar )
391 return 100.0 * labInvf( ( lstar + 16.0 ) / 116.0 );
394static inline QRgb argbFromLstar(
double lstar )
396 const double y = yFromLstar(lstar);
397 const int component = delinearized(y);
399 return qRgb( component, component, component );
402static inline QRgb argbFromLinrgb(
const XYZ& linrgb )
404 const int r = delinearized( linrgb.x );
405 const int g = delinearized( linrgb.y );
406 const int b = delinearized( linrgb.z );
408 return qRgb( r, g, b );
411static inline double sanitizeRadians(
double angle )
413 return fmod( angle + M_PI * 8, M_PI * 2 );
416static inline double trueDelinearized(
double rgbComponent )
418 const double normalized = rgbComponent / 100.0;
421 if ( normalized <= 0.0031308 )
422 v = normalized * 12.92;
424 v = 1.055 * pow( normalized, 1.0 / 2.4 ) - 0.055;
429static inline int signum(
double num )
434 return ( num < 0 ) ? -1 : 1;
437static inline XYZ matrixMultiply(
const XYZ& row,
const XYZ matrix[3] )
441 r.x = row.x * matrix[0].x + row.y * matrix[0].y + row.z * matrix[0].z;
442 r.y = row.x * matrix[1].x + row.y * matrix[1].y + row.z * matrix[1].z;
443 r.z = row.x * matrix[2].x + row.y * matrix[2].y + row.z * matrix[2].z;
448static inline double chromaticAdaptation(
double component )
450 const double af = pow( abs( component ), 0.42);
451 return signum(component) * 400.0 * af / ( af + 27.13 );
454static inline double hueOf(
const XYZ& linrgb )
456 constexpr XYZ matrix[3] =
458 { 0.001200833568784504, 0.002389694492170889, 0.0002795742885861124 },
459 { 0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398 },
460 { 0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076 }
463 const XYZ scaledDiscount = matrixMultiply( linrgb, matrix );
465 const double rA = chromaticAdaptation( scaledDiscount.x );
466 const double gA = chromaticAdaptation( scaledDiscount.y );
467 const double bA = chromaticAdaptation( scaledDiscount.z );
469 const double a = ( 11.0 * rA + -12.0 * gA + bA ) / 11.0;
470 const double b = ( rA + gA - 2.0 * bA ) / 9.0;
472 return atan2( b, a );
475static bool areInCyclicOrder(
double a,
double b,
double c )
477 const double deltaAB = sanitizeRadians( b - a );
478 const double deltaAC = sanitizeRadians( c - a );
480 return deltaAB < deltaAC;
483static inline double intercept(
double source,
double mid,
double target )
485 return ( mid - source ) / ( target - source );
488static inline XYZ setCoordinate(
const XYZ& source,
489 double coordinate,
const XYZ& target,
int axis )
491 const double t = intercept( source.value(axis), coordinate, target.value(axis) );
495 r.x = source.x + ( target.x - source.x ) * t;
496 r.y = source.y + ( target.y - source.y ) * t;
497 r.z = source.z + ( target.z - source.z ) * t;
502static bool isBounded(
double x )
504 return 0.0 <= x && x <= 100.0;
507static XYZ nthVertex(
double y,
int n )
509 const double kR = Y_FROM_LINRGB.x;
510 const double kG = Y_FROM_LINRGB.y;
511 const double kB = Y_FROM_LINRGB.z;
513 const double coordA = ( n % 4 <= 1 ) ? 0.0 : 100.0;
514 const double coordB = ( n % 2 == 0 ) ? 0.0 : 100.0;
518 const double g = coordA;
519 const double b = coordB;
520 const double r = ( y - g * kG - b * kB ) / kR;
523 return XYZ( r, g, b );
527 const double b = coordA;
528 const double r = coordB;
529 const double g = ( y - r * kR - b * kB ) / kG;
532 return XYZ( r, g, b );
536 const double r = coordA;
537 const double g = coordB;
538 const double b = ( y - r * kR - g * kG ) / kB;
541 return XYZ( r, g, b );
544 return { -1.0, -1.0, -1.0 };
547static void bisectToSegment(
double y,
double targetHue, XYZ& left, XYZ& right )
549 left = { -1.0, -1.0, -1.0 };
552 double leftHue = 0.0;
553 double rightHue = 0.0;
555 bool initialized =
false;
558 for (
int n = 0; n < 12; n++ )
560 XYZ mid = nthVertex(y, n);
564 const double midHue = hueOf( mid );
576 if ( uncut || areInCyclicOrder( leftHue, midHue, rightHue ) )
580 if ( areInCyclicOrder( leftHue, targetHue, midHue ) )
594static XYZ midpoint(
const XYZ& a,
const XYZ& b )
597 r.x = ( a.x + b.x ) / 2;
598 r.y = ( a.y + b.y ) / 2;
599 r.z = ( a.z + b.z ) / 2;
604static int planeBelow(
double x )
606 return qFloor( x - 0.5 );
609static int planeAbove(
double x )
611 return qCeil( x - 0.5 );
614static XYZ bisectToLimit(
double y,
double targetHue )
617 bisectToSegment( y, targetHue, left, right );
619 double leftHue = hueOf(left);
621 for (
int axis = 0; axis < 3; axis++ )
623 const double l = left.value(axis);
624 const double r = right.value(axis);
633 lPlane = planeBelow( trueDelinearized( l ) );
634 rPlane = planeAbove( trueDelinearized( r ) );
638 lPlane = planeAbove( trueDelinearized( l ) );
639 rPlane = planeBelow( trueDelinearized( r ) );
642 for (
int i = 0; i < 8; i++ )
644 if ( abs( rPlane - lPlane ) <= 1 )
647 const int mPlane = qFloor( ( lPlane + rPlane ) / 2.0 );
648 const double midPlaneCoordinate = planes[mPlane];
650 const XYZ mid = setCoordinate(left, midPlaneCoordinate, right, axis);
651 const double midHue = hueOf( mid );
653 if ( areInCyclicOrder( leftHue, targetHue, midHue ) )
668 return midpoint( left, right );
671static double inverseChromaticAdaptation(
double adapted )
673 const double adaptedAbs = abs( adapted );
675 double base = 27.13 * adaptedAbs / ( 400.0 - adaptedAbs );
679 return signum(adapted) * pow( base, 1.0 / 0.42 );
682static QRgb findResultByJ(
double hueRadians,
double chroma,
double y )
684 double j = sqrt(y) * 11.0;
686 constexpr ViewingConditions vc;
688 const double tInnerCoeff = 1.0 / pow( 1.64 - pow( 0.29, vc.backgroundYTowhitePointY ), 0.73 );
689 const double eHue = 0.25 * ( cos( hueRadians + 2.0 ) + 3.8 );
690 const double p1 = eHue * ( 50000.0 / 13.0 ) * vc.nbb;
691 const double hSin = sin(hueRadians);
692 const double hCos = cos(hueRadians);
694 for (
int i = 0; i < 5; i++ )
696 const double jNormalized = j / 100.0;
697 const double alpha = ( chroma == 0.0 || j == 0.0 ) ? 0.0 : chroma / sqrt(jNormalized);
698 const double t = pow( alpha * tInnerCoeff, 1.0 / 0.9 );
699 const double ac = vc.aw * pow( jNormalized, 1.0 / 0.69 / vc.z );
700 const double p2 = ac / vc.nbb;
702 const double gamma = 23.0 * ( p2 + 0.305 ) * t /
703 ( 23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin );
704 const double a = gamma * hCos;
705 const double b = gamma * hSin;
707 const double rA = ( 460.0 * p2 + 451.0 * a + 288.0 * b ) / 1403.0;
708 const double gA = ( 460.0 * p2 - 891.0 * a - 261.0 * b ) / 1403.0;
709 const double bA = ( 460.0 * p2 - 220.0 * a - 6300.0 * b ) / 1403.0;
712 rgbScaled.x = inverseChromaticAdaptation( rA );
713 rgbScaled.y = inverseChromaticAdaptation( gA );
714 rgbScaled.z = inverseChromaticAdaptation( bA );
716 constexpr XYZ matrix[3] =
718 { 1373.2198709594231, -1100.4251190754821, -7.278681089101213, },
719 { -271.815969077903, 559.6580465940733, -32.46047482791194 },
720 { 1.9622899599665666, -57.173814538844006, 308.7233197812385 }
723 const XYZ linrgb = matrixMultiply( rgbScaled, matrix );
725 if ( linrgb.x < 0 || linrgb.y < 0 || linrgb.z < 0 )
728 const double kR = Y_FROM_LINRGB.x;
729 const double kG = Y_FROM_LINRGB.y;
730 const double kB = Y_FROM_LINRGB.z;
732 const double fnj = kR * linrgb.x + kG * linrgb.y + kB * linrgb.z;
736 if ( i == 4 || abs(fnj - y) < 0.002 )
738 if ( linrgb.x > 100.01 || linrgb.y > 100.01 || linrgb.z > 100.01 )
741 return argbFromLinrgb( linrgb );
744 j = j - ( fnj - y ) * j / ( 2 * fnj );
750static inline QRgb getRgb(
double hue,
double chroma,
double tone )
752 if ( chroma < 0.0001 || tone < 0.0001 || tone > 99.9999 )
753 return argbFromLstar( tone );
755 hue = sanitizeDegreesDouble( hue );
757 const double hueRadians = hue / 180.0 * M_PI;
758 const double y = yFromLstar( tone );
760 const QRgb rgb = findResultByJ( hueRadians, chroma, y );
764 const XYZ linrgb = bisectToLimit( y, hueRadians );
765 return argbFromLinrgb( linrgb );
768static const XYZ SRGB_TO_XYZ[3] =
770 { 0.41233895, 0.35762064, 0.18051042 },
771 { 0.2126, 0.7152, 0.0722 },
772 { 0.01932141, 0.11916382, 0.95034478 }
775static double linearized(
int rgbComponent )
777 const double normalized = rgbComponent / 255.0;
779 if ( normalized <= 0.040449936 )
780 return normalized / 12.92 * 100.0;
782 return pow( ( normalized + 0.055 ) / 1.055, 2.4) * 100.0;
785static XYZ xyzFromArgb( QRgb rgb)
789 xyz.x = linearized( qRed(rgb) );
790 xyz.y = linearized( qGreen(rgb) );
791 xyz.z = linearized( qBlue(rgb) );
793 return matrixMultiply( xyz, SRGB_TO_XYZ );
796static void getHTC( QRgb rgb,
double& hue,
double& chroma,
double& tone )
798 ViewingConditions vc;
800 const XYZ xyz = xyzFromArgb( rgb );
802 const double x = xyz.x;
803 const double y = xyz.y;
804 const double z = xyz.z;
806 const double rC = 0.401288 * x + 0.650173 * y - 0.051461 * z;
807 const double gC = -0.250268 * x + 1.204414 * y + 0.045854 * z;
808 const double bC = -0.002079 * x + 0.048952 * y + 0.953127 * z;
810 const double rD = vc.rgbD.x * rC;
811 const double gD = vc.rgbD.y * gC;
812 const double bD = vc.rgbD.z * bC;
814 const double rAF = pow( vc.fl * fabs( rD ) / 100.0, 0.42 );
815 const double gAF = pow( vc.fl * fabs( gD ) / 100.0, 0.42 );
817 const double bAF = pow( vc.fl * fabs( bD ) / 100.0, 0.42);
818 const double rA = signum(rD) * 400.0 * rAF / ( rAF + 27.13 );
819 const double gA = signum(gD) * 400.0 * gAF / ( gAF + 27.13 );
820 const double bA = signum(bD) * 400.0 * bAF / ( bAF + 27.13 );
822 const double a = ( 11.0 * rA + -12.0 * gA + bA ) / 11.0;
823 const double b = ( rA + gA - 2.0 * bA ) / 9.0;
824 const double u = ( 20.0 * rA + 20.0 * gA + 21.0 * bA ) / 20.0;
826 const double p2 = ( 40.0 * rA + 20.0 * gA + bA ) / 20.0;
829 const double atanDegrees = atan2(b, a) * 180.0 / M_PI;
832 hue = ( atanDegrees < 0 ) ? atanDegrees + 360.0 : atanDegrees >=
833 360 ? atanDegrees - 360 : atanDegrees;
836 const double ac = p2 * vc.nbb;
838 const double J = 100.0 * pow( ac / vc.aw, 0.69 * vc.z );
840 const double huePrime = ( hue < 20.14 ) ? hue + 360 : hue;
841 const double eHue = ( 1.0 / 4.0 ) * ( cos( huePrime * M_PI / 180.0 + 2.0 ) + 3.8 );
842 const double p1 = 50000.0 / 13.0 * eHue * vc.nbb;
843 const double t = p1 * sqrt(a * a + b * b) / ( u + 0.305 );
846 pow(t, 0.9) * pow( 1.64 - pow(0.29, vc.backgroundYTowhitePointY), 0.73);
848 chroma = alpha * sqrt(J / 100.0);
852 tone = 116.0 * labF( y / 100.0 ) - 16.0;
856QskHctColor::QskHctColor( QRgb rgb )
858 getHTC( rgb, m_hue, m_chroma, m_tone );
861void QskHctColor::setHue( qreal hue )
863 m_hue = fmod( hue, 360.0 );
868void QskHctColor::setChroma( qreal chroma )
noexcept
870 m_chroma = ( chroma < 0.0 ) ? 0.0 : chroma;
873void QskHctColor::setTone( qreal tone )
noexcept
875 m_tone = qBound( 0.0, tone, 100.0 );
878void QskHctColor::setRgb( QRgb rgb )
880 getHTC( rgb, m_hue, m_chroma, m_tone );
883QRgb QskHctColor::rgb()
const
885 return getRgb( m_hue, m_chroma, m_tone );
888#ifndef QT_NO_DEBUG_STREAM
892QDebug operator<<( QDebug debug,
const QskHctColor& color )
894 debug.nospace() <<
"HTC("
895 << color.hue() <<
"," << color.chroma() <<
"," << color.tone() <<
")";
897 return debug.space();