ramps.ts

  1import chroma, { Color, Scale } from "chroma-js"
  2import {
  3    ColorScheme,
  4    Layer,
  5    Player,
  6    RampSet,
  7    Style,
  8    Styles,
  9    StyleSet,
 10    ThemeSyntax,
 11} from "./colorScheme"
 12
 13export function colorRamp(color: Color): Scale {
 14    let endColor = color.desaturate(1).brighten(5)
 15    let startColor = color.desaturate(1).darken(4)
 16    return chroma.scale([startColor, color, endColor]).mode("lab")
 17}
 18
 19export function createColorScheme(
 20    name: string,
 21    isLight: boolean,
 22    colorRamps: { [rampName: string]: Scale },
 23    syntax?: ThemeSyntax
 24): ColorScheme {
 25    // Chromajs scales from 0 to 1 flipped if isLight is true
 26    let ramps: RampSet = {} as any
 27
 28    // Chromajs mutates the underlying ramp when you call domain. This causes problems because
 29    // we now store the ramps object in the theme so that we can pull colors out of them.
 30    // So instead of calling domain and storing the result, we have to construct new ramps for each
 31    // theme so that we don't modify the passed in ramps.
 32    // This combined with an error in the type definitions for chroma js means we have to cast the colors
 33    // function to any in order to get the colors back out from the original ramps.
 34    if (isLight) {
 35        for (var rampName in colorRamps) {
 36            ;(ramps as any)[rampName] = chroma.scale(
 37                colorRamps[rampName].colors(100).reverse()
 38            )
 39        }
 40        ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse())
 41    } else {
 42        for (var rampName in colorRamps) {
 43            ;(ramps as any)[rampName] = chroma.scale(
 44                colorRamps[rampName].colors(100)
 45            )
 46        }
 47        ramps.neutral = chroma.scale(colorRamps.neutral.colors(100))
 48    }
 49
 50    let lowest = lowestLayer(ramps)
 51    let middle = middleLayer(ramps)
 52    let highest = highestLayer(ramps)
 53
 54    let popoverShadow = {
 55        blur: 4,
 56        color: ramps
 57            .neutral(isLight ? 7 : 0)
 58            .darken()
 59            .alpha(0.2)
 60            .hex(), // TODO used blend previously. Replace with something else
 61        offset: [1, 2],
 62    }
 63
 64    let modalShadow = {
 65        blur: 16,
 66        color: ramps
 67            .neutral(isLight ? 7 : 0)
 68            .darken()
 69            .alpha(0.2)
 70            .hex(), // TODO used blend previously. Replace with something else
 71        offset: [0, 2],
 72    }
 73
 74    let players = {
 75        "0": player(ramps.blue),
 76        "1": player(ramps.green),
 77        "2": player(ramps.magenta),
 78        "3": player(ramps.orange),
 79        "4": player(ramps.violet),
 80        "5": player(ramps.cyan),
 81        "6": player(ramps.red),
 82        "7": player(ramps.yellow),
 83    }
 84
 85    return {
 86        name,
 87        isLight,
 88
 89        ramps,
 90
 91        lowest,
 92        middle,
 93        highest,
 94
 95        popoverShadow,
 96        modalShadow,
 97
 98        players,
 99        syntax,
100    }
101}
102
103function player(ramp: Scale): Player {
104    return {
105        selection: ramp(0.5).alpha(0.24).hex(),
106        cursor: ramp(0.5).hex(),
107    }
108}
109
110function lowestLayer(ramps: RampSet): Layer {
111    return {
112        base: buildStyleSet(ramps.neutral, 0.2, 1),
113        variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
114        on: buildStyleSet(ramps.neutral, 0.1, 1),
115        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
116        positive: buildStyleSet(ramps.green, 0.1, 0.5),
117        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
118        negative: buildStyleSet(ramps.red, 0.1, 0.5),
119    }
120}
121
122function middleLayer(ramps: RampSet): Layer {
123    return {
124        base: buildStyleSet(ramps.neutral, 0.1, 1),
125        variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
126        on: buildStyleSet(ramps.neutral, 0, 1),
127        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
128        positive: buildStyleSet(ramps.green, 0.1, 0.5),
129        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
130        negative: buildStyleSet(ramps.red, 0.1, 0.5),
131    }
132}
133
134function highestLayer(ramps: RampSet): Layer {
135    return {
136        base: buildStyleSet(ramps.neutral, 0, 1),
137        variant: buildStyleSet(ramps.neutral, 0, 0.7),
138        on: buildStyleSet(ramps.neutral, 0.1, 1),
139        accent: buildStyleSet(ramps.blue, 0.1, 0.5),
140        positive: buildStyleSet(ramps.green, 0.1, 0.5),
141        warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
142        negative: buildStyleSet(ramps.red, 0.1, 0.5),
143    }
144}
145
146function buildStyleSet(
147    ramp: Scale,
148    backgroundBase: number,
149    foregroundBase: number,
150    step: number = 0.08
151): StyleSet {
152    let styleDefinitions = buildStyleDefinition(
153        backgroundBase,
154        foregroundBase,
155        step
156    )
157
158    function colorString(indexOrColor: number | Color): string {
159        if (typeof indexOrColor === "number") {
160            return ramp(indexOrColor).hex()
161        } else {
162            return indexOrColor.hex()
163        }
164    }
165
166    function buildStyle(style: Styles): Style {
167        return {
168            background: colorString(styleDefinitions.background[style]),
169            border: colorString(styleDefinitions.border[style]),
170            foreground: colorString(styleDefinitions.foreground[style]),
171        }
172    }
173
174    return {
175        default: buildStyle("default"),
176        hovered: buildStyle("hovered"),
177        pressed: buildStyle("pressed"),
178        active: buildStyle("active"),
179        disabled: buildStyle("disabled"),
180        inverted: buildStyle("inverted"),
181    }
182}
183
184function buildStyleDefinition(
185    bgBase: number,
186    fgBase: number,
187    step: number = 0.08
188) {
189    return {
190        background: {
191            default: bgBase,
192            hovered: bgBase + step,
193            pressed: bgBase + step * 1.5,
194            active: bgBase + step * 2.2,
195            disabled: bgBase,
196            inverted: fgBase + step * 6,
197        },
198        border: {
199            default: bgBase + step * 1,
200            hovered: bgBase + step,
201            pressed: bgBase + step,
202            active: bgBase + step * 3,
203            disabled: bgBase + step * 0.5,
204            inverted: bgBase - step * 3,
205        },
206        foreground: {
207            default: fgBase,
208            hovered: fgBase,
209            pressed: fgBase,
210            active: fgBase + step * 6,
211            disabled: bgBase + step * 4,
212            inverted: bgBase + step * 2,
213        },
214    }
215}