Article-22495-Dispatch-Driv.../AI Canvas Primitives.mqh

2072 lines
84 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| AI Canvas Primitives.mqh |
//| 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"
//--- Include guard
#ifndef AI_CANVAS_PRIMITIVES_MQH
#define AI_CANVAS_PRIMITIVES_MQH
//--- Include required libraries
#include <Canvas/Canvas.mqh>
#include "AI Canvas Theme.mqh"
//+------------------------------------------------------------------+
//| Fast canvas subclass exposing direct pixel buffer access |
//+------------------------------------------------------------------+
class CAiCanvasFast : public CCanvas
{
public:
//+---------------------------------------------------------------+
//| Read pixel directly from buffer |
//+---------------------------------------------------------------+
uint GetPixelDirect(int x, int y) const
{
//--- Direct row-major buffer read
return m_pixels[y * Width() + x];
}
//+---------------------------------------------------------------+
//| Write pixel directly to buffer |
//+---------------------------------------------------------------+
void SetPixelDirect(int x, int y, uint v)
{
//--- Direct row-major buffer write
m_pixels[y * Width() + x] = v;
}
//+---------------------------------------------------------------+
//| Copy rectangular region from source canvas to this canvas |
//+---------------------------------------------------------------+
void CopyRectFromCanvas(CCanvas &src, int l, int t, int r, int b)
{
//--- Get source and destination dimensions
const int sw = src.Width();
const int sh = src.Height();
const int dw = Width();
const int dh = Height();
//--- Clamp rectangle bounds to both canvases
const int cl = MathMax(0, l);
const int ct = MathMax(0, t);
const int cr = MathMin(MathMin(r, sw), dw);
const int cb = MathMin(MathMin(b, sh), dh);
//--- Copy row by row
for(int yy = ct; yy < cb; yy++)
{
const int rowBase = yy * dw;
for(int xx = cl; xx < cr; xx++)
m_pixels[rowBase + xx] = src.PixelGet(xx, yy);
}
}
//+---------------------------------------------------------------+
//| Copy rectangular region from this canvas to destination |
//+---------------------------------------------------------------+
void CopyRectToCanvas(CCanvas &dst, int l, int t, int r, int b)
{
//--- Get destination and source dimensions
const int dwOther = dst.Width();
const int dhOther = dst.Height();
const int sw = Width();
const int sh = Height();
//--- Clamp rectangle bounds
const int cl = MathMax(0, l);
const int ct = MathMax(0, t);
const int cr = MathMin(MathMin(r, sw), dwOther);
const int cb = MathMin(MathMin(b, sh), dhOther);
//--- Copy row by row
for(int yy = ct; yy < cb; yy++)
{
const int rowBase = yy * sw;
for(int xx = cl; xx < cr; xx++)
dst.PixelSet(xx, yy, m_pixels[rowBase + xx]);
}
}
//+---------------------------------------------------------------+
//| Fill rectangle with solid color directly in buffer |
//+---------------------------------------------------------------+
void FillRectFast(int l, int t, int r, int b, uint argb)
{
//--- Clamp rectangle to canvas bounds
const int w = Width();
const int h = Height();
const int cl = MathMax(0, l);
const int ct = MathMax(0, t);
const int cr = MathMin(r, w);
const int cb = MathMin(b, h);
//--- Fill row by row
for(int yy = ct; yy < cb; yy++)
{
const int rowBase = yy * w;
for(int xx = cl; xx < cr; xx++)
m_pixels[rowBase + xx] = argb;
}
}
};
//+------------------------------------------------------------------+
//| Build ARGB color from named color and percent opacity |
//+------------------------------------------------------------------+
uint AiColorWithPercentOpacity(color c, int pct)
{
//--- Clamp percentage
if(pct < 0) pct = 0;
if(pct > 100) pct = 100;
//--- Compute alpha and pack
const uchar alpha = (uchar)((255 * pct) / 100);
return ColorToARGB(c, alpha);
}
//+------------------------------------------------------------------+
//| Blend a source pixel onto a canvas with alpha compositing |
//+------------------------------------------------------------------+
void AiBlendPixel(CCanvas &canvas, int x, int y, uint srcArgb)
{
//--- Bail outside canvas bounds
if(x < 0 || y < 0 || x >= canvas.Width() || y >= canvas.Height()) return;
//--- Fast paths for transparent and opaque source
const uchar sa = (uchar)((srcArgb >> 24) & 0xFF);
if(sa == 0) return;
if(sa == 255) { canvas.PixelSet(x, y, srcArgb); return; }
//--- Decompose source and destination components
const uint dst = canvas.PixelGet(x, y);
const double sAf = sa / 255.0;
const double sRf = ((srcArgb >> 16) & 0xFF) / 255.0;
const double sGf = ((srcArgb >> 8) & 0xFF) / 255.0;
const double sBf = ( srcArgb & 0xFF) / 255.0;
const double dAf = ((dst >> 24) & 0xFF) / 255.0;
const double dRf = ((dst >> 16) & 0xFF) / 255.0;
const double dGf = ((dst >> 8) & 0xFF) / 255.0;
const double dBf = ( dst & 0xFF) / 255.0;
//--- Compute output alpha and bail on full transparency
const double oAf = sAf + dAf * (1.0 - sAf);
if(oAf <= 0.0) { canvas.PixelSet(x, y, 0); return; }
//--- Compose output pixel using premultiplied blending
const uint outArgb = ((uint)(uchar)(oAf * 255.0 + 0.5) << 24) |
((uint)(uchar)((sRf*sAf + dRf*dAf*(1.0-sAf)) / oAf * 255.0 + 0.5) << 16) |
((uint)(uchar)((sGf*sAf + dGf*dAf*(1.0-sAf)) / oAf * 255.0 + 0.5) << 8) |
(uint)(uchar)((sBf*sAf + dBf*dAf*(1.0-sAf)) / oAf * 255.0 + 0.5);
canvas.PixelSet(x, y, outArgb);
}
//+------------------------------------------------------------------+
//| Wrapper alias for AiBlendPixel used by widget code |
//+------------------------------------------------------------------+
void WidgetAiBlendPixel(CCanvas &canvas, int x, int y, uint srcArgb)
{
//--- Forward to underlying blend
AiBlendPixel(canvas, x, y, srcArgb);
}
//+------------------------------------------------------------------+
//| Draw thick anti-aliased line on canvas |
//+------------------------------------------------------------------+
void AiThickLineAA(CCanvas &canvas, int x0, int y0, int x1, int y1, int thickness, uint argb)
{
//--- Clamp thickness to supported range
if(thickness < 1) thickness = 1;
if(thickness > 4) thickness = 4;
//--- Fast path for horizontal line
if(y0 == y1)
{
const int xL = MathMin(x0, x1);
const int xR = MathMax(x0, x1);
const int yTop = y0 - thickness / 2;
const int yBot = yTop + thickness - 1;
for(int yy = yTop; yy <= yBot; yy++)
for(int xx = xL; xx <= xR; xx++)
AiBlendPixel(canvas, xx, yy, argb);
return;
}
//--- Fast path for vertical line
if(x0 == x1)
{
const int yT = MathMin(y0, y1);
const int yB = MathMax(y0, y1);
const int xL = x0 - thickness / 2;
const int xR = xL + thickness - 1;
for(int xx = xL; xx <= xR; xx++)
for(int yy = yT; yy <= yB; yy++)
AiBlendPixel(canvas, xx, yy, argb);
return;
}
//--- Setup line geometry for arbitrary slope
const double halfT = (double)thickness / 2.0;
const double ax = (double)x0 + 0.5, ay = (double)y0 + 0.5;
const double bx = (double)x1 + 0.5, by = (double)y1 + 0.5;
const double dx = bx - ax, dy = by - ay;
const double lenSq = dx * dx + dy * dy;
if(lenSq < 1e-9) return;
//--- Compute bounding box with padding
const double pad = halfT + 1.0;
const int bbL = (int)MathFloor(MathMin(ax, bx) - pad);
const int bbT = (int)MathFloor(MathMin(ay, by) - pad);
const int bbR = (int)MathCeil (MathMax(ax, bx) + pad);
const int bbB = (int)MathCeil (MathMax(ay, by) + pad);
const uchar bA = (uchar)((argb >> 24) & 0xFF);
const uint rgb = argb & 0x00FFFFFF;
//--- Setup supersampling parameters
const int sub = 4;
const double step = 1.0 / sub;
const int subSq = sub * sub;
//--- Walk pixels in bounding box
for(int py = bbT; py <= bbB; py++)
{
for(int px = bbL; px <= bbR; px++)
{
//--- Project pixel center onto line
const double pcx = (double)px + 0.5;
const double pcy = (double)py + 0.5;
double t = ((pcx - ax) * dx + (pcy - ay) * dy) / lenSq;
if(t < 0.0) t = 0.0;
if(t > 1.0) t = 1.0;
const double projX = ax + t * dx;
const double projY = ay + t * dy;
const double pdx = pcx - projX;
const double pdy = pcy - projY;
const double centerDist = MathSqrt(pdx * pdx + pdy * pdy);
//--- Skip pixel outside line stroke
if(centerDist > halfT + 1.0) continue;
//--- Solid fill for clearly-inside pixels
if(centerDist <= halfT - 1.0)
{
AiBlendPixel(canvas, px, py, argb);
continue;
}
//--- Supersample edge pixels for smooth boundary
int inside = 0;
for(int sy = 0; sy < sub; sy++)
{
for(int sx = 0; sx < sub; sx++)
{
const double sx_ = (double)px + (sx + 0.5) * step;
const double sy_ = (double)py + (sy + 0.5) * step;
double st = ((sx_ - ax) * dx + (sy_ - ay) * dy) / lenSq;
if(st < 0.0) st = 0.0;
if(st > 1.0) st = 1.0;
const double spx = ax + st * dx;
const double spy = ay + st * dy;
const double sdx = sx_ - spx;
const double sdy = sy_ - spy;
if(sdx * sdx + sdy * sdy <= halfT * halfT) inside++;
}
}
//--- Blend with computed coverage
if(inside == 0) continue;
const uint covArgb = (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb;
AiBlendPixel(canvas, px, py, covArgb);
}
}
}
//+------------------------------------------------------------------+
//| Wrapper alias for AiThickLineAA used by widget code |
//+------------------------------------------------------------------+
void WidgetAiThickLineAA(CCanvas &canvas, int x0, int y0, int x1, int y1, int thickness, uint argb)
{
//--- Forward to underlying thick line
AiThickLineAA(canvas, x0, y0, x1, y1, thickness, argb);
}
//+------------------------------------------------------------------+
//| Canvas drawing primitives helper class |
//+------------------------------------------------------------------+
class CAiCanvasPrimitives
{
public:
bool m_hrFillReady; // HR fill canvas init flag
bool m_hrBorderReady; // HR border canvas init flag
int m_hrFillW; // HR fill canvas width
int m_hrFillH; // HR fill canvas height
int m_hrBorderW; // HR border canvas width
int m_hrBorderH; // HR border canvas height
CAiCanvasFast m_hrFill; // Cached HR fill canvas
CAiCanvasFast m_hrBorder; // Cached HR border canvas
CAiCanvasPrimitives();
bool EnsureHrFill(int needW, int needH);
bool EnsureHrBorder(int needW, int needH);
void BlendPixelSet(CCanvas &canvas, int x, int y, uint sourceARGB);
void DownsampleCanvas(CCanvas &dst, CCanvas &src, int factor);
void FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY);
void FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb);
void FillSelectiveRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb,
bool rTL, bool rTR, bool rBL, bool rBR);
void FillTriangleHR(CCanvas &canvas, int x0, int y0, int x1, int y1, int x2, int y2, uint argb);
void FillQuadrilateralBorder(CCanvas &canvas, double &vx[], double &vy[], uint argb);
void DrawBorderEdge(CCanvas &canvas, double x0, double y0, double x1, double y1, int thickness, uint argb);
bool IsAngleBetween(double angle, double startAngle, double endAngle);
void DrawCornerArc(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb, double startAngle, double endAngle);
void DrawSelectiveRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, int thickness,
bool rTL, bool rTR, bool rBL, bool rBR);
void FillCircleAA(CCanvas &canvas, int cx, int cy, int radius, uint argb);
void DrawCircleBorderAA(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb);
void DrawRoundRectBorderObStyle(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb,
bool drawTop, bool drawLeft, bool drawRight, bool drawBottom,
bool arcTL, bool arcTR, bool arcBL, bool arcBR);
void FillRoundRectSharp(CCanvas &target, int x, int y, int w, int h, int radius, uint argb, int factor = 4);
void DrawRoundRectBorderSharp(CCanvas &target, int x, int y, int w, int h, int radius, int thickness, uint argb, int factor = 4);
};
//+------------------------------------------------------------------+
//| Construct primitives helper with default state |
//+------------------------------------------------------------------+
CAiCanvasPrimitives::CAiCanvasPrimitives()
{
//--- Initialize cached HR canvas state
m_hrFillReady = false;
m_hrBorderReady = false;
m_hrFillW = 0; m_hrFillH = 0;
m_hrBorderW = 0; m_hrBorderH = 0;
}
//+------------------------------------------------------------------+
//| Blend a source pixel onto canvas (member version) |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::BlendPixelSet(CCanvas &canvas, int x, int y, uint src)
{
//--- Bail outside canvas bounds
if(x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return;
//--- Decompose source and destination components
uint dst = canvas.PixelGet(x, y);
double sA = ((src >> 24) & 0xFF) / 255.0, sR = ((src >> 16) & 0xFF) / 255.0;
double sG = ((src >> 8) & 0xFF) / 255.0, sB = ( src & 0xFF) / 255.0;
double dA = ((dst >> 24) & 0xFF) / 255.0, dR = ((dst >> 16) & 0xFF) / 255.0;
double dG = ((dst >> 8) & 0xFF) / 255.0, dB = ( dst & 0xFF) / 255.0;
//--- Compute output alpha
double oA = sA + dA * (1.0 - sA);
if(oA == 0.0) { canvas.PixelSet(x, y, 0); return; }
//--- Compose blended pixel
canvas.PixelSet(x, y,
((uint)(uchar)(oA * 255 + 0.5) << 24) |
((uint)(uchar)((sR * sA + dR * dA * (1.0 - sA)) / oA * 255 + 0.5) << 16) |
((uint)(uchar)((sG * sA + dG * dA * (1.0 - sA)) / oA * 255 + 0.5) << 8) |
(uint)(uchar)((sB * sA + dB * dA * (1.0 - sA)) / oA * 255 + 0.5));
}
//+------------------------------------------------------------------+
//| Lazily create or grow HR fill canvas |
//+------------------------------------------------------------------+
bool CAiCanvasPrimitives::EnsureHrFill(int needW, int needH)
{
//--- Bail on invalid dimensions
if(needW <= 0 || needH <= 0) return false;
//--- No-op if existing canvas is large enough
if(m_hrFillReady && needW <= m_hrFillW && needH <= m_hrFillH) return true;
//--- Compute new size taking max of existing and requested
const int w = MathMax(needW, m_hrFillW);
const int h = MathMax(needH, m_hrFillH);
//--- Destroy old canvas before recreating
if(m_hrFillReady)
{
m_hrFill.Destroy();
m_hrFillReady = false;
}
//--- Create new bitmap
if(!m_hrFill.CreateBitmap("AiPrimHrFill", 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
m_hrFillW = w; m_hrFillH = h;
m_hrFillReady = true;
return true;
}
//+------------------------------------------------------------------+
//| Lazily create or grow HR border canvas |
//+------------------------------------------------------------------+
bool CAiCanvasPrimitives::EnsureHrBorder(int needW, int needH)
{
//--- Bail on invalid dimensions
if(needW <= 0 || needH <= 0) return false;
//--- No-op if existing canvas is large enough
if(m_hrBorderReady && needW <= m_hrBorderW && needH <= m_hrBorderH) return true;
//--- Compute new size taking max of existing and requested
const int w = MathMax(needW, m_hrBorderW);
const int h = MathMax(needH, m_hrBorderH);
//--- Destroy old canvas before recreating
if(m_hrBorderReady)
{
m_hrBorder.Destroy();
m_hrBorderReady = false;
}
//--- Create new bitmap
if(!m_hrBorder.CreateBitmap("AiPrimHrBorder", 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
m_hrBorderW = w; m_hrBorderH = h;
m_hrBorderReady = true;
return true;
}
//+------------------------------------------------------------------+
//| Downsample source canvas into destination using box filter |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::DownsampleCanvas(CCanvas &dst, CCanvas &src, int factor)
{
//--- Compute destination dimensions and supersample area
int dW = dst.Width(), dH = dst.Height(), ss2 = factor * factor;
//--- Walk destination pixels
for(int py = 0; py < dH; py++)
for(int px = 0; px < dW; px++)
{
//--- Average source pixels in the factor x factor block
double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0;
for(int dy = 0; dy < factor; dy++)
for(int dx = 0; dx < factor; dx++)
{
int sx = px * factor + dx, sy = py * factor + dy;
if(sx >= src.Width() || sy >= src.Height()) continue;
uint p = src.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF);
sA += a;
if(a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; }
}
//--- Skip transparent destination pixels
uchar fa = (uchar)(sA / ss2);
if(fa == 0 || wc == 0) { dst.PixelSet(px, py, 0); continue; }
//--- Compose averaged output pixel
dst.PixelSet(px, py, ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) |
((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc));
}
}
//+------------------------------------------------------------------+
//| Fill one quadrant of a circle with anti-aliased edge |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY)
{
//--- Setup geometry and supersampling
double rd = (double)radius;
uchar bA = (uchar)((argb >> 24) & 0xFF);
uint rgb = argb & 0x00FFFFFF;
int sub = 4; double step = 1.0 / sub; int subSq = sub * sub;
//--- Walk pixels in quadrant bounding box
for(int dy = -(radius + 1); dy <= (radius + 1); dy++)
for(int dx = -(radius + 1); dx <= (radius + 1); dx++)
{
//--- Skip pixels outside this quadrant
bool inQ = ((signX > 0) ? (dx >= 0) : (dx <= 0)) && ((signY > 0) ? (dy >= 0) : (dy <= 0));
if(!inQ) continue;
//--- Skip pixels outside circle
double dist = MathSqrt((double)(dx * dx + dy * dy));
if(dist > rd + 1.0) continue;
//--- Solid fill for clearly-inside pixels
if(dist <= rd - 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; }
//--- Supersample edge pixels
int inside = 0;
for(int sy = 0; sy < sub; sy++)
for(int sx = 0; sx < sub; sx++)
{
double sdx = (double)dx - 0.5 + (sx + 0.5) * step;
double sdy = (double)dy - 0.5 + (sy + 0.5) * step;
if(sdx * sdx + sdy * sdy <= rd * rd) inside++;
}
if(inside == 0) continue;
BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb);
}
}
//+------------------------------------------------------------------+
//| Fill rounded rectangle with anti-aliased corners |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb)
{
//--- Clamp radius and fast-path zero-radius
radius = MathMin(radius, MathMin(w / 2, h / 2));
if(radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; }
//--- Fill three central rectangles around corners
canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb);
canvas.FillRectangle(x, y + radius, x + radius - 1, y + h - radius - 1, argb);
canvas.FillRectangle(x + w - radius, y + radius, x + w - 1, y + h - radius - 1, argb);
//--- Fill four corner quadrants with anti-aliasing
FillCornerQuadrantHR(canvas, x + radius, y + radius, radius, argb, -1, -1);
FillCornerQuadrantHR(canvas, x + w - radius, y + radius, radius, argb, 1, -1);
FillCornerQuadrantHR(canvas, x + radius, y + h - radius, radius, argb, -1, 1);
FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb, 1, 1);
}
//+------------------------------------------------------------------+
//| Fill rounded rectangle with selective corner rounding |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::FillSelectiveRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb,
bool rTL, bool rTR, bool rBL, bool rBR)
{
//--- Clamp radius and fast-path zero-radius
radius = MathMin(radius, MathMin(w / 2, h / 2));
if(radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; }
//--- Fill central horizontal band
canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb);
//--- Fill side rectangles accounting for corner rounding flags
canvas.FillRectangle(x, y + (rTL ? radius : 0), x + radius - 1, y + h - 1 - (rBL ? radius : 0), argb);
canvas.FillRectangle(x + w - radius, y + (rTR ? radius : 0), x + w - 1, y + h - 1 - (rBR ? radius : 0), argb);
//--- Top-left corner
if(rTL) FillCornerQuadrantHR(canvas, x + radius, y + radius, radius, argb, -1, -1);
else canvas.FillRectangle(x, y, x + radius - 1, y + radius - 1, argb);
//--- Top-right corner
if(rTR) FillCornerQuadrantHR(canvas, x + w - radius, y + radius, radius, argb, 1, -1);
else canvas.FillRectangle(x + w - radius, y, x + w - 1, y + radius - 1, argb);
//--- Bottom-left corner
if(rBL) FillCornerQuadrantHR(canvas, x + radius, y + h - radius, radius, argb, -1, 1);
else canvas.FillRectangle(x, y + h - radius, x + radius - 1, y + h - 1, argb);
//--- Bottom-right corner
if(rBR) FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb, 1, 1);
else canvas.FillRectangle(x + w - radius, y + h - radius, x + w - 1, y + h - 1, argb);
}
//+------------------------------------------------------------------+
//| Fill triangle using scanline rasterization |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::FillTriangleHR(CCanvas &canvas, int x0, int y0, int x1, int y1, int x2, int y2, uint argb)
{
//--- Pack vertices into arrays
double vx[3] = { (double)x0, (double)x1, (double)x2 };
double vy[3] = { (double)y0, (double)y1, (double)y2 };
//--- Compute Y range
double minY = vy[0], maxY = vy[0];
for(int i = 1; i < 3; i++) { if(vy[i] < minY) minY = vy[i]; if(vy[i] > maxY) maxY = vy[i]; }
//--- Walk scanlines
for(int scanY = (int)MathCeil(minY); scanY <= (int)MathFloor(maxY); scanY++)
{
//--- Compute edge intersections at this Y
double cy = (double)scanY + 0.5; double xi[6]; int nc = 0;
for(int i = 0; i < 3; i++)
{
int ni = (i + 1) % 3;
double eMin = (vy[i] < vy[ni]) ? vy[i] : vy[ni], eMax = (vy[i] > vy[ni]) ? vy[i] : vy[ni];
if(cy < eMin || cy > eMax || MathAbs(vy[ni] - vy[i]) < 1e-12) continue;
double t = (cy - vy[i]) / (vy[ni] - vy[i]);
if(t < 0.0 || t > 1.0) continue;
xi[nc++] = vx[i] + t * (vx[ni] - vx[i]);
}
//--- Sort intersections by X
for(int a = 0; a < nc - 1; a++)
for(int b = a + 1; b < nc; b++)
if(xi[a] > xi[b]) { double tmp = xi[a]; xi[a] = xi[b]; xi[b] = tmp; }
//--- Fill spans between intersection pairs
for(int p = 0; p + 1 < nc; p += 2)
for(int fx = (int)MathCeil(xi[p]); fx <= (int)MathFloor(xi[p + 1]); fx++)
canvas.PixelSet(fx, scanY, argb);
}
}
//+------------------------------------------------------------------+
//| Fill quadrilateral border using scanline rasterization |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::FillQuadrilateralBorder(CCanvas &canvas, double &vx[], double &vy[], uint argb)
{
//--- Compute Y range
double minY = vy[0], maxY = vy[0];
for(int i = 1; i < 4; i++) { if(vy[i] < minY) minY = vy[i]; if(vy[i] > maxY) maxY = vy[i]; }
//--- Walk scanlines
for(int scanY = (int)MathCeil(minY); scanY <= (int)MathCeil(maxY) - 1; scanY++)
{
//--- Compute edge intersections
double cy = (double)scanY + 0.5; double xi[8]; int nc = 0;
for(int i = 0; i < 4; i++)
{
int ni = (i + 1) % 4;
double eMin = (vy[i] < vy[ni]) ? vy[i] : vy[ni], eMax = (vy[i] > vy[ni]) ? vy[i] : vy[ni];
if(cy < eMin || cy > eMax || MathAbs(vy[ni] - vy[i]) < 1e-12) continue;
double t = (cy - vy[i]) / (vy[ni] - vy[i]);
if(t < 0.0 || t > 1.0) continue;
xi[nc++] = vx[i] + t * (vx[ni] - vx[i]);
}
//--- Sort intersections
for(int a = 0; a < nc - 1; a++)
for(int b = a + 1; b < nc; b++)
if(xi[a] > xi[b]) { double tmp = xi[a]; xi[a] = xi[b]; xi[b] = tmp; }
//--- Fill spans
for(int p = 0; p + 1 < nc; p += 2)
for(int fx = (int)MathCeil(xi[p]); fx <= (int)MathCeil(xi[p + 1]) - 1; fx++)
canvas.PixelSet(fx, scanY, argb);
}
}
//+------------------------------------------------------------------+
//| Draw thick straight border edge as rotated rectangle |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::DrawBorderEdge(CCanvas &canvas, double x0, double y0, double x1, double y1, int thickness, uint argb)
{
//--- Compute edge length and skip degenerate edges
double dx = x1 - x0, dy = y1 - y0, len = MathSqrt(dx * dx + dy * dy);
if(len < 1e-6) return;
//--- Compute normal and tangent unit vectors
double px = -dy / len, py = dx / len, ex = dx / len, ey = dy / len;
double ht = thickness / 2.0, ext = 0.23 * thickness;
//--- Extend endpoints slightly to overlap corners cleanly
double sx = x0 - ex * ext, sy = y0 - ey * ext, ex2 = x1 + ex * ext, ey2 = y1 + ey * ext;
//--- Build rectangular quad and fill it
double tvx[4] = { sx - px*ht, sx + px*ht, ex2 + px*ht, ex2 - px*ht };
double tvy[4] = { sy - py*ht, sy + py*ht, ey2 + py*ht, ey2 - py*ht };
FillQuadrilateralBorder(canvas, tvx, tvy, argb);
}
//+------------------------------------------------------------------+
//| Test if angle is within a circular arc range |
//+------------------------------------------------------------------+
bool CAiCanvasPrimitives::IsAngleBetween(double angle, double start, double end)
{
//--- Normalize angles to [0, 2*PI)
double tp = 2.0 * M_PI;
angle = MathMod(angle + tp, tp); start = MathMod(start + tp, tp); end = MathMod(end + tp, tp);
//--- Test against sweep
return MathMod(angle - start + tp, tp) <= MathMod(end - start + tp, tp);
}
//+------------------------------------------------------------------+
//| Draw anti-aliased corner arc as ring segment |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::DrawCornerArc(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb, double startAngle, double endAngle)
{
//--- Setup ring radii and supersampling
double oR = (double)radius, iR = MathMax(0.0, (double)radius - thickness);
uchar bA = (uchar)((argb >> 24) & 0xFF); uint rgb = argb & 0x00FFFFFF;
int sub = 4; double step = 1.0 / sub; int subSq = sub * sub, pr = (int)(oR + 2.0);
//--- Walk pixels in bounding box
for(int dy = -pr; dy <= pr; dy++)
for(int dx = -pr; dx <= pr; dx++)
{
//--- Skip pixels outside ring radii
double dist = MathSqrt((double)(dx * dx + dy * dy));
if(dist > oR + 1.0 || dist < iR - 1.0) continue;
//--- Skip pixels outside angle sweep
if(!IsAngleBetween(MathArctan2((double)dy, (double)dx), startAngle, endAngle)) continue;
//--- Solid fill for clearly inside ring
if(dist <= oR - 1.0 && dist >= iR + 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; }
//--- Supersample edge pixels
int inside = 0;
for(int sy = 0; sy < sub; sy++)
for(int sx = 0; sx < sub; sx++)
{
double sdx = (double)dx - 0.5 + (sx + 0.5) * step, sdy = (double)dy - 0.5 + (sy + 0.5) * step;
double sd = MathSqrt(sdx * sdx + sdy * sdy);
if(sd >= iR && sd <= oR && IsAngleBetween(MathArctan2(sdy, sdx), startAngle, endAngle)) inside++;
}
if(inside == 0) continue;
if(inside >= subSq) canvas.PixelSet(cx + dx, cy + dy, argb);
else BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb);
}
}
//+------------------------------------------------------------------+
//| Draw rounded rectangle border with selective corners and edges |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::DrawSelectiveRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, int thickness,
bool rTL, bool rTR, bool rBL, bool rBR)
{
//--- Bail on zero thickness
if(thickness <= 0) return;
//--- Clamp radius and pick per-corner radii
radius = MathMin(radius, MathMin(w / 2, h / 2));
int tlR = rTL ? radius : 0, trR = rTR ? radius : 0, blR = rBL ? radius : 0, brR = rBR ? radius : 0, h2 = thickness / 2;
//--- Draw four straight edges
DrawBorderEdge(canvas, x + tlR, y + h2, x + w - trR, y + h2, thickness, argb);
if(rTR || rBR) DrawBorderEdge(canvas, x + w - h2, y + trR, x + w - h2, y + h - brR, thickness, argb);
DrawBorderEdge(canvas, x + w - brR, y + h - h2, x + blR, y + h - h2, thickness, argb);
if(rTL || rBL) DrawBorderEdge(canvas, x + h2, y + h - blR, x + h2, y + tlR, thickness, argb);
//--- Draw four corner arcs
if(rTL) DrawCornerArc(canvas, x + radius, y + radius, radius, thickness, argb, M_PI, M_PI * 1.5);
if(rTR) DrawCornerArc(canvas, x + w - radius, y + radius, radius, thickness, argb, M_PI * 1.5, M_PI * 2.0);
if(rBL) DrawCornerArc(canvas, x + radius, y + h - radius, radius, thickness, argb, M_PI * 0.5, M_PI);
if(rBR) DrawCornerArc(canvas, x + w - radius, y + h - radius, radius, thickness, argb, 0.0, M_PI * 0.5);
}
//+------------------------------------------------------------------+
//| Fill anti-aliased filled circle |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::FillCircleAA(CCanvas &canvas, int cx, int cy, int radius, uint argb)
{
//--- Setup geometry and supersampling
double rd = (double)radius; uchar bA = (uchar)((argb >> 24) & 0xFF); uint rgb = argb & 0x00FFFFFF;
int sub = 4; double step = 1.0 / sub; int subSq = sub * sub, pr = radius + 1;
//--- Walk pixels in bounding box
for(int dy = -pr; dy <= pr; dy++)
for(int dx = -pr; dx <= pr; dx++)
{
double dist = MathSqrt((double)(dx * dx + dy * dy));
if(dist > rd + 1.0) continue;
//--- Solid fill for clearly inside
if(dist <= rd - 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; }
//--- Supersample edge
int inside = 0;
for(int sy = 0; sy < sub; sy++)
for(int sx = 0; sx < sub; sx++)
{
double sdx = (double)dx - 0.5 + (sx + 0.5) * step, sdy = (double)dy - 0.5 + (sy + 0.5) * step;
if(sdx * sdx + sdy * sdy <= rd * rd) inside++;
}
if(inside == 0) continue;
BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb);
}
}
//+------------------------------------------------------------------+
//| Draw anti-aliased circular border |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::DrawCircleBorderAA(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb)
{
//--- Forward to corner arc covering full circle
DrawCornerArc(canvas, cx, cy, radius, thickness, argb, 0.0, 2.0 * M_PI);
}
//+------------------------------------------------------------------+
//| Draw rounded rectangle border using OB-style line+arc method |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::DrawRoundRectBorderObStyle(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb,
bool drawTop, bool drawLeft, bool drawRight, bool drawBottom,
bool arcTL, bool arcTR, bool arcBL, bool arcBR)
{
//--- Clamp radius
radius = MathMin(radius, MathMin(w / 2, h / 2));
if(radius < 0) radius = 0;
//--- Setup AA parameters
uchar bA = (uchar)((argb >> 24) & 0xFF);
uint rgb = argb & 0x00FFFFFF;
const int sub = 4;
const double step = 1.0 / sub;
const int subSq = sub * sub;
const double rd = (double)radius;
//--- Draw four straight edges
if(drawTop) canvas.Line(x + radius, y, x + w - radius - 1, y, argb);
if(drawBottom) canvas.Line(x + radius, y + h - 1, x + w - radius - 1, y + h - 1, argb);
if(drawLeft) canvas.Line(x, y + radius,x, y + h - radius - 1, argb);
if(drawRight) canvas.Line(x + w - 1, y + radius,x + w - 1, y + h - radius - 1, argb);
//--- Skip arcs for zero-radius
if(radius == 0) return;
//--- Draw selective corner arcs
for(int corner = 0; corner < 4; corner++)
{
bool draw = (corner == 0 && arcTL)
|| (corner == 1 && arcTR)
|| (corner == 2 && arcBL)
|| (corner == 3 && arcBR);
if(!draw) continue;
//--- Compute corner center and quadrant signs
int cx = (corner == 0 || corner == 2) ? (x + radius) : (x + w - 1 - radius);
int cy = (corner == 0 || corner == 1) ? (y + radius) : (y + h - 1 - radius);
int signX = (corner == 0 || corner == 2) ? -1 : 1;
int signY = (corner == 0 || corner == 1) ? -1 : 1;
//--- Walk pixels near arc edge
for(int adyL = 0; adyL <= radius + 1; adyL++)
{
for(int adxL = 0; adxL <= radius + 1; adxL++)
{
//--- Skip pixels outside ring band
double dist = MathSqrt((double)(adxL * adxL + adyL * adyL));
if(dist > rd + 1.0 || dist < rd - 1.0) continue;
//--- Supersample edge coverage
int inside = 0;
for(int sy2 = 0; sy2 < sub; sy2++)
for(int sx2 = 0; sx2 < sub; sx2++)
{
double sdx = (double)adxL - 0.5 + (sx2 + 0.5) * step;
double sdy = (double)adyL - 0.5 + (sy2 + 0.5) * step;
double sd = MathSqrt(sdx * sdx + sdy * sdy);
if(sd >= rd - 0.5 && sd <= rd + 0.5) inside++;
}
if(inside == 0) continue;
//--- Blend with computed coverage
const uint sample = (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb;
const int px = cx + adxL * signX;
const int py = cy + adyL * signY;
BlendPixelSet(canvas, px, py, sample);
}
}
}
}
//+------------------------------------------------------------------+
//| Fill rounded rectangle using HR supersample then downsample |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::FillRoundRectSharp(CCanvas &target, int x, int y, int w, int h, int radius, uint argb, int factor)
{
//--- Bail on degenerate dimensions
if(w <= 0 || h <= 0) return;
//--- Compute HR canvas size and ensure cache fits
const int hrW = w * factor, hrH = h * factor;
if(!EnsureHrFill(hrW, hrH)) return;
//--- Clear working subregion only
m_hrFill.FillRectangle(0, 0, hrW - 1, hrH - 1, 0);
//--- Render to HR canvas at supersample resolution
FillRoundRectHR(m_hrFill, 0, 0, hrW, hrH, radius * factor, argb);
//--- Downsample HR pixels into target
const int ss2 = factor * factor;
const int tW = target.Width(), tH = target.Height();
for(int py = 0; py < h; py++)
{
const int ty = y + py;
if(ty < 0 || ty >= tH) continue;
for(int px = 0; px < w; px++)
{
const int tx = x + px;
if(tx < 0 || tx >= tW) continue;
//--- Average factor x factor block of HR pixels
double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0;
for(int dy = 0; dy < factor; dy++)
for(int dx = 0; dx < factor; dx++)
{
const int sx = px * factor + dx, sy = py * factor + dy;
if(sx >= hrW || sy >= hrH) continue;
const uint p = m_hrFill.PixelGet(sx, sy);
const uchar a = (uchar)((p >> 24) & 0xFF);
sA += a;
if(a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; }
}
//--- Skip transparent
const uchar fa = (uchar)(sA / ss2);
if(fa == 0 || wc == 0) continue;
//--- Blend averaged sample
const uint sample = ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) |
((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc);
BlendPixelSet(target, tx, ty, sample);
}
}
}
//+------------------------------------------------------------------+
//| Draw rounded rectangle border using HR supersample then downsamp |
//+------------------------------------------------------------------+
void CAiCanvasPrimitives::DrawRoundRectBorderSharp(CCanvas &target, int x, int y, int w, int h, int radius, int thickness, uint argb, int factor)
{
//--- Bail on degenerate input
if(w <= 0 || h <= 0 || thickness <= 0) return;
//--- Compute HR canvas size and ensure cache fits
const int hrW = w * factor, hrH = h * factor;
if(!EnsureHrBorder(hrW, hrH)) return;
//--- Clear working subregion
m_hrBorder.FillRectangle(0, 0, hrW - 1, hrH - 1, 0);
//--- Render border to HR canvas
DrawSelectiveRoundRectBorderHR(m_hrBorder, 0, 0, hrW, hrH, radius * factor, argb, thickness * factor, true, true, true, true);
//--- Downsample HR pixels into target
const int ss2 = factor * factor;
const int tW = target.Width(), tH = target.Height();
for(int py = 0; py < h; py++)
{
const int ty = y + py;
if(ty < 0 || ty >= tH) continue;
for(int px = 0; px < w; px++)
{
const int tx = x + px;
if(tx < 0 || tx >= tW) continue;
//--- Average factor x factor block
double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0;
for(int dy = 0; dy < factor; dy++)
for(int dx = 0; dx < factor; dx++)
{
const int sx = px * factor + dx, sy = py * factor + dy;
if(sx >= hrW || sy >= hrH) continue;
const uint p = m_hrBorder.PixelGet(sx, sy);
const uchar a = (uchar)((p >> 24) & 0xFF);
sA += a;
if(a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; }
}
//--- Skip transparent
const uchar fa = (uchar)(sA / ss2);
if(fa == 0 || wc == 0) continue;
//--- Blend averaged sample
const uint sample = ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) |
((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc);
BlendPixelSet(target, tx, ty, sample);
}
}
}
//+------------------------------------------------------------------+
//| Decompose ARGB pixel into separate components |
//+------------------------------------------------------------------+
void AiGetArgb(uint pixel, uchar &alpha, uchar &red, uchar &green, uchar &blue)
{
//--- Extract bytes from packed pixel
alpha = (uchar)((pixel >> 24) & 0xFF);
red = (uchar)((pixel >> 16) & 0xFF);
green = (uchar)((pixel >> 8) & 0xFF);
blue = (uchar)(pixel & 0xFF);
}
//+------------------------------------------------------------------+
//| Compute one bicubic component using 4x4 cubic kernel |
//+------------------------------------------------------------------+
double AiBicubicComp(uchar &components[], double fx, double fy)
{
//--- Compute X-axis cubic weights
double wx[4]; double t = fx;
wx[0] = (-0.5*t*t*t + t*t - 0.5*t);
wx[1] = ( 1.5*t*t*t - 2.5*t*t + 1.0);
wx[2] = (-1.5*t*t*t + 2.0*t*t + 0.5*t);
wx[3] = ( 0.5*t*t*t - 0.5*t*t);
//--- Apply X weights to each row
double yv[4];
for(int j = 0; j < 4; j++)
yv[j] = wx[0]*components[j*4+0] + wx[1]*components[j*4+1] + wx[2]*components[j*4+2] + wx[3]*components[j*4+3];
//--- Compute Y-axis cubic weights
double wy[4]; t = fy;
wy[0] = (-0.5*t*t*t + t*t - 0.5*t);
wy[1] = ( 1.5*t*t*t - 2.5*t*t + 1.0);
wy[2] = (-1.5*t*t*t + 2.0*t*t + 0.5*t);
wy[3] = ( 0.5*t*t*t - 0.5*t*t);
//--- Apply Y weights and clamp to byte range
double r = wy[0]*yv[0] + wy[1]*yv[1] + wy[2]*yv[2] + wy[3]*yv[3];
return MathMax(0.0, MathMin(255.0, r));
}
//+------------------------------------------------------------------+
//| Sample pixel from buffer using bicubic interpolation |
//+------------------------------------------------------------------+
uint AiBicubicInterp(uint &pixels[], int w, int h, double x, double y)
{
//--- Compute integer base and fractional offsets
int x0 = (int)x, y0 = (int)y;
double fx = x - x0, fy = y - y0;
//--- Compute 4x4 neighborhood indices with edge clamping
int xi[4], yi[4];
for(int i = -1; i <= 2; i++)
{
xi[i+1] = (int)MathMin(MathMax(x0+i, 0), w-1);
yi[i+1] = (int)MathMin(MathMax(y0+i, 0), h-1);
}
//--- Sample 16 neighborhood pixels
uint nb[16];
for(int j = 0; j < 4; j++)
for(int i = 0; i < 4; i++)
nb[j*4+i] = pixels[yi[j]*w + xi[i]];
//--- Decompose pixels into channel arrays
uchar aC[16], rC[16], gC[16], bC[16];
for(int i = 0; i < 16; i++)
AiGetArgb(nb[i], aC[i], rC[i], gC[i], bC[i]);
//--- Compute interpolated channels
uchar oA = (uchar)AiBicubicComp(aC, fx, fy);
uchar oR = (uchar)AiBicubicComp(rC, fx, fy);
uchar oG = (uchar)AiBicubicComp(gC, fx, fy);
uchar oB = (uchar)AiBicubicComp(bC, fx, fy);
//--- Pack into ARGB
return (((uint)oA) << 24) | (((uint)oR) << 16) | (((uint)oG) << 8) | ((uint)oB);
}
//+------------------------------------------------------------------+
//| Scale pixel buffer to new dimensions using bicubic interpolation |
//+------------------------------------------------------------------+
void AiScaleImage(uint &pixels[], int origW, int origH, int newW, int newH)
{
//--- Allocate scaled buffer
uint scaled[];
ArrayResize(scaled, newW * newH);
//--- Walk destination pixels and sample from source
for(int y = 0; y < newH; y++)
for(int x = 0; x < newW; x++)
{
double ox = (double)x * origW / newW;
double oy = (double)y * origH / newH;
scaled[y*newW + x] = AiBicubicInterp(pixels, origW, origH, ox, oy);
}
//--- Replace input buffer with scaled pixels
ArrayResize(pixels, newW * newH);
ArrayCopy(pixels, scaled);
}
//+------------------------------------------------------------------+
//| Stamp a rectangular pixel buffer onto a canvas with blending |
//+------------------------------------------------------------------+
void AiStampPixels(CCanvas &dst, int x, int y, uint &pixels[], int pw, int ph)
{
//--- Read destination dimensions
int dw = dst.Width(), dh = dst.Height();
//--- Walk source pixels
for(int py = 0; py < ph; py++)
{
int dstY = y + py;
if(dstY < 0 || dstY >= dh) continue;
for(int px = 0; px < pw; px++)
{
int dstX = x + px;
if(dstX < 0 || dstX >= dw) continue;
//--- Skip transparent source pixels
uint src = pixels[py * pw + px];
if((src >> 24) == 0) continue;
//--- Blend onto destination
AiBlendPixel(dst, dstX, dstY, src);
}
}
}
//+------------------------------------------------------------------+
//| Stamp anti-aliased text on canvas using dual-buffer technique |
//+------------------------------------------------------------------+
void AiStampTextAA(CCanvas &dst, int x, int y, const string txt,
const string fontName, int fontSize, color textColor)
{
//--- Bail on empty text
if(StringLen(txt) == 0) return;
//--- Set font and measure text
TextSetFont(fontName, -(fontSize * 10));
uint twU = 0, thU = 0;
TextGetSize(txt, twU, thU);
int tw = (int)twU, th = (int)thU;
if(tw <= 0 || th <= 0) return;
//--- Render text on black background buffer
const uint textArgb = ColorToARGB(textColor, 255);
uint bufB[]; ArrayResize(bufB, tw * th);
ArrayFill(bufB, 0, tw * th, 0xFF000000);
TextOut(txt, 0, 0, TA_LEFT | TA_TOP, bufB, tw, th,
textArgb, COLOR_FORMAT_ARGB_NORMALIZE);
//--- Render text on white background buffer
uint bufW[]; ArrayResize(bufW, tw * th);
ArrayFill(bufW, 0, tw * th, 0xFFFFFFFF);
TextOut(txt, 0, 0, TA_LEFT | TA_TOP, bufW, tw, th,
textArgb, COLOR_FORMAT_ARGB_NORMALIZE);
//--- Cache canvas dimensions and source color components
const int cW = dst.Width(), cH = dst.Height();
const uchar srcR = (uchar)( textColor & 0xFF);
const uchar srcG = (uchar)((textColor >> 8) & 0xFF);
const uchar srcB = (uchar)((textColor >> 16) & 0xFF);
//--- Walk text pixels and derive coverage from buffer difference
for(int py = 0; py < th; py++)
{
for(int px = 0; px < tw; px++)
{
//--- Compute alpha from black-vs-white pixel difference
int i = py * tw + px;
int dR = (int)((bufW[i] >> 16) & 0xFF) - (int)((bufB[i] >> 16) & 0xFF);
int dG = (int)((bufW[i] >> 8) & 0xFF) - (int)((bufB[i] >> 8) & 0xFF);
int dB = (int)( bufW[i] & 0xFF) - (int)( bufB[i] & 0xFF);
int a = 255 - (dR + dG + dB) / 3;
if(a <= 0) continue;
if(a > 255) a = 255;
//--- Skip pixels outside canvas
int dstX = x + px, dstY = y + py;
if(dstX < 0 || dstX >= cW || dstY < 0 || dstY >= cH) continue;
//--- Blend text pixel onto destination
uint existing = dst.PixelGet(dstX, dstY);
double sA = (double)a / 255.0;
double dA = ((existing >> 24) & 0xFF) / 255.0;
double oA = sA + dA * (1.0 - sA);
if(oA <= 0.0) continue;
double sRf = srcR / 255.0, sGf = srcG / 255.0, sBf = srcB / 255.0;
double dRf = ((existing >> 16) & 0xFF) / 255.0;
double dGf = ((existing >> 8) & 0xFF) / 255.0;
double dBf = ( existing & 0xFF) / 255.0;
uint outPix = ((uint)(uchar)(oA * 255.0 + 0.5) << 24) |
((uint)(uchar)((sRf*sA + dRf*dA*(1.0-sA)) / oA * 255.0 + 0.5) << 16) |
((uint)(uchar)((sGf*sA + dGf*dA*(1.0-sA)) / oA * 255.0 + 0.5) << 8) |
(uint)(uchar)((sBf*sA + dBf*dA*(1.0-sA)) / oA * 255.0 + 0.5);
dst.PixelSet(dstX, dstY, outPix);
}
}
}
//+------------------------------------------------------------------+
//| Measure pixel width of text in given font |
//+------------------------------------------------------------------+
int AiTextWidth(const string txt, const string fontName, int fontSize)
{
//--- Bail on empty text
if(StringLen(txt) == 0) return 0;
//--- Set font and measure
TextSetFont(fontName, -(fontSize * 10));
uint tw = 0, th = 0;
TextGetSize(txt, tw, th);
return (int)tw;
}
//+------------------------------------------------------------------+
//| Measure standard text height in given font |
//+------------------------------------------------------------------+
int AiTextHeight(const string fontName, int fontSize)
{
//--- Set font and measure ascent/descent reference string
TextSetFont(fontName, -(fontSize * 10));
uint tw = 0, th = 0;
TextGetSize("Ay", tw, th);
return (int)th;
}
//+------------------------------------------------------------------+
//| Fit text to maximum pixel width with ellipsis |
//+------------------------------------------------------------------+
string Ai_FitTextToWidth(const string text, const string fontName, int fontSize, int maxWidthPx)
{
//--- Bail on empty input
if(StringLen(text) == 0) return "";
if(maxWidthPx <= 0) return "";
//--- Fast path - full text fits
const int fullW = AiTextWidth(text, fontName, fontSize);
if(fullW <= maxWidthPx) return text;
//--- Bail if even ellipsis doesn't fit
const string ellipsis = "...";
const int ellipsisW = AiTextWidth(ellipsis, fontName, fontSize);
if(ellipsisW > maxWidthPx) return "";
//--- Binary search longest prefix that fits with ellipsis appended
const int n = StringLen(text);
int lo = 0, hi = n;
while(lo < hi)
{
const int mid = (lo + hi + 1) / 2;
const string trial = StringSubstr(text, 0, mid) + ellipsis;
if(AiTextWidth(trial, fontName, fontSize) <= maxWidthPx)
{
lo = mid;
}
else
{
hi = mid - 1;
}
}
//--- Return ellipsis-only when nothing fits, else prefix + ellipsis
if(lo <= 0) return ellipsis;
return StringSubstr(text, 0, lo) + ellipsis;
}
//+------------------------------------------------------------------+
//| Text Height Cache |
//+------------------------------------------------------------------+
string g_ai_thFonts[]; // Cached font names
int g_ai_thSizes[]; // Cached font sizes
int g_ai_thHeights[]; // Cached text heights
//+------------------------------------------------------------------+
//| Memoized text height lookup |
//+------------------------------------------------------------------+
int AiTextHeightCached(const string fontName, int fontSize)
{
//--- Linear-scan cache for matching entry
const int n = ArraySize(g_ai_thFonts);
for(int i = 0; i < n; i++)
{
if(g_ai_thSizes[i] == fontSize && g_ai_thFonts[i] == fontName)
return g_ai_thHeights[i];
}
//--- Cache miss - compute and store
const int h = AiTextHeight(fontName, fontSize);
ArrayResize(g_ai_thFonts, n + 1);
ArrayResize(g_ai_thSizes, n + 1);
ArrayResize(g_ai_thHeights, n + 1);
g_ai_thFonts[n] = fontName;
g_ai_thSizes[n] = fontSize;
g_ai_thHeights[n] = h;
return h;
}
//+------------------------------------------------------------------+
//| Test if point lies inside rectangle |
//+------------------------------------------------------------------+
bool AiPointInRect(int px, int py, int rx, int ry, int rw, int rh)
{
//--- Inclusive bounds test
return (px >= rx && px <= rx + rw - 1 && py >= ry && py <= ry + rh - 1);
}
//+------------------------------------------------------------------+
//| Draw small chevron arrow at center point |
//+------------------------------------------------------------------+
void AiDrawChevron(CCanvas &canvas, int cx, int cy, bool pointUp, uint argb)
{
//--- Draw two diagonal strokes converging up or down
if(pointUp)
{
AiThickLineAA(canvas, cx - 4, cy + 2, cx, cy - 2, 2, argb);
AiThickLineAA(canvas, cx, cy - 2, cx + 4, cy + 2, 2, argb);
}
else
{
AiThickLineAA(canvas, cx - 4, cy - 2, cx, cy + 2, 2, argb);
AiThickLineAA(canvas, cx, cy + 2, cx + 4, cy - 2, 2, argb);
}
}
//+------------------------------------------------------------------+
//| Stroke an arbitrary anti-aliased arc segment |
//+------------------------------------------------------------------+
void AiStrokeArcAA(CCanvas &canvas, int cx, int cy, double radius, double thickness,
double angStart, double angEnd, uint argb)
{
//--- Normalize angles to [0, 2*PI)
while(angStart < 0) angStart += 2.0 * M_PI;
while(angStart >= 2.0*M_PI) angStart -= 2.0 * M_PI;
while(angEnd < 0) angEnd += 2.0 * M_PI;
while(angEnd >= 2.0*M_PI) angEnd -= 2.0 * M_PI;
//--- Setup AA parameters
const int sub = 4;
uchar bA = (uchar)((argb >> 24) & 0xFF);
uint rgb = argb & 0x00FFFFFF;
const int pr = (int)MathCeil(radius) + 1;
const double half = thickness / 2.0;
//--- Walk pixels in bounding box
for(int dy = -pr; dy <= pr; dy++)
{
for(int dx = -pr; dx <= pr; dx++)
{
//--- Skip pixels outside ring band
double dist = MathSqrt((double)(dx * dx + dy * dy));
if(dist > radius + half + 1.0) continue;
if(dist < radius - half - 1.0) continue;
//--- Compute pixel angle and skip outside sweep
double ang = MathArctan2((double)dy, (double)dx);
if(ang < 0) ang += 2.0 * M_PI;
bool inSweep = (angStart <= angEnd)
? (ang >= angStart && ang <= angEnd)
: (ang >= angStart || ang <= angEnd);
if(!inSweep) continue;
//--- Supersample edge coverage
int inside = 0;
for(int sy = 0; sy < sub; sy++)
for(int sx = 0; sx < sub; sx++)
{
double sdx = (double)dx - 0.5 + ((double)sx + 0.5) / sub;
double sdy = (double)dy - 0.5 + ((double)sy + 0.5) / sub;
double sd = MathSqrt(sdx * sdx + sdy * sdy);
if(sd >= radius - half && sd <= radius + half) inside++;
}
if(inside == 0) continue;
//--- Blend with computed coverage
uint sample = (((uint)(uchar)((int)bA * inside / (sub * sub))) << 24) | rgb;
AiBlendPixel(canvas, cx + dx, cy + dy, sample);
}
}
}
//+------------------------------------------------------------------+
//| Plot single icon pixel with coverage-based alpha |
//+------------------------------------------------------------------+
void AiIconAAPlot(CCanvas &canvas, int px, int py, double coverage, uint argb)
{
//--- Skip near-zero coverage
if(coverage <= 0.01) return;
//--- Bail outside canvas
const int cW = canvas.Width();
const int cH = canvas.Height();
if(px < 0 || px >= cW || py < 0 || py >= cH) return;
//--- Compute alpha from coverage
uchar a = (uchar)(255.0 * coverage);
if(a == 0) return;
//--- Blend onto destination
uint ex = canvas.PixelGet(px, py);
double sA = a / 255.0;
double dA = ((ex >> 24) & 0xFF) / 255.0;
double oA = sA + dA * (1.0 - sA);
if(oA <= 0.0) return;
double sR = ((argb >> 16) & 0xFF) / 255.0;
double sG = ((argb >> 8) & 0xFF) / 255.0;
double sB = ( argb & 0xFF) / 255.0;
double dR = ((ex >> 16) & 0xFF) / 255.0;
double dG = ((ex >> 8) & 0xFF) / 255.0;
double dB = ( ex & 0xFF) / 255.0;
uint ob = ((uint)(uchar)(oA * 255.0 + 0.5) << 24) |
((uint)(uchar)((sR*sA + dR*dA*(1.0-sA)) / oA * 255.0 + 0.5) << 16) |
((uint)(uchar)((sG*sA + dG*dA*(1.0-sA)) / oA * 255.0 + 0.5) << 8) |
(uint)(uchar)((sB*sA + dB*dA*(1.0-sA)) / oA * 255.0 + 0.5);
canvas.PixelSet(px, py, ob);
}
//+------------------------------------------------------------------+
//| Draw thin anti-aliased icon line using Wu algorithm |
//+------------------------------------------------------------------+
void AiIconLine(CCanvas &canvas, double x0, double y0, double x1, double y1, uint argb)
{
//--- Determine steep or shallow slope
double dxL = x1 - x0, dyL = y1 - y0;
bool steep = MathAbs(dyL) > MathAbs(dxL);
//--- Steep slope - iterate Y axis
if(steep)
{
if(y0 > y1) { double t; t = x0; x0 = x1; x1 = t; t = y0; y0 = y1; y1 = t; }
double grad = (y1 == y0) ? 0.0 : (x1 - x0) / (y1 - y0);
int iy0 = (int)MathRound(y0), iy1 = (int)MathRound(y1);
double xf = x0 + grad * (iy0 - y0);
for(int iy = iy0; iy <= iy1; iy++)
{
int ix = (int)MathFloor(xf);
double frac = xf - ix;
AiIconAAPlot(canvas, ix, iy, 1.0 - frac, argb);
AiIconAAPlot(canvas, ix + 1, iy, frac, argb);
xf += grad;
}
}
//--- Shallow slope - iterate X axis
else
{
if(x0 > x1) { double t; t = x0; x0 = x1; x1 = t; t = y0; y0 = y1; y1 = t; }
double grad = (x1 == x0) ? 0.0 : (y1 - y0) / (x1 - x0);
int ix0 = (int)MathRound(x0), ix1 = (int)MathRound(x1);
double yf = y0 + grad * (ix0 - x0);
for(int ix = ix0; ix <= ix1; ix++)
{
int iy = (int)MathFloor(yf);
double frac = yf - iy;
AiIconAAPlot(canvas, ix, iy, 1.0 - frac, argb);
AiIconAAPlot(canvas, ix, iy + 1, frac, argb);
yf += grad;
}
}
}
//+------------------------------------------------------------------+
//| Draw 3-candle chart icon |
//+------------------------------------------------------------------+
void AiDrawIconChart(CCanvas &canvas, int x, int y, int size, uint argbDefault)
{
//--- Compute inner drawing rect
const int gL = x + 2, gR = x + size - 2;
const int gT = y + 2, gB = y + size - 2;
const int W = gR - gL;
const int H = gB - gT;
//--- Compute candle width and gaps
int cw = 3;
int totalGap = W - 3 * cw;
if(totalGap < 4)
{
cw = MathMax(1, (W - 4) / 3);
if(cw % 2 == 0) cw = MathMax(1, cw - 1);
totalGap = W - 3 * cw;
}
const int gapEach = totalGap / 4;
//--- Compute candle X positions
int posX[3];
posX[0] = gL + gapEach;
posX[1] = posX[0] + cw + gapEach;
posX[2] = posX[1] + cw + gapEach;
//--- Setup body and wick Y bounds for each candle
int bodyT[3], bodyB[3], wickT[3], wickB[3];
bodyT[0] = gT + (int)(H * 0.45); bodyB[0] = gT + (int)(H * 0.85);
bodyT[1] = gT + (int)(H * 0.10); bodyB[1] = gT + (int)(H * 0.55);
bodyT[2] = gT + (int)(H * 0.30); bodyB[2] = gT + (int)(H * 0.70);
wickT[0] = gT + (int)(H * 0.30); wickB[0] = gT + (int)(H * 0.95);
wickT[1] = gT + (int)(H * 0.00); wickB[1] = gT + (int)(H * 0.70);
wickT[2] = gT + (int)(H * 0.15); wickB[2] = gT + (int)(H * 0.85);
//--- Setup colors per candle
uint colors[3];
colors[0] = ColorToARGB(C'46,160,67', 235);
colors[1] = ColorToARGB(C'207,55,55', 235);
colors[2] = ColorToARGB(C'58,130,246', 235);
//--- Draw each candle's wick and body
for(int i = 0; i < 3; i++)
{
const int bL = posX[i];
const int bR = posX[i] + cw - 1;
const int wickX = bL + cw / 2;
const uint col = colors[i];
for(int yy = wickT[i]; yy <= wickB[i]; yy++)
AiBlendPixel(canvas, wickX, yy, col);
for(int yy = bodyT[i]; yy <= bodyB[i]; yy++)
for(int xx = bL; xx <= bR; xx++)
AiBlendPixel(canvas, xx, yy, col);
}
}
//+------------------------------------------------------------------+
//| Draw signal/crosshair icon |
//+------------------------------------------------------------------+
void AiDrawIconSignal(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute icon bounds and center
const int L = x;
const int R = x + size - 1;
const int T = y;
const int B = y + size - 1;
const int cx = (L + R) / 2;
const int cy = (T + B) / 2;
//--- Draw center ring
const double ringR = 2.2;
canvas.CircleWu(cx, cy, ringR, argb);
//--- Draw four tick marks
const double tickGap = 2.0;
const double tickStart = ringR + tickGap;
AiIconLine(canvas, cx + tickStart, cy, R, cy, argb);
AiIconLine(canvas, L, cy, cx - tickStart, cy, argb);
AiIconLine(canvas, cx, T, cx, cy - tickStart, argb);
AiIconLine(canvas, cx, cy + tickStart, cx, B, argb);
}
//+------------------------------------------------------------------+
//| Draw regenerate (refresh arrow) icon |
//+------------------------------------------------------------------+
void AiDrawIconRegen(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute center and radius
const int cx = x + size / 2;
const int cy = y + size / 2;
const double R = (double)((size - 4) / 2);
//--- Draw 3/4 arc
AiStrokeArcAA(canvas, cx, cy, R, 1.5, 0.0, 3.0 * M_PI / 2.0, argb);
//--- Define arrowhead triangle vertices
const int v0x = cx, v0y = cy - (int)R - 1;
const int v1x = cx + 3, v1y = cy - (int)R + 1;
const int v2x = cx, v2y = cy - (int)R + 3;
//--- Draw triangle outline
AiIconLine(canvas, (double)v0x, (double)v0y, (double)v1x, (double)v1y, argb);
AiIconLine(canvas, (double)v1x, (double)v1y, (double)v2x, (double)v2y, argb);
AiIconLine(canvas, (double)v2x, (double)v2y, (double)v0x, (double)v0y, argb);
//--- Fill triangle interior using scanline approach
const int triMinY = MathMin(MathMin(v0y, v1y), v2y);
const int triMaxY = MathMax(MathMax(v0y, v1y), v2y);
for(int yy = triMinY; yy <= triMaxY; yy++)
{
double xLeft = 9999, xRight = -9999;
int ex0[3]; ex0[0] = v0x; ex0[1] = v1x; ex0[2] = v2x;
int ey0[3]; ey0[0] = v0y; ey0[1] = v1y; ey0[2] = v2y;
int ex1[3]; ex1[0] = v1x; ex1[1] = v2x; ex1[2] = v0x;
int ey1[3]; ey1[0] = v1y; ey1[1] = v2y; ey1[2] = v0y;
for(int e = 0; e < 3; e++)
{
int y0e = ey0[e], y1e = ey1[e];
if(y0e == y1e) continue;
int yLo = MathMin(y0e, y1e), yHi = MathMax(y0e, y1e);
if(yy < yLo || yy > yHi) continue;
double t = (double)(yy - y0e) / (double)(y1e - y0e);
double xi = ex0[e] + t * (ex1[e] - ex0[e]);
if(xi < xLeft) xLeft = xi;
if(xi > xRight) xRight = xi;
}
if(xLeft > xRight) continue;
for(int xx = (int)MathRound(xLeft); xx <= (int)MathRound(xRight); xx++)
AiBlendPixel(canvas, xx, yy, argb);
}
}
//+------------------------------------------------------------------+
//| Draw export (down arrow into tray) icon |
//+------------------------------------------------------------------+
void AiDrawIconExport(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute inner drawing rect and center
const int gL = x + 2, gR = x + size - 2;
const int gT = y + 2, gB = y + size - 2;
const int cx = (gL + gR) / 2;
//--- Draw tray (open box at bottom)
const int trayT = gT + (gB - gT) * 11 / 16;
AiIconLine(canvas, (double)gL, (double)trayT, (double)gL, (double)gB, argb);
AiIconLine(canvas, (double)gR, (double)trayT, (double)gR, (double)gB, argb);
AiIconLine(canvas, (double)gL, (double)gB, (double)gR, (double)gB, argb);
//--- Draw downward shaft
const int shaftTopY = gT;
const int shaftBotY = gB - 3;
AiIconLine(canvas, (double)cx, (double)shaftTopY, (double)cx, (double)shaftBotY, argb);
//--- Draw arrowhead
AiIconLine(canvas, (double)(cx - 3), (double)(shaftBotY - 3), (double)cx, (double)shaftBotY, argb);
AiIconLine(canvas, (double)cx, (double)shaftBotY, (double)(cx + 3), (double)(shaftBotY - 3), argb);
}
//+------------------------------------------------------------------+
//| Draw send (paper plane) icon |
//+------------------------------------------------------------------+
void AiDrawIconSend(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute inner drawing rect and center column
const int gL = x + 2, gR = x + size - 2;
const int gT = y + 2, gB = y + size - 2;
const int cx = (gL + gR) / 2;
//--- Draw vertical shaft (2 columns wide)
const int shaftTopY = gT + 1;
const int shaftBotY = gB;
AiIconLine(canvas, (double)cx, (double)shaftTopY, (double)cx, (double)shaftBotY, argb);
AiIconLine(canvas, (double)(cx + 1), (double)shaftTopY, (double)(cx + 1), (double)shaftBotY, argb);
//--- Draw arrowhead wings
const int wingDX = 4;
const int wingDY = 5;
AiThickLineAA(canvas, cx, shaftTopY,
cx - wingDX, shaftTopY + wingDY, 2, argb);
AiThickLineAA(canvas, cx + 1, shaftTopY,
cx + 1 + wingDX, shaftTopY + wingDY, 2, argb);
}
//+------------------------------------------------------------------+
//| Draw pencil (edit) icon |
//+------------------------------------------------------------------+
void AiDrawIconEdit(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute inner drawing rect
const int gL = x + 2, gR = x + size - 2;
const int gT = y + 2, gB = y + size - 2;
//--- Setup pencil geometry constants
const int erDX = 3;
const int chDX = 3;
const int tipBack = 4;
//--- Compute eraser cap corners
const int er1x = gR - erDX, er1y = gT;
const int er2x = gR, er2y = gT + erDX;
//--- Compute chamfer corners
const int c1x = gL + tipBack, c1y = gB - tipBack - chDX;
const int c2x = gL + tipBack + chDX, c2y = gB - tipBack;
//--- Compute tip apex
const int tipx = gL, tipy = gB;
//--- Draw eraser cap
AiThickLineAA(canvas, er1x, er1y, er2x, er2y, 1, argb);
//--- Draw body shaft as two parallel diagonals
AiThickLineAA(canvas, er1x, er1y, c1x, c1y, 2, argb);
AiThickLineAA(canvas, er2x, er2y, c2x, c2y, 2, argb);
//--- Draw chamfer between body and tip
AiThickLineAA(canvas, c1x, c1y, c2x, c2y, 1, argb);
//--- Draw tip converging strokes
AiThickLineAA(canvas, c1x, c1y, tipx, tipy, 1, argb);
AiThickLineAA(canvas, c2x, c2y, tipx, tipy, 1, argb);
}
//+------------------------------------------------------------------+
//| Draw downward V-arrow icon for scroll-to-bottom FAB |
//+------------------------------------------------------------------+
void AiDrawIconArrowDown(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute inner drawing rect and center column
const int gL = x + 2, gR = x + size - 2;
const int gT = y + 2, gB = y + size - 2;
const int cx = (gL + gR) / 2;
//--- Compute arrow apex and wing geometry
const int apexY = gB - 1;
const int armSpan = (gR - gL) / 2;
const int armTopY = gT + (gB - gT) / 2;
//--- Draw vertical shaft from top down to apex
AiThickLineAA(canvas, cx, gT, cx, apexY, 2, argb);
//--- Draw two converging wings
AiThickLineAA(canvas, cx - armSpan, armTopY, cx - 1, apexY, 2, argb);
AiThickLineAA(canvas, cx + armSpan, armTopY, cx, apexY, 2, argb);
}
//+------------------------------------------------------------------+
//| Draw twin candlestick icon for Twin Bars action |
//+------------------------------------------------------------------+
void AiDrawIconTwinBars(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute inner drawing rect
const int gL = x + 2, gR = x + size - 2;
const int gT = y + 2, gB = y + size - 2;
const int W = gR - gL;
const int H = gB - gT;
//--- Compute candle width and side padding
int cw = 5;
if(W < 2 * cw + 4)
{
cw = MathMax(1, (W - 4) / 2);
if(cw % 2 == 0) cw = MathMax(1, cw - 1);
}
const int betweenGap = 2;
const int sidePad = MathMax(0, (W - 2 * cw - betweenGap) / 2);
//--- Compute bar X positions
const int b0L = gL + sidePad;
const int b1L = b0L + cw + betweenGap;
//--- Compute body and wick Y bounds (identical for both bars)
const int bodyT = gT + (int)(H * 0.22);
const int bodyB = gT + (int)(H * 0.85);
const int wickT = gT + (int)(H * 0.05);
const int wickB = gT + (int)(H * 0.95);
//--- Use single blue color for both bars
const uint twinBlue = ColorToARGB(C'58,130,246', 235);
const int posX[2] = {b0L, b1L};
//--- Draw each bar's wick and body
for(int i = 0; i < 2; i++)
{
const int bL = posX[i];
const int bR = bL + cw - 1;
const int wickX = bL + cw / 2;
//--- Wick column
for(int yy = wickT; yy <= wickB; yy++)
AiBlendPixel(canvas, wickX, yy, twinBlue);
//--- Body fill
for(int yy = bodyT; yy <= bodyB; yy++)
for(int xx = bL; xx <= bR; xx++)
AiBlendPixel(canvas, xx, yy, twinBlue);
}
}
//+------------------------------------------------------------------+
//| Draw lightning bolt icon for Quick Scalp action |
//+------------------------------------------------------------------+
void AiDrawIconLightning(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Setup gold color and bolt geometry constants
const uint goldArgb = ColorToARGB(C'255,200,50', 240);
const int upperTopL = x + 10, upperTopR = x + 13, upperTopY = y + 2;
const int upperBotL = x + 4, upperBotR = x + 7, upperBotY = y + 8;
const int kinkTopY = y + 8;
const int kinkBotY = y + 9;
const int kinkLeft = x + 4;
const int kinkRight = x + 13;
const int lowerTopL = x + 10, lowerTopR = x + 13, lowerTopY = y + 9;
const int lowerBotL = x + 4, lowerBotR = x + 7, lowerBotY = y + 15;
//--- Fill upper stripe parallelogram by row
for(int yy = upperTopY; yy <= upperBotY; yy++)
{
const double t = (double)(yy - upperTopY) / MathMax(1, (upperBotY - upperTopY));
int xLeft = (int)MathRound(upperTopL + t * (upperBotL - upperTopL));
int xRight = (int)MathRound(upperTopR + t * (upperBotR - upperTopR));
if(xLeft > xRight) { int tt = xLeft; xLeft = xRight; xRight = tt; }
for(int xx = xLeft; xx <= xRight; xx++)
AiBlendPixel(canvas, xx, yy, goldArgb);
}
//--- Fill kink bridge rectangle connecting stripes
for(int yy = kinkTopY; yy <= kinkBotY; yy++)
for(int xx = kinkLeft; xx <= kinkRight; xx++)
AiBlendPixel(canvas, xx, yy, goldArgb);
//--- Fill lower stripe parallelogram by row
for(int yy = lowerTopY; yy <= lowerBotY; yy++)
{
const double t = (double)(yy - lowerTopY) / MathMax(1, (lowerBotY - lowerTopY));
int xLeft = (int)MathRound(lowerTopL + t * (lowerBotL - lowerTopL));
int xRight = (int)MathRound(lowerTopR + t * (lowerBotR - lowerTopR));
if(xLeft > xRight) { int tt = xLeft; xLeft = xRight; xRight = tt; }
for(int xx = xLeft; xx <= xRight; xx++)
AiBlendPixel(canvas, xx, yy, goldArgb);
}
}
//+------------------------------------------------------------------+
//| Draw sun + horizon icon for Daily Signal action |
//+------------------------------------------------------------------+
void AiDrawIconDay(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute inner drawing rect and centerline
const int gL = x + 2, gR = x + size - 2;
const int gT = y + 2, gB = y + size - 2;
const int cx = (gL + gR) / 2;
//--- Position horizon and sun above it
const int horizonY = gT + (gB - gT) * 2 / 3;
const int sunCy = horizonY - 4;
const int rad = 3;
//--- Setup colors
const uint sunArgb = ColorToARGB(C'255,180,50', 240);
const uint horizonArgb = ColorToARGB(C'60,180,90', 240);
//--- Fill sun disc
for(int yy = -rad; yy <= rad; yy++)
{
for(int xx = -rad; xx <= rad; xx++)
{
if(xx * xx + yy * yy <= rad * rad)
AiBlendPixel(canvas, cx + xx, sunCy + yy, sunArgb);
}
}
//--- Top ray
AiBlendPixel(canvas, cx, sunCy - rad - 1, sunArgb);
AiBlendPixel(canvas, cx, sunCy - rad - 2, sunArgb);
//--- Left ray
AiBlendPixel(canvas, cx - rad - 1, sunCy, sunArgb);
AiBlendPixel(canvas, cx - rad - 2, sunCy, sunArgb);
//--- Right ray
AiBlendPixel(canvas, cx + rad + 1, sunCy, sunArgb);
AiBlendPixel(canvas, cx + rad + 2, sunCy, sunArgb);
//--- Draw horizon line
AiThickLineAA(canvas, gL, horizonY, gR, horizonY, 2, horizonArgb);
}
//+------------------------------------------------------------------+
//| Draw trendline icon for Trend Read action |
//+------------------------------------------------------------------+
void AiDrawIconTrend(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute inner rect and teal color
const int gL = x + 3, gR = x + size - 3;
const int gT = y + 3, gB = y + size - 3;
const uint teal = ColorToARGB(C'30,170,180', 240);
//--- Draw diagonal trendline
AiThickLineAA(canvas, gL, gB, gR, gT, 2, teal);
//--- Draw 3x3 anchor dots at endpoints
for(int dy = -1; dy <= 1; dy++)
for(int dx = -1; dx <= 1; dx++)
{
AiBlendPixel(canvas, gL + dx, gB + dy, teal);
AiBlendPixel(canvas, gR + dx, gT + dy, teal);
}
}
//+------------------------------------------------------------------+
//| Draw horizontal level icon for Key Level action |
//+------------------------------------------------------------------+
void AiDrawIconLevel(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute inner rect and magenta color
const int gL = x + 3, gR = x + size - 3;
const int cy = y + size / 2;
const uint magenta = ColorToARGB(C'180,90,200', 240);
//--- Draw horizontal level line
AiThickLineAA(canvas, gL, cy, gR, cy, 2, magenta);
//--- Draw 3x3 endcap dots
for(int dy = -1; dy <= 1; dy++)
for(int dx = -1; dx <= 1; dx++)
{
AiBlendPixel(canvas, gL + dx, cy + dy, magenta);
AiBlendPixel(canvas, gR + dx, cy + dy, magenta);
}
//--- Draw price-touch markers above and below line
const int midX = (gL + gR) / 2;
AiBlendPixel(canvas, midX, cy - 4, magenta);
AiBlendPixel(canvas, midX - 1, cy - 4, magenta);
AiBlendPixel(canvas, midX, cy + 4, magenta);
AiBlendPixel(canvas, midX - 1, cy + 4, magenta);
}
//+------------------------------------------------------------------+
//| Draw red X close icon for Clear Drawings action |
//+------------------------------------------------------------------+
void AiDrawIconClose(CCanvas &canvas, int x, int y, int size, uint argb)
{
//--- Compute inner rect and red color
const int gL = x + 3, gR = x + size - 3;
const int gT = y + 3, gB = y + size - 3;
const uint redArgb = ColorToARGB(C'220,53,69', 240);
//--- Draw two diagonal strokes forming X
AiThickLineAA(canvas, gL, gT, gR, gB, 2, redArgb);
AiThickLineAA(canvas, gR, gT, gL, gB, 2, redArgb);
}
//+------------------------------------------------------------------+
//| Markdown Constants |
//+------------------------------------------------------------------+
#define AI_MD_KIND_BODY 0 // Plain body line
#define AI_MD_KIND_H1 1 // Heading level 1 (sentinel \1)
#define AI_MD_KIND_H2 2 // Heading level 2 (sentinel \2)
#define AI_MD_KIND_H3 3 // Heading level 3 (sentinel \3)
#define AI_MD_KIND_NUMBERED 5 // Numbered list line
//+------------------------------------------------------------------+
//| Styled markdown run |
//+------------------------------------------------------------------+
struct AiMdRun
{
string text; // Run text content
bool bold; // Bold style flag
bool italic; // Italic style flag
};
//+------------------------------------------------------------------+
//| Parse a single markdown line into styled runs |
//+------------------------------------------------------------------+
void AiMdParseInline(const string txt, AiMdRun &runs[])
{
//--- Reset output array and bail on empty input
ArrayResize(runs, 0);
const int len = StringLen(txt);
if(len == 0) return;
//--- Initialize parser state
bool curBold = false, curItalic = false;
string buf = "";
//--- Define flush macro for emitting accumulated runs
#define AI_MD_FLUSH() \
{ \
if(StringLen(buf) > 0) { \
const int sz = ArraySize(runs); \
ArrayResize(runs, sz + 1); \
runs[sz].text = buf; \
runs[sz].bold = curBold; \
runs[sz].italic = curItalic; \
buf = ""; \
} \
}
//--- Walk characters and toggle styles
int i = 0;
while(i < len)
{
const ushort ch = StringGetCharacter(txt, i);
//--- Handle asterisk markers
if(ch == '*')
{
//--- Triple asterisk toggles bold + italic
if(i + 2 < len
&& StringGetCharacter(txt, i + 1) == '*'
&& StringGetCharacter(txt, i + 2) == '*')
{
AI_MD_FLUSH();
curBold = !curBold;
curItalic = !curItalic;
i += 3;
continue;
}
//--- Double asterisk toggles bold
if(i + 1 < len && StringGetCharacter(txt, i + 1) == '*')
{
AI_MD_FLUSH();
curBold = !curBold;
i += 2;
continue;
}
//--- Single asterisk toggles italic
AI_MD_FLUSH();
curItalic = !curItalic;
i++;
continue;
}
//--- Append plain character
buf += StringSubstr(txt, i, 1);
i++;
}
//--- Flush trailing run
AI_MD_FLUSH();
#undef AI_MD_FLUSH
}
//+------------------------------------------------------------------+
//| Compute markdown style state at end of line |
//+------------------------------------------------------------------+
void AiMdComputeEndState(const string txt, bool &openBold, bool &openItalic)
{
//--- Bail on empty input
const int len = StringLen(txt);
if(len == 0) return;
//--- Walk characters mirroring AiMdParseInline transitions
int i = 0;
while(i < len)
{
const ushort ch = StringGetCharacter(txt, i);
if(ch == '*')
{
//--- Triple asterisk toggles both
if(i + 2 < len
&& StringGetCharacter(txt, i + 1) == '*'
&& StringGetCharacter(txt, i + 2) == '*')
{
openBold = !openBold;
openItalic = !openItalic;
i += 3;
continue;
}
//--- Double asterisk toggles bold
if(i + 1 < len && StringGetCharacter(txt, i + 1) == '*')
{
openBold = !openBold;
i += 2;
continue;
}
//--- Single asterisk toggles italic
openItalic = !openItalic;
i++;
continue;
}
i++;
}
}
//+------------------------------------------------------------------+
//| Build marker prefix needed to reopen styles on continuation line |
//+------------------------------------------------------------------+
string AiMdReopenMarkers(const bool openBold, const bool openItalic)
{
//--- Pick marker length based on combined open state
if(openBold && openItalic) return "***";
if(openBold) return "**";
if(openItalic) return "*";
return "";
}
//+------------------------------------------------------------------+
//| Resolve font name for a markdown run's bold/italic combination |
//+------------------------------------------------------------------+
string AiMdRunFont(const AiMdRun &r)
{
//--- Map style flags to Arial variant
if(r.bold && r.italic) return "Arial Bold Italic";
if(r.bold) return "Arial Bold";
if(r.italic) return "Arial Italic";
return "Arial";
}
//+------------------------------------------------------------------+
//| Measure total pixel width of styled run sequence |
//+------------------------------------------------------------------+
int AiMdRunsWidth(const AiMdRun &runs[], int fontSize)
{
//--- Sum widths of each run with its font variant
int total = 0;
const int n = ArraySize(runs);
for(int i = 0; i < n; i++)
{
total += AiTextWidth(runs[i].text, AiMdRunFont(runs[i]), fontSize);
}
return total;
}
//+------------------------------------------------------------------+
//| Stamp styled run sequence side-by-side onto canvas |
//+------------------------------------------------------------------+
void AiMdStampRuns(CCanvas &canvas, int x, int y,
const AiMdRun &runs[], int fontSize, color textCol)
{
//--- Walk runs and stamp each at advancing X position
int curX = x;
const int n = ArraySize(runs);
for(int i = 0; i < n; i++)
{
if(StringLen(runs[i].text) == 0) continue;
const string font = AiMdRunFont(runs[i]);
AiStampTextAA(canvas, curX, y, runs[i].text, font, fontSize, textCol);
curX += AiTextWidth(runs[i].text, font, fontSize);
}
}
#endif // AI_CANVAS_PRIMITIVES_MQH