getTone method

double getTone(
  1. DynamicScheme scheme
)

Return a tone, T in the HCT color space, that this DynamicColor is under the conditions in scheme.

scheme Defines the conditions of the user interface, for example, whether or not it is dark mode or light mode, and what the desired contrast level is.

Implementation

double getTone(DynamicScheme scheme) {
  final bool decreasingContrast = scheme.contrastLevel < 0;

  // Case 1: dual foreground, pair of colors with delta constraint.
  if (toneDeltaPair != null) {
    final ToneDeltaPair pair = toneDeltaPair!(scheme);
    final DynamicColor roleA = pair.roleA;
    final DynamicColor roleB = pair.roleB;
    final double delta = pair.delta;
    final TonePolarity polarity = pair.polarity;
    final bool stayTogether = pair.stayTogether;

    final DynamicColor bg = background!(scheme);
    final double bgTone = bg.getTone(scheme);

    final bool aIsNearer = polarity == TonePolarity.nearer ||
        (polarity == TonePolarity.lighter && !scheme.isDark) ||
        (polarity == TonePolarity.darker && scheme.isDark);
    final DynamicColor nearer = aIsNearer ? roleA : roleB;
    final DynamicColor farther = aIsNearer ? roleB : roleA;
    final bool amNearer = name == nearer.name;
    final int expansionDir = scheme.isDark ? 1 : -1;

    // 1st round: solve to min, each
    final double nContrast = nearer.contrastCurve!.get(scheme.contrastLevel);
    final double fContrast = farther.contrastCurve!.get(scheme.contrastLevel);

    // If a color is good enough, it is not adjusted.
    // Initial and adjusted tones for `nearer`
    final double nInitialTone = nearer.tone(scheme);
    double nTone = Contrast.ratioOfTones(bgTone, nInitialTone) >= nContrast
        ? nInitialTone
        : DynamicColor.foregroundTone(bgTone, nContrast);
    // Initial and adjusted tones for `farther`
    final double fInitialTone = farther.tone(scheme);
    double fTone = Contrast.ratioOfTones(bgTone, fInitialTone) >= fContrast
        ? fInitialTone
        : DynamicColor.foregroundTone(bgTone, fContrast);

    if (decreasingContrast) {
      // If decreasing contrast, adjust color to the "bare minimum"
      // that satisfies contrast.
      nTone = DynamicColor.foregroundTone(bgTone, nContrast);
      fTone = DynamicColor.foregroundTone(bgTone, fContrast);
    }

    if ((fTone - nTone) * expansionDir >= delta) {
      // Good! Tones satisfy the constraint; no change needed.
    } else {
      // 2nd round: expand farther to match delta.
      fTone = MathUtils.clampDouble(0, 100, nTone + delta * expansionDir);
      if ((fTone - nTone) * expansionDir >= delta) {
        // Good! Tones now satisfy the constraint; no change needed.
      } else {
        // 3rd round: contract nearer to match delta.
        nTone = MathUtils.clampDouble(0, 100, fTone - delta * expansionDir);
      }
    }

    // Avoids the 50-59 awkward zone.
    if (50 <= nTone && nTone < 60) {
      // If `nearer` is in the awkward zone, move it away, together with
      // `farther`.
      if (expansionDir > 0) {
        nTone = 60;
        fTone = math.max(fTone, nTone + delta * expansionDir);
      } else {
        nTone = 49;
        fTone = math.min(fTone, nTone + delta * expansionDir);
      }
    } else if (50 <= fTone && fTone < 60) {
      if (stayTogether) {
        // Rydmike: If MCU devs do not hit test this, I'm not going to either.
        // coverage:ignore-start
        // Fixes both, to avoid two colors on opposite sides of the "awkward
        // zone".
        if (expansionDir > 0) {
          nTone = 60;
          fTone = math.max(fTone, nTone + delta * expansionDir);
        } else {
          nTone = 49;
          fTone = math.min(fTone, nTone + delta * expansionDir);
        }
        // coverage:ignore-end
      } else {
        // Not required to stay together; fixes just one.
        if (expansionDir > 0) {
          fTone = 60;
        } else {
          fTone = 49;
        }
      }
    }

    // Returns `nTone` if this color is `nearer`, otherwise `fTone`.
    return amNearer ? nTone : fTone;
  } else {
    // Case 2: No contrast pair; just solve for itself.
    double answer = tone(scheme);

    if (background == null) {
      return answer; // No adjustment for colors with no background.
    }

    final double bgTone = background!(scheme).getTone(scheme);

    final double desiredRatio = contrastCurve!.get(scheme.contrastLevel);

    if (Contrast.ratioOfTones(bgTone, answer) >= desiredRatio) {
      // Don't "improve" what's good enough.
    } else {
      // Rough improvement.
      answer = DynamicColor.foregroundTone(bgTone, desiredRatio);
    }

    if (decreasingContrast) {
      answer = DynamicColor.foregroundTone(bgTone, desiredRatio);
    }

    if (isBackground && 50 <= answer && answer < 60) {
      // Rydmike: If MCU devs do not hit test this, I'm not going to either.
      // coverage:ignore-start
      // Must adjust
      if (Contrast.ratioOfTones(49, bgTone) >= desiredRatio) {
        answer = 49;
      } else {
        answer = 60;
      }
      // coverage:ignore-end
    }

    if (secondBackground != null) {
      // Case 3: Adjust for dual backgrounds.

      final double bgTone1 = background!(scheme).getTone(scheme);
      final double bgTone2 = secondBackground!(scheme).getTone(scheme);

      final double upper = math.max(bgTone1, bgTone2);
      final double lower = math.min(bgTone1, bgTone2);

      if (Contrast.ratioOfTones(upper, answer) >= desiredRatio &&
          Contrast.ratioOfTones(lower, answer) >= desiredRatio) {
        return answer;
      }

      // The darkest light tone that satisfies the desired ratio,
      // or -1 if such ratio cannot be reached.
      final double lightOption =
          Contrast.lighter(tone: upper, ratio: desiredRatio);

      // The lightest dark tone that satisfies the desired ratio,
      // or -1 if such ratio cannot be reached.
      final double darkOption =
          Contrast.darker(tone: lower, ratio: desiredRatio);

      // Tones suitable for the foreground.
      final List<double> availables = <double>[];
      if (lightOption != -1) availables.add(lightOption);
      if (darkOption != -1) availables.add(darkOption);
      // Rydmike: If MCU devs do not hit test this, I'm not going to either.
      // coverage:ignore-start
      final bool prefersLight =
          DynamicColor.tonePrefersLightForeground(bgTone1) ||
              DynamicColor.tonePrefersLightForeground(bgTone2);
      if (prefersLight) {
        return (lightOption < 0) ? 100 : lightOption;
      }
      if (availables.length == 1) {
        return availables[0];
      }
      return (darkOption < 0) ? 0 : darkOption;
      // coverage:ignore-end
    }

    return answer;
  }
}