1import chroma, { Color, Scale } from "chroma-js"
2import {
3 ColorScheme,
4 Layer,
5 Player,
6 RampSet,
7 Style,
8 Styles,
9 StyleSet,
10 ThemeSyntax,
11} from "./colorScheme"
12
13export function colorRamp(color: Color): Scale {
14 let endColor = color.desaturate(1).brighten(5)
15 let startColor = color.desaturate(1).darken(4)
16 return chroma.scale([startColor, color, endColor]).mode("lab")
17}
18
19export function createColorScheme(
20 name: string,
21 isLight: boolean,
22 colorRamps: { [rampName: string]: Scale },
23 syntax?: ThemeSyntax
24): ColorScheme {
25 // Chromajs scales from 0 to 1 flipped if isLight is true
26 let ramps: RampSet = {} as any
27
28 // Chromajs mutates the underlying ramp when you call domain. This causes problems because
29 // we now store the ramps object in the theme so that we can pull colors out of them.
30 // So instead of calling domain and storing the result, we have to construct new ramps for each
31 // theme so that we don't modify the passed in ramps.
32 // This combined with an error in the type definitions for chroma js means we have to cast the colors
33 // function to any in order to get the colors back out from the original ramps.
34 if (isLight) {
35 for (var rampName in colorRamps) {
36 ;(ramps as any)[rampName] = chroma.scale(
37 colorRamps[rampName].colors(100).reverse()
38 )
39 }
40 ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse())
41 } else {
42 for (var rampName in colorRamps) {
43 ;(ramps as any)[rampName] = chroma.scale(
44 colorRamps[rampName].colors(100)
45 )
46 }
47 ramps.neutral = chroma.scale(colorRamps.neutral.colors(100))
48 }
49
50 let lowest = lowestLayer(ramps)
51 let middle = middleLayer(ramps)
52 let highest = highestLayer(ramps)
53
54 let popoverShadow = {
55 blur: 4,
56 color: ramps
57 .neutral(isLight ? 7 : 0)
58 .darken()
59 .alpha(0.2)
60 .hex(), // TODO used blend previously. Replace with something else
61 offset: [1, 2],
62 }
63
64 let modalShadow = {
65 blur: 16,
66 color: ramps
67 .neutral(isLight ? 7 : 0)
68 .darken()
69 .alpha(0.2)
70 .hex(), // TODO used blend previously. Replace with something else
71 offset: [0, 2],
72 }
73
74 let players = {
75 "0": player(ramps.blue),
76 "1": player(ramps.green),
77 "2": player(ramps.magenta),
78 "3": player(ramps.orange),
79 "4": player(ramps.violet),
80 "5": player(ramps.cyan),
81 "6": player(ramps.red),
82 "7": player(ramps.yellow),
83 }
84
85 return {
86 name,
87 isLight,
88
89 ramps,
90
91 lowest,
92 middle,
93 highest,
94
95 popoverShadow,
96 modalShadow,
97
98 players,
99 syntax,
100 }
101}
102
103function player(ramp: Scale): Player {
104 return {
105 selection: ramp(0.5).alpha(0.24).hex(),
106 cursor: ramp(0.5).hex(),
107 }
108}
109
110function lowestLayer(ramps: RampSet): Layer {
111 return {
112 base: buildStyleSet(ramps.neutral, 0.2, 1),
113 variant: buildStyleSet(ramps.neutral, 0.2, 0.7),
114 on: buildStyleSet(ramps.neutral, 0.1, 1),
115 accent: buildStyleSet(ramps.blue, 0.1, 0.5),
116 positive: buildStyleSet(ramps.green, 0.1, 0.5),
117 warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
118 negative: buildStyleSet(ramps.red, 0.1, 0.5),
119 }
120}
121
122function middleLayer(ramps: RampSet): Layer {
123 return {
124 base: buildStyleSet(ramps.neutral, 0.1, 1),
125 variant: buildStyleSet(ramps.neutral, 0.1, 0.7),
126 on: buildStyleSet(ramps.neutral, 0, 1),
127 accent: buildStyleSet(ramps.blue, 0.1, 0.5),
128 positive: buildStyleSet(ramps.green, 0.1, 0.5),
129 warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
130 negative: buildStyleSet(ramps.red, 0.1, 0.5),
131 }
132}
133
134function highestLayer(ramps: RampSet): Layer {
135 return {
136 base: buildStyleSet(ramps.neutral, 0, 1),
137 variant: buildStyleSet(ramps.neutral, 0, 0.7),
138 on: buildStyleSet(ramps.neutral, 0.1, 1),
139 accent: buildStyleSet(ramps.blue, 0.1, 0.5),
140 positive: buildStyleSet(ramps.green, 0.1, 0.5),
141 warning: buildStyleSet(ramps.yellow, 0.1, 0.5),
142 negative: buildStyleSet(ramps.red, 0.1, 0.5),
143 }
144}
145
146function buildStyleSet(
147 ramp: Scale,
148 backgroundBase: number,
149 foregroundBase: number,
150 step: number = 0.08
151): StyleSet {
152 let styleDefinitions = buildStyleDefinition(
153 backgroundBase,
154 foregroundBase,
155 step
156 )
157
158 function colorString(indexOrColor: number | Color): string {
159 if (typeof indexOrColor === "number") {
160 return ramp(indexOrColor).hex()
161 } else {
162 return indexOrColor.hex()
163 }
164 }
165
166 function buildStyle(style: Styles): Style {
167 return {
168 background: colorString(styleDefinitions.background[style]),
169 border: colorString(styleDefinitions.border[style]),
170 foreground: colorString(styleDefinitions.foreground[style]),
171 }
172 }
173
174 return {
175 default: buildStyle("default"),
176 hovered: buildStyle("hovered"),
177 pressed: buildStyle("pressed"),
178 active: buildStyle("active"),
179 disabled: buildStyle("disabled"),
180 inverted: buildStyle("inverted"),
181 }
182}
183
184function buildStyleDefinition(
185 bgBase: number,
186 fgBase: number,
187 step: number = 0.08
188) {
189 return {
190 background: {
191 default: bgBase,
192 hovered: bgBase + step,
193 pressed: bgBase + step * 1.5,
194 active: bgBase + step * 2.2,
195 disabled: bgBase,
196 inverted: fgBase + step * 6,
197 },
198 border: {
199 default: bgBase + step * 1,
200 hovered: bgBase + step,
201 pressed: bgBase + step,
202 active: bgBase + step * 3,
203 disabled: bgBase + step * 0.5,
204 inverted: bgBase - step * 3,
205 },
206 foreground: {
207 default: fgBase,
208 hovered: fgBase,
209 pressed: fgBase,
210 active: fgBase + step * 6,
211 disabled: bgBase + step * 4,
212 inverted: bgBase + step * 2,
213 },
214 }
215}