solveToLinrgb static method
Solves for the linear RGB color with the given hue, chroma, and L* in
the specified gamut.
Returns (r, g, b) in linear RGB space 0..100 for the given gamut.
The hue and L* will be preserved; if the requested chroma exceeds what
the gamut can represent, it will be reduced to the gamut boundary.
To get display-encoded values in 0..1, use:
final (r, g, b) = HctSolver.solveToLinrgb(h, c, t, gamut: Gamut.displayP3);
final displayR = gamut.trueDelinearize(r) / 255.0;
Implementation
static (double, double, double) solveToLinrgb(
double hueDegrees,
double chroma,
double lstar, {
Gamut? gamut,
}) {
gamut ??= Gamut.srgb;
if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
// Achromatic: solve for the gray that matches L*
final y = yFromLstar(lstar);
// Find the linear component value that produces this Y in the gamut.
// For any gamut, Y = kR*R + kG*G + kB*B. For gray, R=G=B=v.
// So v = Y / (kR + kG + kB). Since the Y row should sum to 1.0
// for a D65 gamut, v = Y.
final kSum =
gamut.yFromLinrgb[0] + gamut.yFromLinrgb[1] + gamut.yFromLinrgb[2];
final v = y / kSum;
return (v, v, v);
}
hueDegrees = sanitizeDegreesDouble(hueDegrees);
final hueRadians = hueDegrees / 180 * pi;
final y = yFromLstar(lstar);
final exactAnswer = _findResultByJ(hueRadians, chroma, y, gamut);
if (exactAnswer != null) {
return exactAnswer;
}
// =========================================================================
// No-allocation pattern: nested functions share state via outer scope
// variables instead of allocating tuples/lists. This creates deep nesting
// (bisectToLimit -> bisectToSegment -> nthVertex) but avoids GC pressure.
// =========================================================================
/// Finds a color with the given Y and hue on the boundary of the
/// cube.
///
/// Returns the color with the desired Y value [y] and hue
/// [targetHue], in linear RGB coordinates.
(double, double, double) bisectToLimit(double y, double targetHue) {
var bisectToSegmentLeft0 = -1.0;
var bisectToSegmentLeft1 = -1.0;
var bisectToSegmentLeft2 = -1.0;
var bisectToSegmentRight0 = -1.0;
var bisectToSegmentRight1 = -1.0;
var bisectToSegmentRight2 = -1.0;
var bisectToSegmentMid0 = -1.0;
var bisectToSegmentMid1 = -1.0;
var bisectToSegmentMid2 = -1.0;
/// Finds the segment containing the desired color.
///
/// Given a plane Y = [y] and a desired [target_hue], returns the
/// segment containing the desired color, represented as an array of
/// its two endpoints.
void bisectToSegment(double y, double targetHue) {
/// Returns the nth possible vertex of the polygonal intersection.
///
/// Given a plane Y = [y] and an zero-based index [n] such that 0
/// <= [n] <= 11, returns the nth possible vertex of the polygonal
/// intersection of the plane and the RGB cube, in linear RGB
/// coordinates, if it exists.
/// If this possible vertex lies outside of the cube, [-1.0, -1.0,
/// -1.0] is returned.
void nthVertex(double y, int n) {
final kR = gamut!.yFromLinrgb[0];
final kG = gamut.yFromLinrgb[1];
final kB = gamut.yFromLinrgb[2];
final coordA = n % 4 <= 1 ? 0.0 : 100.0;
final coordB = n % 2 == 0 ? 0.0 : 100.0;
if (n < 4) {
final g = coordA;
final b = coordB;
final r = (y - g * kG - b * kB) / kR;
if (_isBounded(r)) {
bisectToSegmentMid0 = r;
bisectToSegmentMid1 = g;
bisectToSegmentMid2 = b;
} else {
bisectToSegmentMid0 = -1.0;
bisectToSegmentMid1 = -1.0;
bisectToSegmentMid2 = -1.0;
}
} else if (n < 8) {
final b = coordA;
final r = coordB;
final g = (y - r * kR - b * kB) / kG;
if (_isBounded(g)) {
bisectToSegmentMid0 = r;
bisectToSegmentMid1 = g;
bisectToSegmentMid2 = b;
} else {
bisectToSegmentMid0 = -1.0;
bisectToSegmentMid1 = -1.0;
bisectToSegmentMid2 = -1.0;
}
} else {
final r = coordA;
final g = coordB;
final b = (y - r * kR - g * kG) / kB;
if (_isBounded(b)) {
bisectToSegmentMid0 = r;
bisectToSegmentMid1 = g;
bisectToSegmentMid2 = b;
} else {
bisectToSegmentMid0 = -1.0;
bisectToSegmentMid1 = -1.0;
bisectToSegmentMid2 = -1.0;
}
}
}
var leftHue = 0.0;
var rightHue = 0.0;
var initialized = false;
var uncut = true;
for (var n = 0; n < 12; n++) {
nthVertex(y, n);
if (bisectToSegmentMid0 < 0) {
continue;
}
final midHue = _hueOfComponents(
bisectToSegmentMid0,
bisectToSegmentMid1,
bisectToSegmentMid2,
gamut!,
);
if (!initialized) {
bisectToSegmentLeft0 = bisectToSegmentMid0;
bisectToSegmentLeft1 = bisectToSegmentMid1;
bisectToSegmentLeft2 = bisectToSegmentMid2;
bisectToSegmentRight0 = bisectToSegmentMid0;
bisectToSegmentRight1 = bisectToSegmentMid1;
bisectToSegmentRight2 = bisectToSegmentMid2;
leftHue = midHue;
rightHue = midHue;
initialized = true;
continue;
}
if (uncut || _areInCyclicOrder(leftHue, midHue, rightHue)) {
uncut = false;
if (_areInCyclicOrder(leftHue, targetHue, midHue)) {
bisectToSegmentRight0 = bisectToSegmentMid0;
bisectToSegmentRight1 = bisectToSegmentMid1;
bisectToSegmentRight2 = bisectToSegmentMid2;
rightHue = midHue;
} else {
bisectToSegmentLeft0 = bisectToSegmentMid0;
bisectToSegmentLeft1 = bisectToSegmentMid1;
bisectToSegmentLeft2 = bisectToSegmentMid2;
leftHue = midHue;
}
}
}
}
bisectToSegment(y, targetHue);
var bisectToLimitLeft0 = bisectToSegmentLeft0;
var bisectToLimitLeft1 = bisectToSegmentLeft1;
var bisectToLimitLeft2 = bisectToSegmentLeft2;
var leftHue = _hueOfComponents(
bisectToLimitLeft0,
bisectToLimitLeft1,
bisectToLimitLeft2,
gamut!,
);
var bisectToLimitRight0 = bisectToSegmentRight0;
var bisectToLimitRight1 = bisectToSegmentRight1;
var bisectToLimitRight2 = bisectToSegmentRight2;
var bisectToLimitMid0 = -1.0;
var bisectToLimitMid1 = -1.0;
var bisectToLimitMid2 = -1.0;
for (var axis = 0; axis < 3; axis++) {
// The default cases below are unreachable since axis is always 0, 1, or 2.
// StateError is thrown (rather than returning a dummy value) to fail fast
// if the loop bounds ever change, satisfying both exhaustiveness checking
// and defensive programming practices.
var leftForAxis = switch (axis) {
0 => bisectToLimitLeft0,
1 => bisectToLimitLeft1,
2 => bisectToLimitLeft2,
_ => throw StateError('unreachable: axis must be 0, 1, or 2'),
};
var rightForAxis = switch (axis) {
0 => bisectToLimitRight0,
1 => bisectToLimitRight1,
2 => bisectToLimitRight2,
_ => throw StateError('unreachable: axis must be 0, 1, or 2'),
};
if (leftForAxis != rightForAxis) {
var lPlane = -1;
var rPlane = 255;
if (leftForAxis < rightForAxis) {
lPlane = _criticalPlaneBelow(gamut.trueDelinearize(leftForAxis));
rPlane = _criticalPlaneAbove(gamut.trueDelinearize(rightForAxis));
} else {
lPlane = _criticalPlaneAbove(gamut.trueDelinearize(leftForAxis));
rPlane = _criticalPlaneBelow(gamut.trueDelinearize(rightForAxis));
}
for (var i = 0; i < 8; i++) {
if ((rPlane - lPlane).abs() <= 1) {
break;
} else {
final mPlane = ((lPlane + rPlane) / 2.0).floor();
final midPlaneCoordinate = gamut.criticalPlanes[mPlane];
void setCoordinate2(
int axis,
double sourceAxisValue,
double coordinate,
double targetAxisValue,
) {
double lerpPoint(double source, double t, double target) {
return source + (target - source) * t;
}
final t = _intercept(
sourceAxisValue,
coordinate,
targetAxisValue,
);
if (axis == 0) {
bisectToLimitMid0 = lerpPoint(
bisectToLimitLeft0,
t,
bisectToLimitRight0,
);
} else if (axis == 1) {
bisectToLimitMid1 = lerpPoint(
bisectToLimitLeft1,
t,
bisectToLimitRight1,
);
} else if (axis == 2) {
bisectToLimitMid2 = lerpPoint(
bisectToLimitLeft2,
t,
bisectToLimitRight2,
);
}
}
for (var axis2 = 0; axis2 < 3; axis2++) {
setCoordinate2(
axis2,
leftForAxis,
midPlaneCoordinate,
rightForAxis,
);
}
final midHue = _hueOfComponents(
bisectToLimitMid0,
bisectToLimitMid1,
bisectToLimitMid2,
gamut,
);
if (_areInCyclicOrder(leftHue, targetHue, midHue)) {
bisectToLimitRight0 = bisectToLimitMid0;
bisectToLimitRight1 = bisectToLimitMid1;
bisectToLimitRight2 = bisectToLimitMid2;
rPlane = mPlane;
} else {
bisectToLimitLeft0 = bisectToLimitMid0;
bisectToLimitLeft1 = bisectToLimitMid1;
bisectToLimitLeft2 = bisectToLimitMid2;
leftHue = midHue;
lPlane = mPlane;
}
}
}
}
}
final r = (bisectToLimitLeft0 + bisectToLimitRight0) / 2;
final g = (bisectToLimitLeft1 + bisectToLimitRight1) / 2;
final b = (bisectToLimitLeft2 + bisectToLimitRight2) / 2;
return (r, g, b);
}
return bisectToLimit(y, hueRadians);
}