From e5a34870f0f7154712783fb6d9c20edf4c06ad35 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 28 Dec 2019 15:55:42 +0200 Subject: Accent works --- src/services/style_setter/style_setter.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index eaa495c4..1e4bc59e 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -160,7 +160,13 @@ const generateColors = (input) => { } return acc }, {})) - const col = Object.entries(input.colors || input).reduce((acc, [k, v]) => { + + const inputColors = input.colors || input + + const compat = input.v3compat || {} + const compatColors = compat.colors || {} + + const col = Object.entries({ ...inputColors, ...compatColors }).reduce((acc, [k, v]) => { if (typeof v === 'object') { acc[k] = v } else { @@ -174,7 +180,10 @@ const generateColors = (input) => { colors.text = col.text colors.lightText = brightness(20 * mod, colors.text).rgb - colors.link = col.link + + colors.accent = col.accent || col.link + colors.link = col.link || col.accent + colors.faint = col.faint || Object.assign({}, col.text) colors.bg = col.bg -- cgit v1.2.3-70-g09d2 From 1a78461443e4f85461283c1921694a5fb28ea39f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 28 Dec 2019 17:02:34 +0200 Subject: fixed import-export --- src/components/export_import/export_import.vue | 2 +- src/components/style_switcher/style_switcher.js | 34 +++++++++++++++++++----- src/components/style_switcher/style_switcher.vue | 4 +-- src/services/style_setter/style_setter.js | 27 +++++++++++++++++-- 4 files changed, 56 insertions(+), 11 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/components/export_import/export_import.vue b/src/components/export_import/export_import.vue index 20c6f569..ae00487f 100644 --- a/src/components/export_import/export_import.vue +++ b/src/components/export_import/export_import.vue @@ -42,7 +42,7 @@ export default { }, methods: { exportData () { - const stringified = JSON.stringify(this.exportObject) // Pretty-print and indent with 2 spaces + const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces // Create an invisible link with a data url and simulate a click const e = document.createElement('a') diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 1e512407..b450dc8e 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -1,6 +1,7 @@ import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js' import { set, delete as del } from 'vue' -import { generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js' +import { merge } from 'lodash' +import { generateCompat, generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js' import ColorInput from '../color_input/color_input.vue' import RangeInput from '../range_input/range_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' @@ -123,6 +124,15 @@ export default { selectedVersion () { return Array.isArray(this.selected) ? 1 : 2 }, + currentCompat () { + return generateCompat({ + shadows: this.shadowsLocal, + fonts: this.fontsLocal, + opacity: this.currentOpacity, + colors: this.currentColors, + radii: this.currentRadii + }) + }, currentColors () { return { bg: this.bgColorLocal, @@ -332,7 +342,7 @@ export default { return { // To separate from other random JSON files and possible future theme formats - _pleroma_theme_version: 2, theme + _pleroma_theme_version: 2, theme: merge(theme, this.currentCompat) } } }, @@ -364,7 +374,7 @@ export default { onImport (parsed) { if (parsed._pleroma_theme_version === 1) { this.normalizeLocalState(parsed, 1) - } else if (parsed._pleroma_theme_version === 2) { + } else if (parsed._pleroma_theme_version >= 2) { this.normalizeLocalState(parsed.theme, 2) } }, @@ -414,6 +424,7 @@ export default { /** * This applies stored theme data onto form. Supports three versions of data: + * v3 (version = 3) - same as 2 but with some incompatible changes * v2 (version = 2) - newer version of themes. * v1 (version = 1) - older version of themes (import from file) * v1l (version = l1) - older version of theme (load from local storage) @@ -421,12 +432,21 @@ export default { * @param {Object} input - input data * @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type */ - normalizeLocalState (input, version = 0) { - const colors = input.colors || input + normalizeLocalState (originalInput, version = 0) { + let input + if (typeof originalInput.v3compat !== undefined) { + version = 3 + input = merge(originalInput, originalInput.v3compat) + } else { + input = originalInput + } + + const compat = input.v3compat const radii = input.radii || input const opacity = input.opacity const shadows = input.shadows || {} const fonts = input.fonts || {} + const colors = input.colors || input if (version === 0) { if (input.version) version = input.version @@ -530,6 +550,7 @@ export default { currentColors () { try { this.previewColors = generateColors({ + v3compat: this.currentCompat, opacity: this.currentOpacity, colors: this.currentColors }) @@ -542,8 +563,9 @@ export default { currentOpacity () { try { this.previewColors = generateColors({ + v3compat: this.currentCompat, opacity: this.currentOpacity, - colors: this.currentColors + colors: this.currentColors, }) } catch (e) { console.warn(e) diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index 8bbdc7b7..2ecd275a 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -2,7 +2,7 @@
-
- +
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 1e4bc59e..19a06587 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -148,6 +148,26 @@ const getCssColor = (input, a) => { return rgb2rgba({ ...rgb, a }) } +// Generates a "patch" for theme to make it compatible with v2 +export const generateCompat = (input) => { + const { colors } = input + const v3compat = { + colors: {} + } + const v2colorsPatch = {} + + // # Link became optional in v3 + if (typeof colors.link === 'undefined') { + v2colorsPatch.link = colors.accent + v3compat.colors.link = null + } + + return { + v3compat, + colors: v2colorsPatch + } +} + const generateColors = (input) => { const colors = {} const opacity = Object.assign({ @@ -164,7 +184,10 @@ const generateColors = (input) => { const inputColors = input.colors || input const compat = input.v3compat || {} - const compatColors = compat.colors || {} + const compatColors = Object.entries(compat.colors || {}).reduce((acc, [key, value]) => { + const newVal = value === null ? undefined : value + return { ...acc, [key]: newVal } + }, {}) const col = Object.entries({ ...inputColors, ...compatColors }).reduce((acc, [k, v]) => { if (typeof v === 'object') { @@ -210,7 +233,7 @@ const generateColors = (input) => { colors.topBarText = col.topBarText || getTextColor(colors.topBar, colors.fgText) colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink) - colors.faintLink = col.faintLink || Object.assign({}, col.link) + colors.faintLink = col.faintLink || Object.assign({}, col.link || col.accent) colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg) colors.icon = mixrgb(colors.bg, colors.text) -- cgit v1.2.3-70-g09d2 From 6e11924c27288e590de8f5b675fb53704581bcc8 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 28 Dec 2019 18:49:35 +0200 Subject: underlay customization, updated contrast calculations to account for alpha blending --- src/App.scss | 1 + src/components/style_switcher/style_switcher.js | 47 ++++++++++++++---------- src/components/style_switcher/style_switcher.vue | 14 +++++++ src/services/color_convert/color_convert.js | 23 ++++++++++++ src/services/style_setter/style_setter.js | 37 +++++++++++++------ 5 files changed, 91 insertions(+), 31 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/App.scss b/src/App.scss index 3b03a761..b6d4943a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -32,6 +32,7 @@ h4 { min-height: 100vh; max-width: 980px; background-color: rgba(0,0,0,0.15); + background-color: var(--underlay, rgba(0,0,0,0.15)); align-content: flex-start; } diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index b450dc8e..602a635e 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -1,4 +1,4 @@ -import { rgb2hex, hex2rgb, getContrastRatio, alphaBlend } from '../../services/color_convert/color_convert.js' +import { rgb2hex, hex2rgb, getContrastRatio, getContrastRatioLayers, alphaBlend } from '../../services/color_convert/color_convert.js' import { set, delete as del } from 'vue' import { merge } from 'lodash' import { generateCompat, generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js' @@ -53,6 +53,9 @@ export default { bgColorLocal: '', bgOpacityLocal: undefined, + underlayColorLocal: '', + underlayOpacityLocal: undefined, + fgColorLocal: '', fgTextColorLocal: undefined, fgLinkColorLocal: undefined, @@ -145,6 +148,8 @@ export default { accent: this.accentColorLocal, + underlay: this.underlayColorLocal, + panel: this.panelColorLocal, panelText: this.panelTextColorLocal, panelLink: this.panelLinkColorLocal, @@ -182,7 +187,8 @@ export default { panel: this.panelOpacityLocal, topBar: this.topBarOpacityLocal, border: this.borderOpacityLocal, - faint: this.faintOpacityLocal + faint: this.faintOpacityLocal, + underlay: this.underlayOpacityLocal } }, currentRadii () { @@ -240,6 +246,7 @@ export default { const bgs = { bg: hex2rgb(colors.bg), + underlay: hex2rgb(colors.underlay), btn: hex2rgb(colors.btn), panel: hex2rgb(colors.panel), topBar: hex2rgb(colors.topBar), @@ -249,29 +256,31 @@ export default { badgeNotification: hex2rgb(colors.badgeNotification) } - /* This is a bit confusing because "bottom layer" used is text color - * This is done to get worst case scenario when background below transparent - * layer matches text color, making it harder to read the lower alpha is. - */ - const ratios = { - bgText: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.text), fgs.text), - bgLink: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.link), fgs.link), - bgRed: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.red), fgs.red), - bgGreen: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.green), fgs.green), - bgBlue: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.blue), fgs.blue), - bgOrange: getContrastRatio(alphaBlend(bgs.bg, opacity.bg, fgs.orange), fgs.orange), + const bg = [bgs.bg, opacity.bg] + const underlay = [bgs.underlay, opacity.underlay] + const panel = [underlay, bg] + + const ratios = { + bgText: getContrastRatioLayers(fgs.text, panel, fgs.text), + bgLink: getContrastRatioLayers(fgs.link, panel, fgs.link), + bgRed: getContrastRatioLayers(fgs.red, panel, fgs.red), + bgGreen: getContrastRatioLayers(fgs.green, panel, fgs.green), + bgBlue: getContrastRatioLayers(fgs.blue, panel, fgs.blue), + bgOrange: getContrastRatioLayers(fgs.orange, panel, fgs.orange), + + // TODO what's this? tintText: getContrastRatio(alphaBlend(bgs.bg, 0.5, fgs.panelText), fgs.text), - panelText: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelText), fgs.panelText), - panelLink: getContrastRatio(alphaBlend(bgs.panel, opacity.panel, fgs.panelLink), fgs.panelLink), + panelText: getContrastRatioLayers(fgs.text, [...panel, [bgs.panel, opacity.panel]], fgs.panelText), + panelLink: getContrastRatioLayers(fgs.link, [...panel, [bgs.panel, opacity.panel]], fgs.panelLink), - btnText: getContrastRatio(alphaBlend(bgs.btn, opacity.btn, fgs.btnText), fgs.btnText), + btnText: getContrastRatioLayers(fgs.text, [...panel, [bgs.btn, opacity.btn]], fgs.btnText), - inputText: getContrastRatio(alphaBlend(bgs.input, opacity.input, fgs.inputText), fgs.inputText), + inputText: getContrastRatioLayers(fgs.text, [...panel, [bgs.input, opacity.input]], fgs.inputText), - topBarText: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarText), fgs.topBarText), - topBarLink: getContrastRatio(alphaBlend(bgs.topBar, opacity.topBar, fgs.topBarLink), fgs.topBarLink) + topBarText: getContrastRatioLayers(fgs.text, [...panel, [bgs.topBar, opacity.topBar]], fgs.topBarText), + topBarLink: getContrastRatioLayers(fgs.link, [...panel, [bgs.topBar, opacity.topBar]], fgs.topBarLink) } return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {}) diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index 2ecd275a..38ca2017 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -366,6 +366,20 @@ :fallback="previewTheme.opacity.faint || 0.5" />
+
+

{{ $t('settings.style.advanced_colors.underlay') }}

+ + +
console.log('%c##########', 'background: ' + color + '; color: ' + color) + const rgb2hex = (r, g, b) => { if (r === null || typeof r === 'undefined') { return undefined @@ -78,6 +81,16 @@ const getContrastRatio = (a, b) => { return (l1 + 0.05) / (l2 + 0.05) } +/** + * Same as `getContrastRatio` but for multiple layers in-between + * + * @param {Object} text - text color (topmost layer) + * @param {[Object, Number]} layers[] - layers between text and bedrock + * @param {Object} bedrock - layer at the very bottom + */ +export const getContrastRatioLayers = (text, layers, bedrock) => { + return getContrastRatio(alphaBlendLayers(bedrock, layers), text) +} /** * This performs alpha blending between solid background and semi-transparent foreground @@ -97,6 +110,16 @@ const alphaBlend = (fg, fga, bg) => { }, {}) } +/** + * Same as `alphaBlend` but for multiple layers in-between + * + * @param {Object} bedrock - layer at the very bottom + * @param {[Object, Number]} layers[] - layers between text and bedrock + */ +export const alphaBlendLayers = (bedrock, layers) => layers.reduce((acc, [color, opacity]) => { + return alphaBlend(color, opacity, acc) +}, bedrock) + const invert = (rgb) => { return 'rgb'.split('').reduce((acc, c) => { acc[c] = 255 - rgb[c] diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 19a06587..8740fc55 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,6 +1,6 @@ import { times } from 'lodash' import { brightness, invertLightness, convert, contrastRatio } from 'chromatism' -import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend } from '../color_convert/color_convert.js' +import { rgb2hex, hex2rgb, mixrgb, getContrastRatio, alphaBlend, alphaBlendLayers } from '../color_convert/color_convert.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 @@ -64,8 +64,10 @@ const getTextColor = function (bg, text, preserve) { 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 @@ -173,7 +175,8 @@ const generateColors = (input) => { const opacity = Object.assign({ alert: 0.5, input: 0.5, - faint: 0.5 + faint: 0.5, + underlay: 0.15 }, Object.entries(input.opacity || {}).reduce((acc, [k, v]) => { if (typeof v !== 'undefined') { acc[k] = v @@ -210,28 +213,37 @@ const generateColors = (input) => { colors.faint = col.faint || Object.assign({}, col.text) colors.bg = col.bg - colors.lightBg = col.lightBg || brightness(5, colors.bg).rgb + colors.lightBg = col.lightBg || brightness(5 * mod, colors.bg).rgb + + const underlay = [col.underlay, opacity.underlay] + const fg = [col.fg, opacity.fg] + const bg = [col.bg, opacity.bg] colors.fg = col.fg - colors.fgText = col.fgText || getTextColor(colors.fg, colors.text) - colors.fgLink = col.fgLink || getTextColor(colors.fg, colors.link, true) + colors.fgText = col.fgText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, fg]), colors.text) + colors.fgLink = col.fgLink || getTextColor(alphaBlendLayers(colors.link, [underlay, bg, fg]), colors.link, true) + colors.underlay = col.underlay || hex2rgb('#000000') colors.border = col.border || brightness(2 * mod, colors.fg).rgb colors.btn = col.btn || Object.assign({}, col.fg) - colors.btnText = col.btnText || getTextColor(colors.btn, colors.fgText) + const btn = [colors.btn, opacity.btn || 1] + colors.btnText = col.btnText || getTextColor(alphaBlendLayers(colors.fgText, [underlay, bg, fg, btn]), colors.fgText) colors.input = col.input || Object.assign({}, col.fg) - colors.inputText = col.inputText || getTextColor(colors.input, colors.lightText) + const inputCol = [colors.input, opacity.input] + colors.inputText = col.inputText || getTextColor(alphaBlendLayers(colors.lightText, [underlay, bg, fg, inputCol]), colors.lightText) colors.panel = col.panel || Object.assign({}, col.fg) - colors.panelText = col.panelText || getTextColor(colors.panel, colors.fgText) - colors.panelLink = col.panelLink || getTextColor(colors.panel, colors.fgLink) - colors.panelFaint = col.panelFaint || getTextColor(colors.panel, colors.faint) + const panel = [colors.panel, opacity.panel] + colors.panelText = col.panelText || getTextColor(alphaBlendLayers(colors.fgText, [underlay, bg, panel]), colors.fgText) + colors.panelLink = col.panelLink || getTextColor(alphaBlendLayers(colors.fgLink, [underlay, bg, panel]), colors.fgLink) + colors.panelFaint = col.panelFaint || getTextColor(alphaBlendLayers(colors.faint, [underlay, bg, panel]), colors.faint) colors.topBar = col.topBar || Object.assign({}, col.fg) - colors.topBarText = col.topBarText || getTextColor(colors.topBar, colors.fgText) - colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink) + const topBar = [colors.topBar, opacity.topBar] + colors.topBarText = col.topBarText || getTextColor(alphaBlendLayers(colors.fgText, [topBar]), colors.fgText) + colors.topBarLink = col.topBarLink || getTextColor(alphaBlendLayers(colors.fgLink, [topBar]), colors.fgLink) colors.faintLink = col.faintLink || Object.assign({}, col.link || col.accent) colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg) @@ -255,6 +267,7 @@ const generateColors = (input) => { colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb Object.entries(opacity).forEach(([ k, v ]) => { + console.log(k) if (typeof v === 'undefined') return if (k === 'alert') { colors.alertError.a = v -- cgit v1.2.3-70-g09d2 From 332d31dc02b83d6ca06837fdfb4f663050d9effd Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 30 Dec 2019 00:45:48 +0200 Subject: support for "transparent" color keyword --- src/components/style_switcher/style_switcher.js | 60 ++++++++++++++----------- src/services/color_convert/color_convert.js | 3 +- src/services/style_setter/style_setter.js | 50 ++++++++++++++------- static/themes/breezy-dark.json | 8 +++- static/themes/breezy-light.json | 5 +++ 5 files changed, 80 insertions(+), 46 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 602a635e..9fe1bf59 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -25,6 +25,14 @@ const v1OnlyNames = [ 'cOrange' ].map(_ => _ + 'ColorLocal') +const colorConvert = (color) => { + if (color === 'transparent') { + return color + } else { + return hex2rgb(color) + } +} + export default { data () { return { @@ -228,36 +236,36 @@ export default { // fgsfds :DDDD const fgs = { - text: hex2rgb(colors.text), - panelText: hex2rgb(colors.panelText), - panelLink: hex2rgb(colors.panelLink), - btnText: hex2rgb(colors.btnText), - topBarText: hex2rgb(colors.topBarText), - inputText: hex2rgb(colors.inputText), - - link: hex2rgb(colors.link), - topBarLink: hex2rgb(colors.topBarLink), - - red: hex2rgb(colors.cRed), - green: hex2rgb(colors.cGreen), - blue: hex2rgb(colors.cBlue), - orange: hex2rgb(colors.cOrange) + text: colorConvert(colors.text), + panelText: colorConvert(colors.panelText), + panelLink: colorConvert(colors.panelLink), + btnText: colorConvert(colors.btnText), + topBarText: colorConvert(colors.topBarText), + inputText: colorConvert(colors.inputText), + + link: colorConvert(colors.link), + topBarLink: colorConvert(colors.topBarLink), + + red: colorConvert(colors.cRed), + green: colorConvert(colors.cGreen), + blue: colorConvert(colors.cBlue), + orange: colorConvert(colors.cOrange) } const bgs = { - bg: hex2rgb(colors.bg), - underlay: hex2rgb(colors.underlay), - btn: hex2rgb(colors.btn), - panel: hex2rgb(colors.panel), - topBar: hex2rgb(colors.topBar), - input: hex2rgb(colors.input), - alertError: hex2rgb(colors.alertError), - alertWarning: hex2rgb(colors.alertWarning), - badgeNotification: hex2rgb(colors.badgeNotification) + bg: colorConvert(colors.bg), + underlay: colorConvert(colors.underlay), + btn: colorConvert(colors.btn), + panel: colorConvert(colors.panel), + topBar: colorConvert(colors.topBar), + input: colorConvert(colors.input), + alertError: colorConvert(colors.alertError), + alertWarning: colorConvert(colors.alertWarning), + badgeNotification: colorConvert(colors.badgeNotification) } const bg = [bgs.bg, opacity.bg] - const underlay = [bgs.underlay, opacity.underlay] + const underlay = [bgs.underlay || colorConvert('#000000'), opacity.underlay] const panel = [underlay, bg] @@ -443,7 +451,7 @@ export default { */ normalizeLocalState (originalInput, version = 0) { let input - if (typeof originalInput.v3compat !== undefined) { + if (typeof originalInput.v3compat !== 'undefined') { version = 3 input = merge(originalInput, originalInput.v3compat) } else { @@ -574,7 +582,7 @@ export default { this.previewColors = generateColors({ v3compat: this.currentCompat, opacity: this.currentOpacity, - colors: this.currentColors, + colors: this.currentColors }) } catch (e) { console.warn(e) diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js index 757a36bd..32b4d50e 100644 --- a/src/services/color_convert/color_convert.js +++ b/src/services/color_convert/color_convert.js @@ -7,7 +7,8 @@ const rgb2hex = (r, g, b) => { if (r === null || typeof r === 'undefined') { return undefined } - if (r[0] === '#') { + // TODO: clean up this mess + if (r[0] === '#' || r === 'transparent') { return r } if (typeof r === 'object') { diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 8740fc55..df22f94f 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -170,23 +170,32 @@ export const generateCompat = (input) => { } } -const generateColors = (input) => { +const generateColors = (themeData) => { const colors = {} - const opacity = Object.assign({ + const rawOpacity = Object.assign({ alert: 0.5, input: 0.5, faint: 0.5, underlay: 0.15 - }, Object.entries(input.opacity || {}).reduce((acc, [k, v]) => { + }, Object.entries(themeData.opacity || {}).reduce((acc, [k, v]) => { if (typeof v !== 'undefined') { acc[k] = v } return acc }, {})) - const inputColors = input.colors || input + const inputColors = themeData.colors || themeData - const compat = input.v3compat || {} + const transparentsOpacity = Object.entries(inputColors).reduce((acc, [k, v]) => { + if (v === 'transparent') { + acc[k] = 0 + } + return acc + }, {}) + + const opacity = { ...rawOpacity, ...transparentsOpacity } + + const compat = themeData.v3compat || {} const compatColors = Object.entries(compat.colors || {}).reduce((acc, [key, value]) => { const newVal = value === null ? undefined : value return { ...acc, [key]: newVal } @@ -196,15 +205,22 @@ const generateColors = (input) => { if (typeof v === 'object') { acc[k] = v } else { - acc[k] = hex2rgb(v) + let value = v + if (v === 'transparent') { + value = '#FF00FF' + } + acc[k] = hex2rgb(value) } return acc }, {}) - const isLightOnDark = convert(col.bg).hsl.l < convert(col.text).hsl.l + colors.bg = col.bg + colors.underlay = col.underlay || hex2rgb('#000000') + colors.text = col.text + + const isLightOnDark = convert(colors.bg).hsl.l < convert(colors.text).hsl.l const mod = isLightOnDark ? 1 : -1 - colors.text = col.text colors.lightText = brightness(20 * mod, colors.text).rgb colors.accent = col.accent || col.link @@ -212,17 +228,15 @@ const generateColors = (input) => { colors.faint = col.faint || Object.assign({}, col.text) - colors.bg = col.bg colors.lightBg = col.lightBg || brightness(5 * mod, colors.bg).rgb - const underlay = [col.underlay, opacity.underlay] + const underlay = [colors.underlay, opacity.underlay] const fg = [col.fg, opacity.fg] const bg = [col.bg, opacity.bg] colors.fg = col.fg colors.fgText = col.fgText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, fg]), colors.text) colors.fgLink = col.fgLink || getTextColor(alphaBlendLayers(colors.link, [underlay, bg, fg]), colors.link, true) - colors.underlay = col.underlay || hex2rgb('#000000') colors.border = col.border || brightness(2 * mod, colors.fg).rgb @@ -231,8 +245,8 @@ const generateColors = (input) => { colors.btnText = col.btnText || getTextColor(alphaBlendLayers(colors.fgText, [underlay, bg, fg, btn]), colors.fgText) colors.input = col.input || Object.assign({}, col.fg) - const inputCol = [colors.input, opacity.input] - colors.inputText = col.inputText || getTextColor(alphaBlendLayers(colors.lightText, [underlay, bg, fg, inputCol]), colors.lightText) + const input = [colors.input, opacity.input] + colors.inputText = col.inputText || getTextColor(alphaBlendLayers(colors.lightText, [underlay, bg, fg, input]), colors.lightText) colors.panel = col.panel || Object.assign({}, col.fg) const panel = [colors.panel, opacity.panel] @@ -256,12 +270,14 @@ const generateColors = (input) => { colors.cOrange = col.cOrange || hex2rgb('#E3FF00') colors.alertError = col.alertError || Object.assign({}, colors.cRed) - colors.alertErrorText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.bg), colors.text) - colors.alertErrorPanelText = getTextColor(alphaBlend(colors.alertError, opacity.alert, colors.panel), colors.panelText) + const alertError = [colors.alertError, opacity.alert] + colors.alertErrorText = getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertError]), colors.text) + colors.alertErrorPanelText = getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertError]), colors.panelText) colors.alertWarning = col.alertWarning || Object.assign({}, colors.cOrange) - colors.alertWarningText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.bg), colors.text) - colors.alertWarningPanelText = getTextColor(alphaBlend(colors.alertWarning, opacity.alert, colors.panel), colors.panelText) + const alertWarning = [colors.alertWarning, opacity.alert] + colors.alertWarningText = getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertWarning]), colors.text) + colors.alertWarningPanelText = getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertWarning]), colors.panelText) colors.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed) colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb diff --git a/static/themes/breezy-dark.json b/static/themes/breezy-dark.json index 7ce41384..0ed55184 100644 --- a/static/themes/breezy-dark.json +++ b/static/themes/breezy-dark.json @@ -107,8 +107,12 @@ }, "fonts": {}, "opacity": { - "input": "1", - "panel": "0" + "input": "1" + }, + "v3compat": { + "colors": { + "panel": "transparent" + } }, "colors": { "bg": "#31363b", diff --git a/static/themes/breezy-light.json b/static/themes/breezy-light.json index dc43f90e..5db185dd 100644 --- a/static/themes/breezy-light.json +++ b/static/themes/breezy-light.json @@ -109,6 +109,11 @@ "opacity": { "input": "1" }, + "v3compat": { + "colors": { + "panel": "transparent" + } + }, "colors": { "bg": "#eff0f1", "text": "#232627", -- cgit v1.2.3-70-g09d2 From 4bb1c98e0f28bcf1d0dff2d90d01013cd5487522 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 2 Jan 2020 20:36:10 +0200 Subject: Replaced `v3compat` with `source` to reduce code complexity. Made more slots customizable. `theme` now contains a snapshot of theme generated for better compatiblity and future-proofing --- src/components/style_switcher/style_switcher.js | 68 ++++--- src/components/style_switcher/style_switcher.vue | 14 +- src/services/style_setter/style_setter.js | 42 ++--- static/themes/breezy-dark.json | 224 ++++++++++++++++++++++- static/themes/breezy-light.json | 220 +++++++++++++++++++++- 5 files changed, 488 insertions(+), 80 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 9fe1bf59..f751260a 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -1,7 +1,15 @@ import { rgb2hex, hex2rgb, getContrastRatio, getContrastRatioLayers, alphaBlend } from '../../services/color_convert/color_convert.js' import { set, delete as del } from 'vue' import { merge } from 'lodash' -import { generateCompat, generateColors, generateShadows, generateRadii, generateFonts, composePreset, getThemes } from '../../services/style_setter/style_setter.js' +import { + generateColors, + generateShadows, + generateRadii, + generateFonts, + composePreset, + getThemes, + CURRENT_VERSION +} from '../../services/style_setter/style_setter.js' import ColorInput from '../color_input/color_input.vue' import RangeInput from '../range_input/range_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' @@ -135,15 +143,6 @@ export default { selectedVersion () { return Array.isArray(this.selected) ? 1 : 2 }, - currentCompat () { - return generateCompat({ - shadows: this.shadowsLocal, - fonts: this.fontsLocal, - opacity: this.currentOpacity, - colors: this.currentColors, - radii: this.currentRadii - }) - }, currentColors () { return { bg: this.bgColorLocal, @@ -339,27 +338,32 @@ export default { !this.keepColor ) - const theme = {} + const source = { + themeEngineVersion: CURRENT_VERSION + } if (this.keepFonts || saveEverything) { - theme.fonts = this.fontsLocal + source.fonts = this.fontsLocal } if (this.keepShadows || saveEverything) { - theme.shadows = this.shadowsLocal + source.shadows = this.shadowsLocal } if (this.keepOpacity || saveEverything) { - theme.opacity = this.currentOpacity + source.opacity = this.currentOpacity } if (this.keepColor || saveEverything) { - theme.colors = this.currentColors + source.colors = this.currentColors } if (this.keepRoundness || saveEverything) { - theme.radii = this.currentRadii + source.radii = this.currentRadii } + const theme = this.previewTheme + + console.log(source) return { - // To separate from other random JSON files and possible future theme formats - _pleroma_theme_version: 2, theme: merge(theme, this.currentCompat) + // To separate from other random JSON files and possible future source formats + _pleroma_theme_version: 2, theme, source } } }, @@ -392,7 +396,7 @@ export default { if (parsed._pleroma_theme_version === 1) { this.normalizeLocalState(parsed, 1) } else if (parsed._pleroma_theme_version >= 2) { - this.normalizeLocalState(parsed.theme, 2) + this.normalizeLocalState(parsed.theme, 2, parsed.source) } }, importValidator (parsed) { @@ -402,7 +406,7 @@ export default { clearAll () { const state = this.$store.getters.mergedConfig.customTheme const version = state.colors ? 2 : 'l1' - this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version) + this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version, this.$store.getters.mergedConfig.customThemeSource) }, // Clears all the extra stuff when loading V1 theme @@ -441,24 +445,30 @@ export default { /** * This applies stored theme data onto form. Supports three versions of data: - * v3 (version = 3) - same as 2 but with some incompatible changes + * v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity * v2 (version = 2) - newer version of themes. * v1 (version = 1) - older version of themes (import from file) * v1l (version = l1) - older version of theme (load from local storage) * v1 and v1l differ because of way themes were stored/exported. - * @param {Object} input - input data + * @param {Object} theme - theme data (snapshot) * @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type + * @param {Object} source - theme source - this will be used if compatible + * @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently + * this allows importing source anyway */ - normalizeLocalState (originalInput, version = 0) { + normalizeLocalState (theme, version = 0, source, forceSource = false) { let input - if (typeof originalInput.v3compat !== 'undefined') { - version = 3 - input = merge(originalInput, originalInput.v3compat) + if (typeof source !== 'undefined') { + if (forceSource || source.themeEngineVersion === CURRENT_VERSION) { + input = source + version = source.themeEngineVersion + } else { + input = theme + } } else { - input = originalInput + input = theme } - const compat = input.v3compat const radii = input.radii || input const opacity = input.opacity const shadows = input.shadows || {} @@ -615,7 +625,7 @@ export default { this.cOrangeColorLocal = this.selected[8] } } else if (this.selectedVersion >= 2) { - this.normalizeLocalState(this.selected.theme, 2) + this.normalizeLocalState(this.selected.theme, 2, this.selected.source) } } } diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index 38ca2017..2eadbe25 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -106,7 +106,7 @@
@@ -363,7 +363,7 @@
@@ -377,7 +377,7 @@
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index df22f94f..e8a64517 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -2,6 +2,8 @@ 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 + // 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. @@ -150,29 +152,13 @@ const getCssColor = (input, a) => { return rgb2rgba({ ...rgb, a }) } -// Generates a "patch" for theme to make it compatible with v2 -export const generateCompat = (input) => { - const { colors } = input - const v3compat = { - colors: {} - } - const v2colorsPatch = {} - - // # Link became optional in v3 - if (typeof colors.link === 'undefined') { - v2colorsPatch.link = colors.accent - v3compat.colors.link = null - } - - return { - v3compat, - colors: v2colorsPatch - } -} - const generateColors = (themeData) => { const colors = {} const rawOpacity = Object.assign({ + panel: 1, + btn: 1, + border: 1, + bg: 1, alert: 0.5, input: 0.5, faint: 0.5, @@ -207,6 +193,7 @@ const generateColors = (themeData) => { } else { let value = v if (v === 'transparent') { + // TODO: hack to keep rest of the code from complaining value = '#FF00FF' } acc[k] = hex2rgb(value) @@ -221,7 +208,7 @@ const generateColors = (themeData) => { const isLightOnDark = convert(colors.bg).hsl.l < convert(colors.text).hsl.l const mod = isLightOnDark ? 1 : -1 - colors.lightText = brightness(20 * mod, colors.text).rgb + colors.lightText = col.lightText || brightness(20 * mod, colors.text).rgb colors.accent = col.accent || col.link colors.link = col.link || col.accent @@ -231,7 +218,8 @@ const generateColors = (themeData) => { colors.lightBg = col.lightBg || brightness(5 * mod, colors.bg).rgb const underlay = [colors.underlay, opacity.underlay] - const fg = [col.fg, opacity.fg] + // Technically, foreground can't be transparent (descendants can) but let's keep it just in case + const fg = [col.fg, opacity.fg || 1] const bg = [col.bg, opacity.bg] colors.fg = col.fg @@ -271,16 +259,16 @@ const generateColors = (themeData) => { colors.alertError = col.alertError || Object.assign({}, colors.cRed) const alertError = [colors.alertError, opacity.alert] - colors.alertErrorText = getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertError]), colors.text) - colors.alertErrorPanelText = getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertError]), colors.panelText) + colors.alertErrorText = col.alertErrorText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertError]), colors.text) + colors.alertErrorPanelText = col.alertErrorPanelText || getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertError]), colors.panelText) colors.alertWarning = col.alertWarning || Object.assign({}, colors.cOrange) const alertWarning = [colors.alertWarning, opacity.alert] - colors.alertWarningText = getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertWarning]), colors.text) - colors.alertWarningPanelText = getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertWarning]), colors.panelText) + colors.alertWarningText = col.alertWarningText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertWarning]), colors.text) + colors.alertWarningPanelText = col.alertWarningPanelText || getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertWarning]), colors.panelText) colors.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed) - colors.badgeNotificationText = contrastRatio(colors.badgeNotification).rgb + colors.badgeNotificationText = colors.badgeNotificationText || contrastRatio(colors.badgeNotification).rgb Object.entries(opacity).forEach(([ k, v ]) => { console.log(k) diff --git a/static/themes/breezy-dark.json b/static/themes/breezy-dark.json index 0ed55184..d447005f 100644 --- a/static/themes/breezy-dark.json +++ b/static/themes/breezy-dark.json @@ -2,6 +2,218 @@ "_pleroma_theme_version": 2, "name": "Breezy Dark (beta)", "theme": { + "shadows": { + "panel": [ + { + "x": "1", + "y": "2", + "blur": "6", + "spread": 0, + "color": "#000000", + "alpha": 0.6 + } + ], + "topBar": [ + { + "x": 0, + "y": 0, + "blur": 4, + "spread": 0, + "color": "#000000", + "alpha": 0.6 + } + ], + "popup": [ + { + "x": 2, + "y": 2, + "blur": 3, + "spread": 0, + "color": "#000000", + "alpha": 0.5 + } + ], + "avatar": [ + { + "x": 0, + "y": 1, + "blur": 8, + "spread": 0, + "color": "#000000", + "alpha": 0.7 + } + ], + "avatarStatus": [], + "panelHeader": [ + { + "x": 0, + "y": "40", + "blur": "40", + "spread": "-40", + "inset": true, + "color": "#ffffff", + "alpha": "0.1" + } + ], + "button": [ + { + "x": 0, + "y": "0", + "blur": "0", + "spread": "1", + "color": "#ffffff", + "alpha": "0.15", + "inset": true + }, + { + "x": "1", + "y": "1", + "blur": "1", + "spread": 0, + "color": "#000000", + "alpha": "0.3", + "inset": false + } + ], + "buttonHover": [ + { + "x": 0, + "y": "0", + "blur": 0, + "spread": "1", + "color": "--accent", + "alpha": "0.3", + "inset": true + }, + { + "x": "1", + "y": "1", + "blur": "1", + "spread": 0, + "color": "#000000", + "alpha": "0.3", + "inset": false + } + ], + "buttonPressed": [ + { + "x": 0, + "y": 0, + "blur": "0", + "spread": "50", + "color": "--faint", + "alpha": 1, + "inset": true + }, + { + "x": 0, + "y": "0", + "blur": 0, + "spread": "1", + "color": "#ffffff", + "alpha": 0.2, + "inset": true + }, + { + "x": "1", + "y": "1", + "blur": 0, + "spread": 0, + "color": "#000000", + "alpha": "0.3", + "inset": false + } + ], + "input": [ + { + "x": 0, + "y": "0", + "blur": 0, + "spread": "1", + "color": "#FFFFFF", + "alpha": "0.2", + "inset": true + } + ] + }, + "colors": { + "bg": "#31363b", + "underlay": "#000000", + "text": "#eff0f1", + "lightText": "#ffffff", + "accent": "#3daee9", + "link": "#3daee9", + "faint": "#eff0f1", + "lightBg": "#3d4349", + "fg": "#31363b", + "fgText": "#eff0f1", + "fgLink": "#3daee9", + "border": "#4c545b", + "btn": "#31363b", + "btnText": "#eff0f1", + "input": "#232629", + "inputText": "#ffffff", + "panel": "#ff00ff", + "panelText": "#eff0f1", + "panelLink": "#3daee9", + "panelFaint": "#eff0f1", + "topBar": "#31363b", + "topBarText": "#eff0f1", + "topBarLink": "#eff0f1", + "faintLink": "#3daee9", + "linkBg": "#366681", + "icon": "#909396", + "cBlue": "#3daee9", + "cRed": "#da4453", + "cGreen": "#27ae60", + "cOrange": "#f67400", + "alertError": "#da4453", + "alertErrorText": "#eff0f1", + "alertErrorPanelText": "#eff0f1", + "alertWarning": "#f67400", + "alertWarningText": "#eff0f1", + "alertWarningPanelText": "#eff0f1", + "badgeNotification": "#da4453", + "badgeNotificationText": "#ffffff" + }, + "opacity": { + "panel": 0, + "btn": 1, + "border": 1, + "bg": 1, + "alert": 0.5, + "input": 0.5, + "faint": 0.5, + "underlay": 0.15 + }, + "radii": { + "btn": "2", + "input": "2", + "checkbox": "1", + "panel": "2", + "avatar": "2", + "avatarAlt": "2", + "tooltip": "2", + "attachment": "2" + }, + "fonts": { + "interface": { + "family": "sans-serif" + }, + "input": { + "family": "inherit" + }, + "post": { + "family": "inherit" + }, + "postCode": { + "family": "monospace" + } + } + }, + "source": { + "themeEngineVersion": 3, + "fonts": {}, "shadows": { "panel": [ { @@ -105,21 +317,13 @@ } ] }, - "fonts": {}, - "opacity": { - "input": "1" - }, - "v3compat": { - "colors": { - "panel": "transparent" - } - }, + "opacity": {}, "colors": { "bg": "#31363b", "text": "#eff0f1", "link": "#3daee9", "fg": "#31363b", - "panel": "#31363b", + "panel": "transparent", "input": "#232629", "topBarLink": "#eff0f1", "btn": "#31363b", diff --git a/static/themes/breezy-light.json b/static/themes/breezy-light.json index 5db185dd..243b8593 100644 --- a/static/themes/breezy-light.json +++ b/static/themes/breezy-light.json @@ -2,6 +2,218 @@ "_pleroma_theme_version": 2, "name": "Breezy Light (beta)", "theme": { + "shadows": { + "panel": [ + { + "x": "1", + "y": "2", + "blur": "6", + "spread": 0, + "color": "#000000", + "alpha": 0.6 + } + ], + "topBar": [ + { + "x": 0, + "y": 0, + "blur": 4, + "spread": 0, + "color": "#000000", + "alpha": 0.6 + } + ], + "popup": [ + { + "x": 2, + "y": 2, + "blur": 3, + "spread": 0, + "color": "#000000", + "alpha": 0.5 + } + ], + "avatar": [ + { + "x": 0, + "y": 1, + "blur": 8, + "spread": 0, + "color": "#000000", + "alpha": 0.7 + } + ], + "avatarStatus": [], + "panelHeader": [ + { + "x": 0, + "y": "40", + "blur": "40", + "spread": "-40", + "inset": true, + "color": "#ffffff", + "alpha": "0.1" + } + ], + "button": [ + { + "x": 0, + "y": "0", + "blur": "0", + "spread": "1", + "color": "#000000", + "alpha": "0.3", + "inset": true + }, + { + "x": "1", + "y": "1", + "blur": "1", + "spread": 0, + "color": "#000000", + "alpha": "0.3", + "inset": false + } + ], + "buttonHover": [ + { + "x": 0, + "y": "0", + "blur": 0, + "spread": "1", + "color": "--accent", + "alpha": "0.3", + "inset": true + }, + { + "x": "1", + "y": "1", + "blur": "1", + "spread": 0, + "color": "#000000", + "alpha": "0.3", + "inset": false + } + ], + "buttonPressed": [ + { + "x": 0, + "y": 0, + "blur": "0", + "spread": "50", + "color": "--faint", + "alpha": 1, + "inset": true + }, + { + "x": 0, + "y": "0", + "blur": 0, + "spread": "1", + "color": "#ffffff", + "alpha": 0.2, + "inset": true + }, + { + "x": "1", + "y": "1", + "blur": 0, + "spread": 0, + "color": "#000000", + "alpha": "0.3", + "inset": false + } + ], + "input": [ + { + "x": 0, + "y": "0", + "blur": 0, + "spread": "1", + "color": "#000000", + "alpha": "0.2", + "inset": true + } + ] + }, + "colors": { + "bg": "#eff0f1", + "underlay": "#000000", + "text": "#232627", + "lightText": "#000000", + "accent": "#2980b9", + "link": "#2980b9", + "faint": "#232627", + "lightBg": "#e2e4e6", + "fg": "#bcc2c7", + "fgText": "#232627", + "fgLink": "#2980b9", + "border": "#b7bdc3", + "btn": "#eff0f1", + "btnText": "#232627", + "input": "#fcfcfc", + "inputText": "#000000", + "panel": "#475057", + "panelText": "#fcfcfc", + "panelLink": "#ffffff", + "panelFaint": "#d8dbdc", + "topBar": "#475057", + "topBarText": "#d8dbdc", + "topBarLink": "#eff0f1", + "faintLink": "#2980b9", + "linkBg": "#a0c4db", + "icon": "#898b8c", + "cBlue": "#2980b9", + "cRed": "#da4453", + "cGreen": "#27ae60", + "cOrange": "#f67400", + "alertError": "#da4453", + "alertErrorText": "#232627", + "alertErrorPanelText": "#fcfcfc", + "alertWarning": "#f67400", + "alertWarningText": "#232627", + "alertWarningPanelText": "#fcfcfc", + "badgeNotification": "#da4453", + "badgeNotificationText": "#ffffff" + }, + "opacity": { + "panel": 1, + "btn": 1, + "border": 1, + "bg": 1, + "alert": 0.5, + "input": "1", + "faint": 0.5, + "underlay": 0.15 + }, + "radii": { + "btn": "2", + "input": "2", + "checkbox": "1", + "panel": "2", + "avatar": "2", + "avatarAlt": "2", + "tooltip": "2", + "attachment": "2" + }, + "fonts": { + "interface": { + "family": "sans-serif" + }, + "input": { + "family": "inherit" + }, + "post": { + "family": "inherit" + }, + "postCode": { + "family": "monospace" + } + } + }, + "source": { + "themeEngineVersion": 3, + "fonts": {}, "shadows": { "panel": [ { @@ -105,20 +317,14 @@ } ] }, - "fonts": {}, "opacity": { "input": "1" }, - "v3compat": { - "colors": { - "panel": "transparent" - } - }, "colors": { "bg": "#eff0f1", "text": "#232627", - "link": "#2980b9", "fg": "#bcc2c7", + "accent": "#2980b9", "panel": "#475057", "panelText": "#fcfcfc", "input": "#fcfcfc", -- cgit v1.2.3-70-g09d2 From cce64077b54ae57fd5a8f5b5c520ee3274c5a61e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 6 Jan 2020 22:55:14 +0200 Subject: Refactored style_setter to be more declarative instead of walls of copypasted code --- src/services/style_setter/style_setter.js | 382 ++++++++++++++++++++++++------ 1 file changed, 306 insertions(+), 76 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index e8a64517..40a552a1 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -3,6 +3,262 @@ 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', 'fg', 'underlay', 'bg'], + layer: 'fg', + textColor: true + }, + fgLink: { + depends: ['link', 'fg', 'underlay', 'bg'], + layer: 'fg', + textColor: 'preserve' + }, + + // Panel header + panel: '--fg', + panelText: { + depends: ['fgText', 'panel'], + layer: 'panel', + textColor: true + }, + panelFaint: { + depends: ['fgText', 'panel'], + layer: 'panel', + textColor: true + }, + panelLink: { + depends: ['fgLink', 'panel'], + layer: 'panel', + textColor: 'preserve' + }, + + // Top bar + topBar: '--fg', + topBarText: { + depends: ['fgText', 'topBar'], + layer: 'topBar', + textColor: true + }, + topBarLink: { + depends: ['fgLink', 'topBar'], + layer: 'topBar', + textColor: 'preserve' + }, + + // Buttons + btn: '--fg', + btnText: { + depends: ['fgText', 'btn'], + layer: 'btn' + }, + btnPanelText: { + depends: ['panelText', 'btn', 'panel'], + layer: 'btnPanel', + variant: 'btn', + textColor: true + }, + btnTopBarText: { + depends: ['topBarText', 'btn', 'topBar'], + layer: 'btnTopBar', + variant: 'btn', + textColor: true + }, + + // Input fields + input: '--fg', + inputText: { + depends: ['text', 'input'], + layer: 'input', + textColor: true + }, + inputPanelText: { + depends: ['panelText', 'input', 'panel'], + layer: 'inputPanel', + variant: 'input', + textColor: true + }, + inputTopbarText: { + depends: ['topBarText', 'input', 'topBar'], + layer: 'inputTopBar', + variant: 'input', + textColor: true + }, + + alertError: '--cRed', + alertErrorText: { + depends: ['text', 'alertError'], + layer: 'alert', + variant: 'alertError', + textColor: true + }, + alertErrorPanelText: { + depends: ['panelText', 'alertError', 'panel'], + layer: 'alertPanel', + variant: 'alertError', + textColor: true + }, + + alertWarning: '--cOrange', + alertWarningText: { + depends: ['text', 'alertWarning'], + layer: 'alert', + variant: 'alertWarning', + textColor: true + }, + alertWarningPanelText: { + depends: ['panelText', 'alertWarning', 'panel'], + layer: 'alertPanel', + variant: 'alertWarning', + textColor: true + }, + + badgeNotification: '--cRed', + badgeNotificationText: { + depends: ['text', 'badgeNotification'], + layer: 'badge', + variant: 'badgeNotification', + textColor: true + } +} + +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 } = data + if (Array.isArray(depends)) { + return depends + } else if (typeof depends === 'object') { + return [depends] + } else { + return [] + } + } +} + +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 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] + ])) +} // 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 @@ -153,12 +409,13 @@ const getCssColor = (input, a) => { } const generateColors = (themeData) => { - const colors = {} const rawOpacity = Object.assign({ panel: 1, btn: 1, border: 1, bg: 1, + badge: 1, + text: 1, alert: 0.5, input: 0.5, faint: 0.5, @@ -171,7 +428,6 @@ const generateColors = (themeData) => { }, {})) const inputColors = themeData.colors || themeData - const transparentsOpacity = Object.entries(inputColors).reduce((acc, [k, v]) => { if (v === 'transparent') { acc[k] = 0 @@ -181,13 +437,8 @@ const generateColors = (themeData) => { const opacity = { ...rawOpacity, ...transparentsOpacity } - const compat = themeData.v3compat || {} - const compatColors = Object.entries(compat.colors || {}).reduce((acc, [key, value]) => { - const newVal = value === null ? undefined : value - return { ...acc, [key]: newVal } - }, {}) - - const col = Object.entries({ ...inputColors, ...compatColors }).reduce((acc, [k, v]) => { + // Cycle one: just whatever we have + const sourceColors = Object.entries(inputColors).reduce((acc, [k, v]) => { if (typeof v === 'object') { acc[k] = v } else { @@ -201,77 +452,53 @@ const generateColors = (themeData) => { return acc }, {}) - colors.bg = col.bg - colors.underlay = col.underlay || hex2rgb('#000000') - colors.text = col.text - - const isLightOnDark = convert(colors.bg).hsl.l < convert(colors.text).hsl.l + const isLightOnDark = convert(sourceColors.bg).hsl.l < convert(sourceColors.text).hsl.l const mod = isLightOnDark ? 1 : -1 - colors.lightText = col.lightText || brightness(20 * mod, colors.text).rgb - - colors.accent = col.accent || col.link - colors.link = col.link || col.accent - - colors.faint = col.faint || Object.assign({}, col.text) - - colors.lightBg = col.lightBg || brightness(5 * mod, colors.bg).rgb - - const underlay = [colors.underlay, opacity.underlay] - // Technically, foreground can't be transparent (descendants can) but let's keep it just in case - const fg = [col.fg, opacity.fg || 1] - const bg = [col.bg, opacity.bg] - - colors.fg = col.fg - colors.fgText = col.fgText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, fg]), colors.text) - colors.fgLink = col.fgLink || getTextColor(alphaBlendLayers(colors.link, [underlay, bg, fg]), colors.link, true) - - colors.border = col.border || brightness(2 * mod, colors.fg).rgb - - colors.btn = col.btn || Object.assign({}, col.fg) - const btn = [colors.btn, opacity.btn || 1] - colors.btnText = col.btnText || getTextColor(alphaBlendLayers(colors.fgText, [underlay, bg, fg, btn]), colors.fgText) - - colors.input = col.input || Object.assign({}, col.fg) - const input = [colors.input, opacity.input] - colors.inputText = col.inputText || getTextColor(alphaBlendLayers(colors.lightText, [underlay, bg, fg, input]), colors.lightText) - - colors.panel = col.panel || Object.assign({}, col.fg) - const panel = [colors.panel, opacity.panel] - colors.panelText = col.panelText || getTextColor(alphaBlendLayers(colors.fgText, [underlay, bg, panel]), colors.fgText) - colors.panelLink = col.panelLink || getTextColor(alphaBlendLayers(colors.fgLink, [underlay, bg, panel]), colors.fgLink) - colors.panelFaint = col.panelFaint || getTextColor(alphaBlendLayers(colors.faint, [underlay, bg, panel]), colors.faint) - - colors.topBar = col.topBar || Object.assign({}, col.fg) - const topBar = [colors.topBar, opacity.topBar] - colors.topBarText = col.topBarText || getTextColor(alphaBlendLayers(colors.fgText, [topBar]), colors.fgText) - colors.topBarLink = col.topBarLink || getTextColor(alphaBlendLayers(colors.fgLink, [topBar]), colors.fgLink) - - colors.faintLink = col.faintLink || Object.assign({}, col.link || col.accent) - colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg) - - colors.icon = mixrgb(colors.bg, colors.text) - - colors.cBlue = col.cBlue || hex2rgb('#0000FF') - colors.cRed = col.cRed || hex2rgb('#FF0000') - colors.cGreen = col.cGreen || hex2rgb('#00FF00') - colors.cOrange = col.cOrange || hex2rgb('#E3FF00') - - colors.alertError = col.alertError || Object.assign({}, colors.cRed) - const alertError = [colors.alertError, opacity.alert] - colors.alertErrorText = col.alertErrorText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertError]), colors.text) - colors.alertErrorPanelText = col.alertErrorPanelText || getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertError]), colors.panelText) - - colors.alertWarning = col.alertWarning || Object.assign({}, colors.cOrange) - const alertWarning = [colors.alertWarning, opacity.alert] - colors.alertWarningText = col.alertWarningText || getTextColor(alphaBlendLayers(colors.text, [underlay, bg, alertWarning]), colors.text) - colors.alertWarningPanelText = col.alertWarningPanelText || getTextColor(alphaBlendLayers(colors.panelText, [underlay, bg, panel, panel, alertWarning]), colors.panelText) - - colors.badgeNotification = col.badgeNotification || Object.assign({}, colors.cRed) - colors.badgeNotificationText = colors.badgeNotificationText || contrastRatio(colors.badgeNotification).rgb + 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) { + return { + ...acc, + [key]: getTextColor( + alphaBlendLayers( + { ...acc[deps[0]] }, + getLayers( + value.layer, + value.variant || value.layer, + acc, + opacity + ) + ), + { ...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 Object.entries(opacity).forEach(([ k, v ]) => { - console.log(k) if (typeof v === 'undefined') return if (k === 'alert') { colors.alertError.a = v @@ -285,6 +512,9 @@ const generateColors = (themeData) => { if (k === 'bg') { colors['lightBg'].a = v } + if (k === 'badge') { + colors['badgeNotification'].a = v + } if (colors[k]) { colors[k].a = v } else { -- cgit v1.2.3-70-g09d2 From 38f2b969e467d1cf47597dc4cc2b958e5df99828 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sat, 11 Jan 2020 23:07:41 +0200 Subject: simplified definition for text color by accounting for layers automatically, fixed badge notification text color by adding 'bw' option for textColor --- src/services/style_setter/style_setter.js | 123 +++++++++++++++++------------- 1 file changed, 69 insertions(+), 54 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 40a552a1..992b3194 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -65,12 +65,12 @@ export const SLOT_INHERITANCE = { // Foreground fgText: { - depends: ['text', 'fg', 'underlay', 'bg'], + depends: ['text'], layer: 'fg', textColor: true }, fgLink: { - depends: ['link', 'fg', 'underlay', 'bg'], + depends: ['link'], layer: 'fg', textColor: 'preserve' }, @@ -78,17 +78,17 @@ export const SLOT_INHERITANCE = { // Panel header panel: '--fg', panelText: { - depends: ['fgText', 'panel'], + depends: ['fgText'], layer: 'panel', textColor: true }, panelFaint: { - depends: ['fgText', 'panel'], + depends: ['fgText'], layer: 'panel', textColor: true }, panelLink: { - depends: ['fgLink', 'panel'], + depends: ['fgLink'], layer: 'panel', textColor: 'preserve' }, @@ -96,12 +96,12 @@ export const SLOT_INHERITANCE = { // Top bar topBar: '--fg', topBarText: { - depends: ['fgText', 'topBar'], + depends: ['fgText'], layer: 'topBar', textColor: true }, topBarLink: { - depends: ['fgLink', 'topBar'], + depends: ['fgLink'], layer: 'topBar', textColor: 'preserve' }, @@ -109,17 +109,17 @@ export const SLOT_INHERITANCE = { // Buttons btn: '--fg', btnText: { - depends: ['fgText', 'btn'], + depends: ['fgText'], layer: 'btn' }, btnPanelText: { - depends: ['panelText', 'btn', 'panel'], + depends: ['panelText'], layer: 'btnPanel', variant: 'btn', textColor: true }, btnTopBarText: { - depends: ['topBarText', 'btn', 'topBar'], + depends: ['topBarText'], layer: 'btnTopBar', variant: 'btn', textColor: true @@ -128,18 +128,18 @@ export const SLOT_INHERITANCE = { // Input fields input: '--fg', inputText: { - depends: ['text', 'input'], + depends: ['text'], layer: 'input', textColor: true }, inputPanelText: { - depends: ['panelText', 'input', 'panel'], + depends: ['panelText'], layer: 'inputPanel', variant: 'input', textColor: true }, inputTopbarText: { - depends: ['topBarText', 'input', 'topBar'], + depends: ['topBarText'], layer: 'inputTopBar', variant: 'input', textColor: true @@ -153,7 +153,7 @@ export const SLOT_INHERITANCE = { textColor: true }, alertErrorPanelText: { - depends: ['panelText', 'alertError', 'panel'], + depends: ['panelText', 'alertError'], layer: 'alertPanel', variant: 'alertError', textColor: true @@ -167,7 +167,7 @@ export const SLOT_INHERITANCE = { textColor: true }, alertWarningPanelText: { - depends: ['panelText', 'alertWarning', 'panel'], + depends: ['panelText', 'alertWarning'], layer: 'alertPanel', variant: 'alertWarning', textColor: true @@ -178,23 +178,47 @@ export const SLOT_INHERITANCE = { depends: ['text', 'badgeNotification'], layer: 'badge', variant: 'badgeNotification', - textColor: true + 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 } = data + const { depends, layer, variant } = data + const layerDeps = layer + ? getLayersArray(layer).map(currentLayer => { + return currentLayer === layer + ? variant || layer + : currentLayer + }) + : [] if (Array.isArray(depends)) { - return depends - } else if (typeof depends === 'object') { - return [depends] + return [...depends, ...layerDeps] } else { - return [] + return [...layerDeps] } } } @@ -241,25 +265,6 @@ export const topoSort = ( export const SLOT_ORDERED = topoSort(SLOT_INHERITANCE) -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] - ])) -} - // 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. @@ -318,6 +323,8 @@ 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) @@ -468,21 +475,29 @@ const generateColors = (themeData) => { const colorFunc = (isObject && value.color) || defaultColorFunc if (value.textColor) { - return { - ...acc, - [key]: getTextColor( - alphaBlendLayers( - { ...acc[deps[0]] }, - getLayers( - value.layer, - value.variant || value.layer, - acc, - opacity - ) - ), - { ...acc[deps[0]] }, - value.textColor === 'preserve' + 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] }))) -- cgit v1.2.3-70-g09d2 From 622c9d388e0df1f53c544c34b7def2bb6fe498cd Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 12 Jan 2020 03:44:06 +0200 Subject: Refactoring, forgotten files --- src/components/color_input/color_input.scss | 65 +++ src/services/color_convert/color_convert.js | 99 ++++- src/services/style_setter/style_setter.js | 442 ++------------------- src/services/theme_data/theme_data.service.js | 315 +++++++++++++++ .../services/style_setter/style_setter.spec.js | 79 ++++ 5 files changed, 578 insertions(+), 422 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 (limited to 'src/services/style_setter/style_setter.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 }, {}) } +/** + * 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 +} -export { - rgb2hex, - hex2rgb, - mixrgb, - invert, - getContrastRatio, - alphaBlend +/** + * 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 cRedRgb = hex2rgb(theme[5] || '#FF0000') - const cGreenRgb = hex2rgb(theme[6] || '#00FF00') - const cBlueRgb = hex2rgb(theme[7] || '#0000FF') - const cOrangeRgb = hex2rgb(theme[8] || '#E3FF00') - - data.colors = { - bg: bgRgb, - fg: fgRgb, - text: textRgb, - link: linkRgb, - cRed: cRedRgb, - cBlue: cBlueRgb, - cGreen: cGreenRgb, - cOrange: cOrangeRgb - } + const bg = hex2rgb(theme[1]) + const fg = hex2rgb(theme[2]) + const text = hex2rgb(theme[3]) + const link = hex2rgb(theme[4]) + + 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, 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')) + }) +}) -- cgit v1.2.3-70-g09d2 From a9a1fc37f546840f3d0fbaed493b131cb9c60669 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 12 Jan 2020 04:00:41 +0200 Subject: fixes, cleanup --- src/components/style_switcher/style_switcher.js | 13 +++++++++---- src/services/style_setter/style_setter.js | 15 ++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index f751260a..4749d3f7 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -1,15 +1,20 @@ -import { rgb2hex, hex2rgb, getContrastRatio, getContrastRatioLayers, alphaBlend } from '../../services/color_convert/color_convert.js' import { set, delete as del } from 'vue' -import { merge } from 'lodash' +import { + rgb2hex, + hex2rgb, + getContrastRatio, + getContrastRatioLayers, + alphaBlend +} from '../../services/color_convert/color_convert.js' import { generateColors, generateShadows, generateRadii, generateFonts, composePreset, - getThemes, - CURRENT_VERSION + getThemes } from '../../services/style_setter/style_setter.js' +import { CURRENT_VERSION } from '../../services/theme_data/theme_data.service.js' import ColorInput from '../color_input/color_input.vue' import RangeInput from '../range_input/range_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 46b08628..516fb5f6 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -114,7 +114,7 @@ const getCssShadowFilter = (input) => { .join(' ') } -const generateColors = (themeData) => { +export const generateColors = (themeData) => { const rawOpacity = Object.assign({ panel: 1, btn: 1, @@ -182,6 +182,7 @@ const generateColors = (themeData) => { } if (k === 'badge') { colors['badgeNotification'].a = v + return } if (colors[k]) { colors[k].a = v @@ -211,7 +212,7 @@ const generateColors = (themeData) => { } } -const generateRadii = (input) => { +export const generateRadii = (input) => { let inputRadii = input.radii || {} // v1 -> v2 if (typeof input.btnRadius !== 'undefined') { @@ -244,7 +245,7 @@ const generateRadii = (input) => { } } -const generateFonts = (input) => { +export const generateFonts = (input) => { const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => { acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => { acc[k] = v @@ -279,7 +280,7 @@ const generateFonts = (input) => { } } -const generateShadows = (input) => { +export const generateShadows = (input) => { const border = (top, shadow) => ({ x: 0, y: top ? 1 : -1, @@ -376,7 +377,7 @@ const generateShadows = (input) => { } } -const composePreset = (colors, radii, shadows, fonts) => { +export const composePreset = (colors, radii, shadows, fonts) => { return { rules: { ...shadows.rules, @@ -393,14 +394,14 @@ const composePreset = (colors, radii, shadows, fonts) => { } } -const generatePreset = (input) => composePreset( +export const generatePreset = (input) => composePreset( generateColors(input), generateRadii(input), generateShadows(input), generateFonts(input) ) -const getThemes = () => { +export const getThemes = () => { return window.fetch('/static/styles.json') .then((data) => data.json()) .then((themes) => { -- cgit v1.2.3-70-g09d2 From 88f83fc9fa3652efdbe6aa622d3e0089883e8057 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Sun, 12 Jan 2020 17:46:07 +0200 Subject: overhaul how style-switcher makes state, removed tons of copy-paste --- src/components/style_switcher/style_switcher.js | 214 ++++++----------------- src/components/style_switcher/style_switcher.vue | 12 ++ src/services/style_setter/style_setter.js | 15 +- src/services/theme_data/theme_data.service.js | 15 +- 4 files changed, 85 insertions(+), 171 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 4749d3f7..9c6f3266 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -14,7 +14,12 @@ import { composePreset, getThemes } from '../../services/style_setter/style_setter.js' -import { CURRENT_VERSION } from '../../services/theme_data/theme_data.service.js' +import { + CURRENT_VERSION, + SLOT_INHERITANCE, + DEFAULT_OPACITY, + getLayers +} from '../../services/theme_data/theme_data.service.js' import ColorInput from '../color_input/color_input.vue' import RangeInput from '../range_input/range_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' @@ -67,55 +72,13 @@ export default { keepRoundness: false, keepFonts: false, - textColorLocal: '', - accentColorLocal: undefined, - linkColorLocal: undefined, - - bgColorLocal: '', - bgOpacityLocal: undefined, - - underlayColorLocal: '', - underlayOpacityLocal: undefined, - - fgColorLocal: '', - fgTextColorLocal: undefined, - fgLinkColorLocal: undefined, - - btnColorLocal: undefined, - btnTextColorLocal: undefined, - btnOpacityLocal: undefined, - - inputColorLocal: undefined, - inputTextColorLocal: undefined, - inputOpacityLocal: undefined, - - panelColorLocal: undefined, - panelTextColorLocal: undefined, - panelLinkColorLocal: undefined, - panelFaintColorLocal: undefined, - panelOpacityLocal: undefined, - - topBarColorLocal: undefined, - topBarTextColorLocal: undefined, - topBarLinkColorLocal: undefined, - - alertErrorColorLocal: undefined, - alertWarningColorLocal: undefined, - - badgeOpacityLocal: undefined, - badgeNotificationColorLocal: undefined, + ...Object.keys(SLOT_INHERITANCE) + .map(key => [key, '']) + .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}), - borderColorLocal: undefined, - borderOpacityLocal: undefined, - - faintColorLocal: undefined, - faintOpacityLocal: undefined, - faintLinkColorLocal: undefined, - - cRedColorLocal: '', - cBlueColorLocal: '', - cGreenColorLocal: '', - cOrangeColorLocal: '', + ...Object.keys(DEFAULT_OPACITY) + .map(key => [key, undefined]) + .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}), shadowSelected: undefined, shadowsLocal: {}, @@ -149,59 +112,14 @@ export default { return Array.isArray(this.selected) ? 1 : 2 }, currentColors () { - return { - bg: this.bgColorLocal, - text: this.textColorLocal, - link: this.linkColorLocal, - - fg: this.fgColorLocal, - fgText: this.fgTextColorLocal, - fgLink: this.fgLinkColorLocal, - - accent: this.accentColorLocal, - - underlay: this.underlayColorLocal, - - panel: this.panelColorLocal, - panelText: this.panelTextColorLocal, - panelLink: this.panelLinkColorLocal, - panelFaint: this.panelFaintColorLocal, - - input: this.inputColorLocal, - inputText: this.inputTextColorLocal, - - topBar: this.topBarColorLocal, - topBarText: this.topBarTextColorLocal, - topBarLink: this.topBarLinkColorLocal, - - btn: this.btnColorLocal, - btnText: this.btnTextColorLocal, - - alertError: this.alertErrorColorLocal, - alertWarning: this.alertWarningColorLocal, - badgeNotification: this.badgeNotificationColorLocal, - - faint: this.faintColorLocal, - faintLink: this.faintLinkColorLocal, - border: this.borderColorLocal, - - cRed: this.cRedColorLocal, - cBlue: this.cBlueColorLocal, - cGreen: this.cGreenColorLocal, - cOrange: this.cOrangeColorLocal - } + return Object.keys(SLOT_INHERITANCE) + .map(key => [key, this[key + 'ColorLocal']]) + .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) }, currentOpacity () { - return { - bg: this.bgOpacityLocal, - btn: this.btnOpacityLocal, - input: this.inputOpacityLocal, - panel: this.panelOpacityLocal, - topBar: this.topBarOpacityLocal, - border: this.borderOpacityLocal, - faint: this.faintOpacityLocal, - underlay: this.underlayOpacityLocal - } + return Object.keys(DEFAULT_OPACITY) + .map(key => [key, this[key + 'OpacityLocal']]) + .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}) }, currentRadii () { return { @@ -237,63 +155,45 @@ export default { laa: ratio >= 3, laaa: ratio >= 4.5 }) - - // fgsfds :DDDD - const fgs = { - text: colorConvert(colors.text), - panelText: colorConvert(colors.panelText), - panelLink: colorConvert(colors.panelLink), - btnText: colorConvert(colors.btnText), - topBarText: colorConvert(colors.topBarText), - inputText: colorConvert(colors.inputText), - - link: colorConvert(colors.link), - topBarLink: colorConvert(colors.topBarLink), - - red: colorConvert(colors.cRed), - green: colorConvert(colors.cGreen), - blue: colorConvert(colors.cBlue), - orange: colorConvert(colors.cOrange) - } - - const bgs = { - bg: colorConvert(colors.bg), - underlay: colorConvert(colors.underlay), - btn: colorConvert(colors.btn), - panel: colorConvert(colors.panel), - topBar: colorConvert(colors.topBar), - input: colorConvert(colors.input), - alertError: colorConvert(colors.alertError), - alertWarning: colorConvert(colors.alertWarning), - badgeNotification: colorConvert(colors.badgeNotification) - } - - const bg = [bgs.bg, opacity.bg] - const underlay = [bgs.underlay || colorConvert('#000000'), opacity.underlay] - - const panel = [underlay, bg] - - const ratios = { - bgText: getContrastRatioLayers(fgs.text, panel, fgs.text), - bgLink: getContrastRatioLayers(fgs.link, panel, fgs.link), - bgRed: getContrastRatioLayers(fgs.red, panel, fgs.red), - bgGreen: getContrastRatioLayers(fgs.green, panel, fgs.green), - bgBlue: getContrastRatioLayers(fgs.blue, panel, fgs.blue), - bgOrange: getContrastRatioLayers(fgs.orange, panel, fgs.orange), - - // TODO what's this? - tintText: getContrastRatio(alphaBlend(bgs.bg, 0.5, fgs.panelText), fgs.text), - - panelText: getContrastRatioLayers(fgs.text, [...panel, [bgs.panel, opacity.panel]], fgs.panelText), - panelLink: getContrastRatioLayers(fgs.link, [...panel, [bgs.panel, opacity.panel]], fgs.panelLink), - - btnText: getContrastRatioLayers(fgs.text, [...panel, [bgs.btn, opacity.btn]], fgs.btnText), - - inputText: getContrastRatioLayers(fgs.text, [...panel, [bgs.input, opacity.input]], fgs.inputText), - - topBarText: getContrastRatioLayers(fgs.text, [...panel, [bgs.topBar, opacity.topBar]], fgs.topBarText), - topBarLink: getContrastRatioLayers(fgs.link, [...panel, [bgs.topBar, opacity.topBar]], fgs.topBarLink) - } + const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {}) + + const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => { + const slotIsBaseText = key === 'text' || key === 'link' + const slotIsText = slotIsBaseText || ( + typeof value === 'object' && value !== null && value.textColor + ) + if (!slotIsText) return acc + const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value + const background = variant || layer + const textColors = [ + key, + ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : []) + ] + + const layers = getLayers( + layer, + variant || layer, + colorsConverted, + opacity + ) + + return { + ...acc, + ...textColors.reduce((acc, textColorKey) => { + const newKey = slotIsBaseText + ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1) + : textColorKey + return { + ...acc, + [newKey]: getContrastRatioLayers( + colorsConverted[textColorKey], + layers, + colorsConverted[textColorKey] + ) + } + }, {}) + } + }, {}) return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {}) }, diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index 82b85bd5..b059eb8a 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -209,6 +209,12 @@ :label="$t('settings.style.advanced_colors.alert_error')" :fallback="previewTheme.colors.alertError" /> + +

{{ $t('settings.style.advanced_colors.badge') }}

diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 516fb5f6..e11516c0 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 } from '../theme_data/theme_data.service.js' +import { getColors, DEFAULT_OPACITY } 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,18 +115,7 @@ const getCssShadowFilter = (input) => { } export const generateColors = (themeData) => { - const rawOpacity = Object.assign({ - panel: 1, - btn: 1, - border: 1, - bg: 1, - badge: 1, - text: 1, - alert: 0.5, - input: 0.5, - faint: 0.5, - underlay: 0.15 - }, Object.entries(themeData.opacity || {}).reduce((acc, [k, v]) => { + const rawOpacity = Object.assign({ ...DEFAULT_OPACITY }, Object.entries(themeData.opacity || {}).reduce((acc, [k, v]) => { if (typeof v !== 'undefined') { acc[k] = v } diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js index 1117ab05..297d0223 100644 --- a/src/services/theme_data/theme_data.service.js +++ b/src/services/theme_data/theme_data.service.js @@ -23,6 +23,19 @@ export const LAYERS = { alertPanel: 'panel' } +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 +} + export const SLOT_INHERITANCE = { bg: null, fg: null, @@ -289,7 +302,7 @@ export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.redu if (value.textColor === 'bw') { return { ...acc, - [key]: contrastRatio(bg) + [key]: contrastRatio(bg).rgb } } else { return { -- cgit v1.2.3-70-g09d2 From 8f63bbb64fa2860e73d8562de9d6c62629a8668f Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 13 Jan 2020 00:33:04 +0200 Subject: poll slot renamed, lightBg customization implemented --- src/App.scss | 4 ++++ src/components/autosuggest/autosuggest.vue | 4 ++-- src/components/dialog_modal/dialog_modal.vue | 12 +++++----- src/components/emoji_input/emoji_input.vue | 4 ++++ src/components/nav_panel/nav_panel.vue | 10 ++++++++ src/components/selectable_list/selectable_list.vue | 4 ++++ src/components/side_drawer/side_drawer.vue | 5 ++++ src/components/status/status.vue | 7 ++++-- src/components/style_switcher/style_switcher.vue | 27 ++++++++++++++++++++-- src/services/style_setter/style_setter.js | 4 +++- src/services/theme_data/theme_data.service.js | 26 +++++++++++++++++++++ 11 files changed, 94 insertions(+), 13 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/App.scss b/src/App.scss index 7c9c91af..7da3688a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -495,6 +495,10 @@ main-router { color: $fallback--faint; color: var(--panelFaint, $fallback--faint); } + .faint-link { + color: $fallback--faint; + color: var(--faintLink, $fallback--faint); + } .alert { white-space: nowrap; diff --git a/src/components/autosuggest/autosuggest.vue b/src/components/autosuggest/autosuggest.vue index 1f86e996..f283ab82 100644 --- a/src/components/autosuggest/autosuggest.vue +++ b/src/components/autosuggest/autosuggest.vue @@ -40,8 +40,8 @@ top: 100%; right: 0; max-height: 400px; - background-color: $fallback--lightBg; - background-color: var(--lightBg, $fallback--lightBg); + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); border-style: solid; border-width: 1px; border-color: $fallback--border; diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue index 55d7a7d2..3241ce3e 100644 --- a/src/components/dialog_modal/dialog_modal.vue +++ b/src/components/dialog_modal/dialog_modal.vue @@ -75,18 +75,18 @@ .dialog-modal-content { margin: 0; padding: 1rem 1rem; - background-color: $fallback--lightBg; - background-color: var(--lightBg, $fallback--lightBg); + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); white-space: normal; } .dialog-modal-footer { margin: 0; padding: .5em .5em; - background-color: $fallback--lightBg; - background-color: var(--lightBg, $fallback--lightBg); - border-top: 1px solid $fallback--bg; - border-top: 1px solid var(--bg, $fallback--bg); + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + border-top: 1px solid $fallback--border; + border-top: 1px solid var(--border, $fallback--border); display: flex; justify-content: flex-end; diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index a7215670..dcf51ff9 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -158,6 +158,10 @@ &.highlighted { background-color: $fallback--fg; background-color: var(--lightBg, $fallback--fg); + color: var(--lightBgText, $fallback--text); + --faint: var(--lightBgFaintText, $fallback--faint); + --faintLink: var(--lightBgFaintLink, $fallback--faint); + --icon: var(--lightBgIcon, $fallback--icon); } } } diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 034259d9..0886bf8c 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -101,12 +101,22 @@ &:hover { background-color: $fallback--lightBg; background-color: var(--lightBg, $fallback--lightBg); + color: $fallback--link; + color: var(--lightBgText, $fallback--link); + --faint: var(--lightBgFaintText, $fallback--faint); + --faintLink: var(--lightBgFaintLink, $fallback--faint); + --icon: var(--lightBgIcon, $fallback--icon); } &.router-link-active { font-weight: bolder; background-color: $fallback--lightBg; background-color: var(--lightBg, $fallback--lightBg); + color: $fallback--text; + color: var(--lightBgText, $fallback--text); + --faint: var(--lightBgFaintText, $fallback--faint); + --faintLink: var(--lightBgFaintLink, $fallback--faint); + --icon: var(--lightBgIcon, $fallback--icon); &:hover { text-decoration: underline; diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue index d9ec7ece..416c9b6a 100644 --- a/src/components/selectable_list/selectable_list.vue +++ b/src/components/selectable_list/selectable_list.vue @@ -69,6 +69,10 @@ &-item-selected-inner { background-color: $fallback--lightBg; background-color: var(--lightBg, $fallback--lightBg); + color: var(--lightBgText, $fallback--text); + --faint: var(--lightBgFaintText, $fallback--faint); + --faintLink: var(--lightBgFaintLink, $fallback--faint); + --icon: var(--lightBgIcon, $fallback--icon); } &-header { diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 3fba9058..6d75221f 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -290,6 +290,11 @@ &:hover { background-color: $fallback--lightBg; background-color: var(--lightBg, $fallback--lightBg); + color: $fallback--text; + color: var(--lightBgText, $fallback--text); + --faint: var(--lightBgFaintText, $fallback--faint); + --faintLink: var(--lightBgFaintLink, $fallback--faint); + --icon: var(--lightBgIcon, $fallback--icon); } } } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index d291e762..72e9b25a 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -446,6 +446,11 @@ $status-margin: 0.75em; &_focused { background-color: $fallback--lightBg; background-color: var(--lightBg, $fallback--lightBg); + color: $fallback--text; + color: var(--lightBgText, $fallback--text); + --faint: var(--lightBgFaintText, $fallback--faint); + --faintLink: var(--lightBgFaintLink, $fallback--faint); + --icon: var(--lightBgIcon, $fallback--icon); } .timeline & { @@ -573,8 +578,6 @@ $status-margin: 0.75em; overflow: hidden; text-overflow: ellipsis; margin: 0 0.4em 0 0.2em; - color: $fallback--faint; - color: var(--faint, $fallback--faint); } .replies-separator { diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index e0894b6d..1381f1fb 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -414,7 +414,7 @@ /> @@ -423,11 +423,34 @@

{{ $t('settings.style.advanced_colors.icons') }}

+
+

{{ $t('settings.style.advanced_colors.lightBg') }}

+ + + + + +
{ return } if (k === 'faint') { - colors[k + 'Link'].a = v + colors['faintLink'].a = v colors['panelFaint'].a = v + colors['lightBgFaintText'].a = v + colors['lightBgFaintLink'].a = v } if (k === 'bg') { colors['lightBg'].a = v diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js index 21bab1a2..808f67d5 100644 --- a/src/services/theme_data/theme_data.service.js +++ b/src/services/theme_data/theme_data.service.js @@ -12,6 +12,7 @@ export const LAYERS = { badge: null, // no transparency support fg: null, bg: 'underlay', + lightBg: 'bg', panel: 'bg', btn: 'bg', btnPanel: 'panel', @@ -57,6 +58,31 @@ export const SLOT_INHERITANCE = { depends: ['bg'], color: (mod, bg) => brightness(5 * mod, bg).rgb }, + lightBgFaintText: { + depends: ['faint'], + layer: 'lightBg', + textColor: true + }, + lightBgFaintLink: { + depends: ['faintLink'], + layer: 'lightBg', + textColor: 'preserve' + }, + lightBgText: { + depends: ['text'], + layer: 'lightBg', + textColor: true + }, + lightBgLink: { + depends: ['link'], + layer: 'lightBg', + textColor: 'preserve' + }, + lightBgIcon: { + depends: ['lightBg', 'lightBgText'], + color: (mod, bg, text) => mixrgb(bg, text) + }, + lightText: { depends: ['text'], color: (mod, text) => brightness(20 * mod, text).rgb -- cgit v1.2.3-70-g09d2 From 8d2f2866f6d32b4ada155d76e07910b92c218146 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 13 Jan 2020 01:56:29 +0200 Subject: computed colors support --- src/App.scss | 4 ++ src/components/style_switcher/style_switcher.js | 6 ++- src/components/style_switcher/style_switcher.vue | 16 +++++- src/i18n/en.json | 4 +- src/services/style_setter/style_setter.js | 6 ++- src/services/theme_data/theme_data.service.js | 69 +++++++++++++++++++++--- 6 files changed, 92 insertions(+), 13 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/App.scss b/src/App.scss index 7da3688a..ef139e88 100644 --- a/src/App.scss +++ b/src/App.scss @@ -99,6 +99,10 @@ button { &:active { box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset; box-shadow: var(--buttonPressedShadow); + color: $fallback--text; + color: var(--btnPressedText, $fallback--text); + background-color: $fallback--fg; + background-color: var(--btnPressed, $fallback--fg) } &:disabled { diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 49b34405..b84d2489 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -42,7 +42,7 @@ const v1OnlyNames = [ ].map(_ => _ + 'ColorLocal') const colorConvert = (color) => { - if (color === 'transparent') { + if (color.startsWith('--') || color === 'transparent') { return color } else { return hex2rgb(color) @@ -409,7 +409,9 @@ export default { } keys.forEach(key => { - this[key + 'ColorLocal'] = rgb2hex(colors[key]) + const color = colors[key] + const hex = rgb2hex(colors[key]) + this[key + 'ColorLocal'] = hex === '#aN' ? color : hex }) } diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index c1ec7c9a..f2fdfea2 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -347,6 +347,20 @@ :label="$t('settings.text')" /> +

{{ $t('settings.style.advanced_colors.pressed') }}

+ + +

{{ $t('settings.style.advanced_colors.borders') }}

@@ -433,7 +447,7 @@ { // TODO: hack to keep rest of the code from complaining value = '#FF00FF' } - acc[k] = hex2rgb(value) + if (!value || value.startsWith('--')) { + acc[k] = value + } else { + acc[k] = hex2rgb(value) + } } return acc }, {}) diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js index 808f67d5..221c3b48 100644 --- a/src/services/theme_data/theme_data.service.js +++ b/src/services/theme_data/theme_data.service.js @@ -40,12 +40,27 @@ export const DEFAULT_OPACITY = { } export const SLOT_INHERITANCE = { - bg: null, - fg: null, - text: null, + bg: { + depends: [], + priority: 1 + }, + fg: { + depends: [], + priority: 1 + }, + text: { + depends: [], + priority: 1 + }, underlay: '#000000', - link: '--accent', - accent: '--link', + link: { + depends: ['accent'], + priority: 1 + }, + accent: { + depends: ['link'], + priority: 1 + }, faint: '--text', faintLink: '--link', @@ -170,6 +185,26 @@ export const SLOT_INHERITANCE = { textColor: true }, + btnPressed: '--btn', + btnPressedText: { + depends: ['btnText'], + layer: 'btn', + variant: 'btnPressed', + textColor: true + }, + btnPressedPanelText: { + depends: ['btnPanelText'], + layer: 'btnPanel', + variant: 'btnPressed', + textColor: true + }, + btnPressedTopBarText: { + depends: ['btnTopBarText'], + layer: 'btnTopBar', + variant: 'btnPressed', + textColor: true + }, + // Input fields input: '--fg', inputText: { @@ -308,12 +343,30 @@ export const topoSort = ( return output } -export const SLOT_ORDERED = topoSort(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 }), {}) +) + +console.log(SLOT_ORDERED) export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.reduce((acc, key) => { const value = SLOT_INHERITANCE[key] - if (sourceColors[key]) { - return { ...acc, [key]: { ...sourceColors[key] } } + const sourceColor = sourceColors[key] + if (sourceColor) { + let targetColor = sourceColor + if (typeof sourceColor === 'string' && sourceColor.startsWith('--')) { + const [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim()) + const variableSlot = variable.substring(2) + targetColor = acc[variableSlot] || sourceColors[variableSlot] + if (modifier) { + console.log(targetColor, acc, variableSlot) + targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb + } + console.log(targetColor, acc, variableSlot) + } + return { ...acc, [key]: { ...targetColor } } } else if (typeof value === 'string' && value.startsWith('#')) { return { ...acc, [key]: convert(value).rgb } } else { -- cgit v1.2.3-70-g09d2 From 29a0b4a593219a54c01faa982be4752bcddfc7d0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 13 Jan 2020 02:08:39 +0200 Subject: fix shadow and opacity --- src/components/shadow_control/shadow_control.js | 3 ++- src/components/style_switcher/style_switcher.js | 2 +- src/services/style_setter/style_setter.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js index 7e82b9c0..44e4a22f 100644 --- a/src/components/shadow_control/shadow_control.js +++ b/src/components/shadow_control/shadow_control.js @@ -1,6 +1,7 @@ import ColorInput from '../color_input/color_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' -import { hex2rgb, getCssShadow } from '../../services/color_convert/color_convert.js' +import { getCssShadow } from '../../services/style_setter/style_setter.js' +import { hex2rgb } from '../../services/color_convert/color_convert.js' export default { // 'Value' and 'Fallback' can be undefined, but if they are diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 34826f9f..98c2cbc5 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -117,7 +117,7 @@ export default { currentOpacity () { return Object.keys(DEFAULT_OPACITY) .map(key => [key, this[key + 'OpacityLocal']]) - .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}) + .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) }, currentRadii () { return { diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 61612cf4..c1a25101 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -76,7 +76,7 @@ export const applyTheme = (input, commit) => { commit('setOption', { name: 'colors', value: theme.colors }) } -const getCssShadow = (input, usesDropShadow) => { +export const getCssShadow = (input, usesDropShadow) => { if (input.length === 0) { return 'none' } -- cgit v1.2.3-70-g09d2 From f16ec75c7011fa7e6d5deb7763553b2c70d9a86e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 16 Jan 2020 20:53:05 +0200 Subject: opacity handling --- src/components/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(-) (limited to 'src/services/style_setter/style_setter.js') 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: {} }) -- cgit v1.2.3-70-g09d2 From f77d675434ad7238e36e712ed69d01bc3233b156 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Fri, 17 Jan 2020 00:27:46 +0200 Subject: optimized theme loading so that it wouldn't wait until ALL themes are loaded to select one by default --- src/components/style_switcher/style_switcher.js | 23 +++++++- src/services/style_setter/style_setter.js | 75 ++++++++++++------------- 2 files changed, 57 insertions(+), 41 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 03cbb2a1..52ece3a1 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -96,9 +96,26 @@ export default { created () { const self = this - getThemes().then((themesComplete) => { - self.availableStyles = themesComplete - }) + getThemes() + .then((promises) => { + return Promise.all( + Object.entries(promises) + .map(([k, v]) => v.then(res => [k, res])) + ) + }) + .then(themes => themes.reduce((acc, [k, v]) => { + if (v) { + return { + ...acc, + [k]: v + } + } else { + return acc + } + }, {})) + .then((themesComplete) => { + self.availableStyles = themesComplete + }) }, mounted () { this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme) diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 9237a8dc..872dd393 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -336,25 +336,23 @@ export const getThemes = () => { return window.fetch('/static/styles.json') .then((data) => data.json()) .then((themes) => { - return Promise.all(Object.entries(themes).map(([k, v]) => { + return Object.entries(themes).map(([k, v]) => { + let promise = null if (typeof v === 'object') { - return Promise.resolve([k, v]) + promise = Promise.resolve(v) } else if (typeof v === 'string') { - return window.fetch(v) + promise = window.fetch(v) .then((data) => data.json()) - .then((theme) => { - return [k, theme] - }) .catch((e) => { console.error(e) - return [] + return null }) } - })) + return [k, promise] + }) }) .then((promises) => { return promises - .filter(([k, v]) => v) .reduce((acc, [k, v]) => { acc[k] = v return acc @@ -363,33 +361,34 @@ export const getThemes = () => { } 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 bg = hex2rgb(theme[1]) - const fg = hex2rgb(theme[2]) - const text = hex2rgb(theme[3]) - const link = hex2rgb(theme[4]) - - 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, fg, text, link, cRed, cBlue, cGreen, cOrange } - } - - // This is a hack, this function is only called during initial load. - // We want to cancel loading the theme from config.json if we're already - // loading a theme from the persisted state. - // Needed some way of dealing with the async way of things. - // load config -> set preset -> wait for styles.json to load -> - // load persisted state -> set colors -> styles.json loaded -> set colors - if (!window.themeLoaded) { - applyTheme(data, commit) - } - }) + return getThemes() + .then((themes) => console.log(themes) || themes[val] ? themes[val] : themes['pleroma-dark']) + .then((theme) => { + const isV1 = Array.isArray(theme) + const data = isV1 ? {} : theme.theme + + if (isV1) { + const bg = hex2rgb(theme[1]) + const fg = hex2rgb(theme[2]) + const text = hex2rgb(theme[3]) + const link = hex2rgb(theme[4]) + + 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, fg, text, link, cRed, cBlue, cGreen, cOrange } + } + + // This is a hack, this function is only called during initial load. + // We want to cancel loading the theme from config.json if we're already + // loading a theme from the persisted state. + // Needed some way of dealing with the async way of things. + // load config -> set preset -> wait for styles.json to load -> + // load persisted state -> set colors -> styles.json loaded -> set colors + if (!window.themeLoaded) { + applyTheme(data, commit) + } + }) } -- cgit v1.2.3-70-g09d2 From e4033c85e2066d8a575a1cd8e5a59bb685a3adf0 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 20 Jan 2020 00:34:49 +0200 Subject: removed console logs --- src/components/shadow_control/shadow_control.js | 15 ++ src/components/shadow_control/shadow_control.vue | 2 + src/components/style_switcher/style_switcher.js | 49 ++++--- src/components/style_switcher/style_switcher.vue | 2 +- src/services/style_setter/style_setter.js | 178 +++++++++++++---------- src/services/theme_data/theme_data.service.js | 28 +++- 6 files changed, 166 insertions(+), 108 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js index 44e4a22f..653d5a9b 100644 --- a/src/components/shadow_control/shadow_control.js +++ b/src/components/shadow_control/shadow_control.js @@ -61,6 +61,21 @@ export default { } } }, + currentFallback () { + if (this.ready && this.fallback.length > 0) { + return this.fallback[this.selectedId] + } else { + return { + x: 0, + y: 0, + blur: 0, + spread: 0, + inset: false, + color: '#000000', + alpha: 1 + } + } + }, moveUpValid () { return this.ready && this.selectedId > 0 }, diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue index de8a42d1..efbb980e 100644 --- a/src/components/shadow_control/shadow_control.vue +++ b/src/components/shadow_control/shadow_control.vue @@ -191,6 +191,8 @@ v-model="selected.color" :disabled="!present" :label="$t('settings.style.common.color')" + :fallback="currentFallback.color" + :showOptionalTickbox="false" name="shadow" /> ({ ...acc, [ key + 'ColorLocal' ]: val }), {}), ...Object.keys(OPACITIES) - .map(key => console.log(key) || [key, '']) + .map(key => [key, '']) .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}), shadowSelected: undefined, @@ -134,7 +135,7 @@ export default { }, currentOpacity () { return Object.keys(OPACITIES) - .map(key => console.log(key) || [key, this[key + 'OpacityLocal']]) + .map(key => [key, this[key + 'OpacityLocal']]) .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {}) }, currentRadii () { @@ -224,7 +225,7 @@ export default { ].join(';') }, shadowsAvailable () { - return Object.keys(this.previewTheme.shadows).sort() + return Object.keys(DEFAULT_SHADOWS).sort() }, currentShadowOverriden: { get () { @@ -239,7 +240,7 @@ export default { } }, currentShadowFallback () { - return this.previewTheme.shadows[this.shadowSelected] + return (this.previewTheme.shadows || {})[this.shadowSelected] }, currentShadow: { get () { @@ -314,6 +315,17 @@ export default { } }) }, + updatePreviewColorsAndShadows () { + this.previewColors = generateColors({ + opacity: this.currentOpacity, + colors: this.currentColors + }) + this.previewShadows = generateShadows( + { shadows: this.shadowsLocal }, + this.previewColors.theme.colors, + this.previewColors.mod + ) + }, onImport (parsed) { if (parsed._pleroma_theme_version === 1) { this.normalizeLocalState(parsed, 1) @@ -435,6 +447,14 @@ export default { }) } + if (opacity && !this.keepOpacity) { + this.clearOpacity() + Object.entries(opacity).forEach(([k, v]) => { + if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return + this[k + 'OpacityLocal'] = v + }) + } + if (!this.keepRoundness) { this.clearRoundness() Object.entries(radii).forEach(([k, v]) => { @@ -454,14 +474,6 @@ export default { this.clearFonts() this.fontsLocal = fonts } - - if (opacity && !this.keepOpacity) { - this.clearOpacity() - Object.entries(opacity).forEach(([k, v]) => { - if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return - this[k + 'OpacityLocal'] = v - }) - } } }, watch: { @@ -476,8 +488,9 @@ export default { }, shadowsLocal: { handler () { + if (Object.getOwnPropertyNames(this.previewColors).length === 1) return try { - this.previewShadows = generateShadows({ shadows: this.shadowsLocal }) + this.updatePreviewColorsAndShadows() this.shadowsInvalid = false } catch (e) { this.shadowsInvalid = true @@ -500,10 +513,7 @@ export default { }, currentColors () { try { - this.previewColors = generateColors({ - opacity: this.currentOpacity, - colors: this.currentColors - }) + this.updatePreviewColorsAndShadows() this.colorsInvalid = false } catch (e) { this.colorsInvalid = true @@ -512,10 +522,7 @@ export default { }, currentOpacity () { try { - this.previewColors = generateColors({ - opacity: this.currentOpacity, - colors: this.currentColors - }) + this.updatePreviewColorsAndShadows() } catch (e) { console.warn(e) } diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index c8c02b8d..287d31b7 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -691,7 +691,7 @@ {{ $t('settings.style.switcher.clear_all') }}
- { theme: { colors: htmlColors.solid, opacity - } + }, + mod } } @@ -211,83 +212,99 @@ export const generateFonts = (input) => { } } -export const generateShadows = (input) => { - const border = (top, shadow) => ({ - x: 0, - y: top ? 1 : -1, - blur: 0, +const border = (top, shadow) => ({ + x: 0, + y: top ? 1 : -1, + blur: 0, + spread: 0, + color: shadow ? '#000000' : '#FFFFFF', + alpha: 0.2, + inset: true +}) +const buttonInsetFakeBorders = [border(true, false), border(false, true)] +const inputInsetFakeBorders = [border(true, true), border(false, false)] +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--faint', + alpha: 1 +} + +export const DEFAULT_SHADOWS = { + panel: [{ + x: 1, + y: 1, + blur: 4, spread: 0, - color: shadow ? '#000000' : '#FFFFFF', - alpha: 0.2, - inset: true - }) - const buttonInsetFakeBorders = [border(true, false), border(false, true)] - const inputInsetFakeBorders = [border(true, true), border(false, false)] - const hoverGlow = { + color: '#000000', + alpha: 0.6 + }], + topBar: [{ x: 0, y: 0, blur: 4, spread: 0, - color: '--faint', + color: '#000000', + alpha: 0.6 + }], + popup: [{ + x: 2, + y: 2, + blur: 3, + spread: 0, + color: '#000000', + alpha: 0.5 + }], + avatar: [{ + x: 0, + y: 1, + blur: 8, + spread: 0, + color: '#000000', + alpha: 0.7 + }], + avatarStatus: [], + panelHeader: [], + button: [{ + x: 0, + y: 0, + blur: 2, + spread: 0, + color: '#000000', alpha: 1 - } - - const shadows = { - panel: [{ - x: 1, - y: 1, - blur: 4, - spread: 0, - color: '#000000', - alpha: 0.6 - }], - topBar: [{ - x: 0, - y: 0, - blur: 4, - spread: 0, - color: '#000000', - alpha: 0.6 - }], - popup: [{ - x: 2, - y: 2, - blur: 3, - spread: 0, - color: '#000000', - alpha: 0.5 - }], - avatar: [{ - x: 0, - y: 1, - blur: 8, - spread: 0, - color: '#000000', - alpha: 0.7 - }], - avatarStatus: [], - panelHeader: [], - button: [{ - x: 0, - y: 0, - blur: 2, - spread: 0, - color: '#000000', - alpha: 1 - }, ...buttonInsetFakeBorders], - buttonHover: [hoverGlow, ...buttonInsetFakeBorders], - buttonPressed: [hoverGlow, ...inputInsetFakeBorders], - input: [...inputInsetFakeBorders, { - x: 0, - y: 0, - blur: 2, - inset: true, - spread: 0, - color: '#000000', - alpha: 1 - }], + }, ...buttonInsetFakeBorders], + buttonHover: [hoverGlow, ...buttonInsetFakeBorders], + buttonPressed: [hoverGlow, ...inputInsetFakeBorders], + input: [...inputInsetFakeBorders, { + x: 0, + y: 0, + blur: 2, + inset: true, + spread: 0, + color: '#000000', + alpha: 1 + }] +} +export const generateShadows = (input, colors, mod) => { + const shadows = Object.entries({ + ...DEFAULT_SHADOWS, ...(input.shadows || {}) - } + }).reduce((shadowsAcc, [slotName, shadowdefs]) => { + const newShadow = shadowdefs.reduce((shadowAcc, def) => [ + ...shadowAcc, + { + ...def, + color: rgb2hex(computeDynamicColor( + def.color, + (variableSlot) => convert(colors[variableSlot]).rgb, + mod + )) + } + ], []) + return { ...shadowsAcc, [slotName]: newShadow } + }, {}) return { rules: { @@ -325,12 +342,15 @@ export const composePreset = (colors, radii, shadows, fonts) => { } } -export const generatePreset = (input) => composePreset( - generateColors(input), - generateRadii(input), - generateShadows(input), - generateFonts(input) -) +export const generatePreset = (input) => { + const colors = generateColors(input) + return composePreset( + colors, + generateRadii(input), + generateShadows(input, colors.theme.colors, colors.mod), + generateFonts(input) + ) +} export const getThemes = () => { return window.fetch('/static/styles.json') @@ -362,7 +382,7 @@ export const getThemes = () => { export const setPreset = (val, commit) => { return getThemes() - .then((themes) => console.log(themes) || themes[val] ? themes[val] : themes['pleroma-dark']) + .then((themes) => themes[val] ? themes[val] : themes['pleroma-dark']) .then((theme) => { const isV1 = Array.isArray(theme) const data = isV1 ? {} : theme.theme diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js index 9f010fdf..e4456b29 100644 --- a/src/services/theme_data/theme_data.service.js +++ b/src/services/theme_data/theme_data.service.js @@ -697,6 +697,22 @@ export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k, v]) = } }, {}) +/** + * 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. @@ -728,13 +744,11 @@ export const getColors = (sourceColors, sourceOpacity, mod) => SLOT_ORDERED.redu 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 = colors[variableSlot] || sourceColors[variableSlot] - if (modifier) { - targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb - } + targetColor = computeDynamicColor( + sourceColor, + variableSlot => colors[variableSlot] || sourceColors[variableSlot], + mod + ) } else if (typeof sourceColor === 'string' && sourceColor.startsWith('#')) { targetColor = convert(targetColor).rgb } -- cgit v1.2.3-70-g09d2 From 6e1c538e4182263a75fb65b0f3c5d1ad9de94541 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 20 Jan 2020 01:31:54 +0200 Subject: multiple fixes to make style switcher not die. Made shadows work, incuding compatibility --- src/components/style_switcher/style_switcher.js | 121 +++++++++++++----------- src/services/style_setter/style_setter.js | 23 ++++- src/services/theme_data/theme_data.service.js | 9 +- static/themes/breezy-dark.json | 2 +- static/themes/breezy-light.json | 2 +- 5 files changed, 96 insertions(+), 61 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 2984873f..2a1e439b 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -11,7 +11,8 @@ import { generateRadii, generateFonts, composePreset, - getThemes + getThemes, + shadows2to3 } from '../../services/style_setter/style_setter.js' import { CURRENT_VERSION, @@ -159,62 +160,66 @@ export default { }, // This needs optimization maybe previewContrast () { - if (!this.previewTheme.colors.bg) return {} - const colors = this.previewTheme.colors - const opacity = this.previewTheme.opacity - if (!colors.bg) return {} - const hints = (ratio) => ({ - text: ratio.toPrecision(3) + ':1', - // AA level, AAA level - aa: ratio >= 4.5, - aaa: ratio >= 7, - // same but for 18pt+ texts - laa: ratio >= 3, - laaa: ratio >= 4.5 - }) - const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {}) - - const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => { - const slotIsBaseText = key === 'text' || key === 'link' - const slotIsText = slotIsBaseText || ( - typeof value === 'object' && value !== null && value.textColor - ) - if (!slotIsText) return acc - const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value - const background = variant || layer - const opacitySlot = getOpacitySlot(SLOT_INHERITANCE[background]) - const textColors = [ - key, - ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : []) - ] - - const layers = getLayers( - layer, - variant || layer, - opacitySlot, - colorsConverted, - opacity - ) + try { + if (!this.previewTheme.colors.bg) return {} + const colors = this.previewTheme.colors + const opacity = this.previewTheme.opacity + if (!colors.bg) return {} + const hints = (ratio) => ({ + text: ratio.toPrecision(3) + ':1', + // AA level, AAA level + aa: ratio >= 4.5, + aaa: ratio >= 7, + // same but for 18pt+ texts + laa: ratio >= 3, + laaa: ratio >= 4.5 + }) + const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {}) + + const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => { + const slotIsBaseText = key === 'text' || key === 'link' + const slotIsText = slotIsBaseText || ( + typeof value === 'object' && value !== null && value.textColor + ) + if (!slotIsText) return acc + const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value + const background = variant || layer + const opacitySlot = getOpacitySlot(SLOT_INHERITANCE[background]) + const textColors = [ + key, + ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : []) + ] + + const layers = getLayers( + layer, + variant || layer, + opacitySlot, + colorsConverted, + opacity + ) - return { - ...acc, - ...textColors.reduce((acc, textColorKey) => { - const newKey = slotIsBaseText - ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1) - : textColorKey - return { - ...acc, - [newKey]: getContrastRatioLayers( - colorsConverted[textColorKey], - layers, - colorsConverted[textColorKey] - ) - } - }, {}) - } - }, {}) + return { + ...acc, + ...textColors.reduce((acc, textColorKey) => { + const newKey = slotIsBaseText + ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1) + : textColorKey + return { + ...acc, + [newKey]: getContrastRatioLayers( + colorsConverted[textColorKey], + layers, + colorsConverted[textColorKey] + ) + } + }, {}) + } + }, {}) - return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {}) + return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {}) + } catch (e) { + console.warn('Failure computing contrasts', e) + } }, previewRules () { if (!this.preview.rules) return '' @@ -466,7 +471,11 @@ export default { if (!this.keepShadows) { this.clearShadows() - this.shadowsLocal = shadows + if (version === 2) { + this.shadowsLocal = shadows2to3(shadows) + } else { + this.shadowsLocal = shadows + } this.shadowSelected = this.shadowsAvailable[0] } diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 74af190c..ee264c49 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -291,8 +291,8 @@ export const generateShadows = (input, colors, mod) => { const shadows = Object.entries({ ...DEFAULT_SHADOWS, ...(input.shadows || {}) - }).reduce((shadowsAcc, [slotName, shadowdefs]) => { - const newShadow = shadowdefs.reduce((shadowAcc, def) => [ + }).reduce((shadowsAcc, [slotName, shadowDefs]) => { + const newShadow = shadowDefs.reduce((shadowAcc, def) => [ ...shadowAcc, { ...def, @@ -380,6 +380,25 @@ export const getThemes = () => { }) } +/** + * This handles compatibility issues when importing v2 theme's shadows to current format + * + * Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables + */ +export const shadows2to3 = (shadows) => { + return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => { + const isDynamic = ({ color }) => console.log(color) || color.startsWith('--') + const newShadow = shadowDefs.reduce((shadowAcc, def) => [ + ...shadowAcc, + { + ...def, + alpha: isDynamic(def) ? 1 : def.alpha + } + ], []) + return { ...shadowsAcc, [slotName]: newShadow } + }, {}) +} + export const setPreset = (val, commit) => { return getThemes() .then((themes) => themes[val] ? themes[val] : themes['pleroma-dark']) diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js index e4456b29..36837b44 100644 --- a/src/services/theme_data/theme_data.service.js +++ b/src/services/theme_data/theme_data.service.js @@ -663,7 +663,14 @@ 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 }), {}) -) +).sort((a, b) => { + const depsA = getDependencies(a, SLOT_INHERITANCE).length + const depsB = getDependencies(b, SLOT_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 +}) /** * Dictionary where keys are color slots and values are opacity associated diff --git a/static/themes/breezy-dark.json b/static/themes/breezy-dark.json index 97e81f3d..3aafda52 100644 --- a/static/themes/breezy-dark.json +++ b/static/themes/breezy-dark.json @@ -53,7 +53,7 @@ "blur": 0, "spread": "1", "color": "--accent", - "alpha": "0.3", + "alpha": "1", "inset": true }, { diff --git a/static/themes/breezy-light.json b/static/themes/breezy-light.json index fd307b80..df6e1a66 100644 --- a/static/themes/breezy-light.json +++ b/static/themes/breezy-light.json @@ -53,7 +53,7 @@ "blur": 0, "spread": "1", "color": "--accent", - "alpha": "0.3", + "alpha": "1", "inset": true }, { -- cgit v1.2.3-70-g09d2 From 9336140486f50159b935001b7ebadf3d9bda89ec Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Wed, 22 Jan 2020 00:37:19 +0200 Subject: massively improved initial theme loading code, added checks and warnings when loading theme files (import/localStorage/defaults) --- src/boot/after_store.js | 32 +++-- src/components/style_switcher/style_switcher.js | 153 ++++++++++++++++++++-- src/components/style_switcher/style_switcher.scss | 4 + src/components/style_switcher/style_switcher.vue | 79 +++++++---- src/i18n/en.json | 12 +- src/modules/config.js | 7 +- src/modules/instance.js | 17 ++- src/services/style_setter/style_setter.js | 27 ++-- 8 files changed, 259 insertions(+), 72 deletions(-) (limited to 'src/services/style_setter/style_setter.js') diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 228a0497..6c4f0e1b 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -5,6 +5,8 @@ import App from '../App.vue' import { windowWidth } from '../services/window_utils/window_utils' import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' +import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' +import { applyTheme } from '../services/style_setter/style_setter.js' const getStatusnetConfig = async ({ store }) => { try { @@ -261,7 +263,7 @@ const checkOAuthToken = async ({ store }) => { try { await store.dispatch('loginUser', store.getters.getUserToken()) } catch (e) { - console.log(e) + console.error(e) } } resolve() @@ -269,23 +271,29 @@ const checkOAuthToken = async ({ store }) => { } const afterStoreSetup = async ({ store, i18n }) => { - if (store.state.config.customTheme) { - // This is a hack to deal with async loading of config.json and themes - // See: style_setter.js, setPreset() - window.themeLoaded = true - store.dispatch('setOption', { - name: 'customTheme', - value: store.state.config.customTheme - }) - } - const width = windowWidth() store.dispatch('setMobileLayout', width <= 800) + await setConfig({ store }) + + const { customTheme, customThemeSource } = store.state.config + const { theme } = store.state.instance + const customThemePresent = customThemeSource || customTheme + + if (customThemePresent) { + if (customThemeSource && customThemeSource.version === CURRENT_VERSION) { + applyTheme(customThemeSource) + } else { + applyTheme(customTheme) + } + } else if (theme) { + // do nothing, it will load asynchronously + } else { + console.error('Failed to load any theme!') + } // Now we can try getting the server settings and logging in await Promise.all([ checkOAuthToken({ store }), - setConfig({ store }), getTOS({ store }), getInstancePanel({ store }), getStickers({ store }), diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 799646b1..0ef02f39 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -57,6 +57,8 @@ export default { return { availableStyles: [], selected: this.$store.getters.mergedConfig.theme, + themeWarning: undefined, + tempImportFile: undefined, previewShadows: {}, previewColors: {}, @@ -120,12 +122,62 @@ export default { }) }, mounted () { - this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme) + this.loadThemeFromLocalStorage() if (typeof this.shadowSelected === 'undefined') { this.shadowSelected = this.shadowsAvailable[0] } }, computed: { + themeWarningHelp () { + if (!this.themeWarning) return + const t = this.$t + const pre = 'settings.style.switcher.help.' + const { + origin, + themeEngineVersion, + type, + noActionsPossible + } = this.themeWarning + if (origin === 'file') { + // Loaded v2 theme from file + if (themeEngineVersion === 2 && type === 'wrong_version') { + return t(pre + 'v2_imported') + } + if (themeEngineVersion > CURRENT_VERSION) { + return t(pre + 'future_version_imported') + ' ' + + ( + noActionsPossible + ? t(pre + 'snapshot_missing') + : t(pre + 'snapshot_present') + ) + } + if (themeEngineVersion < CURRENT_VERSION) { + return t(pre + 'future_version_imported') + ' ' + + ( + noActionsPossible + ? t(pre + 'snapshot_missing') + : t(pre + 'snapshot_present') + ) + } + } else if (origin === 'localStorage') { + // FE upgraded from v2 + if (themeEngineVersion === 2) { + return 'upgraded_from_v2' + } + // Admin downgraded FE + if (themeEngineVersion > CURRENT_VERSION) { + return noActionsPossible + ? 'downgraded_theme' + : 'downgraded_theme_missing_snapshot' + } + // Admin upgraded FE + if (themeEngineVersion < CURRENT_VERSION) { + return noActionsPossible + ? 'upgraded_theme' + : 'upgraded_theme_missing_snapshot' + } + } + }, selectedVersion () { return Array.isArray(this.selected) ? 1 : 2 }, @@ -308,10 +360,96 @@ export default { Checkbox }, methods: { + loadTheme ( + { + theme, + source, + _pleroma_theme_version: fileVersion + }, + origin, + forceUseSource = false + ) { + if (!source && !theme) { + throw new Error('Can\'t load theme: empty') + } + const version = (origin === 'localstorage' && !theme.colors) + ? 'l1' + : fileVersion + const themeEngineVersion = (source || {}).themeEngineVersion || 2 + const versionsMatch = themeEngineVersion === CURRENT_VERSION + // Force loading of source if user requested it or if snapshot + // is unavailable + const forcedSourceLoad = (source && forceUseSource) || !theme + if (!versionsMatch && + !forcedSourceLoad && + version !== 'l1' && + origin !== 'defaults' + ) { + if (!theme) { + this.themeWarning = { + origin, + noActionsPossible: true, + themeEngineVersion, + type: 'no_snapshot_old_version' + } + } else if (!versionsMatch) { + this.themeWarning = { + origin, + noActionsPossible: !source, + themeEngineVersion, + type: 'wrong_version' + } + } + } + this.normalizeLocalState(theme, version, source, forcedSourceLoad) + }, + forceLoadLocalStorage () { + this.loadThemeFromLocalStorage(true) + }, + dismissWarning () { + this.themeWarning = undefined + this.tempImportFile = undefined + }, + forceLoad () { + const { origin } = this.themeWarning + switch (origin) { + case 'localstorage': + this.loadThemeFromLocalStorage(true) + break + case 'file': + this.onImport(this.tempImportFile, true) + break + } + }, + loadThemeFromLocalStorage (confirmLoadSource = false) { + const { + customTheme: theme, + customThemeSource: source + } = this.$store.getters.mergedConfig + if (!theme && !source) { + // Anon user or never touched themes + this.loadTheme( + this.$store.state.instance.themeData, + 'defaults', + confirmLoadSource + ) + } else { + this.loadTheme( + { theme, source }, + 'localStorage', + confirmLoadSource + ) + } + }, setCustomTheme () { this.$store.dispatch('setOption', { name: 'customTheme', + value: this.previewTheme + }) + this.$store.dispatch('setOption', { + name: 'customThemeSource', value: { + themeEngineVersion: CURRENT_VERSION, shadows: this.shadowsLocal, fonts: this.fontsLocal, opacity: this.currentOpacity, @@ -331,21 +469,16 @@ export default { this.previewColors.mod ) }, - onImport (parsed) { - if (parsed._pleroma_theme_version === 1) { - this.normalizeLocalState(parsed, 1) - } else if (parsed._pleroma_theme_version >= 2) { - this.normalizeLocalState(parsed.theme, 2, parsed.source) - } + onImport (parsed, forceSource = false) { + this.tempImportFile = parsed + this.loadTheme(parsed, 'file', forceSource) }, importValidator (parsed) { const version = parsed._pleroma_theme_version return version >= 1 || version <= 2 }, clearAll () { - const state = this.$store.getters.mergedConfig.customTheme - const version = state.colors ? 2 : 'l1' - this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version, this.$store.getters.mergedConfig.customThemeSource) + this.loadThemeFromLocalStorage() }, // Clears all the extra stuff when loading V1 theme diff --git a/src/components/style_switcher/style_switcher.scss b/src/components/style_switcher/style_switcher.scss index 987245a2..71d0f05e 100644 --- a/src/components/style_switcher/style_switcher.scss +++ b/src/components/style_switcher/style_switcher.scss @@ -1,5 +1,9 @@ @import '../../_variables.scss'; .style-switcher { + .theme-warning { + display: flex; + align-items: baseline; + } .preset-switcher { margin-right: 1em; } diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue index 287d31b7..61f8800a 100644 --- a/src/components/style_switcher/style_switcher.vue +++ b/src/components/style_switcher/style_switcher.vue @@ -1,31 +1,60 @@