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    success: sample(ramps.green, 0.5),
141    info: sample(ramps.blue, 0.5),
142    onMedia: darkest,
143  };
144
145  const player = {
146    1: buildPlayer(sample(ramps.blue, 0.5)),
147    2: buildPlayer(sample(ramps.green, 0.5)),
148    3: buildPlayer(sample(ramps.magenta, 0.5)),
149    4: buildPlayer(sample(ramps.orange, 0.5)),
150    5: buildPlayer(sample(ramps.violet, 0.5)),
151    6: buildPlayer(sample(ramps.cyan, 0.5)),
152    7: buildPlayer(sample(ramps.red, 0.5)),
153    8: buildPlayer(sample(ramps.yellow, 0.5)),
154  };
155
156  const editor = {
157    background: backgroundColor[500].base,
158    indent_guide: borderColor.muted,
159    indent_guide_active: borderColor.secondary,
160    line: {
161      active: sample(ramps.neutral, 1),
162      highlighted: sample(ramps.neutral, 1.25), // TODO: Where is this used?
163    },
164    highlight: {
165      selection: player[1].selectionColor,
166      occurrence: withOpacity(sample(ramps.neutral, 3.5), blend),
167      activeOccurrence: withOpacity(sample(ramps.neutral, 3.5), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
168      matchingBracket: backgroundColor[500].active, // TODO: Not hooked up
169      match: sample(ramps.violet, 0.15),
170      activeMatch: withOpacity(sample(ramps.violet, 0.4), blend * 2), // TODO: Not hooked up - https://github.com/zed-industries/zed/issues/751
171      related: backgroundColor[500].hovered,
172    },
173    gutter: {
174      primary: textColor.placeholder,
175      active: textColor.active,
176    },
177  };
178
179  const syntax: Syntax = {
180    primary: {
181      color: sample(ramps.neutral, 7),
182      weight: fontWeights.normal,
183    },
184    comment: {
185      color: sample(ramps.neutral, 5),
186      weight: fontWeights.normal,
187    },
188    punctuation: {
189      color: sample(ramps.neutral, 6),
190      weight: fontWeights.normal,
191    },
192    constant: {
193      color: sample(ramps.neutral, 4),
194      weight: fontWeights.normal,
195    },
196    keyword: {
197      color: sample(ramps.blue, 0.5),
198      weight: fontWeights.normal,
199    },
200    function: {
201      color: sample(ramps.yellow, 0.5),
202      weight: fontWeights.normal,
203    },
204    type: {
205      color: sample(ramps.cyan, 0.5),
206      weight: fontWeights.normal,
207    },
208    constructor: {
209      color: sample(ramps.blue, 0.5),
210      weight: fontWeights.normal,
211    },
212    variant: {
213      color: sample(ramps.blue, 0.5),
214      weight: fontWeights.normal,
215    },
216    property: {
217      color: sample(ramps.blue, 0.5),
218      weight: fontWeights.normal,
219    },
220    enum: {
221      color: sample(ramps.orange, 0.5),
222      weight: fontWeights.normal,
223    },
224    operator: {
225      color: sample(ramps.orange, 0.5),
226      weight: fontWeights.normal,
227    },
228    string: {
229      color: sample(ramps.orange, 0.5),
230      weight: fontWeights.normal,
231    },
232    number: {
233      color: sample(ramps.green, 0.5),
234      weight: fontWeights.normal,
235    },
236    boolean: {
237      color: sample(ramps.green, 0.5),
238      weight: fontWeights.normal,
239    },
240    predictive: {
241      color: textColor.muted,
242      weight: fontWeights.normal,
243    },
244    title: {
245      color: sample(ramps.yellow, 0.5),
246      weight: fontWeights.bold,
247    },
248    emphasis: {
249      color: textColor.feature,
250      weight: fontWeights.normal,
251    },
252    "emphasis.strong": {
253      color: textColor.feature,
254      weight: fontWeights.bold,
255    },
256    linkUri: {
257      color: sample(ramps.green, 0.5),
258      weight: fontWeights.normal,
259      underline: true,
260    },
261    linkText: {
262      color: sample(ramps.orange, 0.5),
263      weight: fontWeights.normal,
264      italic: true,
265    },
266  };
267
268  const shadow = withOpacity(
269    ramps
270      .neutral(isLight ? 7 : 0)
271      .darken()
272      .hex(),
273    blend
274  );
275
276  return {
277    name,
278    isLight,
279    backgroundColor,
280    borderColor,
281    textColor,
282    iconColor: textColor,
283    editor,
284    syntax,
285    player,
286    shadow,
287    ramps,
288  };
289}