diff options
author | crupest <crupest@outlook.com> | 2023-09-20 20:26:42 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-20 20:26:42 +0800 |
commit | f836d77e73f3ea0af45c5f71dae7268143d6d86f (patch) | |
tree | 573cfafd972106d69bef0d41ff5f270ec3c43ec2 /FrontEnd/tools/theme-generator.ts | |
parent | 4a069bf1268f393d5467166356f691eb89963152 (diff) | |
parent | 901fe3d7c032d284da5c9bce24c4aaee9054c7ac (diff) | |
download | timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.tar.gz timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.tar.bz2 timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.zip |
Merge pull request #1395 from crupest/dev
Refector 2023 v0.1
Diffstat (limited to 'FrontEnd/tools/theme-generator.ts')
-rw-r--r-- | FrontEnd/tools/theme-generator.ts | 495 |
1 files changed, 495 insertions, 0 deletions
diff --git a/FrontEnd/tools/theme-generator.ts b/FrontEnd/tools/theme-generator.ts new file mode 100644 index 00000000..3583d240 --- /dev/null +++ b/FrontEnd/tools/theme-generator.ts @@ -0,0 +1,495 @@ +#!/usr/bin/env ts-node + +/** + * Color variable name scheme: + * no variant: --[prefix]-[name]-color: [color]; + * with variant: --[prefix]-[name]-[variant]-color: [color]; + * + * Lightness variants come from material design (https://m3.material.io/styles/color/the-color-system/tokens) + */ + +import { stdout } from "process"; + +interface CssSegment { + toCssString(): string; +} + +interface Color extends CssSegment { + readonly type: "hsl" | "css-var"; + toString(): string; +} + +class HslColor implements Color { + readonly type = "hsl"; + + constructor( + public h: number, + public s: number, + public l: number, + ) {} + + withLightness(lightness: number): HslColor { + return new HslColor(this.h, this.s, lightness); + } + + toCssString(): string { + return this.toString(); + } + + toString(): string { + return `hsl(${this.h} ${this.s}% ${this.l}%)`; + } + + static readonly white = new HslColor(0, 0, 100); + static readonly black = new HslColor(0, 0, 0); +} + +class ColorVariable implements CssSegment { + constructor( + public prefix: string, + public name: string, + public variant: string, + ) {} + + toString(): string { + const variantPart = this.variant !== "" ? `-${this.variant}` : ""; + return `--${this.prefix}-${this.name}${variantPart}-color`; + } + + toCssString(): string { + return this.toString(); + } +} + +class CssVarColor implements Color { + readonly type = "css-var"; + + constructor(public colorVariable: ColorVariable) {} + + toCssString(): string { + return this.toString(); + } + + toString(): string { + return `var(${this.colorVariable.toString()})`; + } +} + +class ColorVariableDefinition implements CssSegment { + constructor( + public variable: ColorVariable, + public color: Color, + ) {} + + toCssString(): string { + return `${this.variable.toCssString()}: ${this.color.toCssString()};`; + } +} + +abstract class ColorGroup implements CssSegment { + abstract getColorVariables(): ColorVariableDefinition[]; + toCssString(): string { + return this.getColorVariables() + .map((c) => c.toCssString()) + .join("\n"); + } +} + +interface LightnessVariantInfo { + name: string; + lightness: number; +} + +class LightnessVariantColorGroup extends ColorGroup { + constructor( + public prefix: string, + public name: string, + public baseColor: HslColor, + public variants: LightnessVariantInfo[], + ) { + super(); + } + + getColorVariables(): ColorVariableDefinition[] { + const result: ColorVariableDefinition[] = []; + + for (const variant of this.variants) { + const color = this.baseColor.withLightness(variant.lightness); + result.push( + new ColorVariableDefinition( + new ColorVariable(this.prefix, this.name, variant.name), + color, + ), + ); + } + + return result; + } +} + +class VarAliasColorGroup extends ColorGroup { + constructor( + public prefix: string, + public newName: string, + public oldName: string, + public variants: string[], + ) { + super(); + } + + getColorVariables(): ColorVariableDefinition[] { + const result = []; + for (const variant of this.variants) { + result.push( + new ColorVariableDefinition( + new ColorVariable(this.prefix, this.newName, variant), + new CssVarColor( + new ColorVariable(this.prefix, this.oldName, variant), + ), + ), + ); + } + return result; + } +} + +class CompositeColorGroup extends ColorGroup { + constructor(public groups: ColorGroup[]) { + super(); + } + + getColorVariables(): ColorVariableDefinition[] { + return this.groups + .map((g) => g.getColorVariables()) + .reduce((prev, curr) => prev.concat(curr), []); + } +} + +interface ThemeColorsInfo { + keyColors: { name: string; color: HslColor }[]; + neutralColor: HslColor; +} + +type ColorMode = "light" | "dark"; + +type ThemeColorVariantLightnessVariantsInfo = + | number + | number[] + | { + base: number; + direction: "darker" | "lighter"; + levels: number; + step: number; + }; + +interface ThemeColorVariantInfo { + name: string; + variants: { + light: ThemeColorVariantLightnessVariantsInfo; + dark: ThemeColorVariantLightnessVariantsInfo; + }; +} + +class ThemeColorVariant { + constructor( + public name: string, + public variants: { + light: ThemeColorVariantLightnessVariantsInfo; + dark: ThemeColorVariantLightnessVariantsInfo; + }, + ) {} + getLightnessVariants(mode: ColorMode): LightnessVariantInfo[] { + const { name, variants } = this; + const list = variants[mode]; + + function variantName(i: number) { + if (name.length === 0) { + return i === 0 ? "" : String(i); + } else { + return i === 0 ? name : `${name}-${i}`; + } + } + + function fromList(list: number[]): LightnessVariantInfo[] { + return list.map((l, i) => ({ + name: variantName(i), + lightness: l, + })); + } + + if (typeof list === "number") { + return fromList([list]); + } else if (Array.isArray(list)) { + return fromList(list); + } else { + const l = [list.base]; + for (let i = 1; i <= list.levels; i++) { + if (list.direction === "darker") { + l.push(list.base - i * list.step); + } else { + l.push(list.base + i * list.step); + } + } + return fromList(l); + } + } + + static from(info: ThemeColorVariantInfo): ThemeColorVariant { + return new ThemeColorVariant(info.name, info.variants); + } +} + +class ThemeColor { + variants: ThemeColorVariant[]; + + constructor( + public prefix: string, + public name: string, + public color: HslColor, + variants: ThemeColorVariantInfo[], + ) { + this.variants = variants.map((v) => ThemeColorVariant.from(v)); + } + + getLightnessVariants(mode: ColorMode): LightnessVariantInfo[] { + return this.variants.flatMap((v) => v.getLightnessVariants(mode)); + } + + getLightnessVariantColorGroup(mode: ColorMode): LightnessVariantColorGroup { + return new LightnessVariantColorGroup( + this.prefix, + this.name, + this.color, + this.getLightnessVariants(mode), + ); + } +} + +class Theme { + static keyColorVariants: ThemeColorVariantInfo[] = [ + { + name: "", + variants: { + light: [40, 37, 34], + dark: [80, 75, 68], + }, + }, + { + name: "on", + variants: { + light: 100, + dark: 20, + }, + }, + { + name: "container", + variants: { + light: [90, 80, 70], + dark: [30, 25, 20], + }, + }, + { + name: "on-container", + variants: { + light: 10, + dark: 90, + }, + }, + ]; + + static surfaceColorVariants: ThemeColorVariantInfo[] = [ + { + name: "dim", + variants: { + light: 87, + dark: 6, + }, + }, + { + name: "", + variants: { + light: [98, 90, 82], + dark: [6, 25, 40], + }, + }, + { + name: "bright", + variants: { + light: 98, + dark: 24, + }, + }, + { + name: "container-lowest", + variants: { + light: 100, + dark: 4, + }, + }, + { + name: "container-low", + variants: { + light: 96, + dark: 10, + }, + }, + { + name: "container", + variants: { + light: 94, + dark: 12, + }, + }, + { + name: "container-high", + variants: { + light: 92, + dark: 17, + }, + }, + { + name: "container-highest", + variants: { + light: 90, + dark: 22, + }, + }, + { + name: "on", + variants: { + light: 10, + dark: 90, + }, + }, + { + name: "on-variant", + variants: { + light: 30, + dark: 80, + }, + }, + { + name: "outline", + variants: { + light: 50, + dark: 60, + }, + }, + { + name: "outline-variant", + variants: { + light: 80, + dark: 30, + }, + }, + ]; + + constructor( + public prefix: string, + public themeColors: ThemeColorsInfo, + ) {} + + getColorModeColorDefinitions(mode: ColorMode): ColorGroup { + const groups: ColorGroup[] = []; + for (const { name, color } of this.themeColors.keyColors) { + const themeColor = new ThemeColor( + this.prefix, + name, + color, + Theme.keyColorVariants, + ); + groups.push(themeColor.getLightnessVariantColorGroup(mode)); + } + const neutralThemeColor = new ThemeColor( + this.prefix, + "surface", + this.themeColors.neutralColor, + Theme.surfaceColorVariants, + ); + groups.push(neutralThemeColor.getLightnessVariantColorGroup(mode)); + return new CompositeColorGroup(groups); + } + + getAliasColorDefinitions(name: string): ColorGroup { + const sampleThemeColor = this.themeColors.keyColors[0]; + const themeColor = new ThemeColor( + this.prefix, + sampleThemeColor.name, + sampleThemeColor.color, + Theme.keyColorVariants, + ); + const sampleMode = "light"; + return new VarAliasColorGroup( + this.prefix, + "key", + name, + themeColor.getLightnessVariants(sampleMode).map((v) => v.name), + ); + } + + generateCss(print: (text: string, indent: number) => void): void { + print(":root {", 0); + print(this.getColorModeColorDefinitions("light").toCssString(), 1); + print("}", 0); + + print("", 0); + + print("@media (prefers-color-scheme: dark) {", 0); + print(":root {", 1); + print(this.getColorModeColorDefinitions("dark").toCssString(), 2); + print("}", 1); + print("}", 0); + + print("", 0); + + for (const { name } of this.themeColors.keyColors) { + print(`.${this.prefix}-${name} {`, 0); + print(this.getAliasColorDefinitions(name).toCssString(), 1); + print("}", 0); + + print("", 0); + } + } +} + +(function main() { + const prefix = "cru"; + const themeColors: ThemeColorsInfo = { + keyColors: [ + { name: "primary", color: new HslColor(210, 100, 50) }, + { name: "secondary", color: new HslColor(40, 100, 50) }, + { name: "tertiary", color: new HslColor(160, 100, 50) }, + { name: "danger", color: new HslColor(0, 100, 50) }, + { name: "success", color: new HslColor(120, 60, 50) }, + ], + neutralColor: new HslColor(0, 0, 50), + }; + + const theme = new Theme(prefix, themeColors); + + let output = ""; + + function indentText( + text: string, + level: number, + indentWidth = 2, + appendNewlines = 1, + ): string { + const lines = text.split("\n"); + const indent = " ".repeat(level * indentWidth); + return ( + lines + .map((line) => (line.length === 0 ? "" : `${indent}${line}`)) + .join("\n") + "\n".repeat(appendNewlines) + ); + } + + function print(text: string, indent = 0, appendNewlines = 1) { + output += indentText(text, indent, 2, appendNewlines); + } + + print("/* Generated by theme-generator.ts */\n"); + theme.generateCss(print); + + stdout.write(output); +})(); |