colorScheme.ts

  1import { Scale, Color } from "chroma-js"
  2import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax"
  3export { Syntax, ThemeSyntax, SyntaxHighlightStyle }
  4import {
  5    ThemeConfig,
  6    ThemeAppearance,
  7    ThemeConfigInputColors,
  8} from "./themeConfig"
  9import { getRamps } from "./ramps"
 10
 11export interface ColorScheme {
 12    name: string
 13    isLight: boolean
 14
 15    lowest: Layer
 16    middle: Layer
 17    highest: Layer
 18
 19    ramps: RampSet
 20
 21    popoverShadow: Shadow
 22    modalShadow: Shadow
 23
 24    players: Players
 25    syntax?: Partial<ThemeSyntax>
 26}
 27
 28export interface Meta {
 29    name: string
 30    author: string
 31    url: string
 32    license: License
 33}
 34
 35export interface License {
 36    SPDX: SPDXExpression
 37}
 38
 39// License name -> License text
 40export interface Licenses {
 41    [key: string]: string
 42}
 43
 44// FIXME: Add support for the SPDX expression syntax
 45export type SPDXExpression = "MIT"
 46
 47export interface Player {
 48    cursor: string
 49    selection: string
 50}
 51
 52export interface Players {
 53    "0": Player
 54    "1": Player
 55    "2": Player
 56    "3": Player
 57    "4": Player
 58    "5": Player
 59    "6": Player
 60    "7": Player
 61}
 62
 63export interface Shadow {
 64    blur: number
 65    color: string
 66    offset: number[]
 67}
 68
 69export type StyleSets = keyof Layer
 70export interface Layer {
 71    base: StyleSet
 72    variant: StyleSet
 73    on: StyleSet
 74    accent: StyleSet
 75    positive: StyleSet
 76    warning: StyleSet
 77    negative: StyleSet
 78}
 79
 80export interface RampSet {
 81    neutral: Scale
 82    red: Scale
 83    orange: Scale
 84    yellow: Scale
 85    green: Scale
 86    cyan: Scale
 87    blue: Scale
 88    violet: Scale
 89    magenta: Scale
 90}
 91
 92export type Styles = keyof StyleSet
 93export interface StyleSet {
 94    default: Style
 95    active: Style
 96    disabled: Style
 97    hovered: Style
 98    pressed: Style
 99    inverted: Style
100}
101
102export interface Style {
103    background: string
104    border: string
105    foreground: string
106}
107
108export function createColorScheme(theme: ThemeConfig): ColorScheme {
109    const {
110        name,
111        appearance,
112        inputColor,
113        override: { syntax },
114    } = theme
115
116    const isLight = appearance === ThemeAppearance.Light
117    const colorRamps: ThemeConfigInputColors = inputColor
118
119    // Chromajs scales from 0 to 1 flipped if isLight is true
120    const ramps = getRamps(isLight, colorRamps)
121    const lowest = lowestLayer(ramps)
122    const middle = middleLayer(ramps)
123    const highest = highestLayer(ramps)
124
125    const popoverShadow = {
126        blur: 4,
127        color: ramps
128            .neutral(isLight ? 7 : 0)
129            .darken()
130            .alpha(0.2)
131            .hex(), // TODO used blend previously. Replace with something else
132        offset: [1, 2],
133    }
134
135    const modalShadow = {
136        blur: 16,
137        color: ramps
138            .neutral(isLight ? 7 : 0)
139            .darken()
140            .alpha(0.2)
141            .hex(), // TODO used blend previously. Replace with something else
142        offset: [0, 2],
143    }
144
145    const players = {
146        "0": player(ramps.blue),
147        "1": player(ramps.green),
148        "2": player(ramps.magenta),
149        "3": player(ramps.orange),
150        "4": player(ramps.violet),
151        "5": player(ramps.cyan),
152        "6": player(ramps.red),
153        "7": player(ramps.yellow),
154    }
155
156    return {
157        name,
158        isLight,
159
160        ramps,
161
162        lowest,
163        middle,
164        highest,
165
166        popoverShadow,
167        modalShadow,
168
169        players,
170        syntax,
171    }
172}
173
174function player(ramp: Scale): Player {
175    return {
176        selection: ramp(0.5).alpha(0.24).hex(),
177        cursor: ramp(0.5).hex(),
178    }
179}
180
181function lowestLayer(ramps: RampSet): Layer {
182    return {
183        base: buildStyleSet(ramps.neutral, 0.2, 1),
184        variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
185        on: buildStyleSet(ramps.neutral, 0.1, 1),
186        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
187        positive: buildStyleSet(ramps.green, 0.1, 0.5),
188        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
189        negative: buildStyleSet(ramps.red, 0.1, 0.5),
190    }
191}
192
193function middleLayer(ramps: RampSet): Layer {
194    return {
195        base: buildStyleSet(ramps.neutral, 0.1, 1),
196        variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
197        on: buildStyleSet(ramps.neutral, 0, 1),
198        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
199        positive: buildStyleSet(ramps.green, 0.1, 0.5),
200        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
201        negative: buildStyleSet(ramps.red, 0.1, 0.5),
202    }
203}
204
205function highestLayer(ramps: RampSet): Layer {
206    return {
207        base: buildStyleSet(ramps.neutral, 0, 1),
208        variant: buildStyleSet(ramps.neutral, 0, 0.7),
209        on: buildStyleSet(ramps.neutral, 0.1, 1),
210        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
211        positive: buildStyleSet(ramps.green, 0.1, 0.5),
212        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
213        negative: buildStyleSet(ramps.red, 0.1, 0.5),
214    }
215}
216
217function buildStyleSet(
218    ramp: Scale,
219    backgroundBase: number,
220    foregroundBase: number,
221    step: number = 0.08
222): StyleSet {
223    let styleDefinitions = buildStyleDefinition(
224        backgroundBase,
225        foregroundBase,
226        step
227    )
228
229    function colorString(indexOrColor: number | Color): string {
230        if (typeof indexOrColor === "number") {
231            return ramp(indexOrColor).hex()
232        } else {
233            return indexOrColor.hex()
234        }
235    }
236
237    function buildStyle(style: Styles): Style {
238        return {
239            background: colorString(styleDefinitions.background[style]),
240            border: colorString(styleDefinitions.border[style]),
241            foreground: colorString(styleDefinitions.foreground[style]),
242        }
243    }
244
245    return {
246        default: buildStyle("default"),
247        hovered: buildStyle("hovered"),
248        pressed: buildStyle("pressed"),
249        active: buildStyle("active"),
250        disabled: buildStyle("disabled"),
251        inverted: buildStyle("inverted"),
252    }
253}
254
255function buildStyleDefinition(
256    bgBase: number,
257    fgBase: number,
258    step: number = 0.08
259) {
260    return {
261        background: {
262            default: bgBase,
263            hovered: bgBase + step,
264            pressed: bgBase + step * 1.5,
265            active: bgBase + step * 2.2,
266            disabled: bgBase,
267            inverted: fgBase + step * 6,
268        },
269        border: {
270            default: bgBase + step * 1,
271            hovered: bgBase + step,
272            pressed: bgBase + step,
273            active: bgBase + step * 3,
274            disabled: bgBase + step * 0.5,
275            inverted: bgBase - step * 3,
276        },
277        foreground: {
278            default: fgBase,
279            hovered: fgBase,
280            pressed: fgBase,
281            active: fgBase + step * 6,
282            disabled: bgBase + step * 4,
283            inverted: bgBase + step * 2,
284        },
285    }
286}