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