From f16ec75c7011fa7e6d5deb7763553b2c70d9a86e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 16 Jan 2020 20:53:05 +0200 Subject: [PATCH] opacity handling --- .../style_switcher/style_switcher.js | 10 +- src/services/color_convert/color_convert.js | 2 +- src/services/style_setter/style_setter.js | 70 +------ src/services/theme_data/theme_data.service.js | 193 +++++++++++++----- 4 files changed, 149 insertions(+), 126 deletions(-) diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 98c2cbc5..16be209a 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -15,7 +15,7 @@ import { import { CURRENT_VERSION, SLOT_INHERITANCE, - DEFAULT_OPACITY, + OPACITIES, getLayers } from '../../services/theme_data/theme_data.service.js' import ColorInput from '../color_input/color_input.vue' @@ -74,8 +74,8 @@ export default { .map(key => [key, '']) .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}), - ...Object.keys(DEFAULT_OPACITY) - .map(key => [key, undefined]) + ...Object.keys(OPACITIES) + .map(key => console.log(key) || [key, '']) .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}), shadowSelected: undefined, @@ -115,8 +115,8 @@ export default { .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) }, currentOpacity () { - return Object.keys(DEFAULT_OPACITY) - .map(key => [key, this[key + 'OpacityLocal']]) + return Object.keys(OPACITIES) + .map(key => console.log(key) || [key, this[key + 'OpacityLocal']]) .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) }, currentRadii () { diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js index c727a9fe..6b228a58 100644 --- a/src/services/color_convert/color_convert.js +++ b/src/services/color_convert/color_convert.js @@ -159,7 +159,7 @@ export const hex2rgb = (hex) => { * @returns {Object} result */ export const mixrgb = (a, b) => { - return Object.keys(a).reduce((acc, k) => { + return 'rgb'.split('').reduce((acc, k) => { acc[k] = (a[k] + b[k]) / 2 return acc }, {}) diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index c1a25101..9237a8dc 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,7 +1,7 @@ import { times } from 'lodash' import { convert } from 'chromatism' import { rgb2hex, hex2rgb, rgba2css, getCssColor } from '../color_convert/color_convert.js' -import { getColors, DEFAULT_OPACITY } from '../theme_data/theme_data.service.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 @@ -115,76 +115,12 @@ const getCssShadowFilter = (input) => { } export const generateColors = (themeData) => { - const rawOpacity = Object.assign({ ...DEFAULT_OPACITY }, Object.entries(themeData.opacity || {}).reduce((acc, [k, v]) => { - if (typeof v !== 'undefined') { - acc[k] = v - } - return acc - }, {})) - - const inputColors = themeData.colors || themeData - - 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]) => { - if (typeof v === 'object') { - acc[k] = v - } else { - let value = v - if (v === 'transparent') { - // TODO: hack to keep rest of the code from complaining - value = '#FF00FF' - } - if (!value || value.startsWith('--')) { - acc[k] = value - } else { - acc[k] = hex2rgb(value) - } - } - return acc - }, {}) + const sourceColors = themeData.colors || themeData const isLightOnDark = convert(sourceColors.bg).hsl.l < convert(sourceColors.text).hsl.l const mod = isLightOnDark ? 1 : -1 - const colors = getColors(sourceColors, opacity, mod) - - // Inheriting opacities - Object.entries(opacity).forEach(([ k, v ]) => { - if (typeof v === 'undefined') return - if (k === 'alert') { - colors.alertError.a = v - colors.alertWarning.a = v - return - } - if (k === 'faint') { - colors['faintLink'].a = v - colors['panelFaint'].a = v - colors['lightBgFaintText'].a = v - colors['lightBgFaintLink'].a = v - } - if (k === 'bg') { - colors['lightBg'].a = v - } - if (k === 'badge') { - colors['badgeNotification'].a = v - return - } - if (colors[k]) { - colors[k].a = v - } else { - console.error('Wrong key ' + k) - } - }) + const { colors, opacity } = getColors(sourceColors, themeData.opacity || {}, mod) const htmlColors = Object.entries(colors) .reduce((acc, [k, v]) => { diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js index a345d996..e76d70ed 100644 --- a/src/services/theme_data/theme_data.service.js +++ b/src/services/theme_data/theme_data.service.js @@ -26,24 +26,17 @@ export const LAYERS = { } export const DEFAULT_OPACITY = { - panel: 1, - btn: 1, - border: 1, - bg: 1, - badge: 1, - text: 1, alert: 0.5, input: 0.5, faint: 0.5, - underlay: 0.15, - poll: 1, - topBar: 1 + underlay: 0.15 } export const SLOT_INHERITANCE = { bg: { depends: [], - priority: 1 + priority: 1, + opacity: 'bg' }, fg: { depends: [], @@ -53,7 +46,10 @@ export const SLOT_INHERITANCE = { depends: [], priority: 1 }, - underlay: '#000000', + underlay: { + default: '#000000', + opacity: 'underlay' + }, link: { depends: ['accent'], priority: 1 @@ -62,8 +58,14 @@ export const SLOT_INHERITANCE = { depends: ['link'], priority: 1 }, - faint: '--text', - faintLink: '--link', + faint: { + depends: ['text'], + opacity: 'faint' + }, + faintLink: { + depends: ['link'], + opacity: 'faint' + }, cBlue: '#0000ff', cRed: '#FF0000', @@ -158,11 +160,13 @@ export const SLOT_INHERITANCE = { 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: { @@ -173,6 +177,7 @@ export const SLOT_INHERITANCE = { icon: { depends: ['bg', 'text'], + inheritsOpacity: false, color: (mod, bg, text) => mixrgb(bg, text) }, @@ -189,7 +194,10 @@ export const SLOT_INHERITANCE = { }, // Panel header - panel: '--fg', + panel: { + depends: ['fg'], + opacity: 'panel' + }, panelText: { depends: ['fgText'], layer: 'panel', @@ -198,6 +206,7 @@ export const SLOT_INHERITANCE = { panelFaint: { depends: ['fgText'], layer: 'panel', + opacity: 'faint', textColor: true }, panelLink: { @@ -233,7 +242,10 @@ export const SLOT_INHERITANCE = { }, // Buttons - btn: '--fg', + btn: { + depends: ['fg'], + opacity: 'btn' + }, btnText: { depends: ['fgText'], layer: 'btn', @@ -325,7 +337,10 @@ export const SLOT_INHERITANCE = { }, // Input fields - input: '--fg', + input: { + depends: ['fg'], + opacity: 'input' + }, inputText: { depends: ['text'], layer: 'input', @@ -344,7 +359,10 @@ export const SLOT_INHERITANCE = { textColor: true }, - alertError: '--cRed', + alertError: { + depends: ['cRed'], + opacity: 'alert' + }, alertErrorText: { depends: ['text'], layer: 'alert', @@ -358,7 +376,10 @@ export const SLOT_INHERITANCE = { textColor: true }, - alertWarning: '--cOrange', + alertWarning: { + depends: ['cOrange'], + opacity: 'alert' + }, alertWarningText: { depends: ['text'], layer: 'alert', @@ -465,78 +486,144 @@ export const topoSort = ( return output } +export const getOpacitySlot = ( + v, + inheritance = SLOT_INHERITANCE, + getDeps = getDependencies +) => { + if (v.opacity === null) return + if (v.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 (v.depends) { + return findInheritedOpacity(v) + } +} + 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 }), {}) ) -console.log(SLOT_ORDERED) +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 + } +}, {}) -export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.reduce((acc, key) => { +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 + } +}, {}) + +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 (typeof sourceColor === 'string' && sourceColor.startsWith('--')) { + if (targetColor === 'transparent') { + targetColor = { + // TODO: try to use alpha-blended background here + ...convert('#FF00FF').rgb, + a: 0 + } + } else if (typeof sourceColor === 'string' && sourceColor.startsWith('--')) { + // Color references other color const [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim()) const variableSlot = variable.substring(2) - targetColor = acc[variableSlot] || sourceColors[variableSlot] + targetColor = colors[variableSlot] || sourceColors[variableSlot] if (modifier) { - console.log(targetColor, acc, variableSlot) targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb } - console.log(targetColor, acc, variableSlot) + } else if (typeof sourceColor === 'string' && sourceColor.startsWith('#')) { + targetColor = convert(targetColor).rgb } - return { ...acc, [key]: { ...targetColor } } - } else if (typeof value === 'string' && value.startsWith('#')) { - return { ...acc, [key]: convert(value).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 { - const isObject = typeof value === 'object' + // 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( - { ...acc[deps[0]] }, + { ...colors[deps[0]] }, getLayers( value.layer, value.variant || value.layer, - acc, - sourceOpacity + colors, + opacity ) ) if (value.textColor === 'bw') { - return { - ...acc, - [key]: contrastRatio(bg).rgb - } + outputColor = contrastRatio(bg).rgb } else { - let color = { ...acc[deps[0]] } + 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) => ({ ...acc[dep] }))) + color = value.color(mod, ...deps.map((dep) => ({ ...colors[dep] }))) } - return { - ...acc, - [key]: getTextColor( - bg, - { ...color }, - value.textColor === 'preserve' - ) - } - } - } else { - return { - ...acc, - [key]: colorFunc( - mod, - ...deps.map((dep) => ({ ...acc[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 + } + return { + colors: { ...colors, [key]: outputColor }, + opacity: { ...opacity, [opacitySlot]: outputColor.a } + } +}, { colors: {}, opacity: {} })