• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 Google LLC
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.ux.material.libmonet.dynamiccolor;
18 
19 import static java.lang.Math.max;
20 import static java.lang.Math.min;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 import com.google.errorprone.annotations.Var;
25 import com.google.ux.material.libmonet.contrast.Contrast;
26 import com.google.ux.material.libmonet.hct.Hct;
27 import com.google.ux.material.libmonet.palettes.TonalPalette;
28 import com.google.ux.material.libmonet.scheme.DynamicScheme;
29 import com.google.ux.material.libmonet.utils.MathUtils;
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.function.Function;
33 
34 /**
35  * A color that adjusts itself based on UI state, represented by DynamicScheme.
36  *
37  * <p>This color automatically adjusts to accommodate a desired contrast level, or other adjustments
38  * such as differing in light mode versus dark mode, or what the theme is, or what the color that
39  * produced the theme is, etc.
40  *
41  * <p>Colors without backgrounds do not change tone when contrast changes. Colors with backgrounds
42  * become closer to their background as contrast lowers, and further when contrast increases.
43  *
44  * <p>Prefer the static constructors. They provide a much more simple interface, such as requiring
45  * just a hexcode, or just a hexcode and a background.
46  *
47  * <p>Ultimately, each component necessary for calculating a color, adjusting it for a desired
48  * contrast level, and ensuring it has a certain lightness/tone difference from another color, is
49  * provided by a function that takes a DynamicScheme and returns a value. This ensures ultimate
50  * flexibility, any desired behavior of a color for any design system, but it usually unnecessary.
51  * See the default constructor for more information.
52  */
53 // Prevent lint for Function.apply not being available on Android before API level 14 (4.0.1).
54 // "AndroidJdkLibsChecker" for Function, "NewApi" for Function.apply().
55 // A java_library Bazel rule with an Android constraint cannot skip these warnings without this
56 // annotation; another solution would be to create an android_library rule and supply
57 // AndroidManifest with an SDK set higher than 14.
58 @SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"})
59 public final class DynamicColor {
60   public final String name;
61   public final Function<DynamicScheme, TonalPalette> palette;
62   public final Function<DynamicScheme, Double> tone;
63   public final boolean isBackground;
64   public final Function<DynamicScheme, DynamicColor> background;
65   public final Function<DynamicScheme, DynamicColor> secondBackground;
66   public final ContrastCurve contrastCurve;
67   public final Function<DynamicScheme, ToneDeltaPair> toneDeltaPair;
68 
69   public final Function<DynamicScheme, Double> opacity;
70 
71   private final HashMap<DynamicScheme, Hct> hctCache = new HashMap<>();
72 
73   /**
74    * A constructor for DynamicColor.
75    *
76    * <p>_Strongly_ prefer using one of the convenience constructors. This class is arguably too
77    * flexible to ensure it can support any scenario. Functional arguments allow overriding without
78    * risks that come with subclasses.
79    *
80    * <p>For example, the default behavior of adjust tone at max contrast to be at a 7.0 ratio with
81    * its background is principled and matches accessibility guidance. That does not mean it's the
82    * desired approach for _every_ design system, and every color pairing, always, in every case.
83    *
84    * <p>For opaque colors (colors with alpha = 100%).
85    *
86    * @param name The name of the dynamic color.
87    * @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is
88    *     defined by a hue and chroma, so this replaces the need to specify hue/chroma. By providing
89    *     a tonal palette, when contrast adjustments are made, intended chroma can be preserved.
90    * @param tone Function that provides a tone, given a DynamicScheme.
91    * @param isBackground Whether this dynamic color is a background, with some other color as the
92    *     foreground.
93    * @param background The background of the dynamic color (as a function of a `DynamicScheme`), if
94    *     it exists.
95    * @param secondBackground A second background of the dynamic color (as a function of a
96    *     `DynamicScheme`), if it exists.
97    * @param contrastCurve A `ContrastCurve` object specifying how its contrast against its
98    *     background should behave in various contrast levels options.
99    * @param toneDeltaPair A `ToneDeltaPair` object specifying a tone delta constraint between two
100    *     colors. One of them must be the color being constructed.
101    */
DynamicColor( @onNull String name, @NonNull Function<DynamicScheme, TonalPalette> palette, @NonNull Function<DynamicScheme, Double> tone, boolean isBackground, @Nullable Function<DynamicScheme, DynamicColor> background, @Nullable Function<DynamicScheme, DynamicColor> secondBackground, @Nullable ContrastCurve contrastCurve, @Nullable Function<DynamicScheme, ToneDeltaPair> toneDeltaPair)102   public DynamicColor(
103       @NonNull String name,
104       @NonNull Function<DynamicScheme, TonalPalette> palette,
105       @NonNull Function<DynamicScheme, Double> tone,
106       boolean isBackground,
107       @Nullable Function<DynamicScheme, DynamicColor> background,
108       @Nullable Function<DynamicScheme, DynamicColor> secondBackground,
109       @Nullable ContrastCurve contrastCurve,
110       @Nullable Function<DynamicScheme, ToneDeltaPair> toneDeltaPair) {
111 
112     this.name = name;
113     this.palette = palette;
114     this.tone = tone;
115     this.isBackground = isBackground;
116     this.background = background;
117     this.secondBackground = secondBackground;
118     this.contrastCurve = contrastCurve;
119     this.toneDeltaPair = toneDeltaPair;
120     this.opacity = null;
121   }
122 
123   /**
124    * A constructor for DynamicColor.
125    *
126    * <p>_Strongly_ prefer using one of the convenience constructors. This class is arguably too
127    * flexible to ensure it can support any scenario. Functional arguments allow overriding without
128    * risks that come with subclasses.
129    *
130    * <p>For example, the default behavior of adjust tone at max contrast to be at a 7.0 ratio with
131    * its background is principled and matches accessibility guidance. That does not mean it's the
132    * desired approach for _every_ design system, and every color pairing, always, in every case.
133    *
134    * <p>For opaque colors (colors with alpha = 100%).
135    *
136    * @param name The name of the dynamic color.
137    * @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is
138    *     defined by a hue and chroma, so this replaces the need to specify hue/chroma. By providing
139    *     a tonal palette, when contrast adjustments are made, intended chroma can be preserved.
140    * @param tone Function that provides a tone, given a DynamicScheme.
141    * @param isBackground Whether this dynamic color is a background, with some other color as the
142    *     foreground.
143    * @param background The background of the dynamic color (as a function of a `DynamicScheme`), if
144    *     it exists.
145    * @param secondBackground A second background of the dynamic color (as a function of a
146    *     `DynamicScheme`), if it exists.
147    * @param contrastCurve A `ContrastCurve` object specifying how its contrast against its
148    *     background should behave in various contrast levels options.
149    * @param toneDeltaPair A `ToneDeltaPair` object specifying a tone delta constraint between two
150    *     colors. One of them must be the color being constructed.
151    * @param opacity A function returning the opacity of a color, as a number between 0 and 1.
152    */
DynamicColor( @onNull String name, @NonNull Function<DynamicScheme, TonalPalette> palette, @NonNull Function<DynamicScheme, Double> tone, boolean isBackground, @Nullable Function<DynamicScheme, DynamicColor> background, @Nullable Function<DynamicScheme, DynamicColor> secondBackground, @Nullable ContrastCurve contrastCurve, @Nullable Function<DynamicScheme, ToneDeltaPair> toneDeltaPair, @Nullable Function<DynamicScheme, Double> opacity)153   public DynamicColor(
154       @NonNull String name,
155       @NonNull Function<DynamicScheme, TonalPalette> palette,
156       @NonNull Function<DynamicScheme, Double> tone,
157       boolean isBackground,
158       @Nullable Function<DynamicScheme, DynamicColor> background,
159       @Nullable Function<DynamicScheme, DynamicColor> secondBackground,
160       @Nullable ContrastCurve contrastCurve,
161       @Nullable Function<DynamicScheme, ToneDeltaPair> toneDeltaPair,
162       @Nullable Function<DynamicScheme, Double> opacity) {
163     this.name = name;
164     this.palette = palette;
165     this.tone = tone;
166     this.isBackground = isBackground;
167     this.background = background;
168     this.secondBackground = secondBackground;
169     this.contrastCurve = contrastCurve;
170     this.toneDeltaPair = toneDeltaPair;
171     this.opacity = opacity;
172   }
173 
174   /**
175    * A convenience constructor for DynamicColor.
176    *
177    * <p>_Strongly_ prefer using one of the convenience constructors. This class is arguably too
178    * flexible to ensure it can support any scenario. Functional arguments allow overriding without
179    * risks that come with subclasses.
180    *
181    * <p>For example, the default behavior of adjust tone at max contrast to be at a 7.0 ratio with
182    * its background is principled and matches accessibility guidance. That does not mean it's the
183    * desired approach for _every_ design system, and every color pairing, always, in every case.
184    *
185    * <p>For opaque colors (colors with alpha = 100%).
186    *
187    * <p>For colors that are not backgrounds, and do not have backgrounds.
188    *
189    * @param name The name of the dynamic color.
190    * @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is
191    *     defined by a hue and chroma, so this replaces the need to specify hue/chroma. By providing
192    *     a tonal palette, when contrast adjustments are made, intended chroma can be preserved.
193    * @param tone Function that provides a tone, given a DynamicScheme.
194    */
195   @NonNull
fromPalette( @onNull String name, @NonNull Function<DynamicScheme, TonalPalette> palette, @NonNull Function<DynamicScheme, Double> tone)196   public static DynamicColor fromPalette(
197       @NonNull String name,
198       @NonNull Function<DynamicScheme, TonalPalette> palette,
199       @NonNull Function<DynamicScheme, Double> tone) {
200     return new DynamicColor(
201         name,
202         palette,
203         tone,
204         /* isBackground= */ false,
205         /* background= */ null,
206         /* secondBackground= */ null,
207         /* contrastCurve= */ null,
208         /* toneDeltaPair= */ null);
209   }
210 
211   /**
212    * A convenience constructor for DynamicColor.
213    *
214    * <p>_Strongly_ prefer using one of the convenience constructors. This class is arguably too
215    * flexible to ensure it can support any scenario. Functional arguments allow overriding without
216    * risks that come with subclasses.
217    *
218    * <p>For example, the default behavior of adjust tone at max contrast to be at a 7.0 ratio with
219    * its background is principled and matches accessibility guidance. That does not mean it's the
220    * desired approach for _every_ design system, and every color pairing, always, in every case.
221    *
222    * <p>For opaque colors (colors with alpha = 100%).
223    *
224    * <p>For colors that do not have backgrounds.
225    *
226    * @param name The name of the dynamic color.
227    * @param palette Function that provides a TonalPalette given DynamicScheme. A TonalPalette is
228    *     defined by a hue and chroma, so this replaces the need to specify hue/chroma. By providing
229    *     a tonal palette, when contrast adjustments are made, intended chroma can be preserved.
230    * @param tone Function that provides a tone, given a DynamicScheme.
231    * @param isBackground Whether this dynamic color is a background, with some other color as the
232    *     foreground.
233    */
234   @NonNull
fromPalette( @onNull String name, @NonNull Function<DynamicScheme, TonalPalette> palette, @NonNull Function<DynamicScheme, Double> tone, boolean isBackground)235   public static DynamicColor fromPalette(
236       @NonNull String name,
237       @NonNull Function<DynamicScheme, TonalPalette> palette,
238       @NonNull Function<DynamicScheme, Double> tone,
239       boolean isBackground) {
240     return new DynamicColor(
241         name,
242         palette,
243         tone,
244         isBackground,
245         /* background= */ null,
246         /* secondBackground= */ null,
247         /* contrastCurve= */ null,
248         /* toneDeltaPair= */ null);
249   }
250 
251   /**
252    * Create a DynamicColor from a hex code.
253    *
254    * <p>Result has no background; thus no support for increasing/decreasing contrast for a11y.
255    *
256    * @param name The name of the dynamic color.
257    * @param argb The source color from which to extract the hue and chroma.
258    */
259   @NonNull
fromArgb(@onNull String name, int argb)260   public static DynamicColor fromArgb(@NonNull String name, int argb) {
261     Hct hct = Hct.fromInt(argb);
262     TonalPalette palette = TonalPalette.fromInt(argb);
263     return DynamicColor.fromPalette(name, (s) -> palette, (s) -> hct.getTone());
264   }
265 
266   /**
267    * Returns an ARGB integer (i.e. a hex code).
268    *
269    * @param scheme Defines the conditions of the user interface, for example, whether or not it is
270    *     dark mode or light mode, and what the desired contrast level is.
271    */
getArgb(@onNull DynamicScheme scheme)272   public int getArgb(@NonNull DynamicScheme scheme) {
273     int argb = getHct(scheme).toInt();
274     if (opacity == null) {
275       return argb;
276     }
277     double percentage = opacity.apply(scheme);
278     int alpha = MathUtils.clampInt(0, 255, (int) Math.round(percentage * 255));
279     return (argb & 0x00ffffff) | (alpha << 24);
280   }
281 
282   /**
283    * Returns an HCT object.
284    *
285    * @param scheme Defines the conditions of the user interface, for example, whether or not it is
286    *     dark mode or light mode, and what the desired contrast level is.
287    */
288   @NonNull
getHct(@onNull DynamicScheme scheme)289   public Hct getHct(@NonNull DynamicScheme scheme) {
290     Hct cachedAnswer = hctCache.get(scheme);
291     if (cachedAnswer != null) {
292       return cachedAnswer;
293     }
294     // This is crucial for aesthetics: we aren't simply the taking the standard color
295     // and changing its tone for contrast. Rather, we find the tone for contrast, then
296     // use the specified chroma from the palette to construct a new color.
297     //
298     // For example, this enables colors with standard tone of T90, which has limited chroma, to
299     // "recover" intended chroma as contrast increases.
300     double tone = getTone(scheme);
301     Hct answer = palette.apply(scheme).getHct(tone);
302     // NOMUTANTS--trivial test with onerous dependency injection requirement.
303     if (hctCache.size() > 4) {
304       hctCache.clear();
305     }
306     // NOMUTANTS--trivial test with onerous dependency injection requirement.
307     hctCache.put(scheme, answer);
308     return answer;
309   }
310 
311   /** Returns the tone in HCT, ranging from 0 to 100, of the resolved color given scheme. */
getTone(@onNull DynamicScheme scheme)312   public double getTone(@NonNull DynamicScheme scheme) {
313     boolean decreasingContrast = scheme.contrastLevel < 0;
314 
315     // Case 1: dual foreground, pair of colors with delta constraint.
316     if (toneDeltaPair != null) {
317       ToneDeltaPair toneDeltaPair = this.toneDeltaPair.apply(scheme);
318       DynamicColor roleA = toneDeltaPair.getRoleA();
319       DynamicColor roleB = toneDeltaPair.getRoleB();
320       double delta = toneDeltaPair.getDelta();
321       TonePolarity polarity = toneDeltaPair.getPolarity();
322       boolean stayTogether = toneDeltaPair.getStayTogether();
323 
324       DynamicColor bg = background.apply(scheme);
325       double bgTone = bg.getTone(scheme);
326 
327       boolean aIsNearer =
328           (polarity == TonePolarity.NEARER
329               || (polarity == TonePolarity.LIGHTER && !scheme.isDark)
330               || (polarity == TonePolarity.DARKER && scheme.isDark));
331       DynamicColor nearer = aIsNearer ? roleA : roleB;
332       DynamicColor farther = aIsNearer ? roleB : roleA;
333       boolean amNearer = name.equals(nearer.name);
334       double expansionDir = scheme.isDark ? 1 : -1;
335 
336       // 1st round: solve to min, each
337       double nContrast = nearer.contrastCurve.getContrast(scheme.contrastLevel);
338       double fContrast = farther.contrastCurve.getContrast(scheme.contrastLevel);
339 
340       // If a color is good enough, it is not adjusted.
341       // Initial and adjusted tones for `nearer`
342       double nInitialTone = nearer.tone.apply(scheme);
343       @Var
344       double nTone =
345           Contrast.ratioOfTones(bgTone, nInitialTone) >= nContrast
346               ? nInitialTone
347               : DynamicColor.foregroundTone(bgTone, nContrast);
348       // Initial and adjusted tones for `farther`
349       double fInitialTone = farther.tone.apply(scheme);
350       @Var
351       double fTone =
352           Contrast.ratioOfTones(bgTone, fInitialTone) >= fContrast
353               ? fInitialTone
354               : DynamicColor.foregroundTone(bgTone, fContrast);
355 
356       if (decreasingContrast) {
357         // If decreasing contrast, adjust color to the "bare minimum"
358         // that satisfies contrast.
359         nTone = DynamicColor.foregroundTone(bgTone, nContrast);
360         fTone = DynamicColor.foregroundTone(bgTone, fContrast);
361       }
362 
363       // If constraint is not satisfied, try another round.
364       if ((fTone - nTone) * expansionDir < delta) {
365         // 2nd round: expand farther to match delta.
366         fTone = MathUtils.clampDouble(0, 100, nTone + delta * expansionDir);
367         // If constraint is not satisfied, try another round.
368         if ((fTone - nTone) * expansionDir < delta) {
369           // 3rd round: contract nearer to match delta.
370           nTone = MathUtils.clampDouble(0, 100, fTone - delta * expansionDir);
371         }
372       }
373 
374       // Avoids the 50-59 awkward zone.
375       if (50 <= nTone && nTone < 60) {
376         // If `nearer` is in the awkward zone, move it away, together with
377         // `farther`.
378         if (expansionDir > 0) {
379           nTone = 60;
380           fTone = max(fTone, nTone + delta * expansionDir);
381         } else {
382           nTone = 49;
383           fTone = min(fTone, nTone + delta * expansionDir);
384         }
385       } else if (50 <= fTone && fTone < 60) {
386         if (stayTogether) {
387           // Fixes both, to avoid two colors on opposite sides of the "awkward
388           // zone".
389           if (expansionDir > 0) {
390             nTone = 60;
391             fTone = max(fTone, nTone + delta * expansionDir);
392           } else {
393             nTone = 49;
394             fTone = min(fTone, nTone + delta * expansionDir);
395           }
396         } else {
397           // Not required to stay together; fixes just one.
398           if (expansionDir > 0) {
399             fTone = 60;
400           } else {
401             fTone = 49;
402           }
403         }
404       }
405 
406       // Returns `nTone` if this color is `nearer`, otherwise `fTone`.
407       return amNearer ? nTone : fTone;
408     } else {
409       // Case 2: No contrast pair; just solve for itself.
410       @Var double answer = tone.apply(scheme);
411 
412       if (background == null) {
413         return answer; // No adjustment for colors with no background.
414       }
415 
416       double bgTone = background.apply(scheme).getTone(scheme);
417 
418       double desiredRatio = contrastCurve.getContrast(scheme.contrastLevel);
419 
420       if (Contrast.ratioOfTones(bgTone, answer) >= desiredRatio) {
421         // Don't "improve" what's good enough.
422       } else {
423         // Rough improvement.
424         answer = DynamicColor.foregroundTone(bgTone, desiredRatio);
425       }
426 
427       if (decreasingContrast) {
428         answer = DynamicColor.foregroundTone(bgTone, desiredRatio);
429       }
430 
431       if (isBackground && 50 <= answer && answer < 60) {
432         // Must adjust
433         if (Contrast.ratioOfTones(49, bgTone) >= desiredRatio) {
434           answer = 49;
435         } else {
436           answer = 60;
437         }
438       }
439 
440       if (secondBackground != null) {
441         // Case 3: Adjust for dual backgrounds.
442 
443         double bgTone1 = background.apply(scheme).getTone(scheme);
444         double bgTone2 = secondBackground.apply(scheme).getTone(scheme);
445 
446         double upper = max(bgTone1, bgTone2);
447         double lower = min(bgTone1, bgTone2);
448 
449         if (Contrast.ratioOfTones(upper, answer) >= desiredRatio
450             && Contrast.ratioOfTones(lower, answer) >= desiredRatio) {
451           return answer;
452         }
453 
454         // The darkest light tone that satisfies the desired ratio,
455         // or -1 if such ratio cannot be reached.
456         double lightOption = Contrast.lighter(upper, desiredRatio);
457 
458         // The lightest dark tone that satisfies the desired ratio,
459         // or -1 if such ratio cannot be reached.
460         double darkOption = Contrast.darker(lower, desiredRatio);
461 
462         // Tones suitable for the foreground.
463         ArrayList<Double> availables = new ArrayList<>();
464         if (lightOption != -1) {
465           availables.add(lightOption);
466         }
467         if (darkOption != -1) {
468           availables.add(darkOption);
469         }
470 
471         boolean prefersLight =
472             DynamicColor.tonePrefersLightForeground(bgTone1)
473                 || DynamicColor.tonePrefersLightForeground(bgTone2);
474         if (prefersLight) {
475           return (lightOption == -1) ? 100 : lightOption;
476         }
477         if (availables.size() == 1) {
478           return availables.get(0);
479         }
480         return (darkOption == -1) ? 0 : darkOption;
481       }
482 
483       return answer;
484     }
485   }
486 
487   /**
488    * Given a background tone, find a foreground tone, while ensuring they reach a contrast ratio
489    * that is as close to ratio as possible.
490    */
foregroundTone(double bgTone, double ratio)491   public static double foregroundTone(double bgTone, double ratio) {
492     double lighterTone = Contrast.lighterUnsafe(bgTone, ratio);
493     double darkerTone = Contrast.darkerUnsafe(bgTone, ratio);
494     double lighterRatio = Contrast.ratioOfTones(lighterTone, bgTone);
495     double darkerRatio = Contrast.ratioOfTones(darkerTone, bgTone);
496     boolean preferLighter = tonePrefersLightForeground(bgTone);
497 
498     if (preferLighter) {
499       // "Neglible difference" handles an edge case where the initial contrast ratio is high
500       // (ex. 13.0), and the ratio passed to the function is that high ratio, and both the lighter
501       // and darker ratio fails to pass that ratio.
502       //
503       // This was observed with Tonal Spot's On Primary Container turning black momentarily between
504       // high and max contrast in light mode. PC's standard tone was T90, OPC's was T10, it was
505       // light mode, and the contrast level was 0.6568521221032331.
506       boolean negligibleDifference =
507           Math.abs(lighterRatio - darkerRatio) < 0.1 && lighterRatio < ratio && darkerRatio < ratio;
508       if (lighterRatio >= ratio || lighterRatio >= darkerRatio || negligibleDifference) {
509         return lighterTone;
510       } else {
511         return darkerTone;
512       }
513     } else {
514       return darkerRatio >= ratio || darkerRatio >= lighterRatio ? darkerTone : lighterTone;
515     }
516   }
517 
518   /**
519    * Adjust a tone down such that white has 4.5 contrast, if the tone is reasonably close to
520    * supporting it.
521    */
enableLightForeground(double tone)522   public static double enableLightForeground(double tone) {
523     if (tonePrefersLightForeground(tone) && !toneAllowsLightForeground(tone)) {
524       return 49.0;
525     }
526     return tone;
527   }
528 
529   /**
530    * People prefer white foregrounds on ~T60-70. Observed over time, and also by Andrew Somers
531    * during research for APCA.
532    *
533    * <p>T60 used as to create the smallest discontinuity possible when skipping down to T49 in order
534    * to ensure light foregrounds.
535    *
536    * <p>Since `tertiaryContainer` in dark monochrome scheme requires a tone of 60, it should not be
537    * adjusted. Therefore, 60 is excluded here.
538    */
tonePrefersLightForeground(double tone)539   public static boolean tonePrefersLightForeground(double tone) {
540     return Math.round(tone) < 60;
541   }
542 
543   /** Tones less than ~T50 always permit white at 4.5 contrast. */
toneAllowsLightForeground(double tone)544   public static boolean toneAllowsLightForeground(double tone) {
545     return Math.round(tone) <= 49;
546   }
547 }
548