From 34aa9136dba6b0e1d54f657e24ea4ae77f6c406e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Mon, 19 Feb 2024 18:48:49 +0200 Subject: [PATCH] refactored most of the CSS stuff into separate file, refactored color functions and added shadow functions, replaced JS functions in button with PISS functions --- src/components/button.style.js | 14 +- src/services/style_setter/style_setter.js | 6 +- src/services/theme_data/css_utils.js | 43 ++ .../theme_data/theme3_slot_functions.js | 92 ++++ .../theme_data/theme_data_3.service.js | 412 ++++++++---------- 5 files changed, 324 insertions(+), 243 deletions(-) create mode 100644 src/services/theme_data/css_utils.js create mode 100644 src/services/theme_data/theme3_slot_functions.js diff --git a/src/components/button.style.js b/src/components/button.style.js index f12623f9..3c090584 100644 --- a/src/components/button.style.js +++ b/src/components/button.style.js @@ -1,15 +1,5 @@ -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 buttonInsetFakeBorders = ['$borderSide(#FFFFFF, top, 0.2)', '$borderSide(#000000, bottom, 0.2)'] +const inputInsetFakeBorders = ['$borderSide(#FFFFFF, bottom, 0.2)', '$borderSide(#000000, top, 0.2)'] const buttonOuterShadow = { x: 0, y: 0, diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index afc968c7..3c2a2763 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,7 +1,7 @@ import { convert } from 'chromatism' import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js' import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js' -import { init } from '../theme_data/theme_data_3.service.js' +import { init, getCssRules } from '../theme_data/theme_data_3.service.js' import { sampleRules } from 'src/services/theme_data/pleromafe.t3.js' @@ -25,7 +25,7 @@ export const applyTheme = (input) => { styleSheet.toString() styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max') - themes3.css(themes3.eager).forEach(rule => { + getCssRules(themes3.eager, t3b).forEach(rule => { // Hack to support multiple selectors on same component if (rule.match(/::-webkit-scrollbar-button/)) { const parts = rule.split(/[{}]/g) @@ -44,7 +44,7 @@ export const applyTheme = (input) => { }) body.classList.remove('hidden') themes3.lazy.then(lazyRules => { - themes3.css(lazyRules).forEach(rule => { + getCssRules(lazyRules, t3b).forEach(rule => { styleSheet.insertRule(rule, 'index-max') }) const t3 = performance.now() diff --git a/src/services/theme_data/css_utils.js b/src/services/theme_data/css_utils.js new file mode 100644 index 00000000..8b061f8f --- /dev/null +++ b/src/services/theme_data/css_utils.js @@ -0,0 +1,43 @@ +import { convert } from 'chromatism' + +import { rgba2css } from '../color_convert/color_convert.js' + +export const getCssColorString = (color, alpha) => rgba2css({ ...convert(color).rgb, a: alpha }) + +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([ + getCssColorString(shad.color, shad.alpha), + shad.inset ? 'inset' : '' + ]).join(' ')).join(', ') +} + +export 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([ + getCssColorString(shad.color, shad.alpha) + ]).join(' ')) + .map(_ => `drop-shadow(${_})`) + .join(' ') +} diff --git a/src/services/theme_data/theme3_slot_functions.js b/src/services/theme_data/theme3_slot_functions.js new file mode 100644 index 00000000..2324e121 --- /dev/null +++ b/src/services/theme_data/theme3_slot_functions.js @@ -0,0 +1,92 @@ +import { convert, brightness } from 'chromatism' +import { alphaBlend, relativeLuminance } from '../color_convert/color_convert.js' + +export const process = (text, functions, findColor, dynamicVars, staticVars) => { + const { funcName, argsString } = /\$(?\w+)\((?[#a-zA-Z0-9-,.'"\s]*)\)/.exec(text).groups + const args = argsString.split(/,/g).map(a => a.trim()) + + const func = functions[funcName] + if (args.length < func.argsNeeded) { + throw new Error(`$${funcName} requires at least ${func.argsNeeded} arguments, but ${args.length} were provided`) + } + return func.exec(args, findColor, dynamicVars, staticVars) +} + +export const colorFunctions = { + alpha: { + argsNeeded: 2, + exec: (args, findColor, dynamicVars, staticVars) => { + const [color, amountArg] = args + + const colorArg = convert(findColor(color, dynamicVars, staticVars)).rgb + const amount = Number(amountArg) + return { ...colorArg, a: amount } + } + }, + blend: { + argsNeeded: 3, + exec: (args, findColor, dynamicVars, staticVars) => { + const [backgroundArg, amountArg, foregroundArg] = args + + const background = convert(findColor(backgroundArg, dynamicVars, staticVars)).rgb + const foreground = convert(findColor(foregroundArg, dynamicVars, staticVars)).rgb + const amount = Number(amountArg) + + return alphaBlend(background, amount, foreground) + } + }, + mod: { + argsNeeded: 2, + exec: (args, findColor, dynamicVars, staticVars) => { + const [colorArg, amountArg] = args + + const color = convert(findColor(colorArg, dynamicVars, staticVars)).rgb + const amount = Number(amountArg) + + const effectiveBackground = dynamicVars.lowerLevelBackground + const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 + const mod = isLightOnDark ? 1 : -1 + return brightness(amount * mod, color).rgb + } + } +} + +export const shadowFunctions = { + borderSide: { + argsNeeded: 3, + exec: (args, findColor, dynamicVars, staticVars) => { + const [color, side, alpha = '1', widthArg = '1', inset = 'inset'] = args + + const width = Number(widthArg) + const isInset = inset === 'inset' + + const targetShadow = { + x: 0, + y: 0, + blur: 0, + spread: 0, + color, + alpha: Number(alpha), + inset: isInset + } + + side.split('-').forEach((position) => { + switch (position) { + case 'left': + targetShadow.x = width * (inset ? 1 : -1) + break + case 'right': + targetShadow.x = -1 * width * (inset ? 1 : -1) + break + case 'top': + targetShadow.y = width * (inset ? 1 : -1) + break + case 'bottom': + targetShadow.y = -1 * width * (inset ? 1 : -1) + break + } + }) + return targetShadow + } + } +} diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js index cd23908c..88bff2aa 100644 --- a/src/services/theme_data/theme_data_3.service.js +++ b/src/services/theme_data/theme_data_3.service.js @@ -7,6 +7,18 @@ import { relativeLuminance } from '../color_convert/color_convert.js' +import { + colorFunctions, + shadowFunctions, + process +} from './theme3_slot_functions.js' + +import { + getCssShadow, + getCssShadowFilter, + getCssColorString +} from './css_utils.js' + const DEBUG = false // Ensuring the order of components @@ -22,6 +34,149 @@ const components = { ChatMessage: null } +const findColor = (color, dynamicVars, staticVars) => { + if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color + let targetColor = null + if (color.startsWith('--')) { + const [variable, modifier] = color.split(/,/g).map(str => str.trim()) + const variableSlot = variable.substring(2) + if (variableSlot === 'stack') { + const { r, g, b } = dynamicVars.stacked + targetColor = { r, g, b } + } else if (variableSlot.startsWith('parent')) { + if (variableSlot === 'parent') { + const { r, g, b } = dynamicVars.lowerLevelBackground + targetColor = { r, g, b } + } else { + const virtualSlot = variableSlot.replace(/^parent/, '') + targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb + } + } else { + switch (variableSlot) { + case 'inheritedBackground': + targetColor = convert(dynamicVars.inheritedBackground).rgb + break + case 'background': + targetColor = convert(dynamicVars.background).rgb + break + default: + targetColor = convert(staticVars[variableSlot]).rgb + } + } + + if (modifier) { + const effectiveBackground = dynamicVars.lowerLevelBackground ?? targetColor + const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 + const mod = isLightOnDark ? 1 : -1 + targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb + } + } + + if (color.startsWith('$')) { + try { + targetColor = process(color, colorFunctions, findColor, dynamicVars, staticVars) + } catch (e) { + console.error('Failure executing color function', e) + targetColor = '#FF00FF' + } + } + // Color references other color + return targetColor +} + +const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVars) => { + const opacity = directives.textOpacity + const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb + const textColor = convert(findColor(intendedTextColor, dynamicVars, staticVars)).rgb + if (opacity === null || opacity === undefined || opacity >= 1) { + return convert(textColor).hex + } + if (opacity === 0) { + return convert(backgroundColor).hex + } + const opacityMode = directives.textOpacityMode + switch (opacityMode) { + case 'fake': + return convert(alphaBlend(textColor, opacity, backgroundColor)).hex + case 'mixrgb': + return convert(mixrgb(backgroundColor, textColor)).hex + default: + return rgba2css({ a: opacity, ...textColor }) + } +} + +export const getCssRules = (rules, staticVars) => rules.map(rule => { + let selector = rule.selector + if (!selector) { + selector = 'body' + } + const header = selector + ' {' + const footer = '}' + + const virtualDirectives = Object.entries(rule.virtualDirectives || {}).map(([k, v]) => { + return ' ' + k + ': ' + v + }).join(';\n') + + let directives + if (rule.component !== 'Root') { + directives = Object.entries(rule.directives).map(([k, v]) => { + switch (k) { + case 'roundness': { + return ' ' + [ + '--roundness: ' + v + 'px' + ].join(';\n ') + } + case 'shadow': { + return ' ' + [ + '--shadow: ' + getCssShadow(rule.dynamicVars.shadow), + '--shadowFilter: ' + getCssShadowFilter(rule.dynamicVars.shadow), + '--shadowInset: ' + getCssShadow(rule.dynamicVars.shadow, true) + ].join(';\n ') + } + case 'background': { + if (v === 'transparent') { + return [ + rule.directives.backgroundNoCssColor !== 'yes' ? ('background-color: ' + v) : '', + ' --background: ' + v + ].filter(x => x).join(';\n') + } + const color = getCssColorString(rule.dynamicVars.background, rule.directives.opacity) + return [ + rule.directives.backgroundNoCssColor !== 'yes' ? ('background-color: ' + color) : '', + ' --background: ' + color + ].filter(x => x).join(';\n') + } + case 'textColor': { + if (rule.directives.textNoCssColor === 'yes') { return '' } + return 'color: ' + v + } + default: + if (k.startsWith('--')) { + const [type, value] = v.split('|').map(x => x.trim()) // woah, Extreme! + switch (type) { + case 'color': + return k + ': ' + rgba2css(findColor(value, rule.dynamicVars, staticVars)) + default: + return '' + } + } + return '' + } + }).filter(x => x).map(x => ' ' + x).join(';\n') + } else { + directives = {} + } + + return [ + header, + directives + ';', + (!rule.virtual && rule.directives.textNoCssColor !== 'yes') ? ' color: var(--text);' : '', + '', + virtualDirectives, + footer + ].join('\n') +}).filter(x => x) + // Loading all style.js[on] files dynamically const componentsContext = require.context('src', true, /\.style.js(on)?$/) componentsContext.keys().forEach(key => { @@ -147,6 +302,11 @@ const findRules = (criteria, strict) => subject => { return true } +const normalizeCombination = rule => { + rule.variant = rule.variant ?? 'normal' + rule.state = [...new Set(['normal', ...(rule.state || [])])] +} + export const init = (extraRuleset, palette) => { const stacked = {} const computed = {} @@ -154,11 +314,6 @@ export const init = (extraRuleset, palette) => { const eagerRules = [] const lazyRules = [] - const normalizeCombination = rule => { - rule.variant = rule.variant ?? 'normal' - rule.state = [...new Set(['normal', ...(rule.state || [])])] - } - const rulesetUnsorted = [ ...Object.values(components) .map(c => (c.defaultRules || []).map(r => ({ component: c.name, ...r }))) @@ -194,152 +349,6 @@ export const init = (extraRuleset, palette) => { const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name)) - const findColor = (color, dynamicVars) => { - if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color - let targetColor = null - if (color.startsWith('--')) { - const [variable, modifier] = color.split(/,/g).map(str => str.trim()) - const variableSlot = variable.substring(2) - if (variableSlot === 'stack') { - const { r, g, b } = dynamicVars.stacked - targetColor = { r, g, b } - } else if (variableSlot.startsWith('parent')) { - if (variableSlot === 'parent') { - const { r, g, b } = dynamicVars.lowerLevelBackground - targetColor = { r, g, b } - } else { - const virtualSlot = variableSlot.replace(/^parent/, '') - targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb - } - } else { - // TODO add support for --current prefix - switch (variableSlot) { - case 'inheritedBackground': - targetColor = convert(dynamicVars.inheritedBackground).rgb - break - case 'background': - targetColor = convert(dynamicVars.background).rgb - break - default: - targetColor = convert(palette[variableSlot]).rgb - } - } - - if (modifier) { - const effectiveBackground = dynamicVars.lowerLevelBackground ?? targetColor - const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 - const mod = isLightOnDark ? 1 : -1 - targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb - } - } - - if (color.startsWith('$')) { - try { - const { funcName, argsString } = /\$(?\w+)\((?[a-zA-Z0-9-,.'"\s]*)\)/.exec(color).groups - const args = argsString.split(/,/g).map(a => a.trim()) - switch (funcName) { - case 'alpha': { - if (args.length !== 2) { - throw new Error(`$alpha requires 2 arguments, ${args.length} were provided`) - } - const colorArg = convert(findColor(args[0], dynamicVars)).rgb - const amount = Number(args[1]) - targetColor = { ...colorArg, a: amount } - break - } - case 'blend': { - if (args.length !== 3) { - throw new Error(`$blend requires 3 arguments, ${args.length} were provided`) - } - const backgroundArg = convert(findColor(args[2], dynamicVars)).rgb - const foregroundArg = convert(findColor(args[0], dynamicVars)).rgb - const amount = Number(args[1]) - targetColor = alphaBlend(backgroundArg, amount, foregroundArg) - break - } - case 'mod': { - if (args.length !== 2) { - throw new Error(`$mod requires 2 arguments, ${args.length} were provided`) - } - const color = convert(findColor(args[0], dynamicVars)).rgb - const amount = Number(args[1]) - const effectiveBackground = dynamicVars.lowerLevelBackground - const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 - const mod = isLightOnDark ? 1 : -1 - targetColor = brightness(amount * mod, color).rgb - break - } - } - } catch (e) { - console.error('Failure executing color function', e) - targetColor = '#FF00FF' - } - } - // Color references other color - return targetColor - } - - const cssColorString = (color, alpha) => rgba2css({ ...convert(color).rgb, a: alpha }) - - const getTextColorAlpha = (directives, intendedTextColor, dynamicVars) => { - const opacity = directives.textOpacity - const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb - const textColor = convert(findColor(intendedTextColor, dynamicVars)).rgb - if (opacity === null || opacity === undefined || opacity >= 1) { - return convert(textColor).hex - } - if (opacity === 0) { - return convert(backgroundColor).hex - } - const opacityMode = directives.textOpacityMode - switch (opacityMode) { - case 'fake': - return convert(alphaBlend(textColor, opacity, backgroundColor)).hex - case 'mixrgb': - return convert(mixrgb(backgroundColor, textColor)).hex - default: - return rgba2css({ a: opacity, ...textColor }) - } - } - - 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([ - cssColorString(findColor(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([ - cssColorString(findColor(shad.color), shad.alpha) - ]).join(' ')) - .map(_ => `drop-shadow(${_})`) - .join(' ') - } - let counter = 0 const promises = [] const processInnerComponent = (component, rules, parent) => { @@ -408,7 +417,7 @@ export const init = (extraRuleset, palette) => { const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw - const dynamicVars = { + const dynamicVars = computed[selector] || { lowerLevelBackground, lowerLevelVirtualDirectives, lowerLevelVirtualDirectivesRaw @@ -466,7 +475,7 @@ export const init = (extraRuleset, palette) => { dynamicVars.inheritedBackground = lowerLevelBackground dynamicVars.stacked = convert(stacked[lowerLevelSelector]).rgb - const intendedTextColor = convert(findColor(inheritedTextColor, dynamicVars)).rgb + const intendedTextColor = convert(findColor(inheritedTextColor, dynamicVars, palette)).rgb const textColor = newTextRule.directives.textAuto === 'no-auto' ? intendedTextColor : getTextColor( @@ -500,6 +509,7 @@ export const init = (extraRuleset, palette) => { } addRule({ + dynamicVars, selector: cssSelector, virtual: true, component: component.name, @@ -532,7 +542,7 @@ export const init = (extraRuleset, palette) => { dynamicVars.inheritedBackground = inheritedBackground - const rgb = convert(findColor(computedDirectives.background, dynamicVars)).rgb + const rgb = convert(findColor(computedDirectives.background, dynamicVars, palette)).rgb if (!stacked[selector]) { let blend @@ -545,11 +555,28 @@ export const init = (extraRuleset, palette) => { blend = alphaBlend(rgb, computedDirectives.opacity, lowerLevelStackedBackground) } stacked[selector] = blend - dynamicVars.stacked = blend computed[selector].background = { ...rgb, a: computedDirectives.opacity ?? 1 } } } + if (computedDirectives.shadow) { + dynamicVars.shadow = (computedDirectives.shadow || []).map(shadow => { + let targetShadow + if (typeof shadow === 'string') { + if (shadow.startsWith('$')) { + targetShadow = process(shadow, shadowFunctions, findColor, dynamicVars, palette) + } + } else { + targetShadow = shadow + } + + return { + ...targetShadow, + color: findColor(targetShadow.color, dynamicVars, palette) + } + }) + } + if (!stacked[selector]) { computedDirectives.background = 'transparent' computedDirectives.opacity = 0 @@ -561,6 +588,7 @@ export const init = (extraRuleset, palette) => { dynamicVars.background = computed[selector].background addRule({ + dynamicVars, selector: cssSelector, component: component.name, ...combination, @@ -606,78 +634,6 @@ export const init = (extraRuleset, palette) => { return { lazy: lazyExec, - eager: eagerRules, - css: rules => rules.map(rule => { - let selector = rule.selector - if (!selector) { - selector = 'body' - } - const header = selector + ' {' - const footer = '}' - - const virtualDirectives = Object.entries(rule.virtualDirectives || {}).map(([k, v]) => { - return ' ' + k + ': ' + v - }).join(';\n') - - let directives - if (rule.component !== 'Root') { - directives = Object.entries(rule.directives).map(([k, v]) => { - const selector = ruleToSelector(rule, true) - switch (k) { - case 'roundness': { - return ' ' + [ - '--roundness: ' + v + 'px' - ].join(';\n ') - } - case 'shadow': { - return ' ' + [ - '--shadow: ' + getCssShadow(v), - '--shadowFilter: ' + getCssShadowFilter(v), - '--shadowInset: ' + getCssShadow(v, true) - ].join(';\n ') - } - case 'background': { - if (v === 'transparent') { - return [ - rule.directives.backgroundNoCssColor !== 'yes' ? ('background-color: ' + v) : '', - ' --background: ' + v - ].filter(x => x).join(';\n') - } - const color = cssColorString(computed[selector].background, rule.directives.opacity) - return [ - rule.directives.backgroundNoCssColor !== 'yes' ? ('background-color: ' + color) : '', - ' --background: ' + color - ].filter(x => x).join(';\n') - } - case 'textColor': { - if (rule.directives.textNoCssColor === 'yes') { return '' } - return 'color: ' + v - } - default: - if (k.startsWith('--')) { - const [type, value] = v.split('|').map(x => x.trim()) // woah, Extreme! - switch (type) { - case 'color': - return k + ': ' + rgba2css(findColor(value, computed[selector].dynamicVars)) - default: - return '' - } - } - return '' - } - }).filter(x => x).map(x => ' ' + x).join(';\n') - } else { - directives = {} - } - - return [ - header, - directives + ';', - (!rule.virtual && rule.directives.textNoCssColor !== 'yes') ? ' color: var(--text);' : '', - '', - virtualDirectives, - footer - ].join('\n') - }).filter(x => x) + eager: eagerRules } }