So I decided to stop being lazy and learn Lua, the WowAce framework and the WoW API. Started tinkering with an addon and realized "hey, this should be in a library!", yet I couldn't find any seemingly maintained libraries doing what I needed. The result is LibPrism-1.0.
So, what does it do? Well, the intent is to have a library that will provide color manipulation tools. It doesn't do very much on that end yet, with only three functions, but I'm open to ideas and suggestions in that regard. I'm hopefully not too shy when it comes to collaboration, either. Give me a poke if you want in on trying to increase the usefulness of this project and such may or may not be arranged.
Right, so I didn't really answer my own question in the previous paragraph, other than quite vaguely. To elaborate a bit, but just a little bit, on the three functions, there is one for calculating the angle gradient between two colors, then two for converting between hsv and rgb.
A rather unfortunate name in light of recent NSA news, lol.
Anyway, actual feedback:
(1) You need to use the package-as directive in your project's .pkgmeta file so that the packager names your folder properly to match your TOC file. Right now it's using the default from your project's URL slug, which is "libprism-1-0" which does not match the "LibPrism-1.0" used in your TOC file's name. This will prevent your lib from loading standalone, which is how most developers and some users will run it.
(2) Methods for "darken/lighten/saturate/desaturate the specified color by X%" could be useful. See https://github.com/minism/leaf/blob/master/color.lua for some color-related functions in Lua you can "be inspired by".
(3) Passing tables around seems sub-optimal, as it forces addon authors who wouldn't otherwise be using tables for their color values to either implement table recycling or waste the garbage collector's time with single-use tables. I'd suggest accepting individual RGB values instead. If you want to keep accepting tables (and strings? though I don't think I've ever seen an addon storing colors internally as strings) you can easily do that:
function DoSomethingWithColors(r1, g1, b1, r2, g2, b2, percent)
if percent then
-- individual RGB values for both colors
else
-- first color = r1
-- second color = g1
-- use type() to distinguish strings/tables
percent = b1
end
-- do the thing
end
(1) You need to use the package-as directive in your project's .pkgmeta file so that the packager names your folder properly to match your TOC file. Right now it's using the default from your project's URL slug, which is "libprism-1-0" which does not match the "LibPrism-1.0" used in your TOC file's name. This will prevent your lib from loading standalone, which is how most developers and some users will run it.
Yeah, I noticed that and poked the "package as" setting. Gonna nudge it into the .pkgmeta as well before my next push.
(2) Methods for "darken/lighten/saturate/desaturate the specified color by X%" could be useful. See https://github.com/minism/leaf/blob/master/color.lua for some color-related functions in Lua you can "be inspired by".
Oooh. Yeah, ideas for more functionality is something I'm interested in. I suppose the main point should be to aim for useful (as you argued) functions. But I will look into that, and possibly other types of gradients as well.
(3) Passing tables around seems sub-optimal, as it forces addon authors who wouldn't otherwise be using tables for their color values to either implement table recycling or waste the garbage collector's time with single-use tables. I'd suggest accepting individual RGB values instead. If you want to keep accepting tables (and strings? though I don't think I've ever seen an addon storing colors internally as strings) you can easily do that:
Seems reasonable enough. And should someone ever find the need to call the function from a base-16 variable, converting is easy enough on their end.
Hum, still trying to figure out a good way of doing the whole saturate/desaturate and darken/lighten things in a way that warrants inclusion. The only thing those functions would really do would be to convert to hsv, perform arithmetic on one of the values, then convert it back.
Granted, getting the angle gradient is not all that different in that regard, the main difference being it performs an operation on each of the values. With that in mind, including the above mentioned functions may well be equally warranted.
I just have to figure out if the best way to go about it would be to accept a fixed value to modify by, a percentage, or both.
Possibly something akin to:
function Prism:Saturate(r, g, b, m, isPercent)
local h,s,v = Prism:RGBtoHSV(r,g,b)
if isPercent then s = s*(m+1)
else s = s+m end
if s < 0 then s = 0 elseif s > 1 then s = 1 end
return Prism:HSVtoRGB(h,s,v)
end
Hum, still trying to figure out a good way of doing the whole saturate/desaturate and darken/lighten things in a way that warrants inclusion. The only thing those functions would really do would be to convert to hsv, perform arithmetic on one of the values, then convert it back.
None of this could be construed into an API in a configurable manner?
Granted, getting the angle gradient is not all that different in that regard, the main difference being it performs an operation on each of the values. With that in mind, including the above mentioned functions may well be equally warranted.
I just have to figure out if the best way to go about it would be to accept a fixed value to modify by, a percentage, or both.
Possibly something akin to:
function Prism:Saturate(r, g, b, m, isPercent)
local h,s,v = Prism:RGBtoHSV(r,g,b)
if isPercent then s = s*(m+1)
else s = s+m end
if s < 0 then s = 0 elseif s > 1 then s = 1 end
return Prism:HSVtoRGB(h,s,v)
end
With some sanity validation checks included.
People are strange. Would allowing for both hurt performance? If so, why not allow for two distinct API calls?
What would you call them, though? SaturateByPercent & SaturateByValue? SaturateMultiplicatively & SaturateAdditively? The attractive cleanliness of Prism:Saturate is lost when you have to explain the method of action in the function name.
You could take a feather from the string flags in the game API and use tintShadeMethod as the final parameter. It would expect either nil, "ADD" or "MULTIPLY"; nil would pick the default method. (I almost always prefer something clearly declarative when a boolean value would be deciding between two things that are not opposites.) You could even make some convenience constants on the library table: Prism.ADD and Prism.MULTIPLY. Though, to that, it seems a little odd to have "ADD" and "MULTIPLY" just sitting there with no metadata. (Yeah, I'm adding more when I was trying to take away...)
Prism:Saturate(0.5, 0, 0, 0.5, Prism.TintShadeMethod.ADD)
Prism:Saturate(0.5, 0, 0, 0.5) -- if ADD is the default
Prism:Saturate(0.5, 0, 0, 0.5, "ADD") -- if the "constant" isn't used
Also, would an additive Saturate with a negative value be the same as an additive Desaturate? Are those functions going to reject or math.abs negative values?
Also, would an additive Saturate with a negative value be the same as an additive Desaturate? Are those functions going to reject or math.abs negative values?
Well, I hacked together a couple of functions for saturation and brightness, accepting modifier values within the [-1,1] range. So basically one could desaturate by passing a negative value to the saturate function, which is actually how I decided to write the counterpart functions Prism:Desaturate() and Prism:Darken().
They do not yet accept a fifth argument, but I will probably make them do so soon, while taking the above post in consideration. The validation for a multiplicative modifier value will have to be a bit different, though, but just increasing the accepted range to >-1 and simply using 1+m as the factor should do the trick. Would need to tweak Desaturate and Darken, though.
You could even make some convenience constants on the library table: Prism.ADD and Prism.MULTIPLY. Though, to that, it seems a little odd to have "ADD" and "MULTIPLY" just sitting there with no metadata. (Yeah, I'm adding more when I was trying to take away...)
Without being able to write some support code on the C side of things, this is about as good as it gets, I'm afraid. You could add an entry like Prism.TineShadeMethod.DEFAULT = "ADD" and then other Prism code could make reference to that key instead of using "ADD" everywhere, but there's not much more that can be done in pure Lua to emulate simple enumerated types, or elaborate enumerated types a la Java.
On the bright side, string comparison is O(1), so using strings as a "hamstrung enum" doesn't murder performance. :-\
Hum, still trying to figure out a good way of doing the whole saturate/desaturate and darken/lighten things in a way that warrants inclusion. The only thing those functions would really do would be to convert to hsv, perform arithmetic on one of the values, then convert it back.
Well, one could easily argue that RGB<->HSV conversions do nothing but perform arithmetic on values, so they don't warrant inclusion either. :p If you're writing a simple utility lib, you might as well just include all the relevant utilities, even if they seem too simple. People can either use them, or not use them, as they see fit.
Also, would an additive Saturate with a negative value be the same as an additive Desaturate? Are those functions going to reject or math.abs negative values?
Presumably, yes to the first question, and no to the second; in the Lua color lib I linked earlier, the desaturate function just negates the supplied value and then calls the saturate function with it.
Well, one could easily argue that RGB<->HSV conversions do nothing but perform arithmetic on values, so they don't warrant inclusion either. :p
Pff, they do conditional arithmetic operations! :p
Also, I'm not sure my saturate/desaturate (and possible lighten/darken) are spitting out proper values at the moment. I think there may be a problem with my conversion algorithms currently. So I'll be having a look at that, as well as finishing the work I've begun on allowing for multiplicative operations.
Presumably, yes to the first question, and no to the second; in the Lua color lib I linked earlier, the desaturate function just negates the supplied value and then calls the saturate function with it.
What would be the behavior of passing negative values to a multiplicative Desaturate? What about to a multiplicative Saturate?
What would be the behavior of passing negative values to a multiplicative Desaturate? What about to a multiplicative Saturate?
function Prism:Saturate(r, g, b, m, operation)
...
if operation == TYPE_MULTI then
-- Have to take special care of negative values here.
if m < -1 then m = 0 elseif m < 0 then m = 1-(math.abs(m)) else m = 1+m end
s = s*m
...
function Prism:Desaturate(r, g, b, m, operation)
return self:Saturate(r, g, b, -m, operation)
end
Unless I've managed to get myself drunk without realizing it, that should mean that any negative value passed will decrease the value with the specified percentage, and decreasing anything by 100% or more, with the lowest allowed value being 0, would result in setting it to 0. Similarly, positive percentages have no defined maximum value, as for instance, 300% of .1 will not exceed the maximum saturation of 1.
Since I do have:
if s < 0 then s = 0 elseif s > 1 then s = 1 end
before passing the return, I could as well just skip the if m < -1 check I placed in the multiplicative conditional, though, I suppose.
Also, right. That didn't actually answer the question. And I suppose it is something that may need some more thought. Then again, desaturating something by 100% does seem to imply completely desaturating it. So I suppose sticking with the above may well be a sensible approach.
Yeah, I guess the point of my questions was to feel out the issues that arise with convenience functions like this. Desaturate/saturate are just English words for either side of an increase/decrease of a value called "saturation". Lighten/Darken are a little bit less clear, but if you're using HSV (rather than HSL or HSI) then "value" ... it's an increase/decrease of value (the meaning of which Wikipedia defines as*).
That is to say, the purest way to implement them would be something bland like SetSaturation and SetValue. But then, at that point, you might as well just provide RGB<->HSV and let the users do the math :).
The most odd stuff was with the multiplicative functions. If the multiplicative Saturate function basically does:
v = v * (1 + m)
Then, passing -0.3 ends up multiplying v by 0.7 which is ... not that it doesn't make sense, but it is interesting.
So anyway, rambling ... but it's an interesting side effect of trying to make things nicer. I see you're considering this stuff, already.
* "[...] value is defined as the largest component of a color, our M above [...]. This places all three primaries, and also all of the "secondary colors" cyan, yellow, and magenta into a plane with white, forming a hexagonal pyramid out of the RGB cube." (yikes)
The most odd stuff was with the multiplicative functions. If the multiplicative Saturate function basically does:
v = v * (1 + m)
Then, passing -0.3 ends up multiplying v by 0.7 which is ... not that it doesn't make sense, but it is interesting.
...right. *facepalms @ self*
Obviously I did get involuntarily drunk without realizing it while writing that. I can do away with the silly if ... elseif ... else completely, because the end result will be the same regardless.
So anyway, rambling ... but it's an interesting side effect of trying to make things nicer. I see you're considering this stuff, already.
Well, I suppose it's a natural thing to consider. :p
Although given the way I've ended up writing things now, I'm wondering if I should move GetAngleGradient() into just GetGradient() (or Gradient()), passing the desired gradient type to it. For the sake of cleanliness. I'd keep GetAngleGradient() for backwards compatibility, though, but point it to the new function, should it be implemented.
The benefit of having one function to fetch any type of gradient would be that it would be cleaner to fetch based on conditionals such as user configuration.
Although given the way I've ended up writing things now, I'm wondering if I should move GetAngleGradient() into just GetGradient() (or Gradient()), passing the desired gradient type to it. For the sake of cleanliness. I'd keep GetAngleGradient() for backwards compatibility, though, but point it to the new function, should it be implemented.
The benefit of having one function to fetch any type of gradient would be that it would be cleaner to fetch based on conditionals such as user configuration.
I did just that. Different gradients will be added later, though. In the meanwhile, I certainly wouldn't mind if someone tried breaking the lib for me.
Rollback Post to RevisionRollBack
To post a comment, please login or register a new account.
So, what does it do? Well, the intent is to have a library that will provide color manipulation tools. It doesn't do very much on that end yet, with only three functions, but I'm open to ideas and suggestions in that regard. I'm hopefully not too shy when it comes to collaboration, either. Give me a poke if you want in on trying to increase the usefulness of this project and such may or may not be arranged.
Right, so I didn't really answer my own question in the previous paragraph, other than quite vaguely. To elaborate a bit, but just a little bit, on the three functions, there is one for calculating the angle gradient between two colors, then two for converting between hsv and rgb.
Project page: http://www.wowace.com/addons/libprism-1-0/
Documentation: http://www.wowace.com/addons/libprism-1-0/pages/api/ (Once I get the documenter working, anyway. Right now it's just a placeholder page, documentation is available on the main project page until then.)
Edit; Sample:
Anyway, actual feedback:
(1) You need to use the package-as directive in your project's .pkgmeta file so that the packager names your folder properly to match your TOC file. Right now it's using the default from your project's URL slug, which is "libprism-1-0" which does not match the "LibPrism-1.0" used in your TOC file's name. This will prevent your lib from loading standalone, which is how most developers and some users will run it.
(2) Methods for "darken/lighten/saturate/desaturate the specified color by X%" could be useful. See https://github.com/minism/leaf/blob/master/color.lua for some color-related functions in Lua you can "be inspired by".
(3) Passing tables around seems sub-optimal, as it forces addon authors who wouldn't otherwise be using tables for their color values to either implement table recycling or waste the garbage collector's time with single-use tables. I'd suggest accepting individual RGB values instead. If you want to keep accepting tables (and strings? though I don't think I've ever seen an addon storing colors internally as strings) you can easily do that:
Ugh, right. It seemed somehow fitting, though. >.<
Yeah, I noticed that and poked the "package as" setting. Gonna nudge it into the .pkgmeta as well before my next push.
Oooh. Yeah, ideas for more functionality is something I'm interested in. I suppose the main point should be to aim for useful (as you argued) functions. But I will look into that, and possibly other types of gradients as well.
Seems reasonable enough. And should someone ever find the need to call the function from a base-16 variable, converting is easy enough on their end.
Thanks for the feedback. :)
Granted, getting the angle gradient is not all that different in that regard, the main difference being it performs an operation on each of the values. With that in mind, including the above mentioned functions may well be equally warranted.
I just have to figure out if the best way to go about it would be to accept a fixed value to modify by, a percentage, or both.
Possibly something akin to:
With some sanity validation checks included.
None of this could be construed into an API in a configurable manner?
Same as above.
People are strange. Would allowing for both hurt performance? If so, why not allow for two distinct API calls?
You could take a feather from the string flags in the game API and use tintShadeMethod as the final parameter. It would expect either nil, "ADD" or "MULTIPLY"; nil would pick the default method. (I almost always prefer something clearly declarative when a boolean value would be deciding between two things that are not opposites.) You could even make some convenience constants on the library table: Prism.ADD and Prism.MULTIPLY. Though, to that, it seems a little odd to have "ADD" and "MULTIPLY" just sitting there with no metadata. (Yeah, I'm adding more when I was trying to take away...)
Maybe:
(I miss enumerated types.)
Then, the call would look like:
Also, would an additive Saturate with a negative value be the same as an additive Desaturate? Are those functions going to reject or math.abs negative values?
Well, I hacked together a couple of functions for saturation and brightness, accepting modifier values within the [-1,1] range. So basically one could desaturate by passing a negative value to the saturate function, which is actually how I decided to write the counterpart functions Prism:Desaturate() and Prism:Darken().
They do not yet accept a fifth argument, but I will probably make them do so soon, while taking the above post in consideration. The validation for a multiplicative modifier value will have to be a bit different, though, but just increasing the accepted range to >-1 and simply using 1+m as the factor should do the trick. Would need to tweak Desaturate and Darken, though.
Without being able to write some support code on the C side of things, this is about as good as it gets, I'm afraid. You could add an entry like Prism.TineShadeMethod.DEFAULT = "ADD" and then other Prism code could make reference to that key instead of using "ADD" everywhere, but there's not much more that can be done in pure Lua to emulate simple enumerated types, or elaborate enumerated types a la Java.
On the bright side, string comparison is O(1), so using strings as a "hamstrung enum" doesn't murder performance. :-\
Well, one could easily argue that RGB<->HSV conversions do nothing but perform arithmetic on values, so they don't warrant inclusion either. :p If you're writing a simple utility lib, you might as well just include all the relevant utilities, even if they seem too simple. People can either use them, or not use them, as they see fit.
Presumably, yes to the first question, and no to the second; in the Lua color lib I linked earlier, the desaturate function just negates the supplied value and then calls the saturate function with it.
Pff, they do conditional arithmetic operations! :p
Also, I'm not sure my saturate/desaturate (and possible lighten/darken) are spitting out proper values at the moment. I think there may be a problem with my conversion algorithms currently. So I'll be having a look at that, as well as finishing the work I've begun on allowing for multiplicative operations.
What would be the behavior of passing negative values to a multiplicative Desaturate? What about to a multiplicative Saturate?
Unless I've managed to get myself drunk without realizing it, that should mean that any negative value passed will decrease the value with the specified percentage, and decreasing anything by 100% or more, with the lowest allowed value being 0, would result in setting it to 0. Similarly, positive percentages have no defined maximum value, as for instance, 300% of .1 will not exceed the maximum saturation of 1.
Since I do have:
before passing the return, I could as well just skip the if m < -1 check I placed in the multiplicative conditional, though, I suppose.
Also, right. That didn't actually answer the question. And I suppose it is something that may need some more thought. Then again, desaturating something by 100% does seem to imply completely desaturating it. So I suppose sticking with the above may well be a sensible approach.
That is to say, the purest way to implement them would be something bland like SetSaturation and SetValue. But then, at that point, you might as well just provide RGB<->HSV and let the users do the math :).
The most odd stuff was with the multiplicative functions. If the multiplicative Saturate function basically does:
v = v * (1 + m)
Then, passing -0.3 ends up multiplying v by 0.7 which is ... not that it doesn't make sense, but it is interesting.
So anyway, rambling ... but it's an interesting side effect of trying to make things nicer. I see you're considering this stuff, already.
* "[...] value is defined as the largest component of a color, our M above [...]. This places all three primaries, and also all of the "secondary colors" cyan, yellow, and magenta into a plane with white, forming a hexagonal pyramid out of the RGB cube." (yikes)
...right. *facepalms @ self*
Obviously I did get involuntarily drunk without realizing it while writing that. I can do away with the silly if ... elseif ... else completely, because the end result will be the same regardless.
Well, I suppose it's a natural thing to consider. :p
Although given the way I've ended up writing things now, I'm wondering if I should move GetAngleGradient() into just GetGradient() (or Gradient()), passing the desired gradient type to it. For the sake of cleanliness. I'd keep GetAngleGradient() for backwards compatibility, though, but point it to the new function, should it be implemented.
The benefit of having one function to fetch any type of gradient would be that it would be cleaner to fetch based on conditionals such as user configuration.
I did just that. Different gradients will be added later, though. In the meanwhile, I certainly wouldn't mind if someone tried breaking the lib for me.