Howdy persons, Simply, I didn't learn the proper terminology when I should have. Blah blah public school systems, anyways... while trigonometry concepts have become ever-increasingly clear to me on account of programming, it's plausible that I just simply haven't the knowledge of what to "google" in the jargon sense to find what I want. Surely, someone's drawn a triangle with rounded corners before. Surely. But googling variations on "program draw rounded triangle" provides tutorials on photoshop, illustrator, gimp, powerpoint....and maths demos of how to draw triangles with rounded *sides* or fitted inside of circles. This is maddening, so I've re-invented the 3-sided wheel here, since my google doesn't seem to know where the solution is. Problem is, I've never been particularly good at re-inventing branches of maths. Whenever I do, I promptly discover that someone else already did it, but faster, cheaper, and more accurately. I'm feeling up for a good round of derisive laughter today, so here's what I came up with last evening: I've kept the alpha low so that you can see the construction. At 100% opacity, this draws a smooth rounded triangle with variable-radius corners. It also draws 9 triangles because it's all I've got. Is there some way to know all of the "tangents that are on the outside?" Current method: 1) Draw 3 circles 2) Compute the common tangent between circle one and circle two 3) Draw a triangle from a tangent point on C1 to the corresponding tangent point on C2 to C3's origin See how this makes a nice tangent from c1 to c2? 4) But, I don't know which common tangent is "to the outside," so I draw both: 5) But, there's a gap created in the drawing near C1, due to the radius and severity of angles, so I draw a third triangle from each tangent point on C1 over to the midpoint of C2 and C3: So, that's C1 to C2 solved. Repeat these computations for C2-C3 and C3-C1, resulting in the first picture I posted. There's not, I admit, a ton of room for discussion, but basically: A) Is this...right? Are there better math ways to compute this to permit drawing only the correct tangent? (Not a discussion point, this, but a factual correction desired) b) Is this...wise? Drawing 9 triangles and 3 circles isn't expensive, but would people use this method? Are there other methods you'd prefer to draw this kind of geometry? (Discussion hopefully possible)
I personally would use a primitive/triangle fan. Like so: A triangle fan means you can perform alpha blending on the shape without worrying about ugly overlap. It also significantly reduces the number of triangles you're drawing. Code: /// draw_triangle_rounded() /// @jujuadams 2019/05/06 /// /// @param x1 /// @param y1 /// @param x2 /// @param y2 /// @param x3 /// @param y3 /// @param radius /// @param outline #macro __draw_triangle_rounded__max_angle_step 10 var _x1 = argument0; var _y1 = argument1; var _x2 = argument2; var _y2 = argument3; var _x3 = argument4; var _y3 = argument5; var _radius = argument6; var _outline = argument7; //Find the vectors from point to point var _delta_x21 = _x2 - _x1; var _delta_y21 = _y2 - _y1; var _delta_x32 = _x3 - _x2; var _delta_y32 = _y3 - _y2; var _delta_x13 = _x1 - _x3; var _delta_y13 = _y1 - _y3; //If the winding number is greater than zero then the vertices have been defined counterclockwise //Swapping p2 and p3 to corrects this if (_delta_y21*_delta_x32 - _delta_y32*_delta_x21 > 0) { var _temp_x2 = _x2; var _temp_y2 = _y2; _x2 = _x3; _y2 = _y3; _x3 = _temp_x2; _y3 = _temp_y2; _delta_x21 = _x2 - _x1; _delta_y21 = _y2 - _y1; _delta_x32 = _x3 - _x2; _delta_y32 = _y3 - _y2; _delta_x13 = _x1 - _x3; _delta_y13 = _y1 - _y3; } //Force the length of each vector to be the radius of the circle var _f21 = _radius / sqrt(_delta_x21*_delta_x21 + _delta_y21*_delta_y21); var _f32 = _radius / sqrt(_delta_x32*_delta_x32 + _delta_y32*_delta_y32); var _f13 = _radius / sqrt(_delta_x13*_delta_x13 + _delta_y13*_delta_y13); _delta_x21 *= _f21; _delta_y21 *= _f21; _delta_x32 *= _f32; _delta_y32 *= _f32; _delta_x13 *= _f13; _delta_y13 *= _f13; //Find the angle of each vector var _angle_21 = darctan2(_delta_x21, _delta_y21); var _angle_32 = darctan2(_delta_x32, _delta_y32); var _angle_13 = darctan2(_delta_x13, _delta_y13); //Here's a crafty trick to get a perpendicular line // x' = x + dy // y' = y - dx //We add the direction vectors to their parent points //By joining A->B for each pair of points we get the outer boundary of the triangle var _ax21 = _x1 + _delta_y21; var _ay21 = _y1 - _delta_x21; var _bx21 = _x2 + _delta_y21; var _by21 = _y2 - _delta_x21; var _ax32 = _x2 + _delta_y32; var _ay32 = _y2 - _delta_x32; var _bx32 = _x3 + _delta_y32; var _by32 = _y3 - _delta_x32; var _ax13 = _x3 + _delta_y13; var _ay13 = _y3 - _delta_x13; var _bx13 = _x1 + _delta_y13; var _by13 = _y1 - _delta_x13; //Set up the correct kind of primitive if (_outline) { draw_primitive_begin(pr_linestrip); } else { draw_primitive_begin(pr_trianglefan); //The first vertex of a triangle fan is the centre of the fan //We can choose any point inside the rounded triangle, but we choose the centre for convenience draw_vertex((_x1 + _x2 + _x3)/3, (_y1 + _y2 + _y3)/3); } //Iterate around the corner of point 1 to create a rounded edge var _incr = angle_difference(_angle_21, _angle_13); var _steps = ceil(abs(_incr) / __draw_triangle_rounded__max_angle_step); _incr /= _steps; var _angle = _angle_13; repeat(_steps) { draw_vertex(_x1 + lengthdir_x(_radius, _angle), _y1 + lengthdir_y(_radius, _angle)); _angle += _incr; } //Add the edge for the point pair P1->P2 draw_vertex(_ax21, _ay21); draw_vertex(_bx21, _by21); //Iterate around the corner of point 2... _incr = angle_difference(_angle_32, _angle_21); _steps = ceil(abs(_incr) / __draw_triangle_rounded__max_angle_step); _incr /= _steps; _angle = _angle_21; repeat(_steps) { draw_vertex(_x2 + lengthdir_x(_radius, _angle), _y2 + lengthdir_y(_radius, _angle)); _angle += _incr; } //Add the edge for the point pair P2->P3 draw_vertex(_ax32, _ay32); draw_vertex(_bx32, _by32); //Iterate around the corner of point 3... _incr = angle_difference(_angle_13, _angle_32); _steps = ceil(abs(_incr) / __draw_triangle_rounded__max_angle_step); _incr /= _steps; _angle = _angle_32; repeat(_steps) { draw_vertex(_x3 + lengthdir_x(_radius, _angle), _y3 + lengthdir_y(_radius, _angle)); _angle += _incr; } //Add the edge for the point pair P3->P1 to finish the shape draw_vertex(_ax13, _ay13); draw_vertex(_bx13, _by13); //End the primitive and draw it draw_primitive_end(); It is also possible to draw a rounded triangle inside rather than outside the input triangle too, though this script is inefficient and can probably be sped up by simplifying some of the trigonometry: Code: /// draw_triangle_rounded_inside() /// @jujuadams 2019/05/06 /// /// @param x1 /// @param y1 /// @param x2 /// @param y2 /// @param x3 /// @param y3 /// @param radius /// @param outline #macro __draw_triangle_rounded_inside__max_angle_step 10 var _x1 = argument0; var _y1 = argument1; var _x2 = argument2; var _y2 = argument3; var _x3 = argument4; var _y3 = argument5; var _radius = argument6; var _outline = argument7; //Find the vectors from point to point var _delta_x21 = _x2 - _x1; var _delta_y21 = _y2 - _y1; var _delta_x32 = _x3 - _x2; var _delta_y32 = _y3 - _y2; var _delta_x13 = _x1 - _x3; var _delta_y13 = _y1 - _y3; //If the winding number is greater than zero then the vertices have been defined counterclockwise //Swapping p2 and p3 to corrects this if (_delta_y21*_delta_x32 - _delta_y32*_delta_x21 > 0) { var _temp_x2 = _x2; var _temp_y2 = _y2; _x2 = _x3; _y2 = _y3; _x3 = _temp_x2; _y3 = _temp_y2; _delta_x21 = _x2 - _x1; _delta_y21 = _y2 - _y1; _delta_x32 = _x3 - _x2; _delta_y32 = _y3 - _y2; _delta_x13 = _x1 - _x3; _delta_y13 = _y1 - _y3; } //Force the length of each vector to be the radius of the circle var _f21 = _radius / sqrt(_delta_x21*_delta_x21 + _delta_y21*_delta_y21); var _f32 = _radius / sqrt(_delta_x32*_delta_x32 + _delta_y32*_delta_y32); var _f13 = _radius / sqrt(_delta_x13*_delta_x13 + _delta_y13*_delta_y13); _delta_x21 *= _f21; _delta_y21 *= _f21; _delta_x32 *= _f32; _delta_y32 *= _f32; _delta_x13 *= _f13; _delta_y13 *= _f13; //Find the angle of each vector var _angle_21 = darctan2(-_delta_y21, _delta_x21); var _angle_32 = darctan2(-_delta_y32, _delta_x32); var _angle_13 = darctan2(-_delta_y13, _delta_x13); var _angle = angle_difference(_angle_13, _angle_21)/2; var _tan = dtan(_angle); var _ax21 = _x1 + _tan*_delta_x21; var _ay21 = _y1 + _tan*_delta_y21; var _bx13 = _x1 - _tan*_delta_x13; var _by13 = _y1 - _tan*_delta_y13; var _dx = _delta_x21 - _delta_x13; var _dy = _delta_y21 - _delta_y13; var _d = (_radius/dcos(_angle)) / sqrt(_dx*_dx + _dy*_dy); _x1 += _d*_dx; _y1 += _d*_dy; var _angle = angle_difference(_angle_21, _angle_32)/2; var _tan = dtan(_angle); var _ax32 = _x2 + _tan*_delta_x32; var _ay32 = _y2 + _tan*_delta_y32; var _bx21 = _x2 - _tan*_delta_x21; var _by21 = _y2 - _tan*_delta_y21; var _dx = _delta_x32 - _delta_x21; var _dy = _delta_y32 - _delta_y21; var _d = (_radius/dcos(_angle)) / sqrt(_dx*_dx + _dy*_dy); _x2 += _d*_dx; _y2 += _d*_dy; var _angle = angle_difference(_angle_32, _angle_13)/2; var _tan = dtan(_angle); var _ax13 = _x3 + _tan*_delta_x13; var _ay13 = _y3 + _tan*_delta_y13; var _bx32 = _x3 - _tan*_delta_x32; var _by32 = _y3 - _tan*_delta_y32; var _dx = _delta_x13 - _delta_x32; var _dy = _delta_y13 - _delta_y32; var _d = (_radius/dcos(_angle)) / sqrt(_dx*_dx + _dy*_dy); _x3 += _d*_dx; _y3 += _d*_dy; //Set up the correct kind of primitive if (_outline) { draw_primitive_begin(pr_linestrip); } else { draw_primitive_begin(pr_trianglefan); //The first vertex of a triangle fan is the centre of the fan //We can choose any point inside the rounded triangle, but we choose the centre for convenience draw_vertex((_x1 + _x2 + _x3)/3, (_y1 + _y2 + _y3)/3); } //Iterate around the corner of point 1 to create a rounded edge var _angle = point_direction(_x1, _y1, _bx13, _by13); var _incr = angle_difference(point_direction(_x1, _y1, _ax21, _ay21), _angle); var _steps = ceil(abs(_incr) / __draw_triangle_rounded_inside__max_angle_step); _incr /= _steps; repeat(_steps) { draw_vertex(_x1 + lengthdir_x(_radius, _angle), _y1 + lengthdir_y(_radius, _angle)); _angle += _incr; } //Add the edge for the point pair P1->P2 draw_vertex(_ax21, _ay21); draw_vertex(_bx21, _by21); //Iterate around the corner of point 2... var _angle = point_direction(_x2, _y2, _bx21, _by21); var _incr = angle_difference(point_direction(_x2, _y2, _ax32, _ay32), _angle); var _steps = ceil(abs(_incr) / __draw_triangle_rounded_inside__max_angle_step); _incr /= _steps; repeat(_steps) { draw_vertex(_x2 + lengthdir_x(_radius, _angle), _y2 + lengthdir_y(_radius, _angle)); _angle += _incr; } //Add the edge for the point pair P2->P3 draw_vertex(_ax32, _ay32); draw_vertex(_bx32, _by32); //Iterate around the corner of point 3... var _angle = point_direction(_x3, _y3, _bx32, _by32); var _incr = angle_difference(point_direction(_x3, _y3, _ax13, _ay13), _angle); var _steps = ceil(abs(_incr) / __draw_triangle_rounded_inside__max_angle_step); _incr /= _steps; repeat(_steps) { draw_vertex(_x3 + lengthdir_x(_radius, _angle), _y3 + lengthdir_y(_radius, _angle)); _angle += _incr; } //Add the edge for the point pair P3->P1 to finish the shape draw_vertex(_ax13, _ay13); draw_vertex(_bx13, _by13); //End the primitive and draw it draw_primitive_end();
Interesting!! I forget about primitives often as an option; I usually want as much html5 compatibility as I can get, but webGL is pretty darned common now, right? It looks like there's some ups and downs to compare here, running each in the Draw event. Speed: trianglefan is ~72% slower My triangle tangent method appears to be significantly faster in vm; have not got YYC set up rn, tests welcome as I suspect it'll be a lot closer. This is the tangent method: While I'm seeing this from the trianglefan: Drawing flexibility: Trianglefan does 2 things tangent-triangles cannot 1) The ability to draw partial opacity is very significant. Tangent-triangles would require drawing to a surface, then drawing the surface; switching drawing targets is slower than both of these methods combined, so trianglefan would be the definite preference there. 2) The outline option. That's an extraordinary benefit, and drawing multiple shapes cannot use that method. Applying the drawn information: Similar, small advantage to tangent-triangles Strictly speaking, we're talking about drawing methods here, but there's a definite game-making benefit to having the shape data available for use in collision detection and such. In this regard, I was able to add a collision_circle or collision_triangle check for each of the 12 shapes drawn in the tangent-triangles method. The drawing function can return, in real-time, a pixel perfect determination of whether the shape is colliding with a provided point, and the CPU impact is small: With trianglefan, a similar triangle check can be inserted for each set of vertices. It'll need to perform quite a few extra checks, so that should be slower, but still doable. I *think* generally, from this, your trianglefan method, @Juju, is the superior method. If I desperately needed to run the largest number of rounded triangles possible on a computer, in VM, I could get double with the tangent-triangle method, but the impact of both is so low that it's plausibly an edge case. The function I've used for tangent-triangle, including hit detection: Spoiler Code: /// @function draw_rounded_triangle(x1, y1, x2, y2, x3, y3, radius) /// @arg x1 /// @arg y1 /// @arg x2 /// @arg y2 /// @arg x3 /// @arg y3 /// @arg radius /// @author Tsa05 2019.06.04 - forums.yoyogames.com /// @returns bool Whether the mouse is within the shape var x1 = argument[0]; var y1 = argument[1]; var x2 = argument[2]; var y2 = argument[3]; var x3 = argument[4]; var y3 = argument[5]; var rad = argument[6]; var retval = false; var mx = mouse_x; var my = mouse_y; var ang12 = point_direction(x1, y1, x2, y2); if(ang12>180){ ang12-=180; } var ang23 = point_direction(x2, y2, x3, y3); if(ang23>180){ ang23-=180; } var ang31 = point_direction(x3, y3, x1, y1); if(ang31>180){ ang31-=180; } draw_set_color(c_white); draw_circle(x1, y1, rad, 0); if(point_in_circle(mx, my, x1, y1, rad)) retval=true; draw_circle(x2, y2, rad, 0); if(point_in_circle(mx, my, x2, y2, rad)) retval=true; draw_circle(x3, y3, rad, 0); if(point_in_circle(mx, my, x3, y3, rad)) retval=true; var lx = lengthdir_x(rad, ang12-90); var ly = lengthdir_y(rad, ang12-90); draw_triangle(x1+lx, y1+ly, x2+lx, y2+ly, x3, y3, 0); if(point_in_triangle(mx,my, x1+lx, y1+ly, x2+lx, y2+ly, x3, y3)) retval=true; draw_triangle(x1-lx, y1-ly, x2-lx, y2-ly, x3, y3, 0); if(point_in_triangle(mx,my, x1-lx, y1-ly, x2-lx, y2-ly, x3, y3)) retval=true; draw_triangle(x1+lx,y1+ly, x1-lx,y1-ly, (x2+x3)/2,(y2+y3)/2, 0); if(point_in_triangle(mx,my, x1+lx,y1+ly, x1-lx,y1-ly, (x2+x3)/2,(y2+y3)/2)) retval=true; var lx = lengthdir_x(rad, ang23-90); var ly = lengthdir_y(rad, ang23-90); draw_triangle(x2+lx, y2+ly, x3+lx, y3+ly, x1, y1, 0); if(point_in_triangle(mx,my, x2+lx, y2+ly, x3+lx, y3+ly, x1, y1)) retval=true; draw_triangle(x2-lx, y2-ly, x3-lx, y3-ly, x1, y1, 0); if(point_in_triangle(mx,my, x2-lx, y2-ly, x3-lx, y3-ly, x1, y1)) retval=true; draw_triangle(x2+lx,y2+ly, x2-lx,y2-ly, (x1+x3)/2,(y1+y3)/2, 0); if(point_in_triangle(mx,my, x2+lx,y2+ly, x2-lx,y2-ly, (x1+x3)/2,(y1+y3)/2)) retval=true; var lx = lengthdir_x(rad, ang31-90); var ly = lengthdir_y(rad, ang31-90); draw_triangle(x3+lx, y3+ly, x1+lx, y1+ly, x2, y2, 0); if(point_in_triangle(mx,my, x3+lx, y3+ly, x1+lx, y1+ly, x2, y2)) retval=true; draw_triangle(x3-lx, y3-ly, x1-lx, y1-ly, x2, y2, 0); if(point_in_triangle(mx,my, x3-lx, y3-ly, x1-lx, y1-ly, x2, y2)) retval=true; draw_triangle(x3+lx,y3+ly, x3-lx,y3-ly, (x1+x2)/2,(y1+y2)/2, 0); if(point_in_triangle(mx,my, x3+lx,y3+ly, x3-lx,y3-ly, (x1+x2)/2,(y1+y2)/2)) retval=true; return retval;
The gap between the two methods should close significantly in YYC, but all the same, trig is always going to be expensive. There might be a clever method to generate the rounded corners without trig.
This may be a somewhat lazy way to do it, but you could do it with a triangle with 3 circles and 3 calls to draw_line_width: Spoiler: Draw Event Code: // Draw rounded triangle draw_circle(x1,y1,radius,false); draw_circle(x2,y2,radius,false); draw_circle(x3,y3,radius,false); draw_line_width(x1,y1,x2,y2,2*radius); draw_line_width(x2,y2,x3,y3,2*radius); draw_line_width(x1,y1,x3,y3,2*radius); draw_triangle(x1,y1,x2,y2,x3,y3,false); // (Corner points check) var c = c_red; draw_circle_color(x1,y1,radius,c,c,true); draw_circle_color(x2,y2,radius,c,c,true); draw_circle_color(x3,y3,radius,c,c,true); Spoiler: Create Event Code: /// @description Set variables x1 = 200; y1 = 200; x2 = 400; y2 = 200; x3 = 300; y3 = 400; radius = 20; Gives this: The draw_line_width makes it quite slow, though.
That's clever, too! No trig involved at all I think that's probably the most readable, understandable solution; hadn't thought of using line_width!
A script such as this can draw a rounded tri using only sprites and draw_triangle(): Code: var x1=argument0; var y1=argument1; var x2=argument2; var y2=argument3; var x3=argument4; var y3=argument5; var rsize=argument6; var s=rsize/sprite_get_width(sprite0); draw_sprite_ext(sprite0,0,x1,y1,s,s,0,c_white,1); draw_sprite_ext(sprite0,0,x2,y2,s,s,0,c_white,1); draw_sprite_ext(sprite0,0,x3,y3,s,s,0,c_white,1); draw_sprite_ext(sprite1,0,x1,y1,point_distance(x1,y1,x2,y2),s,point_direction(x1,y1,x2,y2),c_white,1); draw_sprite_ext(sprite1,0,x3,y3,point_distance(x3,y3,x2,y2),s,point_direction(x3,y3,x2,y2),c_white,1); draw_sprite_ext(sprite1,0,x3,y3,point_distance(x3,y3,x1,y1),s,point_direction(x3,y3,x1,y1),c_white,1); draw_triangle(x1,y1,x2,y2,x3,y3,0); Result: Code: draw_roundtri(100,100,225,120,140,290,16); Sprite0 is a 255x255 white circle, sprite1 is a 1x255 white rectangle. All origins are Middle Center. This is a very rough script, but with some work one can make it work in even more general cases and even make the entire triangle fit into the 3-point border. There is also a limit imposed by the cirle sprite dimensions, huge triangles will be blurry so you need an even bigger sprite. Just a thought here on half-assing this, maybe it will be fast and useful to someone.
Another advantage of using the triangle fan method is that you can anti-alias the output with display_reset() giving a nice clean look. Not in front of the computer right now, so I can't confirm that this will work with the other methods or not. The sprite method won't clean up with anti-aliasing though, I am confident of that.
The circle sprite will be antialiased (more precise, interpolated as long color interpolation is enabled). For the rectangle sprite, interpolation could also work but you will need to be more creative, that is use a bigger rectangle with empty pixel colums left and right and plan accordingly. The fasterest method to draw an "antialiased" line is take a, say, 1x5 sprite, center pixel is white at full opacity and opacity falls off near the edges until it's zero, and draw it x-stretched and rotated. Scaling and interpolation will do the rest.
The 'fasterest' method is actually to draw a quad with a vertex buffer. Your 'fasterest' method involves multiple internal matrix calculations, higher (internal) triangle count, and playing with opacity (unless you have interpolation turned on and want to blur your entire scene).
Before drawing the buffer, one has to set it up and calculate vertex positions. It would be interesting to see a benchmark about all these methods.
You'd (well I'd) set up a 1 x 1 unit quad once only in the create event. Then you'd just scale, rotate, and position like you would any other sprite. [edit] Actually, scrap all of that. The edges of the sprite will receive anti-aliasing anyway (provided the texture is right to the bounds of the sprite.) So, no need for interpolation after all. You'd still require vertex buffers for the circle elements though. In my opinion @Juju's example would provide the cleanest and most performant output for this task.
You can recreate the primitive method as a vertex buffer+shader, but eh, that's getting esoteric and of limited general use. imo use a primitive if you want to alpha blend / have an outline, use some thick lines and circles for a more convenient solution in the 100% alpha special case.
Obviously the best way is to just warp a triangle sprite using image_angle and image_xscale. Easy Peasy and looks great: