@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
JavaScript
/**
* @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