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

Торговые инструменты на MQL5 (Часть 29): Пошаговая анимация кривой-бабочки на Canvas

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

Введение

У вас уже есть подробная и реалистичная кривая-бабочка, аккуратно отрисованная на холсте MetaTrader 5. Рисунок включает в себя многослойную заливку крыльев, линии прожилок, чешуйчатую текстуру и полное анатомическое изображение тела. Однако в моменте он выглядит как статичное изображение, лишенное ощущения жизни или движения. Эта статья предназначена для разработчиков MQL5 и креативных программистов, желающих выйти за рамки статического рендеринга на холсте. Вы научитесь анимировать кривую-бабочку на протяжении всего ее жизненного цикла, начиная с первого контурного штриха и заканчивая непрерывным полетом.

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

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

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


Понимание фаз анимации кривой-бабочки и механики полета

Анимация разворачивается в четыре последовательных этапа: контур кривой постепенно формируется путем изменения параметра t от нуля до 12π; затем происходит плавное проявление заливки крыльев от прозрачности до полной непрозрачности; далее плавно появляются детали поверхности и тело; и, наконец, бабочка переходит в непрерывный полет. Каждый этап управляется миллисекундным таймером, который на каждом тике увеличивает соответствующую переменную состояния и передает управление следующему этапу после завершения.

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

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

BUTTERFLY CURVE ANIMATION ROADMAP


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

Добавление перечисления анимации, входных параметров и переменных состояния

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

//+------------------------------------------------------------------+
//|                    Canvas Drawing PART 2.1 - 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

//---

enum AnimationPhaseEnum
  {
   ANIM_DRAWING_CURVE,  // Phase 1: progressively draw the outline curve
   ANIM_FILLING_WINGS,  // Phase 2: fade wing fill layers in
   ANIM_ADDING_DETAILS, // Phase 3: fade veins, scales, and body in
   ANIM_FLYING          // Phase 4: continuous flying oscillation
  };

input group "=== ANIMATION SETTINGS ==="
input bool   enableAnimation    = true; // Enable Drawing Animation
input int    animationTimerMs   = 30;   // Animation Timer (ms)
input double curveDrawSpeed     = 0.30; // Curve Draw Speed (radians/frame)
input double fillFadeSpeed      = 0.08; // Fill Fade-In Speed (per frame)
input double detailsFadeSpeed   = 0.12; // Details Fade-In Speed (per frame)

input group "=== FLYING ANIMATION SETTINGS ==="
input bool   enableFlyingAnimation   = true;  // Enable Flying After Draw
input double wingFlapSpeed           = 0.24;  // Wing Flap Speed (radians/frame)
input double wingFlapAmplitude       = 0.25;  // Wing Flap Depth (0=none, 0.5=extreme)
input double verticalBobSpeed        = 0.08;  // Vertical Bob Speed (radians/frame)
input double verticalBobAmplitude    = 0.15;  // Vertical Bob Amount (world units)
input double horizontalSwaySpeed     = 0.05;  // Horizontal Sway Speed (radians/frame)
input double horizontalSwayAmplitude = 0.08;  // Horizontal Sway Amount (world units)
input double tiltSwaySpeed           = 0.06;  // Tilt Oscillation Speed
input double tiltSwayAmplitude       = 0.06;  // Tilt Amount (shear factor)

input group "=== BUTTERFLY ENHANCEMENT SETTINGS ==="
input bool   enableCurveGlow     = true;  // Enable Neon Glow On Curves
input int    curveGlowLayers     = 3;     // Glow Bloom Layers (1-5)
input double curveGlowIntensity  = 0.7;   // Glow Intensity (0-1)
input bool   enableColorCycling  = true;  // Enable Wing Color Cycling (Flying)
input double colorCycleSpeed     = 0.02;  // Color Cycle Speed


//+------------------------------------------------------------------+
//| Global Variables - Animation State                               |
//+------------------------------------------------------------------+
AnimationPhaseEnum animationPhase       = ANIM_DRAWING_CURVE; // Current animation phase
double             animationCurveT      = 0.0;                // Current T value reached by the drawing animation
double             animationFillAlpha   = 0.0;                // Current fill opacity during the fill-in phase
double             animationDetailAlpha = 0.0;                // Current detail opacity during the details phase

double flyWingPhase  = 0.0; // Wing flap phase accumulator
double flyBobPhase   = 0.0; // Vertical bob phase accumulator
double flySwayPhase  = 0.0; // Horizontal sway phase accumulator
double flyTiltPhase  = 0.0; // Tilt phase accumulator

double currentWingFlap   = 0.0; // Current wing X-scale factor (0=fully open)
double currentBobOffset  = 0.0; // Current vertical offset in world coordinates
double currentSwayOffset = 0.0; // Current horizontal offset in world coordinates
double currentTiltShear  = 0.0; // Current shear factor for slight tilt
double globalTime        = 0.0; // Master time accumulator for all frame-based effects
double colorCyclePhase   = 0.0; // Hue shift accumulator for color cycling

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

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

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

Изменяющиеся цвета крыльев в спектре оттенков

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

//+------------------------------------------------------------------+
//| Shift a color hue by rotating through HSV space                  |
//+------------------------------------------------------------------+
color ShiftHue(color c, double shift)
  {
   //--- Normalize red channel to 0-1 range
   double r = ((c >> 16) & 0xFF) / 255.0;
   //--- Normalize green channel to 0-1 range
   double g = ((c >>  8) & 0xFF) / 255.0;
   //--- Normalize blue channel to 0-1 range
   double b = ( c        & 0xFF) / 255.0;

   //--- Compute HSV value as the maximum of R, G, B
   double maxC = MathMax(r, MathMax(g, b));
   //--- Compute the minimum channel for saturation calculation
   double minC = MathMin(r, MathMin(g, b));
   //--- Initialize hue and saturation to zero
   double h = 0, s = 0;
   //--- Set value to the maximum channel
   double v = maxC;
   //--- Compute the channel spread
   double d = maxC - minC;

   //--- Compute saturation; avoid division by zero for achromatic colors
   s = (maxC == 0) ? 0 : d / maxC;

   //--- Compute hue only for chromatic colors
   if(d > 0)
     {
      //--- Hue from red sector
      if(maxC == r)
         h = (g - b) / d + (g < b ? 6 : 0);
      //--- Hue from green sector
      else if(maxC == g)
         h = (b - r) / d + 2;
      //--- Hue from blue sector
      else
         h = (r - g) / d + 4;
      //--- Normalize hue to 0-1 range
      h /= 6.0;
     }

   //--- Apply the requested hue shift
   h += shift;
   //--- Wrap hue above 1.0 back into range
   while(h > 1.0) h -= 1.0;
   //--- Wrap hue below 0.0 back into range
   while(h < 0.0) h += 1.0;

   //--- Compute the HSV sector index
   int    hi = (int)(h * 6);
   //--- Compute the fractional part within the sector
   double f2 = h * 6 - hi;
   //--- Precompute HSV intermediate values
   double p = v * (1 - s),   q = v * (1 - f2 * s),   t2 = v * (1 - (1 - f2) * s);
   //--- Initialize output channels to the value
   double ro = v, go = v, bo = v;

   //--- Map HSV sector to RGB output channels
   switch(hi % 6)
     {
      case 0: ro = v;  go = t2; bo = p;  break;
      case 1: ro = q;  go = v;  bo = p;  break;
      case 2: ro = p;  go = v;  bo = t2; break;
      case 3: ro = p;  go = q;  bo = v;  break;
      case 4: ro = t2; go = p;  bo = v;  break;
      case 5: ro = v;  go = p;  bo = q;  break;
     }

   //--- Recompose and return the hue-shifted color
   return ((uchar)(ro * 255) << 16) | ((uchar)(go * 255) << 8) | (uchar)(bo * 255);
  }

Начнем с извлечения красного, зеленого и синего каналов с помощью побитовых сдвигов и нормализации каждого из них до диапазона 0–1. На основе этих данных мы вычисляем представление цвета в виде оттенка, насыщенности и значения. Значение — это просто максимум из трех каналов, насыщенность — это разброс каналов, деленный на значение (с нулевым порогом для ахроматических цветов), а оттенок выводится путем определения доминирующего канала и вычисления его углового положения в пределах 60-градусного сектора цветового круга. После этого результат нормализуется в диапазоне 0–1, охватывающем все 360 градусов.

После извлечения оттенка мы добавляем к нему значение сдвига и возвращаем результат обратно в диапазон 0–1 с помощью простого цикла. Обеспечиваем, что оттенок всегда остается в допустимых пределах независимо от того, сколько полных оборотов накопил циклический аккумулятор за все время.

Для обратного преобразования в красный, зеленый и синий необходимо сопоставить сдвинутый оттенок с одним из шести секторов цветового круга. Мы вычисляем индекс сектора и долю его положения внутри него, затем получаем три промежуточных значения — p, q и t2 — из насыщенности и значения, которые представляют уровни канала на границах сектора. Далее оператор switch назначает эти промежуточные значения выходным каналам красного, зеленого и синего цветов в зависимости от того, в какой сектор попадает оттенок, охватывая все шесть переходов по кругу. Итоговые каналы масштабируются до диапазона 0–255 и объединяются в одно цветовое значение. Эта функция, вызываемая один раз для каждого сегмента крыла за фрейм с медленно продвигающейся фазой цветового цикла, обеспечивает постоянно меняющиеся цвета крыльев летающей бабочки. Далее мы определяем вспомогательные функции для рендеринга свечения и полета.

Рендеринг свечения и преобразование полета

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

//+------------------------------------------------------------------+
//| Draw a soft radial glow circle with quadratic falloff            |
//+------------------------------------------------------------------+
void DrawGlowCircle(CCanvas &canvas, int cx, int cy, int radius, color clr, uchar maxAlpha)
  {
   //--- Iterate over every row within the bounding box of the glow circle
   for(int dy = -radius; dy <= radius; dy++)
     {
      //--- Iterate over every column within the bounding box
      for(int dx = -radius; dx <= radius; dx++)
        {
         //--- Compute the exact Euclidean distance from the center
         double dist = MathSqrt((double)(dx * dx + dy * dy));
         //--- Process only pixels within the glow radius
         if(dist <= radius)
           {
            //--- Compute linear falloff factor (1 at center, 0 at edge)
            double f = 1.0 - dist / (double)radius;
            //--- Apply quadratic falloff for a soft bloom appearance
            f = f * f;
            //--- Scale max alpha by the falloff factor
            uchar a = (uchar)(maxAlpha * f);
            //--- Skip fully transparent pixels for performance
            if(a > 0)
              {
               //--- Compute the absolute pixel X coordinate
               int px = cx + dx;
               //--- Compute the absolute pixel Y coordinate
               int py = cy + dy;
               //--- Write the pixel only if it lies within canvas bounds
               if(px >= 0 && px < canvas.Width() && py >= 0 && py < canvas.Height())
                  canvas.PixelSet(px, py, ColorToARGB(clr, a));
              }
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Apply flying transform to a world-space point                    |
//+------------------------------------------------------------------+
void ApplyFlyingTransform(double &x, double &y)
  {
   //--- Compute wing flap scale: 1.0 = fully open, reduces by flap amplitude
   double flapScale = 1.0 - currentWingFlap;
   //--- Scale X distance from the body center axis to simulate wing flapping
   x = x * flapScale;

   //--- Apply vertical shear proportional to X distance for organic tilt
   y = y + x * currentTiltShear;

   //--- Translate horizontally by the sway offset
   x += currentSwayOffset;
   //--- Translate vertically by the bob offset
   y += currentBobOffset;
  }

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

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

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

Централизация сбора точек с помощью дополнительного преобразования полета

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

//+------------------------------------------------------------------+
//| Collect full butterfly curve points with optional flying transform|
//+------------------------------------------------------------------+
int CollectButterflyPoints(double tEnd, double &xWorld[], double &yWorld[],
                           double &xPx[], double &yPx[],
                           double rangeX, double rangeY, int plotW, int plotH,
                           bool applyFlying, double &outMaxDist, double &outCX, double &outCY)
  {
   //--- Estimate the maximum number of points to pre-allocate arrays
   int est = (int)((tEnd - butterflyTStart) / butterflyTStep) + 2;
   //--- Ensure at least one slot is allocated
   if(est < 1) est = 1;

   //--- Pre-allocate world-space and pixel-space coordinate arrays
   ArrayResize(xWorld, est); ArrayResize(yWorld, est);
   ArrayResize(xPx,    est); ArrayResize(yPx,    est);

   //--- Initialize point counter
   int count = 0;
   //--- Initialize the maximum radial distance from center
   outMaxDist = 0;

   //--- Compute the wing center at the world-space origin
   double cx = 0, cy = 0;
   //--- Apply flying transform to the center if in flying phase
   if(applyFlying) ApplyFlyingTransform(cx, cy);

   //--- Map the transformed center X to a pixel column
   outCX = (cx - butterflyMinX) / rangeX * plotW;
   //--- Map the transformed center Y to a pixel row (inverted axis)
   outCY = (butterflyMaxY - cy) / rangeY * plotH;

   //--- Traverse the parametric domain and collect valid curve points
   for(double t = butterflyTStart; t <= tEnd; 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 X world coordinate
      double x = MathSin(t) * term;
      //--- Compute Y world coordinate
      double y = MathCos(t) * term;
      //--- Skip invalid coordinate pairs
      if(!MathIsValidNumber(x) || !MathIsValidNumber(y)) continue;

      //--- Store the raw world-space coordinates
      xWorld[count] = x;
      yWorld[count] = y;

      //--- Copy world coordinates for optional transform
      double wx = x, wy = y;
      //--- Apply flying transform to pixel-mapped coordinates if required
      if(applyFlying) ApplyFlyingTransform(wx, wy);

      //--- Map the (possibly transformed) X to a pixel column
      xPx[count] = (wx - butterflyMinX) / rangeX * plotW;
      //--- Map the (possibly transformed) Y to a pixel row (inverted axis)
      yPx[count] = (butterflyMaxY - wy) / rangeY * plotH;

      //--- Compute radial distance of this pixel point from the wing center
      double dist = MathSqrt(MathPow(xPx[count] - outCX, 2) + MathPow(yPx[count] - outCY, 2));
      //--- Update the maximum distance for gradient normalization
      outMaxDist = MathMax(outMaxDist, dist);
      //--- Increment the valid point count
      count++;
     }

   //--- Trim all arrays to the exact number of valid points collected
   ArrayResize(xWorld, count); ArrayResize(yWorld, count);
   ArrayResize(xPx,    count); ArrayResize(yPx,    count);

   //--- Return the total number of valid points collected
   return count;
  }

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

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

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

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

Это небольшое, но важное обновление существующей функции "GetWingColorForT". Сравнение границ сегментов и логика выбора цвета остались прежними, но теперь перед возвратом результата мы добавляем ещё один шаг.

//+------------------------------------------------------------------+
//| Resolve wing curve color for a given parametric T value          |
//+------------------------------------------------------------------+
color GetWingColorForT(double tParameter)
  {
   //--- Select base color by T segment position
   color base;
   if     (tParameter <= 3.0 * M_PI) base = blueCurveColor;    // Segment 1: blue
   else if(tParameter <= 6.0 * M_PI) base = redCurveColor;     // Segment 2: red
   else if(tParameter <= 9.0 * M_PI) base = orangeCurveColor;  // Segment 3: orange
   else                               base = greenCurveColor;  // Segment 4: green

   //--- Apply hue shift during flying phase if color cycling is enabled
   if(enableColorCycling && animationPhase == ANIM_FLYING)
      base = ShiftHue(base, colorCyclePhase);

   //--- Return the resolved wing segment color
   return base;
  }

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

Обновление жилок крыльев и чешуек в целях поддержки функции анимации

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

//+------------------------------------------------------------------+
//| Draw anti-aliased wing vein lines radiating from the body center |
//+------------------------------------------------------------------+
void DrawWingVeins(CCanvas &canvas, double &xPts[], double &yPts[], int ptCount,
                   double rangeX, double rangeY, int plotW, int plotH, double opMul = 1.0)
  {
   //--- Skip drawing if wing veins have been disabled by the user
   if(!showButterflyWingVeins) return;

   //--- Initialize body center in world space at the origin
   double bcx = 0, bcy = 0;
   //--- Apply flying transform to the body center if in flying phase
   if(animationPhase == ANIM_FLYING) ApplyFlyingTransform(bcx, bcy);

   //--- Map the transformed body center X to a pixel column
   int cxPx = (int)((bcx - butterflyMinX) / rangeX * plotW);
   //--- Map the transformed body center Y to a pixel row (inverted axis)
   int cyPx = (int)((butterflyMaxY - bcy) / rangeY * plotH);

   //--- Set vein color as a darkened body color scaled by opacity multiplier
   uint argbVein = ColorToARGB(DarkenColor(butterflyBodyColor, 0.2),
                               (uchar)(150 * butterflyWingOpacity * opMul));

   //--- Sample wing edge points at regular intervals to define vein endpoints
   for(int i = 0; i < ptCount; i += 50)
     {
      //--- Get the world-space X coordinate of this wing edge sample
      double wx = xPts[i];
      //--- Get the world-space Y coordinate of this wing edge sample
      double wy = yPts[i];
      //--- Apply flying transform to the wing edge point if in flying phase
      if(animationPhase == ANIM_FLYING) ApplyFlyingTransform(wx, wy);

      //--- Map the transformed wing point X to a pixel column
      int px = (int)((wx - butterflyMinX) / rangeX * plotW);
      //--- Map the transformed wing point Y to a pixel row (inverted axis)
      int py = (int)((butterflyMaxY - wy) / rangeY * plotH);

      //--- Draw an anti-aliased vein line from the body center to the wing edge
      canvas.LineAA(cxPx, cyPx, px, py, argbVein);
     }
  }

//+------------------------------------------------------------------+
//| Draw wing scale texture dots along and inside the wing boundary  |
//+------------------------------------------------------------------+
void DrawWingScales(CCanvas &canvas, double &xCoords[], double &yCoords[],
                    int ptCount, double rangeX, double rangeY,
                    int plotW, int plotH,
                    double ctrX, double ctrY, double maxDist, double opMul = 1.0)
  {
   //--- 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 < ptCount; i += 4)
     {
      //--- Get the world-space X coordinate of this scale sample
      double wx = xCoords[i];
      //--- Get the world-space Y coordinate of this scale sample
      double wy = yCoords[i];
      //--- Apply flying transform to the scale point if in flying phase
      if(animationPhase == ANIM_FLYING) ApplyFlyingTransform(wx, wy);

      //--- Map the transformed scale point X to a pixel column
      double pixelX = (wx - butterflyMinX) / rangeX * plotW;
      //--- Map the transformed scale point Y to a pixel row (inverted axis)
      double pixelY = (butterflyMaxY - wy) / rangeY * plotH;

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

      //--- Compute the radial distance of this scale from the wing center
      double dist = MathSqrt(MathPow(pixelX - ctrX, 2) + MathPow(pixelY - ctrY, 2));
      //--- Normalize distance to a 0-1 blend factor
      double factor = (maxDist > 0) ? dist / maxDist : 0;
      //--- 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 and multiplier applied
      uint argbScale = ColorToARGB(scaleColor, (uchar)(180 * butterflyWingOpacity * opMul));
      //--- 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 dx = pixelX - ctrX, dy = pixelY - ctrY;
      //--- Compute the vector magnitude for normalization
      double norm = MathSqrt(dx * dx + dy * dy);
      //--- 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
         dx /= norm;
         //--- Normalize the Y component of the inward direction
         dy /= norm;
         //--- Draw a secondary inward scale dot slightly smaller for depth
         DrawFilledCircle(canvas,
                          (int)(pixelX - dx * radius * 2),
                          (int)(pixelY - dy * radius * 2),
                          radius - 1, argbScale);
        }
     }
  }

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

Второе изменение — интеграция преобразования полета. В функции "DrawWingVeins" центр тела инициализируется в начале координат мирового пространства, а затем передается через функцию "ApplyFlyingTransform", если текущая фаза — "ANIM_FLYING", что гарантирует перемещение корня жилок вместе с бабочкой, а не их фиксацию в центре холста. Каждая точка края крыла, выбранная в качестве образца, аналогичным образом преобразуется перед отображением в пиксельное пространство, так что линии жилок во время полета простираются от корректного преобразованного центра тела до корректной преобразованной границы крыльев. Функция "DrawWingScales" применяет тот же принцип: каждая точка выборки чешуек копируется, преобразуется (если объект находится в полете), а затем отображается в пиксельном пространстве перед размещением точки чешуйки, благодаря чему вся текстура чешуйки остается привязанной к движущейся поверхности крыльев на протяжении всей анимации. Далее мы добавим рендеринг с учетом анимации при рисовании бабочки.

Обновление основного рендерера бабочки для анимации и свечения

Функция "DrawRealisticButterfly" сохраняет всю существующую логику заливки, детализации и рисования контура, но получает два важных дополнения вверху и внутри цикла рисования контура — управление рендерингом с учетом фазы и систему неонового свечения.

//+------------------------------------------------------------------+
//| Render filled, outlined, and detailed butterfly with animation   |
//+------------------------------------------------------------------+
void DrawRealisticButterfly(CCanvas &canvas, int plotWidth, int plotHeight,
                             double rangeX, double rangeY)
  {
   //--- Set defaults: use full curve range and full opacity for both fills and details
   double effectiveTEnd       = butterflyTEnd;
   double effectiveFillOp     = 1.0;
   double effectiveDetailOp   = 1.0;
   bool   drawFills           = true;
   bool   drawDetails         = true;
   //--- Determine whether flying transform should be applied this frame
   bool   applyFlying         = (animationPhase == ANIM_FLYING);

   //--- Adjust draw parameters according to the current animation phase
   switch(animationPhase)
     {
      case ANIM_DRAWING_CURVE:
         //--- Limit curve drawing to the current animation T progress
         effectiveTEnd = animationCurveT;
         //--- Suppress fills and details during the curve-drawing phase
         drawFills = false; drawDetails = false;
         break;
      case ANIM_FILLING_WINGS:
         //--- Scale fill opacity by the current fill fade progress
         effectiveFillOp = animationFillAlpha;
         //--- Suppress details until filling is complete
         drawDetails = false;
         break;
      case ANIM_ADDING_DETAILS:
         //--- Scale detail opacity by the current details fade progress
         effectiveDetailOp = animationDetailAlpha;
         break;
      case ANIM_FLYING:
         //--- Use full opacity for all elements during flying
         break;
     }

   //--- EXISTING LOGIC

   //--- Draw each of the four wing outline color segments
   for(int s = 0; s < 4; s++)
     {
      //--- Get the T start value for this segment
      double sFrom = segBounds[s];
      //--- Clamp the T end value to the current animation progress
      double sTo   = MathMin(segBounds[s + 1], effectiveTEnd);
      //--- Skip segments that have not yet been reached by the animation
      if(sFrom >= effectiveTEnd) break;

      //--- Retrieve the base color for this segment
      color curveClr = segBaseColors[s];
      //--- Apply color cycling if in flying phase and cycling is enabled
      if(enableColorCycling && animationPhase == ANIM_FLYING)
         curveClr = ShiftHue(curveClr, colorCyclePhase);
      //--- Convert segment color to fully opaque ARGB
      uint segARGB = ColorToARGB(curveClr, 255);

      //--- Initialize previous pixel coordinates for connectivity
      double prevPx = -1, prevPy = -1;

      //--- Traverse this segment's T range to draw the outline
      for(double t = sFrom; t <= sTo; 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 X world coordinate
         double x = MathSin(t) * term;
         //--- Compute Y world coordinate
         double y = MathCos(t) * term;
         //--- Reset connectivity on invalid points
         if(!MathIsValidNumber(x) || !MathIsValidNumber(y))
           {
            prevPx = -1; prevPy = -1; continue;
           }
         //--- Apply flying transform to the curve point if in flying phase
         if(applyFlying) ApplyFlyingTransform(x, y);

         //--- Map X to pixel column
         double cpx = (x - butterflyMinX) / rangeX * plotWidth;
         //--- Map Y to pixel row (inverted axis)
         double cpy = (butterflyMaxY - y) / rangeY * plotHeight;
         //--- Round to nearest integer pixel coordinate
         int ix = (int)MathRound(cpx), iy = (int)MathRound(cpy);

         //--- Draw the segment line only if a valid previous point exists
         if(prevPx >= 0 && prevPy >= 0)
           {
            //--- Store rounded previous pixel coordinates
            int px1 = (int)MathRound(prevPx), py1 = (int)MathRound(prevPy);

            //--- Draw neon glow bloom layers behind the core line if enabled
            if(enableCurveGlow)
              {
               //--- Iterate from outermost to innermost glow layer
               for(int g = curveGlowLayers; g >= 1; g--)
                 {
                  //--- Compute alpha for this bloom layer (diminishes with layer)
                  uchar ga = (uchar)(25 * curveGlowIntensity / g);
                  //--- Skip layers that are effectively invisible
                  if(ga < 2) continue;
                  //--- Convert glow color to ARGB with computed bloom alpha
                  uint gc = ColorToARGB(curveClr, ga);
                  //--- Draw offset lines around the core to simulate glow spread
                  for(int off = -g; off <= g; off++)
                    {
                     //--- Horizontal offset glow pass
                     canvas.LineAA(px1 + off, py1, ix + off, iy, gc);
                     //--- Vertical offset glow pass
                     canvas.LineAA(px1, py1 + off, ix, iy + off, gc);
                    }
                 }
               //--- Compute a brightened center highlight color for the hot core
               color bright = LightenColor(curveClr, 0.5);
               //--- Draw the white-hot center highlight over the bloom layers
               canvas.LineAA(px1, py1, ix, iy,
                             ColorToARGB(bright, (uchar)(150 * curveGlowIntensity)));
              }

            //--- Draw the primary anti-aliased outline line
            canvas.LineAA(px1, py1, ix, iy, segARGB);
            //--- Draw the offset line for additional visual thickness
            canvas.LineAA(px1 + 1, py1, ix + 1, iy, segARGB);
           }
         //--- Store current pixel X for next iteration
         prevPx = cpx;
         //--- Store current pixel Y for next iteration
         prevPy = cpy;
        }
     }
   //--- EXISTING LOGIC
  }

В начале функции мы объявляем набор эффективных параметров отрисовки до начала любого процесса рисования. Эффективное конечное значение T, непрозрачность заливки и непрозрачность детализации по умолчанию равны своим полным значениям, а флаги заливки и детализации по умолчанию имеют значение true. Далее оператор switch переопределяет эти значения по умолчанию в зависимости от текущей фазы анимации: во время отрисовки кривой эффективное конечное значение T ограничивается текущим аккумулятором анимации, и заливка, и детализация полностью подавляются; во время заливки крыльев непрозрачность заливки масштабируется в соответствии с текущим альфа-каналом заливки, а детализация остается подавленной.

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

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

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

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

Инициализация системы анимации при запуске

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

   //--- EXISTING LOGIC
   
   //--- Initialize animation state if drawing animation is enabled
   if(enableAnimation)
     {
      //--- Start from the curve-drawing phase
      animationPhase       = ANIM_DRAWING_CURVE;
      //--- Begin the T accumulator at the curve start
      animationCurveT      = butterflyTStart;
      //--- Begin fill opacity at zero (fully transparent)
      animationFillAlpha   = 0.0;
      //--- Begin detail opacity at zero (fully transparent)
      animationDetailAlpha = 0.0;
     }
   //--- Skip drawing animation but enable flying if only flying is requested
   else if(enableFlyingAnimation)
     {
      animationPhase = ANIM_FLYING;
     }
   //--- Both animations disabled: show full static butterfly immediately
   else
     {
      //--- Use flying state as the complete steady state
      animationPhase   = ANIM_FLYING;
      //--- Zero all flying transform values to lock wings open and centered
      currentWingFlap  = 0; currentBobOffset   = 0;
      currentSwayOffset = 0; currentTiltShear  = 0;
     }

   //--- Reset all flying oscillation phase accumulators
   flyWingPhase = 0; flyBobPhase  = 0;
   flySwayPhase = 0; flyTiltPhase = 0;
   //--- Reset all flying transform values to neutral
   currentWingFlap   = 0; currentBobOffset  = 0;
   currentSwayOffset = 0; currentTiltShear  = 0;

   //--- Start the millisecond timer if any animation is enabled
   if(enableAnimation || enableFlyingAnimation)
      EventSetMillisecondTimer(animationTimerMs);

   //--- Render the full main visualization on startup
   RenderMainVisualization();
   //--- Render the legend panel on startup
   RenderLegend();

   //--- Enable mouse move events for drag and resize interaction
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   //--- Force an immediate chart redraw
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
  }

Условная конструкция из трёх ветвей определяет начальное состояние на основе входных данных анимации. Если включена полная анимация рисования, фаза устанавливается в значение "ANIM_DRAWING_CURVE", накопитель кривой T сбрасывается до параметрического начального значения, а накопители прозрачности заливки и детализации обнуляются, так что бабочка изначально полностью невидима и строится с нуля. Если включена только анимация полета без последовательности рисования, мы сразу переходим к "ANIM_FLYING", так что бабочка появляется немедленно в своей полной форме и сразу же начинает летать. Если обе анимации отключены, мы также устанавливаем фазу на "ANIM_FLYING", но обнуляем все значения преобразования — взмах крыльев, смещение качания, смещение колебания и сдвиг наклона — фиксируя бабочку в ее полностью открытом, центрированном, статическом положении без движения.

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

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

Обработчик "OnTimer" является движком всей системы анимации — он срабатывает на каждом кадре с настроенным интервалом и продвигает ту фазу, которая в данный момент активна, прежде чем запустить перерисовку. Обработчик OnDeinit получает одно небольшое, но важное дополнение.

//+------------------------------------------------------------------+
//| Expert timer function                                            |
//+------------------------------------------------------------------+
void OnTimer()
  {
   //--- Advance the master global time accumulator each frame
   globalTime += 0.05;

   //--- Drive the correct animation behavior based on the current phase
   switch(animationPhase)
     {
      case ANIM_DRAWING_CURVE:
         //--- Advance the curve T by the configured draw speed
         animationCurveT += curveDrawSpeed;
         //--- Transition to fill phase once the full curve has been drawn
         if(animationCurveT >= butterflyTEnd)
           {
            //--- Lock T at the exact end value
            animationCurveT = butterflyTEnd;
            //--- Advance to the wing-filling phase
            animationPhase  = ANIM_FILLING_WINGS;
            //--- Reset fill opacity to start the fade-in from transparent
            animationFillAlpha = 0.0;
           }
         break;

      case ANIM_FILLING_WINGS:
         //--- Advance fill opacity by the configured fade speed
         animationFillAlpha += fillFadeSpeed;
         //--- Transition to details phase once fill is fully opaque
         if(animationFillAlpha >= 1.0)
           {
            //--- Lock fill opacity at full
            animationFillAlpha = 1.0;
            //--- Advance to the details-adding phase
            animationPhase = ANIM_ADDING_DETAILS;
            //--- Reset detail opacity to start the fade-in from transparent
            animationDetailAlpha = 0.0;
           }
         break;

      case ANIM_ADDING_DETAILS:
         //--- Advance detail opacity by the configured fade speed
         animationDetailAlpha += detailsFadeSpeed;
         //--- Transition to flying phase once all details are fully visible
         if(animationDetailAlpha >= 1.0)
           {
            //--- Lock detail opacity at full
            animationDetailAlpha = 1.0;
            //--- Start the flying animation if it is enabled
            if(enableFlyingAnimation)
              {
               //--- Advance to the flying phase
               animationPhase = ANIM_FLYING;
               //--- Reset all oscillation phase accumulators
               flyWingPhase = 0; flyBobPhase  = 0;
               flySwayPhase = 0; flyTiltPhase = 0;
              }
            else
              {
               //--- Animation fully complete; transition to static flying state
               animationPhase    = ANIM_FLYING;
               //--- Zero transform values to lock the butterfly in place
               currentWingFlap   = 0; currentBobOffset  = 0;
               currentSwayOffset = 0; currentTiltShear  = 0;
               //--- Stop the timer; no further animation is needed
               EventKillTimer();
               //--- Perform a final render to show the completed static butterfly
               RenderMainVisualization();
               ChartRedraw();
               //--- Exit early; no further processing needed this tick
               return;
              }
           }
         break;

      case ANIM_FLYING:
         //--- Stop the timer if flying animation has been disabled
         if(!enableFlyingAnimation) { EventKillTimer(); return; }

         //--- Advance the wing flap oscillation phase
         flyWingPhase  += wingFlapSpeed;
         //--- Advance the vertical bob oscillation phase
         flyBobPhase   += verticalBobSpeed;
         //--- Advance the horizontal sway oscillation phase
         flySwayPhase  += horizontalSwaySpeed;
         //--- Advance the tilt oscillation phase
         flyTiltPhase  += tiltSwaySpeed;

         //--- Compute current wing flap scale using an abs-sine for smooth open-close
         currentWingFlap   = wingFlapAmplitude   * MathAbs(MathSin(flyWingPhase));
         //--- Compute current vertical bob offset using a sine wave
         currentBobOffset  = verticalBobAmplitude   * MathSin(flyBobPhase);
         //--- Compute current horizontal sway offset using a sine wave
         currentSwayOffset = horizontalSwayAmplitude * MathSin(flySwayPhase);
         //--- Compute current tilt shear factor using a sine wave
         currentTiltShear  = tiltSwayAmplitude   * MathSin(flyTiltPhase);

         //--- Advance the color cycle phase if color cycling is enabled
         if(enableColorCycling) colorCyclePhase += colorCycleSpeed;
         break;
     }

   //--- Rebuild the main visualization for this animation frame
   RenderMainVisualization();
   //--- Refresh the chart to display the new frame
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   //--- Stop the animation timer
   EventKillTimer();
   //--- Destroy the main canvas and release its resources
   mainCanvas.Destroy();
   //--- Destroy the curve canvas and release its resources
   curveCanvas.Destroy();
   //--- Destroy the legend canvas and release its resources
   legendCanvas.Destroy();
   //--- Refresh the chart after all objects have been removed
   ChartRedraw();
  }

На каждом тике мы продвигаем глобальный временной аккумулятор, а затем включаем текущую фазу анимации. В "ANIM_DRAWING_CURVE" аккумулятор кривой T продвигается на скорость отрисовки в каждом фрейме — как только он достигает или превышает максимальное конечное значение, он фиксируется точно на этом значении, фаза переходит в "ANIM_FILLING_WINGS", а аккумулятор прозрачности заливки сбрасывается до нуля, чтобы начать следующее проявление от состояния прозрачности. В "ANIM_FILLING_WINGS" альфа-канал заливки продвигается на скорость появления заливки в каждом фрейме, пока не достигнет 1,0, после чего он фиксируется, и фаза переходит в "ANIM_ADDING_DETAILS", при этом альфа-канал деталей сбрасывается до нуля.

В "ANIM_ADDING_DETAILS" альфа-канал детализации продвигается аналогично — после завершения перехода происходит разветвление в зависимости от того, включен ли полет. Если да, фаза переходит в фазу "ANIM_FLYING", и все аккумуляторы осцилляторов сбрасываются до нуля для чистого начала полета. Если функция полета отключена, значения преобразования обнуляются для фиксации статичности бабочки, таймер останавливается с помощью EventKillTimer, запускается финальный рендеринг, и функция завершает работу раньше, поскольку дальнейшие отметки не требуются.

В функции "ANIM_FLYING" все четыре фазовых аккумулятора осцилляторов увеличиваются на величину своих входных значений скорости в каждом фрейме, и четыре выходные переменные преобразования пересчитываются на их основе — взмах крыльев с использованием абсолютной синусоидальной волны для плавного цикла открытия-закрытия, а также покачивание, колебание и наклон с использованием обычной синусоидальной волны. Фаза цикла цвета увеличивается в каждом фрейме, если циклическое движение включено. После переключения вызываются функции "RenderMainVisualization" и ChartRedraw для вывода обновленного фрейма на экран.

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


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

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

BACKTEST GIF

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


Заключение

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

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

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Chacha Ian Maroa
Chacha Ian Maroa | 30 апр. 2026 в 11:14
Отличная работа! Так держать!
Рыночные секреты Ларри Уильямса (Часть 12): Торговля разворотами Smash Day на основе контекста Рыночные секреты Ларри Уильямса (Часть 12): Торговля разворотами Smash Day на основе контекста
В статье показано, как автоматизировать разворотные паттерны Smash Day Ларри Уильямса в MQL5 в рамках структурированного подхода. Мы реализуем советник, который ограничивает период валидности сетапов заданным окном, согласует входы с направлением тренда по Supertrend и применяет фильтры по дням недели, а также поддерживает вход при пересечении уровня или после закрытия бара за уровнем. Код обеспечивает правило: не более одной открытой позиции одновременно и поддерживает расчет объема на основе риска или фиксированный размер позиции. Приведены пошаговая разработка, процедура бэктестирования и воспроизводимые настройки.
Разработка инструментария для анализа Price Action (Часть 66): Создание сканера паттерна "голова и плечи" с проверкой структуры на MQL5 Разработка инструментария для анализа Price Action (Часть 66): Создание сканера паттерна "голова и плечи" с проверкой структуры на MQL5
Стабильно выявлять паттерны "голова и плечи" в рыночных данных в реальном времени достаточно трудно из-за шума и структурной неоднозначности. В этой статье представлен структурированный индикатор на языке MQL5 на основе треугольников, который выделяет компоненты паттерна, строит линию шеи и проверяет формации с использованием ATR, симметрии и ограничений наклона. Система обнаруживает и отображает стандартные и перевернутые паттерны, присваивает им оценку качества и подтверждает пробои с возможностью генерации алертов, обеспечивая последовательный анализ графиков на основе четких правил.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Сеточный советник на клеточном автомате с онлайн-обучением в MQL5 (Часть III): Живой граф признаков Сеточный советник на клеточном автомате с онлайн-обучением в MQL5 (Часть III): Живой граф признаков
Третья статья серии вводит обучаемый граф признаков в архитектуре Cellular10K: веса связей feature → feature онлайн усиливаются после верных прогнозов и ослабляются после ошибок. Разбираются мягкая инициализация, шаг message passing, локальное правило обучения в стиле Хебба, ограничение весов, нормировка и decay. Показана интеграция с клеточным автоматом и бинарным предиктором, а также метрики диагностики и практические пороги запуска для контроля переобучения.