preview
MQL5 Trading Tools (Part 18): Rounded Speech Bubbles/Balloons with Orientation Control

MQL5 Trading Tools (Part 18): Rounded Speech Bubbles/Balloons with Orientation Control

MetaTrader 5Examples |
157 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

In our previous article (Part 17), we explored vector-based methods for drawing rounded rectangles and triangles in MetaQuotes Language 5 (MQL5) using canvas, with supersampling for anti-aliased rendering. In Part 18, we combine these shapes to create rounded speech bubbles/balloons with orientation control, allowing pointers to face up, down, left, or right. This integration includes adjustable positions, borders, and opacities, providing versatile UI elements for trading interfaces. We will cover the following topics:

  1. Understanding Rounded Speech Bubbles with Orientation
  2. Implementation in MQL5
  3. Backtesting
  4. Conclusion

By the end, you’ll have a functional speech bubble system ready for customization in advanced applications—let’s dive in!


Understanding Rounded Speech Bubbles with Orientation

The rounded speech bubble/balloon with orientation control combines a rounded rectangle body and a triangular pointer, allowing dynamic positioning of the pointer in up, down, left, or right directions via an enumeration, creating versatile UI elements for alerts or tooltips in trading interfaces. Orientation determines layout by shifting the body relative to the pointer, incorporating base offsets for alignment, and ensuring smooth merges between shapes to avoid visual discontinuities. This vector approach supports scalable, high-quality rendering with supersampling for anti-aliased edges, enhancing readability and aesthetics in MQL5 applications. Our plan is to precompute body and pointer geometries based on orientation, fill the combined shape using adapted scanline algorithms for horizontal or vertical scans, and draw segmented borders with extended edges for seamless joins. In brief, here is a visual representation of our objectives.

DIFFERENT SPEECH BUBBLES SETUP OBJECTIVES


Implementation in MQL5

To enhance the program in MQL5, we will need to add new defines, global variables, and inputs to control the new speech bubble with orientation capabilities.

//+------------------------------------------------------------------+
//|                           Rounded Rectangle & Triangle PART2.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum BUBBLE_ORIENTATION {
   ORIENT_UP    = 0, // Pointer faces up
   ORIENT_DOWN  = 1, // Pointer faces down
   ORIENT_LEFT  = 2, // Pointer faces left
   ORIENT_RIGHT = 3  // Pointer faces right
};

input group "Bubble Shape"
input int              bubbleBodyWidthPixels            = 250;          // Bubble body width
input int              bubbleBodyHeightPixels           = 100;          // Bubble body height
input int              bubbleBodyCornerRadiusPixels     = 5;            // Bubble body corner radius
input int              bubblePointerBaseWidthPixels     = 60;           // Bubble pointer base width
input int              bubblePointerHeightPixels        = 40;           // Bubble pointer height
input int              bubblePointerApexRadiusPixels    = 12;           // Bubble pointer apex radius
input int              bubblePointerBaseOffsetPixels    = 0;            // Bubble pointer offset from center (0=centered, +/-=shift)
input BUBBLE_ORIENTATION bubblePointerOrientation       = ORIENT_UP;    // Bubble pointer orientation
input bool             bubbleShowBorder                 = true;         // Show bubble border
input int              bubbleBorderThicknessPixels      = 2;            // Bubble border thickness
input color            bubbleBorderColor                = clrGreen;     // Bubble border color
input int              bubbleBorderOpacityPercent       = 80;           // Bubble border opacity (0-100%)
input color            bubbleBackgroundColor            = clrGreen;     // Bubble background color
input int              bubbleBackgroundOpacityPercent   = 30;           // Bubble background opacity (0-100%)

input group "General"
input double borderExtensionMultiplier        = 0.23;    // Border extension multiplier (fraction of thickness)

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CCanvas bubbleCanvas,    bubbleHighResCanvas;                   //--- Declare bubble canvas objects

string  bubbleCanvasName    = "BubbleCanvas";                   //--- Set bubble canvas name

double bubbleBodyLeft, bubbleBodyTop, bubbleBodyRight, bubbleBodyBottom; //--- Store bubble body coordinates
double bubblePointerVerticesX[3], bubblePointerVerticesY[3];    //--- Store bubble pointer vertices X and Y
double bubblePointerArcCentersX[3], bubblePointerArcCentersY[3]; //--- Store bubble pointer arc centers X and Y
double bubblePointerTangentPointsX[3][2], bubblePointerTangentPointsY[3][2]; //--- Store bubble pointer tangent points X and Y
double bubblePointerArcStartAngles[3], bubblePointerArcEndAngles[3]; //--- Store bubble pointer arc start and end angles
int    bubblePointerApexIndex;                                  //--- Store bubble pointer apex index
double bubblePointerBaseStart, bubblePointerBaseEnd;            //--- Store bubble pointer base start and end
bool   bubbleIsHorizontalOrientation;                           //--- Store if bubble orientation is horizontal

We continue by defining the "BUBBLE_ORIENTATION" enumeration with options for pointer directions: "ORIENT_UP" (0), "ORIENT_DOWN" (1), "ORIENT_LEFT" (2), and "ORIENT_RIGHT" (3). This provides flexible control over bubble layout. Under the "Bubble Shape" input group, we add parameters for body dimensions such as "bubbleBodyWidthPixels" and "bubbleBodyHeightPixels". Other parameters include corner radius "bubbleBodyCornerRadiusPixels", pointer specifications like "bubblePointerBaseWidthPixels", "bubblePointerHeightPixels", "bubblePointerApexRadiusPixels", and offset "bubblePointerBaseOffsetPixels" for centering shifts. We also include the orientation from the enum, border toggles, thickness, colors, and opacities, similar to previous shapes.

To fine-tune borders, we include a general input "borderExtensionMultiplier" as a fraction of thickness for edge extensions, ensuring seamless joins. Global variables extend to bubble-specific canvases "bubbleCanvas" and "bubbleHighResCanvas" with name "BubbleCanvas"; we store body bounds in doubles like "bubbleBodyLeft" and "bubbleBodyTop", pointer vertices, arc centers, tangents (3x2 arrays), angles, apex index "bubblePointerApexIndex", base start/end, and a boolean "bubbleIsHorizontalOrientation" to handle scanline orientations dynamically. Now, we will need to define functions to control the creation of the speech bubble, using a similar logic as we did with the other shapes in the previous version, as follows. We will first precompute the bubble geometry.

//+------------------------------------------------------------------+
//| Bubble Shape                                                     |
//+------------------------------------------------------------------+
void PrecomputeBubbleGeometry() {
   int scalingFactor = supersamplingFactor;                     //--- Set scaling factor
   double baseOffset = 10.0 * scalingFactor;                    //--- Set base offset scaled
   
   double centeringAdjustment;                                  //--- Declare centering adjustment

   if(bubblePointerOrientation == ORIENT_UP) {                  //--- Check for up orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset + bubblePointerHeightPixels * scalingFactor; //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyWidthPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterX = bubbleBodyLeft + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center X
      
      bubblePointerVerticesX[0] = pointerCenterX;               //--- Set apex X
      bubblePointerVerticesY[0] = baseOffset;                   //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft + actualOffset; //--- Set left X
      bubblePointerVerticesY[1] = bubbleBodyTop;                //--- Set left Y
      
      bubblePointerVerticesX[2] = bubblePointerVerticesX[1] + bubblePointerBaseWidthPixels * scalingFactor; //--- Set right X
      bubblePointerVerticesY[2] = bubbleBodyTop;                //--- Set right Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesX[1];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesX[2];         //--- Set base end
      
   } else if(bubblePointerOrientation == ORIENT_DOWN) {         //--- Check for down orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyWidthPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterX = bubbleBodyLeft + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center X
      
      bubblePointerVerticesX[0] = pointerCenterX;               //--- Set apex X
      bubblePointerVerticesY[0] = bubbleBodyBottom + bubblePointerHeightPixels * scalingFactor; //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set right X
      bubblePointerVerticesY[1] = bubbleBodyBottom;             //--- Set right Y
      
      bubblePointerVerticesX[2] = bubbleBodyLeft + actualOffset; //--- Set left X
      bubblePointerVerticesY[2] = bubbleBodyBottom;             //--- Set left Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesX[2];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesX[1];         //--- Set base end
      
   } else if(bubblePointerOrientation == ORIENT_LEFT) {         //--- Check for left orientation
      bubbleBodyLeft = baseOffset + bubblePointerHeightPixels * scalingFactor; //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyHeightPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterY = bubbleBodyTop + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center Y
      
      bubblePointerVerticesX[0] = baseOffset;                   //--- Set apex X
      bubblePointerVerticesY[0] = pointerCenterY;               //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft;               //--- Set bottom X
      bubblePointerVerticesY[1] = bubbleBodyTop + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set bottom Y
      
      bubblePointerVerticesX[2] = bubbleBodyLeft;               //--- Set top X
      bubblePointerVerticesY[2] = bubbleBodyTop + actualOffset; //--- Set top Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesY[2];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesY[1];         //--- Set base end
      
   } else {                                                     //--- Handle right orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyHeightPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterY = bubbleBodyTop + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center Y
      
      bubblePointerVerticesX[0] = bubbleBodyRight + bubblePointerHeightPixels * scalingFactor; //--- Set apex X
      bubblePointerVerticesY[0] = pointerCenterY;               //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyRight;              //--- Set top X
      bubblePointerVerticesY[1] = bubbleBodyTop + actualOffset; //--- Set top Y
      
      bubblePointerVerticesX[2] = bubbleBodyRight;              //--- Set bottom X
      bubblePointerVerticesY[2] = bubbleBodyTop + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set bottom Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesY[1];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesY[2];         //--- Set base end
   }

   ComputeBubbleTriangleRoundedCorners();                       //--- Compute rounded corners for bubble pointer
}

void ComputeBubbleTriangleRoundedCorners() {
   double scaledRadius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Scale apex radius

   int cornerIndex = bubblePointerApexIndex;                    //--- Set corner index to apex
   
   int previousIndex = (cornerIndex + 2) % 3;                   //--- Get previous index
   int nextIndex = (cornerIndex + 1) % 3;                       //--- Get next index

   double edgeA_X = bubblePointerVerticesX[cornerIndex] - bubblePointerVerticesX[previousIndex]; //--- Compute edge A X
   double edgeA_Y = bubblePointerVerticesY[cornerIndex] - bubblePointerVerticesY[previousIndex]; //--- Compute edge A Y
   double edgeA_Length = MathSqrt(edgeA_X*edgeA_X + edgeA_Y*edgeA_Y); //--- Compute edge A length
   edgeA_X /= edgeA_Length;  edgeA_Y /= edgeA_Length;           //--- Normalize edge A

   double edgeB_X = bubblePointerVerticesX[nextIndex] - bubblePointerVerticesX[cornerIndex]; //--- Compute edge B X
   double edgeB_Y = bubblePointerVerticesY[nextIndex] - bubblePointerVerticesY[cornerIndex]; //--- Compute edge B Y
   double edgeB_Length = MathSqrt(edgeB_X*edgeB_X + edgeB_Y*edgeB_Y); //--- Compute edge B length
   edgeB_X /= edgeB_Length;  edgeB_Y /= edgeB_Length;           //--- Normalize edge B

   double normalA_X =  edgeA_Y,  normalA_Y = -edgeA_X;          //--- Compute normal A
   double normalB_X =  edgeB_Y,  normalB_Y = -edgeB_X;          //--- Compute normal B

   double bisectorX = normalA_X + normalB_X,  bisectorY = normalA_Y + normalB_Y; //--- Compute bisector
   double bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); //--- Compute bisector length
   if(bisectorLength < 1e-12) { bisectorX = normalA_X; bisectorY = normalA_Y; bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); } //--- Handle small bisector
   bisectorX /= bisectorLength;  bisectorY /= bisectorLength;   //--- Normalize bisector

   double cosInteriorAngle = (-edgeA_X)*edgeB_X + (-edgeA_Y)*edgeB_Y; //--- Compute cosine of interior angle
   if(cosInteriorAngle >  1.0) cosInteriorAngle =  1.0;         //--- Clamp cosine upper
   if(cosInteriorAngle < -1.0) cosInteriorAngle = -1.0;         //--- Clamp cosine lower
   double halfAngle = MathArccos(cosInteriorAngle) / 2.0;       //--- Compute half angle
   double sinHalfAngle = MathSin(halfAngle);                    //--- Compute sine of half angle
   if(sinHalfAngle < 1e-12) sinHalfAngle = 1e-12;               //--- Set minimum sine value

   double distanceToCenter = scaledRadius / sinHalfAngle;       //--- Compute distance to arc center
   bubblePointerArcCentersX[cornerIndex] = bubblePointerVerticesX[cornerIndex] + bisectorX * distanceToCenter; //--- Set arc center X
   bubblePointerArcCentersY[cornerIndex] = bubblePointerVerticesY[cornerIndex] + bisectorY * distanceToCenter; //--- Set arc center Y

   double deltaX_A = bubblePointerVerticesX[cornerIndex] - bubblePointerVerticesX[previousIndex]; //--- Compute delta A X
   double deltaY_A = bubblePointerVerticesY[cornerIndex] - bubblePointerVerticesY[previousIndex]; //--- Compute delta A Y
   double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A; //--- Compute length squared A
   double interpolationFactor_A = ((bubblePointerArcCentersX[cornerIndex] - bubblePointerVerticesX[previousIndex])*deltaX_A + 
                                   (bubblePointerArcCentersY[cornerIndex] - bubblePointerVerticesY[previousIndex])*deltaY_A) / lengthSquared_A; //--- Compute factor A
   bubblePointerTangentPointsX[cornerIndex][1] = bubblePointerVerticesX[previousIndex] + interpolationFactor_A * deltaX_A; //--- Set tangent point X arriving
   bubblePointerTangentPointsY[cornerIndex][1] = bubblePointerVerticesY[previousIndex] + interpolationFactor_A * deltaY_A; //--- Set tangent point Y arriving

   double deltaX_B = bubblePointerVerticesX[nextIndex] - bubblePointerVerticesX[cornerIndex]; //--- Compute delta B X
   double deltaY_B = bubblePointerVerticesY[nextIndex] - bubblePointerVerticesY[cornerIndex]; //--- Compute delta B Y
   double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B; //--- Compute length squared B
   double interpolationFactor_B = ((bubblePointerArcCentersX[cornerIndex] - bubblePointerVerticesX[cornerIndex])*deltaX_B + 
                                   (bubblePointerArcCentersY[cornerIndex] - bubblePointerVerticesY[cornerIndex])*deltaY_B) / lengthSquared_B; //--- Compute factor B
   bubblePointerTangentPointsX[cornerIndex][0] = bubblePointerVerticesX[cornerIndex] + interpolationFactor_B * deltaX_B; //--- Set tangent point X leaving
   bubblePointerTangentPointsY[cornerIndex][0] = bubblePointerVerticesY[cornerIndex] + interpolationFactor_B * deltaY_B; //--- Set tangent point Y leaving

   bubblePointerArcStartAngles[cornerIndex] = MathArctan2(bubblePointerTangentPointsY[cornerIndex][1] - bubblePointerArcCentersY[cornerIndex], 
                                                          bubblePointerTangentPointsX[cornerIndex][1] - bubblePointerArcCentersX[cornerIndex]); //--- Set start angle
   bubblePointerArcEndAngles[cornerIndex] = MathArctan2(bubblePointerTangentPointsY[cornerIndex][0] - bubblePointerArcCentersY[cornerIndex], 
                                                        bubblePointerTangentPointsX[cornerIndex][0] - bubblePointerArcCentersX[cornerIndex]); //--- Set end angle
   
   for(int i = 0; i < 3; i++) {                                 //--- Loop over corners
      if(i == bubblePointerApexIndex) continue;                 //--- Skip apex corner
      
      bubblePointerTangentPointsX[i][0] = bubblePointerVerticesX[i]; //--- Set tangent X leaving to vertex
      bubblePointerTangentPointsY[i][0] = bubblePointerVerticesY[i]; //--- Set tangent Y leaving to vertex
      bubblePointerTangentPointsX[i][1] = bubblePointerVerticesX[i]; //--- Set tangent X arriving to vertex
      bubblePointerTangentPointsY[i][1] = bubblePointerVerticesY[i]; //--- Set tangent Y arriving to vertex
   }
}

We begin by implementing the "PrecomputeBubbleGeometry" function to establish the layout for the speech bubble, assigning the supersampling factor to a local variable and scaling a base offset for padding, then declaring a centering adjustment to position the pointer. Depending on the "bubblePointerOrientation", we set the body coordinates: for "ORIENT_UP", placing it below the pointer with left, top, right, bottom calculated from scaled inputs; compute centering with applied offset for alignment, determine the pointer center X, and define vertices with apex at top center, left and right at body top, setting "bubblePointerApexIndex" to 0 and base start/end from left to right X.

For "ORIENT_DOWN", we position the body above the pointer, invert the apex to bottom, adjust vertex order for consistent winding, and update base start/end accordingly; similarly, for "ORIENT_LEFT", shift body right of pointer, set vertices with apex left, bottom and top at body left, and base as Y values; for "ORIENT_RIGHT", mirror to right with appropriate adjustments. This conditional setup ensures adaptable geometry based on direction, facilitating seamless integration of pointer and body without overlaps.

To round the pointer tip, we call "ComputeBubbleTriangleRoundedCorners", scaling the apex radius, focusing on the apex corner via "bubblePointerApexIndex", computing edge vectors and normals as in triangle precomputation, forming the bisector, calculating half-angle sine for center distance, projecting to find tangents, and setting start/end angles with the MathArctan2 function. Finally, for non-apex corners (base), we set tangents directly to vertices, disabling rounding there to merge flatly with the body, which is crucial for a clean speech bubble appearance without unnecessary curves at the attachment point. For arc-based pixel drawing, we will need a helper function.

bool BubbleAngleInArcSweep(int cornerIndex, double angle) {
   double twoPi = 2.0 * M_PI;                                   //--- Define two pi constant
   double startAngleMod = MathMod(bubblePointerArcStartAngles[cornerIndex] + twoPi, twoPi); //--- Modulo start angle
   double endAngleMod = MathMod(bubblePointerArcEndAngles[cornerIndex] + twoPi, twoPi); //--- Modulo end angle
   angle = MathMod(angle + twoPi, twoPi);                       //--- Modulo angle

   double ccwSpan = MathMod(endAngleMod - startAngleMod + twoPi, twoPi); //--- Compute CCW span

   if(ccwSpan <= M_PI) {                                        //--- Check if short way is CCW
      double relativeAngle = MathMod(angle - startAngleMod + twoPi, twoPi); //--- Compute relative angle
      return(relativeAngle <= ccwSpan + 1e-6);                  //--- Return if within CCW span
   } else {                                                     //--- Else short way is CW
      double cwSpan = twoPi - ccwSpan;                          //--- Compute CW span
      double relativeAngle = MathMod(angle - endAngleMod + twoPi, twoPi); //--- Compute relative angle
      return(relativeAngle <= cwSpan + 1e-6);                   //--- Return if within CW span
   }
}

We implement the "BubbleAngleInArcSweep" function to determine if a given angle falls within the arc sweep for a specific corner of the bubble pointer, starting by defining a two-pi constant and normalizing the start, end, and input angles using MathMod to ensure they range from 0 to 2 pi, handling any wrapping or negative values. Next, we compute the counterclockwise span between normalized start and end, and if it's pi or less (indicating the shorter arc), check the relative angle from start against this span with a small epsilon for precision; otherwise, calculate the clockwise span as 2 pi minus ccw and verify the relative angle from end. This logic accommodates both arc directions, ensuring accurate pixel inclusion during rendering, which is vital for smooth, artifact-free curved borders in the speech bubble's pointer across orientations. To draw the bubble-rounded rectangle, we use the following approach.

void FillBubbleRoundedRectangle(double left, double top, double width, double height, int radius, uint fillColor) {
   bubbleHighResCanvas.FillRectangle((int)(left + radius), (int)top, (int)(left + width - radius), (int)(top + height), fillColor); //--- Fill central rectangle
   bubbleHighResCanvas.FillRectangle((int)left, (int)(top + radius), (int)(left + radius), (int)(top + height - radius), fillColor); //--- Fill left strip
   bubbleHighResCanvas.FillRectangle((int)(left + width - radius), (int)(top + radius), (int)(left + width), (int)(top + height - radius), fillColor); //--- Fill right strip

   FillBubbleCircleQuadrant((int)(left + radius), (int)(top + radius), radius, fillColor, 2); //--- Fill top-left quadrant
   FillBubbleCircleQuadrant((int)(left + width - radius), (int)(top + radius), radius, fillColor, 1); //--- Fill top-right quadrant
   FillBubbleCircleQuadrant((int)(left + radius), (int)(top + height - radius), radius, fillColor, 3); //--- Fill bottom-left quadrant
   FillBubbleCircleQuadrant((int)(left + width - radius), (int)(top + height - radius), radius, fillColor, 4); //--- Fill bottom-right quadrant
}

void FillBubbleCircleQuadrant(int centerX, int centerY, int radius, uint fillColor, int quadrant) {
   double radiusDouble = (double)radius;                        //--- Convert radius to double

   for(int deltaY = -radius - 1; deltaY <= radius + 1; deltaY++) { //--- Loop over delta Y
      for(int deltaX = -radius - 1; deltaX <= radius + 1; deltaX++) { //--- Loop over delta X
         bool inQuadrant = false;                               //--- Initialize quadrant flag
         if(quadrant == 1 && deltaX >= 0 && deltaY <= 0) inQuadrant = true; //--- Check top-right
         else if(quadrant == 2 && deltaX <= 0 && deltaY <= 0) inQuadrant = true; //--- Check top-left
         else if(quadrant == 3 && deltaX <= 0 && deltaY >= 0) inQuadrant = true; //--- Check bottom-left
         else if(quadrant == 4 && deltaX >= 0 && deltaY >= 0) inQuadrant = true; //--- Check bottom-right

         if(!inQuadrant) continue;                              //--- Skip if not in quadrant

         double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); //--- Compute distance
         if(distance <= radiusDouble)                           //--- Check if within radius
            bubbleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, fillColor); //--- Set pixel
      }
   }
}

Here, we implement the "FillBubbleRoundedRectangle" function to render the filled body of the speech bubble as a rounded rectangle on the high-res canvas, casting coordinates to integers for pixel accuracy, filling the central area excluding corners with "FillRectangle", then adding left and right vertical strips to cover the sides seamlessly. To complete the rounding, we call "FillBubbleCircleQuadrant" for each corner, adapting the quadrant-specific logic to fill only the relevant quarter-circle pixels.

In "FillBubbleCircleQuadrant", we convert radius to double for precision, loop over an extended delta range for anti-aliasing, check quadrant membership based on delta signs (e.g., quadrant 1 for positive X, negative Y), compute distance with MathSqrt, and set pixels within radius using PixelSet, ensuring smooth curves that integrate with the rectangle strips. This modular filling is significant for maintaining consistency across orientations, as it reuses rectangle logic while allowing pointer attachment without fill gaps. Filling the rectangle border is easy and straightforward. Let us now define a logic to fill a rounded triangle for the pointer.

void FillBubbleRoundedTriangle(uint fillColor) {
   if(bubbleIsHorizontalOrientation) {                          //--- Check for horizontal orientation
      double minX = bubblePointerVerticesX[0], maxX = bubblePointerVerticesX[0]; //--- Initialize min and max X
      for(int i = 1; i < 3; i++) {                              //--- Loop over vertices
         if(bubblePointerVerticesX[i] < minX) minX = bubblePointerVerticesX[i]; //--- Update min X
         if(bubblePointerVerticesX[i] > maxX) maxX = bubblePointerVerticesX[i]; //--- Update max X
      }

      int xStart = (int)MathCeil(minX);                         //--- Compute start X
      int xEnd   = (int)MathFloor(maxX);                        //--- Compute end X

      for(int x = xStart; x <= xEnd; x++) {                     //--- Loop over scanlines
         double scanlineX = (double)x + 0.5;                    //--- Set scanline X position
         double yIntersections[12];                             //--- Declare intersections array
         int intersectionCount = 0;                             //--- Initialize intersection count

         for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {   //--- Loop over edges
            int nextIndex = (edgeIndex + 1) % 3;                //--- Get next index

            if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges

            double startX, startY, endX, endY;                  //--- Declare edge coordinates

            if(edgeIndex == bubblePointerApexIndex) {           //--- Check if from apex
               startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
               startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
               endX   = bubblePointerVerticesX[nextIndex];     //--- Set end X to next vertex
               endY   = bubblePointerVerticesY[nextIndex];     //--- Set end Y to next vertex
            } else {                                            //--- Handle to apex
               startX = bubblePointerVerticesX[edgeIndex];     //--- Set start X from vertex
               startY = bubblePointerVerticesY[edgeIndex];     //--- Set start Y from vertex
               endX   = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
               endY   = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
            }

            double edgeMinX = (startX < endX) ? startX : endX;  //--- Compute edge min X
            double edgeMaxX = (startX > endX) ? startX : endX;  //--- Compute edge max X

            if(scanlineX < edgeMinX || scanlineX > edgeMaxX) continue; //--- Skip if outside edge X
            if(MathAbs(endX - startX) < 1e-12) continue;        //--- Skip if vertical edge

            double interpolationFactor = (scanlineX - startX) / (endX - startX); //--- Compute factor
            if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment

            yIntersections[intersectionCount++] = startY + interpolationFactor * (endY - startY); //--- Add intersection Y
         }

         {                                                      //--- Intersect apex arc block
            int cornerIndex = bubblePointerApexIndex;           //--- Set corner index
            double centerX  = bubblePointerArcCentersX[cornerIndex]; //--- Get center X
            double centerY  = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y
            double radius   = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius
            double deltaX   = scanlineX - centerX;              //--- Compute delta X

            if(MathAbs(deltaX) <= radius) {                     //--- Check if within radius
               double deltaY = MathSqrt(radius * radius - deltaX * deltaX); //--- Compute delta Y

               double candidates[2];                            //--- Declare candidates array
               candidates[0] = centerY - deltaY;                //--- Set top candidate
               candidates[1] = centerY + deltaY;                //--- Set bottom candidate

               for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { //--- Loop over candidates
                  double angle = MathArctan2(candidates[candidateIndex] - centerY, scanlineX - centerX); //--- Compute angle
                  if(BubbleAngleInArcSweep(cornerIndex, angle)) //--- Check if in arc sweep
                     yIntersections[intersectionCount++] = candidates[candidateIndex]; //--- Add intersection
               }
            }
         }

         for(int a = 0; a < intersectionCount - 1; a++)         //--- Sort intersections (bubble sort)
            for(int b = a + 1; b < intersectionCount; b++)       //--- Inner loop for sorting
               if(yIntersections[a] > yIntersections[b]) {       //--- Check if swap needed
                  double temp = yIntersections[a];               //--- Temporary store
                  yIntersections[a] = yIntersections[b];         //--- Swap values
                  yIntersections[b] = temp;                      //--- Complete swap
               }

         for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs
            int yTop    = (int)MathCeil(yIntersections[pairIndex]); //--- Compute top Y
            int yBottom = (int)MathFloor(yIntersections[pairIndex + 1]); //--- Compute bottom Y
            for(int y = yTop; y <= yBottom; y++)                //--- Loop over vertical span
               bubbleHighResCanvas.PixelSet(x, y, fillColor);   //--- Set pixel with fill color
         }
      }

   } else {                                                     //--- Handle vertical orientation
      double minY = bubblePointerVerticesY[0], maxY = bubblePointerVerticesY[0]; //--- Initialize min and max Y
      for(int i = 1; i < 3; i++) {                              //--- Loop over vertices
         if(bubblePointerVerticesY[i] < minY) minY = bubblePointerVerticesY[i]; //--- Update min Y
         if(bubblePointerVerticesY[i] > maxY) maxY = bubblePointerVerticesY[i]; //--- Update max Y
      }

      int yStart = (int)MathCeil(minY);                         //--- Compute start Y
      int yEnd = (int)MathFloor(maxY);                          //--- Compute end Y

      for(int y = yStart; y <= yEnd; y++) {                     //--- Loop over scanlines
         double scanlineY = (double)y + 0.5;                    //--- Set scanline Y position
         double xIntersections[12];                             //--- Declare intersections array
         int intersectionCount = 0;                             //--- Initialize intersection count

         for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {   //--- Loop over edges
            int nextIndex = (edgeIndex + 1) % 3;                //--- Get next index
            
            if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges
            
            double startX, startY, endX, endY;                  //--- Declare edge coordinates
            
            if(edgeIndex == bubblePointerApexIndex) {           //--- Check if from apex
               startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
               startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
               endX = bubblePointerVerticesX[nextIndex];       //--- Set end X to next vertex
               endY = bubblePointerVerticesY[nextIndex];       //--- Set end Y to next vertex
            } else {                                            //--- Handle to apex
               startX = bubblePointerVerticesX[edgeIndex];     //--- Set start X from vertex
               startY = bubblePointerVerticesY[edgeIndex];     //--- Set start Y from vertex
               endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
               endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
            }

            double edgeMinY = (startY < endY) ? startY : endY;  //--- Compute edge min Y
            double edgeMaxY = (startY > endY) ? startY : endY;  //--- Compute edge max Y

            if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue; //--- Skip if outside edge Y
            if(MathAbs(endY - startY) < 1e-12) continue;        //--- Skip if horizontal

            double interpolationFactor = (scanlineY - startY) / (endY - startY); //--- Compute factor
            if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment

            xIntersections[intersectionCount++] = startX + interpolationFactor * (endX - startX); //--- Add intersection X
         }

         int cornerIndex = bubblePointerApexIndex;              //--- Set corner index
         double centerX = bubblePointerArcCentersX[cornerIndex]; //--- Get center X
         double centerY = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y
         double radius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius
         double deltaY = scanlineY - centerY;                   //--- Compute delta Y

         if(MathAbs(deltaY) <= radius) {                        //--- Check if within radius
            double deltaX = MathSqrt(radius*radius - deltaY*deltaY); //--- Compute delta X

            double candidates[2];                               //--- Declare candidates array
            candidates[0] = centerX - deltaX;                   //--- Set left candidate
            candidates[1] = centerX + deltaX;                   //--- Set right candidate

            for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { //--- Loop over candidates
               double angle = MathArctan2(scanlineY - centerY, candidates[candidateIndex] - centerX); //--- Compute angle
               if(BubbleAngleInArcSweep(cornerIndex, angle))    //--- Check if in arc sweep
                  xIntersections[intersectionCount++] = candidates[candidateIndex]; //--- Add intersection
            }
         }

         for(int a = 0; a < intersectionCount - 1; a++)         //--- Sort intersections (bubble sort)
            for(int b = a + 1; b < intersectionCount; b++)       //--- Inner loop for sorting
               if(xIntersections[a] > xIntersections[b]) {       //--- Check if swap needed
                  double temp = xIntersections[a];               //--- Temporary store
                  xIntersections[a] = xIntersections[b];         //--- Swap values
                  xIntersections[b] = temp;                      //--- Complete swap
               }

         for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs
            int xLeft = (int)MathCeil(xIntersections[pairIndex]); //--- Compute left X
            int xRight = (int)MathFloor(xIntersections[pairIndex + 1]); //--- Compute right X
            for(int x = xLeft; x <= xRight; x++)                //--- Loop over horizontal span
               bubbleHighResCanvas.PixelSet(x, y, fillColor);   //--- Set pixel with fill color
         }
      }
   }
}

We implement the "FillBubbleRoundedTriangle" function to fill the triangular pointer of the speech bubble on the high-res canvas, adapting the scanline direction based on orientation for optimal efficiency—using horizontal scans (x-loop) if "bubbleIsHorizontalOrientation" is true for left/right pointers, or vertical (y-loop) otherwise. For horizontal orientations, we determine min and max X from pointer vertices, loop over integer x with half-pixel offset, collect y-intersections only from apex-adjacent edges via interpolation (skipping base), add arc candidates by solving circle equation at deltaX if within radius and validated by "BubbleAngleInArcSweep", sort with bubble sort, and fill vertical spans between pairs using PixelSet with fillColor.

In vertical cases, we mirror the process with min/max Y, y-loop on scanlineY, x-intersections from edges and arc (using deltaY for candidates), sorting, and horizontal fills, ensuring complete coverage without gaps. With the filling for the bubble rectangle and triangle pointer complete, we now define the border logic to encase the bubble. We use the following approach to achieve that.

void DrawBubbleBorder(uint borderColorARGB) {
   int scaledThickness = bubbleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness
   int scaledBodyRadius = bubbleBodyCornerRadiusPixels * supersamplingFactor; //--- Scale body radius

   if(bubblePointerOrientation == ORIENT_UP || bubblePointerOrientation == ORIENT_DOWN) { //--- Check for up or down orientation
      if(bubblePointerOrientation == ORIENT_DOWN) {             //--- Check for down orientation
         DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw top edge
      } else {                                                  //--- Handle up orientation
         if(bubblePointerBaseStart > bubbleBodyLeft + scaledBodyRadius) //--- Check if base start exceeds left radius
            DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubblePointerBaseStart, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw left segment of top edge
         if(bubblePointerBaseEnd < bubbleBodyRight - scaledBodyRadius) //--- Check if base end below right radius
            DrawBubbleHorizontalEdge(bubblePointerBaseEnd, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw right segment of top edge
      }

      if(bubblePointerOrientation == ORIENT_UP) {               //--- Check for up orientation
         DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw bottom edge
      } else {                                                  //--- Handle down orientation
         if(bubblePointerBaseStart > bubbleBodyLeft + scaledBodyRadius) //--- Check if base start exceeds left radius
            DrawBubbleHorizontalEdge(bubblePointerBaseStart, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw left segment of bottom edge
         if(bubblePointerBaseEnd < bubbleBodyRight - scaledBodyRadius) //--- Check if base end below right radius
            DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubblePointerBaseEnd, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw right segment of bottom edge
      }

      DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw left edge
      DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw right edge

   } else {                                                     //--- Handle left or right orientation
      DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw top edge
      DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw bottom edge

      if(bubblePointerOrientation == ORIENT_RIGHT) {            //--- Check for right orientation
         DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw left edge
      } else {                                                  //--- Handle left orientation
         if(bubblePointerBaseStart > bubbleBodyTop + scaledBodyRadius) //--- Check if base start exceeds top radius
            DrawBubbleVerticalEdge(bubbleBodyLeft, bubblePointerBaseStart, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw top segment of left edge
         if(bubblePointerBaseEnd < bubbleBodyBottom - scaledBodyRadius) //--- Check if base end below bottom radius
            DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubblePointerBaseEnd, scaledThickness, borderColorARGB); //--- Draw bottom segment of left edge
      }

      if(bubblePointerOrientation == ORIENT_LEFT) {             //--- Check for left orientation
         DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw right edge
      } else {                                                  //--- Handle right orientation
         if(bubblePointerBaseStart > bubbleBodyTop + scaledBodyRadius) //--- Check if base start exceeds top radius
            DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubblePointerBaseStart, scaledThickness, borderColorARGB); //--- Draw top segment of right edge
         if(bubblePointerBaseEnd < bubbleBodyBottom - scaledBodyRadius) //--- Check if base end below bottom radius
            DrawBubbleVerticalEdge(bubbleBodyRight, bubblePointerBaseEnd, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw bottom segment of right edge
      }
   }

   DrawBubbleCornerArc((int)(bubbleBodyLeft + scaledBodyRadius), (int)(bubbleBodyTop + scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI, M_PI * 1.5); //--- Draw top-left arc
   DrawBubbleCornerArc((int)(bubbleBodyRight - scaledBodyRadius), (int)(bubbleBodyTop + scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI * 1.5, M_PI * 2.0); //--- Draw top-right arc
   DrawBubbleCornerArc((int)(bubbleBodyLeft + scaledBodyRadius), (int)(bubbleBodyBottom - scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI * 0.5, M_PI); //--- Draw bottom-left arc
   DrawBubbleCornerArc((int)(bubbleBodyRight - scaledBodyRadius), (int)(bubbleBodyBottom - scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, 0.0, M_PI * 0.5); //--- Draw bottom-right arc

   for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {         //--- Loop over pointer edges
      int nextIndex = (edgeIndex + 1) % 3;                      //--- Get next index
      
      if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges
      
      double startX, startY, endX, endY;                        //--- Declare edge coordinates
      
      if(edgeIndex == bubblePointerApexIndex) {                 //--- Check if from apex
         startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
         startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
         endX = bubblePointerVerticesX[nextIndex];              //--- Set end X to next vertex
         endY = bubblePointerVerticesY[nextIndex];              //--- Set end Y to next vertex
      } else {                                                  //--- Handle to apex
         startX = bubblePointerVerticesX[edgeIndex];            //--- Set start X from vertex
         startY = bubblePointerVerticesY[edgeIndex];            //--- Set start Y from vertex
         endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
         endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
      }

      DrawBubbleStraightEdge(startX, startY, endX, endY, scaledThickness, borderColorARGB); //--- Draw straight edge
   }

   DrawBubbleTriangleCornerArc(bubblePointerApexIndex, scaledThickness, borderColorARGB); //--- Draw apex arc
}

void DrawBubbleHorizontalEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   DrawBubbleStraightEdge(startX, startY, endX, endY, thickness, edgeColor); //--- Draw horizontal edge using straight edge
}

void DrawBubbleVerticalEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   DrawBubbleStraightEdge(startX, startY, endX, endY, thickness, edgeColor); //--- Draw vertical edge using straight edge
}

void DrawBubbleStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   double deltaX = endX - startX;                               //--- Compute delta X
   double deltaY = endY - startY;                               //--- Compute delta Y
   double edgeLength = MathSqrt(deltaX*deltaX + deltaY*deltaY); //--- Compute edge length
   if(edgeLength < 1e-6) return;                                //--- Return if length too small

   double perpendicularX = -deltaY / edgeLength;                //--- Compute perpendicular X
   double perpendicularY = deltaX / edgeLength;                 //--- Compute perpendicular Y

   double edgeDirectionX = deltaX / edgeLength;                 //--- Compute edge direction X
   double edgeDirectionY = deltaY / edgeLength;                 //--- Compute edge direction Y

   double halfThickness = (double)thickness / 2.0;              //--- Compute half thickness
   double extensionLength = borderExtensionMultiplier * (double)thickness; //--- Set extension length
   
   double extendedStartX = startX - edgeDirectionX * extensionLength; //--- Extend start X
   double extendedStartY = startY - edgeDirectionY * extensionLength; //--- Extend start Y
   double extendedEndX = endX + edgeDirectionX * extensionLength; //--- Extend end X
   double extendedEndY = endY + edgeDirectionY * extensionLength; //--- Extend end Y

   double verticesX[4], verticesY[4];                           //--- Declare vertices arrays
   verticesX[0] = extendedStartX - perpendicularX * halfThickness;  verticesY[0] = extendedStartY - perpendicularY * halfThickness; //--- Set vertex 0
   verticesX[1] = extendedStartX + perpendicularX * halfThickness;  verticesY[1] = extendedStartY + perpendicularY * halfThickness; //--- Set vertex 1
   verticesX[2] = extendedEndX + perpendicularX * halfThickness;  verticesY[2] = extendedEndY + perpendicularY * halfThickness; //--- Set vertex 2
   verticesX[3] = extendedEndX - perpendicularX * halfThickness;  verticesY[3] = extendedEndY - perpendicularY * halfThickness; //--- Set vertex 3

   FillQuadrilateral(bubbleHighResCanvas, verticesX, verticesY, edgeColor); //--- Fill quadrilateral for edge
}

void DrawBubbleCornerArc(int centerX, int centerY, int radius, int thickness, uint edgeColor,
                         double startAngle, double endAngle) {
   int halfThickness = thickness / 2;                           //--- Compute half thickness
   double outerRadius = (double)radius + halfThickness;         //--- Compute outer radius
   double innerRadius = (double)radius - halfThickness;         //--- Compute inner radius
   if(innerRadius < 0) innerRadius = 0;                         //--- Set inner radius to zero if negative

   int pixelRange = (int)(outerRadius + 2);                     //--- Compute pixel range

   for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { //--- Loop over delta Y
      for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { //--- Loop over delta X
         double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); //--- Compute distance
         if(distance < innerRadius || distance > outerRadius) continue; //--- Skip if outside radii

         double angle = MathArctan2((double)deltaY, (double)deltaX); //--- Compute angle
         
         if(IsAngleBetween(angle, startAngle, endAngle))        //--- Check if angle within range
            bubbleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, edgeColor); //--- Set pixel
      }
   }
}

void DrawBubbleTriangleCornerArc(int cornerIndex, int thickness, uint edgeColor) {
   double centerX = bubblePointerArcCentersX[cornerIndex];      //--- Get center X
   double centerY = bubblePointerArcCentersY[cornerIndex];      //--- Get center Y
   double radius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius

   int halfThickness = thickness / 2;                           //--- Compute half thickness
   double outerRadius = radius + halfThickness;                 //--- Compute outer radius
   double innerRadius = radius - halfThickness;                 //--- Compute inner radius
   if(innerRadius < 0) innerRadius = 0;                         //--- Set inner radius to zero if negative

   int pixelRange = (int)(outerRadius + 2);                     //--- Compute pixel range

   for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { //--- Loop over delta Y
      for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { //--- Loop over delta X
         double distance = MathSqrt((double)(deltaX*deltaX + deltaY*deltaY)); //--- Compute distance
         if(distance < innerRadius || distance > outerRadius) continue; //--- Skip if outside radii

         double angle = MathArctan2((double)deltaY, (double)deltaX); //--- Compute angle
         
         if(BubbleAngleInArcSweep(cornerIndex, angle)) {        //--- Check if in arc sweep
            int pixelX = (int)MathRound(centerX + deltaX);      //--- Round to pixel X
            int pixelY = (int)MathRound(centerY + deltaY);      //--- Round to pixel Y
            if(pixelX >= 0 && pixelX < bubbleHighResCanvas.Width() && pixelY >= 0 && pixelY < bubbleHighResCanvas.Height()) //--- Check if within bounds
               bubbleHighResCanvas.PixelSet(pixelX, pixelY, edgeColor); //--- Set pixel
         }
      }
   }
}

Here, we implement the "DrawBubbleBorder" function to render the complete border of the speech bubble on the high-res canvas, scaling thickness and body radius with supersampling, then conditionally drawing segmented edges based on orientation—for up/down, handling top and bottom horizontals with splits around pointer base if attached, and full verticals; for left/right, full horizontals and segmented verticals similarly, using "DrawBubbleHorizontalEdge" and "DrawBubbleVerticalEdge" to avoid drawing over the pointer junction. Next, we draw the four body corner arcs with "DrawBubbleCornerArc" using predefined angle ranges in radians, loop over the three pointer edges (focusing on apex-adjacent) to add straight borders from tangents via "DrawBubbleStraightEdge", and complete with the apex arc through "DrawBubbleTriangleCornerArc".

To standardize edge drawing, "DrawBubbleHorizontalEdge" and "DrawBubbleVerticalEdge" simply invoke "DrawBubbleStraightEdge", which computes direction and perpendicular vectors, extends endpoints by "borderExtensionMultiplier" times thickness for smooth joins, forms a quadrilateral strip, and fills it with "FillQuadrilateral" using the edge color. In "DrawBubbleCornerArc", we create the curved border ring by calculating inner/outer radii from half-thickness, iterating an expanded pixel grid, and setting pixels if within distance, and "IsAngleBetween" confirms the arc segment, ensuring precise curvature. Finally, "DrawBubbleTriangleCornerArc" mirrors this for the pointer tip, using scaled radius, pixel loops with MathSqrt for distance, MathArctan2 for angle, "BubbleAngleInArcSweep" for inclusion, and MathRound for coordinates with bounds checks before PixelSet, providing anti-aliased borders that blend seamlessly with the body. We can now combine this logic to create the bubble.

void DrawBubble() {
   uint backgroundColorARGB = ColorToARGBWithOpacity(bubbleBackgroundColor, bubbleBackgroundOpacityPercent); //--- Get background ARGB
   uint borderColorARGB = ColorToARGBWithOpacity(bubbleBorderColor, bubbleBorderOpacityPercent); //--- Get border ARGB

   FillBubble(backgroundColorARGB);                             //--- Fill bubble

   if(bubbleShowBorder && bubbleBorderThicknessPixels > 0)      //--- Check if border should be shown
      DrawBubbleBorder(borderColorARGB);                        //--- Draw bubble border

   BicubicDownsample(bubbleCanvas, bubbleHighResCanvas);        //--- Downsample to display canvas

   bubbleCanvas.FontSet("Arial", 13, FW_NORMAL);                //--- Set font for text
   string displayText = "Bubble";                               //--- Set display text
   int textWidth, textHeight;                                   //--- Declare text dimensions
   bubbleCanvas.TextSize(displayText, textWidth, textHeight);   //--- Get text size
   
   int bodyDisplayLeft = (int)(bubbleBodyLeft / supersamplingFactor); //--- Compute display left
   int bodyDisplayTop = (int)(bubbleBodyTop / supersamplingFactor); //--- Compute display top
   int bodyDisplayWidth = (int)((bubbleBodyRight - bubbleBodyLeft) / supersamplingFactor); //--- Compute display width
   int bodyDisplayHeight = (int)((bubbleBodyBottom - bubbleBodyTop) / supersamplingFactor); //--- Compute display height
   
   int textPositionX = bodyDisplayLeft + (bodyDisplayWidth - textWidth) / 2; //--- Compute text X position
   int textPositionY = bodyDisplayTop + (bodyDisplayHeight - textHeight) / 2; //--- Compute text Y position
   bubbleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas
}

void FillBubble(uint fillColor) {
   FillBubbleRoundedRectangle(bubbleBodyLeft, bubbleBodyTop, bubbleBodyRight - bubbleBodyLeft, bubbleBodyBottom - bubbleBodyTop, 
                              bubbleBodyCornerRadiusPixels * supersamplingFactor, fillColor); //--- Fill bubble body rectangle

   FillBubbleRoundedTriangle(fillColor);                        //--- Fill bubble pointer triangle
}

To combine the drawing logic, we define the "DrawBubble" function to orchestrate the rendering of the speech bubble on the canvas, starting by converting background and border colors to ARGB with opacity via "ColorToARGBWithOpacity" for translucent effects. To construct the shape, we call "FillBubble" with the background color to fill the high-res canvas, and if borders are enabled, invoke "DrawBubbleBorder" to add the outline. We then downsample to the standard canvas using "BicubicDownsample" for anti-aliased output. Finally, on the standard canvas, we set "Arial" font at size 13 with FW_NORMAL, measure and center the label "Bubble" within the body bounds scaled back to display coordinates, and draw it with TextOut in solid black aligned left for identification.

In the "FillBubble" function, we fill the body with "FillBubbleRoundedRectangle" using precomputed coordinates and scaled radius, then add the pointer fill via "FillBubbleRoundedTriangle", combining the shapes seamlessly on high-res for a unified bubble. We can now call this function in the initialization to draw the bubble as well.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   
   // added this for the bubble in initialization alongside the other prior shapes
   
   bubbleIsHorizontalOrientation = (bubblePointerOrientation == ORIENT_LEFT || bubblePointerOrientation == ORIENT_RIGHT); //--- Determine if orientation is horizontal

   int bubbleCanvasWidth, bubbleCanvasHeight;                   //--- Declare bubble canvas dimensions
   if(bubbleIsHorizontalOrientation) {                          //--- Check for horizontal orientation
      bubbleCanvasWidth = bubbleBodyWidthPixels + bubblePointerHeightPixels + 40; //--- Compute width for horizontal
      bubbleCanvasHeight = bubbleBodyHeightPixels + 40;         //--- Compute height for horizontal
   } else {                                                     //--- Handle vertical orientation
      bubbleCanvasWidth = bubbleBodyWidthPixels + 40;           //--- Compute width for vertical
      bubbleCanvasHeight = bubbleBodyHeightPixels + bubblePointerHeightPixels + 40; //--- Compute height for vertical
   }

   int bubblePositionY = trianglePositionY + triangleCanvasHeight + shapesGapPixels; //--- Set bubble Y position below triangle

   if(!bubbleCanvas.CreateBitmapLabel(0, 0, bubbleCanvasName, shapesPositionX, bubblePositionY,
                                    bubbleCanvasWidth, bubbleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create bubble canvas bitmap label
      Print("Error creating bubble canvas: ", GetLastError());  //--- Print error message if creation fails
      return(INIT_FAILED);                                      //--- Return initialization failure
   }

   if(!bubbleHighResCanvas.Create(bubbleCanvasName + "_hires",
                        bubbleCanvasWidth * supersamplingFactor,
                        bubbleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res bubble canvas
      Print("Error creating bubble hi-res canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                      //--- Return initialization failure
   }

   bubbleCanvas.Erase(ColorToARGB(clrNONE, 0));                 //--- Clear bubble canvas
   bubbleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));          //--- Clear high-res bubble canvas

   PrecomputeBubbleGeometry();                                  //--- Precompute bubble geometry
   
   DrawBubble();                                                //--- Draw bubble

   bubbleCanvas.Update();                                       //--- Update bubble canvas display

   return(INIT_SUCCEEDED);                                      //--- Return initialization success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   rectangleHighResCanvas.Destroy();                            //--- Destroy high-res rectangle canvas
   rectangleCanvas.Destroy();                                   //--- Destroy rectangle canvas
   ObjectDelete(0, rectangleCanvasName);                        //--- Delete rectangle canvas object

   triangleHighResCanvas.Destroy();                             //--- Destroy high-res triangle canvas
   triangleCanvas.Destroy();                                    //--- Destroy triangle canvas
   ObjectDelete(0, triangleCanvasName);                         //--- Delete triangle canvas object

   bubbleHighResCanvas.Destroy();                               //--- Destroy high-res bubble canvas
   bubbleCanvas.Destroy();                                      //--- Destroy bubble canvas
   ObjectDelete(0, bubbleCanvasName);                           //--- Delete bubble canvas object

   ChartRedraw();                                               //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Chart event function                                             |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam) {
   if(id == CHARTEVENT_CHART_CHANGE) {                          //--- Check for chart change event
      rectangleCanvas.Erase(ColorToARGB(clrNONE, 0));           //--- Clear rectangle canvas
      rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));    //--- Clear high-res rectangle canvas
      DrawRoundedRectangle();                                   //--- Redraw rounded rectangle
      rectangleCanvas.Update();                                 //--- Update rectangle canvas display

      triangleCanvas.Erase(ColorToARGB(clrNONE, 0));            //--- Clear triangle canvas
      triangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));     //--- Clear high-res triangle canvas
      DrawRoundedTriangle();                                    //--- Redraw rounded triangle
      triangleCanvas.Update();                                  //--- Update triangle canvas display

      bubbleCanvas.Erase(ColorToARGB(clrNONE, 0));              //--- Clear bubble canvas
      bubbleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));       //--- Clear high-res bubble canvas
      DrawBubble();                                             //--- Redraw bubble
      bubbleCanvas.Update();                                    //--- Update bubble canvas display
   }
}

We enhance the OnInit event handler to incorporate the speech bubble alongside previous shapes, determining if the orientation is horizontal with "bubbleIsHorizontalOrientation" based on left or right pointers, then calculating canvas dimensions accordingly—adding pointer height to width for horizontal, or to height for vertical, with padding. Next, we position the bubble below the triangle using the Y from the prior canvas plus gap, create the standard canvas via "CreateBitmapLabel" with ARGB normalize for transparency, and the high-res version with Create using scaled sizes, logging errors, and failing initialization if unsuccessful. To prepare rendering, we clear both with transparent ARGB, invoke "PrecomputeBubbleGeometry" for layout, draw via "DrawBubble", update the display, and return INIT_SUCCEEDED.

In the OnDeinit handler, we extend cleanup by destroying high-res and standard bubble canvases, deleting the object, and redrawing the chart. We have highlighted the specific extensions for clarity.

Finally, in OnChartEvent, for CHARTEVENT_CHART_CHANGE, we add bubble handling by clearing, redrawing with "DrawBubble", and updating alongside rectangle and triangle for responsive adaptation to chart modifications. Upon compilation, we get the following outcome.

ROUNDED SPEECH BUBBLE

From the image, we can see that the bubble is rendered correctly as per our objectives. Now, there is another change that we did to control the border extension length, by removing the fixed length and incorporating a ratio of the length for more dynamic control. We thought that was better because when we increased the border width, the connection between the pointer and the rectangle borders was not seamless. We updated the border extension in the "DrawRectStraightEdge" and "DrawTriStraightEdge" functions for consistency as follows.

double extensionLength = borderExtensionMultiplier * (double)thickness; //--- Set extension length

See the significance that the update brought, especially to the bubble borders.

EXTENSION LENGTHS COMPARISON

From the visualization, we can see how the extension ratio impacts the bubble connections. This is majorly important when you are using shapes with large widths. This was the easiest way we could solve the connection issue, rationing the extension dynamically. If you want a complex smoother approach, you can consider using the transition curves approach, commonly called the bezier curves approach. I learnt this in highway transportation engineering, and briefly, it looks like this.

BEZIER CURVES

It is important to note that there are different types of the bezier curves, and I would recommend the quadratic cubic method if you actually consider this approach, or the cubic method if you are bold enough. However, for us, we just stick with the simple extension method. Generally, we can see that we have created a speech bubble, hence achieving our objectives. What now remains is testing the workability of the system, and that is handled in the preceding section.


Backtesting

We did the testing, and below is the compiled visualization in a single Graphics Interchange Format (GIF) bitmap image format.

BACKTEST GIF


Conclusion

In conclusion, we’ve built rounded speech bubbles/balloons in MQL5 by combining vector-based rectangles and triangles, with orientation control for pointers in up, down, left, or right directions. We incorporated supersampling for anti-aliased rendering, along with customizable dimensions, radii, borders, and opacities to produce dynamic UI elements. With these rounded speech bubbles and orientation features, we’re equipped to develop engaging graphical components, ready for advanced applications in our future trading tools. Happy trading!

Introduction to MQL5 (Part 40): Beginner Guide to File Handling in MQL5 (II) Introduction to MQL5 (Part 40): Beginner Guide to File Handling in MQL5 (II)
Create a CSV trading journal in MQL5 by reading account history over a defined period and writing structured records to file. The article explains deal counting, ticket retrieval, symbol and order type decoding, and capturing entry (lot, time, price, SL/TP) and exit (time, price, profit, result) data with dynamic arrays. The result is an organized, persistent log suitable for analysis and reporting.
Price Action Analysis Toolkit Development (Part 61): Structural Slanted Trendline Breakouts with 3-Swing Validation Price Action Analysis Toolkit Development (Part 61): Structural Slanted Trendline Breakouts with 3-Swing Validation
We present a slanted trendline breakout tool that relies on three‑swing validation to generate objective, price‑action signals. The system automates swing detection, trendline construction, and breakout confirmation using crossing logic to reduce noise and standardize execution. The article explains the strategy rules, shows the MQL5 implementation, and reviews testing results; the tool is intended for analysis and signal confirmation, not automated trading.
Automating Market Memory Zones Indicator: Where Price is Likely to Return Automating Market Memory Zones Indicator: Where Price is Likely to Return
This article turns Market Memory Zones from a chart-only concept into a complete MQL5 Expert Advisor. It automates Displacement, Structure Transition (CHoCH), and Liquidity Sweep zones using ATR- and candle-structure filters, applies lower-timeframe confirmation, and enforces risk-based position sizing with dynamic SL and structure-based TP. You will get the code architecture for detection, entries, trade management, and visualization, plus a brief backtest review.
Risk Management (Part 4): Completing the Key Class Methods Risk Management (Part 4): Completing the Key Class Methods
This is Part 4 of our series on risk management in MQL5, where we continue exploring advanced methods for protecting and optimizing trading strategies. Having laid important foundations in earlier articles, we will now focus on completing all remaining methods postponed in Part 3, including functions for checking whether specific profit or loss levels have been reached. In addition, we will introduce new key events that enable more accurate and flexible risk management.