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