opacity handling

This commit is contained in:
Henry Jameson 2020-01-16 20:53:05 +02:00
parent c3cd66335f
commit f16ec75c70
4 changed files with 149 additions and 126 deletions

View File

@ -15,7 +15,7 @@ import {
import { import {
CURRENT_VERSION, CURRENT_VERSION,
SLOT_INHERITANCE, SLOT_INHERITANCE,
DEFAULT_OPACITY, OPACITIES,
getLayers getLayers
} from '../../services/theme_data/theme_data.service.js' } from '../../services/theme_data/theme_data.service.js'
import ColorInput from '../color_input/color_input.vue' import ColorInput from '../color_input/color_input.vue'
@ -74,8 +74,8 @@ export default {
.map(key => [key, '']) .map(key => [key, ''])
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}), .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),
...Object.keys(DEFAULT_OPACITY) ...Object.keys(OPACITIES)
.map(key => [key, undefined]) .map(key => console.log(key) || [key, ''])
.reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}), .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),
shadowSelected: undefined, shadowSelected: undefined,
@ -115,8 +115,8 @@ export default {
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
}, },
currentOpacity () { currentOpacity () {
return Object.keys(DEFAULT_OPACITY) return Object.keys(OPACITIES)
.map(key => [key, this[key + 'OpacityLocal']]) .map(key => console.log(key) || [key, this[key + 'OpacityLocal']])
.reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
}, },
currentRadii () { currentRadii () {

View File

@ -159,7 +159,7 @@ export const hex2rgb = (hex) => {
* @returns {Object} result * @returns {Object} result
*/ */
export const mixrgb = (a, b) => { 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 acc[k] = (a[k] + b[k]) / 2
return acc return acc
}, {}) }, {})

View File

@ -1,7 +1,7 @@
import { times } from 'lodash' import { times } from 'lodash'
import { convert } from 'chromatism' import { convert } from 'chromatism'
import { rgb2hex, hex2rgb, rgba2css, getCssColor } from '../color_convert/color_convert.js' 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 // 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
@ -115,76 +115,12 @@ const getCssShadowFilter = (input) => {
} }
export const generateColors = (themeData) => { export const generateColors = (themeData) => {
const rawOpacity = Object.assign({ ...DEFAULT_OPACITY }, Object.entries(themeData.opacity || {}).reduce((acc, [k, v]) => { const sourceColors = themeData.colors || themeData
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 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 = getColors(sourceColors, opacity, mod) const { colors, opacity } = getColors(sourceColors, themeData.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 htmlColors = Object.entries(colors) const htmlColors = Object.entries(colors)
.reduce((acc, [k, v]) => { .reduce((acc, [k, v]) => {

View File

@ -26,24 +26,17 @@ export const LAYERS = {
} }
export const DEFAULT_OPACITY = { export const DEFAULT_OPACITY = {
panel: 1,
btn: 1,
border: 1,
bg: 1,
badge: 1,
text: 1,
alert: 0.5, alert: 0.5,
input: 0.5, input: 0.5,
faint: 0.5, faint: 0.5,
underlay: 0.15, underlay: 0.15
poll: 1,
topBar: 1
} }
export const SLOT_INHERITANCE = { export const SLOT_INHERITANCE = {
bg: { bg: {
depends: [], depends: [],
priority: 1 priority: 1,
opacity: 'bg'
}, },
fg: { fg: {
depends: [], depends: [],
@ -53,7 +46,10 @@ export const SLOT_INHERITANCE = {
depends: [], depends: [],
priority: 1 priority: 1
}, },
underlay: '#000000', underlay: {
default: '#000000',
opacity: 'underlay'
},
link: { link: {
depends: ['accent'], depends: ['accent'],
priority: 1 priority: 1
@ -62,8 +58,14 @@ export const SLOT_INHERITANCE = {
depends: ['link'], depends: ['link'],
priority: 1 priority: 1
}, },
faint: '--text', faint: {
faintLink: '--link', depends: ['text'],
opacity: 'faint'
},
faintLink: {
depends: ['link'],
opacity: 'faint'
},
cBlue: '#0000ff', cBlue: '#0000ff',
cRed: '#FF0000', cRed: '#FF0000',
@ -158,11 +160,13 @@ export const SLOT_INHERITANCE = {
border: { border: {
depends: ['fg'], depends: ['fg'],
opacity: 'border',
color: (mod, fg) => brightness(2 * mod, fg).rgb color: (mod, fg) => brightness(2 * mod, fg).rgb
}, },
poll: { poll: {
depends: ['accent', 'bg'], depends: ['accent', 'bg'],
copacity: 'poll',
color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg) color: (mod, accent, bg) => alphaBlend(accent, 0.4, bg)
}, },
pollText: { pollText: {
@ -173,6 +177,7 @@ export const SLOT_INHERITANCE = {
icon: { icon: {
depends: ['bg', 'text'], depends: ['bg', 'text'],
inheritsOpacity: false,
color: (mod, bg, text) => mixrgb(bg, text) color: (mod, bg, text) => mixrgb(bg, text)
}, },
@ -189,7 +194,10 @@ export const SLOT_INHERITANCE = {
}, },
// Panel header // Panel header
panel: '--fg', panel: {
depends: ['fg'],
opacity: 'panel'
},
panelText: { panelText: {
depends: ['fgText'], depends: ['fgText'],
layer: 'panel', layer: 'panel',
@ -198,6 +206,7 @@ export const SLOT_INHERITANCE = {
panelFaint: { panelFaint: {
depends: ['fgText'], depends: ['fgText'],
layer: 'panel', layer: 'panel',
opacity: 'faint',
textColor: true textColor: true
}, },
panelLink: { panelLink: {
@ -233,7 +242,10 @@ export const SLOT_INHERITANCE = {
}, },
// Buttons // Buttons
btn: '--fg', btn: {
depends: ['fg'],
opacity: 'btn'
},
btnText: { btnText: {
depends: ['fgText'], depends: ['fgText'],
layer: 'btn', layer: 'btn',
@ -325,7 +337,10 @@ export const SLOT_INHERITANCE = {
}, },
// Input fields // Input fields
input: '--fg', input: {
depends: ['fg'],
opacity: 'input'
},
inputText: { inputText: {
depends: ['text'], depends: ['text'],
layer: 'input', layer: 'input',
@ -344,7 +359,10 @@ export const SLOT_INHERITANCE = {
textColor: true textColor: true
}, },
alertError: '--cRed', alertError: {
depends: ['cRed'],
opacity: 'alert'
},
alertErrorText: { alertErrorText: {
depends: ['text'], depends: ['text'],
layer: 'alert', layer: 'alert',
@ -358,7 +376,10 @@ export const SLOT_INHERITANCE = {
textColor: true textColor: true
}, },
alertWarning: '--cOrange', alertWarning: {
depends: ['cOrange'],
opacity: 'alert'
},
alertWarningText: { alertWarningText: {
depends: ['text'], depends: ['text'],
layer: 'alert', layer: 'alert',
@ -465,78 +486,144 @@ export const topoSort = (
return output 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( export const SLOT_ORDERED = topoSort(
Object.entries(SLOT_INHERITANCE) Object.entries(SLOT_INHERITANCE)
.sort(([aK, aV], [bK, bV]) => ((aV && aV.priority) || 0) - ((bV && bV.priority) || 0)) .sort(([aK, aV], [bK, bV]) => ((aV && aV.priority) || 0) - ((bV && bV.priority) || 0))
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}) .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 value = SLOT_INHERITANCE[key]
const isObject = typeof value === 'object'
const isString = typeof value === 'string'
const sourceColor = sourceColors[key] const sourceColor = sourceColors[key]
let outputColor = null
if (sourceColor) { if (sourceColor) {
// Color is defined in source color
let targetColor = sourceColor 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 [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim())
const variableSlot = variable.substring(2) const variableSlot = variable.substring(2)
targetColor = acc[variableSlot] || sourceColors[variableSlot] targetColor = colors[variableSlot] || sourceColors[variableSlot]
if (modifier) { if (modifier) {
console.log(targetColor, acc, variableSlot)
targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb 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 } } outputColor = { ...targetColor }
} else if (typeof value === 'string' && value.startsWith('#')) { } else if (isString && value.startsWith('#')) {
return { ...acc, [key]: convert(value).rgb } // 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 { } else {
const isObject = typeof value === 'object' // calculate color
const defaultColorFunc = (mod, dep) => ({ ...dep }) const defaultColorFunc = (mod, dep) => ({ ...dep })
const deps = getDependencies(key, SLOT_INHERITANCE) const deps = getDependencies(key, SLOT_INHERITANCE)
const colorFunc = (isObject && value.color) || defaultColorFunc const colorFunc = (isObject && value.color) || defaultColorFunc
if (value.textColor) { if (value.textColor) {
// textColor case
const bg = alphaBlendLayers( const bg = alphaBlendLayers(
{ ...acc[deps[0]] }, { ...colors[deps[0]] },
getLayers( getLayers(
value.layer, value.layer,
value.variant || value.layer, value.variant || value.layer,
acc, colors,
sourceOpacity opacity
) )
) )
if (value.textColor === 'bw') { if (value.textColor === 'bw') {
return { outputColor = contrastRatio(bg).rgb
...acc,
[key]: contrastRatio(bg).rgb
}
} else { } else {
let color = { ...acc[deps[0]] } let color = { ...colors[deps[0]] }
if (value.color) { if (value.color) {
const isLightOnDark = convert(bg).hsl.l < convert(color).hsl.l const isLightOnDark = convert(bg).hsl.l < convert(color).hsl.l
const mod = isLightOnDark ? 1 : -1 const mod = isLightOnDark ? 1 : -1
color = value.color(mod, ...deps.map((dep) => ({ ...acc[dep] }))) color = value.color(mod, ...deps.map((dep) => ({ ...colors[dep] })))
} }
return { outputColor = getTextColor(
...acc, bg,
[key]: getTextColor( { ...color },
bg, value.textColor === 'preserve'
{ ...color },
value.textColor === 'preserve'
)
}
}
} else {
return {
...acc,
[key]: colorFunc(
mod,
...deps.map((dep) => ({ ...acc[dep] }))
) )
} }
} 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: {} })