base16.ts

  1import chroma, { Color, Scale } from "chroma-js";
  2import { fontWeights } from "../../common";
  3import { withOpacity } from "../../utils/color";
  4import Theme, { buildPlayer, Syntax } from "./theme";
  5
  6export function colorRamp(color: Color): Scale {
  7  let hue = color.hsl()[0];
  8  let endColor = chroma.hsl(hue, 0.88, 0.96);
  9  let startColor = chroma.hsl(hue, 0.68, 0.12);
 10  return chroma.scale([startColor, color, endColor]).mode("hsl");
 11}
 12
 13export function createTheme(
 14  name: string,
 15  isLight: boolean,
 16  color_ramps: { [rampName: string]: Scale }
 17): Theme {
 18  let ramps: typeof color_ramps = {};
 19  // Chromajs mutates the underlying ramp when you call domain. This causes problems because
 20  // we now store the ramps object in the theme so that we can pull colors out of them.
 21  // So instead of calling domain and storing the result, we have to construct new ramps for each
 22  // theme so that we don't modify the passed in ramps.
 23  // This combined with an error in the type definitions for chroma js means we have to cast the colors
 24  // function to any in order to get the colors back out from the original ramps.
 25  if (isLight) {
 26    for (var rampName in color_ramps) {
 27      ramps[rampName] = chroma
 28        .scale((color_ramps[rampName].colors as any)())
 29        .domain([1, 0]);
 30    }
 31    ramps.neutral = chroma
 32      .scale((color_ramps.neutral.colors as any)())
 33      .domain([7, 0]);
 34  } else {
 35    for (var rampName in color_ramps) {
 36      ramps[rampName] = chroma
 37        .scale((color_ramps[rampName].colors as any)())
 38        .domain([0, 1]);
 39    }
 40    ramps.neutral = chroma
 41      .scale((color_ramps.neutral.colors as any)())
 42      .domain([0, 7]);
 43  }
 44
 45  let blend = isLight ? 0.12 : 0.24;
 46
 47  function sample(ramp: Scale, index: number): string {
 48    return ramp(index).hex();
 49  }
 50  const darkest = ramps.neutral(isLight ? 7 : 0).hex();
 51
 52  const backgroundColor = {
 53    // Title bar
 54    100: {
 55      base: sample(ramps.neutral, 1.25),
 56      hovered: sample(ramps.neutral, 1.5),
 57      active: sample(ramps.neutral, 1.75),
 58    },
 59    // Midground (panels, etc)
 60    300: {
 61      base: sample(ramps.neutral, 1),
 62      hovered: sample(ramps.neutral, 1.25),
 63      active: sample(ramps.neutral, 1.5),
 64    },
 65    // Editor
 66    500: {
 67      base: sample(ramps.neutral, 0),
 68      hovered: sample(ramps.neutral, 0.25),
 69      active: sample(ramps.neutral, 0.5),
 70    },
 71    on300: {
 72      base: sample(ramps.neutral, 0),
 73      hovered: sample(ramps.neutral, 0.5),
 74      active: sample(ramps.neutral, 1),
 75    },
 76    on500: {
 77      base: sample(ramps.neutral, 1),
 78      hovered: sample(ramps.neutral, 1.5),
 79      active: sample(ramps.neutral, 2),
 80    },
 81    ok: {
 82      base: withOpacity(sample(ramps.green, 0.5), 0.15),
 83      hovered: withOpacity(sample(ramps.green, 0.5), 0.2),
 84      active: withOpacity(sample(ramps.green, 0.5), 0.25),
 85    },
 86    error: {
 87      base: withOpacity(sample(ramps.red, 0.5), 0.15),
 88      hovered: withOpacity(sample(ramps.red, 0.5), 0.2),
 89      active: withOpacity(sample(ramps.red, 0.5), 0.25),
 90    },
 91    on500Error: {
 92      base: sample(ramps.red, 0.05),
 93      hovered: sample(ramps.red, 0.1),
 94      active: sample(ramps.red, 0.15),
 95    },
 96    warning: {
 97      base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
 98      hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
 99      active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
100    },
101    on500Warning: {
102      base: sample(ramps.yellow, 0.05),
103      hovered: sample(ramps.yellow, 0.1),
104      active: sample(ramps.yellow, 0.15),
105    },
106    info: {
107      base: withOpacity(sample(ramps.blue, 0.5), 0.15),
108      hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
109      active: withOpacity(sample(ramps.blue, 0.5), 0.25),
110    },
111    on500Info: {
112      base: sample(ramps.blue, 0.05),
113      hovered: sample(ramps.blue, 0.1),
114      active: sample(ramps.blue, 0.15),
115    },
116  };
117
118  const borderColor = {
119    primary: sample(ramps.neutral, isLight ? 1.5 : 0),
120    secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
121    muted: sample(ramps.neutral, isLight ? 1 : 3),
122    active: sample(ramps.neutral, isLight ? 4 : 3),
123    onMedia: withOpacity(darkest, 0.1),
124    ok: sample(ramps.green, 0.3),
125    error: sample(ramps.red, 0.3),
126    warning: sample(ramps.yellow, 0.3),
127    info: sample(ramps.blue, 0.3),
128  };
129
130  const textColor = {
131    primary: sample(ramps.neutral, 6),
132    secondary: sample(ramps.neutral, 5),
133    muted: sample(ramps.neutral, 4),
134    placeholder: sample(ramps.neutral, 3),
135    active: sample(ramps.neutral, 7),
136    feature: sample(ramps.blue, 0.5),
137    ok: sample(ramps.green, 0.5),
138    error: sample(ramps.red, 0.5),
139    warning: sample(ramps.yellow, 0.5),
140    info: sample(ramps.blue, 0.5),
141    onMedia: darkest,
142  };
143
144  const player = {
145    1: buildPlayer(sample(ramps.blue, 0.5)),
146    2: buildPlayer(sample(ramps.green, 0.5)),
147    3: buildPlayer(sample(ramps.magenta, 0.5)),
148    4: buildPlayer(sample(ramps.orange, 0.5)),
149    5: buildPlayer(sample(ramps.violet, 0.5)),
150    6: buildPlayer(sample(ramps.cyan, 0.5)),
151    7: buildPlayer(sample(ramps.red, 0.5)),
152    8: buildPlayer(sample(ramps.yellow, 0.5)),
153  };
154
155  const editor = {
156    background: backgroundColor[500].base,
157    indent_guide: borderColor.muted,
158    indent_guide_active: borderColor.secondary,
159    line: {
160      active: sample(ramps.neutral, 1),
161      highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
162    },
163    highlight: {
164      selection: player[1].selectionColor,
165      occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
166      activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
167      matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
168      match: sample(ramps.violet, 0.15),
169      activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
170      related: backgroundColor[500].hovered,
171    },
172    gutter: {
173      primary: textColor.placeholder,
174      active: textColor.active,
175    },
176  };
177
178  const syntax: Syntax = {
179    primary: {
180      color: sample(ramps.neutral, 7),
181      weight: fontWeights.normal,
182    },
183    comment: {
184      color: sample(ramps.neutral, 5),
185      weight: fontWeights.normal,
186    },
187    punctuation: {
188      color: sample(ramps.neutral, 6),
189      weight: fontWeights.normal,
190    },
191    constant: {
192      color: sample(ramps.neutral, 4),
193      weight: fontWeights.normal,
194    },
195    keyword: {
196      color: sample(ramps.blue, 0.5),
197      weight: fontWeights.normal,
198    },
199    function: {
200      color: sample(ramps.yellow, 0.5),
201      weight: fontWeights.normal,
202    },
203    type: {
204      color: sample(ramps.cyan, 0.5),
205      weight: fontWeights.normal,
206    },
207    constructor: {
208      color: sample(ramps.blue, 0.5),
209      weight: fontWeights.normal,
210    },
211    variant: {
212      color: sample(ramps.blue, 0.5),
213      weight: fontWeights.normal,
214    },
215    property: {
216      color: sample(ramps.blue, 0.5),
217      weight: fontWeights.normal,
218    },
219    enum: {
220      color: sample(ramps.orange, 0.5),
221      weight: fontWeights.normal,
222    },
223    operator: {
224      color: sample(ramps.orange, 0.5),
225      weight: fontWeights.normal,
226    },
227    string: {
228      color: sample(ramps.orange, 0.5),
229      weight: fontWeights.normal,
230    },
231    number: {
232      color: sample(ramps.green, 0.5),
233      weight: fontWeights.normal,
234    },
235    boolean: {
236      color: sample(ramps.green, 0.5),
237      weight: fontWeights.normal,
238    },
239    predictive: {
240      color: textColor.muted,
241      weight: fontWeights.normal,
242    },
243    title: {
244      color: sample(ramps.yellow, 0.5),
245      weight: fontWeights.bold,
246    },
247    emphasis: {
248      color: textColor.feature,
249      weight: fontWeights.normal,
250    },
251    "emphasis.strong": {
252      color: textColor.feature,
253      weight: fontWeights.bold,
254    },
255    linkUri: {
256      color: sample(ramps.green, 0.5),
257      weight: fontWeights.normal,
258      underline: true,
259    },
260    linkText: {
261      color: sample(ramps.orange, 0.5),
262      weight: fontWeights.normal,
263      italic: true,
264    },
265  };
266
267  const shadow = withOpacity(
268    ramps
269      .neutral(isLight ? 7 : 0)
270      .darken()
271      .hex(),
272    blend
273  );
274
275  return {
276    name,
277    isLight,
278    backgroundColor,
279    borderColor,
280    textColor,
281    iconColor: textColor,
282    editor,
283    syntax,
284    player,
285    shadow,
286    ramps,
287  };
288}