Color extraction implementation for gradient colors
used by all apps background.

Change-Id: Id7a652815cb3ef61ff4b0a442d8350337014b56d
diff --git a/src/com/android/launcher3/dynamicui/ColorExtractionService.java b/src/com/android/launcher3/dynamicui/ColorExtractionService.java
index f6b02aa..349b4ff 100644
--- a/src/com/android/launcher3/dynamicui/ColorExtractionService.java
+++ b/src/com/android/launcher3/dynamicui/ColorExtractionService.java
@@ -66,7 +66,7 @@
             if (FeatureFlags.QSB_IN_HOTSEAT || FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) {
                 extractedColors.updateWallpaperThemePalette(null);
                 if (FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) {
-                    extractedColors.updateAllAppsGradientPalette(null);
+                    extractedColors.updateAllAppsGradientPalette(this);
                 }
             }
         } else {
@@ -79,10 +79,9 @@
             }
 
             if (FeatureFlags.QSB_IN_HOTSEAT || FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) {
-                Palette wallpaperPalette = getWallpaperPalette();
-                extractedColors.updateWallpaperThemePalette(wallpaperPalette);
+                extractedColors.updateWallpaperThemePalette(getWallpaperPalette());
                 if (FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS) {
-                    extractedColors.updateAllAppsGradientPalette(wallpaperPalette);
+                    extractedColors.updateAllAppsGradientPalette(this);
                 }
             }
         }
diff --git a/src/com/android/launcher3/dynamicui/ExtractedColors.java b/src/com/android/launcher3/dynamicui/ExtractedColors.java
index 3c4aba1..e72ab3d 100644
--- a/src/com/android/launcher3/dynamicui/ExtractedColors.java
+++ b/src/com/android/launcher3/dynamicui/ExtractedColors.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.dynamicui;
 
+import android.app.WallpaperManager;
 import android.content.Context;
 import android.graphics.Color;
 import android.support.annotation.Nullable;
@@ -25,6 +26,7 @@
 
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dynamicui.colorextraction.ColorExtractor;
 
 import java.util.Arrays;
 
@@ -161,14 +163,17 @@
                 ? defaultColor : wallpaperPalette.getVibrantColor(defaultColor));
     }
 
-    public void updateAllAppsGradientPalette(@Nullable Palette wallpaperPalette) {
-        // TODO b/37089857 will be modified to take the system extracted colors into account
-        int idx;
-        idx = ALLAPPS_GRADIENT_MAIN_INDEX;
-        setColorAtIndex(idx, wallpaperPalette == null
-                ? DEFAULT_VALUES[idx] : wallpaperPalette.getDarkVibrantColor(DEFAULT_VALUES[idx]));
-        idx = ALLAPPS_GRADIENT_SECONDARY_INDEX;
-        setColorAtIndex(idx, wallpaperPalette == null
-                ? DEFAULT_VALUES[idx] : wallpaperPalette.getVibrantColor(DEFAULT_VALUES[idx]));
+    public void updateAllAppsGradientPalette(Context context) {
+        // TODO use isAtLeastO when available
+        try {
+            WallpaperManager.class.getDeclaredMethod("getWallpaperColors", int.class);
+            ColorExtractor extractor = new ColorExtractor(context);
+            ColorExtractor.GradientColors colors = extractor.getColors(WallpaperManager.FLAG_SYSTEM);
+            setColorAtIndex(ALLAPPS_GRADIENT_MAIN_INDEX, colors.getMainColor());
+            setColorAtIndex(ALLAPPS_GRADIENT_SECONDARY_INDEX, colors.getSecondaryColor());
+        } catch (NoSuchMethodException e) {
+            setColorAtIndex(ALLAPPS_GRADIENT_MAIN_INDEX, Color.WHITE);
+            setColorAtIndex(ALLAPPS_GRADIENT_SECONDARY_INDEX, Color.WHITE);
+        }
     }
 }
diff --git a/src/com/android/launcher3/dynamicui/colorextraction/ColorExtractor.java b/src/com/android/launcher3/dynamicui/colorextraction/ColorExtractor.java
new file mode 100644
index 0000000..153b529
--- /dev/null
+++ b/src/com/android/launcher3/dynamicui/colorextraction/ColorExtractor.java
@@ -0,0 +1,136 @@
+package com.android.launcher3.dynamicui.colorextraction;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.android.launcher3.dynamicui.colorextraction.types.ExtractionType;
+import com.android.launcher3.dynamicui.colorextraction.types.Tonal;
+
+import java.lang.reflect.Method;
+
+
+/**
+ * Class to process wallpaper colors and generate a tonal palette based on them.
+ *
+ * TODO remove this class if available by platform
+ */
+public class ColorExtractor {
+    private static final String TAG = "ColorExtractor";
+    private static final int FALLBACK_COLOR = Color.WHITE;
+
+    private int mMainFallbackColor = FALLBACK_COLOR;
+    private int mSecondaryFallbackColor = FALLBACK_COLOR;
+    private final GradientColors mSystemColors;
+    private final GradientColors mLockColors;
+    private final Context mContext;
+    private final ExtractionType mExtractionType;
+
+    public ColorExtractor(Context context) {
+        mContext = context;
+        mSystemColors = new GradientColors();
+        mLockColors = new GradientColors();
+        mExtractionType = new Tonal();
+        WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class);
+
+        if (wallpaperManager == null) {
+            Log.w(TAG, "Can't listen to color changes!");
+        } else {
+            Parcelable wallpaperColorsObj;
+            try {
+                Method method = WallpaperManager.class
+                        .getDeclaredMethod("getWallpaperColors", int.class);
+
+                wallpaperColorsObj = (Parcelable) method.invoke(wallpaperManager,
+                        WallpaperManager.FLAG_SYSTEM);
+                extractInto(new WallpaperColorsCompat(wallpaperColorsObj), mSystemColors);
+                wallpaperColorsObj = (Parcelable) method.invoke(wallpaperManager,
+                        WallpaperManager.FLAG_LOCK);
+                extractInto(new WallpaperColorsCompat(wallpaperColorsObj), mLockColors);
+            } catch (Exception e) {
+                Log.e(TAG, "reflection failed", e);
+            }
+        }
+    }
+
+    public GradientColors getColors(int which) {
+        if (which == WallpaperManager.FLAG_LOCK) {
+            return mLockColors;
+        } else if (which == WallpaperManager.FLAG_SYSTEM) {
+            return mSystemColors;
+        } else {
+            throw new IllegalArgumentException("which should be either FLAG_SYSTEM or FLAG_LOCK");
+        }
+    }
+
+    private void extractInto(WallpaperColorsCompat inWallpaperColors, GradientColors outGradientColors) {
+        applyFallback(outGradientColors);
+        if (inWallpaperColors == null) {
+            return;
+        }
+        mExtractionType.extractInto(inWallpaperColors, outGradientColors);
+    }
+
+    private void applyFallback(GradientColors outGradientColors) {
+        outGradientColors.setMainColor(mMainFallbackColor);
+        outGradientColors.setSecondaryColor(mSecondaryFallbackColor);
+    }
+
+    public static class GradientColors {
+        private int mMainColor = FALLBACK_COLOR;
+        private int mSecondaryColor = FALLBACK_COLOR;
+        private boolean mSupportsDarkText;
+
+        public void setMainColor(int mainColor) {
+            mMainColor = mainColor;
+        }
+
+        public void setSecondaryColor(int secondaryColor) {
+            mSecondaryColor = secondaryColor;
+        }
+
+        public void setSupportsDarkText(boolean supportsDarkText) {
+            mSupportsDarkText = supportsDarkText;
+        }
+
+        public void set(GradientColors other) {
+            mMainColor = other.mMainColor;
+            mSecondaryColor = other.mSecondaryColor;
+            mSupportsDarkText = other.mSupportsDarkText;
+        }
+
+        public int getMainColor() {
+            return mMainColor;
+        }
+
+        public int getSecondaryColor() {
+            return mSecondaryColor;
+        }
+
+        public boolean supportsDarkText() {
+            return mSupportsDarkText;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == null || o.getClass() != getClass()) {
+                return false;
+            }
+            GradientColors other = (GradientColors) o;
+            return other.mMainColor == mMainColor &&
+                    other.mSecondaryColor == mSecondaryColor &&
+                    other.mSupportsDarkText == mSupportsDarkText;
+        }
+
+        @Override
+        public int hashCode() {
+            int code = mMainColor;
+            code = 31 * code + mSecondaryColor;
+            code = 31 * code + (mSupportsDarkText ? 0 : 1);
+            return code;
+        }
+    }
+}
+
diff --git a/src/com/android/launcher3/dynamicui/colorextraction/WallpaperColorsCompat.java b/src/com/android/launcher3/dynamicui/colorextraction/WallpaperColorsCompat.java
new file mode 100644
index 0000000..f80a675
--- /dev/null
+++ b/src/com/android/launcher3/dynamicui/colorextraction/WallpaperColorsCompat.java
@@ -0,0 +1,69 @@
+package com.android.launcher3.dynamicui.colorextraction;
+
+import android.graphics.Color;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+import java.util.List;
+
+/**
+ * A wrapper around platform implementation of WallpaperColors until the
+ * updated SDK is available.
+ *
+ * TODO remove this class if available by platform
+ */
+public class WallpaperColorsCompat implements Parcelable {
+
+    private final Parcelable mObject;
+
+    public WallpaperColorsCompat(Parcelable object) {
+        mObject = object;
+    }
+
+    private Object invokeMethod(String methodName) {
+        try {
+            return mObject.getClass().getDeclaredMethod(methodName).invoke(mObject);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int i) {
+        parcel.writeParcelable(mObject, i);
+    }
+
+    public static final Parcelable.Creator<WallpaperColorsCompat> CREATOR =
+            new Parcelable.Creator<WallpaperColorsCompat>() {
+                public WallpaperColorsCompat createFromParcel(Parcel source) {
+                    Parcelable object = source.readParcelable(null);
+                    return new WallpaperColorsCompat(object);
+                }
+
+                public WallpaperColorsCompat[] newArray(int size) {
+                    return new WallpaperColorsCompat[size];
+                }
+            };
+
+    public List<Pair<Color, Integer>> getColors() {
+        try {
+            return (List<Pair<Color, Integer>>) invokeMethod("getColors");
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public boolean supportsDarkText() {
+        try {
+            return (Boolean) invokeMethod("supportsDarkText");
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/dynamicui/colorextraction/types/ExtractionType.java b/src/com/android/launcher3/dynamicui/colorextraction/types/ExtractionType.java
new file mode 100644
index 0000000..166c7c6
--- /dev/null
+++ b/src/com/android/launcher3/dynamicui/colorextraction/types/ExtractionType.java
@@ -0,0 +1,23 @@
+package com.android.launcher3.dynamicui.colorextraction.types;
+
+import com.android.launcher3.dynamicui.colorextraction.ColorExtractor;
+import com.android.launcher3.dynamicui.colorextraction.WallpaperColorsCompat;
+
+
+/**
+ * Interface to allow various color extraction implementations.
+ *
+ * TODO remove this class if available by platform
+ */
+public interface ExtractionType {
+
+    /**
+     * Executes color extraction by reading WallpaperColors and setting
+     * main and secondary colors on GradientColors.
+     *
+     * @param inWallpaperColors where to read from
+     * @param outGradientColors object that should receive the colors
+     */
+    void extractInto(WallpaperColorsCompat inWallpaperColors,
+                     ColorExtractor.GradientColors outGradientColors);
+}
diff --git a/src/com/android/launcher3/dynamicui/colorextraction/types/Tonal.java b/src/com/android/launcher3/dynamicui/colorextraction/types/Tonal.java
new file mode 100644
index 0000000..1e165a3
--- /dev/null
+++ b/src/com/android/launcher3/dynamicui/colorextraction/types/Tonal.java
@@ -0,0 +1,299 @@
+package com.android.launcher3.dynamicui.colorextraction.types;
+
+import android.graphics.Color;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.graphics.ColorUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.launcher3.dynamicui.colorextraction.ColorExtractor;
+import com.android.launcher3.dynamicui.colorextraction.WallpaperColorsCompat;
+
+import java.util.Comparator;
+
+
+/**
+ * Implementation of tonal color extraction
+ *
+ * TODO remove this class if available by platform
+ */
+public class Tonal implements ExtractionType {
+    private static final String TAG = "Tonal";
+
+    // Used for tonal palette fitting
+    private static final float FIT_WEIGHT_H = 1.0f;
+    private static final float FIT_WEIGHT_S = 1.0f;
+    private static final float FIT_WEIGHT_L = 10.0f;
+
+    private static final float MIN_COLOR_OCCURRENCE = 0.1f;
+    private static final float MIN_LUMINOSITY = 0.5f;
+
+    public void extractInto(WallpaperColorsCompat wallpaperColors,
+                            ColorExtractor.GradientColors gradientColors) {
+        if (wallpaperColors.getColors().size() == 0) {
+            return;
+        }
+        // Tonal is not really a sort, it takes a color from the extracted
+        // palette and finds a best fit amongst a collection of pre-defined
+        // palettes. The best fit is tweaked to be closer to the source color
+        // and replaces the original palette
+
+        // First find the most representative color in the image
+        populationSort(wallpaperColors);
+        // Calculate total
+        int total = 0;
+        for (Pair<Color, Integer> weightedColor : wallpaperColors.getColors()) {
+            total += weightedColor.second;
+        }
+
+        // Get bright colors that occur often enough in this image
+        Pair<Color, Integer> bestColor = null;
+        float[] hsl = new float[3];
+        for (Pair<Color, Integer> weightedColor : wallpaperColors.getColors()) {
+            float colorOccurrence = weightedColor.second / (float) total;
+            if (colorOccurrence < MIN_COLOR_OCCURRENCE) {
+                break;
+            }
+
+            int colorValue = weightedColor.first.toArgb();
+            ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue),
+                    Color.blue(colorValue), hsl);
+            if (hsl[2] > MIN_LUMINOSITY) {
+                bestColor = weightedColor;
+            }
+        }
+
+        // Fallback to first color
+        if (bestColor == null) {
+            bestColor = wallpaperColors.getColors().get(0);
+        }
+
+        int colorValue = bestColor.first.toArgb();
+        ColorUtils.RGBToHSL(Color.red(colorValue), Color.green(colorValue), Color.blue(colorValue),
+                hsl);
+        hsl[0] /= 360.0f; // normalize
+
+        // TODO, we're finding a tonal palette for a hue, not all components
+        TonalPalette palette = findTonalPalette(hsl[0]);
+
+        // Fall back to population sort if we couldn't find a tonal palette
+        if (palette == null) {
+            Log.w(TAG, "Could not find a tonal palette!");
+            return;
+        }
+
+        int fitIndex = bestFit(palette, hsl[0], hsl[1], hsl[2]);
+        if (fitIndex == -1) {
+            Log.w(TAG, "Could not find best fit!");
+            return;
+        }
+        float[] h = fit(palette.h, hsl[0], fitIndex,
+                Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
+        float[] s = fit(palette.s, hsl[1], fitIndex, 0.0f, 1.0f);
+        float[] l = fit(palette.l, hsl[2], fitIndex, 0.0f, 1.0f);
+
+
+        hsl[0] = fract(h[0]) * 360.0f;
+        hsl[1] = s[0];
+        hsl[2] = l[0];
+        gradientColors.setMainColor(ColorUtils.HSLToColor(hsl));
+
+        hsl[0] = fract(h[1]) * 360.0f;
+        hsl[1] = s[1];
+        hsl[2] = l[1];
+        gradientColors.setSecondaryColor(ColorUtils.HSLToColor(hsl));
+    }
+
+    private static void populationSort(@NonNull WallpaperColorsCompat wallpaperColors) {
+        wallpaperColors.getColors().sort(new Comparator<Pair<Color, Integer>>() {
+            @Override
+            public int compare(Pair<Color, Integer> a, Pair<Color, Integer> b) {
+                return b.second - a.second;
+            }
+        });
+    }
+
+    /**
+     * Offsets all colors by a delta, clamping values that go beyond what's
+     * supported on the color space.
+     * @param data what you want to fit
+     * @param v how big should be the offset
+     * @param index which index to calculate the delta against
+     * @param min minimum accepted value (clamp)
+     * @param max maximum accepted value (clamp)
+     * @return
+     */
+    private static float[] fit(float[] data, float v, int index, float min, float max) {
+        float[] fitData = new float[data.length];
+        float delta = v - data[index];
+
+        for (int i = 0; i < data.length; i++) {
+            fitData[i] = constrain(data[i] + delta, min, max);
+        }
+
+        return fitData;
+    }
+
+    // TODO no MathUtils
+    private static float constrain(float x, float min, float max) {
+        x = Math.min(x, max);
+        x = Math.max(x, min);
+        return x;
+    }
+
+    /*function adjustSatLumForFit(val, points, fitIndex) {
+        var fitValue = lerpBetweenPoints(points, fitIndex);
+        var diff = val - fitValue;
+
+        var newPoints = [];
+        for (var ii=0; ii<points.length; ii++) {
+            var point = [points[ii][0], points[ii][1]];
+            point[1] += diff;
+            if (point[1] > 1) point[1] = 1;
+            if (point[1] < 0) point[1] = 0;
+            newPoints[ii] = point;
+        }
+        return newPoints;
+    }*/
+
+    /**
+     * Finds the closest color in a palette, given another HSL color
+     *
+     * @param palette where to search
+     * @param h hue
+     * @param s saturation
+     * @param l lightness
+     * @return closest index or -1 if palette is empty.
+     */
+    private static int bestFit(@NonNull TonalPalette palette, float h, float s, float l) {
+        int minErrorIndex = -1;
+        float minError = Float.POSITIVE_INFINITY;
+
+        for (int i = 0; i < palette.h.length; i++) {
+            float error =
+                    FIT_WEIGHT_H * Math.abs(h - palette.h[i])
+                            + FIT_WEIGHT_S * Math.abs(s - palette.s[i])
+                            + FIT_WEIGHT_L * Math.abs(l - palette.l[i]);
+            if (error < minError) {
+                minError = error;
+                minErrorIndex = i;
+            }
+        }
+
+        return minErrorIndex;
+    }
+
+    @Nullable
+    private static TonalPalette findTonalPalette(float h) {
+        TonalPalette best = null;
+        float error = Float.POSITIVE_INFINITY;
+
+        for (TonalPalette candidate : TONAL_PALETTES) {
+            if (h >= candidate.minHue && h <= candidate.maxHue) {
+                best = candidate;
+                break;
+            }
+
+            if (candidate.maxHue > 1.0f && h >= 0.0f && h <= fract(candidate.maxHue)) {
+                best = candidate;
+                break;
+            }
+
+            if (candidate.minHue < 0.0f && h >= fract(candidate.minHue) && h <= 1.0f) {
+                best = candidate;
+                break;
+            }
+
+            if (h <= candidate.minHue && candidate.minHue - h < error) {
+                best = candidate;
+                error = candidate.minHue - h;
+            } else if (h >= candidate.maxHue && h - candidate.maxHue < error) {
+                best = candidate;
+                error = h - candidate.maxHue;
+            } else if (candidate.maxHue > 1.0f && h >= fract(candidate.maxHue)
+                    && h - fract(candidate.maxHue) < error) {
+                best = candidate;
+                error = h - fract(candidate.maxHue);
+            } else if (candidate.minHue < 0.0f && h <= fract(candidate.minHue)
+                    && fract(candidate.minHue) - h < error) {
+                best = candidate;
+                error = fract(candidate.minHue) - h;
+            }
+        }
+
+        return best;
+    }
+
+    private static float fract(float v) {
+        return v - (float) Math.floor(v);
+    }
+
+    static class TonalPalette {
+        final float[] h;
+        final float[] s;
+        final float[] l;
+        final float minHue;
+        final float maxHue;
+
+        TonalPalette(float[] h, float[] s, float[] l) {
+            this.h = h;
+            this.s = s;
+            this.l = l;
+
+            float minHue = Float.POSITIVE_INFINITY;
+            float maxHue = Float.NEGATIVE_INFINITY;
+
+            for (float v : h) {
+                minHue = Math.min(v, minHue);
+                maxHue = Math.max(v, maxHue);
+            }
+
+            this.minHue = minHue;
+            this.maxHue = maxHue;
+        }
+    }
+
+    // Data definition of Material Design tonal palettes
+    // When the sort type is set to TONAL, these palettes are used to find
+    // a best fist. Each palette is defined as 10 HSL colors
+    private static final TonalPalette[] TONAL_PALETTES = {
+            // Orange
+            new TonalPalette(
+                    new float[] { 0.028f, 0.042f, 0.053f, 0.061f, 0.078f, 0.1f, 0.111f, 0.111f, 0.111f, 0.111f },
+                    new float[] { 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f },
+                    new float[] { 0.5f, 0.53f, 0.54f, 0.55f, 0.535f, 0.52f, 0.5f, 0.63f, 0.75f, 0.85f }
+            ),
+            // Yellow
+            new TonalPalette(
+                    new float[] { 0.111f, 0.111f, 0.125f, 0.133f, 0.139f, 0.147f, 0.156f, 0.156f, 0.156f, 0.156f },
+                    new float[] { 1f, 0.942f, 1f, 1f, 1f, 1f, 1f, 1f, 1f, 1f },
+                    new float[] { 0.43f, 0.484f, 0.535f, 0.555f, 0.57f, 0.575f, 0.595f, 0.715f, 0.78f, 0.885f }
+            ),
+            // Green
+            new TonalPalette(
+                    new float[] { 0.325f, 0.336f, 0.353f, 0.353f, 0.356f, 0.356f, 0.356f, 0.356f, 0.356f, 0.356f },
+                    new float[] { 1f, 1f, 0.852f, 0.754f, 0.639f, 0.667f, 0.379f, 0.542f, 1f, 1f },
+                    new float[] { 0.06f, 0.1f, 0.151f, 0.194f, 0.25f, 0.312f, 0.486f, 0.651f, 0.825f, 0.885f }
+            ),
+            // Blue
+            new TonalPalette(
+                    new float[] { 0.631f, 0.603f, 0.592f, 0.586f, 0.572f, 0.544f, 0.519f, 0.519f, 0.519f, 0.519f },
+                    new float[] { 0.852f, 1f, 0.887f, 0.852f, 0.871f, 0.907f, 0.949f, 0.934f, 0.903f, 0.815f },
+                    new float[] { 0.34f, 0.38f, 0.482f, 0.497f, 0.536f, 0.571f, 0.608f, 0.696f, 0.794f, 0.892f }
+            ),
+            // Purple
+            new TonalPalette(
+                    new float[] { 0.839f, 0.831f, 0.825f, 0.819f, 0.803f, 0.803f, 0.772f, 0.772f, 0.772f, 0.772f },
+                    new float[] { 1f, 1f, 1f, 1f, 1f, 1f, 0.769f, 0.701f, 0.612f, 0.403f },
+                    new float[] { 0.125f, 0.15f, 0.2f, 0.245f, 0.31f, 0.36f, 0.567f, 0.666f, 0.743f, 0.833f }
+            ),
+            // Red
+            new TonalPalette(
+                    new float[] { 0.964f, 0.975f, 0.975f, 0.975f, 0.972f, 0.992f, 1.003f, 1.011f, 1.011f, 1.011f },
+                    new float[] { 0.869f, 0.802f, 0.739f, 0.903f, 1f, 1f, 1f, 1f, 1f, 1f },
+                    new float[] { 0.241f, 0.316f, 0.46f, 0.586f, 0.655f, 0.7f, 0.75f, 0.8f, 0.84f, 0.88f }
+            )
+    };
+}
+