# All files / fast-colors/src component-state-color-palette.ts

16.98% Statements 9/53
0% Branches 0/6
0% Functions 0/3
17.31% Lines 9/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    1543x
    3x
    3x
    3x
    3x
    3x
    3x

    3x
    3x

     import { ColorRGBA64 } from "./color-rgba-64";
    import { ColorPalette } from "./color-palette";
    import { hslToRGB, rgbToHSL, rgbToLinearLuminance } from "./color-converters";
    import { ColorScale, ColorScaleStop } from "./color-scale";
    import { ColorHSL } from "./color-hsl";
    import { ColorInterpolationSpace } from "./color-interpolation";
    import { parseColorHexRGB } from "./parse-color";

    export interface ComponentStateColorPaletteConfig {
        baseColor?: ColorRGBA64;
        steps?: number;
    }

    export class ComponentStateColorPalette {
        public static readonly defaultPaletteConfig: ComponentStateColorPaletteConfig = {
            baseColor: parseColorHexRGB("#808080"),
            steps: 94,
        };

        public palette: ColorRGBA64[] = [];
        private readonly config: ComponentStateColorPaletteConfig;

        constructor(config?: ComponentStateColorPaletteConfig) {
            this.config = Object.assign(
                {},
                ComponentStateColorPalette.defaultPaletteConfig,
                config
            );
            this.regenPalettes();
        }

        private regenPalettes(): void {
            let steps: number = this.config.steps;
            if (isNaN(steps) || steps < 3) {
                steps = 3;
            }

            // This palette is tuned to go as dark as differences between the levels can be perceived according to tests
            // on numerous monitors in different conditions. Stay linear from white until this first cutoff.
            const darkLum: number = 0.14;

            // In the dark compression, this is the last luminance value before full black.
            const darkestLum: number = 0.06;

            // The Color for the luminance value above, placed on the ramp at it's normal position, so darker colors after
            // it can be compressed.
            const darkLumColor: ColorRGBA64 = new ColorRGBA64(darkLum, darkLum, darkLum, 1);

            // The number of steps in the ramp that has been tuned for default use. This coincides with the size of the
            // default ramp, but the palette could be generated with fewer steps to increase final contrast. This number
            // should however stay the same.
            const stepsForLuminanceRamp: number = 94;

            // Create the reference, dark-compressed, grey palette, like:
            // F------------------------------------------------------------------------------------[dark]------[darkest]0
            //                                                                                      |--compressed area--|
            const r: ColorPalette = new ColorPalette({
                ...ColorPalette.greyscalePaletteConfig,
                baseColor: darkLumColor,
                baseScalePosition: ((1 - darkLum) * 100) / stepsForLuminanceRamp,
                steps,
            });
            const referencePalette: ColorRGBA64[] = r.palette;

            // Find the requested base color on the adjusted luminance reference ramp.
            // There is no _right_ way to desaturate a color, and both methods we've tested have value, so average them out.
            const baseColorLum1: number = rgbToLinearLuminance(this.config.baseColor);
            const baseColorLum2: number = rgbToHSL(this.config.baseColor).l;
            const baseColorLum: number = (baseColorLum1 + baseColorLum2) / 2;
            const baseColorRefIndex: number = this.matchRelativeLuminanceIndex(
                baseColorLum,
                referencePalette
            );
            const baseColorPercent: number = baseColorRefIndex / (steps - 1);

            // Find the luminance location for the dark cutoff.
            const darkRefIndex: number = this.matchRelativeLuminanceIndex(
                darkLum,
                referencePalette
            );
            const darkPercent: number = darkRefIndex / (steps - 1);

            // Issue https://github.com/microsoft/fast-dna/issues/1904
            // Creating a color from H, S, and a known L value is not the inverse of getting the relative
            // luminace as above. Need to derive a relative luminance version of the color to better match on the dark end.

            // Find the dark cutoff and darkest variations of the requested base color.
            const baseColorHSL: ColorHSL = rgbToHSL(this.config.baseColor);
            const darkBaseColor: ColorRGBA64 = hslToRGB(
                ColorHSL.fromObject({
                    h: baseColorHSL.h,
                    s: baseColorHSL.s,
                    l: darkLum,
                })
            );
            const darkestBaseColor: ColorRGBA64 = hslToRGB(
                ColorHSL.fromObject({
                    h: baseColorHSL.h,
                    s: baseColorHSL.s,
                    l: darkestLum,
                })
            );

            // Create the gradient stops, including the base color and anchor colors for the dark end compression.
            const fullColorScaleStops: ColorScaleStop[] = new Array(5);
            fullColorScaleStops[0] = {
                position: 0,
                color: new ColorRGBA64(1, 1, 1, 1),
            };
            fullColorScaleStops[1] = {
                position: baseColorPercent,
                color: this.config.baseColor,
            };
            fullColorScaleStops[2] = {
                position: darkPercent,
                color: darkBaseColor,
            };
            fullColorScaleStops[3] = {
                position: 0.99,
                color: darkestBaseColor,
            };
            fullColorScaleStops[4] = {
                position: 1,
                color: new ColorRGBA64(0, 0, 0, 1),
            };
            const scale: ColorScale = new ColorScale(fullColorScaleStops);

            // Create the palette.
            this.palette = new Array(steps);
            for (let i: number = 0; i < steps; i++) {
                const c: ColorRGBA64 = scale.getColor(
                    i / (steps - 1),
                    ColorInterpolationSpace.RGB
                );
                this.palette[i] = c;
            }
        }

        private matchRelativeLuminanceIndex(input: number, reference: ColorRGBA64[]): number {
            let bestFitValue: number = Number.MAX_VALUE;
            let bestFitIndex: number = 0;
            let i: number = 0;
            const referenceLength: number = reference.length;
            for (; i < referenceLength; i++) {
                const fitValue: number = Math.abs(rgbToLinearLuminance(reference[i]) - input);
                if (fitValue < bestFitValue) {
                    bestFitValue = fitValue;
                    bestFitIndex = i;
                }
            }
            return bestFitIndex;
        }
    }