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}