Page MenuHomePhorge

No OneTemporary

Size
18 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 27efa23065..d4381202b9 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -1,366 +1,388 @@
-import { rgb2hex, hex2rgb, getContrastRatio } from '../../services/color_convert/color_convert.js'
+import { rgb2hex, hex2rgb, getContrastRatio, worstCase } from '../../services/color_convert/color_convert.js'
import ColorInput from '../color_input/color_input.vue'
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
import OpacityInput from '../opacity_input/opacity_input.vue'
import StyleSetter from '../../services/style_setter/style_setter.js'
export default {
data () {
return {
availableStyles: [],
selected: this.$store.state.config.theme,
invalidThemeImported: false,
textColorLocal: '',
linkColorLocal: '',
bgColorLocal: '',
bgOpacityLocal: undefined,
fgColorLocal: '',
fgTextColorLocal: undefined,
fgLinkColorLocal: undefined,
btnColorLocal: undefined,
btnTextColorLocal: undefined,
btnOpacityLocal: undefined,
inputColorLocal: undefined,
inputTextColorLocal: undefined,
inputOpacityLocal: undefined,
panelColorLocal: undefined,
panelTextColorLocal: undefined,
panelFaintColorLocal: undefined,
panelOpacityLocal: undefined,
topBarColorLocal: undefined,
topBarTextColorLocal: undefined,
topBarLinkColorLocal: undefined,
alertOpacityLocal: undefined,
borderColorLocal: undefined,
borderOpacityLocal: undefined,
faintColorLocal: undefined,
faintOpacityLocal: undefined,
faintLinkColorLocal: undefined,
cRedColorLocal: '',
cBlueColorLocal: '',
cGreenColorLocal: '',
cOrangeColorLocal: '',
btnRadiusLocal: '',
inputRadiusLocal: '',
panelRadiusLocal: '',
avatarRadiusLocal: '',
avatarAltRadiusLocal: '',
attachmentRadiusLocal: '',
tooltipRadiusLocal: ''
}
},
created () {
const self = this
window.fetch('/static/styles.json')
.then((data) => data.json())
.then((themes) => {
self.availableStyles = themes
})
},
mounted () {
this.normalizeLocalState(this.$store.state.config.customTheme)
},
computed: {
selectedVersion () {
return Array.isArray(this.selected) ? 1 : 2
},
currentTheme () {
return {
colors: {
bg: this.bgColorLocal,
text: this.textColorLocal,
link: this.linkColorLocal,
fg: this.fgColorLocal,
fgText: this.fgTextColorLocal,
fgLink: this.fgLinkColorLocal,
panel: this.panelColorLocal,
panelText: this.panelTextColorLocal,
panelFaint: this.panelFaintColorLocal,
input: this.inputColorLocal,
inputText: this.inputTextColorLocal,
topBar: this.topBarColorLocal,
topBarText: this.topBarTextColorLocal,
topBarLink: this.topBarLinkColorLocal,
btn: this.btnColorLocal,
btnText: this.btnTextColorLocal,
faint: this.faintColorLocal,
faintLink: this.faintLinkColorLocal,
border: this.borderColorLocal,
cRed: this.cRedColorLocal,
cBlue: this.cBlueColorLocal,
cGreen: this.cGreenColorLocal,
cOrange: this.cOrangeColorLocal
},
opacity: {
bg: this.bgOpacityLocal,
btn: this.btnOpacityLocal,
input: this.inputOpacityLocal,
panel: this.panelOpacityLocal,
topBar: this.topBarOpacityLocal,
border: this.borderOpacityLocal,
faint: this.faintOpacityLocal
},
radii: {
btnRadius: this.btnRadiusLocal,
inputRadius: this.inputRadiusLocal,
panelRadius: this.panelRadiusLocal,
avatarRadius: this.avatarRadiusLocal,
avatarAltRadius: this.avatarAltRadiusLocal,
tooltipRadius: this.tooltipRadiusLocal,
attachmentRadius: this.attachmentRadiusLocal
},
}
},
preview () {
try {
if (!this.currentTheme.colors.bg) {
return {}
}
return StyleSetter.generatePreset(this.currentTheme)
} catch (e) {
console.error('CATCH')
console.error(e)
return {}
}
},
previewTheme () {
- if (!this.preview.theme) return { colors: {}, opacity: {}, radii: {}, contrast: {} }
+ if (!this.preview.theme) return { colors: {}, opacity: {}, radii: {} }
return this.preview.theme
},
previewContrast () {
if (!this.previewTheme.colors) return {}
const colors = this.previewTheme.colors
+ const opacity = this.previewTheme.opacity
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
})
+ // fgsfds :DDDD
+ const fgs = {
+ text: hex2rgb(colors.text),
+ panelText: hex2rgb(colors.panelText),
+ btnText: hex2rgb(colors.btnText),
+ topBarText: hex2rgb(colors.topBarText),
+
+ link: hex2rgb(colors.link),
+ topBarLink: hex2rgb(colors.topBarLink),
+ }
+
+ const bgs = {
+ bg: hex2rgb(colors.bg),
+ btn: hex2rgb(colors.btn),
+ panel: hex2rgb(colors.panel),
+ topBar: hex2rgb(colors.topBar)
+ }
+
const ratios = {
- bgText: getContrastRatio(hex2rgb(colors.bg), hex2rgb(colors.text)),
- bgLink: getContrastRatio(hex2rgb(colors.bg), hex2rgb(colors.link)),
+ bgText: getContrastRatio(worstCase(bgs.bg, opacity.bg, fgs.text), fgs.text),
+ bgLink: getContrastRatio(worstCase(bgs.bg, opacity.bg, fgs.link), fgs.link),
+
+ // User Profile
+ tintText: getContrastRatio(worstCase(bgs.bg, 0.5, fgs.panelText), fgs.text),
- panelText: getContrastRatio(hex2rgb(colors.panel), hex2rgb(colors.panelText)),
+ panelText: getContrastRatio(worstCase(bgs.panel, opacity.panel, fgs.panelText), fgs.panelText),
- btnText: getContrastRatio(hex2rgb(colors.btn), hex2rgb(colors.btnText)),
+ btnText: getContrastRatio(worstCase(bgs.btn, opacity.btn, fgs.btnText), fgs.btnText),
- topBarText: getContrastRatio(hex2rgb(colors.topBar), hex2rgb(colors.topBarText)),
- topBarLink: getContrastRatio(hex2rgb(colors.topBar), hex2rgb(colors.topBarLink)),
+ topBarText: getContrastRatio(worstCase(bgs.topBar, opacity.topBar, fgs.topBarText), fgs.topBarText),
+ topBarLink: getContrastRatio(worstCase(bgs.topBar, opacity.topBar, fgs.topBarLink), fgs.topBarLink)
}
return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
},
previewRules () {
if (!this.preview.colorRules) return ''
return [this.preview.colorRules, this.preview.radiiRules, 'color: var(--text)'].join(';')
}
},
components: {
ColorInput,
OpacityInput,
ContrastRatio
},
methods: {
exportCurrentTheme () {
const stringified = JSON.stringify({
// To separate from other random JSON files and possible future theme formats
_pleroma_theme_version: 2,
theme: this.currentTheme
}, 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')
e.setAttribute('download', 'pleroma_theme.json')
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
e.style.display = 'none'
document.body.appendChild(e)
e.click()
document.body.removeChild(e)
},
importTheme () {
this.invalidThemeImported = false
const filePicker = document.createElement('input')
filePicker.setAttribute('type', 'file')
filePicker.setAttribute('accept', '.json')
filePicker.addEventListener('change', event => {
if (event.target.files[0]) {
// eslint-disable-next-line no-undef
const reader = new FileReader()
reader.onload = ({target}) => {
try {
const parsed = JSON.parse(target.result)
if (parsed._pleroma_theme_version === 1) {
this.normalizeLocalState(parsed, 1)
} else if (parsed._pleroma_theme_version === 2) {
this.normalizeLocalState(parsed.theme, 2)
} else {
// A theme from the future, spooky
this.invalidThemeImported = true
}
} catch (e) {
// This will happen both if there is a JSON syntax error or the theme is missing components
this.invalidThemeImported = true
}
}
reader.readAsText(event.target.files[0])
}
})
document.body.appendChild(filePicker)
filePicker.click()
document.body.removeChild(filePicker)
},
setCustomTheme () {
if (!this.bgColorLocal && !this.btnColorLocal && !this.linkColorLocal) {
// reset to picked themes
}
const rgb = (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),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null
}
this.$store.dispatch('setOption', {
name: 'customTheme',
value: this.currentTheme
})
},
clearV1 () {
this.bgOpacityLocal = undefined
this.fgOpacityLocal = undefined
this.fgLinkColorLocal = undefined
this.btnColorLocal = undefined
this.btnTextColorLocal = undefined
this.btnOpacityLocal = undefined
this.inputColorLocal = undefined
this.inputTextColorLocal = undefined
this.inputOpacityLocal = undefined
this.panelColorLocal = undefined
this.panelTextColorLocal = undefined
this.panelFaintColorLocal = undefined
this.panelOpacityLocal = undefined
this.topBarColorLocal = undefined
this.topBarTextColorLocal = undefined
this.topBarLinkColorLocal = undefined
this.topBarOpacityLocal = undefined
this.borderColorLocal = undefined
this.borderOpacityLocal = undefined
this.faintColorLocal = undefined
this.faintOpacityLocal = undefined
this.faintLinkColorLocal = undefined
},
/**
* This applies stored theme data onto form.
* @param {Object} input - input data
* @param {Number} version - version of data. 0 means try to guess based on data.
*/
normalizeLocalState (input, version = 0) {
const colors = input.colors || input
const radii = input.radii || input
const opacity = input.opacity || input
if (version === 0) {
if (input.version) version = input.version
// Old v1 naming: fg is text, btn is foreground
if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
version = 1
}
// New v2 naming: text is text, fg is foreground
if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
version = 2
}
}
// Stuff that differs between V1 and V2
if (version === 1) {
this.fgColorLocal = rgb2hex(colors.btn)
this.textColorLocal = rgb2hex(colors.fg)
}
const keys = new Set(version !== 1 ? Object.keys(colors) : [])
if (version === 1) {
// V1 ignores the rest
this.clearV1()
keys
.add('bg')
.add('link')
.add('cRed')
.add('cBlue')
.add('cGreen')
.add('cOrange')
}
keys.forEach(key => {
this[key + 'ColorLocal'] = rgb2hex(colors[key])
})
// TODO optimize this
this.btnRadiusLocal = radii.btnRadius || 4
this.inputRadiusLocal = radii.inputRadius || 4
this.panelRadiusLocal = radii.panelRadius || 10
this.avatarRadiusLocal = radii.avatarRadius || 5
this.avatarAltRadiusLocal = radii.avatarAltRadius || 50
this.tooltipRadiusLocal = radii.tooltipRadius || 2
this.attachmentRadiusLocal = radii.attachmentRadius || 5
Object.entries(opacity).forEach(([k, v]) => {
if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
this[k + 'OpacityLocal'] = v
})
}
},
watch: {
selected () {
if (this.selectedVersion === 1) {
this.clearV1()
this.bgColorLocal = this.selected[1]
this.fgColorLocal = this.selected[2]
this.textColorLocal = this.selected[3]
this.linkColorLocal = this.selected[4]
this.redColorLocal = this.selected[5]
this.greenColorLocal = this.selected[6]
this.blueColorLocal = this.selected[7]
this.orangeColorLocal = this.selected[8]
}
}
}
}
diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js
index 31ee3a6b7c..0acc7e7ce8 100644
--- a/src/services/color_convert/color_convert.js
+++ b/src/services/color_convert/color_convert.js
@@ -1,88 +1,140 @@
import { map } from 'lodash'
const rgb2hex = (r, g, b) => {
if (r === null || typeof r === 'undefined') {
return undefined
}
if (r[0] === '#') {
return r
}
if (typeof r === 'object') {
({ r, g, b } = r)
}
[r, g, b] = map([r, g, b], (val) => {
val = Math.ceil(val)
val = val < 0 ? 0 : val
val = val > 255 ? 255 : val
return val
})
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`
}
-// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
-// https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml
-// https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
-const c2linear = (b) => {
+/**
+ * Converts 8-bit RGB component into linear component
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml
+ * https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
+ *
+ * @param {Number} bit - color component [0..255]
+ * @returns {Number} linear component [0..1]
+ */
+const c2linear = (bit) => {
// W3C gives 0.03928 while wikipedia states 0.04045
// what those magical numbers mean - I don't know.
// something about gamma-correction, i suppose.
// Sticking with W3C example.
- const c = b / 255
+ const c = bit / 255
if (c < 0.03928) {
return c / 12.92
} else {
return Math.pow((c + 0.055) / 1.055, 2.4)
}
}
+/**
+ * Converts sRGB into linear RGB
+ * @param {Object} srgb - sRGB color
+ * @returns {Object} linear rgb color
+ */
const srgbToLinear = (srgb) => {
return 'rgb'.split('').reduce((acc, c) => { acc[c] = c2linear(srgb[c]); return acc }, {})
}
-// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
-// https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml
+/**
+ * Calculates relative luminance for given color
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml
+ *
+ * @param {Object} srgb - sRGB color
+ * @returns {Number} relative luminance
+ */
const relativeLuminance = (srgb) => {
const {r, g, b} = srgbToLinear(srgb)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
}
-// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
+/**
+ * Generates color ratio between two colors. Order is unimporant
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
+ *
+ * @param {Object} a - sRGB color
+ * @param {Object} b - sRGB color
+ * @returns {Number} color ratio
+ */
const getContrastRatio = (a, b) => {
const la = relativeLuminance(a)
const lb = relativeLuminance(b)
const [l1, l2] = la > lb ? [la, lb] : [lb, la]
return (l1 + 0.05) / (l2 + 0.05)
}
+/**
+ * This generates what "worst case" color would look like for transparent
+ * segments. I.e. black with .2 alpha and pure-white background image
+ * could make white text unreadable
+ *
+ * @param {Object} srgb - transparent color
+ * @param {Number} alpha - color's opacity/alpha channel
+ * @param {Boolean} white - use white "background" if true, black otherwise
+ * @returns {Object} sRGB of resulting color
+ */
+const transparentWorstCase = (srgb, alpha, white = false) => {
+ const bg = 'rgb'.split('').reduce((acc, c) => { acc[c] = Number(white) * 255; return acc }, {})
+ return 'rgb'.split('').reduce((acc, c) => {
+ // Simplified https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
+ // for opaque bg and transparent fg
+ acc[c] = (srgb[c] * alpha + bg[c] * (1 - alpha))
+ return acc
+ }, {})
+}
+
+const worstCase = (bg, bga, text) => {
+ if (bga === 1 || typeof bga === 'undefined') return bg
+ // taken from https://github.com/toish/chromatism/blob/master/src/operations/contrastRatio.js
+ const blackWorse = ((text.r * 299) + (text.g * 587) + (text.b * 114)) / 1000 <= 128
+ return transparentWorstCase(bg, bga, !blackWorse)
+}
+
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),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null
}
const rgbstr2hex = (rgb) => {
if (rgb[0] === '#') {
return rgb
}
rgb = rgb.match(/\d+/g)
return `#${((Number(rgb[0]) << 16) + (Number(rgb[1]) << 8) + Number(rgb[2])).toString(16)}`
}
const mixrgb = (a, b) => {
return Object.keys(a).reduce((acc, k) => {
acc[k] = (a[k] + b[k]) / 2
return acc
}, {})
}
export {
rgb2hex,
hex2rgb,
mixrgb,
rgbstr2hex,
- getContrastRatio
+ getContrastRatio,
+ worstCase
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 19, 2:15 AM (18 h, 46 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768649
Default Alt Text
(18 KB)

Event Timeline