diff options
| author | Henry Jameson <me@hjkos.com> | 2024-02-21 12:23:10 +0200 |
|---|---|---|
| committer | Henry Jameson <me@hjkos.com> | 2024-02-21 12:23:10 +0200 |
| commit | da0db933d91c7658a33831ce167095073223285d (patch) | |
| tree | 80e096d461c59fc357f708fdb791610cc4b8a591 /src/services/theme_data | |
| parent | c3e6e7c61de8fcf9cdeeeac6479d8a00a9b47b3a (diff) | |
moved themes2 stuff into theme_data.service.js
Diffstat (limited to 'src/services/theme_data')
| -rw-r--r-- | src/services/theme_data/theme_data.service.js | 346 |
1 files changed, 345 insertions, 1 deletions
diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js index b376ef4d..6e477674 100644 --- a/src/services/theme_data/theme_data.service.js +++ b/src/services/theme_data/theme_data.service.js @@ -1,5 +1,5 @@ import { convert, brightness, contrastRatio } from 'chromatism' -import { alphaBlendLayers, getTextColor, relativeLuminance } from '../color_convert/color_convert.js' +import { rgb2hex, rgba2css, alphaBlendLayers, getTextColor, relativeLuminance, getCssColor } from '../color_convert/color_convert.js' import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js' /* @@ -407,3 +407,347 @@ export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({ } } }, { colors: {}, opacity: {} }) + +export const composePreset = (colors, radii, shadows, fonts) => { + return { + rules: { + ...shadows.rules, + ...colors.rules, + ...radii.rules, + ...fonts.rules + }, + theme: { + ...shadows.theme, + ...colors.theme, + ...radii.theme, + ...fonts.theme + } + } +} + +export const generatePreset = (input) => { + const colors = generateColors(input) + return composePreset( + colors, + generateRadii(input), + generateShadows(input, colors.theme.colors, colors.mod), + generateFonts(input) + ) +} + +export const getCssShadow = (input, usesDropShadow) => { + if (input.length === 0) { + return 'none' + } + + return input + .filter(_ => usesDropShadow ? _.inset : _) + .map((shad) => [ + shad.x, + shad.y, + shad.blur, + shad.spread + ].map(_ => _ + 'px').concat([ + getCssColor(shad.color, shad.alpha), + shad.inset ? 'inset' : '' + ]).join(' ')).join(', ') +} + +const getCssShadowFilter = (input) => { + if (input.length === 0) { + return 'none' + } + + return input + // drop-shadow doesn't support inset or spread + .filter((shad) => !shad.inset && Number(shad.spread) === 0) + .map((shad) => [ + shad.x, + shad.y, + // drop-shadow's blur is twice as strong compared to box-shadow + shad.blur / 2 + ].map(_ => _ + 'px').concat([ + getCssColor(shad.color, shad.alpha) + ]).join(' ')) + .map(_ => `drop-shadow(${_})`) + .join(' ') +} + +export const generateColors = (themeData) => { + const sourceColors = !themeData.themeEngineVersion + ? colors2to3(themeData.colors || themeData) + : themeData.colors || themeData + + const { colors, opacity } = getColors(sourceColors, themeData.opacity || {}) + + const htmlColors = Object.entries(colors) + .reduce((acc, [k, v]) => { + if (!v) return acc + acc.solid[k] = rgb2hex(v) + acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v) + return acc + }, { complete: {}, solid: {} }) + return { + rules: { + colors: Object.entries(htmlColors.complete) + .filter(([k, v]) => v) + .map(([k, v]) => `--${k}: ${v}`) + .join(';') + }, + theme: { + colors: htmlColors.solid, + opacity + } + } +} + +export const generateRadii = (input) => { + let inputRadii = input.radii || {} + // v1 -> v2 + if (typeof input.btnRadius !== 'undefined') { + inputRadii = Object + .entries(input) + .filter(([k, v]) => k.endsWith('Radius')) + .reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {}) + } + const radii = Object.entries(inputRadii).filter(([k, v]) => v).reduce((acc, [k, v]) => { + acc[k] = v + return acc + }, { + btn: 4, + input: 4, + checkbox: 2, + panel: 10, + avatar: 5, + avatarAlt: 50, + tooltip: 2, + attachment: 5, + chatMessage: inputRadii.panel + }) + + return { + rules: { + radii: Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';') + }, + theme: { + radii + } + } +} + +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 + return acc + }, acc[k]) + return acc + }, { + interface: { + family: 'sans-serif' + }, + input: { + family: 'inherit' + }, + post: { + family: 'inherit' + }, + postCode: { + family: 'monospace' + } + }) + + return { + rules: { + fonts: Object + .entries(fonts) + .filter(([k, v]) => v) + .map(([k, v]) => `--${k}Font: ${v.family}`).join(';') + }, + theme: { + fonts + } + } +} + +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: '#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 + }] +} +export const generateShadows = (input, colors) => { + // TODO this is a small hack for `mod` to work with shadows + // this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element + const hackContextDict = { + button: 'btn', + panel: 'bg', + top: 'topBar', + popup: 'popover', + avatar: 'bg', + panelHeader: 'panel', + input: 'input' + } + + const cleanInputShadows = Object.fromEntries( + Object.entries(input.shadows || {}) + .map(([name, shadowSlot]) => [ + name, + // defaulting color to black to avoid potential problems + shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef })) + ]) + ) + const inputShadows = cleanInputShadows && !input.themeEngineVersion + ? shadows2to3(cleanInputShadows, input.opacity) + : cleanInputShadows || {} + const shadows = Object.entries({ + ...DEFAULT_SHADOWS, + ...inputShadows + }).reduce((shadowsAcc, [slotName, shadowDefs]) => { + const slotFirstWord = slotName.replace(/[A-Z].*$/, '') + const colorSlotName = hackContextDict[slotFirstWord] + const isLightOnDark = relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5 + const mod = isLightOnDark ? 1 : -1 + 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: { + shadows: Object + .entries(shadows) + // TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally + // convert all non-inset shadows into filter: drop-shadow() to boost performance + .map(([k, v]) => [ + `--${k}Shadow: ${getCssShadow(v)}`, + `--${k}ShadowFilter: ${getCssShadowFilter(v)}`, + `--${k}ShadowInset: ${getCssShadow(v, true)}` + ].join(';')) + .join(';') + }, + theme: { + shadows + } + } +} + +/** + * 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, opacity) => { + return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => { + const isDynamic = ({ color = '#000000' }) => color.startsWith('--') + const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])] + const newShadow = shadowDefs.reduce((shadowAcc, def) => [ + ...shadowAcc, + { + ...def, + alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha + } + ], []) + return { ...shadowsAcc, [slotName]: newShadow } + }, {}) +} + +export const colors2to3 = (colors) => { + return Object.entries(colors).reduce((acc, [slotName, color]) => { + const btnPositions = ['', 'Panel', 'TopBar'] + switch (slotName) { + case 'lightBg': + return { ...acc, highlight: color } + case 'btnText': + return { + ...acc, + ...btnPositions + .reduce( + (statePositionAcc, position) => + ({ ...statePositionAcc, ['btn' + position + 'Text']: color }) + , {} + ) + } + default: + return { ...acc, [slotName]: color } + } + }, {}) +} |
