@material/material-color-utilities
Advanced tools
Comparing version 0.2.6 to 0.2.7
@@ -20,4 +20,6 @@ /** | ||
import { DynamicScheme } from '../scheme/dynamic_scheme.js'; | ||
import { ToneDeltaConstraint } from './tone_delta_constraint.js'; | ||
import { ContrastCurve } from './contrast_curve.js'; | ||
import { ToneDeltaPair } from './tone_delta_pair.js'; | ||
/** | ||
* @param name The name of the dynamic color. Defaults to empty. | ||
* @param palette Function that provides a TonalPalette given | ||
@@ -27,85 +29,27 @@ * DynamicScheme. A TonalPalette is defined by a hue and chroma, so this | ||
* contrast adjustments are made, intended chroma can be preserved. | ||
* @param tone Function that provides a tone given DynamicScheme. (useful | ||
* for dark vs. light mode) | ||
* @param tone Function that provides a tone given DynamicScheme. | ||
* @param isBackground Whether this dynamic color is a background, with | ||
* some other color as the foreground. Defaults to false. | ||
* @param background The background of the dynamic color (as a function of a | ||
* `DynamicScheme`), if it exists. | ||
* @param secondBackground A second background of the dynamic color (as a | ||
* function of a `DynamicScheme`), if it | ||
* exists. | ||
* @param contrastCurve A `ContrastCurve` object specifying how its contrast | ||
* against its background should behave in various contrast levels options. | ||
* @param toneDeltaPair A `ToneDeltaPair` object specifying a tone delta | ||
* constraint between two colors. One of them must be the color being | ||
* constructed. | ||
*/ | ||
interface FromPaletteOptions extends BaseOptions { | ||
interface FromPaletteOptions { | ||
name?: string; | ||
palette: (scheme: DynamicScheme) => TonalPalette; | ||
tone: (scheme: DynamicScheme) => number; | ||
} | ||
/** | ||
* @param hue Function with DynamicScheme input and HCT hue output. | ||
* @param chroma Function with DynamicScheme input and HCT chroma output. | ||
* @param tone Function with DynamicScheme input and HCT tone output. | ||
*/ | ||
interface FromHueAndChromaOptions extends BaseOptions { | ||
hue: (scheme: DynamicScheme) => number; | ||
chroma: (scheme: DynamicScheme) => number; | ||
tone: (scheme: DynamicScheme) => number; | ||
} | ||
/** | ||
* @param argb Function with DynamicScheme input and ARGB/hex code output. | ||
* @param [tone=null] Function with DynamicScheme input and HCT tone output. If | ||
* provided, overrides tone of argb parameter. | ||
*/ | ||
interface FromArgbOptions extends BaseOptions { | ||
argb: (scheme: DynamicScheme) => number; | ||
tone?: (scheme: DynamicScheme) => number; | ||
} | ||
/** | ||
* @param tone The tone standard. | ||
* @param scheme The scheme in which to adjust the tone. | ||
*/ | ||
interface ToneContrastOptions extends BaseOptions { | ||
tone: (scheme: DynamicScheme) => number; | ||
scheme: DynamicScheme; | ||
} | ||
/** | ||
* @param [background=null] Function that provides background | ||
* DynamicColor given DynamicScheme. Useful for contrast, given a background, | ||
* colors can adjust to increase/decrease contrast. | ||
* @param [toneDeltaConstraint=null] Function that provides a | ||
* ToneDeltaConstraint given DynamicScheme. Useful for ensuring lightness | ||
* difference between colors that don't require contrast or have a formal | ||
* background/foreground relationship. | ||
*/ | ||
interface BaseOptions { | ||
isBackground?: boolean; | ||
background?: (scheme: DynamicScheme) => DynamicColor; | ||
toneDeltaConstraint?: (scheme: DynamicScheme) => ToneDeltaConstraint; | ||
secondBackground?: (scheme: DynamicScheme) => DynamicColor; | ||
contrastCurve?: ContrastCurve; | ||
toneDeltaPair?: (scheme: DynamicScheme) => ToneDeltaPair; | ||
} | ||
/** | ||
* @param scheme Defines the conditions of the user interface, for example, | ||
* whether or not it is dark mode or light mode, and what the desired | ||
* contrast level is. | ||
* @param toneStandard Function with input of DynamicScheme that outputs the | ||
* tone to be used at default contrast. | ||
* @param toneToJudge Function with input of DynamicColor that outputs tone the | ||
* color is in the current UI state. Used to determine the tone of the | ||
* background. | ||
* @param desiredTone Function with inputs of contrast ratio with background at | ||
* default contrast and the background tone at current contrast level. Outputs | ||
* tone. | ||
* @param [background] Optional, function with input of DynamicScheme that | ||
* returns a DynamicColor that is the background of the color whose tone is | ||
* being calculated. | ||
* @param [constraint] Optional, function with input of DynamicScheme that | ||
* returns a ToneDeltaConstraint. If provided, the ToneDeltaConstraint is | ||
* enforced. | ||
* @param [minRatio] Optional, function with input of DynamicScheme that returns | ||
* the minimum contrast ratio between background and the color whose tone is | ||
* being calculated. | ||
* @param [maxRatio] Optional, function with input of DynamicScheme that returns | ||
* the maximum contrast ratio between background and the color whose tone is | ||
* being calculated. | ||
*/ | ||
interface CalculateDynamicToneOptions { | ||
scheme: DynamicScheme; | ||
toneStandard: (scheme: DynamicScheme) => number; | ||
toneToJudge: (dynamicColor: DynamicColor) => number; | ||
desiredTone: (standardRatio: number, bgTone: number) => number; | ||
background?: (scheme: DynamicScheme) => DynamicColor; | ||
toneDeltaConstraint?: (scheme: DynamicScheme) => ToneDeltaConstraint; | ||
minRatio?: (scheme: number) => number; | ||
maxRatio?: (scheme: number) => number; | ||
} | ||
/** | ||
* A color that adjusts itself based on UI state provided by DynamicScheme. | ||
@@ -122,9 +66,10 @@ * | ||
export declare class DynamicColor { | ||
readonly hue: (scheme: DynamicScheme) => number; | ||
readonly chroma: (scheme: DynamicScheme) => number; | ||
readonly name: string; | ||
readonly palette: (scheme: DynamicScheme) => TonalPalette; | ||
readonly tone: (scheme: DynamicScheme) => number; | ||
readonly toneMinContrast: (scheme: DynamicScheme) => number; | ||
readonly toneMaxContrast: (scheme: DynamicScheme) => number; | ||
readonly isBackground: boolean; | ||
readonly background?: (scheme: DynamicScheme) => DynamicColor; | ||
readonly toneDeltaConstraint?: (scheme: DynamicScheme) => ToneDeltaConstraint; | ||
readonly secondBackground?: (scheme: DynamicScheme) => DynamicColor; | ||
readonly contrastCurve?: ContrastCurve; | ||
readonly toneDeltaPair?: (scheme: DynamicScheme) => ToneDeltaPair; | ||
private readonly hctCache; | ||
@@ -139,18 +84,2 @@ /** | ||
/** | ||
* Create a DynamicColor defined by a HCT hue, chroma, and tone. | ||
* | ||
* @param args Functions with DynamicScheme as input. Must provide hue, | ||
* chroma, and tone. May provide background DynamicColor and | ||
* ToneDeltaConstraint. | ||
*/ | ||
static fromHueAndChroma(args: FromHueAndChromaOptions): DynamicColor; | ||
/** | ||
* Create a DynamicColor from a ARGB color (hex code). | ||
* | ||
* @param args Functions with DynamicScheme as input. Must provide ARGB (hex | ||
* code). May provide tone that overrides hex code's, background DynamicColor, | ||
* and ToneDeltaConstraint. | ||
*/ | ||
static fromArgb(args: FromArgbOptions): DynamicColor; | ||
/** | ||
* The base constructor for DynamicColor. | ||
@@ -168,31 +97,22 @@ * | ||
* | ||
* @param hue given DynamicScheme, return the hue in HCT of the output | ||
* color. | ||
* @param chroma given DynamicScheme, return chroma in HCT of the output | ||
* color. | ||
* @param tone given DynamicScheme, return tone in HCT of the output color. | ||
* This tone is used for standard contrast. | ||
* @param toneMinContrast given DynamicScheme, return tone in HCT this color | ||
* should be at minimum contrast. See toneMinContrastDefault for the default | ||
* behavior, and strongly consider using it unless you have strong opinions | ||
* on color and accessibility. The convenience constructors use it. | ||
* @param toneMaxContrast given DynamicScheme, return tone in HCT this color | ||
* should be at maximum contrast. See toneMaxContrastDefault for the default | ||
* behavior, and strongly consider using it unless you have strong opinions | ||
* on color and accessibility. The convenience constructors use it. | ||
* @param background given DynamicScheme, return the DynamicColor that is | ||
* the background of this DynamicColor. When this is provided, | ||
* automated adjustments to lower and raise contrast are made. | ||
* @param toneDeltaConstraint given DynamicScheme, return a | ||
* ToneDeltaConstraint that describes a requirement that this | ||
* DynamicColor must always have some difference in tone from another | ||
* DynamicColor. | ||
* | ||
* Unlikely to be useful unless a design system has some distortions | ||
* where colors that don't have a background/foreground relationship | ||
* don't want to have a formal relationship or a principled value for their | ||
* tone distance based on common contrast / tone delta values, yet, want | ||
* tone distance. | ||
* @param name The name of the dynamic color. Defaults to empty. | ||
* @param palette Function that provides a TonalPalette given | ||
* DynamicScheme. A TonalPalette is defined by a hue and chroma, so this | ||
* replaces the need to specify hue/chroma. By providing a tonal palette, when | ||
* contrast adjustments are made, intended chroma can be preserved. | ||
* @param tone Function that provides a tone, given a DynamicScheme. | ||
* @param isBackground Whether this dynamic color is a background, with | ||
* some other color as the foreground. Defaults to false. | ||
* @param background The background of the dynamic color (as a function of a | ||
* `DynamicScheme`), if it exists. | ||
* @param secondBackground A second background of the dynamic color (as a | ||
* function of a `DynamicScheme`), if it | ||
* exists. | ||
* @param contrastCurve A `ContrastCurve` object specifying how its contrast | ||
* against its background should behave in various contrast levels options. | ||
* @param toneDeltaPair A `ToneDeltaPair` object specifying a tone delta | ||
* constraint between two colors. One of them must be the color being | ||
* constructed. | ||
*/ | ||
constructor(hue: (scheme: DynamicScheme) => number, chroma: (scheme: DynamicScheme) => number, tone: (scheme: DynamicScheme) => number, toneMinContrast: (scheme: DynamicScheme) => number, toneMaxContrast: (scheme: DynamicScheme) => number, background?: (scheme: DynamicScheme) => DynamicColor, toneDeltaConstraint?: (scheme: DynamicScheme) => ToneDeltaConstraint); | ||
constructor(name: string, palette: (scheme: DynamicScheme) => TonalPalette, tone: (scheme: DynamicScheme) => number, isBackground: boolean, background?: (scheme: DynamicScheme) => DynamicColor, secondBackground?: (scheme: DynamicScheme) => DynamicColor, contrastCurve?: ContrastCurve, toneDeltaPair?: (scheme: DynamicScheme) => ToneDeltaPair); | ||
/** | ||
@@ -225,16 +145,2 @@ * Return a ARGB integer (i.e. a hex code). | ||
/** | ||
* Enforce a ToneDeltaConstraint between two DynamicColors. | ||
* | ||
* @param tone The desired tone of the color. | ||
* @param toneStandard The tone of the color at standard contrast. | ||
* @param scheme Defines the conditions of the user interface, for example, | ||
* whether or not it is dark mode or light mode, and what the desired | ||
* contrast level is. | ||
* @param constraintProvider Given a DynamicScheme, return a | ||
* ToneDeltaConstraint or null. | ||
* @param toneToDistanceFrom Given a DynamicColor, return a tone that the | ||
* ToneDeltaConstraint should enforce a delta from. | ||
*/ | ||
static ensureToneDelta(tone: number, toneStandard: number, scheme: DynamicScheme, constraintProvider?: (scheme: DynamicScheme) => ToneDeltaConstraint | null, toneToDistanceFrom?: (color: DynamicColor) => number): number; | ||
/** | ||
* Given a background tone, find a foreground tone, while ensuring they reach | ||
@@ -250,26 +156,2 @@ * a contrast ratio that is as close to [ratio] as possible. | ||
/** | ||
* Core method for calculating a tone for under dynamic contrast. | ||
* | ||
* It calculates tone while enforcing these properties: | ||
* #1. Desired contrast ratio is reached. | ||
* #2. Darken to enable light foregrounds on midtones. | ||
* #3. Enforce tone delta constraint, if needed. | ||
*/ | ||
static calculateDynamicTone(args: CalculateDynamicToneOptions): number; | ||
/** | ||
* Default algorithm for calculating the tone of a color at maximum contrast. | ||
* | ||
* If the color's background has a background, reach contrast 7.0. | ||
* If it doesn't, maintain the original contrast ratio. | ||
*/ | ||
static toneMaxContrastDefault(args: ToneContrastOptions): number; | ||
/** | ||
* Default algorithm for calculating the tone of a color at minimum contrast. | ||
* | ||
* If the original contrast ratio was >= 7.0, reach contrast 4.5. | ||
* If the original contrast ratio was >= 3.0, reach contrast 3.0. | ||
* If the original contrast ratio was < 3.0, reach that ratio. | ||
*/ | ||
static toneMinContrastDefault(args: ToneContrastOptions): number; | ||
/** | ||
* Returns whether [tone] prefers a light foreground. | ||
@@ -276,0 +158,0 @@ * |
@@ -18,3 +18,2 @@ /** | ||
import { Contrast } from '../contrast/contrast.js'; | ||
import { Hct } from '../hct/hct.js'; | ||
import * as math from '../utils/math_utils.js'; | ||
@@ -40,67 +39,5 @@ /** | ||
static fromPalette(args) { | ||
return new DynamicColor((scheme) => args.palette(scheme).hue, (scheme) => args.palette(scheme).chroma, args.tone, (scheme) => DynamicColor.toneMinContrastDefault({ | ||
tone: args.tone, | ||
scheme, | ||
background: args.background, | ||
toneDeltaConstraint: args.toneDeltaConstraint, | ||
}), (scheme) => DynamicColor.toneMaxContrastDefault({ | ||
tone: args.tone, | ||
scheme, | ||
background: args.background, | ||
toneDeltaConstraint: args.toneDeltaConstraint, | ||
}), args.background, args.toneDeltaConstraint); | ||
return new DynamicColor(args.name ?? '', args.palette, args.tone, args.isBackground ?? false, args.background, args.secondBackground, args.contrastCurve, args.toneDeltaPair); | ||
} | ||
/** | ||
* Create a DynamicColor defined by a HCT hue, chroma, and tone. | ||
* | ||
* @param args Functions with DynamicScheme as input. Must provide hue, | ||
* chroma, and tone. May provide background DynamicColor and | ||
* ToneDeltaConstraint. | ||
*/ | ||
static fromHueAndChroma(args) { | ||
return new DynamicColor(args.hue, args.chroma, args.tone, (scheme) => DynamicColor.toneMinContrastDefault({ | ||
tone: args.tone, | ||
scheme, | ||
background: args.background, | ||
toneDeltaConstraint: args.toneDeltaConstraint, | ||
}), (scheme) => DynamicColor.toneMaxContrastDefault({ | ||
tone: args.tone, | ||
scheme, | ||
background: args.background, | ||
toneDeltaConstraint: args.toneDeltaConstraint, | ||
}), args.background, args.toneDeltaConstraint); | ||
} | ||
/** | ||
* Create a DynamicColor from a ARGB color (hex code). | ||
* | ||
* @param args Functions with DynamicScheme as input. Must provide ARGB (hex | ||
* code). May provide tone that overrides hex code's, background DynamicColor, | ||
* and ToneDeltaConstraint. | ||
*/ | ||
static fromArgb(args) { | ||
return new DynamicColor((scheme) => { | ||
const hct = Hct.fromInt(args.argb(scheme)); | ||
return hct.hue; | ||
}, (scheme) => { | ||
const hct = Hct.fromInt(args.argb(scheme)); | ||
return hct.chroma; | ||
}, (scheme) => { | ||
return args.tone?.(scheme) ?? Hct.fromInt(args.argb(scheme)).tone; | ||
}, (scheme) => { | ||
return DynamicColor.toneMinContrastDefault({ | ||
tone: (scheme) => args.tone?.(scheme) ?? Hct.fromInt(args.argb(scheme)).tone, | ||
scheme, | ||
background: args.background, | ||
toneDeltaConstraint: args.toneDeltaConstraint, | ||
}); | ||
}, (scheme) => { | ||
return DynamicColor.toneMaxContrastDefault({ | ||
tone: (scheme) => args.tone?.(scheme) ?? Hct.fromInt(args.argb(scheme)).tone, | ||
scheme, | ||
background: args.background, | ||
toneDeltaConstraint: args.toneDeltaConstraint, | ||
}); | ||
}, args.background, args.toneDeltaConstraint); | ||
} | ||
/** | ||
* The base constructor for DynamicColor. | ||
@@ -118,39 +55,43 @@ * | ||
* | ||
* @param hue given DynamicScheme, return the hue in HCT of the output | ||
* color. | ||
* @param chroma given DynamicScheme, return chroma in HCT of the output | ||
* color. | ||
* @param tone given DynamicScheme, return tone in HCT of the output color. | ||
* This tone is used for standard contrast. | ||
* @param toneMinContrast given DynamicScheme, return tone in HCT this color | ||
* should be at minimum contrast. See toneMinContrastDefault for the default | ||
* behavior, and strongly consider using it unless you have strong opinions | ||
* on color and accessibility. The convenience constructors use it. | ||
* @param toneMaxContrast given DynamicScheme, return tone in HCT this color | ||
* should be at maximum contrast. See toneMaxContrastDefault for the default | ||
* behavior, and strongly consider using it unless you have strong opinions | ||
* on color and accessibility. The convenience constructors use it. | ||
* @param background given DynamicScheme, return the DynamicColor that is | ||
* the background of this DynamicColor. When this is provided, | ||
* automated adjustments to lower and raise contrast are made. | ||
* @param toneDeltaConstraint given DynamicScheme, return a | ||
* ToneDeltaConstraint that describes a requirement that this | ||
* DynamicColor must always have some difference in tone from another | ||
* DynamicColor. | ||
* | ||
* Unlikely to be useful unless a design system has some distortions | ||
* where colors that don't have a background/foreground relationship | ||
* don't want to have a formal relationship or a principled value for their | ||
* tone distance based on common contrast / tone delta values, yet, want | ||
* tone distance. | ||
* @param name The name of the dynamic color. Defaults to empty. | ||
* @param palette Function that provides a TonalPalette given | ||
* DynamicScheme. A TonalPalette is defined by a hue and chroma, so this | ||
* replaces the need to specify hue/chroma. By providing a tonal palette, when | ||
* contrast adjustments are made, intended chroma can be preserved. | ||
* @param tone Function that provides a tone, given a DynamicScheme. | ||
* @param isBackground Whether this dynamic color is a background, with | ||
* some other color as the foreground. Defaults to false. | ||
* @param background The background of the dynamic color (as a function of a | ||
* `DynamicScheme`), if it exists. | ||
* @param secondBackground A second background of the dynamic color (as a | ||
* function of a `DynamicScheme`), if it | ||
* exists. | ||
* @param contrastCurve A `ContrastCurve` object specifying how its contrast | ||
* against its background should behave in various contrast levels options. | ||
* @param toneDeltaPair A `ToneDeltaPair` object specifying a tone delta | ||
* constraint between two colors. One of them must be the color being | ||
* constructed. | ||
*/ | ||
constructor(hue, chroma, tone, toneMinContrast, toneMaxContrast, background, toneDeltaConstraint) { | ||
this.hue = hue; | ||
this.chroma = chroma; | ||
constructor(name, palette, tone, isBackground, background, secondBackground, contrastCurve, toneDeltaPair) { | ||
this.name = name; | ||
this.palette = palette; | ||
this.tone = tone; | ||
this.toneMinContrast = toneMinContrast; | ||
this.toneMaxContrast = toneMaxContrast; | ||
this.isBackground = isBackground; | ||
this.background = background; | ||
this.toneDeltaConstraint = toneDeltaConstraint; | ||
this.secondBackground = secondBackground; | ||
this.contrastCurve = contrastCurve; | ||
this.toneDeltaPair = toneDeltaPair; | ||
this.hctCache = new Map(); | ||
if ((!background) && secondBackground) { | ||
throw new Error(`Color ${name} has secondBackground` + | ||
`defined, but background is not defined.`); | ||
} | ||
if ((!background) && contrastCurve) { | ||
throw new Error(`Color ${name} has contrastCurve` + | ||
`defined, but background is not defined.`); | ||
} | ||
if (background && !contrastCurve) { | ||
throw new Error(`Color ${name} has background` + | ||
`defined, but contrastCurve is not defined.`); | ||
} | ||
} | ||
@@ -180,3 +121,4 @@ /** | ||
} | ||
const answer = Hct.from(this.hue(scheme), this.chroma(scheme), this.getTone(scheme)); | ||
const tone = this.getTone(scheme); | ||
const answer = this.palette(scheme).getHct(tone); | ||
if (this.hctCache.size > 4) { | ||
@@ -197,79 +139,153 @@ this.hctCache.clear(); | ||
getTone(scheme) { | ||
let answer = this.tone(scheme); | ||
const decreasingContrast = scheme.contrastLevel < 0.0; | ||
if (scheme.contrastLevel !== 0.0) { | ||
const startTone = this.tone(scheme); | ||
const endTone = decreasingContrast ? this.toneMinContrast(scheme) : | ||
this.toneMaxContrast(scheme); | ||
const delta = (endTone - startTone) * Math.abs(scheme.contrastLevel); | ||
answer = delta + startTone; | ||
} | ||
const bg = this.background?.(scheme); | ||
let standardRatio; | ||
let minRatio; | ||
let maxRatio; | ||
if (bg != null) { | ||
const bgHasBg = bg?.background?.(scheme) != null; | ||
standardRatio = Contrast.ratioOfTones(this.tone(scheme), bg.tone(scheme)); | ||
const decreasingContrast = scheme.contrastLevel < 0; | ||
// Case 1: dual foreground, pair of colors with delta constraint. | ||
if (this.toneDeltaPair) { | ||
const toneDeltaPair = this.toneDeltaPair(scheme); | ||
const roleA = toneDeltaPair.roleA; | ||
const roleB = toneDeltaPair.roleB; | ||
const delta = toneDeltaPair.delta; | ||
const polarity = toneDeltaPair.polarity; | ||
const stayTogether = toneDeltaPair.stayTogether; | ||
const bg = this.background(scheme); | ||
const bgTone = bg.getTone(scheme); | ||
const aIsNearer = (polarity === 'nearer' || | ||
(polarity === 'lighter' && !scheme.isDark) || | ||
(polarity === 'darker' && scheme.isDark)); | ||
const nearer = aIsNearer ? roleA : roleB; | ||
const farther = aIsNearer ? roleB : roleA; | ||
const amNearer = this.name === nearer.name; | ||
const expansionDir = scheme.isDark ? 1 : -1; | ||
// 1st round: solve to min, each | ||
const nContrast = nearer.contrastCurve.getContrast(scheme.contrastLevel); | ||
const fContrast = farther.contrastCurve.getContrast(scheme.contrastLevel); | ||
// If a color is good enough, it is not adjusted. | ||
// Initial and adjusted tones for `nearer` | ||
const nInitialTone = nearer.tone(scheme); | ||
let nTone = Contrast.ratioOfTones(bgTone, nInitialTone) >= nContrast ? | ||
nInitialTone : | ||
DynamicColor.foregroundTone(bgTone, nContrast); | ||
// Initial and adjusted tones for `farther` | ||
const fInitialTone = farther.tone(scheme); | ||
let fTone = Contrast.ratioOfTones(bgTone, fInitialTone) >= fContrast ? | ||
fInitialTone : | ||
DynamicColor.foregroundTone(bgTone, fContrast); | ||
if (decreasingContrast) { | ||
const minContrastRatio = Contrast.ratioOfTones(this.toneMinContrast(scheme), bg.toneMinContrast(scheme)); | ||
minRatio = bgHasBg ? minContrastRatio : null; | ||
maxRatio = standardRatio; | ||
// If decreasing contrast, adjust color to the "bare minimum" | ||
// that satisfies contrast. | ||
nTone = DynamicColor.foregroundTone(bgTone, nContrast); | ||
fTone = DynamicColor.foregroundTone(bgTone, fContrast); | ||
} | ||
if ((fTone - nTone) * expansionDir >= delta) { | ||
// Good! Tones satisfy the constraint; no change needed. | ||
} | ||
else { | ||
const maxContrastRatio = Contrast.ratioOfTones(this.toneMaxContrast(scheme), bg.toneMaxContrast(scheme)); | ||
minRatio = bgHasBg ? Math.min(maxContrastRatio, standardRatio) : null; | ||
maxRatio = bgHasBg ? Math.max(maxContrastRatio, standardRatio) : null; | ||
// 2nd round: expand farther to match delta. | ||
fTone = math.clampDouble(0, 100, nTone + delta * expansionDir); | ||
if ((fTone - nTone) * expansionDir >= delta) { | ||
// Good! Tones now satisfy the constraint; no change needed. | ||
} | ||
else { | ||
// 3rd round: contract nearer to match delta. | ||
nTone = math.clampDouble(0, 100, fTone - delta * expansionDir); | ||
} | ||
} | ||
// Avoids the 50-59 awkward zone. | ||
if (50 <= nTone && nTone < 60) { | ||
// If `nearer` is in the awkward zone, move it away, together with | ||
// `farther`. | ||
if (expansionDir > 0) { | ||
nTone = 60; | ||
fTone = Math.max(fTone, nTone + delta * expansionDir); | ||
} | ||
else { | ||
nTone = 49; | ||
fTone = Math.min(fTone, nTone + delta * expansionDir); | ||
} | ||
} | ||
else if (50 <= fTone && fTone < 60) { | ||
if (stayTogether) { | ||
// Fixes both, to avoid two colors on opposite sides of the "awkward | ||
// zone". | ||
if (expansionDir > 0) { | ||
nTone = 60; | ||
fTone = Math.max(fTone, nTone + delta * expansionDir); | ||
} | ||
else { | ||
nTone = 49; | ||
fTone = Math.min(fTone, nTone + delta * expansionDir); | ||
} | ||
} | ||
else { | ||
// Not required to stay together; fixes just one. | ||
if (expansionDir > 0) { | ||
fTone = 60; | ||
} | ||
else { | ||
fTone = 49; | ||
} | ||
} | ||
} | ||
// Returns `nTone` if this color is `nearer`, otherwise `fTone`. | ||
return amNearer ? nTone : fTone; | ||
} | ||
answer = DynamicColor.calculateDynamicTone({ | ||
scheme, | ||
toneStandard: this.tone, | ||
toneToJudge: (c) => c.getTone(scheme), | ||
desiredTone: (s, t) => answer, | ||
background: bg != null ? (s) => bg : undefined, | ||
toneDeltaConstraint: this.toneDeltaConstraint, | ||
minRatio: (s) => minRatio ?? 1.0, | ||
maxRatio: (s) => maxRatio ?? 21.0, | ||
}); | ||
return answer; | ||
} | ||
/** | ||
* Enforce a ToneDeltaConstraint between two DynamicColors. | ||
* | ||
* @param tone The desired tone of the color. | ||
* @param toneStandard The tone of the color at standard contrast. | ||
* @param scheme Defines the conditions of the user interface, for example, | ||
* whether or not it is dark mode or light mode, and what the desired | ||
* contrast level is. | ||
* @param constraintProvider Given a DynamicScheme, return a | ||
* ToneDeltaConstraint or null. | ||
* @param toneToDistanceFrom Given a DynamicColor, return a tone that the | ||
* ToneDeltaConstraint should enforce a delta from. | ||
*/ | ||
static ensureToneDelta(tone, toneStandard, scheme, constraintProvider, toneToDistanceFrom) { | ||
const constraint = constraintProvider ? constraintProvider(scheme) : null; | ||
if (constraint == null || toneToDistanceFrom == null) { | ||
return tone; | ||
else { | ||
// Case 2: No contrast pair; just solve for itself. | ||
let answer = this.tone(scheme); | ||
if (this.background == null) { | ||
return answer; // No adjustment for colors with no background. | ||
} | ||
const bgTone = this.background(scheme).getTone(scheme); | ||
const desiredRatio = this.contrastCurve.getContrast(scheme.contrastLevel); | ||
if (Contrast.ratioOfTones(bgTone, answer) >= desiredRatio) { | ||
// Don't "improve" what's good enough. | ||
} | ||
else { | ||
// Rough improvement. | ||
answer = DynamicColor.foregroundTone(bgTone, desiredRatio); | ||
} | ||
if (decreasingContrast) { | ||
answer = DynamicColor.foregroundTone(bgTone, desiredRatio); | ||
} | ||
if (this.isBackground && 50 <= answer && answer < 60) { | ||
// Must adjust | ||
if (Contrast.ratioOfTones(49, bgTone) >= desiredRatio) { | ||
answer = 49; | ||
} | ||
else { | ||
answer = 60; | ||
} | ||
} | ||
if (this.secondBackground) { | ||
// Case 3: Adjust for dual backgrounds. | ||
const [bg1, bg2] = [this.background, this.secondBackground]; | ||
const [bgTone1, bgTone2] = [bg1(scheme).getTone(scheme), bg2(scheme).getTone(scheme)]; | ||
const [upper, lower] = [Math.max(bgTone1, bgTone2), Math.min(bgTone1, bgTone2)]; | ||
if (Contrast.ratioOfTones(upper, answer) >= desiredRatio && | ||
Contrast.ratioOfTones(lower, answer) >= desiredRatio) { | ||
return answer; | ||
} | ||
// The darkest light tone that satisfies the desired ratio, | ||
// or -1 if such ratio cannot be reached. | ||
const lightOption = Contrast.lighter(upper, desiredRatio); | ||
// The lightest dark tone that satisfies the desired ratio, | ||
// or -1 if such ratio cannot be reached. | ||
const darkOption = Contrast.darker(lower, desiredRatio); | ||
// Tones suitable for the foreground. | ||
const availables = []; | ||
if (lightOption !== -1) | ||
availables.push(lightOption); | ||
if (darkOption !== -1) | ||
availables.push(darkOption); | ||
const prefersLight = DynamicColor.tonePrefersLightForeground(bgTone1) || | ||
DynamicColor.tonePrefersLightForeground(bgTone2); | ||
if (prefersLight) { | ||
return (lightOption < 0) ? 100 : lightOption; | ||
} | ||
if (availables.length === 1) { | ||
return availables[0]; | ||
} | ||
return (darkOption < 0) ? 0 : darkOption; | ||
} | ||
return answer; | ||
} | ||
const requiredDelta = constraint.delta; | ||
const keepAwayTone = toneToDistanceFrom(constraint.keepAway); | ||
const delta = Math.abs(tone - keepAwayTone); | ||
if (delta > requiredDelta) { | ||
return tone; | ||
} | ||
switch (constraint.keepAwayPolarity) { | ||
case 'darker': | ||
return math.clampDouble(0.0, 100.0, keepAwayTone + requiredDelta); | ||
case 'lighter': | ||
return math.clampDouble(0.0, 100.0, keepAwayTone - requiredDelta); | ||
case 'no-preference': | ||
const keepAwayToneStandard = constraint.keepAway.tone(scheme); | ||
const preferLighten = toneStandard > keepAwayToneStandard; | ||
const alterAmount = Math.abs(delta - requiredDelta); | ||
const lighten = preferLighten ? tone + alterAmount <= 100.0 : tone < alterAmount; | ||
return lighten ? tone + alterAmount : tone - alterAmount; | ||
default: | ||
return tone; | ||
} | ||
} | ||
@@ -314,109 +330,2 @@ /** | ||
/** | ||
* Core method for calculating a tone for under dynamic contrast. | ||
* | ||
* It calculates tone while enforcing these properties: | ||
* #1. Desired contrast ratio is reached. | ||
* #2. Darken to enable light foregrounds on midtones. | ||
* #3. Enforce tone delta constraint, if needed. | ||
*/ | ||
static calculateDynamicTone(args) { | ||
const background = args.background; | ||
const scheme = args.scheme; | ||
const toneStandard = args.toneStandard; | ||
const toneToJudge = args.toneToJudge; | ||
const desiredTone = args.desiredTone; | ||
const minRatio = args.minRatio; | ||
const maxRatio = args.maxRatio; | ||
const toneDeltaConstraint = args.toneDeltaConstraint; | ||
// Start with the tone with no adjustment for contrast. | ||
// If there is no background, don't perform any adjustment, return | ||
// immediately. | ||
const toneStd = toneStandard(scheme); | ||
let answer = toneStd; | ||
const bgDynamic = background?.(scheme); | ||
if (bgDynamic == null) { | ||
return answer; | ||
} | ||
const bgToneStd = bgDynamic.tone(scheme); | ||
const stdRatio = Contrast.ratioOfTones(toneStd, bgToneStd); | ||
// If there is a background, determine its tone. | ||
// Then, calculate tone that ensures the desired contrast ratio is met. | ||
const bgTone = toneToJudge(bgDynamic); | ||
const myDesiredTone = desiredTone(stdRatio, bgTone); | ||
const currentRatio = Contrast.ratioOfTones(bgTone, myDesiredTone); | ||
const desiredRatio = math.clampDouble(minRatio?.(stdRatio) ?? 1.0, maxRatio?.(stdRatio) ?? 21.0, currentRatio); | ||
if (desiredRatio === currentRatio) { | ||
answer = myDesiredTone; | ||
} | ||
else { | ||
answer = DynamicColor.foregroundTone(bgTone, desiredRatio); | ||
} | ||
// If the background has no background, adjust calculated tone to ensure it | ||
// is dark enough to have a light foreground. | ||
if (bgDynamic.background?.(scheme) == null) { | ||
answer = DynamicColor.enableLightForeground(answer); | ||
} | ||
// If there is a tone delta constraint, enforce it. | ||
answer = DynamicColor.ensureToneDelta(answer, toneStd, scheme, toneDeltaConstraint, (c) => toneToJudge(c)); | ||
return answer; | ||
} | ||
/** | ||
* Default algorithm for calculating the tone of a color at maximum contrast. | ||
* | ||
* If the color's background has a background, reach contrast 7.0. | ||
* If it doesn't, maintain the original contrast ratio. | ||
*/ | ||
static toneMaxContrastDefault(args) { | ||
return DynamicColor.calculateDynamicTone({ | ||
scheme: args.scheme, | ||
toneStandard: args.tone, | ||
toneToJudge: (c) => c.toneMaxContrast(args.scheme), | ||
desiredTone: (stdRatio, bgTone) => { | ||
const backgroundHasBackground = args.background?.(args.scheme)?.background?.(args.scheme) != null; | ||
if (backgroundHasBackground) { | ||
return DynamicColor.foregroundTone(bgTone, 7.0); | ||
} | ||
else { | ||
return DynamicColor.foregroundTone(bgTone, Math.max(7.0, stdRatio)); | ||
} | ||
}, | ||
background: args.background, | ||
toneDeltaConstraint: args.toneDeltaConstraint, | ||
}); | ||
} | ||
/** | ||
* Default algorithm for calculating the tone of a color at minimum contrast. | ||
* | ||
* If the original contrast ratio was >= 7.0, reach contrast 4.5. | ||
* If the original contrast ratio was >= 3.0, reach contrast 3.0. | ||
* If the original contrast ratio was < 3.0, reach that ratio. | ||
*/ | ||
static toneMinContrastDefault(args) { | ||
return DynamicColor.calculateDynamicTone({ | ||
scheme: args.scheme, | ||
toneStandard: args.tone, | ||
toneToJudge: (c) => c.toneMinContrast(args.scheme), | ||
desiredTone: (stdRatio, bgTone) => { | ||
let answer = args.tone(args.scheme); | ||
if (stdRatio >= 7.0) { | ||
answer = DynamicColor.foregroundTone(bgTone, 4.5); | ||
} | ||
else if (stdRatio >= 3.0) { | ||
answer = DynamicColor.foregroundTone(bgTone, 3.0); | ||
} | ||
else { | ||
const backgroundHasBackground = args.background?.(args.scheme)?.background?.(args.scheme) != null; | ||
if (backgroundHasBackground) { | ||
answer = DynamicColor.foregroundTone(bgTone, stdRatio); | ||
} | ||
} | ||
return answer; | ||
}, | ||
background: args.background, | ||
toneDeltaConstraint: args.toneDeltaConstraint, | ||
minRatio: (standardRatio) => 1.0, | ||
maxRatio: (standardRatio) => standardRatio, | ||
}); | ||
} | ||
/** | ||
* Returns whether [tone] prefers a light foreground. | ||
@@ -423,0 +332,0 @@ * |
@@ -25,2 +25,7 @@ /** | ||
static highestSurface(s: DynamicScheme): DynamicColor; | ||
static primaryPaletteKeyColor: DynamicColor; | ||
static secondaryPaletteKeyColor: DynamicColor; | ||
static tertiaryPaletteKeyColor: DynamicColor; | ||
static neutralPaletteKeyColor: DynamicColor; | ||
static neutralVariantPaletteKeyColor: DynamicColor; | ||
static background: DynamicColor; | ||
@@ -45,3 +50,3 @@ static onBackground: DynamicColor; | ||
static scrim: DynamicColor; | ||
static surfaceTintColor: DynamicColor; | ||
static surfaceTint: DynamicColor; | ||
static primary: DynamicColor; | ||
@@ -52,3 +57,2 @@ static onPrimary: DynamicColor; | ||
static inversePrimary: DynamicColor; | ||
static inverseOnPrimary: DynamicColor; | ||
static secondary: DynamicColor; | ||
@@ -55,0 +59,0 @@ static onSecondary: DynamicColor; |
@@ -21,4 +21,5 @@ /** | ||
import { Variant } from '../scheme/variant.js'; | ||
import { ContrastCurve } from './contrast_curve.js'; | ||
import { DynamicColor } from './dynamic_color.js'; | ||
import { ToneDeltaConstraint } from './tone_delta_constraint.js'; | ||
import { ToneDeltaPair } from './tone_delta_pair.js'; | ||
function isFidelity(scheme) { | ||
@@ -85,58 +86,110 @@ return scheme.variant === Variant.FIDELITY || | ||
MaterialDynamicColors.contentAccentToneDelta = 15.0; | ||
MaterialDynamicColors.primaryPaletteKeyColor = DynamicColor.fromPalette({ | ||
name: 'primary_palette_key_color', | ||
palette: (s) => s.primaryPalette, | ||
tone: (s) => s.primaryPalette.keyColor.tone, | ||
}); | ||
MaterialDynamicColors.secondaryPaletteKeyColor = DynamicColor.fromPalette({ | ||
name: 'secondary_palette_key_color', | ||
palette: (s) => s.secondaryPalette, | ||
tone: (s) => s.secondaryPalette.keyColor.tone, | ||
}); | ||
MaterialDynamicColors.tertiaryPaletteKeyColor = DynamicColor.fromPalette({ | ||
name: 'tertiary_palette_key_color', | ||
palette: (s) => s.tertiaryPalette, | ||
tone: (s) => s.tertiaryPalette.keyColor.tone, | ||
}); | ||
MaterialDynamicColors.neutralPaletteKeyColor = DynamicColor.fromPalette({ | ||
name: 'neutral_palette_key_color', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.neutralPalette.keyColor.tone, | ||
}); | ||
MaterialDynamicColors.neutralVariantPaletteKeyColor = DynamicColor.fromPalette({ | ||
name: 'neutral_variant_palette_key_color', | ||
palette: (s) => s.neutralVariantPalette, | ||
tone: (s) => s.neutralVariantPalette.keyColor.tone, | ||
}); | ||
MaterialDynamicColors.background = DynamicColor.fromPalette({ | ||
name: 'background', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 6 : 98, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.onBackground = DynamicColor.fromPalette({ | ||
name: 'on_background', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 90 : 10, | ||
background: (s) => MaterialDynamicColors.background, | ||
contrastCurve: new ContrastCurve(3, 3, 4.5, 7), | ||
}); | ||
MaterialDynamicColors.surface = DynamicColor.fromPalette({ | ||
name: 'surface', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 6 : 98, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.surfaceDim = DynamicColor.fromPalette({ | ||
name: 'surface_dim', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 6 : 87, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.surfaceBright = DynamicColor.fromPalette({ | ||
name: 'surface_bright', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 24 : 98, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.surfaceContainerLowest = DynamicColor.fromPalette({ | ||
name: 'surface_container_lowest', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 4 : 100, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.surfaceContainerLow = DynamicColor.fromPalette({ | ||
name: 'surface_container_low', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 10 : 96, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.surfaceContainer = DynamicColor.fromPalette({ | ||
name: 'surface_container', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 12 : 94, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.surfaceContainerHigh = DynamicColor.fromPalette({ | ||
name: 'surface_container_high', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 17 : 92, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.surfaceContainerHighest = DynamicColor.fromPalette({ | ||
name: 'surface_container_highest', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 22 : 90, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.onSurface = DynamicColor.fromPalette({ | ||
name: 'on_surface', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 90 : 10, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.surfaceVariant = DynamicColor.fromPalette({ | ||
name: 'surface_variant', | ||
palette: (s) => s.neutralVariantPalette, | ||
tone: (s) => s.isDark ? 30 : 90, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.onSurfaceVariant = DynamicColor.fromPalette({ | ||
name: 'on_surface_variant', | ||
palette: (s) => s.neutralVariantPalette, | ||
tone: (s) => s.isDark ? 80 : 30, | ||
background: (s) => MaterialDynamicColors.surfaceVariant, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(3, 4.5, 7, 11), | ||
}); | ||
MaterialDynamicColors.inverseSurface = DynamicColor.fromPalette({ | ||
name: 'inverse_surface', | ||
palette: (s) => s.neutralPalette, | ||
@@ -146,17 +199,24 @@ tone: (s) => s.isDark ? 90 : 20, | ||
MaterialDynamicColors.inverseOnSurface = DynamicColor.fromPalette({ | ||
name: 'inverse_on_surface', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => s.isDark ? 20 : 95, | ||
background: (s) => MaterialDynamicColors.inverseSurface, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.outline = DynamicColor.fromPalette({ | ||
name: 'outline', | ||
palette: (s) => s.neutralVariantPalette, | ||
tone: (s) => 50, | ||
tone: (s) => s.isDark ? 60 : 50, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1.5, 3, 4.5, 7), | ||
}); | ||
MaterialDynamicColors.outlineVariant = DynamicColor.fromPalette({ | ||
name: 'outline_variant', | ||
palette: (s) => s.neutralVariantPalette, | ||
tone: (s) => s.isDark ? 30 : 80, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
}); | ||
MaterialDynamicColors.shadow = DynamicColor.fromPalette({ | ||
name: 'shadow', | ||
palette: (s) => s.neutralPalette, | ||
@@ -166,10 +226,14 @@ tone: (s) => 0, | ||
MaterialDynamicColors.scrim = DynamicColor.fromPalette({ | ||
name: 'scrim', | ||
palette: (s) => s.neutralPalette, | ||
tone: (s) => 0, | ||
}); | ||
MaterialDynamicColors.surfaceTintColor = DynamicColor.fromPalette({ | ||
MaterialDynamicColors.surfaceTint = DynamicColor.fromPalette({ | ||
name: 'surface_tint', | ||
palette: (s) => s.primaryPalette, | ||
tone: (s) => s.isDark ? 80 : 40, | ||
isBackground: true, | ||
}); | ||
MaterialDynamicColors.primary = DynamicColor.fromPalette({ | ||
name: 'primary', | ||
palette: (s) => s.primaryPalette, | ||
@@ -182,6 +246,9 @@ tone: (s) => { | ||
}, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
toneDeltaConstraint: (s) => new ToneDeltaConstraint(MaterialDynamicColors.contentAccentToneDelta, MaterialDynamicColors.primaryContainer, s.isDark ? 'darker' : 'lighter'), | ||
contrastCurve: new ContrastCurve(3, 4.5, 7, 11), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.primaryContainer, MaterialDynamicColors.primary, 15, 'nearer', false), | ||
}); | ||
MaterialDynamicColors.onPrimary = DynamicColor.fromPalette({ | ||
name: 'on_primary', | ||
palette: (s) => s.primaryPalette, | ||
@@ -195,46 +262,54 @@ tone: (s) => { | ||
background: (s) => MaterialDynamicColors.primary, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.primaryContainer = DynamicColor.fromPalette({ | ||
name: 'primary_container', | ||
palette: (s) => s.primaryPalette, | ||
tone: (s) => { | ||
if (isFidelity(s)) { | ||
return performAlbers(s.sourceColorHct, s); | ||
} | ||
if (isMonochrome(s)) { | ||
return s.isDark ? 85 : 25; | ||
} | ||
if (!isFidelity(s)) { | ||
return s.isDark ? 30 : 90; | ||
} | ||
return performAlbers(s.sourceColorHct, s); | ||
return s.isDark ? 30 : 90; | ||
}, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.primaryContainer, MaterialDynamicColors.primary, 15, 'nearer', false), | ||
}); | ||
MaterialDynamicColors.onPrimaryContainer = DynamicColor.fromPalette({ | ||
name: 'on_primary_container', | ||
palette: (s) => s.primaryPalette, | ||
tone: (s) => { | ||
if (isFidelity(s)) { | ||
return DynamicColor.foregroundTone(MaterialDynamicColors.primaryContainer.tone(s), 4.5); | ||
} | ||
if (isMonochrome(s)) { | ||
return s.isDark ? 0 : 100; | ||
} | ||
if (!isFidelity(s)) { | ||
return s.isDark ? 90 : 10; | ||
} | ||
return DynamicColor.foregroundTone(MaterialDynamicColors.primaryContainer.tone(s), 4.5); | ||
return s.isDark ? 90 : 10; | ||
}, | ||
background: (s) => MaterialDynamicColors.primaryContainer, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.inversePrimary = DynamicColor.fromPalette({ | ||
name: 'inverse_primary', | ||
palette: (s) => s.primaryPalette, | ||
tone: (s) => s.isDark ? 40 : 80, | ||
background: (s) => MaterialDynamicColors.inverseSurface, | ||
contrastCurve: new ContrastCurve(3, 4.5, 7, 11), | ||
}); | ||
MaterialDynamicColors.inverseOnPrimary = DynamicColor.fromPalette({ | ||
palette: (s) => s.primaryPalette, | ||
tone: (s) => s.isDark ? 100 : 20, | ||
background: (s) => MaterialDynamicColors.inversePrimary, | ||
}); | ||
MaterialDynamicColors.secondary = DynamicColor.fromPalette({ | ||
name: 'secondary', | ||
palette: (s) => s.secondaryPalette, | ||
tone: (s) => s.isDark ? 80 : 40, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
toneDeltaConstraint: (s) => new ToneDeltaConstraint(MaterialDynamicColors.contentAccentToneDelta, MaterialDynamicColors.secondaryContainer, s.isDark ? 'darker' : 'lighter'), | ||
contrastCurve: new ContrastCurve(3, 4.5, 7, 11), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.secondaryContainer, MaterialDynamicColors.secondary, 15, 'nearer', false), | ||
}); | ||
MaterialDynamicColors.onSecondary = DynamicColor.fromPalette({ | ||
name: 'on_secondary', | ||
palette: (s) => s.secondaryPalette, | ||
@@ -250,4 +325,6 @@ tone: (s) => { | ||
background: (s) => MaterialDynamicColors.secondary, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.secondaryContainer = DynamicColor.fromPalette({ | ||
name: 'secondary_container', | ||
palette: (s) => s.secondaryPalette, | ||
@@ -266,5 +343,9 @@ tone: (s) => { | ||
}, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.secondaryContainer, MaterialDynamicColors.secondary, 15, 'nearer', false), | ||
}); | ||
MaterialDynamicColors.onSecondaryContainer = DynamicColor.fromPalette({ | ||
name: 'on_secondary_container', | ||
palette: (s) => s.secondaryPalette, | ||
@@ -278,4 +359,6 @@ tone: (s) => { | ||
background: (s) => MaterialDynamicColors.secondaryContainer, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.tertiary = DynamicColor.fromPalette({ | ||
name: 'tertiary', | ||
palette: (s) => s.tertiaryPalette, | ||
@@ -288,6 +371,9 @@ tone: (s) => { | ||
}, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
toneDeltaConstraint: (s) => new ToneDeltaConstraint(MaterialDynamicColors.contentAccentToneDelta, MaterialDynamicColors.tertiaryContainer, s.isDark ? 'darker' : 'lighter'), | ||
contrastCurve: new ContrastCurve(3, 4.5, 7, 11), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.tertiaryContainer, MaterialDynamicColors.tertiary, 15, 'nearer', false), | ||
}); | ||
MaterialDynamicColors.onTertiary = DynamicColor.fromPalette({ | ||
name: 'on_tertiary', | ||
palette: (s) => s.tertiaryPalette, | ||
@@ -301,4 +387,6 @@ tone: (s) => { | ||
background: (s) => MaterialDynamicColors.tertiary, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.tertiaryContainer = DynamicColor.fromPalette({ | ||
name: 'tertiary_container', | ||
palette: (s) => s.tertiaryPalette, | ||
@@ -316,5 +404,9 @@ tone: (s) => { | ||
}, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.tertiaryContainer, MaterialDynamicColors.tertiary, 15, 'nearer', false), | ||
}); | ||
MaterialDynamicColors.onTertiaryContainer = DynamicColor.fromPalette({ | ||
name: 'on_tertiary_container', | ||
palette: (s) => s.tertiaryPalette, | ||
@@ -331,104 +423,138 @@ tone: (s) => { | ||
background: (s) => MaterialDynamicColors.tertiaryContainer, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.error = DynamicColor.fromPalette({ | ||
name: 'error', | ||
palette: (s) => s.errorPalette, | ||
tone: (s) => s.isDark ? 80 : 40, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
toneDeltaConstraint: (s) => new ToneDeltaConstraint(MaterialDynamicColors.contentAccentToneDelta, MaterialDynamicColors.errorContainer, s.isDark ? 'darker' : 'lighter'), | ||
contrastCurve: new ContrastCurve(3, 4.5, 7, 11), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.errorContainer, MaterialDynamicColors.error, 15, 'nearer', false), | ||
}); | ||
MaterialDynamicColors.onError = DynamicColor.fromPalette({ | ||
name: 'on_error', | ||
palette: (s) => s.errorPalette, | ||
tone: (s) => s.isDark ? 20 : 100, | ||
background: (s) => MaterialDynamicColors.error, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.errorContainer = DynamicColor.fromPalette({ | ||
name: 'error_container', | ||
palette: (s) => s.errorPalette, | ||
tone: (s) => s.isDark ? 30 : 90, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.errorContainer, MaterialDynamicColors.error, 15, 'nearer', false), | ||
}); | ||
MaterialDynamicColors.onErrorContainer = DynamicColor.fromPalette({ | ||
name: 'on_error_container', | ||
palette: (s) => s.errorPalette, | ||
tone: (s) => s.isDark ? 90 : 10, | ||
background: (s) => MaterialDynamicColors.errorContainer, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.primaryFixed = DynamicColor.fromPalette({ | ||
name: 'primary_fixed', | ||
palette: (s) => s.primaryPalette, | ||
tone: (s) => { | ||
if (isMonochrome(s)) { | ||
return s.isDark ? 100.0 : 10.0; | ||
} | ||
return 90.0; | ||
}, | ||
tone: (s) => isMonochrome(s) ? 40.0 : 90.0, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.primaryFixed, MaterialDynamicColors.primaryFixedDim, 10, 'lighter', true), | ||
}); | ||
MaterialDynamicColors.primaryFixedDim = DynamicColor.fromPalette({ | ||
name: 'primary_fixed_dim', | ||
palette: (s) => s.primaryPalette, | ||
tone: (s) => { | ||
if (isMonochrome(s)) { | ||
return s.isDark ? 90.0 : 20.0; | ||
} | ||
return 80.0; | ||
}, | ||
tone: (s) => isMonochrome(s) ? 30.0 : 80.0, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.primaryFixed, MaterialDynamicColors.primaryFixedDim, 10, 'lighter', true), | ||
}); | ||
MaterialDynamicColors.onPrimaryFixed = DynamicColor.fromPalette({ | ||
name: 'on_primary_fixed', | ||
palette: (s) => s.primaryPalette, | ||
tone: (s) => { | ||
if (isMonochrome(s)) { | ||
return s.isDark ? 10.0 : 90.0; | ||
} | ||
return 10.0; | ||
}, | ||
tone: (s) => isMonochrome(s) ? 100.0 : 10.0, | ||
background: (s) => MaterialDynamicColors.primaryFixedDim, | ||
secondBackground: (s) => MaterialDynamicColors.primaryFixed, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.onPrimaryFixedVariant = DynamicColor.fromPalette({ | ||
name: 'on_primary_fixed_variant', | ||
palette: (s) => s.primaryPalette, | ||
tone: (s) => { | ||
if (isMonochrome(s)) { | ||
return s.isDark ? 30.0 : 70.0; | ||
} | ||
return 30.0; | ||
}, | ||
tone: (s) => isMonochrome(s) ? 90.0 : 30.0, | ||
background: (s) => MaterialDynamicColors.primaryFixedDim, | ||
secondBackground: (s) => MaterialDynamicColors.primaryFixed, | ||
contrastCurve: new ContrastCurve(3, 4.5, 7, 11), | ||
}); | ||
MaterialDynamicColors.secondaryFixed = DynamicColor.fromPalette({ | ||
name: 'secondary_fixed', | ||
palette: (s) => s.secondaryPalette, | ||
tone: (s) => isMonochrome(s) ? 80.0 : 90.0, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.secondaryFixed, MaterialDynamicColors.secondaryFixedDim, 10, 'lighter', true), | ||
}); | ||
MaterialDynamicColors.secondaryFixedDim = DynamicColor.fromPalette({ | ||
name: 'secondary_fixed_dim', | ||
palette: (s) => s.secondaryPalette, | ||
tone: (s) => isMonochrome(s) ? 70.0 : 80.0, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.secondaryFixed, MaterialDynamicColors.secondaryFixedDim, 10, 'lighter', true), | ||
}); | ||
MaterialDynamicColors.onSecondaryFixed = DynamicColor.fromPalette({ | ||
name: 'on_secondary_fixed', | ||
palette: (s) => s.secondaryPalette, | ||
tone: (s) => 10.0, | ||
background: (s) => MaterialDynamicColors.secondaryFixedDim, | ||
secondBackground: (s) => MaterialDynamicColors.secondaryFixed, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.onSecondaryFixedVariant = DynamicColor.fromPalette({ | ||
name: 'on_secondary_fixed_variant', | ||
palette: (s) => s.secondaryPalette, | ||
tone: (s) => isMonochrome(s) ? 25.0 : 30.0, | ||
background: (s) => MaterialDynamicColors.secondaryFixedDim, | ||
secondBackground: (s) => MaterialDynamicColors.secondaryFixed, | ||
contrastCurve: new ContrastCurve(3, 4.5, 7, 11), | ||
}); | ||
MaterialDynamicColors.tertiaryFixed = DynamicColor.fromPalette({ | ||
name: 'tertiary_fixed', | ||
palette: (s) => s.tertiaryPalette, | ||
tone: (s) => isMonochrome(s) ? 40.0 : 90.0, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.tertiaryFixed, MaterialDynamicColors.tertiaryFixedDim, 10, 'lighter', true), | ||
}); | ||
MaterialDynamicColors.tertiaryFixedDim = DynamicColor.fromPalette({ | ||
name: 'tertiary_fixed_dim', | ||
palette: (s) => s.tertiaryPalette, | ||
tone: (s) => isMonochrome(s) ? 30.0 : 80.0, | ||
isBackground: true, | ||
background: (s) => MaterialDynamicColors.highestSurface(s), | ||
contrastCurve: new ContrastCurve(1, 1, 3, 7), | ||
toneDeltaPair: (s) => new ToneDeltaPair(MaterialDynamicColors.tertiaryFixed, MaterialDynamicColors.tertiaryFixedDim, 10, 'lighter', true), | ||
}); | ||
MaterialDynamicColors.onTertiaryFixed = DynamicColor.fromPalette({ | ||
name: 'on_tertiary_fixed', | ||
palette: (s) => s.tertiaryPalette, | ||
tone: (s) => isMonochrome(s) ? 90.0 : 10.0, | ||
tone: (s) => isMonochrome(s) ? 100.0 : 10.0, | ||
background: (s) => MaterialDynamicColors.tertiaryFixedDim, | ||
secondBackground: (s) => MaterialDynamicColors.tertiaryFixed, | ||
contrastCurve: new ContrastCurve(4.5, 7, 11, 21), | ||
}); | ||
MaterialDynamicColors.onTertiaryFixedVariant = DynamicColor.fromPalette({ | ||
name: 'on_tertiary_fixed_variant', | ||
palette: (s) => s.tertiaryPalette, | ||
tone: (s) => isMonochrome(s) ? 70.0 : 30.0, | ||
tone: (s) => isMonochrome(s) ? 90.0 : 30.0, | ||
background: (s) => MaterialDynamicColors.tertiaryFixedDim, | ||
secondBackground: (s) => MaterialDynamicColors.tertiaryFixed, | ||
contrastCurve: new ContrastCurve(3, 4.5, 7, 11), | ||
}); | ||
//# sourceMappingURL=material_dynamic_colors.js.map |
{ | ||
"name": "@material/material-color-utilities", | ||
"version": "0.2.6", | ||
"version": "0.2.7", | ||
"publishConfig": { | ||
@@ -5,0 +5,0 @@ "access": "public" |
@@ -25,2 +25,3 @@ /** | ||
readonly chroma: number; | ||
readonly keyColor: Hct; | ||
private readonly cache; | ||
@@ -33,2 +34,7 @@ /** | ||
/** | ||
* @param hct Hct | ||
* @return Tones matching that color's hue and chroma. | ||
*/ | ||
static fromHct(hct: Hct): TonalPalette; | ||
/** | ||
* @param hue HCT hue | ||
@@ -40,2 +46,3 @@ * @param chroma HCT chroma | ||
private constructor(); | ||
private static createKeyColor; | ||
/** | ||
@@ -42,0 +49,0 @@ * @param tone HCT tone, measured from 0 to 100. |
@@ -29,5 +29,12 @@ /** | ||
const hct = Hct.fromInt(argb); | ||
return TonalPalette.fromHueAndChroma(hct.hue, hct.chroma); | ||
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 | ||
@@ -38,9 +45,43 @@ * @param chroma HCT chroma | ||
static fromHueAndChroma(hue, chroma) { | ||
return new TonalPalette(hue, chroma); | ||
return new TonalPalette(hue, chroma, TonalPalette.createKeyColor(hue, chroma)); | ||
} | ||
constructor(hue, chroma) { | ||
constructor(hue, chroma, keyColor) { | ||
this.hue = hue; | ||
this.chroma = chroma; | ||
this.keyColor = keyColor; | ||
this.cache = new Map(); | ||
} | ||
static createKeyColor(hue, chroma) { | ||
const startTone = 50.0; | ||
let smallestDeltaHct = Hct.from(hue, chroma, startTone); | ||
let smallestDelta = Math.abs(smallestDeltaHct.chroma - chroma); | ||
// Starting from T50, check T+/-delta to see if they match the requested | ||
// chroma. | ||
// | ||
// Starts from T50 because T50 has the most chroma available, on | ||
// average. Thus it is most likely to have a direct answer and minimize | ||
// iteration. | ||
for (let delta = 1.0; delta < 50.0; delta += 1.0) { | ||
// Termination condition rounding instead of minimizing delta to avoid | ||
// case where requested chroma is 16.51, and the closest chroma is 16.49. | ||
// Error is minimized, but when rounded and displayed, requested chroma | ||
// is 17, key color's chroma is 16. | ||
if (Math.round(chroma) === Math.round(smallestDeltaHct.chroma)) { | ||
return smallestDeltaHct; | ||
} | ||
const hctAdd = Hct.from(hue, chroma, startTone + delta); | ||
const hctAddDelta = Math.abs(hctAdd.chroma - chroma); | ||
if (hctAddDelta < smallestDelta) { | ||
smallestDelta = hctAddDelta; | ||
smallestDeltaHct = hctAdd; | ||
} | ||
const hctSubtract = Hct.from(hue, chroma, startTone - delta); | ||
const hctSubtractDelta = Math.abs(hctSubtract.chroma - chroma); | ||
if (hctSubtractDelta < smallestDelta) { | ||
smallestDelta = hctSubtractDelta; | ||
smallestDeltaHct = hctSubtract; | ||
} | ||
} | ||
return smallestDeltaHct; | ||
} | ||
/** | ||
@@ -47,0 +88,0 @@ * @param tone HCT tone, measured from 0 to 100. |
@@ -31,7 +31,7 @@ /** | ||
isDark, | ||
primaryPalette: TonalPalette.fromHueAndChroma(math.sanitizeDegreesDouble(sourceColorHct.hue + 120.0), 40.0), | ||
primaryPalette: TonalPalette.fromHueAndChroma(math.sanitizeDegreesDouble(sourceColorHct.hue + 240.0), 40.0), | ||
secondaryPalette: TonalPalette.fromHueAndChroma(DynamicScheme.getRotatedHue(sourceColorHct, SchemeExpressive.hues, SchemeExpressive.secondaryRotations), 24.0), | ||
tertiaryPalette: TonalPalette.fromHueAndChroma(DynamicScheme.getRotatedHue(sourceColorHct, SchemeExpressive.hues, SchemeExpressive.tertiaryRotations), 32.0), | ||
neutralPalette: TonalPalette.fromHueAndChroma(sourceColorHct.hue, 8.0), | ||
neutralVariantPalette: TonalPalette.fromHueAndChroma(sourceColorHct.hue, 12.0), | ||
neutralPalette: TonalPalette.fromHueAndChroma(sourceColorHct.hue + 15, 8.0), | ||
neutralVariantPalette: TonalPalette.fromHueAndChroma(sourceColorHct.hue + 15, 12.0), | ||
}); | ||
@@ -38,0 +38,0 @@ } |
@@ -34,3 +34,3 @@ /** | ||
isDark, | ||
primaryPalette: TonalPalette.fromHueAndChroma(sourceColorHct.hue, 40.0), | ||
primaryPalette: TonalPalette.fromHueAndChroma(sourceColorHct.hue, 36.0), | ||
secondaryPalette: TonalPalette.fromHueAndChroma(sourceColorHct.hue, 16.0), | ||
@@ -37,0 +37,0 @@ tertiaryPalette: TonalPalette.fromHueAndChroma(math.sanitizeDegreesDouble(sourceColorHct.hue + 60.0), 24.0), |
@@ -34,3 +34,3 @@ /** | ||
tertiaryPalette: TonalPalette.fromHueAndChroma(DynamicScheme.getRotatedHue(sourceColorHct, SchemeVibrant.hues, SchemeVibrant.tertiaryRotations), 32.0), | ||
neutralPalette: TonalPalette.fromHueAndChroma(sourceColorHct.hue, 8.0), | ||
neutralPalette: TonalPalette.fromHueAndChroma(sourceColorHct.hue, 10.0), | ||
neutralVariantPalette: TonalPalette.fromHueAndChroma(sourceColorHct.hue, 12.0), | ||
@@ -37,0 +37,0 @@ }); |
@@ -29,3 +29,5 @@ /** | ||
FIDELITY = 5, | ||
CONTENT = 6 | ||
CONTENT = 6, | ||
RAINBOW = 7, | ||
FRUIT_SALAD = 8 | ||
} |
@@ -31,3 +31,5 @@ /** | ||
Variant[Variant["CONTENT"] = 6] = "CONTENT"; | ||
Variant[Variant["RAINBOW"] = 7] = "RAINBOW"; | ||
Variant[Variant["FRUIT_SALAD"] = 8] = "FRUIT_SALAD"; | ||
})(Variant || (Variant = {})); | ||
//# sourceMappingURL=variant.js.map |
@@ -18,2 +18,16 @@ /** | ||
/** | ||
* Default options for ranking colors based on usage counts. | ||
* desired: is the max count of the colors returned. | ||
* fallbackColorARGB: Is the default color that should be used if no | ||
* other colors are suitable. | ||
* filter: controls if the resulting colors should be filtered to not include | ||
* hues that are not used often enough, and colors that are effectively | ||
* grayscale. | ||
*/ | ||
declare interface ScoreOptions { | ||
desired?: number; | ||
fallbackColorARGB?: number; | ||
filter?: boolean; | ||
} | ||
/** | ||
* Given a large set of colors, remove colors that are unsuitable for a UI | ||
@@ -32,3 +46,2 @@ * theme, and rank the rest based on suitability. | ||
private static readonly CUTOFF_CHROMA; | ||
private static readonly CUTOFF_TONE; | ||
private static readonly CUTOFF_EXCITED_PROPORTION; | ||
@@ -42,2 +55,3 @@ private constructor(); | ||
* 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 | ||
@@ -49,5 +63,4 @@ * color is the first item, the least suitable is the last. There will | ||
*/ | ||
static score(colorsToPopulation: Map<number, number>, contentColor?: boolean): number[]; | ||
private static filter; | ||
private static filterContent; | ||
static score(colorsToPopulation: Map<number, number>, options?: ScoreOptions): number[]; | ||
} | ||
export {}; |
@@ -17,5 +17,18 @@ /** | ||
*/ | ||
import { Cam16 } from '../hct/cam16.js'; | ||
import * as utils from '../utils/color_utils.js'; | ||
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; | ||
} | ||
/** | ||
@@ -37,2 +50,3 @@ * Given a large set of colors, remove colors that are unsuitable for a UI | ||
* 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 | ||
@@ -44,105 +58,78 @@ * color is the first item, the least suitable is the last. There will | ||
*/ | ||
static score(colorsToPopulation, contentColor = false) { | ||
// Determine the total count of all colors. | ||
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 population of colorsToPopulation.values()) { | ||
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; | ||
} | ||
// Turn the count of each color into a proportion by dividing by the total | ||
// count. Also, fill a cache of CAM16 colors representing each color, and | ||
// record the proportion of colors for each CAM16 hue. | ||
const colorsToProportion = new Map(); | ||
const colorsToCam = new Map(); | ||
const hueProportions = new Array(360).fill(0); | ||
for (const [color, population] of colorsToPopulation.entries()) { | ||
const proportion = population / populationSum; | ||
colorsToProportion.set(color, proportion); | ||
const cam = Cam16.fromInt(color); | ||
colorsToCam.set(color, cam); | ||
const hue = Math.round(cam.hue); | ||
hueProportions[hue] += proportion; | ||
} | ||
// Determine the proportion of the colors around each color, by summing the | ||
// proportions around each color's hue. | ||
const colorsToExcitedProportion = new Map(); | ||
for (const [color, cam] of colorsToCam.entries()) { | ||
const hue = Math.round(cam.hue); | ||
let excitedProportion = 0; | ||
for (let i = (hue - 15); i < (hue + 15); i++) { | ||
// 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); | ||
excitedProportion += hueProportions[neighborHue]; | ||
hueExcitedProportions[neighborHue] += proportion; | ||
} | ||
colorsToExcitedProportion.set(color, excitedProportion); | ||
} | ||
// Score the colors by their proportion, as well as how chromatic they are. | ||
const colorsToScore = new Map(); | ||
for (const [color, cam] of colorsToCam.entries()) { | ||
const proportion = colorsToExcitedProportion.get(color); | ||
// 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 = cam.chroma < Score.TARGET_CHROMA ? | ||
Score.WEIGHT_CHROMA_BELOW : | ||
Score.WEIGHT_CHROMA_ABOVE; | ||
const chromaScore = (cam.chroma - Score.TARGET_CHROMA) * chromaWeight; | ||
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; | ||
colorsToScore.set(color, score); | ||
scoredHct.push({ hct, score }); | ||
} | ||
// Remove colors that are unsuitable, ex. very dark or unchromatic colors. | ||
// Also, remove colors that are very similar in hue. | ||
const filteredColors = contentColor ? | ||
Score.filterContent(colorsToCam) : | ||
Score.filter(colorsToExcitedProportion, colorsToCam); | ||
const dedupedColorsToScore = new Map(); | ||
for (const color of filteredColors) { | ||
let duplicateHue = false; | ||
const hue = colorsToCam.get(color).hue; | ||
for (const [alreadyChosenColor,] of dedupedColorsToScore) { | ||
const alreadyChosenHue = colorsToCam.get(alreadyChosenColor).hue; | ||
if (math.differenceDegrees(hue, alreadyChosenHue) < 15) { | ||
duplicateHue = true; | ||
// 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 (duplicateHue) { | ||
continue; | ||
} | ||
dedupedColorsToScore.set(color, colorsToScore.get(color)); | ||
if (chosenColors.length >= desired) | ||
break; | ||
} | ||
// Ensure the list of colors returned is sorted such that the first in the | ||
// list is the most suitable, and the last is the least suitable. | ||
const colorsByScoreDescending = Array.from(dedupedColorsToScore.entries()); | ||
colorsByScoreDescending.sort((first, second) => { | ||
return second[1] - first[1]; | ||
}); | ||
const answer = colorsByScoreDescending.map((entry) => { | ||
return entry[0]; | ||
}); | ||
// Ensure that at least one color is returned. | ||
if (answer.length === 0) { | ||
answer.push(0xff4285F4); // Google Blue | ||
const colors = []; | ||
if (chosenColors.length === 0) { | ||
colors.push(fallbackColorARGB); | ||
} | ||
return answer; | ||
} | ||
static filter(colorsToExcitedProportion, colorsToCam) { | ||
const filtered = new Array(); | ||
for (const [color, cam] of colorsToCam.entries()) { | ||
const proportion = colorsToExcitedProportion.get(color); | ||
if (cam.chroma >= Score.CUTOFF_CHROMA && | ||
utils.lstarFromArgb(color) >= Score.CUTOFF_TONE && | ||
proportion >= Score.CUTOFF_EXCITED_PROPORTION) { | ||
filtered.push(color); | ||
} | ||
for (const chosenHct of chosenColors) { | ||
colors.push(chosenHct.toInt()); | ||
} | ||
return filtered; | ||
return colors; | ||
} | ||
static filterContent(colorsToCam) { | ||
return Array.from(colorsToCam.keys()); | ||
} | ||
} | ||
Score.TARGET_CHROMA = 48.0; | ||
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 = 15.0; | ||
Score.CUTOFF_TONE = 10.0; | ||
Score.CUTOFF_CHROMA = 5.0; | ||
Score.CUTOFF_EXCITED_PROPORTION = 0.01; | ||
//# sourceMappingURL=score.js.map |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 4 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Unidentified License
License(Experimental) Something that seems like a license was found, but its contents could not be matched with a known license.
Found 2 instances in 1 package
734896
126
8155
17