Refactoring, forgotten files

This commit is contained in:
Henry Jameson 2020-01-12 03:44:06 +02:00
parent 38f2b969e4
commit 622c9d388e
5 changed files with 577 additions and 421 deletions

View file

@ -0,0 +1,65 @@
@import '../../_variables.scss';
.color-input {
display: inline-flex;
&-field.input {
display: inline-flex;
flex: 0 0 0;
max-width: 9em;
align-items: stretch;
padding: .2em 8px;
input {
background: none;
color: $fallback--lightText;
color: var(--inputText, $fallback--lightText);
border: none;
padding: 0;
margin: 0;
&.textColor {
flex: 1 0 3em;
min-width: 3em;
padding: 0;
}
&.nativeColor {
flex: 0 0 2em;
min-width: 2em;
align-self: center;
height: 100%;
}
}
.transparentIndicator {
flex: 0 0 2em;
min-width: 2em;
align-self: center;
height: 100%;
// forgot to install counter-strike source, ooops
background-color: #FF00FF;
position: relative;
&::before, &::after {
display: block;
content: '';
background-color: #000000;
position: absolute;
height: 50%;
width: 50%;
}
&::after {
top: 0;
left: 0;
}
&::before {
bottom: 0;
right: 0;
}
}
}
.label {
flex: 1 1 auto;
}
}

View file

@ -1,9 +1,16 @@
import { map } from 'lodash' import { invertLightness, convert, contrastRatio } from 'chromatism'
// useful for visualizing color when debugging // useful for visualizing color when debugging
export const consoleColor = (color) => console.log('%c##########', 'background: ' + color + '; color: ' + color) export const consoleColor = (color) => console.log('%c##########', 'background: ' + color + '; color: ' + color)
const rgb2hex = (r, g, b) => { /**
* Convert r, g, b values into hex notation. All components are [0-255]
*
* @param {Number|String|Object} r - Either red component, {r,g,b} object, or hex string
* @param {Number} [g] - Green component
* @param {Number} [b] - Blue component
*/
export const rgb2hex = (r, g, b) => {
if (r === null || typeof r === 'undefined') { if (r === null || typeof r === 'undefined') {
return undefined return undefined
} }
@ -14,7 +21,7 @@ const rgb2hex = (r, g, b) => {
if (typeof r === 'object') { if (typeof r === 'object') {
({ r, g, b } = r) ({ r, g, b } = r)
} }
[r, g, b] = map([r, g, b], (val) => { [r, g, b] = [r, g, b].map(val => {
val = Math.ceil(val) val = Math.ceil(val)
val = val < 0 ? 0 : val val = val < 0 ? 0 : val
val = val > 255 ? 255 : val val = val > 255 ? 255 : val
@ -82,6 +89,7 @@ const getContrastRatio = (a, b) => {
return (l1 + 0.05) / (l2 + 0.05) return (l1 + 0.05) / (l2 + 0.05)
} }
/** /**
* Same as `getContrastRatio` but for multiple layers in-between * Same as `getContrastRatio` but for multiple layers in-between
* *
@ -101,7 +109,7 @@ export const getContrastRatioLayers = (text, layers, bedrock) => {
* @param {Object} bg - bottom layer color * @param {Object} bg - bottom layer color
* @returns {Object} sRGB of resulting color * @returns {Object} sRGB of resulting color
*/ */
const alphaBlend = (fg, fga, bg) => { export const alphaBlend = (fg, fga, bg) => {
if (fga === 1 || typeof fga === 'undefined') return fg if (fga === 1 || typeof fga === 'undefined') return fg
return 'rgb'.split('').reduce((acc, c) => { return 'rgb'.split('').reduce((acc, c) => {
// Simplified https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending // Simplified https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
@ -121,14 +129,20 @@ export const alphaBlendLayers = (bedrock, layers) => layers.reduce((acc, [color,
return alphaBlend(color, opacity, acc) return alphaBlend(color, opacity, acc)
}, bedrock) }, bedrock)
const invert = (rgb) => { export const invert = (rgb) => {
return 'rgb'.split('').reduce((acc, c) => { return 'rgb'.split('').reduce((acc, c) => {
acc[c] = 255 - rgb[c] acc[c] = 255 - rgb[c]
return acc return acc
}, {}) }, {})
} }
const hex2rgb = (hex) => { /**
* Converts #rrggbb hex notation into an {r, g, b} object
*
* @param {String} hex - #rrggbb string
* @returns {Object} rgb representation of the color, values are 0-255
*/
export const hex2rgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? { return result ? {
r: parseInt(result[1], 16), r: parseInt(result[1], 16),
@ -137,18 +151,75 @@ const hex2rgb = (hex) => {
} : null } : null
} }
const mixrgb = (a, b) => { /**
* Old somewhat weird function for mixing two colors together
*
* @param {Object} a - one color (rgb)
* @param {Object} b - other color (rgb)
* @returns {Object} result
*/
export const mixrgb = (a, b) => {
return Object.keys(a).reduce((acc, k) => { return Object.keys(a).reduce((acc, k) => {
acc[k] = (a[k] + b[k]) / 2 acc[k] = (a[k] + b[k]) / 2
return acc return acc
}, {}) }, {})
} }
/**
export { * Converts rgb object into a CSS rgba() color
rgb2hex, *
hex2rgb, * @param {Object} color - rgb
mixrgb, * @returns {String} CSS rgba() color
invert, */
getContrastRatio, export const rgba2css = function (rgba) {
alphaBlend return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
}
/**
* Get text color for given background color and intended text color
* This checks if text and background don't have enough color and inverts
* text color's lightness if needed. If text color is still not enough it
* will fall back to black or white
*
* @param {Object} bg - background color
* @param {Object} text - intended text color
* @param {Boolean} preserve - try to preserve intended text color's hue/saturation (i.e. no BW)
*/
export const getTextColor = function (bg, text, preserve) {
const bgIsLight = convert(bg).hsl.l > 50
const textIsLight = convert(text).hsl.l > 50
if ((bgIsLight && textIsLight) || (!bgIsLight && !textIsLight)) {
const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
const result = Object.assign(base, invertLightness(text).rgb)
if (!preserve && getContrastRatio(bg, result) < 4.5) {
// B&W
return contrastRatio(bg, text).rgb
}
// Inverted color
return result
}
return text
}
/**
* Converts color to CSS Color value
*
* @param {Object|String} input - color
* @param {Number} [a] - alpha value
* @returns {String} a CSS Color value
*/
export const getCssColor = (input, a) => {
let rgb = {}
if (typeof input === 'object') {
rgb = input
} else if (typeof input === 'string') {
if (input.startsWith('#')) {
rgb = hex2rgb(input)
} else if (input.startsWith('--')) {
return `var(${input})`
} else {
return input
}
}
return rgba2css({ ...rgb, a })
} }

View file

@ -1,275 +1,13 @@
import { times } from 'lodash' import { times } from 'lodash'
import { brightness, invertLightness, convert, contrastRatio } from 'chromatism' import { convert } from 'chromatism'
import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend, alphaBlendLayers } from '../color_convert/color_convert.js' import { rgb2hex, hex2rgb, rgba2css, getCssColor } from '../color_convert/color_convert.js'
import { getColors } from '../theme_data/theme_data.service.js'
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',
panel: 'bg',
btn: 'bg',
btnPanel: 'panel',
btnTopBar: 'topBar',
input: 'bg',
inputPanel: 'panel',
inputTopBar: 'topBar',
alert: 'bg',
alertPanel: 'panel'
}
export const SLOT_INHERITANCE = {
bg: null,
fg: null,
text: null,
underlay: '#000000',
link: '--accent',
accent: '--link',
faint: '--text',
faintLink: '--link',
cBlue: '#0000ff',
cRed: '#FF0000',
cGreen: '#00FF00',
cOrange: '#E3FF00',
lightBg: {
depends: ['bg'],
color: (mod, bg) => brightness(5 * mod, bg).rgb
},
lightText: {
depends: ['text'],
color: (mod, text) => brightness(20 * mod, text).rgb
},
border: {
depends: 'fg',
color: (mod, fg) => brightness(2 * mod, fg).rgb
},
linkBg: {
depends: ['accent', 'bg'],
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg).rgb
},
icon: {
depends: ['bg', 'text'],
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: '--fg',
panelText: {
depends: ['fgText'],
layer: 'panel',
textColor: true
},
panelFaint: {
depends: ['fgText'],
layer: 'panel',
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'
},
// Buttons
btn: '--fg',
btnText: {
depends: ['fgText'],
layer: 'btn'
},
btnPanelText: {
depends: ['panelText'],
layer: 'btnPanel',
variant: 'btn',
textColor: true
},
btnTopBarText: {
depends: ['topBarText'],
layer: 'btnTopBar',
variant: 'btn',
textColor: true
},
// Input fields
input: '--fg',
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: '--cRed',
alertErrorText: {
depends: ['text', 'alertError'],
layer: 'alert',
variant: 'alertError',
textColor: true
},
alertErrorPanelText: {
depends: ['panelText', 'alertError'],
layer: 'alertPanel',
variant: 'alertError',
textColor: true
},
alertWarning: '--cOrange',
alertWarningText: {
depends: ['text', 'alertWarning'],
layer: 'alert',
variant: 'alertWarning',
textColor: true
},
alertWarningPanelText: {
depends: ['panelText', 'alertWarning'],
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, colors, opacity) => {
return getLayersArray(layer).map((currentLayer) => ([
currentLayer === layer
? colors[variant]
: colors[currentLayer],
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]
}
}
}
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
}
export const SLOT_ORDERED = topoSort(SLOT_INHERITANCE)
// While this is not used anymore right now, I left it in if we want to do custom // While this is not used anymore right now, I left it in if we want to do custom
// styles that aren't just colors, so user can pick from a few different distinct // styles that aren't just colors, so user can pick from a few different distinct
// styles as well as set their own colors in the future. // styles as well as set their own colors in the future.
const setStyle = (href, commit) => { export const setStyle = (href, commit) => {
/*** /***
What's going on here? What's going on here?
I want to make it easy for admins to style this application. To have I want to make it easy for admins to style this application. To have
@ -315,30 +53,7 @@ const setStyle = (href, commit) => {
cssEl.addEventListener('load', setDynamic) cssEl.addEventListener('load', setDynamic)
} }
const rgb2rgba = function (rgba) { export const applyTheme = (input, commit) => {
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
}
const getTextColor = function (bg, text, preserve) {
const bgIsLight = convert(bg).hsl.l > 50
const textIsLight = convert(text).hsl.l > 50
console.log(bgIsLight, textIsLight)
if ((bgIsLight && textIsLight) || (!bgIsLight && !textIsLight)) {
const base = typeof text.a !== 'undefined' ? { a: text.a } : {}
const result = Object.assign(base, invertLightness(text).rgb)
if (!preserve && getContrastRatio(bg, result) < 4.5) {
// B&W
return contrastRatio(bg, text).rgb
}
// Inverted color
return result
}
return text
}
const applyTheme = (input, commit) => {
const { rules, theme } = generatePreset(input) const { rules, theme } = generatePreset(input)
const head = document.head const head = document.head
const body = document.body const body = document.body
@ -399,22 +114,6 @@ const getCssShadowFilter = (input) => {
.join(' ') .join(' ')
} }
const getCssColor = (input, a) => {
let rgb = {}
if (typeof input === 'object') {
rgb = input
} else if (typeof input === 'string') {
if (input.startsWith('#')) {
rgb = hex2rgb(input)
} else if (input.startsWith('--')) {
return `var(${input})`
} else {
return input
}
}
return rgb2rgba({ ...rgb, a })
}
const generateColors = (themeData) => { const generateColors = (themeData) => {
const rawOpacity = Object.assign({ const rawOpacity = Object.assign({
panel: 1, panel: 1,
@ -435,14 +134,16 @@ const generateColors = (themeData) => {
}, {})) }, {}))
const inputColors = themeData.colors || themeData const inputColors = themeData.colors || themeData
const transparentsOpacity = Object.entries(inputColors).reduce((acc, [k, v]) => {
if (v === 'transparent') {
acc[k] = 0
}
return acc
}, {})
const opacity = { ...rawOpacity, ...transparentsOpacity } const opacity = {
...rawOpacity,
...Object.entries(inputColors).reduce((acc, [k, v]) => {
if (v === 'transparent') {
acc[k] = 0
}
return acc
}, {})
}
// Cycle one: just whatever we have // Cycle one: just whatever we have
const sourceColors = Object.entries(inputColors).reduce((acc, [k, v]) => { const sourceColors = Object.entries(inputColors).reduce((acc, [k, v]) => {
@ -462,55 +163,7 @@ const generateColors = (themeData) => {
const isLightOnDark = convert(sourceColors.bg).hsl.l < convert(sourceColors.text).hsl.l const isLightOnDark = convert(sourceColors.bg).hsl.l < convert(sourceColors.text).hsl.l
const mod = isLightOnDark ? 1 : -1 const mod = isLightOnDark ? 1 : -1
const colors = SLOT_ORDERED.reduce((acc, key) => { const colors = getColors(sourceColors, opacity, mod)
const value = SLOT_INHERITANCE[key]
if (sourceColors[key]) {
return { ...acc, [key]: { ...sourceColors[key] } }
} else if (typeof value === 'string' && value.startsWith('#')) {
return { ...acc, [key]: convert(value).rgb }
} else {
const isObject = typeof value === 'object'
const defaultColorFunc = (mod, dep) => ({ ...dep })
const deps = getDependencies(key, SLOT_INHERITANCE)
const colorFunc = (isObject && value.color) || defaultColorFunc
if (value.textColor) {
const bg = alphaBlendLayers(
{ ...acc[deps[0]] },
getLayers(
value.layer,
value.variant || value.layer,
acc,
opacity
)
)
if (value.textColor === 'bw') {
return {
...acc,
[key]: contrastRatio(bg)
}
} else {
return {
...acc,
[key]: getTextColor(
bg,
{ ...acc[deps[0]] },
value.textColor === 'preserve'
)
}
}
} else {
console.log('BENIS', key, deps, deps.map((dep) => ({ ...acc[dep] })))
return {
...acc,
[key]: colorFunc(
mod,
...deps.map((dep) => ({ ...acc[dep] }))
)
}
}
}
}, {})
// Inheriting opacities // Inheriting opacities
Object.entries(opacity).forEach(([ k, v ]) => { Object.entries(opacity).forEach(([ k, v ]) => {
@ -541,7 +194,7 @@ const generateColors = (themeData) => {
.reduce((acc, [k, v]) => { .reduce((acc, [k, v]) => {
if (!v) return acc if (!v) return acc
acc.solid[k] = rgb2hex(v) acc.solid[k] = rgb2hex(v)
acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgb2rgba(v) acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
return acc return acc
}, { complete: {}, solid: {} }) }, { complete: {}, solid: {} })
return { return {
@ -740,14 +393,12 @@ const composePreset = (colors, radii, shadows, fonts) => {
} }
} }
const generatePreset = (input) => { const generatePreset = (input) => composePreset(
const shadows = generateShadows(input) generateColors(input),
const colors = generateColors(input) generateRadii(input),
const radii = generateRadii(input) generateShadows(input),
const fonts = generateFonts(input) generateFonts(input)
)
return composePreset(colors, radii, shadows, fonts)
}
const getThemes = () => { const getThemes = () => {
return window.fetch('/static/styles.json') return window.fetch('/static/styles.json')
@ -779,33 +430,24 @@ const getThemes = () => {
}) })
} }
const setPreset = (val, commit) => { export const setPreset = (val, commit) => {
return getThemes().then((themes) => { return getThemes().then((themes) => {
const theme = themes[val] ? themes[val] : themes['pleroma-dark'] const theme = themes[val] ? themes[val] : themes['pleroma-dark']
const isV1 = Array.isArray(theme) const isV1 = Array.isArray(theme)
const data = isV1 ? {} : theme.theme const data = isV1 ? {} : theme.theme
if (isV1) { if (isV1) {
const bgRgb = hex2rgb(theme[1]) const bg = hex2rgb(theme[1])
const fgRgb = hex2rgb(theme[2]) const fg = hex2rgb(theme[2])
const textRgb = hex2rgb(theme[3]) const text = hex2rgb(theme[3])
const linkRgb = hex2rgb(theme[4]) const link = hex2rgb(theme[4])
const cRedRgb = hex2rgb(theme[5] || '#FF0000') const cRed = hex2rgb(theme[5] || '#FF0000')
const cGreenRgb = hex2rgb(theme[6] || '#00FF00') const cGreen = hex2rgb(theme[6] || '#00FF00')
const cBlueRgb = hex2rgb(theme[7] || '#0000FF') const cBlue = hex2rgb(theme[7] || '#0000FF')
const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00') const cOrange = hex2rgb(theme[8] || '#E3FF00')
data.colors = { data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
bg: bgRgb,
fg: fgRgb,
text: textRgb,
link: linkRgb,
cRed: cRedRgb,
cBlue: cBlueRgb,
cGreen: cGreenRgb,
cOrange: cOrangeRgb
}
} }
// This is a hack, this function is only called during initial load. // This is a hack, this function is only called during initial load.
@ -819,19 +461,3 @@ const setPreset = (val, commit) => {
} }
}) })
} }
export {
setStyle,
setPreset,
applyTheme,
getTextColor,
generateColors,
generateRadii,
generateShadows,
generateFonts,
generatePreset,
getThemes,
composePreset,
getCssShadow,
getCssShadowFilter
}

View file

@ -0,0 +1,315 @@
import { convert, brightness, contrastRatio } from 'chromatism'
import { alphaBlend, alphaBlendLayers, getTextColor, mixrgb } from '../color_convert/color_convert.js'
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',
panel: 'bg',
btn: 'bg',
btnPanel: 'panel',
btnTopBar: 'topBar',
input: 'bg',
inputPanel: 'panel',
inputTopBar: 'topBar',
alert: 'bg',
alertPanel: 'panel'
}
export const SLOT_INHERITANCE = {
bg: null,
fg: null,
text: null,
underlay: '#000000',
link: '--accent',
accent: '--link',
faint: '--text',
faintLink: '--link',
cBlue: '#0000ff',
cRed: '#FF0000',
cGreen: '#00FF00',
cOrange: '#E3FF00',
lightBg: {
depends: ['bg'],
color: (mod, bg) => brightness(5 * mod, bg).rgb
},
lightText: {
depends: ['text'],
color: (mod, text) => brightness(20 * mod, text).rgb
},
border: {
depends: 'fg',
color: (mod, fg) => brightness(2 * mod, fg).rgb
},
linkBg: {
depends: ['accent', 'bg'],
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg).rgb
},
icon: {
depends: ['bg', 'text'],
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: '--fg',
panelText: {
depends: ['fgText'],
layer: 'panel',
textColor: true
},
panelFaint: {
depends: ['fgText'],
layer: 'panel',
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'
},
// Buttons
btn: '--fg',
btnText: {
depends: ['fgText'],
layer: 'btn'
},
btnPanelText: {
depends: ['panelText'],
layer: 'btnPanel',
variant: 'btn',
textColor: true
},
btnTopBarText: {
depends: ['topBarText'],
layer: 'btnTopBar',
variant: 'btn',
textColor: true
},
// Input fields
input: '--fg',
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: '--cRed',
alertErrorText: {
depends: ['text', 'alertError'],
layer: 'alert',
variant: 'alertError',
textColor: true
},
alertErrorPanelText: {
depends: ['panelText', 'alertError'],
layer: 'alertPanel',
variant: 'alertError',
textColor: true
},
alertWarning: '--cOrange',
alertWarningText: {
depends: ['text', 'alertWarning'],
layer: 'alert',
variant: 'alertWarning',
textColor: true
},
alertWarningPanelText: {
depends: ['panelText', 'alertWarning'],
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, colors, opacity) => {
return getLayersArray(layer).map((currentLayer) => ([
currentLayer === layer
? colors[variant]
: colors[currentLayer],
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]
}
}
}
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
}
export const SLOT_ORDERED = topoSort(SLOT_INHERITANCE)
export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.reduce((acc, key) => {
const value = SLOT_INHERITANCE[key]
if (sourceColors[key]) {
return { ...acc, [key]: { ...sourceColors[key] } }
} else if (typeof value === 'string' && value.startsWith('#')) {
return { ...acc, [key]: convert(value).rgb }
} else {
const isObject = typeof value === 'object'
const defaultColorFunc = (mod, dep) => ({ ...dep })
const deps = getDependencies(key, SLOT_INHERITANCE)
const colorFunc = (isObject && value.color) || defaultColorFunc
if (value.textColor) {
const bg = alphaBlendLayers(
{ ...acc[deps[0]] },
getLayers(
value.layer,
value.variant || value.layer,
acc,
sourceOpacity
)
)
if (value.textColor === 'bw') {
return {
...acc,
[key]: contrastRatio(bg)
}
} else {
return {
...acc,
[key]: getTextColor(
bg,
{ ...acc[deps[0]] },
value.textColor === 'preserve'
)
}
}
} else {
console.log('BENIS', key, deps, deps.map((dep) => ({ ...acc[dep] })))
return {
...acc,
[key]: colorFunc(
mod,
...deps.map((dep) => ({ ...acc[dep] }))
)
}
}
}
}, {})

View file

@ -0,0 +1,79 @@
import { getLayersArray, topoSort } from 'src/services/style_setter/style_setter'
describe('getLayersArray', () => {
const fixture = {
layer1: null,
layer2: 'layer1',
layer3a: 'layer2',
layer3b: 'layer2'
}
it('should expand layers properly (3b)', () => {
const out = getLayersArray('layer3b', fixture)
expect(out).to.eql(['layer1', 'layer2', 'layer3b'])
})
it('should expand layers properly (3a)', () => {
const out = getLayersArray('layer3a', fixture)
expect(out).to.eql(['layer1', 'layer2', 'layer3a'])
})
it('should expand layers properly (2)', () => {
const out = getLayersArray('layer2', fixture)
expect(out).to.eql(['layer1', 'layer2'])
})
it('should expand layers properly (1)', () => {
const out = getLayersArray('layer1', fixture)
expect(out).to.eql(['layer1'])
})
})
describe('topoSort', () => {
const fixture1 = {
layerA: [],
layer1A: ['layerA'],
layer2A: ['layer1A'],
layerB: [],
layer1B: ['layerB'],
layer2B: ['layer1B'],
layer3AB: ['layer2B', 'layer2A']
}
// Same thing but messed up order
const fixture2 = {
layer1A: ['layerA'],
layer1B: ['layerB'],
layer2A: ['layer1A'],
layerB: [],
layer3AB: ['layer2B', 'layer2A'],
layer2B: ['layer1B'],
layerA: []
}
it('should make a topologically sorted array', () => {
const out = topoSort(fixture1, (node, inheritance) => inheritance[node])
// This basically checks all ordering that matters
expect(out.indexOf('layerA')).to.be.below(out.indexOf('layer1A'))
expect(out.indexOf('layer1A')).to.be.below(out.indexOf('layer2A'))
expect(out.indexOf('layerB')).to.be.below(out.indexOf('layer1B'))
expect(out.indexOf('layer1B')).to.be.below(out.indexOf('layer2B'))
expect(out.indexOf('layer2A')).to.be.below(out.indexOf('layer3AB'))
expect(out.indexOf('layer2B')).to.be.below(out.indexOf('layer3AB'))
})
it('order in object shouldn\'t matter', () => {
const out = topoSort(fixture2, (node, inheritance) => inheritance[node])
// This basically checks all ordering that matters
expect(out.indexOf('layerA')).to.be.below(out.indexOf('layer1A'))
expect(out.indexOf('layer1A')).to.be.below(out.indexOf('layer2A'))
expect(out.indexOf('layerB')).to.be.below(out.indexOf('layer1B'))
expect(out.indexOf('layer1B')).to.be.below(out.indexOf('layer2B'))
expect(out.indexOf('layer2A')).to.be.below(out.indexOf('layer3AB'))
expect(out.indexOf('layer2B')).to.be.below(out.indexOf('layer3AB'))
})
it('ignores cyclic dependencies', () => {
const out = topoSort({ a: 'b', b: 'a', c: 'a' }, (node, inheritance) => inheritance[node])
expect(out.indexOf('a')).to.be.below(out.indexOf('c'))
})
})