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