blob: e839d9bc3c52b5b22a06181c04cc9c4da7593f3e [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.monet.dynamiccolor;
import static java.lang.Math.max;
import static java.lang.Math.min;
import com.android.systemui.monet.contrast.Contrast;
import com.android.systemui.monet.hct.Hct;
import com.android.systemui.monet.palettes.TonalPalette;
import com.android.systemui.monet.scheme.DynamicScheme;
import com.android.systemui.monet.utils.MathUtils;
import java.util.HashMap;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* A color that adjusts itself based on UI state, represented by DynamicScheme.
*
* <p>This color automatically adjusts to accommodate a desired contrast level, or other adjustments
* such as differing in light mode versus dark mode, or what the theme is, or what the color that
* produced the theme is, etc.
*
* <p>Colors without backgrounds do not change tone when contrast changes. Colors with backgrounds
* become closer to their background as contrast lowers, and further when contrast increases.
*
* <p>Prefer the static constructors. They provide a much more simple interface, such as requiring
* just a hexcode, or just a hexcode and a background.
*
* <p>Ultimately, each component necessary for calculating a color, adjusting it for a desired
* contrast level, and ensuring it has a certain lightness/tone difference from another color, is
* provided by a function that takes a DynamicScheme and returns a value. This ensures ultimate
* flexibility, any desired behavior of a color for any design system, but it usually unnecessary.
* See the default constructor for more information.
*/
// Prevent lint for Function.apply not being available on Android before API level 14 (4.0.1).
// "AndroidJdkLibsChecker" for Function, "NewApi" for Function.apply().
// A java_library Bazel rule with an Android constraint cannot skip these warnings without this
// annotation; another solution would be to create an android_library rule and supply
// AndroidManifest with an SDK set higher than 14.
@SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"})
public final class DynamicColor {
public final Function<DynamicScheme, Double> hue;
public final Function<DynamicScheme, Double> chroma;
public final Function<DynamicScheme, Double> tone;
public final Function<DynamicScheme, Double> opacity;
public final Function<DynamicScheme, DynamicColor> background;
public final Function<DynamicScheme, Double> toneMinContrast;
public final Function<DynamicScheme, Double> toneMaxContrast;
public final Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint;
private final HashMap<DynamicScheme, Hct> hctCache = new HashMap<>();
/**
* The base constructor for DynamicColor.
*
* <p>Functional arguments allow overriding without risks that come with subclasses. _Strongly_
* prefer using one of the static convenience constructors. This class is arguably too
* flexible to
* ensure it can support any scenario.
*
* <p>For example, the default behavior of adjust tone at max contrast to be at a 7.0 ratio with
* its background is principled and matches a11y guidance. That does not mean it's the desired
* approach for _every_ design system, and every color pairing, always, in every case.
*
* @param hue given DynamicScheme, return the hue in HCT of the output color.
* @param chroma given DynamicScheme, return chroma in HCT of the output color.
* @param tone given DynamicScheme, return tone in HCT of the output color.
* @param background given DynamicScheme, return the DynamicColor that is the
* background of this
* DynamicColor. When this is provided, automated adjustments to
* lower and raise contrast are
* made.
* @param toneMinContrast given DynamicScheme, return tone in HCT/L* in L*a*b* this color
* should
* be at minimum contrast. See toneMinContrastDefault for the
* default behavior, and strongly
* consider using it unless you have strong opinions on a11y. The
* static constructors use it.
* @param toneMaxContrast given DynamicScheme, return tone in HCT/L* in L*a*b* this color
* should
* be at maximum contrast. See toneMaxContrastDefault for the
* default behavior, and strongly
* consider using it unless you have strong opinions on a11y. The
* static constructors use it.
* @param toneDeltaConstraint given DynamicScheme, return a ToneDeltaConstraint instance that
* describes a requirement that this DynamicColor must always have
* some difference in tone/L*
* from another DynamicColor.<br>
* Unlikely to be useful unless a design system has some
* distortions where colors that don't
* have a background/foreground relationship must have _some_
* difference in tone, yet, not
* enough difference to create meaningful contrast.
*/
public DynamicColor(
Function<DynamicScheme, Double> hue,
Function<DynamicScheme, Double> chroma,
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, Double> opacity,
Function<DynamicScheme, DynamicColor> background,
Function<DynamicScheme, Double> toneMinContrast,
Function<DynamicScheme, Double> toneMaxContrast,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
this.hue = hue;
this.chroma = chroma;
this.tone = tone;
this.opacity = opacity;
this.background = background;
this.toneMinContrast = toneMinContrast;
this.toneMaxContrast = toneMaxContrast;
this.toneDeltaConstraint = toneDeltaConstraint;
}
/**
* Create a DynamicColor from a hex code.
*
* <p>Result has no background; thus no support for increasing/decreasing contrast for a11y.
*/
public static DynamicColor fromArgb(int argb) {
final Hct hct = Hct.fromInt(argb);
final TonalPalette palette = TonalPalette.fromInt(argb);
return DynamicColor.fromPalette((s) -> palette, (s) -> hct.getTone());
}
/**
* Create a DynamicColor from just a hex code.
*
* <p>Result has no background; thus cannot support increasing/decreasing contrast for a11y.
*
* @param argb A hex code.
* @param tone Function that provides a tone given DynamicScheme. Useful for adjusting for dark
* vs. light mode.
*/
public static DynamicColor fromArgb(int argb, Function<DynamicScheme, Double> tone) {
return DynamicColor.fromPalette((s) -> TonalPalette.fromInt(argb), tone);
}
/**
* Create a DynamicColor.
*
* <p>If you don't understand HCT fully, or your design team doesn't, but wants support for
* automated contrast adjustment, this method is _extremely_ useful: you can take a standard
* design system expressed as hex codes, create DynamicColors corresponding to each color, and
* then wire up backgrounds.
*
* <p>If the design system uses the same hex code on multiple backgrounds, define that in
* multiple
* DynamicColors so that the background is accurate for each one. If you define a DynamicColor
* with one background, and actually use it on another, DynamicColor can't guarantee contrast
* . For
* example, if you use a color on both black and white, increasing the contrast on one
* necessarily
* decreases contrast of the other.
*
* @param argb A hex code.
* @param tone Function that provides a tone given DynamicScheme. (useful for dark vs.
* light mode)
* @param background Function that provides background DynamicColor given DynamicScheme. Useful
* for contrast, given a background, colors can adjust to increase/decrease
* contrast.
*/
public static DynamicColor fromArgb(
int argb,
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background) {
return DynamicColor.fromPalette((s) -> TonalPalette.fromInt(argb), tone, background);
}
/**
* Create a DynamicColor from:
*
* @param argb A hex code.
* @param tone Function that provides a tone given DynamicScheme. (useful for
* dark vs. light mode)
* @param background Function that provides background DynamicColor given
* DynamicScheme. Useful
* for contrast, given a background, colors can adjust to
* increase/decrease contrast.
* @param toneDeltaConstraint Function that provides a ToneDeltaConstraint given DynamicScheme.
* Useful for ensuring lightness difference between colors that
* don't _require_ contrast or
* have a formal background/foreground relationship.
*/
public static DynamicColor fromArgb(
int argb,
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
return DynamicColor.fromPalette(
(s) -> TonalPalette.fromInt(argb), tone, background, toneDeltaConstraint);
}
/**
* Create a DynamicColor.
*
* @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is
* defined by a hue and chroma, so this replaces the need to specify
* hue/chroma. By providing
* a tonal palette, when contrast adjustments are made, intended chroma can be
* preserved. For
* example, at T/L* 90, there is a significant limit to the amount of chroma.
* There is no
* colorful red, a red that light is pink. By preserving the _intended_ chroma
* if lightness
* lowers for contrast adjustments, the intended chroma is restored.
* @param tone Function that provides a tone given DynamicScheme. (useful for dark vs.
* light mode)
*/
public static DynamicColor fromPalette(
Function<DynamicScheme, TonalPalette> palette, Function<DynamicScheme, Double> tone) {
return fromPalette(palette, tone, null, null);
}
/**
* Create a DynamicColor.
*
* @param palette Function that provides a TonalPalette given DynamicScheme. A
* TonalPalette is
* defined by a hue and chroma, so this replaces the need to specify
* hue/chroma. By providing
* a tonal palette, when contrast adjustments are made, intended chroma can
* be preserved. For
* example, at T/L* 90, there is a significant limit to the amount of
* chroma. There is no
* colorful red, a red that light is pink. By preserving the _intended_
* chroma if lightness
* lowers for contrast adjustments, the intended chroma is restored.
* @param tone Function that provides a tone given DynamicScheme. (useful for dark vs.
* light mode)
* @param background Function that provides background DynamicColor given DynamicScheme. Useful
* for contrast, given a background, colors can adjust to increase/decrease
* contrast.
*/
public static DynamicColor fromPalette(
Function<DynamicScheme, TonalPalette> palette,
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background) {
return fromPalette(palette, tone, background, null);
}
/**
* Create a DynamicColor.
*
* @param palette Function that provides a TonalPalette given DynamicScheme. A
* TonalPalette is
* defined by a hue and chroma, so this replaces the need to
* specify hue/chroma. By providing
* a tonal palette, when contrast adjustments are made, intended
* chroma can be preserved. For
* example, at T/L* 90, there is a significant limit to the amount
* of chroma. There is no
* colorful red, a red that light is pink. By preserving the
* _intended_ chroma if lightness
* lowers for contrast adjustments, the intended chroma is restored.
* @param tone Function that provides a tone given DynamicScheme. (useful for
* dark vs. light mode)
* @param background Function that provides background DynamicColor given
* DynamicScheme. Useful
* for contrast, given a background, colors can adjust to
* increase/decrease contrast.
* @param toneDeltaConstraint Function that provides a ToneDeltaConstraint given DynamicScheme.
* Useful for ensuring lightness difference between colors that
* don't _require_ contrast or
* have a formal background/foreground relationship.
*/
public static DynamicColor fromPalette(
Function<DynamicScheme, TonalPalette> palette,
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
return new DynamicColor(
scheme -> palette.apply(scheme).getHue(),
scheme -> palette.apply(scheme).getChroma(),
tone,
null,
background,
scheme -> toneMinContrastDefault(tone, background, scheme, toneDeltaConstraint),
scheme -> toneMaxContrastDefault(tone, background, scheme, toneDeltaConstraint),
toneDeltaConstraint);
}
/**
* The default algorithm for calculating the tone of a color at minimum contrast.<br>
* If the original contrast ratio was >= 7.0, reach contrast 4.5.<br>
* If the original contrast ratio was >= 3.0, reach contrast 3.0.<br>
* If the original contrast ratio was < 3.0, reach that ratio.
*/
public static double toneMinContrastDefault(
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background,
DynamicScheme scheme,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
return DynamicColor.calculateDynamicTone(
scheme,
tone,
c -> c.toneMinContrast.apply(scheme),
(stdRatio, bgTone) -> {
double answer = tone.apply(scheme);
if (stdRatio >= Contrast.RATIO_70) {
answer = contrastingTone(bgTone, Contrast.RATIO_45);
} else if (stdRatio >= Contrast.RATIO_30) {
answer = contrastingTone(bgTone, Contrast.RATIO_30);
} else {
final boolean backgroundHasBackground =
background != null
&& background.apply(scheme) != null
&& background.apply(scheme).background != null
&& background.apply(scheme).background.apply(scheme)
!= null;
if (backgroundHasBackground) {
answer = contrastingTone(bgTone, stdRatio);
}
}
return answer;
},
background,
toneDeltaConstraint,
null,
standardRatio -> standardRatio);
}
/**
* The default algorithm for calculating the tone of a color at maximum contrast.<br>
* If the color's background has a background, reach contrast 7.0.<br>
* If it doesn't, maintain the original contrast ratio.<br>
*
* <p>This ensures text on surfaces maintains its original, often detrimentally excessive,
* contrast ratio. But, text on buttons can soften to not have excessive contrast.
*
* <p>Historically, digital design uses pure whites and black for text and surfaces. It's too
* much
* of a jump at this point in history to introduce a dynamic contrast system _and_ insist that
* text always had excessive contrast and should reach 7.0, it would deterimentally affect
* desire
* to understand and use dynamic contrast.
*/
public static double toneMaxContrastDefault(
Function<DynamicScheme, Double> tone,
Function<DynamicScheme, DynamicColor> background,
DynamicScheme scheme,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint) {
return DynamicColor.calculateDynamicTone(
scheme,
tone,
c -> c.toneMaxContrast.apply(scheme),
(stdRatio, bgTone) -> {
final boolean backgroundHasBackground =
background != null
&& background.apply(scheme) != null
&& background.apply(scheme).background != null
&& background.apply(scheme).background.apply(scheme) != null;
if (backgroundHasBackground) {
return contrastingTone(bgTone, Contrast.RATIO_70);
} else {
return contrastingTone(bgTone, max(Contrast.RATIO_70, stdRatio));
}
},
background,
toneDeltaConstraint,
null,
null);
}
/**
* Core method for calculating a tone for under dynamic contrast.
*
* <p>It enforces important properties:<br>
* #1. Desired contrast ratio is reached.<br>
* As contrast increases from standard to max, the tones involved should always be at least the
* standard ratio. For example, if a button is T90, and button text is T0, and the button is
* T0 at
* max contrast, the button text cannot simply linearly interpolate from T0 to T100, or at some
* point they'll both be at the same tone.
*
* <p>#2. Enable light foregrounds on midtones.<br>
* The eye prefers light foregrounds on T50 to T60, possibly up to T70, but, contrast ratio 4.5
* can't be reached with T100 unless the foreground is T50. Contrast ratio 4.5 is crucial, it
* represents 'readable text', i.e. text smaller than ~40 dp / 1/4". So, if a tone is between
* T50
* and T60, it is proactively changed to T49 to enable light foregrounds.
*
* <p>#3. Ensure tone delta with another color.<br>
* In design systems, there may be colors that don't have a pure background/foreground
* relationship, but, do require different tones for visual differentiation. ToneDeltaConstraint
* models this requirement, and DynamicColor enforces it.
*/
public static double calculateDynamicTone(
DynamicScheme scheme,
Function<DynamicScheme, Double> toneStandard,
Function<DynamicColor, Double> toneToJudge,
BiFunction<Double, Double, Double> desiredTone,
Function<DynamicScheme, DynamicColor> background,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint,
Function<Double, Double> minRatio,
Function<Double, Double> maxRatio) {
// Start with the tone with no adjustment for contrast.
// If there is no background, don't perform any adjustment, return immediately.
final double toneStd = toneStandard.apply(scheme);
double answer = toneStd;
final DynamicColor bgDynamic = background == null ? null : background.apply(scheme);
if (bgDynamic == null) {
return answer;
}
final double bgToneStd = bgDynamic.tone.apply(scheme);
final double stdRatio = Contrast.ratioOfTones(toneStd, bgToneStd);
// If there is a background, determine its tone after contrast adjustment.
// Then, calculate the foreground tone that ensures the caller's desired contrast ratio
// is met.
final double bgTone = toneToJudge.apply(bgDynamic);
final double myDesiredTone = desiredTone.apply(stdRatio, bgTone);
final double currentRatio = Contrast.ratioOfTones(bgTone, myDesiredTone);
final double minRatioRealized =
minRatio == null
? Contrast.RATIO_MIN
: minRatio.apply(stdRatio) == null ? Contrast.RATIO_MIN : minRatio.apply(
stdRatio);
final double maxRatioRealized =
maxRatio == null
? Contrast.RATIO_MAX
: maxRatio.apply(stdRatio) == null ? Contrast.RATIO_MAX : maxRatio.apply(
stdRatio);
final double desiredRatio =
MathUtils.clampDouble(minRatioRealized, maxRatioRealized, currentRatio);
if (desiredRatio == currentRatio) {
answer = myDesiredTone;
} else {
answer = DynamicColor.contrastingTone(bgTone, desiredRatio);
}
// If the background has no background, adjust the foreground tone to ensure that
// it is dark enough to have a light foreground.
if (bgDynamic.background == null || bgDynamic.background.apply(scheme) == null) {
answer = DynamicColor.enableLightForeground(answer);
}
// If the caller has specified a constraint where it must have a certain tone distance from
// another color, enforce that constraint.
answer = ensureToneDelta(answer, toneStd, scheme, toneDeltaConstraint, toneToJudge);
return answer;
}
static double ensureToneDelta(
double tone,
double toneStandard,
DynamicScheme scheme,
Function<DynamicScheme, ToneDeltaConstraint> toneDeltaConstraint,
Function<DynamicColor, Double> toneToDistanceFrom) {
final ToneDeltaConstraint constraint =
(toneDeltaConstraint == null ? null : toneDeltaConstraint.apply(scheme));
if (constraint == null) {
return tone;
}
final double requiredDelta = constraint.delta;
final double keepAwayTone = toneToDistanceFrom.apply(constraint.keepAway);
final double delta = Math.abs(tone - keepAwayTone);
if (delta >= requiredDelta) {
return tone;
}
switch (constraint.keepAwayPolarity) {
case DARKER:
return MathUtils.clampDouble(0, 100, keepAwayTone + requiredDelta);
case LIGHTER:
return MathUtils.clampDouble(0, 100, keepAwayTone - requiredDelta);
case NO_PREFERENCE:
final double keepAwayToneStandard = constraint.keepAway.tone.apply(scheme);
final boolean preferLighten = toneStandard > keepAwayToneStandard;
final double alterAmount = Math.abs(delta - requiredDelta);
final boolean lighten =
preferLighten ? (tone + alterAmount <= 100.0) : tone < alterAmount;
return lighten ? tone + alterAmount : tone - alterAmount;
}
return tone;
}
/**
* Given a background tone, find a foreground tone, while ensuring they reach a contrast ratio
* that is as close to ratio as possible.
*/
public static double contrastingTone(double bgTone, double ratio) {
final double lighterTone = Contrast.lighterUnsafe(bgTone, ratio);
final double darkerTone = Contrast.darkerUnsafe(bgTone, ratio);
final double lighterRatio = Contrast.ratioOfTones(lighterTone, bgTone);
final double darkerRatio = Contrast.ratioOfTones(darkerTone, bgTone);
final boolean preferLighter = tonePrefersLightForeground(bgTone);
if (preferLighter) {
// "Neglible difference" handles an edge case where the initial contrast ratio is high
// (ex. 13.0), and the ratio passed to the function is that high ratio, and both the
// lighter
// and darker ratio fails to pass that ratio.
//
// This was observed with Tonal Spot's On Primary Container turning black momentarily
// between
// high and max contrast in light mode. PC's standard tone was T90, OPC's was T10, it
// was
// light mode, and the contrast level was 0.6568521221032331.
final boolean negligibleDifference =
Math.abs(lighterRatio - darkerRatio) < 0.1 && lighterRatio < ratio
&& darkerRatio < ratio;
if (lighterRatio >= ratio || lighterRatio >= darkerRatio || negligibleDifference) {
return lighterTone;
} else {
return darkerTone;
}
} else {
return darkerRatio >= ratio || darkerRatio >= lighterRatio ? darkerTone : lighterTone;
}
}
/**
* Adjust a tone down such that white has 4.5 contrast, if the tone is reasonably close to
* supporting it.
*/
public static double enableLightForeground(double tone) {
if (tonePrefersLightForeground(tone) && !toneAllowsLightForeground(tone)) {
return 49.0;
}
return tone;
}
/**
* People prefer white foregrounds on ~T60-70. Observed over time, and also by Andrew Somers
* during research for APCA.
*
* <p>T60 used as to create the smallest discontinuity possible when skipping down to T49 in
* order
* to ensure light foregrounds.
*
* <p>Since `tertiaryContainer` in dark monochrome scheme requires a tone of 60, it should
* not be
* adjusted. Therefore, 60 is excluded here.
*/
public static boolean tonePrefersLightForeground(double tone) {
return Math.round(tone) < 60;
}
/**
* Tones less than ~T50 always permit white at 4.5 contrast.
*/
public static boolean toneAllowsLightForeground(double tone) {
return Math.round(tone) <= 49;
}
public int getArgb(DynamicScheme scheme) {
final int argb = getHct(scheme).toInt();
if (opacity == null) {
return argb;
}
final double percentage = opacity.apply(scheme);
final int alpha = MathUtils.clampInt(0, 255, (int) Math.round(percentage * 255));
return (argb & 0x00ffffff) | (alpha << 24);
}
public Hct getHct(DynamicScheme scheme) {
final Hct cachedAnswer = hctCache.get(scheme);
if (cachedAnswer != null) {
return cachedAnswer;
}
// This is crucial for aesthetics: we aren't simply the taking the standard color
// and changing its tone for contrast. Rather, we find the tone for contrast, then
// use the specified chroma from the palette to construct a new color.
//
// For example, this enables colors with standard tone of T90, which has limited chroma, to
// "recover" intended chroma as contrast increases.
final Hct answer = Hct.from(hue.apply(scheme), chroma.apply(scheme), getTone(scheme));
// NOMUTANTS--trivial test with onerous dependency injection requirement.
if (hctCache.size() > 4) {
hctCache.clear();
}
// NOMUTANTS--trivial test with onerous dependency injection requirement.
hctCache.put(scheme, answer);
return answer;
}
/**
* Returns the tone in HCT, ranging from 0 to 100, of the resolved color given scheme.
*/
public double getTone(DynamicScheme scheme) {
double answer = tone.apply(scheme);
final boolean decreasingContrast = scheme.contrastLevel < 0.0;
if (scheme.contrastLevel != 0.0) {
final double startTone = tone.apply(scheme);
final double endTone =
decreasingContrast ? toneMinContrast.apply(scheme) : toneMaxContrast.apply(
scheme);
final double delta = (endTone - startTone) * Math.abs(scheme.contrastLevel);
answer = delta + startTone;
}
final DynamicColor bgDynamicColor = background == null ? null : background.apply(scheme);
double minRatio = Contrast.RATIO_MIN;
double maxRatio = Contrast.RATIO_MAX;
if (bgDynamicColor != null) {
final boolean bgHasBg =
bgDynamicColor.background != null && bgDynamicColor.background.apply(scheme)
!= null;
final double standardRatio =
Contrast.ratioOfTones(tone.apply(scheme), bgDynamicColor.tone.apply(scheme));
if (decreasingContrast) {
final double minContrastRatio =
Contrast.ratioOfTones(
toneMinContrast.apply(scheme),
bgDynamicColor.toneMinContrast.apply(scheme));
minRatio = bgHasBg ? minContrastRatio : 1.0;
maxRatio = standardRatio;
} else {
final double maxContrastRatio =
Contrast.ratioOfTones(
toneMaxContrast.apply(scheme),
bgDynamicColor.toneMaxContrast.apply(scheme));
minRatio = bgHasBg ? min(maxContrastRatio, standardRatio) : 1.0;
maxRatio = bgHasBg ? max(maxContrastRatio, standardRatio) : 21.0;
}
}
final double finalMinRatio = minRatio;
final double finalMaxRatio = maxRatio;
final double finalAnswer = answer;
answer =
calculateDynamicTone(
scheme,
this.tone,
(dynamicColor) -> dynamicColor.getTone(scheme),
(a, b) -> finalAnswer,
(s) -> bgDynamicColor,
toneDeltaConstraint,
(s) -> finalMinRatio,
(s) -> finalMaxRatio);
return answer;
}
}