getTone method

  1. @override
double getTone(
  1. DynamicScheme scheme,
  2. DynamicColor color
)
override

Implementation

@override
double getTone(DynamicScheme scheme, DynamicColor color) {
  final decreasingContrast = scheme.contrastLevel < 0;
  final toneDeltaPair = color.toneDeltaPair?.call(scheme);

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

    final aIsNearer =
        (toneDeltaPair.constraint == .nearer ||
        (polarity == .lighter && !scheme.isDark) ||
        (polarity == .darker && !scheme.isDark));
    final nearer = aIsNearer ? roleA : roleB;
    final farther = aIsNearer ? roleB : roleA;
    final amNearer = color.name == nearer.name;
    final expansionDir = scheme.isDark ? 1 : -1;
    var nTone = nearer.tone(scheme);
    var fTone = farther.tone(scheme);

    // 1st round: solve to min, each
    if (color.background != null &&
        nearer.contrastCurve != null &&
        farther.contrastCurve != null) {
      final bg = color.background?.call(scheme);
      final nContrastCurve = nearer.contrastCurve?.call(scheme);
      final fContrastCurve = farther.contrastCurve?.call(scheme);
      if (bg != null && nContrastCurve != null && fContrastCurve != null) {
        final nContrast = nContrastCurve.get(scheme.contrastLevel);
        final fContrast = fContrastCurve.get(scheme.contrastLevel);
        final bgTone = bg.getTone(scheme);

        // If a color is good enough, it is not adjusted.
        // Initial and adjusted tones for `nearer`
        if (Contrast.ratioOfTones(bgTone, nTone) < nContrast) {
          nTone = DynamicColor.foregroundTone(bgTone, nContrast);
        }
        // Initial and adjusted tones for `farther`
        if (Contrast.ratioOfTones(bgTone, fTone) < fContrast) {
          fTone = 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 constraint is not satisfied, try another round.
    if ((fTone - nTone) * expansionDir < delta) {
      // 2nd round: expand farther to match delta.
      fTone = MathUtils.clampDouble(0.0, 100.0, nTone + delta * expansionDir);
      // If constraint is not satisfied, try another round.
      if ((fTone - nTone) * expansionDir < delta) {
        // 3rd round: contract nearer to match delta.
        nTone = MathUtils.clampDouble(
          0.0,
          100.0,
          fTone - delta * expansionDir,
        );
      }
    }

    // Avoids the 50-59 awkward zone.
    if (50.0 <= nTone && nTone < 60.0) {
      // If `nearer` is in the awkward zone, move it away, together with
      // `farther`.
      if (expansionDir > 0.0) {
        nTone = 60.0;
        fTone = math.max(fTone, nTone + delta * expansionDir);
      } else {
        nTone = 49.0;
        fTone = math.min(fTone, nTone + delta * expansionDir);
      }
    } else if (50.0 <= fTone && fTone < 60.0) {
      if (stayTogether) {
        // Fixes both, to avoid two colors on opposite sides of the "awkward
        // zone".
        if (expansionDir > 0) {
          nTone = 60.0;
          fTone = math.max(fTone, nTone + delta * expansionDir);
        } else {
          nTone = 49.0;
          fTone = math.min(fTone, nTone + delta * expansionDir);
        }
      } else {
        // Not required to stay together; fixes just one.
        if (expansionDir > 0) {
          fTone = 60.0;
        } else {
          fTone = 49.0;
        }
      }
    }

    // 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 = color.tone(scheme);

    if (color.background?.call(scheme) == null ||
        color.contrastCurve?.call(scheme) == null) {
      return answer; // No adjustment for colors with no background.
    }

    final bgTone = color.background!(scheme)!.getTone(scheme);
    final desiredRatio = color.contrastCurve!(scheme)!.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 (color.isBackground && 50.0 <= answer && answer < 60.0) {
      // Must adjust
      if (Contrast.ratioOfTones(49.0, bgTone) >= desiredRatio) {
        answer = 49.0;
      } else {
        answer = 60.0;
      }
    }

    if (color.secondBackground?.call(scheme) == null) {
      return answer;
    }

    // Case 3: Adjust for dual backgrounds.
    final bgTone1 = color.background!(scheme)!.getTone(scheme);
    final bgTone2 = color.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(upper, desiredRatio);

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

    // Tones suitable for the foreground.
    final availables = <double>[?lightOption, ?darkOption];
    final prefersLight =
        DynamicColor.tonePrefersLightForeground(bgTone1) ||
        DynamicColor.tonePrefersLightForeground(bgTone2);
    if (prefersLight) {
      return lightOption ?? 100.0;
    }
    return availables.length == 1 ? availables[0] : (darkOption ?? 0.0);
  }
}