colorable-dominant
Version:
Create ARIA-compliant color themes based on a predominant color palette.
121 lines (102 loc) • 4.11 kB
JavaScript
const colorable = require('colorable')
const Color = require('color')
const getColorDominance = require('./get-color-dominance')
const getRGBRange = require('./get-rgb-range')
const THRESHOLD_CONTRAST_RATIO = 1.0
const MINIMUM_CONTRAST_RATIO = 4.5
const DEFAULT = {
threshold: THRESHOLD_CONTRAST_RATIO,
minContrast: MINIMUM_CONTRAST_RATIO,
compact: true
}
const isNil = value => value === null || value === undefined
const isEmpty = value => value.length === 0
const exists = value => !isNil(value) && !isEmpty(value)
const calculateTotalPairScore = pairs => pairs.reduce((score, color) => score + color.score, 0)
const sortPairsByScore = (pairs = []) =>
pairs.sort((a, b) => {
if (a.score === b.score) return 0
return a.score > b.score ? -1 : 1
})
const getMostDominantPrimaryColor = (colors, WCAGCompliantColorPairs) => {
let highestDominanceScore = 0
let mostDominantColor = ''
for (const dominantColor in WCAGCompliantColorPairs) {
const pairs = WCAGCompliantColorPairs[dominantColor]
const dominance = getColorDominance(dominantColor, colors)
const totalPairScore = calculateTotalPairScore(pairs)
const score = pairs.length ? (pairs.length + totalPairScore) * dominance : 0
if (score > highestDominanceScore) {
highestDominanceScore = score
mostDominantColor = dominantColor
}
}
WCAGCompliantColorPairs[mostDominantColor] = sortPairsByScore(
WCAGCompliantColorPairs[mostDominantColor]
)
return mostDominantColor
}
module.exports = (colors, opts) => {
opts = Object.assign({ compact: true }, DEFAULT, opts)
const { minContrast } = opts
const colorsCombinations = colorable(colors, opts)
const WCAGCompliantColorPairs = {}
colorsCombinations.forEach((colorCombination, index) => {
const { hex, combinations } = colorCombination
const dominantColor = Color(hex)
const pairs = (WCAGCompliantColorPairs[dominantColor.hex()] = [])
combinations.forEach(innerColorCombination => {
let { hex, contrast } = innerColorCombination
let color = Color(hex)
/**
* The score is determined based three things:
*
* contrast:
* how well contrasted the color is with the dominant color.
* dominance:
* the level of dominance of the dominant color
* which is based on the index of the color in
* the palette array.
* range/vibrance:
* we want some vibrant colors
*/
const range = getRGBRange(color)
/**
* If the contrast isn't high enough, lighten/darken
* the color so that we get a more accessible
* version of the color.
*/
if (contrast < minContrast) {
const delta = (minContrast - contrast) / 20
let lighten = dominantColor.isDark()
while (contrast < minContrast) {
const newColor = lighten ? color.lighten(delta) : color.darken(delta)
// If the new color is the same as the old one, we're not getting any
// lighter or darker so we need to stop.
if (newColor.hex() === color.hex()) break
const newContrast = dominantColor.contrast(newColor)
// If the new contrast is lower than the old contrast
// then we need to start moving the other direction in the spectrum
if (newContrast < contrast) {
lighten = !lighten
}
color = newColor
contrast = newContrast
}
}
const score = contrast + range
pairs.push({ color, score, contrast })
})
})
const backgroundColor = getMostDominantPrimaryColor(colors, WCAGCompliantColorPairs)
// prettier-ignore
let [color, alternativeColor, accentColor] = WCAGCompliantColorPairs[backgroundColor]
if (!alternativeColor) alternativeColor = color
if (!accentColor) accentColor = alternativeColor
return {
backgroundColor: exists(backgroundColor) ? backgroundColor : null,
color: exists(color) ? color.color.hex() : null,
alternativeColor: exists(alternativeColor) ? alternativeColor.color.hex() : null
}
}