From 622c9d388e0df1f53c544c34b7def2bb6fe498cd Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 12 Jan 2020 03:44:06 +0200 Subject: [PATCH] Refactoring, forgotten files --- src/components/color_input/color_input.scss | 65 +++ src/services/color_convert/color_convert.js | 101 +++- src/services/style_setter/style_setter.js | 438 ++---------------- src/services/theme_data/theme_data.service.js | 315 +++++++++++++ .../style_setter/style_setter.spec.js | 79 ++++ 5 files changed, 577 insertions(+), 421 deletions(-) create mode 100644 src/components/color_input/color_input.scss create mode 100644 src/services/theme_data/theme_data.service.js create mode 100644 test/unit/specs/services/style_setter/style_setter.spec.js diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss new file mode 100644 index 00000000..92bf87c5 --- /dev/null +++ b/src/components/color_input/color_input.scss @@ -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; + } + +} diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js index 32b4d50e..464f6495 100644 --- a/src/services/color_convert/color_convert.js +++ b/src/services/color_convert/color_convert.js @@ -1,9 +1,16 @@ -import { map } from 'lodash' +import { invertLightness, convert, contrastRatio } from 'chromatism' // useful for visualizing color when debugging 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') { return undefined } @@ -14,7 +21,7 @@ const rgb2hex = (r, g, b) => { if (typeof r === 'object') { ({ 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 = val < 0 ? 0 : val val = val > 255 ? 255 : val @@ -82,6 +89,7 @@ const getContrastRatio = (a, b) => { return (l1 + 0.05) / (l2 + 0.05) } + /** * 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 * @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 return 'rgb'.split('').reduce((acc, c) => { // 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) }, bedrock) -const invert = (rgb) => { +export const invert = (rgb) => { return 'rgb'.split('').reduce((acc, c) => { acc[c] = 255 - rgb[c] 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) return result ? { r: parseInt(result[1], 16), @@ -137,18 +151,75 @@ const hex2rgb = (hex) => { } : 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) => { acc[k] = (a[k] + b[k]) / 2 return acc }, {}) } - -export { - rgb2hex, - hex2rgb, - mixrgb, - invert, - getContrastRatio, - alphaBlend +/** + * Converts rgb object into a CSS rgba() color + * + * @param {Object} color - rgb + * @returns {String} CSS rgba() color + */ +export const rgba2css = function (rgba) { + 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 }) } diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 992b3194..46b08628 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,275 +1,13 @@ import { times } from 'lodash' -import { brightness, invertLightness, convert, contrastRatio } from 'chromatism' -import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend, alphaBlendLayers } 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) +import { convert } from 'chromatism' +import { rgb2hex, hex2rgb, rgba2css, getCssColor } from '../color_convert/color_convert.js' +import { getColors } from '../theme_data/theme_data.service.js' // 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 as well as set their own colors in the future. -const setStyle = (href, commit) => { +export const setStyle = (href, commit) => { /*** What's going on here? 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) } -const rgb2rgba = function (rgba) { - 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) => { +export const applyTheme = (input, commit) => { const { rules, theme } = generatePreset(input) const head = document.head const body = document.body @@ -399,22 +114,6 @@ const getCssShadowFilter = (input) => { .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 rawOpacity = Object.assign({ panel: 1, @@ -435,14 +134,16 @@ const generateColors = (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 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 mod = isLightOnDark ? 1 : -1 - const colors = 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, - 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] })) - ) - } - } - } - }, {}) + const colors = getColors(sourceColors, opacity, mod) // Inheriting opacities Object.entries(opacity).forEach(([ k, v ]) => { @@ -541,7 +194,7 @@ const generateColors = (themeData) => { .reduce((acc, [k, v]) => { if (!v) return acc 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 }, { complete: {}, solid: {} }) return { @@ -740,14 +393,12 @@ const composePreset = (colors, radii, shadows, fonts) => { } } -const generatePreset = (input) => { - const shadows = generateShadows(input) - const colors = generateColors(input) - const radii = generateRadii(input) - const fonts = generateFonts(input) - - return composePreset(colors, radii, shadows, fonts) -} +const generatePreset = (input) => composePreset( + generateColors(input), + generateRadii(input), + generateShadows(input), + generateFonts(input) +) const getThemes = () => { 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) => { const theme = themes[val] ? themes[val] : themes['pleroma-dark'] const isV1 = Array.isArray(theme) const data = isV1 ? {} : theme.theme if (isV1) { - const bgRgb = hex2rgb(theme[1]) - const fgRgb = hex2rgb(theme[2]) - const textRgb = hex2rgb(theme[3]) - const linkRgb = hex2rgb(theme[4]) + const bg = hex2rgb(theme[1]) + const fg = hex2rgb(theme[2]) + const text = hex2rgb(theme[3]) + const link = hex2rgb(theme[4]) - const cRedRgb = hex2rgb(theme[5] || '#FF0000') - const cGreenRgb = hex2rgb(theme[6] || '#00FF00') - const cBlueRgb = hex2rgb(theme[7] || '#0000FF') - const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00') + const cRed = hex2rgb(theme[5] || '#FF0000') + const cGreen = hex2rgb(theme[6] || '#00FF00') + const cBlue = hex2rgb(theme[7] || '#0000FF') + const cOrange = hex2rgb(theme[8] || '#E3FF00') - data.colors = { - bg: bgRgb, - fg: fgRgb, - text: textRgb, - link: linkRgb, - cRed: cRedRgb, - cBlue: cBlueRgb, - cGreen: cGreenRgb, - cOrange: cOrangeRgb - } + data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange } } // 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 -} diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js new file mode 100644 index 00000000..c9c80727 --- /dev/null +++ b/src/services/theme_data/theme_data.service.js @@ -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] })) + ) + } + } + } +}, {}) diff --git a/test/unit/specs/services/style_setter/style_setter.spec.js b/test/unit/specs/services/style_setter/style_setter.spec.js new file mode 100644 index 00000000..7f789124 --- /dev/null +++ b/test/unit/specs/services/style_setter/style_setter.spec.js @@ -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')) + }) +})