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    warning: {
 92      base: withOpacity(sample(ramps.yellow, 0.5), 0.15),
 93      hovered: withOpacity(sample(ramps.yellow, 0.5), 0.2),
 94      active: withOpacity(sample(ramps.yellow, 0.5), 0.25),
 95    },
 96    info: {
 97      base: withOpacity(sample(ramps.blue, 0.5), 0.15),
 98      hovered: withOpacity(sample(ramps.blue, 0.5), 0.2),
 99      active: withOpacity(sample(ramps.blue, 0.5), 0.25),
100    },
101  };
102
103  const borderColor = {
104    primary: sample(ramps.neutral, isLight ? 1.5 : 0),
105    secondary: sample(ramps.neutral, isLight ? 1.25 : 1),
106    muted: sample(ramps.neutral, isLight ? 1 : 3),
107    active: sample(ramps.neutral, isLight ? 4 : 3),
108    onMedia: withOpacity(darkest, 0.1),
109    ok: withOpacity(sample(ramps.green, 0.5), 0.15),
110    error: withOpacity(sample(ramps.red, 0.5), 0.15),
111    warning: withOpacity(sample(ramps.yellow, 0.5), 0.15),
112    info: withOpacity(sample(ramps.blue, 0.5), 0.15),
113  };
114
115  const textColor = {
116    primary: sample(ramps.neutral, 6),
117    secondary: sample(ramps.neutral, 5),
118    muted: sample(ramps.neutral, 5),
119    placeholder: sample(ramps.neutral, 4),
120    active: sample(ramps.neutral, 7),
121    feature: sample(ramps.blue, 0.5),
122    ok: sample(ramps.green, 0.5),
123    error: sample(ramps.red, 0.5),
124    warning: sample(ramps.yellow, 0.5),
125    info: sample(ramps.blue, 0.5),
126    onMedia: darkest,
127  };
128
129  const player = {
130    1: buildPlayer(sample(ramps.blue, 0.5)),
131    2: buildPlayer(sample(ramps.green, 0.5)),
132    3: buildPlayer(sample(ramps.magenta, 0.5)),
133    4: buildPlayer(sample(ramps.orange, 0.5)),
134    5: buildPlayer(sample(ramps.violet, 0.5)),
135    6: buildPlayer(sample(ramps.cyan, 0.5)),
136    7: buildPlayer(sample(ramps.red, 0.5)),
137    8: buildPlayer(sample(ramps.yellow, 0.5)),
138  };
139
140  const editor = {
141    background: backgroundColor[500].base,
142    indent_guide: borderColor.muted,
143    indent_guide_active: borderColor.secondary,
144    line: {
145      active: sample(ramps.neutral, 1),
146      highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
147    },
148    highlight: {
149      selection: player[1].selectionColor,
150      occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
151      activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
152      matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
153      match: sample(ramps.violet, 0.15),
154      activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
155      related: backgroundColor[500].hovered,
156    },
157    gutter: {
158      primary: textColor.placeholder,
159      active: textColor.active,
160    },
161  };
162
163  const syntax: Syntax = {
164    primary: {
165      color: sample(ramps.neutral, 7),
166      weight: fontWeights.normal,
167    },
168    comment: {
169      color: sample(ramps.neutral, 5),
170      weight: fontWeights.normal,
171    },
172    punctuation: {
173      color: sample(ramps.neutral, 6),
174      weight: fontWeights.normal,
175    },
176    constant: {
177      color: sample(ramps.neutral, 4),
178      weight: fontWeights.normal,
179    },
180    keyword: {
181      color: sample(ramps.blue, 0.5),
182      weight: fontWeights.normal,
183    },
184    function: {
185      color: sample(ramps.yellow, 0.5),
186      weight: fontWeights.normal,
187    },
188    type: {
189      color: sample(ramps.cyan, 0.5),
190      weight: fontWeights.normal,
191    },
192    constructor: {
193      color: sample(ramps.blue, 0.5),
194      weight: fontWeights.normal,
195    },
196    variant: {
197      color: sample(ramps.blue, 0.5),
198      weight: fontWeights.normal,
199    },
200    property: {
201      color: sample(ramps.blue, 0.5),
202      weight: fontWeights.normal,
203    },
204    enum: {
205      color: sample(ramps.orange, 0.5),
206      weight: fontWeights.normal,
207    },
208    operator: {
209      color: sample(ramps.orange, 0.5),
210      weight: fontWeights.normal,
211    },
212    string: {
213      color: sample(ramps.orange, 0.5),
214      weight: fontWeights.normal,
215    },
216    number: {
217      color: sample(ramps.green, 0.5),
218      weight: fontWeights.normal,
219    },
220    boolean: {
221      color: sample(ramps.green, 0.5),
222      weight: fontWeights.normal,
223    },
224    predictive: {
225      color: textColor.muted,
226      weight: fontWeights.normal,
227    },
228    title: {
229      color: sample(ramps.yellow, 0.5),
230      weight: fontWeights.bold,
231    },
232    emphasis: {
233      color: textColor.feature,
234      weight: fontWeights.normal,
235    },
236    "emphasis.strong": {
237      color: textColor.feature,
238      weight: fontWeights.bold,
239    },
240    linkUri: {
241      color: sample(ramps.green, 0.5),
242      weight: fontWeights.normal,
243      underline: true,
244    },
245    linkText: {
246      color: sample(ramps.orange, 0.5),
247      weight: fontWeights.normal,
248      italic: true,
249    },
250  };
251
252  const shadow = withOpacity(
253    ramps
254      .neutral(isLight ? 7 : 0)
255      .darken()
256      .hex(),
257    blend
258  );
259
260  return {
261    name,
262    isLight,
263    backgroundColor,
264    borderColor,
265    textColor,
266    iconColor: textColor,
267    editor,
268    syntax,
269    player,
270    shadow,
271    ramps,
272  };
273}