Skip to content

Commit c179cc3

Browse files
authored
feat(lib): initial release of color library (#118)
1 parent 84cbd9c commit c179cc3

File tree

2 files changed

+517
-0
lines changed

2 files changed

+517
-0
lines changed

lua/astrotheme/lib/color.lua

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
---AstroTheme Configuration
2+
---
3+
---Default configuration of AstroTheme
4+
---
5+
---This module can be loaded with `local astrotheme_config = require "astrotheme.lib.color"`
6+
---
7+
---copyright 2023 license GNU General Public License v3.0 @class astrocore
8+
---@class astrotheme.lib.color
9+
10+
---Converts an RGB array or single number to a
11+
---RGB dictionary.
12+
---@private
13+
---@param color Color | table | number
14+
---@return Color | table
15+
local function _to_rgb(color)
16+
if type(color) == "table" and color["r"] == nil then
17+
color.r = color[1]
18+
color.g = color[2]
19+
color.b = color[3]
20+
elseif type(color) == "number" then
21+
local num = color
22+
color = { r = num, g = num, b = num }
23+
end
24+
return color
25+
end
26+
27+
---@class Color
28+
---@field r number
29+
---@field g number
30+
---@field b number
31+
local Color = {}
32+
33+
Color.__index = Color
34+
35+
Color.__add = function(lhs, rhs)
36+
lhs = _to_rgb(lhs)
37+
rhs = _to_rgb(rhs)
38+
return setmetatable({
39+
r = lhs.r + rhs.r,
40+
g = lhs.g + rhs.g,
41+
b = lhs.b + rhs.b,
42+
}, Color)
43+
end
44+
45+
Color.__sub = function(lhs, rhs)
46+
lhs = _to_rgb(lhs)
47+
rhs = _to_rgb(rhs)
48+
return setmetatable({
49+
r = lhs.r - rhs.r,
50+
g = lhs.g - rhs.g,
51+
b = lhs.b - rhs.b,
52+
}, Color)
53+
end
54+
55+
Color.__div = function(lhs, rhs)
56+
lhs = _to_rgb(lhs)
57+
rhs = _to_rgb(rhs)
58+
return setmetatable({
59+
r = lhs.r / rhs.r,
60+
g = lhs.g / rhs.g,
61+
b = lhs.b / rhs.b,
62+
}, Color)
63+
end
64+
65+
Color.__mul = function(lhs, rhs)
66+
lhs = _to_rgb(lhs)
67+
rhs = _to_rgb(rhs)
68+
return setmetatable({
69+
r = lhs.r * rhs.r,
70+
g = lhs.g * rhs.g,
71+
b = lhs.b * rhs.b,
72+
}, Color)
73+
end
74+
75+
Color.__eq = function(lhs, rhs)
76+
lhs = _to_rgb(lhs)
77+
rhs = _to_rgb(rhs)
78+
local bool = false
79+
if lhs.r == rhs.r and lhs.g == rhs.g and lhs.b == rhs.b then bool = true end
80+
return bool
81+
end
82+
83+
Color.__lt = function(lhs, rhs)
84+
lhs = _to_rgb(lhs)
85+
rhs = _to_rgb(rhs)
86+
local y1 = 0.2126 * lhs.r + 0.7152 * lhs.g + 0.0722 * lhs.b
87+
local y2 = 0.2126 * rhs.r + 0.7152 * rhs.g + 0.0722 * rhs.b
88+
89+
return y1 < y2
90+
end
91+
92+
Color.__tostring = function(rgb)
93+
-- local text = string.format("{%f, %f, %f}", rgb.r, rgb.g, rgb.b)
94+
local text = " " .. rgb:tohex() .. " "
95+
local text_color = "0;0;0"
96+
if rgb:less(127) then text_color = "255;255;255" end
97+
local out = string.format("\27[48;2;%d;%d;%dm\27[38;2;%sm%s\27[0m", rgb.r, rgb.g, rgb.b, text_color, text)
98+
return out
99+
end
100+
101+
--- Creates a new Color object from a RGB table,
102+
--- a hex value, or neovim raw value.
103+
---@param color table | string | number
104+
---@return Color
105+
Color.new = function(color)
106+
if type(color) == "string" then
107+
local hex = color:gsub("#", "")
108+
109+
local r = tonumber("0x" .. hex:sub(1, 2))
110+
local g = tonumber("0x" .. hex:sub(3, 4))
111+
local b = tonumber("0x" .. hex:sub(5, 6))
112+
113+
color = { r = r, g = g, b = b }
114+
elseif type(color) == "table" then
115+
color = { r = color[1], g = color[2], b = color[3] }
116+
elseif type(color) == "number" then
117+
local raw = string.format("%06x", color)
118+
color = {
119+
r = tonumber(raw:sub(1, 2), 16),
120+
g = tonumber(raw:sub(3, 4), 16),
121+
b = tonumber(raw:sub(5, 6), 16),
122+
}
123+
end
124+
return setmetatable(color, Color)
125+
end
126+
127+
---Clamps RGB values between 0 and 255.
128+
---@param self Color
129+
---@return Color
130+
Color.clamp = function(self)
131+
local function clamp(num) return math.max(0, math.min(255, num)) end
132+
133+
return setmetatable({
134+
r = clamp(self.r),
135+
g = clamp(self.g),
136+
b = clamp(self.b),
137+
}, Color)
138+
end
139+
140+
---Rounds RGB values to the nearest integer.
141+
---@param self Color
142+
---@return Color
143+
Color.round = function(self)
144+
return setmetatable({
145+
r = math.floor(self.r + 0.5),
146+
g = math.floor(self.g + 0.5),
147+
b = math.floor(self.b + 0.5),
148+
}, Color)
149+
end
150+
151+
---Converts negative values to positive.
152+
---@param self Color
153+
---@return Color
154+
Color.abs = function(self)
155+
return setmetatable({
156+
r = math.abs(self.r),
157+
g = math.abs(self.g),
158+
b = math.abs(self.b),
159+
}, Color)
160+
end
161+
162+
---Evaluates if your base color is a lesser lightness value,
163+
---then your supplied value.
164+
---@param self Color
165+
---@param value number lightness
166+
---@return boolean
167+
Color.less = function(self, value)
168+
local y1 = 0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
169+
return y1 < value
170+
end
171+
172+
---Evaluates if the base color is a higher lightness value,
173+
---then your supplied value.
174+
---@param self Color
175+
---@param value number lightness
176+
---@return boolean
177+
Color.more = function(self, value)
178+
local y1 = 0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
179+
return y1 > value
180+
end
181+
182+
---Sorts color from min to max in a dictionary.
183+
---@param self Color
184+
---@return string[]
185+
Color.sort = function(self)
186+
local keys = {}
187+
for k, _ in pairs(self) do
188+
table.insert(keys, k)
189+
end
190+
191+
table.sort(keys, function(a, b) return self[a] < self[b] end)
192+
193+
return keys
194+
end
195+
196+
---Lerps between 2 colors.
197+
---@param self Color
198+
---@param color Color | string | number
199+
---@param amount number
200+
---@return Color
201+
Color.blend = function(self, color, amount)
202+
self = (self + (color - self) * amount):round():clamp()
203+
return self
204+
end
205+
206+
---Darken color.
207+
---@param self Color
208+
---@param amount number
209+
---@return Color
210+
Color.darken = function(self, amount)
211+
self = (self - amount):clamp()
212+
return self
213+
end
214+
215+
---Lighten color.
216+
---@param self Color
217+
---@param amount number
218+
---@return Color
219+
Color.lighten = function(self, amount)
220+
self = (self + amount):clamp()
221+
return self
222+
end
223+
224+
---Alternate darken mode with a bias none linear and can look better.
225+
---@param self Color
226+
---@param amount number
227+
---@return Color
228+
Color.darken_2 = function(self, amount)
229+
self = (self - { amount * 0.6, amount * 0.8, amount * 0.6 }):clamp()
230+
return self
231+
end
232+
233+
---Alternate lighten mode with a bias none linear and can look better.
234+
---@param self Color
235+
---@param amount number
236+
---@return Color
237+
Color.lighten_2 = function(self, amount)
238+
self = (self + { amount, amount * 0.6, amount * 0.6 }):clamp()
239+
return self
240+
end
241+
242+
---Desaturates color bringing it to it's lightness grey value.
243+
---@param self Color
244+
---@param amount number
245+
---@return Color
246+
Color.desat = function(self, amount)
247+
local y1 = 0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
248+
self = self:blend(y1, amount)
249+
return self
250+
end
251+
252+
--- TODO: This is a work in progress more artistic way of saturating
253+
--- this function is not ready and should not be used.
254+
255+
---Do not use this is a WIP algorithm
256+
-- Color.sat_2 = function(self, amount)
257+
-- local min, mid, max = unpack(self:sort())
258+
-- if self[mid] == self[max] then
259+
-- self[min] = self[min] - amount
260+
-- elseif self[min] == self[mid] then
261+
-- self[min] = self[min] - amount
262+
-- self[mid] = self[mid] - amount
263+
-- elseif self[min] == self[mid] and self[min] == self[max] then
264+
-- self[min] = self[min] - amount
265+
-- self[mid] = self[mid] - amount
266+
-- self[max] = self[max] - amount
267+
-- else
268+
-- local offset = 1 + (self[mid] - self[min]) / 255
269+
-- local adjust = math.abs(self[min] - amount)
270+
-- -- self[min] = self[min] - (amount * offset)
271+
-- self[min] = self[min] - (amount * 2)
272+
-- if self[min] >= 0 then adjust = 0 end
273+
-- self[mid] = (self[mid] - amount) + adjust
274+
-- end
275+
-- return self:clamp()
276+
-- end
277+
278+
---Saturates color while trying to preserve hue.
279+
---@param self Color
280+
---@param amount number
281+
---@return Color
282+
Color.sat = function(self, amount)
283+
local min, mid, max = unpack(self:sort())
284+
local sat = Color.new { self.r, self.g, self.b }
285+
286+
if self[mid] == self[max] then
287+
sat[min] = sat[min] - sat[min]
288+
elseif self[min] == self[mid] then
289+
sat[min] = sat[min] - sat[min]
290+
sat[mid] = sat[mid] - sat[mid]
291+
elseif self[min] == self[mid] and self[min] == self[max] then
292+
sat[min] = sat[min] - sat[min]
293+
sat[mid] = sat[mid] - sat[mid]
294+
sat[max] = sat[max] - sat[max]
295+
else
296+
local offset = (sat[mid] - sat[min]) / 255
297+
sat[min] = sat[min] - sat[min]
298+
sat[mid] = sat[mid] - (self[min] * offset)
299+
end
300+
return self:blend(sat, amount)
301+
end
302+
303+
---Calculates the difference between 2 colors lightness values.
304+
---@param self Color
305+
---@param color Color
306+
---@return number
307+
Color.dif = function(self, color)
308+
local y1 = 0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
309+
local y2 = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b
310+
return math.abs(math.floor((y1 - y2) + 0.5))
311+
end
312+
313+
Color.di = function(self, color) return Color.abs(color - self) end
314+
315+
---Converts Color object to a hex string.
316+
---@param self Color
317+
---@return string
318+
Color.tohex = function(self)
319+
local r = string.format("%02X", self.r)
320+
local g = string.format("%02X", self.g)
321+
local b = string.format("%02X", self.b)
322+
323+
return "#" .. r .. g .. b
324+
end
325+
326+
---Debug function that prints an int array of RGB value with,
327+
---terminal color support.
328+
---@param self Color
329+
---@return string
330+
Color.debug = function(self)
331+
local text = string.format("{ %.2f, %.2f, %.2f }", self.r, self.g, self.b)
332+
local text_color = "0;0;0"
333+
if self:less(127) then text_color = "255;255;255" end
334+
print(string.format("\27[48;2;%d;%d;%dm\27[38;2;%sm%s\27[0m", self.r, self.g, self.b, text_color, text))
335+
end
336+
337+
return Color

0 commit comments

Comments
 (0)