English
preview
Торговые инструменты MQL5 (Часть 28): Полигональная заливка кривой-бабочки в MQL5

Торговые инструменты MQL5 (Часть 28): Полигональная заливка кривой-бабочки в MQL5

MetaTrader 5Торговые системы |
50 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

У вас уже есть параметрическая кривая-бабочка, аккуратно отрисованная на холсте MetaTrader 5. Четыре цветных сегмента обрисовывают математический контур плавными, сглаженными штрихами. Однако крылья пусты, нет тела, нет текстуры — ничто не делает её чем-то большим, чем сухая математическая схема. Эта статья предназначена для разработчиков MetaQuotes Language 5 (MQL5) и креативных программистов, которые хотят выйти за рамки контура и заполнить бабочку многослойной цветовой гаммой, реалистичной детализацией крыльев и полной анатомической структурой.

В своей предыдущей статье (Часть 27) мы создали визуальный инструмент на базе холста на MQL5. Он отрисовывает кривую-бабочку — параметрическое математическое уравнение — непосредственно на графике MetaTrader 5. Мы реализовали многослойную систему холста с градиентным фоном, плавающим окном с возможностью перетаскивания и изменения размера, рендерингом кривых с суперсэмплированием и сглаживанием по четырем цветным сегментам. Инструмент включает в себя калиброванную сетку осей с отметками делений и метками, а также плавающую панель легенды, идентифицирующую каждый сегмент. В этой статье мы развиваем эту основу. Мы представляем многослойную градиентную заливку крыльев, жилки крыла, точки текстуры чешуек и полностью детализированное анатомическое тело с сегментированным брюшком, грудью, головой, сложными глазами и изогнутыми усиками — все это отрисовано с помощью одного и того же конвейера суперсэмплирования. Мы рассмотрим следующие темы:

  1. Понимание заливки крыльев, текстуры и анатомической структуры бабочки
  2. Реализация средствами MQL5
  3. Визуализация
  4. Заключение

К концу статьи вы превратите простой параметрический контур кривой в визуально насыщенную и реалистичную иллюстрацию бабочки, отрисованную непосредственно на графике MetaTrader 5. Давайте погрузимся в процесс!


Понимание заливки крыльев, текстуры и анатомической структуры бабочки

Настоящее крыло бабочки — это не однотонный, однородный цвет. Это многослойная структура, состоящая из тысяч перекрывающихся чешуек, по-разному отражающих свет. Это создает градиенты, которые переходят от насыщенных оттенков у тела к более светлым, полупрозрачным тонам по краям. Эта естественная многослойность придает крыльям бабочки глубину и переливчатость. Мы воспроизводим тот же принцип на холсте, заполняя крыло снаружи внутрь постепенно уменьшающимися слоями. Каждый слой использует разные цвета и перекрывает предыдущий.

Для заполнения формы крыла, заданной параметрической кривой, мы используем заливкe многоугольника методом сканирующей строки. Это стандартный метод растеризации замкнутых фигур на пиксельной сетке. Для каждого горизонтального ряда пикселей в пределах вертикальной протяженности крыла мы находим точку пересечения границы крыла с этим рядом, а затем закрашиваем каждый пиксель между этими точками пересечения. Цвет каждого пикселя определяется либо его вертикальным положением внутри крыла — создавая плавный градиент сверху вниз — либо его радиальным расстоянием от центра крыла, создавая градиент, который расширяется наружу, подобно реальному цветовому узору, исходящему от тела. Всего мы применяем три слоя заливки: самый внешний слой крыла с вертикальным градиентом, первый внутренний слой, масштабированный внутрь и заполненный другим набором цветов, и второй внутренний слой, заполненный радиальным градиентом для эффекта свечения в центре.

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

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

REALISTIC FILLED BUTTERFLY CURVE IN MT5 CHART


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

Расширение входных параметров для заливок крыльев, тела и деталей крыльев

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

//+------------------------------------------------------------------+
//|                      Canvas Drawing PART 2 - Butterfly Curve.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

//---

input group "=== BUTTERFLY FILL SETTINGS ==="
input bool   enableButterflyFill    = true;          // Enable Coloring (Fill)
input double butterflyFillOpacity   = 0.7;           // Butterfly Fill Opacity (0-1)
input color  fillBottomColor        = clrDarkOrange; // Fill Bottom Color
input color  fillMiddleColor        = clrGreen;      // Fill Middle Color
input color  fillTopColor           = clrBlue;       // Fill Top Color

input group "=== INNER BUTTERFLY FILL SETTINGS ==="
input bool   enableInnerButterflyFill    = true;       // Enable Inner Fill
input double innerButterflyScale         = 0.85;       // Inner Scale Factor (0-1)
input double innerButterflyFillOpacity   = 0.8;        // Inner Fill Opacity (0-1)
input color  innerFillBottomColor        = clrYellow;  // Inner Fill Bottom Color
input color  innerFillMiddleColor        = clrDarkRed; // Inner Fill Middle Color
input color  innerFillTopColor           = clrPurple;  // Inner Fill Top Color

input group "=== SECOND INNER BUTTERFLY FILL SETTINGS ==="
input bool   enableSecondInnerButterflyFill    = true;     // Enable Second Inner Fill
input double secondInnerButterflyScale         = 0.6;      // Second Inner Scale Factor (0-1)
input double secondInnerButterflyFillOpacity   = 0.75;     // Second Inner Fill Opacity (0-1)
input color  secondInnerFillBottomColor        = clrBrown; // Second Inner Fill Bottom Color
input color  secondInnerFillMiddleColor        = clrTan;   // Second Inner Fill Middle Color
input color  secondInnerFillTopColor           = clrPlum;  // Second Inner Fill Top Color

input group "=== BUTTERFLY BODY SETTINGS ==="
input color butterflyBodyColor    = C'50,30,20';   // Body Color (Dark Brown)
input color butterflyEyeColor     = clrBlack;      // Eye Color
input color butterflyAntennaColor = C'40,25,15';   // Antenna Color

input group "=== BUTTERFLY WING SETTINGS ==="
input bool   showButterflyWingVeins  = true; // Show Wing Veins
input bool   showButterflyWingScales = true; // Show Wing Scales
input double butterflyWingOpacity    = 0.85; // Wing Opacity (0-1)

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

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

Разрешение цвета крыла по параметрическому сегменту

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

//+------------------------------------------------------------------+
//| Resolve wing curve color for a given parametric T value          |
//+------------------------------------------------------------------+
color GetWingColorForT(double tParameter)
  {
   //--- Define the T boundary ending segment 1 (blue)
   double segmentEnd1 = 3.0 * M_PI;
   //--- Define the T boundary ending segment 2 (red)
   double segmentEnd2 = 6.0 * M_PI;
   //--- Define the T boundary ending segment 3 (orange)
   double segmentEnd3 = 9.0 * M_PI;

   //--- Return blue for the first parametric segment
   if(tParameter <= segmentEnd1) return blueCurveColor;
   //--- Return red for the second parametric segment
   else if(tParameter <= segmentEnd2) return redCurveColor;
   //--- Return orange for the third parametric segment
   else if(tParameter <= segmentEnd3) return orangeCurveColor;
   //--- Return green for the fourth parametric segment
   else return greenCurveColor;
  }

Мы определяем три граничных значения сегмента на 3π, 6π и 9π — те же деления, которые используются при рисовании контуров кривой — и используем простую цепочку условий для возврата соответствующего цвета кривой для того сегмента, в который попадает данный параметр. Точки до 3π возвращают синий цвет, до 6π — красный, до 9π — оранжевый, а все значения за этими пределами, возвращают зеленый цвет. Это гарантирует, что при размещении точек текстуры чешуек вдоль границы крыла каждая из них наследует корректный цвет сегмента кривой, к которому она принадлежит, обеспечивая визуальную согласованность текстуры чешуек с цветами контура под ней. Далее мы определим вспомогательные функции для рисования залитых кругов и эллипсов.

//+------------------------------------------------------------------+
//| Draw a solid filled circle at a given canvas position            |
//+------------------------------------------------------------------+
void DrawFilledCircle(CCanvas &canvas, int centerX, int centerY, int radius, uint argbColor)
  {
   //--- Iterate over every row within the bounding box of the circle
   for(int deltaY = -radius; deltaY <= radius; deltaY++)
     {
      //--- Iterate over every column within the bounding box
      for(int deltaX = -radius; deltaX <= radius; deltaX++)
        {
         //--- Test if the current offset falls inside the circle
         if(deltaX * deltaX + deltaY * deltaY <= radius * radius)
           {
            //--- Compute the absolute pixel X coordinate
            int pixelX = centerX + deltaX;
            //--- Compute the absolute pixel Y coordinate
            int pixelY = centerY + deltaY;
            //--- Write the pixel only if it lies within the canvas bounds
            if(pixelX >= 0 && pixelX < canvas.Width() &&
               pixelY >= 0 && pixelY < canvas.Height())
              {
               canvas.PixelSet(pixelX, pixelY, argbColor);
              }
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Draw a solid filled ellipse at a given canvas position           |
//+------------------------------------------------------------------+
void DrawFilledEllipse(CCanvas &canvas, int centerX, int centerY, int radiusX, int radiusY, uint argbColor)
  {
   //--- Iterate over every row within the vertical extent of the ellipse
   for(int deltaY = -radiusY; deltaY <= radiusY; deltaY++)
     {
      //--- Iterate over every column within the horizontal extent
      for(int deltaX = -radiusX; deltaX <= radiusX; deltaX++)
        {
         //--- Compute the normalized distance squared from ellipse center
         double normalized = (double)(deltaX * deltaX) / (radiusX * radiusX) +
                             (double)(deltaY * deltaY) / (radiusY * radiusY);
         //--- Write the pixel only if it lies inside the ellipse boundary
         if(normalized <= 1.0)
           {
            //--- Compute the absolute pixel X coordinate
            int pixelX = centerX + deltaX;
            //--- Compute the absolute pixel Y coordinate
            int pixelY = centerY + deltaY;
            //--- Validate the pixel is within canvas bounds before writing
            if(pixelX >= 0 && pixelX < canvas.Width() &&
               pixelY >= 0 && pixelY < canvas.Height())
              {
               canvas.PixelSet(pixelX, pixelY, argbColor);
              }
           }
        }
     }
  }

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

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

Заполнение фигуры крыла вертикальными и радиальными градиентами

После того, как примитивы уже отрисованы, мы определим наиболее важную новую функцию в этом обновлении — «FillPolygon», которая принимает контур кривой-бабочки в виде многоугольника и заливает его цветом, используя подход растеризации сканирующей линии, поддерживая как вертикальный, так и радиальный режимы градиента.

//+------------------------------------------------------------------+
//| Fill a polygon with vertical or radial gradient using scanlines  |
//+------------------------------------------------------------------+
void FillPolygon(CCanvas &canvas, double &verticesX[], double &verticesY[],
                 color bottomColor, color middleColor, color topColor,
                 double fillOpacity, bool isRadial = false,
                 double centerX = 0, double centerY = 0, double maxDistance = 1)
  {
   //--- Get the number of polygon vertices
   int numVertices = ArraySize(verticesX);
   //--- Abort if the polygon has fewer than 3 vertices
   if(numVertices < 3) return;

   //--- Initialize vertical bounds to the first vertex Y
   double minY = verticesY[0], maxY = verticesY[0];
   //--- Scan all vertices to find the true vertical extents
   for(int i = 1; i < numVertices; i++)
     {
      //--- Update minimum Y if this vertex is lower
      if(verticesY[i] < minY) minY = verticesY[i];
      //--- Update maximum Y if this vertex is higher
      if(verticesY[i] > maxY) maxY = verticesY[i];
     }

   //--- Compute the first scanline row to process
   int yStart = (int)MathCeil(minY);
   //--- Compute the last scanline row to process
   int yEnd   = (int)MathFloor(maxY);

   //--- Convert fill opacity to an alpha byte value
   uchar alpha = (uchar)(255 * fillOpacity);

   //--- Declare arrays to hold X intersections and winding deltas per scanline
   double intersectionX[];
   int    edgeDeltas[];
   //--- Allocate intersection and delta arrays to hold up to numVertices entries
   ArrayResize(intersectionX, numVertices);
   ArrayResize(edgeDeltas,    numVertices);

   //--- Process each horizontal scanline between the polygon's vertical bounds
   for(int y = yStart; y <= yEnd; y++)
     {
      //--- Offset the scanline to the pixel center for sub-pixel accuracy
      double scanlineY = (double)y + 0.5;
      //--- Reset the intersection count for this scanline
      int intersectionCount = 0;

      //--- Test every polygon edge for intersection with the current scanline
      for(int i = 0; i < numVertices; i++)
        {
         //--- Compute the index of the next vertex (wrap around)
         int    nextIndex = (i + 1) % numVertices;
         //--- Get the X and Y of the current edge start vertex
         double x0 = verticesX[i],         y0 = verticesY[i];
         //--- Get the X and Y of the current edge end vertex
         double x1 = verticesX[nextIndex],  y1 = verticesY[nextIndex];

         //--- Determine the vertical extent of this edge
         double edgeMinY = MathMin(y0, y1);
         double edgeMaxY = MathMax(y0, y1);

         //--- Skip this edge if the scanline does not cross its vertical range
         if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue;
         //--- Skip horizontal edges to avoid division by zero
         if(MathAbs(y1 - y0) < 1e-12) continue;

         //--- Compute the linear interpolation factor along the edge
         double interpolationFactor = (scanlineY - y0) / (y1 - y0);
         //--- Skip if the factor falls outside the valid edge range
         if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue;

         //--- Compute and store the X coordinate of the intersection
         intersectionX[intersectionCount] = x0 + interpolationFactor * (x1 - x0);
         //--- Record the winding delta for this edge direction
         edgeDeltas[intersectionCount]    = (y1 > y0) ? -1 : 1;
         //--- Increment the intersection counter
         intersectionCount++;
        }

      //--- Skip scanlines with no intersections
      if(intersectionCount == 0) continue;

      //--- Sort intersections in ascending X order using bubble sort
      for(int a = 0; a < intersectionCount - 1; a++)
        {
         for(int b = a + 1; b < intersectionCount; b++)
           {
            //--- Swap if left intersection is greater than right
            if(intersectionX[a] > intersectionX[b])
              {
               //--- Swap X intersection values
               double tempX       = intersectionX[a];
               intersectionX[a]   = intersectionX[b];
               intersectionX[b]   = tempX;
               //--- Swap corresponding winding deltas
               int tempDelta    = edgeDeltas[a];
               edgeDeltas[a]    = edgeDeltas[b];
               edgeDeltas[b]    = tempDelta;
              }
           }
        }

      //--- Initialize the nonzero winding number accumulator
      int    winding   = 0;
      //--- Start just before the first intersection
      double previousX = intersectionX[0] - 1;

      //--- Walk through each intersection span and fill inside regions
      for(int k = 0; k < intersectionCount; k++)
        {
         //--- Compute the leftmost pixel column to fill in this span
         int xLeft  = (int)MathCeil(previousX);
         //--- Compute the rightmost pixel column to fill in this span
         int xRight = (int)MathFloor(intersectionX[k]);

         //--- Fill the span only when inside the polygon (nonzero winding)
         if(winding != 0 && xLeft <= xRight)
           {
            //--- Apply vertical gradient: color determined once per row
            if(!isRadial)
              {
               //--- Compute normalized vertical position within the polygon bounds
               double factor   = (scanlineY - minY) / (maxY - minY);
               //--- Declare the color for this scanline row
               color  rowColor;
               //--- Interpolate between bottom and middle colors for the lower half
               if(factor <= 0.5)
                 {
                  rowColor = InterpolateColors(bottomColor, middleColor, factor / 0.5);
                 }
               //--- Interpolate between middle and top colors for the upper half
               else
                 {
                  rowColor = InterpolateColors(middleColor, topColor, (factor - 0.5) / 0.5);
                 }
               //--- Convert the row color to ARGB with the fill alpha
               uint fillColor = ColorToARGB(rowColor, alpha);
               //--- Paint every pixel in the horizontal span with the row color
               for(int x = xLeft; x <= xRight; x++)
                 {
                  canvas.PixelSet(x, y, fillColor);
                 }
              }
            //--- Apply radial gradient: color determined per pixel by distance from center
            else
              {
               //--- Iterate over every pixel in the span individually
               for(int x = xLeft; x <= xRight; x++)
                 {
                  //--- Compute Euclidean distance from this pixel to the radial center
                  double distance = MathSqrt(MathPow(x - centerX, 2) + MathPow(y - centerY, 2));
                  //--- Normalize distance to a 0-1 factor using the max radius
                  double factor   = (maxDistance > 0) ? distance / maxDistance : 0;
                  //--- Declare the color for this pixel
                  color  rowColor;
                  //--- Interpolate between bottom and middle for the inner half
                  if(factor <= 0.5)
                    {
                     rowColor = InterpolateColors(bottomColor, middleColor, factor / 0.5);
                    }
                  //--- Interpolate between middle and top for the outer half
                  else
                    {
                     rowColor = InterpolateColors(middleColor, topColor, (factor - 0.5) / 0.5);
                    }
                  //--- Convert the pixel color to ARGB with the fill alpha
                  uint fillColor = ColorToARGB(rowColor, alpha);
                  //--- Write the gradient pixel
                  canvas.PixelSet(x, y, fillColor);
                 }
              }
           }
         //--- Accumulate the winding number using the edge direction delta
         winding   += edgeDeltas[k];
         //--- Advance the previous X to the current intersection boundary
         previousX  = intersectionX[k];
        }
     }
  }

Мы начинаем с чтения количества вершин из переданных массивов и прерываем работу, если присутствует менее трех вершин. Далее мы сканируем все координаты Y вершин, чтобы найти вертикальную протяженность многоугольника, вычисляя первую и последнюю строки сканирующей линии для обработки с помощью функций  MathCeil и MathFloor.  Прозрачность заливки предварительно преобразуется в байтовое значение альфа-канала, а для хранения до одной записи на вершину в каждой строке развертки выделяются массивы пересечений.

Для каждой горизонтальной строки развертки в пределах вертикальных границ мы смещаем ее на 0,5 пикселя в сторону центра пикселя для субпиксельной точности, а затем проходим по каждому ребру полигона, проверяя, пересекает ли его строка развертки. Горизонтальные ребра пропускаются, чтобы избежать деления на ноль. Для ребер, которые пересекаются, мы вычисляем коэффициент интерполяции вдоль ребра и определяем точную X-координату пересечения, сохраняя ее вместе с дельтой, которая указывает, движется ли ребро вверх или вниз. После того, как собраны все пересечения для строки развертки, мы сортируем их в порядке возрастания X с помощью пузырьковой сортировки, меняя местами значения пересечений и соответствующие им дельты обхода, чтобы сохранить их парными.

Затем мы проходим по отсортированным пересечениям, используя правило ненулевого обхода — накапливаем число обходов на каждой границе и заполняем горизонтальный участок между последовательными пересечениями только тогда, когда число обходов ненулевое, то есть когда мы находимся внутри многоугольника. Это корректно обрабатывает сложную самопересекающуюся форму кривой-бабочки, где контур пересекается сам с собой несколько раз, и наивное правило чётности/нечётности при заливке привело бы к некорректным результатам.

Внутри каждого залитого участка цвет определяется режимом градиента. Для вертикального градиента мы вычисляем нормализованный коэффициент из положения строки сканирования между минимальным и максимальным значениями многоугольника по оси Y. Далее используем функцию "InterpolateColors" для смешивания от нижнего цвета к среднему цвету в нижней половине и от среднего к верхнему цвету в верхней половине. Так создается плавный трехступенчатый градиент по всей высоте крыла, вычисляемый один раз для каждой строки и применяемый равномерно по всему диапазону. При этом для радиального градиента мы вычисляем цвет для каждого пикселя индивидуально — определяем евклидово расстояние от каждого пикселя до заданного центра с помощью функции MathSqrt, нормализуем его относительно максимального расстояния и применяем ту же трехступенчатую интерполяцию от центра — в результате чего получается светящийся узор, который исходит из начала координат крыла, а не распространяется сверху вниз. В обоих режимах конечный цвет преобразуется в "ARGB" с альфа-каналом прозрачности и записывается с помощью метода PixelSet.

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

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

SCANLINE ALGORITHM

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

Рисование жилок крыла и текстуры чешуек

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

//+------------------------------------------------------------------+
//| Draw anti-aliased wing vein lines radiating from the body center |
//+------------------------------------------------------------------+
void DrawWingVeins(CCanvas &canvas, double &xPoints[], double &yPoints[], int pointCount,
                   double rangeX, double rangeY, int plotWidth, int plotHeight)
  {
   //--- Skip drawing if wing veins have been disabled by the user
   if(!showButterflyWingVeins) return;

   //--- Map the world-space body center X to a pixel column
   int centerX = (int)((0 - butterflyMinX) / rangeX * plotWidth);
   //--- Map the world-space body center Y to a pixel row (inverted axis)
   int centerY = (int)((butterflyMaxY - 0) / rangeY * plotHeight);

   //--- Set vein color as a darkened body color with wing opacity applied
   uint argbVein = ColorToARGB(DarkenColor(butterflyBodyColor, 0.2), (uchar)(150 * butterflyWingOpacity));

   //--- Sample wing edge points at regular intervals to define vein endpoints
   for(int i = 0; i < pointCount; i += 50)
     {
      //--- Map this wing edge point X to a pixel column
      int pixelX = (int)((xPoints[i] - butterflyMinX) / rangeX * plotWidth);
      //--- Map this wing edge point Y to a pixel row (inverted axis)
      int pixelY = (int)((butterflyMaxY - yPoints[i]) / rangeY * plotHeight);

      //--- Draw an anti-aliased vein line from the body center to the wing edge
      canvas.LineAA(centerX, centerY, pixelX, pixelY, argbVein);
     }
  }

//+------------------------------------------------------------------+
//| Draw wing scale texture dots along and inside the wing boundary  |
//+------------------------------------------------------------------+
void DrawWingScales(CCanvas &canvas, double &xCoordinates[], double &yCoordinates[],
                    int pointCount, double rangeX, double rangeY,
                    int plotWidth, int plotHeight,
                    double centerX, double centerY, double maxDistance)
  {
   //--- Skip drawing if wing scales have been disabled by the user
   if(!showButterflyWingScales) return;

   //--- Sample wing edge points at a dense interval for scale placement
   for(int i = 0; i < pointCount; i += 4)
     {
      //--- Map the sampled wing edge point X to a pixel column
      double pixelX = (xCoordinates[i] - butterflyMinX) / rangeX * plotWidth;
      //--- Map the sampled wing edge point Y to a pixel row (inverted axis)
      double pixelY = (butterflyMaxY - yCoordinates[i]) / rangeY * plotHeight;

      //--- Reconstruct the approximate T parameter for this point index
      double tParameter = butterflyTStart + (double)i * butterflyTStep;
      //--- Resolve the wing segment color for this T value
      color baseColor   = GetWingColorForT(tParameter);

      //--- Compute the radial distance of this scale from the wing center
      double distance = MathSqrt(MathPow(pixelX - centerX, 2) + MathPow(pixelY - centerY, 2));
      //--- Normalize distance to a 0-1 blend factor
      double factor   = distance / maxDistance;
      //--- Blend the base color slightly toward white for a shimmering edge effect
      color scaleColor = InterpolateColors(baseColor, LightenColor(baseColor, 0.2), factor);

      //--- Convert scale color to ARGB with wing opacity applied
      uint argbScale = ColorToARGB(scaleColor, (uchar)(180 * butterflyWingOpacity));

      //--- Vary scale dot radius slightly based on point index for organic texture
      int radius = 2 + (i % 3);
      //--- Draw the primary scale dot on the wing boundary
      DrawFilledCircle(canvas, (int)pixelX, (int)pixelY, radius, argbScale);

      //--- Compute a vector from the wing center to this scale point
      double deltaX = pixelX - centerX;
      double deltaY = pixelY - centerY;
      //--- Compute the vector magnitude for normalization
      double norm = MathSqrt(deltaX * deltaX + deltaY * deltaY);
      //--- Add a second inward-offset scale dot only if the vector is non-degenerate
      if(norm > 0)
        {
         //--- Normalize the X component of the inward direction
         deltaX /= norm;
         //--- Normalize the Y component of the inward direction
         deltaY /= norm;
         //--- Compute the inward-offset X position for the secondary scale
         double inwardPixelX = pixelX - deltaX * (radius * 2);
         //--- Compute the inward-offset Y position for the secondary scale
         double inwardPixelY = pixelY - deltaY * (radius * 2);
         //--- Draw the secondary inward scale dot slightly smaller for depth
         DrawFilledCircle(canvas, (int)inwardPixelX, (int)inwardPixelY, radius - 1, argbScale);
        }
     }
  }

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

Функция "DrawWingScales" берет точки границы гораздо плотнее — через каждую четвертую точку — чтобы разместить плотный узор из точек, имитирующих чешуйки, по всей кромке крыла. Для каждой измеренной позиции мы отображаем ее координаты в пиксельное пространство, восстанавливаем приблизительное параметрическое значение t из индекса точки и передаем его функции "GetWingColorForT" для получения корректного цвета сегмента. Затем мы вычисляем радиальное расстояние этого пикселя от центра крыла с помощью функции MathSqrt, нормализуем его относительно максимального расстояния и используем "InterpolateColors", чтобы слегка смешать базовый цвет с более светлым вариантом самого себя, создавая едва заметное мерцание, которое становится ярче по краям крыла. Радиус каждой чешуйчатой точки слегка изменяется с помощью деления по модулю индекса точки, чтобы создать органическую неровность, а не однородную текстуру.

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

Построение тела бабочки от груди до кончиков усиков

После завершения прорисовки слоев крыльев, мы переходим к изображению анатомической части тела, расположенной в центре композиции. Функция "DrawButterflyBody" создает полную структуру бабочки, начиная от кончика брюшка и заканчивая грудью, головой, глазами и, наконец, двумя изогнутыми усиками.

//+------------------------------------------------------------------+
//| Draw segmented abdomen, thorax, head, eyes, and antennae         |
//+------------------------------------------------------------------+
void DrawButterflyBody(CCanvas &canvas, double centerX, double centerY,
                       double rangeX, double rangeY, int plotWidth, int plotHeight)
  {
   //--- Map the body center X world coordinate to a pixel column
   int centerPixelX = (int)((centerX - butterflyMinX) / rangeX * plotWidth);
   //--- Map the body center Y world coordinate to a pixel row (inverted axis)
   int centerPixelY = (int)((butterflyMaxY - centerY) / rangeY * plotHeight);

   //--- Apply a vertical pixel offset to shift the body upward on the canvas
   int bodyPixelYOffset = -50;
   //--- Adjust the center pixel Y by the vertical offset
   centerPixelY += bodyPixelYOffset;

   //--- Compute the abdomen length as a proportion of plot height
   int abdomenLength  = (int)(plotHeight * 0.20);
   //--- Compute the abdomen width as a proportion of plot width
   int abdomenWidth   = (int)(plotWidth  * 0.02);
   //--- Compute the thorax radius as a proportion of plot width
   int thoraxRadius   = (int)(plotWidth  * 0.022);
   //--- Compute the head radius as a proportion of plot width (larger than thorax)
   int headRadius     = (int)(plotWidth  * 0.035);
   //--- Compute the eye radius as a proportion of plot width
   int eyeRadius      = (int)(plotWidth  * 0.015);
   //--- Compute the antenna length as a proportion of plot width
   int antennaLength  = (int)(plotWidth  * 0.10);

   //--- Convert body color to fully opaque ARGB
   uint argbBody          = ColorToARGB(butterflyBodyColor, 255);
   //--- Convert eye color to fully opaque ARGB
   uint argbEye           = ColorToARGB(butterflyEyeColor,  255);
   //--- Convert antenna color to fully opaque ARGB
   uint argbAntenna       = ColorToARGB(butterflyAntennaColor, 255);
   //--- Compute a lightened highlight color for the head and convert to ARGB
   uint argbBodyHighlight = ColorToARGB(LightenColor(butterflyBodyColor, 0.3), 255);

   //--- Compute the vertical center of the thorax oval
   int thoraxYPosition = centerPixelY - (int)(0.5 * thoraxRadius);
   //--- Draw the thorax as a vertically stretched filled ellipse
   DrawFilledEllipse(canvas, centerPixelX, thoraxYPosition,
                     thoraxRadius, thoraxRadius * 3 / 2, argbBody);

   //--- Set the number of abdomen segments for a detailed segmented look
   int segmentCount   = 10;
   //--- Compute the Y pixel where the abdomen begins (below the thorax)
   int abdomenStartY  = thoraxYPosition + (thoraxRadius * 3 / 2) - 10;

   //--- Draw each abdomen segment as a tapered ellipse
   for(int i = 0; i < segmentCount; i++)
     {
      //--- Compute the normalized progress along the abdomen (0=top, 1=tip)
      double segmentProgress = (double)i / (segmentCount - 1);
      //--- Compute the Y pixel position for this segment
      int segmentY           = abdomenStartY + (int)(segmentProgress * abdomenLength);
      //--- Taper the segment width toward the abdomen tip
      int segmentWidth       = (int)(abdomenWidth * (1.0 - segmentProgress * 0.5));

      //--- Draw this abdomen segment as a small ellipse
      DrawFilledEllipse(canvas, centerPixelX, segmentY,
                        segmentWidth, abdomenWidth / 2, argbBody);

      //--- Draw a horizontal segment divider line for interior segments
      if(i > 0 && i < segmentCount - 1)
        {
         canvas.LineHorizontal(centerPixelX - segmentWidth, centerPixelX + segmentWidth,
                               segmentY,
                               ColorToARGB(DarkenColor(butterflyBodyColor, 0.3), 255));
        }
     }

   //--- Compute the Y pixel position for the head (above the thorax)
   int headYPosition = thoraxYPosition - (int)(1.8 * headRadius);
   //--- Draw the main head circle
   DrawFilledCircle(canvas, centerPixelX, headYPosition, headRadius, argbBody);
   //--- Draw a small highlight circle on the head for a rounded appearance
   DrawFilledCircle(canvas,
                    centerPixelX - headRadius / 3,
                    headYPosition - headRadius / 3,
                    headRadius / 4, argbBodyHighlight);

   //--- Compute the horizontal offset for positioning each compound eye
   int eyeOffsetX = headRadius * 3 / 4;
   //--- Draw the left compound eye
   DrawFilledCircle(canvas, centerPixelX - eyeOffsetX, headYPosition, eyeRadius, argbEye);
   //--- Draw the right compound eye
   DrawFilledCircle(canvas, centerPixelX + eyeOffsetX, headYPosition, eyeRadius, argbEye);

   //--- Set eye shine color as semi-transparent white
   uint argbShine = ColorToARGB(clrWhite, 200);
   //--- Draw the shine highlight on the left eye
   DrawFilledCircle(canvas,
                    centerPixelX - eyeOffsetX + eyeRadius / 3,
                    headYPosition - eyeRadius / 3,
                    eyeRadius / 3, argbShine);
   //--- Draw the shine highlight on the right eye
   DrawFilledCircle(canvas,
                    centerPixelX + eyeOffsetX + eyeRadius / 3,
                    headYPosition - eyeRadius / 3,
                    eyeRadius / 3, argbShine);

   //--- Compute the Y pixel where both antennae originate from the head
   int antennaStartY = headYPosition - headRadius / 2;

   //--- Draw the left antenna as a series of overlapping filled circles
   for(int i = 0; i <= 27; i++)
     {
      //--- Compute normalized progress along the antenna shaft
      double t       = (double)i / 27.0;
      //--- Compute the X position curving outward to the left
      int antennaX   = centerPixelX - headRadius / 2 - (int)(t * antennaLength * 0.4);
      //--- Compute the Y position curving upward with a sine arc
      int antennaY   = antennaStartY - (int)(t * antennaLength * MathSin(t * M_PI * 0.5));
      //--- Vary thickness slightly near the club tip
      int thickness  = (i < 18) ? 5 : 6;
      //--- Draw a filled circle at this antenna shaft point
      DrawFilledCircle(canvas, antennaX, antennaY, thickness, argbAntenna);
     }

   //--- Draw the right antenna as a series of overlapping filled circles
   for(int i = 0; i <= 27; i++)
     {
      //--- Compute normalized progress along the antenna shaft
      double t       = (double)i / 27.0;
      //--- Compute the X position curving outward to the right
      int antennaX   = centerPixelX + headRadius / 2 + (int)(t * antennaLength * 0.4);
      //--- Compute the Y position curving upward with a sine arc
      int antennaY   = antennaStartY - (int)(t * antennaLength * MathSin(t * M_PI * 0.5));
      //--- Vary thickness slightly near the club tip
      int thickness  = (i < 18) ? 5 : 6;
      //--- Draw a filled circle at this antenna shaft point
      DrawFilledCircle(canvas, antennaX, antennaY, thickness, argbAntenna);
     }

   //--- Compute the X and Y position of the left antenna club
   int clubXLeft  = centerPixelX - headRadius / 2 - (int)(antennaLength * 0.4);
   int clubYLeft  = antennaStartY - antennaLength;
   //--- Draw the left antenna club as a larger filled circle
   DrawFilledCircle(canvas, clubXLeft, clubYLeft, thoraxRadius / 3 + 1, argbAntenna);

   //--- Compute the X and Y position of the right antenna club
   int clubXRight = centerPixelX + headRadius / 2 + (int)(antennaLength * 0.4);
   int clubYRight = antennaStartY - antennaLength;
   //--- Draw the right antenna club as a larger filled circle
   DrawFilledCircle(canvas, clubXRight, clubYRight, thoraxRadius / 3 + 1, argbAntenna);
  }

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

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

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

Оба усика состоят из 28 перекрывающихся залитых кругов каждый, выступающих наружу и вверх вдоль синусоидальной дуги — левый усик изгибается влево, а правый симметрично его зеркалит. Горизонтальное положение линейно смещается наружу, в то время как вертикальное положение поднимается в соответствии с функцией MathSin, примененной к дуге в четверть числа Пи, создавая естественную, плавную восходящую кривую. Толщина круга незначительно увеличивается на последних десяти шагах, что указывает на его утолщение в направлении к булаве. После завершения петель стержня, в вычисленном положении кончика каждого усика помещается один больший залитый круг, образующий характерный булавовидный кончик усиков, которым заканчиваются реальные усики бабочек. Теперь мы объединим все это в функции финальной отрисовки.

Замена рендерера только кривых на полноценный реалистичный конвейер отрисовки бабочки

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

//+------------------------------------------------------------------+
//| Render filled, outlined, and detailed realistic butterfly        |
//+------------------------------------------------------------------+
void DrawRealisticButterfly(CCanvas &canvas, int plotWidth, int plotHeight,
                             double rangeX, double rangeY)
  {
   //--- Declare arrays to store X world coordinates of all curve points
   double xPoints[];
   //--- Declare arrays to store Y world coordinates of all curve points
   double yPoints[];
   //--- Declare array to store the T parameter value for each curve point
   double tValues[];
   //--- Initialize the point counter before collecting curve data
   int pointCount = 0;

   //--- Estimate the maximum number of points to pre-allocate arrays
   int estimatedPoints = (int)((butterflyTEnd - butterflyTStart) / butterflyTStep) + 1;
   //--- Pre-allocate the X coordinate array
   ArrayResize(xPoints, estimatedPoints);
   //--- Pre-allocate the Y coordinate array
   ArrayResize(yPoints, estimatedPoints);
   //--- Pre-allocate the T parameter array
   ArrayResize(tValues, estimatedPoints);

   //--- Traverse the full parametric domain to collect valid curve points
   for(double t = butterflyTStart; t <= butterflyTEnd; t += butterflyTStep)
     {
      //--- Evaluate the butterfly radial term at this T
      double term = MathExp(MathCos(t)) - 2 * MathCos(4 * t) - MathPow(MathSin(t / 12), 5);
      //--- Compute the X world coordinate
      double x    = MathSin(t) * term;
      //--- Compute the Y world coordinate
      double y    = MathCos(t) * term;

      //--- Store the point only if both coordinates are finite
      if(MathIsValidNumber(x) && MathIsValidNumber(y))
        {
         //--- Store the X world coordinate
         xPoints[pointCount] = x;
         //--- Store the Y world coordinate
         yPoints[pointCount] = y;
         //--- Store the T parameter value
         tValues[pointCount] = t;
         //--- Increment the valid point count
         pointCount++;
        }
     }

   //--- Trim X array to the exact number of valid points collected
   ArrayResize(xPoints, pointCount);
   //--- Trim Y array to the exact number of valid points collected
   ArrayResize(yPoints, pointCount);
   //--- Trim T array to the exact number of valid points collected
   ArrayResize(tValues, pointCount);

   //--- Declare arrays for pixel-space X and Y coordinates
   double xPixels[], yPixels[];
   //--- Allocate pixel X array
   ArrayResize(xPixels, pointCount);
   //--- Allocate pixel Y array
   ArrayResize(yPixels, pointCount);

   //--- Initialize the maximum radial distance from center
   double maxDistance = 0;
   //--- Compute the pixel-space X coordinate of the wing center (world origin)
   double centerX = (0 - butterflyMinX) / rangeX * plotWidth;
   //--- Compute the pixel-space Y coordinate of the wing center (inverted axis)
   double centerY = (butterflyMaxY - 0) / rangeY * plotHeight;

   //--- Convert all world-space points to pixel-space and track max distance
   for(int i = 0; i < pointCount; i++)
     {
      //--- Map X world coordinate to pixel column
      xPixels[i] = (xPoints[i] - butterflyMinX) / rangeX * plotWidth;
      //--- Map Y world coordinate to pixel row (inverted axis)
      yPixels[i] = (butterflyMaxY - yPoints[i]) / rangeY * plotHeight;
      //--- Compute distance of this pixel point from the wing center
      double distance = MathSqrt(MathPow(xPixels[i] - centerX, 2) +
                                 MathPow(yPixels[i] - centerY, 2));
      //--- Update the maximum distance for gradient normalization
      maxDistance = MathMax(maxDistance, distance);
     }

   //--- Draw all fill layers if the butterfly fill feature is enabled
   if(enableButterflyFill)
     {
      //--- Fill the outermost wing shape with a vertical three-color gradient
      FillPolygon(canvas, xPixels, yPixels,
                  fillBottomColor, fillMiddleColor, fillTopColor,
                  butterflyFillOpacity, false);

      //--- Draw the first inner fill layer if enabled
      if(enableInnerButterflyFill)
        {
         //--- Declare scaled pixel arrays for the inner butterfly outline
         double innerX[], innerY[];
         //--- Allocate inner X array
         ArrayResize(innerX, pointCount);
         //--- Allocate inner Y array
         ArrayResize(innerY, pointCount);

         //--- Scale each point toward the wing center by the inner scale factor
         for(int i = 0; i < pointCount; i++)
           {
            //--- Compute the X displacement from the center
            double deltaX = xPixels[i] - centerX;
            //--- Compute the Y displacement from the center
            double deltaY = yPixels[i] - centerY;
            //--- Apply the inner scale factor to X
            innerX[i] = centerX + innerButterflyScale * deltaX;
            //--- Apply the inner scale factor to Y
            innerY[i] = centerY + innerButterflyScale * deltaY;
           }
         //--- Fill the inner wing shape with a vertical three-color gradient
         FillPolygon(canvas, innerX, innerY,
                     innerFillBottomColor, innerFillMiddleColor, innerFillTopColor,
                     innerButterflyFillOpacity, false);

         //--- Draw the second inner fill layer if enabled
         if(enableSecondInnerButterflyFill)
           {
            //--- Declare scaled pixel arrays for the second inner butterfly outline
            double secondInnerX[], secondInnerY[];
            //--- Allocate second inner X array
            ArrayResize(secondInnerX, pointCount);
            //--- Allocate second inner Y array
            ArrayResize(secondInnerY, pointCount);
            //--- Initialize the max distance for the second inner radial gradient
            double secondMaxDistance = 0;

            //--- Scale each point toward the wing center by the second inner scale
            for(int i = 0; i < pointCount; i++)
              {
               //--- Compute X displacement from the center
               double deltaX    = xPixels[i] - centerX;
               //--- Compute Y displacement from the center
               double deltaY    = yPixels[i] - centerY;
               //--- Apply the second inner scale factor to X
               secondInnerX[i] = centerX + secondInnerButterflyScale * deltaX;
               //--- Apply the second inner scale factor to Y
               secondInnerY[i] = centerY + secondInnerButterflyScale * deltaY;
               //--- Compute distance from center for this scaled point
               double distance  = MathSqrt(MathPow(secondInnerX[i] - centerX, 2) +
                                           MathPow(secondInnerY[i] - centerY, 2));
               //--- Update the second inner max distance
               secondMaxDistance = MathMax(secondMaxDistance, distance);
              }
            //--- Fill the second inner wing shape with a radial three-color gradient
            FillPolygon(canvas, secondInnerX, secondInnerY,
                        secondInnerFillBottomColor, secondInnerFillMiddleColor, secondInnerFillTopColor,
                        secondInnerButterflyFillOpacity, true, centerX, centerY, secondMaxDistance);
           }
        }
     }

   //--- Define the T boundary separating segment 1 from segment 2
   double segmentEnd1 = 3.0 * M_PI;
   //--- Define the T boundary separating segment 2 from segment 3
   double segmentEnd2 = 6.0 * M_PI;
   //--- Define the T boundary separating segment 3 from segment 4
   double segmentEnd3 = 9.0 * M_PI;
   //--- Define the T end of the final segment
   double segmentEnd4 = butterflyTEnd;

   //--- Convert blue curve color to fully opaque ARGB
   uint argbBlue   = ColorToARGB(blueCurveColor,   255);
   //--- Convert red curve color to fully opaque ARGB
   uint argbRed    = ColorToARGB(redCurveColor,    255);
   //--- Convert orange curve color to fully opaque ARGB
   uint argbOrange = ColorToARGB(orangeCurveColor, 255);
   //--- Convert green curve color to fully opaque ARGB
   uint argbGreen  = ColorToARGB(greenCurveColor,  255);

   //--- Initialize previous pixel coordinates for blue segment connectivity
   double previousCurveXPixel = -1;
   double previousCurveYPixel = -1;

   //--- Draw the first wing outline segment (blue)
   for(double t = butterflyTStart; t <= segmentEnd1; t += butterflyTStep)
     {
      //--- Evaluate the butterfly radial term
      double term = MathExp(MathCos(t)) - 2 * MathCos(4 * t) - MathPow(MathSin(t / 12), 5);
      //--- Compute X world coordinate
      double x = MathSin(t) * term;
      //--- Compute Y world coordinate
      double y = MathCos(t) * term;
      //--- Process only finite coordinate pairs
      if(MathIsValidNumber(x) && MathIsValidNumber(y))
        {
         //--- Map X to pixel column
         double currentCurveXPixel = (x - butterflyMinX) / rangeX * plotWidth;
         //--- Map Y to pixel row (inverted axis)
         double currentCurveYPixel = (butterflyMaxY - y) / rangeY * plotHeight;
         //--- Round pixel X to nearest integer
         int intX = (int)MathRound(currentCurveXPixel);
         //--- Round pixel Y to nearest integer
         int intY = (int)MathRound(currentCurveYPixel);
         //--- Draw the segment only if a previous valid point exists
         if(previousCurveXPixel >= 0 && previousCurveYPixel >= 0)
           {
            //--- Draw anti-aliased primary outline line
            canvas.LineAA((int)MathRound(previousCurveXPixel),
                          (int)MathRound(previousCurveYPixel), intX, intY, argbBlue);
            //--- Draw anti-aliased offset line for additional thickness
            canvas.LineAA((int)MathRound(previousCurveXPixel) + 1,
                          (int)MathRound(previousCurveYPixel), intX + 1, intY, argbBlue);
           }
         //--- Store current pixel X for next iteration
         previousCurveXPixel = currentCurveXPixel;
         //--- Store current pixel Y for next iteration
         previousCurveYPixel = currentCurveYPixel;
        }
      else
        {
         //--- Reset previous X on an invalid point (curve break)
         previousCurveXPixel = -1;
         //--- Reset previous Y on an invalid point (curve break)
         previousCurveYPixel = -1;
        }
     }

   //--- Reset previous pixel coordinates for red segment connectivity
   previousCurveXPixel = -1;
   previousCurveYPixel = -1;

   //--- Draw the second wing outline segment (red)
   for(double t = segmentEnd1; t <= segmentEnd2; t += butterflyTStep)
     {
      //--- Evaluate the butterfly radial term
      double term = MathExp(MathCos(t)) - 2 * MathCos(4 * t) - MathPow(MathSin(t / 12), 5);
      //--- Compute X world coordinate
      double x = MathSin(t) * term;
      //--- Compute Y world coordinate
      double y = MathCos(t) * term;
      //--- Process only finite coordinate pairs
      if(MathIsValidNumber(x) && MathIsValidNumber(y))
        {
         //--- Map X to pixel column
         double currentCurveXPixel = (x - butterflyMinX) / rangeX * plotWidth;
         //--- Map Y to pixel row (inverted axis)
         double currentCurveYPixel = (butterflyMaxY - y) / rangeY * plotHeight;
         //--- Round pixel X to nearest integer
         int intX = (int)MathRound(currentCurveXPixel);
         //--- Round pixel Y to nearest integer
         int intY = (int)MathRound(currentCurveYPixel);
         //--- Draw the segment only if a previous valid point exists
         if(previousCurveXPixel >= 0 && previousCurveYPixel >= 0)
           {
            //--- Draw anti-aliased primary outline line
            canvas.LineAA((int)MathRound(previousCurveXPixel),
                          (int)MathRound(previousCurveYPixel), intX, intY, argbRed);
            //--- Draw anti-aliased offset line for additional thickness
            canvas.LineAA((int)MathRound(previousCurveXPixel) + 1,
                          (int)MathRound(previousCurveYPixel), intX + 1, intY, argbRed);
           }
         //--- Store current pixel X for next iteration
         previousCurveXPixel = currentCurveXPixel;
         //--- Store current pixel Y for next iteration
         previousCurveYPixel = currentCurveYPixel;
        }
      else
        {
         //--- Reset previous X on an invalid point (curve break)
         previousCurveXPixel = -1;
         //--- Reset previous Y on an invalid point (curve break)
         previousCurveYPixel = -1;
        }
     }

   //--- Reset previous pixel coordinates for orange segment connectivity
   previousCurveXPixel = -1;
   previousCurveYPixel = -1;

   //--- Draw the third wing outline segment (orange)
   for(double t = segmentEnd2; t <= segmentEnd3; t += butterflyTStep)
     {
      //--- Evaluate the butterfly radial term
      double term = MathExp(MathCos(t)) - 2 * MathCos(4 * t) - MathPow(MathSin(t / 12), 5);
      //--- Compute X world coordinate
      double x = MathSin(t) * term;
      //--- Compute Y world coordinate
      double y = MathCos(t) * term;
      //--- Process only finite coordinate pairs
      if(MathIsValidNumber(x) && MathIsValidNumber(y))
        {
         //--- Map X to pixel column
         double currentCurveXPixel = (x - butterflyMinX) / rangeX * plotWidth;
         //--- Map Y to pixel row (inverted axis)
         double currentCurveYPixel = (butterflyMaxY - y) / rangeY * plotHeight;
         //--- Round pixel X to nearest integer
         int intX = (int)MathRound(currentCurveXPixel);
         //--- Round pixel Y to nearest integer
         int intY = (int)MathRound(currentCurveYPixel);
         //--- Draw the segment only if a previous valid point exists
         if(previousCurveXPixel >= 0 && previousCurveYPixel >= 0)
           {
            //--- Draw anti-aliased primary outline line
            canvas.LineAA((int)MathRound(previousCurveXPixel),
                          (int)MathRound(previousCurveYPixel), intX, intY, argbOrange);
            //--- Draw anti-aliased offset line for additional thickness
            canvas.LineAA((int)MathRound(previousCurveXPixel) + 1,
                          (int)MathRound(previousCurveYPixel), intX + 1, intY, argbOrange);
           }
         //--- Store current pixel X for next iteration
         previousCurveXPixel = currentCurveXPixel;
         //--- Store current pixel Y for next iteration
         previousCurveYPixel = currentCurveYPixel;
        }
      else
        {
         //--- Reset previous X on an invalid point (curve break)
         previousCurveXPixel = -1;
         //--- Reset previous Y on an invalid point (curve break)
         previousCurveYPixel = -1;
        }
     }

   //--- Reset previous pixel coordinates for green segment connectivity
   previousCurveXPixel = -1;
   previousCurveYPixel = -1;

   //--- Draw the fourth wing outline segment (green)
   for(double t = segmentEnd3; t <= segmentEnd4; t += butterflyTStep)
     {
      //--- Evaluate the butterfly radial term
      double term = MathExp(MathCos(t)) - 2 * MathCos(4 * t) - MathPow(MathSin(t / 12), 5);
      //--- Compute X world coordinate
      double x = MathSin(t) * term;
      //--- Compute Y world coordinate
      double y = MathCos(t) * term;
      //--- Process only finite coordinate pairs
      if(MathIsValidNumber(x) && MathIsValidNumber(y))
        {
         //--- Map X to pixel column
         double currentCurveXPixel = (x - butterflyMinX) / rangeX * plotWidth;
         //--- Map Y to pixel row (inverted axis)
         double currentCurveYPixel = (butterflyMaxY - y) / rangeY * plotHeight;
         //--- Round pixel X to nearest integer
         int intX = (int)MathRound(currentCurveXPixel);
         //--- Round pixel Y to nearest integer
         int intY = (int)MathRound(currentCurveYPixel);
         //--- Draw the segment only if a previous valid point exists
         if(previousCurveXPixel >= 0 && previousCurveYPixel >= 0)
           {
            //--- Draw anti-aliased primary outline line
            canvas.LineAA((int)MathRound(previousCurveXPixel),
                          (int)MathRound(previousCurveYPixel), intX, intY, argbGreen);
            //--- Draw anti-aliased offset line for additional thickness
            canvas.LineAA((int)MathRound(previousCurveXPixel) + 1,
                          (int)MathRound(previousCurveYPixel), intX + 1, intY, argbGreen);
           }
         //--- Store current pixel X for next iteration
         previousCurveXPixel = currentCurveXPixel;
         //--- Store current pixel Y for next iteration
         previousCurveYPixel = currentCurveYPixel;
        }
      else
        {
         //--- Reset previous X on an invalid point (curve break)
         previousCurveXPixel = -1;
         //--- Reset previous Y on an invalid point (curve break)
         previousCurveYPixel = -1;
        }
     }

   //--- Draw wing detail layers only when fill is enabled
   if(enableButterflyFill)
     {
      //--- Draw vein lines radiating from the body center across the wings
      DrawWingVeins(canvas, xPoints, yPoints, pointCount, rangeX, rangeY, plotWidth, plotHeight);
      //--- Draw scale texture dots along the wing boundary
      DrawWingScales(canvas, xPoints, yPoints, pointCount, rangeX, rangeY,
                     plotWidth, plotHeight, centerX, centerY, maxDistance);
      //--- Draw the body components over the wing fill layers
      DrawButterflyBody(canvas, 0, 0, rangeX, rangeY, plotWidth, plotHeight);
     }
  }

Первое существенное изменение по сравнению со старым подходом заключается в том, что мы больше не вычисляем уравнение бабочки на лету во время рисования. Вместо этого мы начинаем с предварительного выделения трех массивов координат для значений X, Y и T в мировом пространстве, один раз проходим по всей параметрической области, чтобы собрать все допустимые точки в эти массивы, а затем обрезаем их до точного количества допустимых точек с помощью функции ArrayResize. Этот предварительный сбор необходим, поскольку функциям заливки, прожилок и чешуек требуется одновременный доступ ко всему набору точек кривой, тогда как старой функции требовалась только одна точка за раз.

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

После подготовки всех координатных данных, слои заливки отрисовываются первыми, если функция заливки включена. Мы вызываем "FillPolygon" для всего контура в пиксельном пространстве, используя цвета самого внешнего вертикального градиента, затем условно создаём первый внутренний контур, масштабируя смещение каждого пикселя от центра на внутренний коэффициент масштабирования и заполняя его собственным вертикальным градиентом, а затем второй, ещё меньший внутренний контур, масштабированный ещё больше внутрь и заполненный радиальным градиентом — передавая отдельно отслеживаемое максимальное расстояние для этого масштабированного контура в качестве эталона нормализации. Именно этот трёхслойный стек заливки придаёт крыльям глубину и светящееся ядро.

После заливки четыре цветных сегмента контура рисуются поверх них точно так же, как и в предыдущей части — пошагово вычисляя уравнение бабочки на каждой границе 3π и соединяя точки парными вызовами LineAA для толщины. Рисование контуров после заливки гарантирует, что цветные граничные линии аккуратно располагаются поверх градиентной внутренней области, а не скрываются под ней.

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

// In DrawButterflyPlot(), replace the existing function
//---
// DrawButterflyCurves(plotHighResolutionCanvas, highResolutionWidth, highResolutionHeight, rangeX, rangeY);

// New call: 

DrawRealisticButterfly(plotHighResolutionCanvas, highResolutionWidth, highResolutionHeight, rangeX, rangeY);

После компиляции получаем следующий результат.

FINAL OUTCOME

На скриншоте показано, что реалистичное рисование завершено. Осталось только протестировать программу, и это будет рассмотрено в следующем разделе.


Визуализация

Мы скомпилировали программу и подключили ее к графику MetaTrader 5, чтобы проверить результат всего рендеринга. Ниже показан итоговый результат в одном изображении.

BUTTERFLY CURVE PART 2 BACKTEST

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


Заключение

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

  • Заполнять цветом любую параметрическую кривую на холсте MQL5, используя растеризацию многоугольников с помощью как вертикальных и радиальных трехцветных градиентов
  • Создавать многослойные заливки крыльев, масштабируя контур кривой внутрь к его центру и применяя постепенно разные наборы цветов на каждом уровне глубины
  • Добавлять текстуру поверхности и анатомическую структуру к рисункам на холсте, используя примитивы круга и эллипса с заливкой, жилки крыла и узоры из точек, имитирующих чешуйки

В следующей части мы добавим четырехэтапную систему анимации. Она будет рисовать контур, плавно добавлять заливку, раскрывать детали поверхности, а затем переходить в непрерывный полет (махание крыльями, покачивание, раскачивание, наклон, неоновое свечение и циклическое изменение цвета).

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

Прикрепленные файлы |
От начального до среднего уровня: События в объектах (IV) От начального до среднего уровня: События в объектах (IV)
В этой статье мы завершим то, что было начато в предыдущей. То есть, полностью интерактивный способ изменения размеров объектов непосредственно на графике. Хотя многие представляют, что для создания подобного потребуется гораздо более глубокое знание MQL5, вы заметите, что с помощью простых концепций и базовых знаний мы можем реализовать способ работы с объектами непосредственно на графике. Это дает очень интересный и увлекательный результат.
Почему MetaTrader 5 подходит для AI-торговли: MQL5 + Python + ONNX + AI Assistant как экосистема алготрейдинга Почему MetaTrader 5 подходит для AI-торговли: MQL5 + Python + ONNX + AI Assistant как экосистема алготрейдинга
MetaTrader 5 подходит для AI-торговли, потому что объединяет рыночные данные, MQL5-разработку, Python-исследования, ONNX-модели, Strategy Tester, VPS и экосистему MQL5.community в одном рабочем процессе. Статья показывает практический путь от AI-подсказки на графике к структурированному сигналу, работе с кодом через AI Assistant в MetaEditor, модели качества, созданию советнику, тестированию и контролируемому запуску торговой системы.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Разработка инструментария для анализа Price Action (Часть 63): Автоматизация обнаружения восходящих и нисходящих клиньев на MQL5 Разработка инструментария для анализа Price Action (Часть 63): Автоматизация обнаружения восходящих и нисходящих клиньев на MQL5
В этой части серии "Разработка инструментария для анализа Price Action" мы разрабатываем индикатор на языке MQL5, который в реальном времени автоматически обнаруживает паттерны восходящего и нисходящего клина. Система подтверждает структуру опорных точек, математически проверяет сходимость границ, предотвращает перекрытие формаций и отслеживает условия пробоя и слома паттерна с точной визуальной индикацией. Построенная на чистой объектно-ориентированной архитектуре, эта реализация превращает субъективное распознавание клина в структурированный компонент анализа, учитывающий состояние паттерна, предназначенный для более дисциплинированного анализа Price Action.