Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85710795
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
18 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 2:22 AM (21 h, 30 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768649
Default Alt Text
(18 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment