UNPKG

@poupe/material-color-utilities

Version:

Algorithms and utilities that power the Material Design 3 (M3) color system, including choosing theme colors from images and creating tones of colors; all in a new color space.

132 lines 5.64 kB
/** * @license * Copyright 2021 Google LLC * * 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. */ import { Hct } from '../hct/hct.js'; import * as math from '../utils/math_utils.js'; const SCORE_OPTION_DEFAULTS = { desired: 4, fallbackColorARGB: 0xff4285f4, filter: true, // Avoid unsuitable colors. }; function compare(a, b) { if (a.score > b.score) { return -1; } else if (a.score < b.score) { return 1; } return 0; } /** * Given a large set of colors, remove colors that are unsuitable for a UI * theme, and rank the rest based on suitability. * * Enables use of a high cluster count for image quantization, thus ensuring * colors aren't muddied, while curating the high cluster count to a much * smaller number of appropriate choices. */ export class Score { constructor() { } /** * Given a map with keys of colors and values of how often the color appears, * rank the colors based on suitability for being used for a UI theme. * * @param colorsToPopulation map with keys of colors and values of how often * the color appears, usually from a source image. * @param {ScoreOptions} options optional parameters. * @return Colors sorted by suitability for a UI theme. The most suitable * color is the first item, the least suitable is the last. There will * always be at least one color returned. If all the input colors * were not suitable for a theme, a default fallback color will be * provided, Google Blue. */ static score(colorsToPopulation, options) { const { desired, fallbackColorARGB, filter } = { ...SCORE_OPTION_DEFAULTS, ...options }; // Get the HCT color for each Argb value, while finding the per hue count and // total count. const colorsHct = []; const huePopulation = new Array(360).fill(0); let populationSum = 0; for (const [argb, population] of colorsToPopulation.entries()) { const hct = Hct.fromInt(argb); colorsHct.push(hct); const hue = Math.floor(hct.hue); huePopulation[hue] += population; populationSum += population; } // Hues with more usage in neighboring 30 degree slice get a larger number. const hueExcitedProportions = new Array(360).fill(0.0); for (let hue = 0; hue < 360; hue++) { const proportion = huePopulation[hue] / populationSum; for (let i = hue - 14; i < hue + 16; i++) { const neighborHue = math.sanitizeDegreesInt(i); hueExcitedProportions[neighborHue] += proportion; } } // Scores each HCT color based on usage and chroma, while optionally // filtering out values that do not have enough chroma or usage. const scoredHct = new Array(); for (const hct of colorsHct) { const hue = math.sanitizeDegreesInt(Math.round(hct.hue)); const proportion = hueExcitedProportions[hue]; if (filter && (hct.chroma < Score.CUTOFF_CHROMA || proportion <= Score.CUTOFF_EXCITED_PROPORTION)) { continue; } const proportionScore = proportion * 100.0 * Score.WEIGHT_PROPORTION; const chromaWeight = hct.chroma < Score.TARGET_CHROMA ? Score.WEIGHT_CHROMA_BELOW : Score.WEIGHT_CHROMA_ABOVE; const chromaScore = (hct.chroma - Score.TARGET_CHROMA) * chromaWeight; const score = proportionScore + chromaScore; scoredHct.push({ hct, score }); } // Sorted so that colors with higher scores come first. scoredHct.sort(compare); // Iterates through potential hue differences in degrees in order to select // the colors with the largest distribution of hues possible. Starting at // 90 degrees(maximum difference for 4 colors) then decreasing down to a // 15 degree minimum. const chosenColors = []; for (let differenceDegrees = 90; differenceDegrees >= 15; differenceDegrees--) { chosenColors.length = 0; for (const { hct } of scoredHct) { const duplicateHue = chosenColors.find(chosenHct => { return math.differenceDegrees(hct.hue, chosenHct.hue) < differenceDegrees; }); if (!duplicateHue) { chosenColors.push(hct); } if (chosenColors.length >= desired) break; } if (chosenColors.length >= desired) break; } const colors = []; if (chosenColors.length === 0) { colors.push(fallbackColorARGB); } for (const chosenHct of chosenColors) { colors.push(chosenHct.toInt()); } return colors; } } Score.TARGET_CHROMA = 48.0; // A1 Chroma Score.WEIGHT_PROPORTION = 0.7; Score.WEIGHT_CHROMA_ABOVE = 0.3; Score.WEIGHT_CHROMA_BELOW = 0.1; Score.CUTOFF_CHROMA = 5.0; Score.CUTOFF_EXCITED_PROPORTION = 0.01; //# sourceMappingURL=score.js.map