2072 lines
No EOL
84 KiB
MQL5
2072 lines
No EOL
84 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 |