Торговые инструменты MQL5 (Часть 28): Полигональная заливка кривой-бабочки в MQL5
Введение
У вас уже есть параметрическая кривая-бабочка, аккуратно отрисованная на холсте MetaTrader 5. Четыре цветных сегмента обрисовывают математический контур плавными, сглаженными штрихами. Однако крылья пусты, нет тела, нет текстуры — ничто не делает её чем-то большим, чем сухая математическая схема. Эта статья предназначена для разработчиков MetaQuotes Language 5 (MQL5) и креативных программистов, которые хотят выйти за рамки контура и заполнить бабочку многослойной цветовой гаммой, реалистичной детализацией крыльев и полной анатомической структурой.
В своей предыдущей статье (Часть 27) мы создали визуальный инструмент на базе холста на MQL5. Он отрисовывает кривую-бабочку — параметрическое математическое уравнение — непосредственно на графике MetaTrader 5. Мы реализовали многослойную систему холста с градиентным фоном, плавающим окном с возможностью перетаскивания и изменения размера, рендерингом кривых с суперсэмплированием и сглаживанием по четырем цветным сегментам. Инструмент включает в себя калиброванную сетку осей с отметками делений и метками, а также плавающую панель легенды, идентифицирующую каждый сегмент. В этой статье мы развиваем эту основу. Мы представляем многослойную градиентную заливку крыльев, жилки крыла, точки текстуры чешуек и полностью детализированное анатомическое тело с сегментированным брюшком, грудью, головой, сложными глазами и изогнутыми усиками — все это отрисовано с помощью одного и того же конвейера суперсэмплирования. Мы рассмотрим следующие темы:
- Понимание заливки крыльев, текстуры и анатомической структуры бабочки
- Реализация средствами MQL5
- Визуализация
- Заключение
К концу статьи вы превратите простой параметрический контур кривой в визуально насыщенную и реалистичную иллюстрацию бабочки, отрисованную непосредственно на графике MetaTrader 5. Давайте погрузимся в процесс!
Понимание заливки крыльев, текстуры и анатомической структуры бабочки
Настоящее крыло бабочки — это не однотонный, однородный цвет. Это многослойная структура, состоящая из тысяч перекрывающихся чешуек, по-разному отражающих свет. Это создает градиенты, которые переходят от насыщенных оттенков у тела к более светлым, полупрозрачным тонам по краям. Эта естественная многослойность придает крыльям бабочки глубину и переливчатость. Мы воспроизводим тот же принцип на холсте, заполняя крыло снаружи внутрь постепенно уменьшающимися слоями. Каждый слой использует разные цвета и перекрывает предыдущий.
Для заполнения формы крыла, заданной параметрической кривой, мы используем заливкe многоугольника методом сканирующей строки. Это стандартный метод растеризации замкнутых фигур на пиксельной сетке. Для каждого горизонтального ряда пикселей в пределах вертикальной протяженности крыла мы находим точку пересечения границы крыла с этим рядом, а затем закрашиваем каждый пиксель между этими точками пересечения. Цвет каждого пикселя определяется либо его вертикальным положением внутри крыла — создавая плавный градиент сверху вниз — либо его радиальным расстоянием от центра крыла, создавая градиент, который расширяется наружу, подобно реальному цветовому узору, исходящему от тела. Всего мы применяем три слоя заливки: самый внешний слой крыла с вертикальным градиентом, первый внутренний слой, масштабированный внутрь и заполненный другим набором цветов, и второй внутренний слой, заполненный радиальным градиентом для эффекта свечения в центре.
Затем жилки крыла изображаются в виде тонких линий, расходящихся от центра тела к выборочным точкам вдоль границы крыла, имитируя структурные жилки крыла, которые придают настоящим крыльям жесткость. Чешуйки на крыльях отрисованы в виде небольших закрашенных точек, плотно расположенных вдоль края крыла, каждая из которых окрашена в соответствии со своим параметрическим сегментом и слегка осветлена к внешнему краю для создания мерцающего эффекта. Вторая точка, расположенная ближе к центру, добавлена для придания глубины. Тело находится в центре всей композиции — эллипс груди, десять сужающихся к кончику сегментов брюшка, круглая голова с выделенной областью, сложные глаза с блестящими точками и пара дугообразных усиков, составленных из перекрывающихся кругов, заканчивающихся булавовидными кончиками усиков.
Каждый слой реализован как отдельная функция. Все параметрические точки собираются заранее, преобразуются в пиксельное пространство и отображаются с помощью того же конвейера суперсэмплирования, что и в предыдущей части. Это обеспечивает стабильное качество сглаживания для заливок, жилок, чешуек и деталей тела. Вкратце, ниже представлено наглядное представление наших целей.

Реализация средствами 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.
Если вам интересно, что такое алгоритм сканирующей строки, вот краткое объяснение. Этот алгоритм обрабатывает изображение слева направо, сканируя по одной горизонтальной строке за раз, а не обрабатывая отдельные пиксели. Он фиксирует все точки пересечения ребер вдоль каждой строки сканирования и заполняет многоугольник, раскрашивая области между парами пересечений.
Это можно сравнить с проведением прямой линии поперек фигуры на бумаге одной ручкой: начиная от левой границы и двигаясь вправо, вы рисуете непрерывно, но всякий раз, когда сталкиваетесь с пересечением с границей многоугольника, вы соответственно останавливаете или возобновляете рисование. Алгоритм работает по тому же принципу. На рисунке ниже проиллюстрировано это поведение: красные точки представляют вершины многоугольника, в то время как синие точки указывают точки пересечения вдоль линии сканирования.

После этого можно определить вспомогательные функции для рисования жилок крыла и чешуек.
Рисование жилок крыла и текстуры чешуек
После того, как в результате заливки полигона наложены цветовые слои, мы добавляем мелкие детали поверхности, которые придают крыльям их органичный, натуралистичный вид — расходящиеся линии жилок крыла и плотно расположенные точки текстуры чешуек, каждая из которых обрабатывается отдельной функцией.
//+------------------------------------------------------------------+ //| 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);
После компиляции получаем следующий результат.

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

Трехслойная градиентная заливка чисто отображается по всей форме крыла, при этом каждый внутренний слой расположен постепенно внутрь, а радиальное ядро отчетливо светится в центре. Четыре цветных контура остаются четкими поверх заливки, жилки крыла естественно расходятся от центра тела, а точки, имитирующих чешуйки, плотно расположены вдоль границы крыла с видимым мерцанием к краям. Тело правильно расположено по центру, сужающееся к низу сегментированное брюшко, грудь, выделенная голова, сложные глаза с блестящими точками и два естественно изогнутых усика, заканчивающихся булавовидными кончиками усиков.
Заключение
В заключение, мы преобразовали простую параметрическую кривую-бабочку в полностью детализированную и реалистичную иллюстрацию бабочки, добавив трехслойную градиентную заливку крыльев, расходящиеся жилки крыла, плотно расположенные точки текстуры чешуек и полное анатомическое тело с сегментированным брюшком, грудью, головой, сложными глазами и изогнутыми усиками — все это отрисовано с помощью одного и того же конвейера суперсэмплирования холста. После прочтения статьи вы сможете:
- Заполнять цветом любую параметрическую кривую на холсте MQL5, используя растеризацию многоугольников с помощью как вертикальных и радиальных трехцветных градиентов
- Создавать многослойные заливки крыльев, масштабируя контур кривой внутрь к его центру и применяя постепенно разные наборы цветов на каждом уровне глубины
- Добавлять текстуру поверхности и анатомическую структуру к рисункам на холсте, используя примитивы круга и эллипса с заливкой, жилки крыла и узоры из точек, имитирующих чешуйки
В следующей части мы добавим четырехэтапную систему анимации. Она будет рисовать контур, плавно добавлять заливку, раскрывать детали поверхности, а затем переходить в непрерывный полет (махание крыльями, покачивание, раскачивание, наклон, неоновое свечение и циклическое изменение цвета).
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22129
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От начального до среднего уровня: События в объектах (IV)
Почему MetaTrader 5 подходит для AI-торговли: MQL5 + Python + ONNX + AI Assistant как экосистема алготрейдинга
Разработка инструментария для анализа Price Action (Часть 63): Автоматизация обнаружения восходящих и нисходящих клиньев на MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования