Fast implementation to find if a color is a lerp between two colors?

If you’re given two colors that specify the start and end lerp value, and a Color3 that lies in-between those colors (or not), will return a boolean.

Example,

Color3 0 0 0 and Color3 200 200 200
Color3 100 100 100, Color3 47.3 47.3 47.3
They all work, but Color3 45 47 49 and Color3 202 202 202 don’t.

This function should have leniency to one pixel value for each R, G, and B components and should run fast. I tried looking for a fast implementation about this, but wasn’t able to find any.

1 Like

I don’t see where lerp fits in? Are you just asking how to tell if a Color3 is in between 2 other?

1 Like

Yes.

1 Like

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

This question is similar to asking if a point is on a line connected by 2 points. In this case, if the sum of the lengths of the 2 lines created between the end points of the base line and the test point equals the length of the base line, it is on the line.

--[[
Returns the "distance" between 2 colors.
--]]
local function ColorMag(Color1,Color2)
	--Get the distance between the R, G, and B values.
	local DeltaR = Color1.r - Color2.r
	local DeltaG = Color1.g - Color2.g
	local DeltaB = Color1.b - Color2.b
	
	--Apply the distance formula.	
	return ((DeltaR ^ 2) + (DeltaG ^ 2) + (DeltaB ^ 2)) ^ 0.5
end

--[[
Returns if a color is between 2 colors.
--]]
local function IsColorBetween(Color1,Color2,TestColor)
	--Get the line magnitudes.
	local Mag12 = ColorMag(Color1,Color2)
	local Mag1Test = ColorMag(Color1,TestColor)
	local Mag2Test = ColorMag(Color2,TestColor)
	
	--Return if the difference between the sum and the base length is less than some epsilon (floating point precision).
	return math.abs(Mag12 - (Mag1Test + Mag2Test)) < 0.00001
end

--Run tests.
local StartColor = Color3.new(0,0,0)
local EndColor = Color3.new(200/255,200/255,200/255)
print(IsColorBetween(StartColor,EndColor,Color3.new(100/255,100/255,100/255))) --true
print(IsColorBetween(StartColor,EndColor,Color3.new(200/255,200/255,200/255))) --true
print(IsColorBetween(StartColor,EndColor,Color3.new(47.3/255,47.3/255,47.3/255))) --true
print(IsColorBetween(StartColor,EndColor,Color3.new(45/255,47/255,49/255))) --false
print(IsColorBetween(StartColor,EndColor,Color3.new(202/255,202/255,202/255))) --false

One thing that may seem odd is the case with 202,202,202. This works because the distance formula makes sure the answer is positive, so the sum of the lengths is beyond the distance for 0,0,0 and 200,200,200.

5 Likes

Never thought of that, thanks :slight_smile:

local t = tick()
print(IsColorBetween(StartColor,EndColor,Color3.new(100/255,100/255,100/255))) --true
print(tick() - t) -- 0.00035214424133301
1 Like

This question is equivalent to asking how close a point is to a line. The main tool you’ll need to solve this is the dot product.

Let’s start with a drawing in 2D, but keep in mind even though we have a 3D value (rgb) the concept is still the same. We have three colours the two lerping colours (C1 and C2) and the colour we want to check (C3). We represent these as vectors.

Using some vector subtraction we can get displacement vectors that go from C1 to both C2 and C3.

2019-02-26_21-58-22

Then using the dot product between u and v we can find the vector projection of u onto v.

2019-02-26_22-01-59

Then finding the difference in distance between w and u we have our answer! We just check the length of the d vector.

2019-02-26_22-02-47

In code this looks like:

local function isBetweenLerp(c1, c2, c3)
	local v1 = Vector3.new(c1.r, c1.b, c1.g)
	local v2 = Vector3.new(c2.r, c2.b, c2.g)
	local v3 = Vector3.new(c3.r, c3.b, c3.g)
	
	local v_unit = (v2 - v1).unit
	local u = (v3 - v1)
	local w = u:Dot(v_unit)*v_unit
	local d = u - w
	
	return d.magnitude <= 0
end

print(isBetweenLerp(
	Color3.new(0, 0, 0),
	Color3.new(200, 200, 200), 
	Color3.new(100, 100, 100)
))

Now if on the other hand you truly want a box as opposed to a radius then you have to check each individual component of the d-vector.

local function isBetweenLerp(c1, c2, c3)
	local v1 = Vector3.new(c1.r, c1.b, c1.g)
	local v2 = Vector3.new(c2.r, c2.b, c2.g)
	local v3 = Vector3.new(c3.r, c3.b, c3.g)
	
	local v_unit = (v2 - v1).unit
	local u = (v3 - v1)
	local w = u:Dot(v_unit)*v_unit
	local d = u - w
	
	return math.abs(d.x) <= 1 and math.abs(d.y) <= 1 and math.abs(d.z) <= 1
end

print(isBetweenLerp(
	Color3.new(0, 0, 0),
	Color3.new(200, 200, 200), 
	Color3.new(101, 99, 100)
))
3 Likes

I wish I could put @TheNexusAvenger’s solution as well in this thread, but that’s really well thought out. :wink:

(Note: this solution assumes that you wanted Lerping and comparisons with RGB isn’t very easy, however if we convert the color to HSV then comparisons become easy. HSV is Hue, Saturation, and Value. Hue controls the color, Saturation controls how much of that color is present (minimum is grayscale, while maximum is full color), and Value is the brightness. Here are your colors converted to HSV:

<0, 0, 0> → <0, 0, 0>
<200, 200, 200> → <0, 0, 78>
<100, 100, 100> → <0, 0, 39> (note, 78 / 2)
<47, 47, 47> → <0, 0, 18> (since a byte is used for each value, 47.3 is not possible but 47 is)

Now comparing if they are the same color (same hue and optionally the same saturation) with a value in the range of 0 to 200 is easy. We can even see if it is close to the same hue in case of some rounding errors. For the other two colors you gave:

<45, 47, 49> → <210, 8, 19>
<202, 202, 202> → <0, 0, 79>

Since the first isn’t the same color and the second isn’t in the given value range then neither of these are a lerp between <0, 0, 0> and <0, 0, 78>.

This would also work if you had two colors with different hues or saturation. You’d simply make sure that the lerped color’s hue and saturation are between the original colors’ hues and saturations.

Here are what the RGB and HSV color spaces look like:

If you pay attention, you’ll note that this method forms a rectangular prism in the HSV space rather than a line in the RGB space. As a result, you have more control over what types of colors you accept as a lerp. You can disregard the brightness, only care about the hue, or require a saturation in a range.

Now, to convert from RGB to HSV we need this function which I got from here: https://github.com/EmmanuelOga/columns/blob/master/utils/color.lua

--[[
 * Converts an RGB color value to HSV. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSV_color_space.
 * Assumes r, g, and b are contained in the set [0, 255] and
 * returns h, s, and v in the set [0, 1].
 *
 * @param   Number  r       The red color value
 * @param   Number  g       The green color value
 * @param   Number  b       The blue color value
 * @return  Array           The HSV representation
]]
function rgbToHsv(r, g, b, a)
  r, g, b, a = r / 255, g / 255, b / 255, a / 255
  local max, min = math.max(r, g, b), math.min(r, g, b)
  local h, s, v
  v = max

  local d = max - min
  if max == 0 then s = 0 else s = d / max end

  if max == min then
    h = 0 -- achromatic
  else
    if max == r then
    h = (g - b) / d
    if g < b then h = h + 6 end
    elseif max == g then h = (b - r) / d + 2
    elseif max == b then h = (r - g) / d + 4
    end
    h = h / 6
  end

  return h, s, v, a
end
3 Likes

I remember running into a similar problem as the OP awhile ago and remember trying to use the straight line method in RGB space as you described. While not inherently wrong, it may yield results that were not expected. In the RGB cube shown above, a straight line would include colors that got brighter or darker and gain or lose saturation depending on where the interpolated colors were at. For example, orange to purple (both at the center of a face) would get considerably brighter in the middle of the line where as red to teal would stay the same brightness. Red to teal would also lose all saturation in the middle (turn into grey) while orange to purple would not.

If these effects are not desirable (it depends on one’s definition of lerping a color) then a line in HSV with a constant saturation and value would allow a smooth transition between colors. If different types of lerps are desired, then I feel that HSV allows more freedom.