import { hex as chromaHex } from 'chroma-js' ; import { hsluvToHex } from 'hsluv' ; import { Params , Scheme } from '../interfaces' ; const lightTextLuminance = 95 ; const darkTextLuminance = 20 ; const lightScaleLuminance = 70 ; const darkScaleLuminance = 70 ; const maxSaturation = 100 ; const maxLightLuminanceOffset = 5 ; const maxDarkLuminanceOffset = 10 ; const maxBackgroundChroma = 4 ; const lightScaleBackgroundLuminanceShift = 10 ; const darkScaleBackgroundLuminanceShift = 3 ; const analogousRange = 60 ; const complementaryRange = 60 ; const nominalSaturation = 90 ; const nominalLighterShift = 15 ; const nominalDarkerShift = 30 ; const boldSaturationShift = 10 ; const mutedSaturationShift = 55 ; const minNominalSaturation = 10 ; const minNominalLuminance = 50 ; const linearExponent = 1 ; const polynomialExponent = 1.5 ; const lowContrastBackgroundShift = 20 ; const darkestGrey = 20 ; const lightestGrey = 90 ; const offsetBackgroundLuminanceShift = 1 ; export interface NominalHueSequence { bold : [ number , number , number ] [ ] ; std : [ number , number , number ] [ ] ; muted : [ number , number , number ] [ ] ; hues : number [ ] ; size : number ; } export function polynomial_scale ( exp , start , end , size ) { const base = 1 / ( size - 1 ) ; const interval = end - start ; const result = [ ] ; for ( let i = 0 ; i < size ; i ++ ) { result . push ( start + interval * ( i * base ) ** exp ) ; } return result ; } export type HSLVector = [ number , number , number ] ; export function polynomial_hsl_scale ( exp , [ sh , ss , sl ] , [ eh , es , el ] , size ) { const hues = polynomial_scale ( exp , sh , eh , size ) ; const saturations = polynomial_scale ( exp , ss , es , size ) ; const luminances = polynomial_scale ( exp , sl , el , size ) ; const hexvalues = [ ] ; for ( let i = 0 ; i < size ; i ++ ) { hexvalues . push ( [ hues [ i ] , saturations [ i ] , luminances [ i ] ] ) ; } return hexvalues ; } function getOffsetHue ( [ h , s , l ] , hueShift ) { if ( hueShift >= 25 && hueShift <= 75 ) { const delta = ( analogousRange * ( hueShift - 50 ) ) / 25 ; return ( h + delta ) % 360 ; } else if ( hueShift > 75 ) { const delta = 180 - ( complementaryRange * ( 100 - hueShift ) ) / 25 ; return ( h + delta ) % 360 ; } else { const delta = 180 + ( complementaryRange * hueShift ) / 25 ; return ( h + delta ) % 360 ; } } function getBackgroundSaturationaAndLuminance ( hue , backgroundLevel , light ) { function normalizeSaturation ( h , l ) { let satGivingMaxChroma = 100 ; let c = chromaHex ( hsluvToHex ( [ hue , 100 , l ] ) ) . hcl ( ) [ 1 ] ; while ( c > maxBackgroundChroma && satGivingMaxChroma >= 0 ) { satGivingMaxChroma -= 1 ; c = chromaHex ( hsluvToHex ( [ hue , satGivingMaxChroma , l ] ) ) . hcl ( ) [ 1 ] ; } return satGivingMaxChroma ; } if ( light ) { if ( backgroundLevel < 50 ) { const l = 100 - maxLightLuminanceOffset ; return [ ( backgroundLevel * normalizeSaturation ( hue , l ) ) / 50 , l ] ; } else { const l = 100 - maxLightLuminanceOffset * ( 1 - ( backgroundLevel - 50 ) / 50 ) ; return [ normalizeSaturation ( hue , l ) , l ] ; } } else { if ( backgroundLevel < 50 ) { const l = maxDarkLuminanceOffset ; return [ ( backgroundLevel * normalizeSaturation ( hue , l ) ) / 50 , l ] ; } else { const l = maxDarkLuminanceOffset * ( 1 - ( backgroundLevel - 50 ) / 50 ) ; return [ normalizeSaturation ( hue , l ) , l ] ; } } } export class ColorMaker { public readonly textLuminance : number ; public readonly scaleLuminance : number ; public readonly backgroundHsl : [ number , number , number ] ; public readonly offsetbackgroundHsl : [ number , number , number ] ; public readonly foregroundHsl : [ number , number , number ] ; public readonly faintAnnotationHsl : [ number , number , number ] ; public readonly lowContrastAnnotationHsl : [ number , number , number ] ; public readonly lowMidContrastAnnotationHsl : [ number , number , number ] ; public readonly midContrastAnnotationHsl : [ number , number , number ] ; public readonly midHighContrastAnnotationHsl : [ number , number , number ] ; public readonly highContrastAnnotationHsl : [ number , number , number ] ; public readonly greyLuminance : number ; private cachedNominalSequence : NominalHueSequence | undefined ; public constructor ( public readonly accentHsl , public readonly backgroundLevel , public readonly backgroundHueShift , public readonly nominalHueStep , public readonly light ) { this . textLuminance = light ? darkTextLuminance : lightTextLuminance ; this . scaleLuminance = light ? darkScaleLuminance : lightScaleLuminance ; const h = getOffsetHue ( this . accentHsl , this . backgroundHueShift ) ; const [ s , l ] = getBackgroundSaturationaAndLuminance ( h , this . backgroundLevel , this . light ) ; this . backgroundHsl = [ h , s , l ] ; const offsetBackgroundLuminance = light ? Math . min ( 100 , l + offsetBackgroundLuminanceShift ) : Math . max ( 0 , l - offsetBackgroundLuminanceShift ) ; this . offsetbackgroundHsl = [ h , s , offsetBackgroundLuminance ] ; this . foregroundHsl = [ 0 , 0 , this . textLuminance ] ; this . greyLuminance = this . light ? this . backgroundHsl [ 2 ] - darkScaleBackgroundLuminanceShift : this . backgroundHsl [ 2 ] + lightScaleBackgroundLuminanceShift ; const mutedGreyLuminance = this . light ? this . backgroundHsl [ 2 ] - lowContrastBackgroundShift : this . backgroundHsl [ 2 ] + lowContrastBackgroundShift ; const boldGreyLuminance = this . light ? darkestGrey : lightestGrey ; const greys = this . explicitGrey ( 5 , mutedGreyLuminance , boldGreyLuminance ) ; this . lowContrastAnnotationHsl = greys [ 0 ] ; this . lowMidContrastAnnotationHsl = greys [ 1 ] ; this . midContrastAnnotationHsl = greys [ 2 ] ; this . midHighContrastAnnotationHsl = greys [ 3 ] ; this . highContrastAnnotationHsl = greys [ 4 ] ; const faintLuminance = this . light ? ( 100 - lightestGrey ) / 2 + lightestGrey : darkestGrey / 2 ; this . faintAnnotationHsl = [ this . accentHsl [ 0 ] , 0 , faintLuminance ] ; } public grey ( size ) { const boldGreyLuminance = this . light ? darkestGrey : lightestGrey ; return this . explicitGrey ( size , this . greyLuminance , boldGreyLuminance ) ; } public nominal ( size ) { if ( ! this . cachedNominalSequence || this . cachedNominalSequence . size !== size ) { const { nominalBold , nominal , nominalMuted , nominalHues , } = this . getNominalHueSequences ( size ) ; this . cachedNominalSequence = { bold : nominalBold , std : nominal , muted : nominalMuted , hues : nominalHues , size : nominal . length , } ; } return this . cachedNominalSequence ; } private explicitGrey ( size , startLuminance , endLuminance ) { const greyFrom = [ this . accentHsl [ 0 ] , 0 , startLuminance ] ; const greyTo = [ this . accentHsl [ 0 ] , 0 , endLuminance ] ; return polynomial_hsl_scale ( linearExponent , greyFrom , greyTo , size ) ; } private getAccentsAndComplements ( size , offset ) { const { hues : nominalHues } = this . nominal ( size ) ; const accentHue = nominalHues [ offset % nominalHues . length ] ; const colorLuminance = this . accentHsl [ 2 ] ; const greyLuminance = this . greyLuminance ; const complementHue = ( accentHue + 90 ) % 360 ; const greyAccent = [ accentHue , 0 , greyLuminance ] ; const greyComplement = [ complementHue , 0 , greyLuminance ] ; const maxSaturationAccent = [ accentHue , maxSaturation , colorLuminance , ] ; const maxSaturationComplement = [ complementHue , maxSaturation , colorLuminance , ] ; return { greyAccent , greyComplement , maxSaturationAccent , maxSaturationComplement , } ; } public sequential ( size , offset = 0 ) { const { greyAccent , maxSaturationAccent } = this . getAccentsAndComplements ( size , offset ) ; return polynomial_hsl_scale ( linearExponent , greyAccent , maxSaturationAccent , size ) ; } public sequentialComplement ( size , offset = 0 ) { const { greyAccent , maxSaturationComplement , } = this . getAccentsAndComplements ( size , offset ) ; return polynomial_hsl_scale ( linearExponent , greyAccent , maxSaturationComplement , size ) ; } public diverging ( size , offset = 0 ) { const { greyAccent , greyComplement , maxSaturationAccent , maxSaturationComplement , } = this . getAccentsAndComplements ( size , offset ) ; const half = Math . round ( size / 2 ) ; const cut = size % 2 === 0 ? half + 1 : half ; let diverging = polynomial_hsl_scale ( polynomialExponent , greyComplement , maxSaturationComplement , cut ) . slice ( 1 ) . reverse ( ) ; let toAdd = polynomial_hsl_scale ( polynomialExponent , greyAccent , maxSaturationAccent , cut ) ; if ( size % 2 === 0 ) { toAdd = toAdd . slice ( 1 , cut ) ; } diverging = diverging . concat ( toAdd ) ; return diverging ; } private getNominalHueSequences ( size ) { const nominal = [ ] ; const nominalBold = [ ] ; const nominalMuted = [ ] ; const nominalHues = [ ] ; let hues = [ ] ; const h = this . accentHsl [ 0 ] ; const initialHues = this . nominalHueStep <= 10 ? 13 - this . nominalHueStep : this . nominalHueStep - 8 ; const hueDirection = this . nominalHueStep <= 10 ? - 1 : 1 ; let hueStep = 360.0 / initialHues ; for ( let i = 0 ; i < initialHues ; i += 1 ) { hues . push ( ( h + i * hueDirection * hueStep ) % 360 ) ; } while ( hues . length < size ) { hueStep = hueStep / 2 ; const newHues = [ ] ; for ( const existingHue of hues ) { newHues . push ( ( existingHue + hueDirection * hueStep ) % 360 ) ; if ( hues . length + newHues . length === size ) { break ; } } hues = hues . concat ( newHues ) ; } hues = hues . slice ( 0 , size ) ; let baseSaturation = nominalSaturation ; let baseLuminance = this . scaleLuminance ; const addHue = ( hue ) => { nominal . push ( [ hue , baseSaturation , baseLuminance ] ) ; nominalBold . push ( [ hue , 0.5 * ( nominalSaturation + baseSaturation ) + boldSaturationShift , 0.5 * ( this . scaleLuminance + baseLuminance ) - nominalDarkerShift , ] ) ; nominalMuted . push ( [ hue , 0.5 * ( nominalSaturation + baseSaturation ) - mutedSaturationShift , 0.5 * ( this . scaleLuminance + baseLuminance ) + nominalLighterShift , ] ) ; nominalHues . push ( hue ) ; if ( nominal . length % 3 === 0 ) { baseSaturation = Math . max ( baseSaturation - 1 , minNominalSaturation ) ; } if ( nominal . length % 6 === 0 ) { baseLuminance = Math . max ( baseLuminance - 1 , minNominalLuminance ) ; } } ; for ( const finalHue of hues ) { addHue ( finalHue ) ; } return { nominal , nominalBold , nominalMuted , nominalHues , } ; } } export function hsluv_to_hex ( values ) { return values . map ( c => hsluvToHex ( c ) ) ; } export function getScheme ( params , nominalItemCount , sequentialItemCount , light ) { const { accentHue , accentSaturation , accentLuminance , backgroundLevel , backgroundHueShift , nominalHueStep , } = params ; const colorMaker = new ColorMaker ( [ accentHue , accentSaturation , accentLuminance ] , backgroundLevel , backgroundHueShift , nominalHueStep , light ) ; const nominalSequences = colorMaker . nominal ( nominalItemCount ) ; return { background : hsluvToHex ( colorMaker . backgroundHsl ) , offsetBackground : hsluvToHex ( colorMaker . offsetbackgroundHsl ) , foreground : hsluvToHex ( colorMaker . foregroundHsl ) , accent : hsluvToHex ( colorMaker . accentHsl ) , lowContrastAnnotation : hsluvToHex ( colorMaker . lowContrastAnnotationHsl ) , lowMidContrastAnnotation : hsluvToHex ( colorMaker . lowMidContrastAnnotationHsl ) , midContrastAnnotation : hsluvToHex ( colorMaker . midContrastAnnotationHsl ) , midHighContrastAnnotation : hsluvToHex ( colorMaker . midHighContrastAnnotationHsl ) , highContrastAnnotation : hsluvToHex ( colorMaker . highContrastAnnotationHsl ) , faintAnnotation : hsluvToHex ( colorMaker . faintAnnotationHsl ) , sequential : hsluv_to_hex ( colorMaker . sequential ( sequentialItemCount , 0 ) ) , sequential2 : hsluv_to_hex ( colorMaker . sequential ( sequentialItemCount , 1 ) ) , diverging : hsluv_to_hex ( colorMaker . diverging ( sequentialItemCount , 0 ) ) , diverging2 : hsluv_to_hex ( colorMaker . diverging ( sequentialItemCount , 1 ) ) , nominalBold : hsluv_to_hex ( nominalSequences . bold ) , nominal : hsluv_to_hex ( nominalSequences . std ) , nominalMuted : hsluv_to_hex ( nominalSequences . muted ) , greys : hsluv_to_hex ( colorMaker . grey ( sequentialItemCount ) ) , warning : '#ff8c00' , error : '#d13438' , } ; }