Skip to content

Commit b888267

Browse files
committed
feat: Added rough palettes based on a primer designs
I added the `color` library to a new module called `lib`, with credit to @EdenEast from nightfox.nvim. With this commit, I rewrote the palettes (also inspired by the nightfox codebase) and assigned metadata to the newly added colorscheme, as I mentioned the names of the colorschemes in #226.
1 parent e21282d commit b888267

10 files changed

+2266
-0
lines changed

lua/github-theme/lib/color.lua

+356
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
-- Credit EdenEast
2+
-- https://github.com/EdenEast/nightfox.nvim/blob/12e0ca70e978f58318e7f0279bb7b243ababbd49/lua/nightfox/lib/color.lua
3+
4+
---Round float to nearest int
5+
---@param x number Float
6+
---@return number
7+
local function round(x)
8+
return x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)
9+
end
10+
11+
---Clamp value between the min and max values.
12+
---@param value number
13+
---@param min number
14+
---@param max number
15+
local function clamp(value, min, max)
16+
if value < min then
17+
return min
18+
elseif value > max then
19+
return max
20+
end
21+
return value
22+
end
23+
24+
--#region Types ----------------------------------------------------------------
25+
26+
---RGBA color representation stored in float [0,1]
27+
---@class RGBA
28+
---@field red number [0,255]
29+
---@field green number [0,255]
30+
---@field blue number [0,255]
31+
---@field alpha number [0,1]
32+
33+
---@class HSL
34+
---@field hue number Float [0,360)
35+
---@field saturation number Float [0,100]
36+
---@field lightness number Float [0,100]
37+
38+
---@class HSV
39+
---@field hue number Float [0,360)
40+
---@field saturation number Float [0,100]
41+
---@field value number Float [0,100]
42+
43+
--#endregion
44+
45+
--#region Helpers --------------------------------------------------------------
46+
47+
local function calc_hue(r, g, b)
48+
local max = math.max(r, g, b)
49+
local min = math.min(r, g, b)
50+
local delta = max - min
51+
local h = 0
52+
53+
if max == min then
54+
h = 0
55+
elseif max == r then
56+
h = 60 * ((g - b) / delta)
57+
elseif max == g then
58+
h = 60 * ((b - r) / delta + 2)
59+
elseif max == b then
60+
h = 60 * ((r - g) / delta + 4)
61+
end
62+
63+
if h < 0 then
64+
h = h + 360
65+
end
66+
67+
return { hue = h, max = max, min = min, delta = delta }
68+
end
69+
70+
--#endregion
71+
72+
local Color = setmetatable({}, {})
73+
Color.__index = Color
74+
75+
function Color.__tostring(self)
76+
return self:to_css()
77+
end
78+
79+
function Color.new(opts)
80+
if type(opts) == 'string' or type(opts) == 'number' then
81+
return Color.from_hex(opts)
82+
end
83+
if opts.red then
84+
return Color.from_rgba(opts.red, opts.green, opts.blue, opts.alpha)
85+
end
86+
if opts.value then
87+
return Color.from_hsv(opts.hue, opts.saturation, opts.value)
88+
end
89+
if opts.lightness then
90+
return Color.from_hsv(opts.hue, opts.saturation, opts.lightness)
91+
end
92+
end
93+
94+
function Color.init(r, g, b, a)
95+
local self = setmetatable({}, Color)
96+
self.red = clamp(r, 0, 1)
97+
self.green = clamp(g, 0, 1)
98+
self.blue = clamp(b, 0, 1)
99+
self.alpha = clamp(a or 1, 0, 1)
100+
return self
101+
end
102+
103+
--#region from_* ---------------------------------------------------------------
104+
105+
---Create color from RGBA 0,255
106+
---@param r number Integer [0,255]
107+
---@param g number Integer [0,255]
108+
---@param b number Integer [0,255]
109+
---@param a number Float [0,1]
110+
---@return Color
111+
function Color.from_rgba(r, g, b, a)
112+
return Color.init(r / 0xff, g / 0xff, b / 0xff, a or 1)
113+
end
114+
115+
---Create a color from a hex number
116+
---@param c number|string Either a literal number or a css-style hex string ('#RRGGBB[AA]')
117+
---@return Color
118+
function Color.from_hex(c)
119+
local n = c
120+
if type(c) == 'string' then
121+
local s = c:lower():match('#?([a-f0-9]+)')
122+
n = tonumber(s, 16)
123+
if #s <= 6 then
124+
n = bit.lshift(n, 8) + 0xff
125+
end
126+
end
127+
128+
return Color.init(bit.rshift(n, 24) / 0xff, bit.band(bit.rshift(n, 16), 0xff) / 0xff, bit.band(bit.rshift(n, 8), 0xff) / 0xff, bit.band(n, 0xff) / 0xff)
129+
end
130+
131+
---Create a Color from HSV value
132+
---@param h number Hue. Float [0,360]
133+
---@param s number Saturation. Float [0,100]
134+
---@param v number Value. Float [0,100]
135+
---@param a number? (Optional) Alpha. Float [0,1]
136+
---@return Color
137+
function Color.from_hsv(h, s, v, a)
138+
h = h % 360
139+
s = clamp(s, 0, 100) / 100
140+
v = clamp(v, 0, 100) / 100
141+
a = clamp(a or 1, 0, 1)
142+
143+
local function f(n)
144+
local k = (n + h / 60) % 6
145+
return v - v * s * math.max(math.min(k, 4 - k, 1), 0)
146+
end
147+
148+
return Color.init(f(5), f(3), f(1), a)
149+
end
150+
151+
---Create a Color from HSL value
152+
---@param h number Hue. Float [0,360]
153+
---@param s number Saturation. Float [0,100]
154+
---@param l number Lightness. Float [0,100]
155+
---@param a number? (Optional) Alpha. Float [0,1]
156+
---@return Color
157+
function Color.from_hsl(h, s, l, a)
158+
h = h % 360
159+
s = clamp(s, 0, 100) / 100
160+
l = clamp(l, 0, 100) / 100
161+
a = clamp(a or 1, 0, 1)
162+
local _a = s * math.min(l, 1 - l)
163+
164+
local function f(n)
165+
local k = (n + h / 30) % 12
166+
return l - _a * math.max(math.min(k - 3, 9 - k, 1), -1)
167+
end
168+
169+
return Color.init(f(0), f(8), f(4), a)
170+
end
171+
172+
--#endregion
173+
174+
--#region to_* -----------------------------------------------------------------
175+
176+
---Convert Color to RGBA
177+
---@return RGBA
178+
function Color:to_rgba()
179+
return {
180+
red = round(self.red * 0xff),
181+
green = round(self.green * 0xff),
182+
blue = round(self.blue * 0xff),
183+
alpha = self.alpha,
184+
}
185+
end
186+
187+
---Convert Color to HSV
188+
---@return HSV
189+
function Color:to_hsv()
190+
local res = calc_hue(self.red, self.green, self.blue)
191+
local h, min, max = res.hue, res.min, res.max
192+
local s, v = 0, max
193+
194+
if max ~= 0 then
195+
s = (max - min) / max
196+
end
197+
198+
return { hue = h, saturation = s * 100, value = v * 100 }
199+
end
200+
201+
---Convert the color to HSL.
202+
---@return HSL
203+
function Color:to_hsl()
204+
local res = calc_hue(self.red, self.green, self.blue)
205+
local h, min, max = res.hue, res.min, res.max
206+
local s, l = 0, (max + min) / 2
207+
208+
if max ~= 0 and min ~= 1 then
209+
s = (max - l) / math.min(l, 1 - l)
210+
end
211+
212+
return { hue = h, saturation = s * 100, lightness = l * 100 }
213+
end
214+
215+
---Convert the color to a hex number representation (`0xRRGGBB[AA]`).
216+
---@param with_alpha boolean Include the alpha component.
217+
---@return integer
218+
function Color:to_hex(with_alpha)
219+
local ls, bor, fl = bit.lshift, bit.bor, math.floor
220+
local n = bor(bor(ls(fl((self.red * 0xff) + 0.5), 16), ls(fl((self.green * 0xff) + 0.5), 8)), fl((self.blue * 0xff) + 0.5))
221+
return with_alpha and bit.lshift(n, 8) + (self.alpha * 0xff) or n
222+
end
223+
224+
---Convert the color to a css hex color (`#RRGGBB[AA]`).
225+
---@param with_alpha boolean Include the alpha component.
226+
---@return string
227+
function Color:to_css(with_alpha)
228+
local n = self:to_hex(with_alpha)
229+
local l = with_alpha and 8 or 6
230+
return string.format('#%0' .. l .. 'x', n)
231+
end
232+
233+
---Calculate the relative lumanance of the color
234+
---https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
235+
---@return number
236+
function Color:lumanance()
237+
local r, g, b = self.red, self.green, self.blue
238+
r = (r > 0.04045) and ((r + 0.055) / 1.055) ^ 2.4 or (r / 12.92)
239+
g = (g > 0.04045) and ((g + 0.055) / 1.055) ^ 2.4 or (g / 12.92)
240+
b = (b > 0.04045) and ((b + 0.055) / 1.055) ^ 2.4 or (b / 12.92)
241+
242+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
243+
end
244+
245+
--#endregion
246+
247+
--#region Manipulate -----------------------------------------------------------
248+
249+
---Returns a new color that a linear blend between two colors
250+
---@param other Color
251+
---@param f number Float [0,1]. 0 being this and 1 being other
252+
---@return Color
253+
function Color:blend(other, f)
254+
return Color.init((other.red - self.red) * f + self.red, (other.green - self.green) * f + self.green, (other.blue - self.blue) * f + self.blue, self.alpha)
255+
end
256+
257+
---Returns a new shaded color.
258+
---@param f number Amount. Float [-1,1]. -1 is black, 1 is white
259+
---@return Color
260+
function Color:shade(f)
261+
local t = f < 0 and 0 or 1.0
262+
local p = f < 0 and f * -1.0 or f
263+
264+
return Color.init((t - self.red) * p + self.red, (t - self.green) * p + self.green, (t - self.blue) * p + self.blue, self.alpha)
265+
end
266+
267+
---Adds value of `v` to the `value` of the current color. This returns either
268+
---a brighter version if +v and darker if -v.
269+
---@param v number Value. Float [-100,100].
270+
---@return Color
271+
function Color:brighten(v)
272+
local hsv = self:to_hsv()
273+
local value = clamp(hsv.value + v, 0, 100)
274+
return Color.from_hsv(hsv.hue, hsv.saturation, value)
275+
end
276+
277+
---Adds value of `v` to the `lightness` of the current color. This returns
278+
---either a lighter version if +v and darker if -v.
279+
---@param v number Lightness. Float [-100,100].
280+
---@return Color
281+
function Color:lighten(v)
282+
local hsl = self:to_hsl()
283+
local lightness = clamp(hsl.lightness + v, 0, 100)
284+
return Color.from_hsl(hsl.hue, hsl.saturation, lightness)
285+
end
286+
287+
---Adds value of `v` to the `saturation` of the current color. This returns
288+
---either a more or less saturated version depending of +/- v.
289+
---@param v number Saturation. Float [-100,100].
290+
---@return Color
291+
function Color:saturate(v)
292+
local hsv = self:to_hsv()
293+
local saturation = clamp(hsv.saturation + v, 0, 100)
294+
return Color.from_hsv(hsv.hue, saturation, hsv.value)
295+
end
296+
297+
---Adds value of `v` to the `hue` of the current color. This returns a rotation of
298+
---hue based on +/- of v. Resulting `hue` is wrapped [0,360]
299+
---@return Color
300+
function Color:rotate(v)
301+
local hsv = self:to_hsv()
302+
local hue = (hsv.hue + v) % 360
303+
return Color.from_hsv(hue, hsv.saturation, hsv.value)
304+
end
305+
306+
--#endregion
307+
308+
--#region Constants ------------------------------------------------------------
309+
310+
Color.WHITE = Color.init(1, 1, 1, 1)
311+
Color.BLACK = Color.init(0, 0, 0, 1)
312+
Color.BG = Color.init(0, 0, 0, 1)
313+
314+
--#endregion
315+
316+
--#region ty --------------------------------------------------------------
317+
318+
---Returns the contrast ratio of the other against another
319+
---@param other Color
320+
function Color:contrast(other)
321+
local l1 = self:lumanance()
322+
local l2 = other:lumanance()
323+
if l2 > l1 then
324+
l1, l2 = l2, l1
325+
end
326+
return (l1 + 0.05) / (l2 + 0.05)
327+
end
328+
329+
---Check if color passes WCAG AA
330+
---https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
331+
---@param background Color background to check against
332+
---@return boolean, number
333+
function Color:valid_wcag_aa(background)
334+
local ratio = self:contrast(background)
335+
return ratio >= 4.5, ratio
336+
end
337+
338+
--#endregion
339+
340+
local mt = getmetatable(Color)
341+
function mt.__call(_, opts)
342+
if type(opts) == 'string' or type(opts) == 'number' then
343+
return Color.from_hex(opts)
344+
end
345+
if opts.red then
346+
return Color.from_rgba(opts.red, opts.green, opts.blue, opts.alpha)
347+
end
348+
if opts.value then
349+
return Color.from_hsv(opts.hue, opts.saturation, opts.value)
350+
end
351+
if opts.lightness then
352+
return Color.from_hsl(opts.hue, opts.saturation, opts.lightness)
353+
end
354+
end
355+
356+
return Color

0 commit comments

Comments
 (0)