2D triangles

Introduction[edit]

Every so often you may find yourself wanting to draw triangles in GUI. They have many uses in game development, but probably the most common and arguably the most useful is their ability to build up any polygon. In this article we will go over the math and code behind drawing triangles in Roblox.

Prerequisites

This article will get a little mathy and as a result in order to fully understand the concepts behind this you are going to need to know the following:

  • Dot product
  • Cross product
    • Specifically the right hand rule!
  • 2D rotation matrix
  • Trigonometric functions
    • How to use them to solve right triangles
  • Vector math (covered in the dot product article)
    • Addition
    • Subtraction
    • Magnitude
    • Multiplication by scalars
    • Unit vectors

The basis of the algorithm[edit]

In Roblox we are limited to using semi-static shapes (I say semi-static because they can be stretched and squished). Triangles however tend to be a dynamic shape. Luckily for us we can take any triangle and split it into a maximum of two right angle triangles which we can draw via image labels.

2dtriangle 1.png

What’s important to note from this is that the base of the two right triangles is always the longest edge. In a similar fashion the two right triangles are split where the two shorter edges meet. Believe it or not this general concept is enough to get us into the math.

The math behind triangles[edit]

To start off we’re going to get as much information about the triangle as we can without worrying about the GUI placement. As was discussed above our first goal is to get the longest edge. We’ll treat the edge as not only the bottom (base) of our two right triangles, but also as the base of the whole triangle (this will become more important once we get to rotation). We don’t want to lose any information either though so what we will do is get the longest edge in vector form relative to one of the vertices it’s connected to. We’ll also get the other edge connected to that vertex in vector form as well.

2dtriangle 2.png

In code form:

function drawTriangle(a, b, c, parent)
	local edges = {
		{longest = (c - b), other = (a - b), position = b};
		{longest = (a - c), other = (b - c), position = c};
		{longest = (b - a), other = (c - a), position = a};
	};
 
	table.sort(edges, function(a, b) return a.longest.magnitude > b.longest.magnitude end);
 
	local edge = edges[1];
end;


From here our next step is going to be to find the height of the triangle and where along the longest edge the two right angle triangles are split. We can do this very easily by getting the angle between the two vectors using the dot product and then using trigonometric functions to solve.

2dtriangle 3.png

Once again in code form:

function dotv2(a, b)
	return a.x * b.x + a.y * b.y;
end;
 
function drawTriangle(a, b, c, parent)
	-- ... stuff from before
 
	-- unit vectors have a magnitude of 1 so 1 * 1 = 1 and division by 1 is redundant
	edge.angle = math.acos(dotv2(edge.longest.unit, edge.other.unit));
	edge.x = edge.other.magnitude * math.cos(edge.angle);
	edge.y = edge.other.magnitude * math.sin(edge.angle);
end;


The last thing we are going to solve before moving onto finding values specifically regarding the placement of user interface is the rotation of the triangle as a whole. Now remember we’re viewing the longest side of the triangle as the base which is what we’re measuring for rotation. In other words, if I drew the triangle with its base parallel to the x-axis how much would I have to rotate it to get it tilted so it lines up with the points I have.

The key to doing this right is to remember that the rotation of the triangle isn’t just dependant on the longest edge, it is also very much dependant on the vertex that is not connected to it. That means we need to add that “other” vector to our calculation, use arc tan to get the new answer and then subtract 90 degrees to get the properly adjusted rotation. This part is a little difficult to explain over text because it’s mainly just trig so you kind of have to just work it out on a piece of paper. If you’re too lazy to do that you can always accept the results as proof.

In code form:

function drawTriangle(a, b, c, parent)
	-- ... stuff from before
 
	local r = edge.longest.unit * edge.x - edge.other;
	local rotation = math.atan2(r.y, r.x) - math.pi/2;
end;


Drawing the UI[edit]

Alright, we have avoided it for as long as we can, but now it’s time to deal with our user interface. From this point on we'll be using these two right-triangle images to do our dirty work:

http://www.roblox.com/r1-item?id=319692151 http://www.roblox.com/r2-item?id=319692171

Now because those are white images, they’re not exactly easy to see. As a result here’s a masterful artist’s rendition:

2dtriangle 4.png

The important thing you take away from this is that r1’s hypotenuse is sloping up and r2’s hypotenuse is sloping down.

If we arbitrarily position either triangle with the values we calculate it might not look right. Visually we as humans know how to stretch or squish those triangles and then place them like jigsaw, but the computer doesn’t. As a result we have to mathematically figure out which triangle goes on what side. To do this we’re going to be smart and instead of using a trig equation straight out of a Lovecraftian horror we’ll use what we know about the right hand rule and the cross product.

2dtriangle 5.png

function drawTriangle(a, b, c, parent)
	-- ... stuff from before
 
	local tp = -edge.other;
	local tx = (edge.longest.unit * edge.x) - edge.other;
	local nz = tp.x * tx.y - tp.y * tx.x; -- all we care about is the depth
end;


If we cross the negative vector of the “other” edge (t) with the vector we get from subtracting the point along the longest edge where the two right angle splits and the “other” vector (s) (meaning t × s) we will get either a positive or negative value as our z-value. According to the right hand rule, if that value is positive we know the triangle attached to our “edge.position” point is upward sloping, otherwise its downward sloping.

2dtriangle 6.png

We can now solve for each triangle’s top-left corner which is needed since image labels are positioned from their top left corners. We know the downward sloping triangle is always going to have its top left corner in the position of the vertex that’s not related to the longest edge. We can find the other corner with our knowledge about the right hand rule and some simple vector addition and subtraction. Once we have found those values we can also solve for the size of our two triangles too!

function drawTriangle(a, b, c, parent)
	-- ... stuff from before
 
	-- top left corner 1 & top left corner 2
	local tlc1 = edge.position + edge.other;
	local tlc2 = nz > 0 and edge.position + edge.longest - tx or edge.position - tx;
 
	local tasize = Vector2.new((tlc1 - tlc2).magnitude, edge.y);
	local tbsize = Vector2.new(edge.longest.magnitude - tasize.x, edge.y);
end;


Finally, the last thing we need is to adjust the corner positions we just calculated. We have the real position, but Roblox (regardless of rotation) will always place GUI elements as if they aren’t rotated. Since this is the case we have to essentially “de-rotate” our corners and use those. To do that we need to follow a process that is pretty mechanical. First get the corners relative to their shapes center since that’s what they rotate around, “de-rotate” them, finally add the converted vector back to the shape’s center to get the world position again:

function rotateV2(vec, angle)
	-- 2D rotation matrix
	local x = vec.x * math.cos(angle) + vec.y * math.sin(angle);
	local y = -vec.x * math.sin(angle) + vec.y * math.cos(angle);
	return Vector2.new(x, y);
end;
 
function drawTriangle(a, b, c, parent)
	-- ... stuff from before
 
	local center1 = nz <= 0 and edge.position + ((edge.longest + edge.other)/2) or (edge.position + edge.other/2);
	local center2 = nz > 0 and edge.position + ((edge.longest + edge.other)/2) or (edge.position + edge.other/2);
 
	tlc1 = center1 + rotateV2(tlc1 - center1, math.rad(ta.Rotation));
	tlc2 = center2 + rotateV2(tlc2 - center2, math.rad(tb.Rotation));
end;


Wow! Congrats you’ve done all the math, now it’s just a matter of plugging the values into the two GUI elements with their respective right triangle images and you’re done!

Here’s the final product:

local extra = 2;
 
local img = Instance.new("ImageLabel");
img.BackgroundTransparency = 1;
img.BorderSizePixel = 0;
 
function dotv2(a, b)
	return a.x * b.x + a.y * b.y;
end;
 
function rotateV2(vec, angle)
	local x = vec.x * math.cos(angle) + vec.y * math.sin(angle);
	local y = -vec.x * math.sin(angle) + vec.y * math.cos(angle);
	return Vector2.new(x, y);
end;
 
function drawTriangle(a, b, c, parent)
	local edges = {
		{longest = (c - b), other = (a - b), position = b};
		{longest = (a - c), other = (b - c), position = c};
		{longest = (b - a), other = (c - a), position = a};
	};
 
	table.sort(edges, function(a, b) return a.longest.magnitude > b.longest.magnitude end);
 
	local edge = edges[1];
	edge.angle = math.acos(dotv2(edge.longest.unit, edge.other.unit));
	edge.x = edge.other.magnitude * math.cos(edge.angle);
	edge.y = edge.other.magnitude * math.sin(edge.angle);
 
	local r = edge.longest.unit * edge.x - edge.other;
	local rotation = math.atan2(r.y, r.x) - math.pi/2;
 
	local tp = -edge.other;
	local tx = (edge.longest.unit * edge.x) - edge.other;
	local nz = tp.x * tx.y - tp.y * tx.x;
 
	local tlc1 = edge.position + edge.other;
	local tlc2 = nz > 0 and edge.position + edge.longest - tx or edge.position - tx;
 
	local tasize = Vector2.new((tlc1 - tlc2).magnitude, edge.y);
	local tbsize = Vector2.new(edge.longest.magnitude - tasize.x, edge.y);
 
	local center1 = nz <= 0 and edge.position + ((edge.longest + edge.other)/2) or (edge.position + edge.other/2);
	local center2 = nz > 0 and edge.position + ((edge.longest + edge.other)/2) or (edge.position + edge.other/2);
 
	tlc1 = center1 + rotateV2(tlc1 - center1, rotation);
	tlc2 = center2 + rotateV2(tlc2 - center2, rotation);
 
	local ta = img:Clone();
	local tb = img:Clone();
	ta.Image = "rbxassetid://319692171";
	tb.Image = "rbxassetid://319692151";
	ta.Position = UDim2.new(0, tlc1.x, 0, tlc1.y);
	tb.Position = UDim2.new(0, tlc2.x, 0, tlc2.y);
	ta.Size = UDim2.new(0, tbsize.x + extra, 0, tbsize.y + extra);
	tb.Size = UDim2.new(0, tasize.x + extra, 0, tasize.y + extra);
	ta.Rotation = math.deg(rotation);
	tb.Rotation = ta.Rotation;
	ta.Parent = parent;
	tb.Parent = parent;
end;


Note: I added an “extra” variable to add a few extra pixels. All it does is make the triangles a little bit smoother.

Conclusion[edit]

Hope you enjoyed this explanation and learned something new from it. Now go do something awesome the triangles you now know how to draw!

2dtriangle 7.gif