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 decreasingContrast = scheme.contrastLevel < 0;

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

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

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

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

    // If a color is good enough, it is not adjusted.
    // Initial and adjusted tones for `nearer`
    final nInitialTone = nearer.tone(scheme);
    var nTone = Contrast.ratioOfTones(bgTone, nInitialTone) >= nContrast
        ? nInitialTone
        : DynamicColor.foregroundTone(bgTone, nContrast);
    // Initial and adjusted tones for `farther`
    final fInitialTone = farther.tone(scheme);
    var 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) {
        // 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);
        }
      } 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.
    var answer = this.tone(scheme);

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

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

    final desiredRatio = this.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 (this.isBackground && 50 <= answer && answer < 60) {
      // Must adjust
      if (Contrast.ratioOfTones(49, bgTone) >= desiredRatio) {
        answer = 49;
      } else {
        answer = 60;
      }
    }

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

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

      final upper = math.max(bgTone1, bgTone2);
      final 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 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 darkOption = Contrast.darker(tone: lower, ratio: desiredRatio);

      // Tones suitable for the foreground.
      final availables = [];
      if (lightOption != -1) availables.add(lightOption);
      if (darkOption != -1) availables.add(darkOption);

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

    return answer;
  }
}