commit: 34aa9136dba6b0e1d54f657e24ea4ae77f6c406e
parent 11fd220734ae697e8157d25fbf4cdfc250fe2df7
Author: Henry Jameson <me@hjkos.com>
Date: Mon, 19 Feb 2024 18:48:49 +0200
refactored most of the CSS stuff into separate file, refactored color
functions and added shadow functions, replaced JS functions in button
with PISS functions
Diffstat:
5 files changed, 324 insertions(+), 243 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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 } = /\$(?<funcName>\w+)\((?<argsString>[#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
@@ -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 } = /\$(?<funcName>\w+)\((?<argsString>[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
}
}