Differences From
Artifact [1e12e4a44b]:
- File
lib/color.lua
— part of check-in
[0a49ac4849]
at
2020-08-13 05:11:22
on branch glowpelt/hsl
— feat(color): Change color lightening to use HSL
Change color lightening, including the readable utility, to use HSL. This is
because the earlier implementation was broken and hacky, and using HSL is
a way to implement this in a much more natural-feeling way (being closer to
percieved lightness), especially for the current uses. Add a color:to_hsl()
function to make this easier, as well as a from_hsl utility that is only
in color, for now, but maybe should be exposed as an alternate constructor?
(user:
glowpelt,
size: 6197)
[annotate]
[blame]
[check-ins using]
52 52 };
53 53
54 54 construct = function(r,g,b,a)
55 55 local clip = function(v)
56 56 return math.max(0,math.min(255,v))
57 57 end;
58 58 local from_hsl = function(hsl, alpha)
59 - -- convert from a hsl table and alpha value to a color
60 - local weird = function(n)
61 - -- Yeah... this is a really weird function, only named f
62 - local k = math.fmod(n + hsl.hue/(math.pi/6),12)
63 - local a = hsl.saturation * math.min(hsl.luminosity, 1 - hsl.luminosity)
64 - return hsl.luminosity * math.max(-1, math.min(k-3,9-k,1))
59 + -- Based on the algorithm in Computer Graphics: Principles and Practice, by
60 + -- James D. Foley et. al., 2nd ed., p. 596
61 + -- Degree version, though radian is more natural, I don't want to translate it yet
62 + local h = hsl.hue
63 + local s = hsl.saturation
64 + local l = hsl.luminosity
65 + local value = function(n1, n2, hue)
66 + if hue > 360 then
67 + hue = hue - 360
68 + elseif hue < 0 then
69 + hue = hue + 360
70 + end
71 + if hue < 60 then
72 + return n1 + (n2 - n1) * hue/60
73 + elseif hue < 180 then
74 + return n2
75 + elseif hue < 240 then
76 + return n1 + (n2 - n1) * (240 - hue)/60
77 + else
78 + return n1
79 + end
80 + end
81 + local m2
82 + if l < 0.5 then
83 + m2 = l * (1 + s)
84 + else
85 + m2 = l + s - l * s
65 86 end
66 - return color(clip(weird(0)*255), clip(weird(8)*255), clip(weird(4)*255), alpha)
87 + local m1 = 2 * l - m2
88 + if s == 0 then
89 + -- Achromatic, there is no hue
90 + -- In book this errors if hue is not undefined, but we set hue to 0 in this case, not nil or something, so
91 + return color(l, l, l, alpha)
92 + else
93 + -- Chromatic case, so there is a hue
94 + return color(
95 + clip(value(m1, m2, h + 120)*255),
96 + clip(value(m1, m2, h)*255),
97 + clip(value(m1, m2, h - 120)*255),
98 + alpha
99 + )
100 + end
67 101 end;
68 102 local warp = function(f)
69 103 return function(self, ...)
70 104 local n = color(self)
71 105 f(n, ...)
72 106 return n
73 107 end;
................................................................................
82 116 end;
83 117
84 118 luminosity = function(self) return
85 119 (self.red + self.green + self.blue) / 3
86 120 end;
87 121
88 122 to_hsl = function(self)
89 - -- https://en.wikipedia.org/wiki/HSL_and_HSV
90 - -- www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
91 - -- https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.211.6425
92 - -- https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.413.9004
123 + -- Based on the algorithm in Computer Graphics: Principles and Practice, by
124 + -- James D. Foley et. al., 2nd ed., p. 595
93 125 -- We need the rgb between 0 and 1
94 - local rgb = { r = self.red/255, g = self.green/255, b = self.blue/255 }
95 - -- First, the hue.
96 - -- This version of the calculation can be up to 1.12deg off at the right few colors
97 - -- but is overall very close, easier to implement, and runs much faster
98 - -- TODO: consider memoizing something to do with this all?
99 - local alpha = 0.5 * (2*rgb.r - rgb.g - rgb.b)
100 - local beta = (math.sqrt(3)/2)*(rgb.g - rgb.b)
101 - local hue = math.atan2(beta, alpha)
102 - -- Next the luminosity/lightness. This one's easy enough
103 - local luminosity = 0.5*(math.max(rgb.r, rgb.g, rgb.b) + math.min(rgb.r, rgb.g, rgb.b))
104 - -- Finally, saturation
126 + local r = self.red/255
127 + local g = self.green/255
128 + local b = self.blue/255
129 + local max = math.max(r, g, b)
130 + local min = math.min(r, g, b)
131 + local luminosity = (max + min)/2
132 + local hue = 0
105 133 local saturation = 0
106 - -- If luminosity isn't essentially 1 or 0
107 - if math.abs(luminosity - 1) < 1e-18 or math.abs(luminosity) < 1e-18 then
108 - -- need the chroma
109 - local chroma = math.max(rgb.r,rgb.g,rgb.b) - math.min(rgb.r,rgb.g,rgb.b)
110 - saturation = chroma / (1 - math.abs(2*luminosity - 1))
134 + if max == min then
135 + -- Achromatic case, because r=g=b
136 + saturation = 0
137 + hue = 0 -- Undefined, so just replace w/ 0 for usability
138 + else
139 + -- Chromatic case
140 + if luminosity <= 0.5 then
141 + saturation = (max - min)/(max + min)
142 + else
143 + saturation = (max - min)/(2 - max - min)
144 + end
145 + -- Next calculate the hue
146 + local delta = max - min
147 + if r == max then
148 + hue = (g - b)/delta
149 + elseif g == max then
150 + hue = 2 + (b - r)/delta
151 + else -- blue must be max, so no point in checking
152 + hue = 4 + (r - g)/delta
153 + end
154 + hue = hue * 60 -- degrees
155 + --hue = hue * (math.pi / 3) -- for hue in radians instead of degrees
156 + if hue < 0 then
157 + hue = hue + 2 * math.pi
158 + end
111 159 end
160 + print("r"..self.red.."g"..self.green.."b"..self.blue.." is h"..hue.."s"..saturation.."l"..luminosity)
161 + local temp = from_hsl({hue=hue,saturation=saturation,luminosity=luminosity},self.alpha)
162 + print("back is r"..temp.red.."g"..temp.green.."b"..temp.blue)
112 163 return { hue = hue, saturation = saturation, luminosity = luminosity }
113 164 end;
114 165
115 166 readable = function(self, target)
116 - target = target or 200
167 + target = target or 0.5
117 168 local hsl = self:to_hsl()
118 169 hsl.luminosity = target
119 170 return from_hsl(hsl, self.alpha)
120 171 end;
121 172
122 173 bg = function(self, text) return
123 174 text .. minetest.get_background_escape_sequence(self:hex())
................................................................................
128 179 end);
129 180
130 181 brighten = function(self, fac)
131 182 -- Use HSL to brighten
132 183 -- To HSL
133 184 local hsl = self:to_hsl()
134 185 -- Do the calculation, clamp to 0-1 instead of the clamp fn
135 - hsl.luminosity = math.max(math.min(hsl.luminosity * fac, 0), 1)
186 + hsl.luminosity = math.min(math.max(hsl.luminosity * fac, 0), 1)
136 187 -- Turn back into RGB color
188 + local t = from_hsl(hsl, self.alpha)
189 + print("brighten is r"..t.red.."g"..t.green.."b"..t.blue)
137 190 return from_hsl(hsl, self.alpha)
138 191 end;
139 192
140 193 darken = warp(function(new, fac)
141 194 -- TODO: is there any point to this being different than brighten? Probably especially not now.
142 195 new.red = clip(new.red - (new.red * fac))
143 196 new.blue = clip(new.blue - (new.blue * fac))