solveToLinrgb static method

(double, double, double) solveToLinrgb(
  1. double hueDegrees,
  2. double chroma,
  3. double lstar, {
  4. Gamut? gamut,
})

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);
}