830 lines
22 KiB
JavaScript
830 lines
22 KiB
JavaScript
import { convert, brightness, contrastRatio } from 'chromatism'
|
|
import { alphaBlend, alphaBlendLayers, getTextColor, mixrgb } from '../color_convert/color_convert.js'
|
|
|
|
/*
|
|
* # What's all this?
|
|
* Here be theme engine for pleromafe. All of this supposed to ease look
|
|
* and feel customization, making widget styles and make developer's life
|
|
* easier when it comes to supporting themes. Like many other theme systems
|
|
* it operates on color definitions, or "slots" - for example you define
|
|
* "button" color slot and then in UI component Button's CSS you refer to
|
|
* it as a CSS3 Variable.
|
|
*
|
|
* Some applications allow you to customize colors for certain things.
|
|
* Some UI toolkits allow you to define colors for each type of widget.
|
|
* Most of them are pretty barebones and have no assistance for common
|
|
* problems and cases, and in general themes themselves are very hard to
|
|
* maintain in all aspects. This theme engine tries to solve all of the
|
|
* common problems with themes.
|
|
*
|
|
* You don't have redefine several similar colors if you just want to
|
|
* change one color - all color slots are derived from other ones, so you
|
|
* can have at least one or two "basic" colors defined and have all other
|
|
* components inherit and modify basic ones.
|
|
*
|
|
* You don't have to test contrast ratio for colors or pick text color for
|
|
* each element even if you have light-on-dark elements in dark-on-light
|
|
* theme.
|
|
*
|
|
* You don't have to maintain order of code for inheriting slots from othet
|
|
* slots - dependency graph resolving does it for you.
|
|
*/
|
|
|
|
/* This indicates that this version of code outputs similar theme data and
|
|
* should be incremented if output changes - for instance if getTextColor
|
|
* function changes and older themes no longer render text colors as
|
|
* author intended previously.
|
|
*/
|
|
export const CURRENT_VERSION = 3
|
|
|
|
/* This is a definition of all layer combinations
|
|
* each key is a topmost layer, each value represents layer underneath
|
|
* this is essentially a simplified tree
|
|
*/
|
|
export const LAYERS = {
|
|
undelay: null, // root
|
|
topBar: null, // no transparency support
|
|
badge: null, // no transparency support
|
|
fg: null,
|
|
bg: 'underlay',
|
|
highlight: 'bg',
|
|
panel: 'bg',
|
|
btn: 'bg',
|
|
btnPanel: 'panel',
|
|
btnTopBar: 'topBar',
|
|
input: 'bg',
|
|
inputPanel: 'panel',
|
|
inputTopBar: 'topBar',
|
|
alert: 'bg',
|
|
alertPanel: 'panel',
|
|
poll: 'bg'
|
|
}
|
|
|
|
/* By default opacity slots have 1 as default opacity
|
|
* this allows redefining it to something else
|
|
*/
|
|
export const DEFAULT_OPACITY = {
|
|
alert: 0.5,
|
|
input: 0.5,
|
|
faint: 0.5,
|
|
underlay: 0.15
|
|
}
|
|
|
|
/** SUBJECT TO CHANGE IN THE FUTURE, this is all beta
|
|
* Color and opacity slots definitions. Each key represents a slot.
|
|
*
|
|
* Short-hands:
|
|
* String beginning with `--` - value after dashes treated as sole
|
|
* dependency - i.e. `--value` equivalent to { depends: ['value']}
|
|
* String beginning with `#` - value would be treated as solid color
|
|
* defined in hexadecimal representation (i.e. #FFFFFF) and will be
|
|
* used as default. `#FFFFFF` is equivalent to { default: '#FFFFFF'}
|
|
*
|
|
* Full definition:
|
|
* @property {String[]} depends - color slot names this color depends ones.
|
|
* cyclic dependencies are supported to some extent but not recommended.
|
|
* @property {String} [opacity] - opacity slot used by this color slot.
|
|
* opacity is inherited from parents. To break inheritance graph use null
|
|
* @property {Number} [priority] - EXPERIMENTAL. used to pre-sort slots so
|
|
* that slots with higher priority come earlier
|
|
* @property {Function(mod, ...colors)} [color] - function that will be
|
|
* used to determine the color. By default it just copies first color in
|
|
* dependency list.
|
|
* @argument {Number} mod - `1` (light-on-dark) or `-1` (dark-on-light)
|
|
* depending on background color (for textColor)/given color.
|
|
* @argument {...Object} deps - each argument after mod represents each
|
|
* color from `depends` array. All colors take user customizations into
|
|
* account and represented by { r, g, b } objects.
|
|
* @returns {Object} resulting color, should be in { r, g, b } form
|
|
*
|
|
* @property {Boolean|String} [textColor] - true to mark color slot as text
|
|
* color. This enables automatic text color generation for the slot. Use
|
|
* 'preserve' string if you don't want text color to fall back to
|
|
* black/white. Use 'bw' to only ever use black or white. This also makes
|
|
* following properties required:
|
|
* @property {String} [layer] - which layer the text sit on top on - used
|
|
* to account for transparency in text color calculation
|
|
* layer is inherited from parents. To break inheritance graph use null
|
|
* @property {String} [variant] - which color slot is background (same as
|
|
* above, used to account for transparency)
|
|
*/
|
|
export const SLOT_INHERITANCE = {
|
|
bg: {
|
|
depends: [],
|
|
opacity: 'bg',
|
|
priority: 1
|
|
},
|
|
fg: {
|
|
depends: [],
|
|
priority: 1
|
|
},
|
|
text: {
|
|
depends: [],
|
|
priority: 1
|
|
},
|
|
underlay: {
|
|
default: '#000000',
|
|
opacity: 'underlay'
|
|
},
|
|
link: {
|
|
depends: ['accent'],
|
|
priority: 1
|
|
},
|
|
accent: {
|
|
depends: ['link'],
|
|
priority: 1
|
|
},
|
|
faint: {
|
|
depends: ['text'],
|
|
opacity: 'faint'
|
|
},
|
|
faintLink: {
|
|
depends: ['link'],
|
|
opacity: 'faint'
|
|
},
|
|
|
|
cBlue: '#0000ff',
|
|
cRed: '#FF0000',
|
|
cGreen: '#00FF00',
|
|
cOrange: '#E3FF00',
|
|
|
|
highlight: {
|
|
depends: ['bg'],
|
|
color: (mod, bg) => brightness(5 * mod, bg).rgb
|
|
},
|
|
highlightFaintText: {
|
|
depends: ['faint'],
|
|
layer: 'highlight',
|
|
textColor: true
|
|
},
|
|
highlightFaintLink: {
|
|
depends: ['faintLink'],
|
|
layer: 'highlight',
|
|
textColor: 'preserve'
|
|
},
|
|
highlightText: {
|
|
depends: ['text'],
|
|
layer: 'highlight',
|
|
textColor: true
|
|
},
|
|
highlightLink: {
|
|
depends: ['link'],
|
|
layer: 'highlight',
|
|
textColor: 'preserve'
|
|
},
|
|
highlightIcon: {
|
|
depends: ['highlight', 'highlightText'],
|
|
color: (mod, bg, text) => mixrgb(bg, text)
|
|
},
|
|
|
|
selectedPost: '--highlight',
|
|
selectedPostFaintText: {
|
|
depends: ['highlightFaintText'],
|
|
layer: 'highlight',
|
|
variant: 'selectedPost',
|
|
textColor: true
|
|
},
|
|
selectedPostFaintLink: {
|
|
depends: ['highlightFaintLink'],
|
|
layer: 'highlight',
|
|
variant: 'selectedPost',
|
|
textColor: 'preserve'
|
|
},
|
|
selectedPostText: {
|
|
depends: ['highlightText'],
|
|
layer: 'highlight',
|
|
variant: 'selectedPost',
|
|
textColor: true
|
|
},
|
|
selectedPostLink: {
|
|
depends: ['highlightLink'],
|
|
layer: 'highlight',
|
|
variant: 'selectedPost',
|
|
textColor: 'preserve'
|
|
},
|
|
selectedPostIcon: {
|
|
depends: ['selectedPost', 'selectedPostText'],
|
|
color: (mod, bg, text) => mixrgb(bg, text)
|
|
},
|
|
|
|
selectedMenu: '--highlight',
|
|
selectedMenuFaintText: {
|
|
depends: ['highlightFaintText'],
|
|
layer: 'highlight',
|
|
variant: 'selectedMenu',
|
|
textColor: true
|
|
},
|
|
selectedMenuFaintLink: {
|
|
depends: ['highlightFaintLink'],
|
|
layer: 'highlight',
|
|
variant: 'selectedMenu',
|
|
textColor: 'preserve'
|
|
},
|
|
selectedMenuText: {
|
|
depends: ['highlightText'],
|
|
layer: 'highlight',
|
|
variant: 'selectedMenu',
|
|
textColor: true
|
|
},
|
|
selectedMenuLink: {
|
|
depends: ['highlightLink'],
|
|
layer: 'highlight',
|
|
variant: 'selectedMenu',
|
|
textColor: 'preserve'
|
|
},
|
|
selectedMenuIcon: {
|
|
depends: ['selectedMenu', 'selectedMenuText'],
|
|
color: (mod, bg, text) => mixrgb(bg, text)
|
|
},
|
|
|
|
lightText: {
|
|
depends: ['text'],
|
|
color: (mod, text) => brightness(20 * mod, text).rgb
|
|
},
|
|
|
|
border: {
|
|
depends: ['fg'],
|
|
opacity: 'border',
|
|
color: (mod, fg) => brightness(2 * mod, fg).rgb
|
|
},
|
|
|
|
poll: {
|
|
depends: ['accent', 'bg'],
|
|
copacity: 'poll',
|
|
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg)
|
|
},
|
|
pollText: {
|
|
depends: ['text'],
|
|
layer: 'poll',
|
|
textColor: true
|
|
},
|
|
|
|
icon: {
|
|
depends: ['bg', 'text'],
|
|
inheritsOpacity: false,
|
|
color: (mod, bg, text) => mixrgb(bg, text)
|
|
},
|
|
|
|
// Foreground
|
|
fgText: {
|
|
depends: ['text'],
|
|
layer: 'fg',
|
|
textColor: true
|
|
},
|
|
fgLink: {
|
|
depends: ['link'],
|
|
layer: 'fg',
|
|
textColor: 'preserve'
|
|
},
|
|
|
|
// Panel header
|
|
panel: {
|
|
depends: ['fg'],
|
|
opacity: 'panel'
|
|
},
|
|
panelText: {
|
|
depends: ['fgText'],
|
|
layer: 'panel',
|
|
textColor: true
|
|
},
|
|
panelFaint: {
|
|
depends: ['fgText'],
|
|
layer: 'panel',
|
|
opacity: 'faint',
|
|
textColor: true
|
|
},
|
|
panelLink: {
|
|
depends: ['fgLink'],
|
|
layer: 'panel',
|
|
textColor: 'preserve'
|
|
},
|
|
|
|
// Top bar
|
|
topBar: '--fg',
|
|
topBarText: {
|
|
depends: ['fgText'],
|
|
layer: 'topBar',
|
|
textColor: true
|
|
},
|
|
topBarLink: {
|
|
depends: ['fgLink'],
|
|
layer: 'topBar',
|
|
textColor: 'preserve'
|
|
},
|
|
|
|
// Tabs
|
|
tab: '--btn',
|
|
tabText: {
|
|
depends: ['btnText'],
|
|
layer: 'btn',
|
|
textColor: true
|
|
},
|
|
tabActiveText: {
|
|
depends: ['text'],
|
|
layer: 'bg',
|
|
textColor: true
|
|
},
|
|
|
|
// Buttons
|
|
btn: {
|
|
depends: ['fg'],
|
|
opacity: 'btn'
|
|
},
|
|
btnText: {
|
|
depends: ['fgText'],
|
|
layer: 'btn',
|
|
textColor: true
|
|
},
|
|
btnPanelText: {
|
|
depends: ['panelText'],
|
|
layer: 'btnPanel',
|
|
variant: 'btn',
|
|
textColor: true
|
|
},
|
|
btnTopBarText: {
|
|
depends: ['topBarText'],
|
|
layer: 'btnTopBar',
|
|
variant: 'btn',
|
|
textColor: true
|
|
},
|
|
|
|
// Buttons: pressed
|
|
btnPressed: '--btn',
|
|
btnPressedText: {
|
|
depends: ['btnText'],
|
|
layer: 'btn',
|
|
variant: 'btnPressed',
|
|
textColor: true
|
|
},
|
|
btnPressedPanel: {
|
|
depends: ['btnPressed']
|
|
},
|
|
btnPressedPanelText: {
|
|
depends: ['btnPanelText'],
|
|
layer: 'btnPanel',
|
|
variant: 'btnPressed',
|
|
textColor: true
|
|
},
|
|
btnPressedTopBarText: {
|
|
depends: ['btnTopBarText'],
|
|
layer: 'btnTopBar',
|
|
variant: 'btnPressed',
|
|
textColor: true
|
|
},
|
|
|
|
// Buttons: toggled
|
|
btnToggled: {
|
|
depends: ['btn'],
|
|
color: (mod, btn) => brightness(mod * 20, btn).rgb
|
|
},
|
|
btnToggledText: {
|
|
depends: ['btnText'],
|
|
layer: 'btn',
|
|
variant: 'btnToggled',
|
|
textColor: true
|
|
},
|
|
btnToggledPanelText: {
|
|
depends: ['btnPanelText'],
|
|
layer: 'btnPanel',
|
|
variant: 'btnToggled',
|
|
textColor: true
|
|
},
|
|
btnToggledTopBarText: {
|
|
depends: ['btnTopBarText'],
|
|
layer: 'btnTopBar',
|
|
variant: 'btnToggled',
|
|
textColor: true
|
|
},
|
|
|
|
// Buttons: disabled
|
|
btnDisabled: {
|
|
depends: ['btn', 'bg'],
|
|
color: (mod, btn, bg) => alphaBlend(btn, 0.5, bg)
|
|
},
|
|
btnDisabledText: {
|
|
depends: ['btnText'],
|
|
layer: 'btn',
|
|
variant: 'btnDisabled',
|
|
textColor: true,
|
|
color: (mod, text) => brightness(mod * -60, text).rgb
|
|
},
|
|
btnDisabledPanelText: {
|
|
depends: ['btnPanelText'],
|
|
layer: 'btnPanel',
|
|
variant: 'btnDisabled',
|
|
textColor: true,
|
|
color: (mod, text) => brightness(mod * -60, text).rgb
|
|
},
|
|
btnDisabledTopBarText: {
|
|
depends: ['btnTopBarText'],
|
|
layer: 'btnTopBar',
|
|
variant: 'btnDisabled',
|
|
textColor: true,
|
|
color: (mod, text) => brightness(mod * -60, text).rgb
|
|
},
|
|
|
|
// Input fields
|
|
input: {
|
|
depends: ['fg'],
|
|
opacity: 'input'
|
|
},
|
|
inputText: {
|
|
depends: ['text'],
|
|
layer: 'input',
|
|
textColor: true
|
|
},
|
|
inputPanelText: {
|
|
depends: ['panelText'],
|
|
layer: 'inputPanel',
|
|
variant: 'input',
|
|
textColor: true
|
|
},
|
|
inputTopbarText: {
|
|
depends: ['topBarText'],
|
|
layer: 'inputTopBar',
|
|
variant: 'input',
|
|
textColor: true
|
|
},
|
|
|
|
alertError: {
|
|
depends: ['cRed'],
|
|
opacity: 'alert'
|
|
},
|
|
alertErrorText: {
|
|
depends: ['text'],
|
|
layer: 'alert',
|
|
variant: 'alertError',
|
|
textColor: true
|
|
},
|
|
alertErrorPanelText: {
|
|
depends: ['panelText'],
|
|
layer: 'alertPanel',
|
|
variant: 'alertError',
|
|
textColor: true
|
|
},
|
|
|
|
alertWarning: {
|
|
depends: ['cOrange'],
|
|
opacity: 'alert'
|
|
},
|
|
alertWarningText: {
|
|
depends: ['text'],
|
|
layer: 'alert',
|
|
variant: 'alertWarning',
|
|
textColor: true
|
|
},
|
|
alertWarningPanelText: {
|
|
depends: ['panelText'],
|
|
layer: 'alertPanel',
|
|
variant: 'alertWarning',
|
|
textColor: true
|
|
},
|
|
|
|
badgeNotification: '--cRed',
|
|
badgeNotificationText: {
|
|
depends: ['text', 'badgeNotification'],
|
|
layer: 'badge',
|
|
variant: 'badgeNotification',
|
|
textColor: 'bw'
|
|
}
|
|
}
|
|
|
|
export const getLayersArray = (layer, data = LAYERS) => {
|
|
let array = [layer]
|
|
let parent = data[layer]
|
|
while (parent) {
|
|
array.unshift(parent)
|
|
parent = data[parent]
|
|
}
|
|
return array
|
|
}
|
|
|
|
export const getLayers = (layer, variant = layer, opacitySlot, colors, opacity) => {
|
|
return getLayersArray(layer).map((currentLayer) => ([
|
|
currentLayer === layer
|
|
? colors[variant]
|
|
: colors[currentLayer],
|
|
currentLayer === layer
|
|
? opacity[opacitySlot] || 1
|
|
: opacity[currentLayer]
|
|
]))
|
|
}
|
|
|
|
const getDependencies = (key, inheritance) => {
|
|
const data = inheritance[key]
|
|
if (typeof data === 'string' && data.startsWith('--')) {
|
|
return [data.substring(2)]
|
|
} else {
|
|
if (data === null) return []
|
|
const { depends, layer, variant } = data
|
|
const layerDeps = layer
|
|
? getLayersArray(layer).map(currentLayer => {
|
|
return currentLayer === layer
|
|
? variant || layer
|
|
: currentLayer
|
|
})
|
|
: []
|
|
if (Array.isArray(depends)) {
|
|
return [...depends, ...layerDeps]
|
|
} else {
|
|
return [...layerDeps]
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sorts inheritance object topologically - dependant slots come after
|
|
* dependencies
|
|
*
|
|
* @property {Object} inheritance - object defining the nodes
|
|
* @property {Function} getDeps - function that returns dependencies for
|
|
* given value and inheritance object.
|
|
* @returns {String[]} keys of inheritance object, sorted in topological
|
|
* order. Additionally, dependency-less nodes will always be first in line
|
|
*/
|
|
export const topoSort = (
|
|
inheritance = SLOT_INHERITANCE,
|
|
getDeps = getDependencies
|
|
) => {
|
|
// This is an implementation of https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
|
|
|
|
const allKeys = Object.keys(inheritance)
|
|
const whites = new Set(allKeys)
|
|
const grays = new Set()
|
|
const blacks = new Set()
|
|
const unprocessed = [...allKeys]
|
|
const output = []
|
|
|
|
const step = (node) => {
|
|
if (whites.has(node)) {
|
|
// Make node "gray"
|
|
whites.delete(node)
|
|
grays.add(node)
|
|
// Do step for each node connected to it (one way)
|
|
getDeps(node, inheritance).forEach(step)
|
|
// Make node "black"
|
|
grays.delete(node)
|
|
blacks.add(node)
|
|
// Put it into the output list
|
|
output.push(node)
|
|
} else if (grays.has(node)) {
|
|
console.debug('Cyclic depenency in topoSort, ignoring')
|
|
output.push(node)
|
|
} else if (blacks.has(node)) {
|
|
// do nothing
|
|
} else {
|
|
throw new Error('Unintended condition in topoSort!')
|
|
}
|
|
}
|
|
while (unprocessed.length > 0) {
|
|
step(unprocessed.pop())
|
|
}
|
|
return output.sort((a, b) => {
|
|
const depsA = getDeps(a, inheritance).length
|
|
const depsB = getDeps(b, inheritance).length
|
|
|
|
if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return 0
|
|
if (depsA === 0 && depsB !== 0) return -1
|
|
if (depsB === 0 && depsA !== 0) return 1
|
|
})
|
|
}
|
|
|
|
/**
|
|
* retrieves opacity slot for given slot. This goes up the depenency graph
|
|
* to find which parent has opacity slot defined for it.
|
|
* TODO refactor this
|
|
*/
|
|
export const getOpacitySlot = (
|
|
v,
|
|
inheritance = SLOT_INHERITANCE,
|
|
getDeps = getDependencies
|
|
) => {
|
|
const value = typeof v === 'string'
|
|
? {
|
|
depends: v.startsWith('--') ? [v.substring(2)] : []
|
|
}
|
|
: v
|
|
if (value.opacity === null) return
|
|
if (value.opacity) return v.opacity
|
|
const findInheritedOpacity = (val) => {
|
|
const depSlot = val.depends[0]
|
|
if (depSlot === undefined) return
|
|
const dependency = getDeps(depSlot, inheritance)[0]
|
|
if (dependency === undefined) return
|
|
if (dependency.opacity || dependency === null) {
|
|
return dependency.opacity
|
|
} else if (dependency.depends) {
|
|
return findInheritedOpacity(dependency)
|
|
} else {
|
|
return null
|
|
}
|
|
}
|
|
if (value.depends) {
|
|
return findInheritedOpacity(value)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* retrieves layer slot for given slot. This goes up the depenency graph
|
|
* to find which parent has opacity slot defined for it.
|
|
* this is basically copypaste of getOpacitySlot except it checks if key is
|
|
* in LAYERS
|
|
* TODO refactor this
|
|
*/
|
|
export const getLayerSlot = (
|
|
k,
|
|
v,
|
|
inheritance = SLOT_INHERITANCE,
|
|
getDeps = getDependencies
|
|
) => {
|
|
const value = typeof v === 'string'
|
|
? {
|
|
depends: v.startsWith('--') ? [v.substring(2)] : []
|
|
}
|
|
: v
|
|
if (LAYERS[k]) return k
|
|
if (value.layer === null) return
|
|
if (value.layer) return v.layer
|
|
const findInheritedLayer = (val) => {
|
|
const depSlot = val.depends[0]
|
|
if (depSlot === undefined) return
|
|
const dependency = getDeps(depSlot, inheritance)[0]
|
|
if (dependency === undefined) return
|
|
if (dependency.layer || dependency === null) {
|
|
return dependency.layer
|
|
} else if (dependency.depends) {
|
|
return findInheritedLayer(dependency)
|
|
} else {
|
|
return null
|
|
}
|
|
}
|
|
if (value.depends) {
|
|
return findInheritedLayer(value)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* topologically sorted SLOT_INHERITANCE
|
|
*/
|
|
export const SLOT_ORDERED = topoSort(
|
|
Object.entries(SLOT_INHERITANCE)
|
|
.sort(([aK, aV], [bK, bV]) => ((aV && aV.priority) || 0) - ((bV && bV.priority) || 0))
|
|
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
|
|
)
|
|
|
|
/**
|
|
* Dictionary where keys are color slots and values are opacity associated
|
|
* with them
|
|
*/
|
|
export const SLOTS_OPACITIES_DICT = Object.entries(SLOT_INHERITANCE).reduce((acc, [k, v]) => {
|
|
const opacity = getOpacitySlot(v, SLOT_INHERITANCE, getDependencies)
|
|
if (opacity) {
|
|
return { ...acc, [k]: opacity }
|
|
} else {
|
|
return acc
|
|
}
|
|
}, {})
|
|
|
|
/**
|
|
* All opacity slots used in color slots, their default values and affected
|
|
* color slots.
|
|
*/
|
|
export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k, v]) => {
|
|
const opacity = getOpacitySlot(v, SLOT_INHERITANCE, getDependencies)
|
|
if (opacity) {
|
|
return {
|
|
...acc,
|
|
[opacity]: {
|
|
defaultValue: DEFAULT_OPACITY[opacity] || 1,
|
|
affectedSlots: [...((acc[opacity] && acc[opacity].affectedSlots) || []), k]
|
|
}
|
|
}
|
|
} else {
|
|
return acc
|
|
}
|
|
}, {})
|
|
|
|
/**
|
|
* Handle dynamic color
|
|
*/
|
|
export const computeDynamicColor = (sourceColor, getColor, mod) => {
|
|
if (typeof sourceColor !== 'string' || !sourceColor.startsWith('--')) return sourceColor
|
|
let targetColor = null
|
|
// Color references other color
|
|
const [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim())
|
|
const variableSlot = variable.substring(2)
|
|
targetColor = getColor(variableSlot)
|
|
if (modifier) {
|
|
targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
|
|
}
|
|
return targetColor
|
|
}
|
|
|
|
/**
|
|
* THE function you want to use. Takes provided colors and opacities, mod
|
|
* value and uses inheritance data to figure out color needed for the slot.
|
|
*/
|
|
export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.reduce(({ colors, opacity }, key) => {
|
|
const value = SLOT_INHERITANCE[key]
|
|
const isObject = typeof value === 'object'
|
|
const isString = typeof value === 'string'
|
|
const sourceColor = sourceColors[key]
|
|
let outputColor = null
|
|
if (sourceColor) {
|
|
// Color is defined in source color
|
|
let targetColor = sourceColor
|
|
if (targetColor === 'transparent') {
|
|
// We take only layers below current one
|
|
const layers = getLayers(
|
|
getLayerSlot(key, value),
|
|
key,
|
|
value.opacity || key,
|
|
colors,
|
|
opacity
|
|
).slice(0, -1)
|
|
targetColor = {
|
|
// TODO: try to use alpha-blended background here
|
|
...alphaBlendLayers(
|
|
convert('#FF00FF').rgb,
|
|
layers
|
|
),
|
|
a: 0
|
|
}
|
|
} else if (typeof sourceColor === 'string' && sourceColor.startsWith('--')) {
|
|
targetColor = computeDynamicColor(
|
|
sourceColor,
|
|
variableSlot => colors[variableSlot] || sourceColors[variableSlot],
|
|
mod
|
|
)
|
|
} else if (typeof sourceColor === 'string' && sourceColor.startsWith('#')) {
|
|
targetColor = convert(targetColor).rgb
|
|
}
|
|
outputColor = { ...targetColor }
|
|
} else if (isString && value.startsWith('#')) {
|
|
// slot: '#000000' shorthand
|
|
outputColor = convert(value).rgb
|
|
} else if (isObject && value.default) {
|
|
// same as above except in object form
|
|
outputColor = convert(value.default).rgb
|
|
} else {
|
|
// calculate color
|
|
const defaultColorFunc = (mod, dep) => ({ ...dep })
|
|
const deps = getDependencies(key, SLOT_INHERITANCE)
|
|
const colorFunc = (isObject && value.color) || defaultColorFunc
|
|
|
|
if (value.textColor) {
|
|
// textColor case
|
|
const bg = alphaBlendLayers(
|
|
{ ...colors[deps[0]] },
|
|
getLayers(
|
|
value.layer,
|
|
value.variant || value.layer,
|
|
getOpacitySlot(SLOT_INHERITANCE[value.variant || value.layer]),
|
|
colors,
|
|
opacity
|
|
)
|
|
)
|
|
if (value.textColor === 'bw') {
|
|
outputColor = contrastRatio(bg).rgb
|
|
} else {
|
|
let color = { ...colors[deps[0]] }
|
|
if (value.color) {
|
|
const isLightOnDark = convert(bg).hsl.l < convert(color).hsl.l
|
|
const mod = isLightOnDark ? 1 : -1
|
|
color = value.color(mod, ...deps.map((dep) => ({ ...colors[dep] })))
|
|
}
|
|
|
|
outputColor = getTextColor(
|
|
bg,
|
|
{ ...color },
|
|
value.textColor === 'preserve'
|
|
)
|
|
}
|
|
} else {
|
|
// background color case
|
|
outputColor = colorFunc(
|
|
mod,
|
|
...deps.map((dep) => ({ ...colors[dep] }))
|
|
)
|
|
}
|
|
}
|
|
if (!outputColor) {
|
|
throw new Error('Couldn\'t generate color for ' + key)
|
|
}
|
|
const opacitySlot = SLOTS_OPACITIES_DICT[key]
|
|
if (opacitySlot && outputColor.a === undefined) {
|
|
outputColor.a = sourceOpacity[opacitySlot] || OPACITIES[opacitySlot].defaultValue || 1
|
|
}
|
|
if (opacitySlot) {
|
|
return {
|
|
colors: { ...colors, [key]: outputColor },
|
|
opacity: { ...opacity, [opacitySlot]: outputColor.a }
|
|
}
|
|
} else {
|
|
return {
|
|
colors: { ...colors, [key]: outputColor },
|
|
opacity
|
|
}
|
|
}
|
|
}, { colors: {}, opacity: {} })
|