getTone method
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;
}
}