UNPKG

@w3h/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.

162 lines 5.81 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'; /** * A convenience class for retrieving colors that are constant in hue and * chroma, but vary in tone. */ export class TonalPalette { /** * @param argb ARGB representation of a color * @return Tones matching that color's hue and chroma. */ static fromInt(argb) { const hct = Hct.fromInt(argb); return TonalPalette.fromHct(hct); } /** * @param hct Hct * @return Tones matching that color's hue and chroma. */ static fromHct(hct) { return new TonalPalette(hct.hue, hct.chroma, hct); } /** * @param hue HCT hue * @param chroma HCT chroma * @return Tones matching hue and chroma. */ static fromHueAndChroma(hue, chroma) { const keyColor = new KeyColor(hue, chroma).create(); return new TonalPalette(hue, chroma, keyColor); } constructor(hue, chroma, keyColor) { this.hue = hue; this.chroma = chroma; this.keyColor = keyColor; this.cache = new Map(); } /** * @param tone HCT tone, measured from 0 to 100. * @return ARGB representation of a color with that tone. */ tone(tone) { let argb = this.cache.get(tone); if (argb === undefined) { if (tone == 99 && Hct.isYellow(this.hue)) { argb = this.averageArgb(this.tone(98), this.tone(100)); } else { argb = Hct.from(this.hue, this.chroma, tone).toInt(); } this.cache.set(tone, argb); } return argb; } /** * @param tone HCT tone. * @return HCT representation of a color with that tone. */ getHct(tone) { return Hct.fromInt(this.tone(tone)); } averageArgb(argb1, argb2) { const red1 = (argb1 >>> 16) & 0xff; const green1 = (argb1 >>> 8) & 0xff; const blue1 = argb1 & 0xff; const red2 = (argb2 >>> 16) & 0xff; const green2 = (argb2 >>> 8) & 0xff; const blue2 = argb2 & 0xff; const red = Math.round((red1 + red2) / 2); const green = Math.round((green1 + green2) / 2); const blue = Math.round((blue1 + blue2) / 2); return (255 << 24 | (red & 255) << 16 | (green & 255) << 8 | (blue & 255)) >>> 0; } } /** * Key color is a color that represents the hue and chroma of a tonal palette */ class KeyColor { constructor(hue, requestedChroma) { this.hue = hue; this.requestedChroma = requestedChroma; // Cache that maps tone to max chroma to avoid duplicated HCT calculation. this.chromaCache = new Map(); this.maxChromaValue = 200.0; } /** * Creates a key color from a [hue] and a [chroma]. * The key color is the first tone, starting from T50, matching the given hue * and chroma. * * @return Key color [Hct] */ create() { // Pivot around T50 because T50 has the most chroma available, on // average. Thus it is most likely to have a direct answer. const pivotTone = 50; const toneStepSize = 1; // Epsilon to accept values slightly higher than the requested chroma. const epsilon = 0.01; // Binary search to find the tone that can provide a chroma that is closest // to the requested chroma. let lowerTone = 0; let upperTone = 100; while (lowerTone < upperTone) { const midTone = Math.floor((lowerTone + upperTone) / 2); const isAscending = this.maxChroma(midTone) < this.maxChroma(midTone + toneStepSize); const sufficientChroma = this.maxChroma(midTone) >= this.requestedChroma - epsilon; if (sufficientChroma) { // Either range [lowerTone, midTone] or [midTone, upperTone] has // the answer, so search in the range that is closer the pivot tone. if (Math.abs(lowerTone - pivotTone) < Math.abs(upperTone - pivotTone)) { upperTone = midTone; } else { if (lowerTone === midTone) { return Hct.from(this.hue, this.requestedChroma, lowerTone); } lowerTone = midTone; } } else { // As there is no sufficient chroma in the midTone, follow the direction // to the chroma peak. if (isAscending) { lowerTone = midTone + toneStepSize; } else { // Keep midTone for potential chroma peak. upperTone = midTone; } } } return Hct.from(this.hue, this.requestedChroma, lowerTone); } // Find the maximum chroma for a given tone maxChroma(tone) { if (this.chromaCache.has(tone)) { return this.chromaCache.get(tone); } const chroma = Hct.from(this.hue, this.maxChromaValue, tone).chroma; this.chromaCache.set(tone, chroma); return chroma; } } //# sourceMappingURL=tonal_palette.js.map