refactored most of the CSS stuff into separate file, refactored color

functions and added shadow functions, replaced JS functions in button
with PISS functions
This commit is contained in:
Henry Jameson 2024-02-19 18:48:49 +02:00
parent 11fd220734
commit 34aa9136db
5 changed files with 324 additions and 243 deletions

View File

@ -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,

View File

@ -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()

View File

@ -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(' ')
}

View File

@ -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
}
}
}

View File

@ -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
}
}