English
preview
Торговые инструменты на MQL5 (Часть 17): Изучение векторных скругленных прямоугольников и треугольников

Торговые инструменты на MQL5 (Часть 17): Изучение векторных скругленных прямоугольников и треугольников

MetaTrader 5Трейдинг |
61 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В своей предыдущей статье (Часть 16) мы улучшили панель на базе canvas на MetaQuotes Language 5 (MQL5) путем включения методов сглаживания и рендеринга с высоким разрешением, используя суперсэмплинг для получения более плавной графики, рамок и элементов. В Части 17 мы рассматриваем векторные методы для рисования скругленных прямоугольников и треугольников с использованием canvas, применяя метод суперсэмплирования для сглаживания изображения. Это закладывает основу для создания современных объектов canvas в будущих инструментах за счет обработки геометрических предварительных вычислений, заливки методом сканирования строк и точных рамок. В статье рассмотрим следующие темы:

  1. Изучение векторных скругленных прямоугольников и треугольников
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

К концу статьи, у вас появятся функции многократного использования для создания плавных, округлых форм, готовые к интеграции в продвинутые элементы пользовательского интерфейса. Приступим к реализации!


Изучение векторных скругленных прямоугольников и треугольников

Векторный подход к рендерингу скругленных прямоугольников и треугольников использует математические описания фигур — точки, линии и кривые — вместо пиксельных сеток, что позволяет создавать масштабируемые, независимые от разрешения графические изображения, сохраняющие четкость при любом размере. В отличие от растровых методов, которые при масштабировании могут создавать неровные края (зубчатость изображения), векторные методы вычисляют точные рамки и заливки, используя уравнения для дуг и касательных, что делает их идеальными для элементов пользовательского интерфейса в MQL5, где плавные визуальные эффекты повышают удобство использования без потери эффективности. Закругленные углы достигаются заменой острых вершин дугами окружности, радиусы которых определяют кривизну. Границы представляют собой смещенные контуры или утолщенные края, а суперсэмплинг дополнительно улучшает результат, выполняя рендеринг в более высоком разрешении с последующим понижением дискретизации для устранения артефактов.

Мы планируем реализовать объекты canvas высокого разрешения с помощью суперсэмплинга, предварительно вычислять геометрию дуг и касательных в треугольниках, применять заливку методом сканирования строк для обеих фигур, чтобы обеспечить точное отображение внутренних областей, а также добавлять настраиваемые рамки посредством векторных прямых линий и угловых дуг. Мы будем обрабатывать пользовательские входные параметры для указания размеров, радиусов, прозрачности и цветов в целях создания гибких, сглаженных фигур, подходящих для современных торговых интерфейсов. Вкратце, вот наглядное представление наших целей.

ROUNDED TRIANGLES AND RECTANGLES


Реализация средствами MQL5

Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку «Советники» (Experts), щелкните кнопкой мыши на вкладке "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нужно будет объявить некоторые входные параметры и глобальные переменные, которые будем использовать во всей программе.

//+------------------------------------------------------------------+
//|                           Rounded Rectangle & Triangle PART1.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

#include <Canvas\Canvas.mqh>

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "Position"
input int    shapesPositionX                  = 20;    // Shapes X position
input int    shapesPositionY                  = 20;    // Shapes Y position
input int    shapesGapPixels                  = 15;    // Gap between shapes (pixels)

input group "Rectangle"
input int    rectangleWidthPixels             = 250;   // Rectangle width
input int    rectangleHeightPixels            = 100;   // Rectangle height
input int    rectangleCornerRadiusPixels      = 5;     // Rectangle corner radius
input bool   rectangleShowBorder              = true;  // Show rectangle border
input int    rectangleBorderThicknessPixels   = 1;     // Rectangle border thickness
input color  rectangleBorderColor             = clrBlue; // Rectangle border color
input int    rectangleBorderOpacityPercent    = 80;      // Rectangle border opacity (0-100%)
input color  rectangleBackgroundColor         = clrBlue; // Rectangle background color
input int    rectangleBackgroundOpacityPercent= 30;      // Rectangle background opacity (0-100%)

input group "Triangle"
input int    triangleBaseWidthPixels          = 250;     // Triangle base width (pixels)
input double triangleHeightAsPercentOfWidth   = 86.6;    // Height as % of width (86.6=equilateral, <86.6=flat, >86.6=tall)
input int    triangleCornerRadiusPixels       = 12;      // Triangle corner radius
input bool   triangleShowBorder               = true;    // Show triangle border
input int    triangleBorderThicknessPixels    = 1;       // Triangle border thickness
input color  triangleBorderColor              = clrRed;  // Triangle border color
input int    triangleBorderOpacityPercent     = 80;      // Triangle border opacity (0-100%)
input color  triangleBackgroundColor          = clrRed;  // Triangle background color
input int    triangleBackgroundOpacityPercent = 30;      // Triangle background opacity (0-100%)

input group "General"
input int    supersamplingLevel               = 4;       // Supersampling level (1=off, 2=2x, 4=4x)

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CCanvas rectangleCanvas, rectangleHighResCanvas;                //--- Declare rectangle canvas objects
CCanvas triangleCanvas,  triangleHighResCanvas;                 //--- Declare triangle canvas objects
string  rectangleCanvasName = "RoundedRectCanvas";              //--- Set rectangle canvas name
string  triangleCanvasName  = "RoundedTriCanvas";               //--- Set triangle canvas name
int     supersamplingFactor;                                    //--- Store supersampling factor
int     computedTriangleHeightPixels;                           //--- Store computed triangle height in pixels
double triangleSharpVerticesX[3], triangleSharpVerticesY[3];    //--- Store sharp vertices for triangle
double triangleArcCentersX[3], triangleArcCentersY[3];          //--- Store arc centers for triangle
double triangleTangentPointsX[3][2], triangleTangentPointsY[3][2]; //--- Store tangent points for triangle
double triangleArcStartAngles[3], triangleArcEndAngles[3];      //--- Store arc sweep angles in radians
int    triangleHighResWidth, triangleHighResHeight;             //--- Store high-res dimensions for triangle

Начнем реализацию с подключения библиотеки Canvas с помощью макроса "#include <Canvas\Canvas.mqh>", который предоставляет необходимые классы и методы для создания и управления графическими объектами canvas в MQL5, позволяя нам рисовать пользовательские фигуры, такие как прямоугольники и треугольники, непосредственно на графике.

Далее мы организуем пользовательские данные в логические группы для более удобной настройки: группа "Position" (Положение) с параметрами для координат X и Y фигур и промежуток между ними; группа "Rectangle" (Прямоугольник), определяющая ширину, высоту, радиус скругления углов, параметры рамки, включая видимость, толщину, цвет и прозрачность, а также цвет и прозрачность фона; группа "Triangle" (Треугольник), аналогично задающая ширину основания, высоту в процентах от ширины (по умолчанию 86,6 для равносторонних пропорций), радиус скругления углов и соответствующие настройки рамки и фона; а также группа "General" (Общая) с уровнем суперсэмплирования для управления качеством сглаживания изображения (1 — отсутствие сглаживания, более высокие значения, например, 4, — означают улучшенную плавность).

Для поддержки рендеринга мы объявляем глобальные объекты canvas как для стандартной версии прямоугольника и треугольника, так и для версии с высоким разрешением, присваивая им имена типа "RoundedRectCanvas" и "RoundedTriCanvas" для идентификации. Наконец, мы создадим переменные для хранения коэффициента суперсэмплирования, вычисленной высоты треугольника и массивов для геометрии треугольника, включая острые вершины, центры дуг, точки касания (в виде массивов 3x2 для каждого угла), начальные и конечные углы в радианах, а также размеры в высоком разрешении для canvas треугольника. После установки глобальных переменных нам потребуется объявить несколько вспомогательных функций, которые будут использоваться на протяжении всей программы.

//+------------------------------------------------------------------+
//| Shared Utilities                                                 |
//+------------------------------------------------------------------+
uint ColorToARGBWithOpacity(color clr, int opacityPercent) {
   uchar redComponent = (uchar)((clr >> 0)  & 0xFF);             //--- Extract red component
   uchar greenComponent = (uchar)((clr >> 8)  & 0xFF);           //--- Extract green component
   uchar blueComponent = (uchar)((clr >> 16) & 0xFF);            //--- Extract blue component
   uchar alphaComponent = (uchar)((opacityPercent * 255) / 100); //--- Calculate alpha component from opacity
   return ((uint)alphaComponent << 24) | ((uint)redComponent << 16) | ((uint)greenComponent << 8) | (uint)blueComponent; //--- Combine into ARGB value
}

void BicubicDownsample(CCanvas &targetCanvas, CCanvas &highResCanvas) {
   int targetWidth  = targetCanvas.Width();                      //--- Get target canvas width
   int targetHeight = targetCanvas.Height();                     //--- Get target canvas height

   for(int pixelY = 0; pixelY < targetHeight; pixelY++) {        //--- Loop over target height pixels
      for(int pixelX = 0; pixelX < targetWidth; pixelX++) {      //--- Loop over target width pixels
         double sourceX = pixelX * supersamplingFactor;          //--- Calculate source X position
         double sourceY = pixelY * supersamplingFactor;          //--- Calculate source Y position

         double sumAlpha = 0, sumRed = 0, sumGreen = 0, sumBlue = 0; //--- Initialize sum variables
         double weightSum = 0;                                   //--- Initialize weight sum

         for(int deltaY = 0; deltaY < supersamplingFactor; deltaY++) { //--- Loop over delta Y for supersampling
            for(int deltaX = 0; deltaX < supersamplingFactor; deltaX++) { //--- Loop over delta X for supersampling
               int sourcePixelX = (int)(sourceX + deltaX);       //--- Compute source pixel X
               int sourcePixelY = (int)(sourceY + deltaY);       //--- Compute source pixel Y

               if(sourcePixelX >= 0 && sourcePixelX < highResCanvas.Width() && sourcePixelY >= 0 && sourcePixelY < highResCanvas.Height()) { //--- Check if within high-res bounds
                  uint pixelValue = highResCanvas.PixelGet(sourcePixelX, sourcePixelY); //--- Get pixel value from high-res canvas

                  uchar alpha = (uchar)((pixelValue >> 24) & 0xFF); //--- Extract alpha component
                  uchar red = (uchar)((pixelValue >> 16) & 0xFF);   //--- Extract red component
                  uchar green = (uchar)((pixelValue >> 8)  & 0xFF); //--- Extract green component
                  uchar blue = (uchar)(pixelValue         & 0xFF);  //--- Extract blue component

                  double weight = 1.0;                           //--- Set weight to 1.0
                  sumAlpha += alpha * weight;                    //--- Accumulate weighted alpha
                  sumRed += red * weight;                        //--- Accumulate weighted red
                  sumGreen += green * weight;                    //--- Accumulate weighted green
                  sumBlue += blue * weight;                      //--- Accumulate weighted blue
                  weightSum += weight;                           //--- Accumulate total weight
               }
            }
         }

         if(weightSum > 0) {                                       //--- Check if weight sum is positive
            uchar finalAlpha = (uchar)(sumAlpha / weightSum);      //--- Compute final alpha
            uchar finalRed = (uchar)(sumRed / weightSum);          //--- Compute final red
            uchar finalGreen = (uchar)(sumGreen / weightSum);      //--- Compute final green
            uchar finalBlue = (uchar)(sumBlue / weightSum);        //--- Compute final blue

            uint finalColor = ((uint)finalAlpha << 24) | ((uint)finalRed << 16) |
                              ((uint)finalGreen << 8)  | (uint)finalBlue; //--- Combine into final color
            targetCanvas.PixelSet(pixelX, pixelY, finalColor);     //--- Set pixel on target canvas
         }
      }
   }
}

double NormalizeAngle(double angle) {
   double twoPi = 2.0 * M_PI;                                   //--- Define two pi constant
   angle = MathMod(angle, twoPi);                               //--- Modulo angle by two pi
   if(angle < 0) angle += twoPi;                                //--- Adjust if angle is negative
   return angle;                                                //--- Return normalized angle
}

bool IsAngleBetween(double angle, double startAngle, double endAngle) {
   angle = NormalizeAngle(angle);                               //--- Normalize angle
   startAngle = NormalizeAngle(startAngle);                     //--- Normalize start angle
   endAngle = NormalizeAngle(endAngle);                         //--- Normalize end angle
   
   double span = NormalizeAngle(endAngle - startAngle);         //--- Compute span
   double relativeAngle = NormalizeAngle(angle - startAngle);   //--- Compute relative angle
   
   return relativeAngle <= span;                                //--- Return if within span
}

Начнем с создания функции "ColorToARGBWithOpacity", которая преобразует цвет в формат ARGB, добавляя при этом заданный процент непрозрачности. Мы извлекаем красную, зеленую и синюю составляющие с помощью битовых сдвигов, вычисляем альфа-канал, масштабируя непрозрачность в диапазоне 0-255, и объединяем их в одно целочисленное значение, что позволяет создавать прозрачные заливки и рамок для наших фигур. Далее мы реализуем функцию "BicubicDownsample" для выполнения сглаживания изображения при понижении дискретизации от высокого разрешения объекта canvas до целевого значения разрешения. Мы получаем целевые размеры, перебираем каждый пиксель, сопоставляем его с областью исходного изображения, полученного методом суперсэмплирования, накапливаем взвешенные суммы компонентов ARGB из субпикселей (с равномерным весом для усреднения) и, если существуют образцы, вычисляем окончательные значения перед установкой пикселя, тем самым сглаживая края путем смешивания деталей из более высокого разрешения.

Для обеспечения согласованности угловых вычислений мы определяем функцию "NormalizeAngle", используя константу, равную двум пи, для вычисления остатка от деления угла и корректировки отрицательных значений, гарантируя, что все углы находятся в диапазоне от 0 до 2 пи для надежного сравнения при рендеринге дуг. Далее мы добавляем функцию "IsAngleBetween", которая проверяет, находится ли угол в пределах начального и конечного диапазонов, нормализует входные данные, вычисляет нормализованный диапазон и относительное положение и возвращает значение true, если он находится в заданном диапазоне. Это крайне важно для точного включения пикселей в изогнутые рамки без перекрытия или разрывов. В дополнение к этим угловым операциям нам также понадобится функция для заполнения четырехугольной формы.

void FillQuadrilateral(CCanvas &canvas, double &verticesX[], double &verticesY[], uint fillColor) {
   double minY = verticesY[0], maxY = verticesY[0];             //--- Initialize min and max Y
   for(int i = 1; i < 4; i++) {                                 //--- Loop over vertices
      if(verticesY[i] < minY) minY = verticesY[i];              //--- Update min Y
      if(verticesY[i] > maxY) maxY = verticesY[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[8];                                 //--- Declare intersections array
      int intersectionCount = 0;                                //--- Initialize intersection count

      for(int i = 0; i < 4; i++) {                              //--- Loop over edges
         int nextIndex = (i + 1) % 4;                           //--- Get next index
         double x0 = verticesX[i],    y0 = verticesY[i];        //--- Get start coordinates
         double x1 = verticesX[nextIndex], y1 = verticesY[nextIndex]; //--- Get end coordinates

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

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

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

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

      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
            canvas.PixelSet(x, y, fillColor);                   //--- Set pixel with fill color
      }
   }
}

Мы реализуем функцию "FillQuadrilateral" для визуализации заполненных четырехугольников на canvas с использованием алгоритма сканирования строк, который обеспечивает точное векторное заполнение таких фигур, как рамки или тела, не полагаясь на встроенные методы, которые могут не поддаваться контролю. Для этого мы сначала определяем вертикальные границы, находя минимальные и максимальные координаты Y из входных вершин "verticesY", а затем вычисляем начальные и конечные целочисленные значения сканирования строк, используя значения ceiling и floor для полного покрытия. Для каждой строки сканирования y мы смещаем на полпикселя "scanlineY" для достижения субпиксельной точности, что способствует сглаживанию, и собираем до 8 точек пересечения по оси x путем интерполяции вдоль каждого из четырех ребер (используя остаток от деления для циклического закрытия), если строка сканирования пересекает ребро вертикально, пропуская горизонтальные точки или точки, выходящие за пределы диапазона.

Для сортировки этих пересечений мы используем пузырьковую сортировку для небольших массивов, а затем заполняем горизонтальные промежутки между парами значений x, задавая пиксели от верхнего левого угла до нижнего правого с помощью параметра "fillColor", используя метод PixelSet. Этот метод имеет решающее значение для обработки невыпуклых или неправильных четырехугольников при рендеринге высокого разрешения, поскольку он вычисляет точную заливку пиксель за пикселем, обеспечивая плавные границы в округлых формах за счет заполнения утолщенных реберных полос без наложений или зазоров.

Если вам интересно, что это за алгоритм сканирования строк, мы немного объясним, что это такое, чтобы у вас было понимание. Этот алгоритм обрабатывает изображение слева направо, сканируя по одной горизонтальной строке за раз, а не обрабатывая отдельные пиксели. Он фиксирует все точки пересечения ребер вдоль каждой строки сканирования и заполняет многоугольник, раскрашивая области между парами пересечений.

Это можно сравнить с проведением прямой линии поперек фигуры на бумаге одной ручкой: начиная от левой границы и двигаясь вправо, вы рисуете непрерывно, но всякий раз, когда сталкиваетесь с пересечением с границей многоугольника, вы соответственно останавливаете или возобновляете рисование. Алгоритм работает по тому же принципу. На рисунке ниже проиллюстрировано это поведение: красные точки представляют вершины многоугольника, в то время как синие точки указывают точки пересечения вдоль линии сканирования.

SCANLINE ALGORITHM

После этого мы можем использовать эти функции для создания округлых форм. Начнем с прямоугольника. Для этого нам также понадобятся вспомогательные функции, чтобы сделать наш код модульным.

void FillRoundedRectangleHiRes(int positionX, int positionY, int width, int height, int radius, uint fillColor) {
   rectangleHighResCanvas.FillRectangle(positionX + radius, positionY,          positionX + width - radius, positionY + height,          fillColor); //--- Fill central rectangle
   rectangleHighResCanvas.FillRectangle(positionX,          positionY + radius, positionX + radius,         positionY + height - radius, fillColor); //--- Fill left strip
   rectangleHighResCanvas.FillRectangle(positionX + width - radius, positionY + radius, positionX + width, positionY + height - radius, fillColor); //--- Fill right strip

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

void FillCircleQuadrant(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
            rectangleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, fillColor); //--- Set pixel
      }
   }
}

void DrawRoundedRectangleBorderHiRes(int positionX, int positionY, int width, int height, int radius, uint borderColorARGB) {
   int scaledThickness = rectangleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness

   DrawRectStraightEdge(positionX + radius, positionY, positionX + width - radius, positionY, scaledThickness, borderColorARGB); //--- Draw top edge
   DrawRectStraightEdge(positionX + width - radius, positionY + height - 1, positionX + radius, positionY + height - 1, scaledThickness, borderColorARGB); //--- Draw bottom edge
   DrawRectStraightEdge(positionX, positionY + height - radius, positionX, positionY + radius, scaledThickness, borderColorARGB); //--- Draw left edge
   DrawRectStraightEdge(positionX + width - 1, positionY + radius, positionX + width - 1, positionY + height - radius, scaledThickness, borderColorARGB); //--- Draw right edge

   DrawRectCornerArcPrecise(positionX + radius, positionY + radius, radius, scaledThickness, borderColorARGB, 
                            M_PI, M_PI * 1.5);                  //--- Draw top-left arc
   DrawRectCornerArcPrecise(positionX + width - radius, positionY + radius, radius, scaledThickness, borderColorARGB,
                            M_PI * 1.5, M_PI * 2.0);            //--- Draw top-right arc
   DrawRectCornerArcPrecise(positionX + radius, positionY + height - radius, radius, scaledThickness, borderColorARGB,
                            M_PI * 0.5, M_PI);                  //--- Draw bottom-left arc
   DrawRectCornerArcPrecise(positionX + width - radius, positionY + height - radius, radius, scaledThickness, borderColorARGB,
                            0.0, M_PI * 0.5);                   //--- Draw bottom-right arc
}

void DrawRectStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint borderColor) {
   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 = 1.5;                                //--- 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(rectangleHighResCanvas, verticesX, verticesY, borderColor); //--- Fill quadrilateral for edge
}

void DrawRectCornerArcPrecise(int centerX, int centerY, int radius, int thickness, uint borderColor,
                              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
            rectangleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, borderColor); //--- Set pixel
      }
   }
}

Начнём с реализации функции "FillRoundedRectangleHiRes", которая отобразит закрашенное тело скруглённого прямоугольника на объекте canvas высокого разрешения. Сначала заполняем центральную прямоугольную область, исключая углы. Далее, слева и справа добавляем вертикальные полосы, чтобы соединить прямые стороны. Такой подход обеспечивает бесшовное покрытие без наложений. Для завершения закругленных углов вызываем функцию "FillCircleQuadrant" для каждого квадранта. Мы передаем соответствующие значения центра, радиуса, цвета заливки и номер квадранта (1 для верхнего правого угла, 2 для верхнего левого угла и т. д.). Эта функция перебирает немного увеличенный диапазон пикселей, проверяет, находятся ли точки внутри квадранта и радиуса с помощью вычисления расстояния, и устанавливает подходящие пиксели. Это обеспечивает точное заполнение в виде четверти круга, плавно переходящее в полосы.

Далее мы создаём функцию "DrawRoundedRectangleBorderHiRes" для обработки границ, масштабирования толщины с помощью суперсэмплинга, отрисовки четырёх прямых граней с помощью функции "DrawRectStraightEdge" для верхней, нижней, левой и правой сторон, а также рендеринга угловых дуг с помощью функции "DrawRectCornerArcPrecise" с использованием предопределённых углов в радианах (например, от pi до 1,5pi для верхнего левого угла), что обеспечивает согласованную кривизну и сглаживание граней. В функции "DrawRectStraightEdge" мы вычисляем направления векторов и перпендикуляры от начальной до конечной точки, немного удлиняем линию для лучшего соединения углов, определяем четырехстороннюю полосу со смещениями в половину толщины и заливаем ее с помощью ранее определенной функции quadrilateral, создавая толстые, гладкие, прямые рамки, идеально совпадающие с дугами.

Наконец, функция "DrawRectCornerArcPrecise" формирует кольцо между внутренним и внешним радиусами, перебирая пиксели, проверяя расстояния и углы с помощью функции "IsAngleBetween" и устанавливая пиксели цвета рамки только в пределах указанной дуги, что крайне важно для получения высококачественных, криволинейных рамок без зазубрин в масштабированных изображениях. Теперь можно использовать эти функции для рисования скругленного прямоугольника.

//+------------------------------------------------------------------+
//| Rounded Rectangle                                                |
//+------------------------------------------------------------------+
void DrawRoundedRectangle() {
   int positionX      = 10 * supersamplingFactor;                     //--- Set X position scaled
   int positionY      = 10 * supersamplingFactor;                     //--- Set Y position scaled
   int scaledWidth  = rectangleWidthPixels  * supersamplingFactor;    //--- Scale width
   int scaledHeight = rectangleHeightPixels * supersamplingFactor;    //--- Scale height
   int scaledRadius = rectangleCornerRadiusPixels * supersamplingFactor; //--- Scale radius

   uint backgroundColorARGB     = ColorToARGBWithOpacity(rectangleBackgroundColor,     rectangleBackgroundOpacityPercent); //--- Get background ARGB
   uint borderColorARGB = ColorToARGBWithOpacity(rectangleBorderColor, rectangleBorderOpacityPercent); //--- Get border ARGB

   FillRoundedRectangleHiRes(positionX, positionY, scaledWidth, scaledHeight, scaledRadius, backgroundColorARGB); //--- Fill high-res rectangle

   if(rectangleShowBorder && rectangleBorderThicknessPixels > 0)      //--- Check if border should be shown
      DrawRoundedRectangleBorderHiRes(positionX, positionY, scaledWidth, scaledHeight, scaledRadius, borderColorARGB); //--- Draw border on high-res

   BicubicDownsample(rectangleCanvas, rectangleHighResCanvas);        //--- Downsample to display canvas

   rectangleCanvas.FontSet("Arial", 13, FW_NORMAL);                   //--- Set font for text
   string displayText = "Rounded Rectangle";                          //--- Set display text
   int textWidth, textHeight;                                         //--- Declare text dimensions
   rectangleCanvas.TextSize(displayText, textWidth, textHeight);      //--- Get text size
   int textPositionX = 10 + (rectangleWidthPixels  - textWidth)  / 2; //--- Compute text X position
   int textPositionY = 10 + (rectangleHeightPixels - textHeight) / 2; //--- Compute text Y position
   rectangleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas
}

Здесь мы определяем функцию "DrawRoundedRectangle" для управления отрисовкой скругленного прямоугольника на объекте canvas, начиная с масштабирования смещений положения, ширины, высоты и радиуса с использованием коэффициента суперсэмплирования для обеспечения точности с высоким разрешением и готовности к сглаживанию. Далее мы преобразуем цвета фона и рамок в формат ARGB, добавляя прозрачность с помощью функции "ColorToARGBWithOpacity", которая позволяет создавать полупрозрачные эффекты, усиливающие визуальную глубину без полной прозрачности. Для создания фигуры мы вызываем функцию "FillRoundedRectangleHiRes" с указанными параметрами масштабирования и цветом фона, чтобы заполнить внутреннюю часть объекта canvas высокого разрешения. Если границы включены с помощью функции "rectangleShowBorder" и толщина положительная, вызываем функцию "DrawRoundedRectangleBorderHiRes", чтобы добавить контур с цветом рамки.

Затем понижаем дискретизацию объекта canvas высокого разрешения до стандартной, используя функцию "BicubicDownsample", смешивая детали для получения плавного результата. Наконец, на стандартном объекте canvas мы устанавливаем шрифт с помощью FontSet, используя "Arial" размером 13 и толщиной FW_NORMAL, вычисляем центрированные позиции для метки "Rounded Rectangle" с помощью TextSize, чтобы получить размеры, и рисуем ее с помощью TextOut непрозрачным черным цветом (0xFF000000) с выравниванием по левому краю, создавая дескриптивный оверлей для большей наглядности. Теперь можно вызвать эту функцию в обработчике события инициализации, чтобы отобразить закругленный прямоугольник.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   supersamplingFactor = supersamplingLevel;                    //--- Assign supersampling factor from input
   if(supersamplingFactor < 1) {                                //--- Check if supersampling factor is less than 1
      Print("Warning: supersamplingLevel must be at least 1. Setting to 1."); //--- Print warning message
      supersamplingFactor = 1;                                  //--- Set supersampling factor to minimum value
   }


   int rectangleCanvasWidth = rectangleWidthPixels  + 40;       //--- Compute rectangle canvas width with padding
   int rectangleCanvasHeight = rectangleHeightPixels + 40;      //--- Compute rectangle canvas height with padding
   int rectanglePositionY       = shapesPositionY;              //--- Set rectangle Y position

   if(!rectangleCanvas.CreateBitmapLabel(0, 0, rectangleCanvasName, shapesPositionX, rectanglePositionY,
                                    rectangleCanvasWidth, rectangleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create rectangle canvas bitmap label
      Print("Error creating rectangle canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                        //--- Return initialization failure
   }

   if(!rectangleHighResCanvas.Create(rectangleCanvasName + "_hires",
                        rectangleCanvasWidth * supersamplingFactor,
                        rectangleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res rectangle canvas
      Print("Error creating rectangle hi-res canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                        //--- Return initialization failure
   }


   rectangleCanvas.Erase(ColorToARGB(clrNONE, 0));                //--- Clear rectangle canvas
   rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));         //--- Clear high-res rectangle canvas

   DrawRoundedRectangle();                                        //--- Draw rounded rectangle

   rectangleCanvas.Update();                                      //--- Update rectangle canvas display

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

В обработчике OnInit мы инициализируем программу, присваивая введенное пользователем значение "supersamplingLevel" глобальному значению "supersamplingFactor", проверяем, меньше ли оно, чем 1, и сбрасываем его до минимального значения, выводя предупреждение о корректном сглаживании. Далее мы вычисляем размеры прямоугольного объекта canvas, добавляя отступы к ширине и высоте входного значения, устанавливаем его положение по оси Y из значения "shapesPositionY" и создаем стандартный объект canvas с помощью функции CreateBitmapLabel, указывая идентификатор графика, подокно, имя, положение, размер и параметр COLOR_FORMAT_ARGB_NORMALIZE для поддержки прозрачности, выводя сообщения об ошибке с помощью GetLastError и возвращая INIT_FAILED в случае неудачи.

Затем мы создаём canvas высокого разрешения с помощью функции Create, указывая имя с суффиксом, масштабированные размеры, умноженные на "supersamplingFactor", и тот же цветовой формат, обрабатывая ошибки аналогичным образом. Для подготовки к рисованию очищаем оба объекта canvas с помощью функции Erase, передавая прозрачный цвет ARGB из clrNONE. Наконец, вызываем функцию "DrawRoundedRectangle" для выполнения фактического рендеринга, обновляем стандартное отображение объекта canvas с помощью "Update" и возвращаем INIT_SUCCEEDED для подтверждения успешной настройки. После компиляции получаем следующий результат.

ROUNDED RECTANGLE

На изображении видно, что мы отобразили закругленный прямоугольник. Теперь остается лишь использовать аналогичный подход для отрисовки закругленного треугольника. Для этого мы используем следующую логику. Начнём с создания вспомогательной функции для предварительного вычисления геометрии треугольника.

//+------------------------------------------------------------------+
//| Rounded Triangle                                                 |
//+------------------------------------------------------------------+
void PrecomputeTriangleGeometry() {
   int scalingFactor = supersamplingFactor;                     //--- Set scaling factor

   double basePositionX = 10.0 * scalingFactor;                 //--- Set base X position scaled
   double basePositionY = 10.0 * scalingFactor;                 //--- Set base Y position scaled
   double baseWidth = (double)triangleBaseWidthPixels   * scalingFactor; //--- Scale base width
   double baseHeight = (double)computedTriangleHeightPixels * scalingFactor; //--- Scale base height

   triangleSharpVerticesX[0] = basePositionX + baseWidth / 2.0; triangleSharpVerticesY[0] = basePositionY; //--- Set top vertex
   triangleSharpVerticesX[1] = basePositionX;               triangleSharpVerticesY[1] = basePositionY + baseHeight; //--- Set bottom-left vertex
   triangleSharpVerticesX[2] = basePositionX + baseWidth;          triangleSharpVerticesY[2] = basePositionY + baseHeight; //--- Set bottom-right vertex

   double scaledRadius = (double)triangleCornerRadiusPixels * scalingFactor; //--- Scale radius

   for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) {   //--- Loop over corners
      int previousIndex = (cornerIndex + 2) % 3;                //--- Get previous index
      int nextIndex = (cornerIndex + 1) % 3;                    //--- Get next index

      double edgeA_X = triangleSharpVerticesX[cornerIndex] - triangleSharpVerticesX[previousIndex],  edgeA_Y = triangleSharpVerticesY[cornerIndex] - triangleSharpVerticesY[previousIndex]; //--- Compute edge A vector
      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 = triangleSharpVerticesX[nextIndex] - triangleSharpVerticesX[cornerIndex],  edgeB_Y = triangleSharpVerticesY[nextIndex] - triangleSharpVerticesY[cornerIndex]; //--- Compute edge B vector
      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
      triangleArcCentersX[cornerIndex] = triangleSharpVerticesX[cornerIndex] + bisectorX * distanceToCenter; //--- Set arc center X
      triangleArcCentersY[cornerIndex] = triangleSharpVerticesY[cornerIndex] + bisectorY * distanceToCenter; //--- Set arc center Y

      double deltaX_A = triangleSharpVerticesX[cornerIndex] - triangleSharpVerticesX[previousIndex],  deltaY_A = triangleSharpVerticesY[cornerIndex] - triangleSharpVerticesY[previousIndex]; //--- Compute delta A
      double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A; //--- Compute length squared A
      double interpolationFactor_A = ((triangleArcCentersX[cornerIndex] - triangleSharpVerticesX[previousIndex])*deltaX_A + (triangleArcCentersY[cornerIndex] - triangleSharpVerticesY[previousIndex])*deltaY_A) / lengthSquared_A; //--- Compute factor A
      triangleTangentPointsX[cornerIndex][1] = triangleSharpVerticesX[previousIndex] + interpolationFactor_A * deltaX_A; //--- Set tangent point X arriving
      triangleTangentPointsY[cornerIndex][1] = triangleSharpVerticesY[previousIndex] + interpolationFactor_A * deltaY_A; //--- Set tangent point Y arriving

      double deltaX_B = triangleSharpVerticesX[nextIndex] - triangleSharpVerticesX[cornerIndex],  deltaY_B = triangleSharpVerticesY[nextIndex] - triangleSharpVerticesY[cornerIndex]; //--- Compute delta B
      double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B; //--- Compute length squared B
      double interpolationFactor_B = ((triangleArcCentersX[cornerIndex] - triangleSharpVerticesX[cornerIndex])*deltaX_B + (triangleArcCentersY[cornerIndex] - triangleSharpVerticesY[cornerIndex])*deltaY_B) / lengthSquared_B; //--- Compute factor B
      triangleTangentPointsX[cornerIndex][0] = triangleSharpVerticesX[cornerIndex] + interpolationFactor_B * deltaX_B; //--- Set tangent point X leaving
      triangleTangentPointsY[cornerIndex][0] = triangleSharpVerticesY[cornerIndex] + interpolationFactor_B * deltaY_B; //--- Set tangent point Y leaving

      triangleArcStartAngles[cornerIndex] = MathArctan2(triangleTangentPointsY[cornerIndex][1] - triangleArcCentersY[cornerIndex], triangleTangentPointsX[cornerIndex][1] - triangleArcCentersX[cornerIndex]); //--- Set start angle
      triangleArcEndAngles[cornerIndex]   = MathArctan2(triangleTangentPointsY[cornerIndex][0] - triangleArcCentersY[cornerIndex], triangleTangentPointsX[cornerIndex][0] - triangleArcCentersX[cornerIndex]); //--- Set end angle
   }
}

bool AngleInArcSweep(int cornerIndex, double angle) {
   double twoPi  = 2.0 * M_PI;                                  //--- Define two pi constant
   double startAngleMod  = MathMod(triangleArcStartAngles[cornerIndex] + twoPi, twoPi); //--- Modulo start angle
   double endAngleMod  = MathMod(triangleArcEndAngles[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
   }
}

Мы начинаем с функции "PrecomputeTriangleGeometry", чтобы подготовить геометрические данные для рендеринга закругленного треугольника на объекте canvas с высоким разрешением, присвоив коэффициент суперсэмплирования локальной переменной, масштабировав базовые положения, ширину и высоту на основе входных данных, чтобы сохранить пропорции в высоком разрешении, и определив три острые вершины: верхнюю в центре по оси X с основанием по оси Y, нижнюю левую в основании по оси X с добавленной высотой и нижнюю правую в основании по оси X плюс ширина с той же высотой. Затем мы масштабируем радиус угла и проходим циклом по каждому из трех углов, используя "cornerIndex", вычисляя предыдущий и следующий индексы по модулю 3 для циклической обработки, вычисляя и нормализуя векторы ребер A (от предыдущего к текущему) и B (от текущего к следующему), выводя внешние нормали путем поворота на 90 градусов, и формируя биссектрису угла путем суммирования и нормализации нормалей с возвратом к одной нормали, если длина близка к нулю, чтобы избежать ошибок деления.

Для позиционирования центра дуги мы вычисляем косинус внутреннего угла из скалярного произведения отрицательного ребра, ограничиваем его, находим половинный угол и его синус (с минимумом, чтобы предотвратить равенство нулю), вычисляем расстояние вдоль биссектрисы как радиус, деленный на синус, и устанавливаем центры дуг, смещаясь относительно вершины. Затем мы проецируем центр дуги на каждое смежное ребро, чтобы найти точки касания: для входящего ребра (A) используя векторную проекцию для хранения в индексе 1 массивов касательных, и для выходящего ребра (B) — в индексе 0, обеспечивая плавные переходы между прямыми сторонами и дугами. Наконец, мы задаем начальный и конечный углы для каждого пролета дуги, используя MathArctan2 на смещениях касательных от центра, что определяет точный угловой диапазон для последующих проверок пикселей во время заливки и обводки контуров. Это делает предварительные вычисления необходимыми для точного, векторно-управляемого скругления без искажений.

В функции "AngleInArcSweep" мы нормализуем начальный, конечный и входной углы до значений от 0 до 2 pi с помощью MathMod и сложения, вычисляем хорду против часовой стрелки, и если она равна pi или меньше (короткая дуга), проверяем относительный угол от начала. В противном случае используем хорду по часовой стрелке и проверяем от конца, добавляя небольшой эпсилон для допуска по числам с плавающей запятой, что позволяет надежно определить, попадает ли угол точки в пределы дуги независимо от направления. Далее создадим параметрические вычислительные функции.

void FillRoundedTriangleHiRes(uint fillColor) {
   double minY = triangleSharpVerticesY[0], maxY = triangleSharpVerticesY[0]; //--- Initialize min and max Y
   for(int i = 1; i < 3; i++) {                                 //--- Loop over vertices
      if(triangleSharpVerticesY[i] < minY) minY = triangleSharpVerticesY[i]; //--- Update min Y
      if(triangleSharpVerticesY[i] > maxY) maxY = triangleSharpVerticesY[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 straight edges
         int nextIndex = (edgeIndex + 1) % 3;                   //--- Get next index
         double startX = triangleTangentPointsX[edgeIndex][0],    startY = triangleTangentPointsY[edgeIndex][0]; //--- Get start tangent
         double endX = triangleTangentPointsX[nextIndex][1], endY = triangleTangentPointsY[nextIndex][1]; //--- Get end 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
      }

      for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) { //--- Loop over corner arcs
         double centerX = triangleArcCentersX[cornerIndex],  centerY = triangleArcCentersY[cornerIndex]; //--- Get arc center
         double radius  = (double)triangleCornerRadiusPixels * supersamplingFactor; //--- Get scaled radius
         double deltaY = scanlineY - centerY;                   //--- Compute delta Y

         if(MathAbs(deltaY) > radius) continue;                 //--- Skip if outside 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(AngleInArcSweep(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
            triangleHighResCanvas.PixelSet(x, y, fillColor);     //--- Set pixel with fill color
      }
   }
}

void DrawRoundedTriangleBorderHiRes(uint borderColor) {
   int scaledThickness = triangleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness

   for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {         //--- Loop over edges
      int nextIndex = (edgeIndex + 1) % 3;                      //--- Get next index
      double startX = triangleTangentPointsX[edgeIndex][0],    startY = triangleTangentPointsY[edgeIndex][0]; //--- Get start tangent
      double endX = triangleTangentPointsX[nextIndex][1], endY = triangleTangentPointsY[nextIndex][1]; //--- Get end tangent

      DrawTriStraightEdge(startX, startY, endX, endY, scaledThickness, borderColor); //--- Draw straight edge
   }

   for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++)     //--- Loop over corners
      DrawTriCornerArcPrecise(cornerIndex, scaledThickness, borderColor); //--- Draw corner arc
}

void DrawTriStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint borderColor) {
   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 = 1.5;                                //--- 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(triangleHighResCanvas, verticesX, verticesY, borderColor); //--- Fill quadrilateral for edge
}

void DrawTriCornerArcPrecise(int cornerIndex, int thickness, uint borderColor) {
   double centerX = triangleArcCentersX[cornerIndex],  centerY = triangleArcCentersY[cornerIndex]; //--- Get arc center
   double radius  = (double)triangleCornerRadiusPixels * 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(AngleInArcSweep(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 < triangleHighResWidth && pixelY >= 0 && pixelY < triangleHighResHeight) //--- Check if within bounds
               triangleHighResCanvas.PixelSet(pixelX, pixelY, borderColor); //--- Set pixel
         }
      }
   }
}

Здесь мы реализуем функцию "FillRoundedTriangleHiRes" для отрисовки закрашенной внутренней части скругленного треугольника на объекте canvas высокого разрешения с использованием алгоритма сканирования строк, сначала определяя вертикальные границы от острых вершин с помощью минимального и максимального значений Y, а затем перебирая в цикле каждое целое число y со смещением в полпикселя для повышения точности. Для каждой строки сканирования мы собираем точки пересечения по оси x с тремя касательными ребрами с помощью линейной интерполяции, если они находятся в пределах диапазона, а также с угловыми дугами, решая уравнение окружности для deltaX при заданном deltaY, добавляя кандидатов только в том случае, если их углы проходят параметр "AngleInArcSweep", чтобы обеспечить ограничение дуги. Мы сортируем пересечения с помощью пузырьковой сортировки, затем заполняем промежутки между парами, используя PixelSet с параметром fillColor, обеспечивая точное сглаживание, которое использует предварительно вычисленную геометрию для плавных кривых.

Далее, в функции "DrawRoundedTriangleBorderHiRes" мы масштабируем толщину рамки и выполняем цикл по ребрам для рисования прямых сегментов с помощью "DrawTriStraightEdge", а затем угловых дуг с помощью "DrawTriCornerArcPrecise", создавая полный, утолщенный контур. Для построения каждого прямого ребра в функции "DrawTriStraightEdge" мы вычисляем векторы направления и перпендикулярные векторы от точек касания, слегка удлиняем конечные точки для бесшовных соединений, определяем четырехугольную полосу, смещенную на половину толщины, и заполняем ее с помощью функции "FillQuadrilateral" для получения равномерной ширины рамки.

Наконец, функция "DrawTriCornerArcPrecise" формирует изогнутое кольцо рамки для каждого угла, вычисляя внутренний и внешний радиусы, перебирая расширенную пиксельную сетку и устанавливая пиксели, если расстояния попадают в кольцо, а углы удовлетворяют условию "AngleInArcSweep", с проверкой границ во избежание переполнения текста за границы, что обеспечивает высококачественные, ровные границы без зазубрин при масштабировании изображения. Теперь можно использовать эти функции для вычисления функции, которую будем использовать для создания закругленного треугольника, то есть для соединения всех элементов.

void DrawRoundedTriangle() {
   uint backgroundColorARGB     = ColorToARGBWithOpacity(triangleBackgroundColor,     triangleBackgroundOpacityPercent); //--- Get background ARGB
   uint borderColorARGB = ColorToARGBWithOpacity(triangleBorderColor, triangleBorderOpacityPercent); //--- Get border ARGB

   FillRoundedTriangleHiRes(backgroundColorARGB);               //--- Fill high-res triangle

   if(triangleShowBorder && triangleBorderThicknessPixels > 0)  //--- Check if border should be shown
      DrawRoundedTriangleBorderHiRes(borderColorARGB);          //--- Draw border on high-res

   BicubicDownsample(triangleCanvas, triangleHighResCanvas);    //--- Downsample to display canvas

   triangleCanvas.FontSet("Arial", 13, FW_NORMAL);              //--- Set font for text
   string displayText = "Rounded Triangle";                     //--- Set display text
   int textWidth, textHeight;                                   //--- Declare text dimensions
   triangleCanvas.TextSize(displayText, textWidth, textHeight); //--- Get text size
   int textPositionX = 10 + (triangleBaseWidthPixels   - textWidth)  / 2; //--- Compute text X position
   int textPositionY = 10 + (computedTriangleHeightPixels - textHeight) / 2; //--- Compute text Y position
   triangleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas
}

Мы определяем функцию "DrawRoundedTriangle" для управления отрисовкой скругленного треугольника на объекте canvas, начиная с преобразования цветов фона и рамки в ARGB с интеграцией прозрачности с помощью "ColorToARGBWithOpacity", что позволяет настраивать прозрачность и добавлять глубину фигуре. Для создания внутреннего пространства мы вызываем функцию "FillRoundedTriangleHiRes" с фоновым цветом ARGB, чтобы заполнить объект canvas высокого разрешения с помощью предварительно вычисленной геометрией. Если рамки активированы с помощью функции "triangleShowBorder" и толщина задана положительно, мы вызываем "DrawRoundedTriangleBorderHiRes", чтобы добавить контур с цветовой гаммой ARGB для рамки. Затем мы понижаем дискретизацию изображения с высокого до стандартного canvas, используя функцию "BicubicDownsample" для сглаживания.

Наконец, на стандартном объекте canvas мы настраиваем шрифт с помощью параметра "FontSet" на "Arial" размером 13 и FW_NORMAL, измеряем и центрируем метку "Rounded Triangle" с помощью параметра TextSize и рисуем ее с помощью "TextOut" сплошным черным цветом (0xFF000000) с выравниванием по левому краю, что улучшает идентификацию. Однако вы можете использовать любой цветовой формат по своему выбору. Теперь мы будем использовать ту же логику для отображения треугольника на графике, что и для прямоугольника, и теперь весь фрагмент кода инициализации выглядит следующим образом.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   supersamplingFactor = supersamplingLevel;                    //--- Assign supersampling factor from input
   if(supersamplingFactor < 1) {                                //--- Check if supersampling factor is less than 1
      Print("Warning: supersamplingLevel must be at least 1. Setting to 1."); //--- Print warning message
      supersamplingFactor = 1;                                  //--- Set supersampling factor to minimum value
   }

   computedTriangleHeightPixels = (int)MathRound((double)triangleBaseWidthPixels * triangleHeightAsPercentOfWidth / 100.0); //--- Calculate triangle height based on width and percentage
   if(computedTriangleHeightPixels < 10) {                      //--- Check if computed height is too small
      Print("Warning: Computed triangle height too small (" + string(computedTriangleHeightPixels) + "px). Minimum set to 10."); //--- Print warning message
      computedTriangleHeightPixels = 10;                        //--- Set minimum height value
   }

   int rectangleCanvasWidth = rectangleWidthPixels  + 40;       //--- Compute rectangle canvas width with padding
   int rectangleCanvasHeight = rectangleHeightPixels + 40;      //--- Compute rectangle canvas height with padding
   int rectanglePositionY       = shapesPositionY;              //--- Set rectangle Y position

   if(!rectangleCanvas.CreateBitmapLabel(0, 0, rectangleCanvasName, shapesPositionX, rectanglePositionY,
                                    rectangleCanvasWidth, rectangleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create rectangle canvas bitmap label
      Print("Error creating rectangle canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                        //--- Return initialization failure
   }

   if(!rectangleHighResCanvas.Create(rectangleCanvasName + "_hires",
                        rectangleCanvasWidth * supersamplingFactor,
                        rectangleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res rectangle canvas
      Print("Error creating rectangle hi-res canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                        //--- Return initialization failure
   }

   int triangleCanvasWidth  = triangleBaseWidthPixels   + 40;     //--- Compute triangle canvas width with padding
   int triangleCanvasHeight  = computedTriangleHeightPixels + 40; //--- Compute triangle canvas height with padding
   int trianglePositionY        = rectanglePositionY + rectangleCanvasHeight + shapesGapPixels; //--- Set triangle Y position below rectangle

   triangleHighResWidth = triangleCanvasWidth  * supersamplingFactor;    //--- Compute high-res triangle width
   triangleHighResHeight  = triangleCanvasHeight  * supersamplingFactor; //--- Compute high-res triangle height

   if(!triangleCanvas.CreateBitmapLabel(0, 0, triangleCanvasName, shapesPositionX, trianglePositionY,
                                   triangleCanvasWidth, triangleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create triangle canvas bitmap label
      Print("Error creating triangle canvas: ", GetLastError());  //--- Print error message if creation fails
      return(INIT_FAILED);                                        //--- Return initialization failure
   }

   if(!triangleHighResCanvas.Create(triangleCanvasName + "_hires",
                       triangleHighResWidth, triangleHighResHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res triangle canvas
      Print("Error creating triangle hi-res canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                      //--- Return initialization failure
   }

   rectangleCanvas.Erase(ColorToARGB(clrNONE, 0));              //--- Clear rectangle canvas
   rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));       //--- Clear high-res rectangle canvas
   triangleCanvas.Erase(ColorToARGB(clrNONE, 0));               //--- Clear triangle canvas
   triangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));        //--- Clear high-res triangle canvas

   PrecomputeTriangleGeometry();                                //--- Precompute triangle geometry
   DrawRoundedRectangle();                                      //--- Draw rounded rectangle
   DrawRoundedTriangle();                                       //--- Draw rounded triangle

   rectangleCanvas.Update();                                    //--- Update rectangle canvas display
   triangleCanvas.Update();                                     //--- Update triangle 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

   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
   }
}

В обработчике OnDeinit очищаем ресурсы при завершении работы программы, уничтожая объекты canvas высокого разрешения и стандартные для прямоугольников и треугольников с помощью метода Destroy. После этого удаляем их объекты на графике с помощью ObjectDelete для освобождения памяти и удаления визуальных остатков. Затем вызываем ChartRedraw для обновления графика, чтобы убедиться в отсутствии остаточных артефактов.

Далее, в обработчике OnChartEvent мы реагируем на событие CHARTEVENT_CHART_CHANGE. Он срабатывает при изменении размера графика или изменении его свойств, очищая оба прямоугольных объекта canvas с помощью функции "Erase" с использованием прозрачного ARGB из "clrNONE", перерисовывая скругленный прямоугольник с помощью функции "DrawRoundedRectangle" и обновляя отображение с помощью метода Update. Аналогичным образом очищаем объекты canvas треугольников, перерисовываем их с помощью функции "DrawRoundedTriangle" и обновляем, сохраняя интерактивные визуальные элементы при изменениях графика. После компиляции получаем следующий результат.

ROUNDED TRIANGLE AND RECTANGLE RENDER

Как видно на изображении, мы создали закругленный треугольник и прямоугольник, тем самым достигнув поставленных целей. Теперь остаётся проверить работоспособность системы, что и рассматривается в следующем разделе.


Тестирование на истории

Мы провели тестирование, а ниже приведена итоговая визуализация в формате Graphics Interchange Format (GIF).

BACKTEST GIF


Заключение

В заключение отметим, что мы рассмотрели векторные методы для рисования скругленных прямоугольников и треугольников на MQL5 с использованием canvas, применяя метод суперсэмплирования для рендеринга со сглаживанием изображения. Мы реализовали заливку методом сканирования строк, геометрические предварительные вычисления для дуг и касательных, а также рисование рамок для создания плавных, настраиваемых фигур. Такой подход закладывает основу для современных элементов пользовательского интерфейса в наших будущих торговых инструментах, поддерживающего входные параметры для установки размеров, радиусов, рамок и прозрачности. В следующей части мы рассмотрим, как можно объединить эти две фигуры, чтобы создать современный пузырь с указателем, который можно использовать в различных приложениях. Следите за обновлениями!

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21264

Прикрепленные файлы |
Разработка инструментария анализа Price Action (Часть 25): Пробой фракталов по двум EMA Разработка инструментария анализа Price Action (Часть 25): Пробой фракталов по двум EMA
Price Action – это фундаментальный подход к выявлению прибыльных сетапов. Однако вручную отслеживать движение цены и паттерны бывает сложно и долго. Для решения этой задачи мы разрабатываем инструменты, которые автоматически анализируют Price Action и подают своевременные сигналы при обнаружении потенциальных возможностей. В этой статье представлен надежный инструмент, который использует пробои фракталов в сочетании с EMA 14 и EMA 200 для генерации надежных торговых сигналов, помогая трейдерам принимать более обоснованные решения.
Внедрение в MQL5 практических модулей из других языков (Часть 05): Модуль Logging из Python — ведите логи профессионально Внедрение в MQL5 практических модулей из других языков (Часть 05): Модуль Logging из Python — ведите логи профессионально
Интеграция модуля Logging языка Python с языком MQL5 предоставляет трейдерам систематический подход к ведению логов, упрощая процесс мониторинга, отладки и документирования торговой деятельности. В этой статье описывается процесс адаптации, предлагая трейдерам мощный инструмент для поддержания четкости и организованности в процессе разработки программного обеспечения для трейдинга.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Нелинейные признаки OHLC из эллиптических кривых Нелинейные признаки OHLC из эллиптических кривых
В статье рассматривается проекция дневных свечей EURUSD на эллиптическую кривую secp256k1 и извлечение 96 признаков (EC+TA) для прогноза направления следующей свечи в CatBoost. Показаны маппинг цен на кривую и конвейер обучения на 2000 барах D1; полная модель достигает AUC на тесте 0,6508, вклад EC-признаков — 60,6%. Материалы пригодны для воспроизведения в Python/MetaTrader 5.