| //+------------------------------------------------------------------+
|
| //| 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]);
|
| }
|
| }
|
|
|
| //+---------------------------------------------------------------+
|
| //| Fast row-block copy between two fast canvases |
|
| //+---------------------------------------------------------------+
|
| void CopyRectFromCanvas(CAiCanvasFast &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);
|
| const int cnt = cr - cl;
|
| if(cnt <= 0)
|
| return;
|
| //--- Copy each row as one contiguous block
|
| for(int yy = ct; yy < cb; yy++)
|
| ArrayCopy(m_pixels, src.m_pixels, yy * dw + cl, yy * sw + cl, cnt);
|
| }
|
|
|
| //+---------------------------------------------------------------+
|
| //| FAST overload - block row copy out to another fast canvas |
|
| //+---------------------------------------------------------------+
|
| void CopyRectToCanvas(CAiCanvasFast &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);
|
| const int cnt = cr - cl;
|
| if(cnt <= 0)
|
| return;
|
| //--- Copy each row as one contiguous block
|
| for(int yy = ct; yy < cb; yy++)
|
| ArrayCopy(dst.m_pixels, m_pixels, yy * dwOther + cl, yy * sw + cl, cnt);
|
| }
|
|
|
| //+---------------------------------------------------------------+
|
| //| 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;
|
| //--- Fast path: fill interior at target resolution, supersample only the corners
|
| const uchar fillA = (uchar)((argb >> 24) & 0xFF);
|
| if(fillA == 255 && radius > 0 && (w - 2 * radius) >= 1 && (h - 2 * radius) >= 1)
|
| {
|
| const int rr = radius;
|
| //--- Fill three central rectangles at target resolution
|
| target.FillRectangle(x + rr, y, x + w - rr - 1, y + h - 1, argb);
|
| target.FillRectangle(x, y + rr, x + rr - 1, y + h - rr - 1, argb);
|
| target.FillRectangle(x + w - rr, y + rr, x + w - 1, y + h - rr - 1, argb);
|
| //--- Corners: supersample only each rr x rr box into a small HR scratch.
|
| const int hrs = rr * factor;
|
| const int ss2 = factor * factor;
|
| if(!EnsureHrFill(hrs, hrs))
|
| return;
|
| //--- Per-corner geometry: box origin (ox,oy), HR arc center (cx,cy), signs.
|
| int cOx[4];
|
| int cOy[4];
|
| int cCx[4];
|
| int cCy[4];
|
| int cSx[4];
|
| int cSy[4];
|
| cOx[0] = x;
|
| cOy[0] = y;
|
| cCx[0] = hrs;
|
| cCy[0] = hrs;
|
| cSx[0] = -1;
|
| cSy[0] = -1; // TL
|
| cOx[1] = x + w - rr;
|
| cOy[1] = y;
|
| cCx[1] = 0;
|
| cCy[1] = hrs;
|
| cSx[1] = 1;
|
| cSy[1] = -1; // TR
|
| cOx[2] = x;
|
| cOy[2] = y + h - rr;
|
| cCx[2] = hrs;
|
| cCy[2] = 0;
|
| cSx[2] = -1;
|
| cSy[2] = 1; // BL
|
| cOx[3] = x + w - rr;
|
| cOy[3] = y + h - rr;
|
| cCx[3] = 0;
|
| cCy[3] = 0;
|
| cSx[3] = 1;
|
| cSy[3] = 1; // BR
|
| const int tW = target.Width(), tH = target.Height();
|
| for(int ci = 0; ci < 4; ci++)
|
| {
|
| //--- Clear this corner's HR region to transparent, then render the arc
|
| m_hrFill.FillRectangle(0, 0, hrs - 1, hrs - 1, 0);
|
| FillCornerQuadrantHR(m_hrFill, cCx[ci], cCy[ci], hrs, argb, cSx[ci], cSy[ci]);
|
| //--- Downsample rr x rr into the target corner box
|
| for(int py = 0; py < rr; py++)
|
| {
|
| const int ty = cOy[ci] + py;
|
| if(ty < 0 || ty >= tH)
|
| continue;
|
| for(int px = 0; px < rr; px++)
|
| {
|
| const int tx = cOx[ci] + px;
|
| if(tx < 0 || tx >= tW)
|
| continue;
|
| 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 >= hrs || sy >= hrs)
|
| 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;
|
| }
|
| }
|
| const uchar fa = (uchar)(sA / ss2);
|
| if(fa == 0 || wc == 0)
|
| continue;
|
| 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);
|
| }
|
| }
|
| }
|
| return;
|
| }
|
| //--- Supersample path for translucent or degenerate-radius fills
|
| 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 ss2b = factor * factor;
|
| const int tWb = target.Width(), tHb = target.Height();
|
| for(int py = 0; py < h; py++)
|
| {
|
| const int ty = y + py;
|
| if(ty < 0 || ty >= tHb)
|
| continue;
|
| for(int px = 0; px < w; px++)
|
| {
|
| const int tx = x + px;
|
| if(tx < 0 || tx >= tWb)
|
| 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 / ss2b);
|
| 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 only a near-edge band since the border interior is transparent
|
| const int ss2 = factor * factor;
|
| const int tW = target.Width(), tH = target.Height();
|
| const int band = radius + thickness + 4;
|
| for(int py = 0; py < h; py++)
|
| {
|
| const int ty = y + py;
|
| if(ty < 0 || ty >= tH)
|
| continue;
|
| const bool rowEdge = (py < band || py >= h - band);
|
| for(int px = 0; px < w; px++)
|
| {
|
| const int tx = x + px;
|
| if(tx < 0 || tx >= tW)
|
| continue;
|
| //--- Skip the transparent interior (identical to fa==0 continue)
|
| if(!(rowEdge || px < band || px >= w - band))
|
| 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);
|
| }
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| 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));
|
| }
|
|
|
|
|
|
|
|
|
| //+------------------------------------------------------------------+
|
| //| Cache global font state to skip redundant TextSetFont calls |
|
| //+------------------------------------------------------------------+
|
| string g_ai_fsName = ""; // Last font name passed to TextSetFont
|
| int g_ai_fsSize = -12345; // Last font size passed to TextSetFont
|
|
|
| //+------------------------------------------------------------------+
|
| //| Set global text font only when it differs from the cached state |
|
| //+------------------------------------------------------------------+
|
| void AiFontSet(const string fontName, const int fontSize)
|
| {
|
| //--- Skip the OS call when the font state is unchanged
|
| if(fontSize == g_ai_fsSize && fontName == g_ai_fsName)
|
| return;
|
| //--- Apply and remember the new font state
|
| TextSetFont(fontName, -(fontSize * 10));
|
| g_ai_fsName = fontName;
|
| g_ai_fsSize = fontSize;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| FNV-1a hash over a string - shared by the metric/glyph caches |
|
| //+------------------------------------------------------------------+
|
| uint AiStrHash(const string k)
|
| {
|
| //--- Walk characters and fold into FNV-1a accumulator
|
| uint h = 2166136261;
|
| const int n = StringLen(k);
|
| for(int i = 0; i < n; i++)
|
| {
|
| h ^= (uint)StringGetCharacter(k, i);
|
| h *= 16777619;
|
| }
|
| return h;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Text width memo cache with open-addressed eviction |
|
| //+------------------------------------------------------------------+
|
| #define AI_TWCACHE_SLOTS 4096 // Power of two
|
| #define AI_TWCACHE_MASK (AI_TWCACHE_SLOTS - 1)
|
| #define AI_TWCACHE_PROBE 4 // Linear probe depth
|
| string g_ai_twKey[AI_TWCACHE_SLOTS]; // Composite keys (txt|font|size)
|
| int g_ai_twVal[AI_TWCACHE_SLOTS]; // Cached pixel widths
|
|
|
| //+------------------------------------------------------------------+
|
| //| Glyph-run coverage cache keyed by text, font and size |
|
| //+------------------------------------------------------------------+
|
| #define AI_GLYPH_SLOTS 1024 // Power of two
|
| #define AI_GLYPH_MASK (AI_GLYPH_SLOTS - 1)
|
| #define AI_GLYPH_PROBE 8 // Linear probe depth
|
| #define AI_GLYPH_BYTE_BUDGET 8388608 // 8 MB alpha-map budget
|
|
|
| //--- One cached glyph-run coverage map
|
| struct AiGlyphEntry
|
| {
|
| string key; // Composite key (txt|font|size)
|
| int w; // Raster width in pixels
|
| int h; // Raster height in pixels
|
| uchar a[]; // Coverage map, w*h alpha values
|
| };
|
|
|
| AiGlyphEntry g_ai_glyphCache[AI_GLYPH_SLOTS]; // Slot table
|
| long g_ai_glyphBytes = 0; // Total cached bytes
|
|
|
| //+------------------------------------------------------------------+
|
| //| Wipe the glyph cache when the byte budget is exceeded |
|
| //+------------------------------------------------------------------+
|
| void AiGlyphCacheReset()
|
| {
|
| //--- Free every occupied slot
|
| for(int i = 0; i < AI_GLYPH_SLOTS; i++)
|
| {
|
| if(StringLen(g_ai_glyphCache[i].key) == 0)
|
| continue;
|
| g_ai_glyphCache[i].key = "";
|
| ArrayFree(g_ai_glyphCache[i].a);
|
| g_ai_glyphCache[i].w = 0;
|
| g_ai_glyphCache[i].h = 0;
|
| }
|
| g_ai_glyphBytes = 0;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| 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;
|
| //--- Probe the glyph-run cache for this exact (text, font, size)
|
| const string gKey = txt + "\x1F" + fontName + "\x1F" + IntegerToString(fontSize);
|
| int slot = -1;
|
| int freeSlot = -1;
|
| const uint gh = AiStrHash(gKey);
|
| for(int pr = 0; pr < AI_GLYPH_PROBE; pr++)
|
| {
|
| const int cand = (int)((gh + (uint)pr) & AI_GLYPH_MASK);
|
| if(StringLen(g_ai_glyphCache[cand].key) == 0)
|
| {
|
| if(freeSlot < 0)
|
| freeSlot = cand;
|
| continue;
|
| }
|
| if(g_ai_glyphCache[cand].key == gKey)
|
| {
|
| slot = cand;
|
| break;
|
| }
|
| }
|
| int tw = 0, th = 0;
|
| //--- Cache miss - derive the coverage map once and store it
|
| if(slot < 0)
|
| {
|
| //--- Set font (cached) and measure text
|
| AiFontSet(fontName, fontSize);
|
| uint twU = 0, thU = 0;
|
| TextGetSize(txt, twU, thU);
|
| tw = (int)twU;
|
| th = (int)thU;
|
| if(tw <= 0 || th <= 0)
|
| return;
|
| //--- Render text on black background buffer
|
| const uint deriveArgb = 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,
|
| deriveArgb, 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,
|
| deriveArgb, COLOR_FORMAT_ARGB_NORMALIZE);
|
| //--- Enforce the byte budget with a deterministic full reset
|
| if(g_ai_glyphBytes + (long)(tw * th) > AI_GLYPH_BYTE_BUDGET)
|
| {
|
| AiGlyphCacheReset();
|
| freeSlot = (int)(gh & AI_GLYPH_MASK);
|
| }
|
| //--- Choose destination slot - free probe slot or evict at home
|
| slot = (freeSlot >= 0) ? freeSlot : (int)(gh & AI_GLYPH_MASK);
|
| if(StringLen(g_ai_glyphCache[slot].key) > 0)
|
| {
|
| g_ai_glyphBytes -= (long)(g_ai_glyphCache[slot].w * g_ai_glyphCache[slot].h);
|
| ArrayFree(g_ai_glyphCache[slot].a);
|
| }
|
| //--- Derive color-independent coverage from the black-vs-white buffer difference
|
| g_ai_glyphCache[slot].key = gKey;
|
| g_ai_glyphCache[slot].w = tw;
|
| g_ai_glyphCache[slot].h = th;
|
| ArrayResize(g_ai_glyphCache[slot].a, tw * th);
|
| const int np = tw * th;
|
| for(int i = 0; i < np; i++)
|
| {
|
| const int dR = (int)((bufW[i] >> 16) & 0xFF) - (int)((bufB[i] >> 16) & 0xFF);
|
| const int dG = (int)((bufW[i] >> 8) & 0xFF) - (int)((bufB[i] >> 8) & 0xFF);
|
| const int dB = (int)(bufW[i] & 0xFF) - (int)(bufB[i] & 0xFF);
|
| int a = 255 - (dR + dG + dB) / 3;
|
| if(a < 0)
|
| a = 0;
|
| if(a > 255)
|
| a = 255;
|
| g_ai_glyphCache[slot].a[i] = (uchar)a;
|
| }
|
| g_ai_glyphBytes += (long)np;
|
| }
|
| else
|
| {
|
| //--- Cache hit - reuse stored raster dimensions
|
| tw = g_ai_glyphCache[slot].w;
|
| th = g_ai_glyphCache[slot].h;
|
| }
|
| //--- Precompute source color components and the opaque write pixel
|
| const uchar srcR = (uchar)(textColor & 0xFF);
|
| const uchar srcG = (uchar)((textColor >> 8) & 0xFF);
|
| const uchar srcB = (uchar)((textColor >> 16) & 0xFF);
|
| const uint srcOpaque = 0xFF000000
|
| | ((uint)srcR << 16)
|
| | ((uint)srcG << 8)
|
| | (uint)srcB;
|
| //--- Clip the stamp rect against the canvas once, up front
|
| const int cW = dst.Width(), cH = dst.Height();
|
| const int pxLo = (x < 0) ? -x : 0;
|
| const int pyLo = (y < 0) ? -y : 0;
|
| const int pxHi = (x + tw > cW) ? (cW - x) : tw;
|
| const int pyHi = (y + th > cH) ? (cH - y) : th;
|
| if(pxLo >= pxHi || pyLo >= pyHi)
|
| return;
|
| //--- Blend the cached coverage map onto the destination
|
| for(int py = pyLo; py < pyHi; py++)
|
| {
|
| const int rowA = py * tw;
|
| const int dstY = y + py;
|
| for(int px = pxLo; px < pxHi; px++)
|
| {
|
| const int a = (int)g_ai_glyphCache[slot].a[rowA + px];
|
| if(a == 0)
|
| continue;
|
| const int dstX = x + px;
|
| //--- Fully covered pixel: write opaque with no read-back
|
| if(a == 255)
|
| {
|
| dst.PixelSet(dstX, dstY, srcOpaque);
|
| continue;
|
| }
|
| //--- Read destination pixel for partial coverage
|
| const uint existing = dst.PixelGet(dstX, dstY);
|
| const int dAi = (int)((existing >> 24) & 0xFF);
|
| //--- Opaque destination: integer source-over, verified bit-exact
|
| if(dAi == 255)
|
| {
|
| const int ia = 255 - a;
|
| const int oR = ((int)srcR * a + (int)((existing >> 16) & 0xFF) * ia + 127) / 255;
|
| const int oG = ((int)srcG * a + (int)((existing >> 8) & 0xFF) * ia + 127) / 255;
|
| const int oB = ((int)srcB * a + (int)(existing & 0xFF) * ia + 127) / 255;
|
| dst.PixelSet(dstX, dstY,
|
| 0xFF000000 | ((uint)oR << 16) | ((uint)oG << 8) | (uint)oB);
|
| continue;
|
| }
|
| //--- Semi-transparent destination - exact original formula
|
| double sA = (double)a / 255.0;
|
| double dA = dAi / 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;
|
| //--- Probe the width memo table
|
| const string wKey = txt + "\x1F" + fontName + "\x1F" + IntegerToString(fontSize);
|
| const uint wh = AiStrHash(wKey);
|
| int freeSlot = -1;
|
| for(int pr = 0; pr < AI_TWCACHE_PROBE; pr++)
|
| {
|
| const int cand = (int)((wh + (uint)pr) & AI_TWCACHE_MASK);
|
| if(StringLen(g_ai_twKey[cand]) == 0)
|
| {
|
| if(freeSlot < 0)
|
| freeSlot = cand;
|
| continue;
|
| }
|
| if(g_ai_twKey[cand] == wKey)
|
| return g_ai_twVal[cand];
|
| }
|
| //--- Miss - set font (cached) and measure
|
| AiFontSet(fontName, fontSize);
|
| uint tw = 0, th = 0;
|
| TextGetSize(txt, tw, th);
|
| //--- Store into a free probe slot, or evict at the home slot
|
| const int put = (freeSlot >= 0) ? freeSlot : (int)(wh & AI_TWCACHE_MASK);
|
| g_ai_twKey[put] = wKey;
|
| g_ai_twVal[put] = (int)tw;
|
| return (int)tw;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Text Height Memo |
|
| //+------------------------------------------------------------------+
|
| //--- Memoize height per font and size since the value is pure
|
| string g_ai_hFonts[]; // Memoized font names
|
| int g_ai_hSizes[]; // Memoized font sizes
|
| int g_ai_hVals[]; // Memoized heights
|
|
|
| //+------------------------------------------------------------------+
|
| //| Measure standard text height in given font |
|
| //+------------------------------------------------------------------+
|
| int AiTextHeight(const string fontName, int fontSize)
|
| {
|
| //--- Linear-scan the tiny memo (only a handful of fonts exist)
|
| const int n = ArraySize(g_ai_hFonts);
|
| for(int i = 0; i < n; i++)
|
| if(g_ai_hSizes[i] == fontSize && g_ai_hFonts[i] == fontName)
|
| return g_ai_hVals[i];
|
| //--- Miss - set font (cached) and measure reference string
|
| AiFontSet(fontName, fontSize);
|
| uint tw = 0, th = 0;
|
| TextGetSize("Ay", tw, th);
|
| //--- Store the measurement
|
| ArrayResize(g_ai_hFonts, n + 1);
|
| ArrayResize(g_ai_hSizes, n + 1);
|
| ArrayResize(g_ai_hVals, n + 1);
|
| g_ai_hFonts[n] = fontName;
|
| g_ai_hSizes[n] = fontSize;
|
| g_ai_hVals[n] = (int)th;
|
| return (int)th;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Fit Result Memo |
|
| //+------------------------------------------------------------------+
|
| //--- Memoize fitted string per text, font, size and width since it is pure
|
| #define AI_FITCACHE_SLOTS 1024
|
| #define AI_FITCACHE_MASK (AI_FITCACHE_SLOTS - 1)
|
| #define AI_FITCACHE_PROBE 4
|
| string g_ai_fitKey[AI_FITCACHE_SLOTS]; // Composite keys
|
| string g_ai_fitVal[AI_FITCACHE_SLOTS]; // Fitted results
|
|
|
| //+------------------------------------------------------------------+
|
| //| 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 "";
|
| //--- Probe the fit memo - skips the whole measure/binary-search below
|
| const string fKey = text + "\x1F" + fontName + "\x1F"
|
| + IntegerToString(fontSize) + "\x1F" + IntegerToString(maxWidthPx);
|
| const uint fh = AiStrHash(fKey);
|
| int fitFree = -1;
|
| for(int pr = 0; pr < AI_FITCACHE_PROBE; pr++)
|
| {
|
| const int cand = (int)((fh + (uint)pr) & AI_FITCACHE_MASK);
|
| if(StringLen(g_ai_fitKey[cand]) == 0)
|
| {
|
| if(fitFree < 0)
|
| fitFree = cand;
|
| continue;
|
| }
|
| if(g_ai_fitKey[cand] == fKey)
|
| return g_ai_fitVal[cand];
|
| }
|
| const int fitPut = (fitFree >= 0) ? fitFree : (int)(fh & AI_FITCACHE_MASK);
|
| //--- Fast path - full text fits
|
| const int fullW = AiTextWidth(text, fontName, fontSize);
|
| if(fullW <= maxWidthPx)
|
| {
|
| g_ai_fitKey[fitPut] = fKey;
|
| g_ai_fitVal[fitPut] = text;
|
| 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 plus ellipsis (memoized)
|
| const string fitOut = (lo <= 0) ? ellipsis : (StringSubstr(text, 0, lo) + ellipsis);
|
| g_ai_fitKey[fitPut] = fKey;
|
| g_ai_fitVal[fitPut] = fitOut;
|
| return fitOut;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| 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);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| VECTOR BRAND MARKS AND SIDEBAR ICONS |
|
| //+------------------------------------------------------------------+
|
| //--- Draw all brand marks and icons as resolution-free vectors, replacing old BMP resources
|
| #define AI_BRAND_BLUE C'44,107,204' // MQL wordmark blue
|
| #define AI_BRAND_AMBER C'249,160,4' // MQL5 "5" amber
|
| #define AI_ORB_DEEP C'9,60,146' // orb core, darkest
|
| #define AI_ORB_MID C'22,85,189' // orb body
|
| #define AI_ORB_LIVE C'26,104,240' // orb lit face
|
| #define AI_ORB_RIM C'55,208,242' // orb cyan rim highlight
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw thick anti-aliased vector stroke with round caps |
|
| //+------------------------------------------------------------------+
|
| void AiVecStroke(CCanvas &canvas, const double x0, const double y0,
|
| const double x1, const double y1, const double thick, const uint argb)
|
| {
|
| double half = MathMax(0.5, thick * 0.5);
|
| double dx = x1 - x0, dy = y1 - y0;
|
| double len2 = dx * dx + dy * dy;
|
| int bx0 = (int)MathFloor(MathMin(x0, x1) - half - 1.0);
|
| int bx1 = (int)MathCeil(MathMax(x0, x1) + half + 1.0);
|
| int by0 = (int)MathFloor(MathMin(y0, y1) - half - 1.0);
|
| int by1 = (int)MathCeil(MathMax(y0, y1) + half + 1.0);
|
| for(int py = by0; py <= by1; py++)
|
| for(int px = bx0; px <= bx1; px++)
|
| {
|
| double sx = px + 0.5, sy = py + 0.5;
|
| double t = (len2 > 1e-9) ? ((sx - x0) * dx + (sy - y0) * dy) / len2 : 0.0;
|
| if(t < 0.0)
|
| t = 0.0;
|
| if(t > 1.0)
|
| t = 1.0;
|
| double qx = x0 + t * dx - sx, qy = y0 + t * dy - sy;
|
| double cov = (half + 0.5) - MathSqrt(qx * qx + qy * qy);
|
| if(cov <= 0.0)
|
| continue;
|
| if(cov > 1.0)
|
| cov = 1.0;
|
| AiIconAAPlot(canvas, px, py, cov, argb);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw anti-aliased filled disc |
|
| //+------------------------------------------------------------------+
|
| void AiVecDisc(CCanvas &canvas, const double cx, const double cy, const double r, const uint argb)
|
| {
|
| if(r <= 0.0)
|
| return;
|
| int x0 = (int)MathFloor(cx - r - 1.0), x1 = (int)MathCeil(cx + r + 1.0);
|
| int y0 = (int)MathFloor(cy - r - 1.0), y1 = (int)MathCeil(cy + r + 1.0);
|
| for(int py = y0; py <= y1; py++)
|
| for(int px = x0; px <= x1; px++)
|
| {
|
| double dx = (px + 0.5) - cx, dy = (py + 0.5) - cy;
|
| double cov = 0.5 - (MathSqrt(dx * dx + dy * dy) - r);
|
| if(cov <= 0.0)
|
| continue;
|
| if(cov > 1.0)
|
| cov = 1.0;
|
| AiIconAAPlot(canvas, px, py, cov, argb);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw anti-aliased ring |
|
| //+------------------------------------------------------------------+
|
| void AiVecRing(CCanvas &canvas, const double cx, const double cy,
|
| const double r, const double thick, const uint argb)
|
| {
|
| if(r <= 0.0 || thick <= 0.0)
|
| return;
|
| double half = thick * 0.5;
|
| int x0 = (int)MathFloor(cx - r - thick - 1.0), x1 = (int)MathCeil(cx + r + thick + 1.0);
|
| int y0 = (int)MathFloor(cy - r - thick - 1.0), y1 = (int)MathCeil(cy + r + thick + 1.0);
|
| for(int py = y0; py <= y1; py++)
|
| for(int px = x0; px <= x1; px++)
|
| {
|
| double dx = (px + 0.5) - cx, dy = (py + 0.5) - cy;
|
| double cov = (half + 0.5) - MathAbs(MathSqrt(dx * dx + dy * dy) - r);
|
| if(cov <= 0.0)
|
| continue;
|
| if(cov > 1.0)
|
| cov = 1.0;
|
| AiIconAAPlot(canvas, px, py, cov, argb);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw uniform rounded-rectangle outline |
|
| //+------------------------------------------------------------------+
|
| void AiVecRoundRectRing(CCanvas &canvas, const double x, const double y,
|
| const double w, const double h, const double r,
|
| const double thick, const uint argb)
|
| {
|
| if(w <= 0.0 || h <= 0.0)
|
| return;
|
| double rr = MathMin(r, MathMin(w, h) * 0.5);
|
| double half = MathMax(0.5, thick * 0.5);
|
| double cx = x + w * 0.5, cy = y + h * 0.5;
|
| int x0 = (int)MathFloor(x - thick - 1.0), x1 = (int)MathCeil(x + w + thick + 1.0);
|
| int y0 = (int)MathFloor(y - thick - 1.0), y1 = (int)MathCeil(y + h + thick + 1.0);
|
| for(int py = y0; py <= y1; py++)
|
| for(int px = x0; px <= x1; px++)
|
| {
|
| double qx = MathAbs((px + 0.5) - cx) - (w * 0.5 - rr);
|
| double qy = MathAbs((py + 0.5) - cy) - (h * 0.5 - rr);
|
| double ax = MathMax(qx, 0.0), ay = MathMax(qy, 0.0);
|
| double d = MathSqrt(ax * ax + ay * ay) + MathMin(MathMax(qx, qy), 0.0) - rr;
|
| double cov = (half + 0.5) - MathAbs(d);
|
| if(cov <= 0.0)
|
| continue;
|
| if(cov > 1.0)
|
| cov = 1.0;
|
| AiIconAAPlot(canvas, px, py, cov, argb);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Fill rounded rectangle with anti-aliased corners |
|
| //+------------------------------------------------------------------+
|
| void AiVecRoundRect(CCanvas &canvas, const double x, const double y,
|
| const double w, const double h, const double r, const uint argb)
|
| {
|
| if(w <= 0.0 || h <= 0.0)
|
| return;
|
| double rr = MathMin(r, MathMin(w, h) * 0.5);
|
| int x0 = (int)MathFloor(x - 1.0), x1 = (int)MathCeil(x + w + 1.0);
|
| int y0 = (int)MathFloor(y - 1.0), y1 = (int)MathCeil(y + h + 1.0);
|
| for(int py = y0; py <= y1; py++)
|
| for(int px = x0; px <= x1; px++)
|
| {
|
| double sx = px + 0.5, sy = py + 0.5;
|
| double qx = MathAbs(sx - (x + w * 0.5)) - (w * 0.5 - rr);
|
| double qy = MathAbs(sy - (y + h * 0.5)) - (h * 0.5 - rr);
|
| double ax = MathMax(qx, 0.0), ay = MathMax(qy, 0.0);
|
| double cov = 0.5 - (MathSqrt(ax * ax + ay * ay) + MathMin(MathMax(qx, qy), 0.0) - rr);
|
| if(cov <= 0.0)
|
| continue;
|
| if(cov > 1.0)
|
| cov = 1.0;
|
| AiIconAAPlot(canvas, px, py, cov, argb);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Fill convex polygon with anti-aliased edges |
|
| //+------------------------------------------------------------------+
|
| void AiVecPoly(CCanvas &canvas, double &xs[], double &ys[], const int n, const uint argb)
|
| {
|
| if(n < 3)
|
| return;
|
| double minx = xs[0], maxx = xs[0], miny = ys[0], maxy = ys[0];
|
| for(int i = 1; i < n; i++)
|
| {
|
| if(xs[i] < minx)
|
| minx = xs[i];
|
| if(xs[i] > maxx)
|
| maxx = xs[i];
|
| if(ys[i] < miny)
|
| miny = ys[i];
|
| if(ys[i] > maxy)
|
| maxy = ys[i];
|
| }
|
| //--- 3x3 supersample inside the bounding box: cheap and exact enough at icon sizes
|
| for(int py = (int)MathFloor(miny); py <= (int)MathCeil(maxy); py++)
|
| for(int px = (int)MathFloor(minx); px <= (int)MathCeil(maxx); px++)
|
| {
|
| int hits = 0;
|
| for(int sy = 0; sy < 3; sy++)
|
| for(int sx = 0; sx < 3; sx++)
|
| {
|
| double tx = px + (sx + 0.5) / 3.0, ty = py + (sy + 0.5) / 3.0;
|
| bool inside = false;
|
| int j = n - 1;
|
| for(int i = 0; i < n; j = i++)
|
| if(((ys[i] > ty) != (ys[j] > ty)) &&
|
| (tx < (xs[j] - xs[i]) * (ty - ys[i]) / (ys[j] - ys[i]) + xs[i]))
|
| inside = !inside;
|
| if(inside)
|
| hits++;
|
| }
|
| if(hits == 0)
|
| continue;
|
| AiIconAAPlot(canvas, px, py, hits / 9.0, argb);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw MQL5 wordmark sized to fill its box |
|
| //+------------------------------------------------------------------+
|
| void AiDrawLogoMQL5(CCanvas &canvas, const int x, const int y, const int w, const int h)
|
| {
|
| const int padX = 5;
|
| const int usableW = w - padX * 2;
|
| if(usableW <= 10)
|
| return;
|
| int fs = h;
|
| int tw = 0, th = 0;
|
| for(; fs >= 8; fs--)
|
| {
|
| tw = AiTextWidth("MQL", "Arial Black", fs) + AiTextWidth("5", "Arial Black", fs);
|
| th = AiTextHeight("Arial Black", fs);
|
| if(tw <= usableW && th <= h)
|
| break;
|
| }
|
| if(fs < 8)
|
| return;
|
| int tx = x + padX + (usableW - tw) / 2;
|
| int ty = y + (h - th) / 2;
|
| int wMQL = AiTextWidth("MQL", "Arial Black", fs);
|
| AiStampTextAA(canvas, tx, ty, "MQL", "Arial Black", fs, AI_BRAND_BLUE);
|
| AiStampTextAA(canvas, tx + wMQL, ty, "5", "Arial Black", fs, AI_BRAND_AMBER);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw layered AI orb logo |
|
| //+------------------------------------------------------------------+
|
| void AiDrawLogoOrb(CCanvas &canvas, const int x, const int y, const int size)
|
| {
|
| double cx = x + size / 2.0, cy = y + size / 2.0;
|
| double R = size / 2.0 - 1.0;
|
| if(R <= 1.0)
|
| return;
|
| AiVecDisc(canvas, cx, cy, R, AiColorWithPercentOpacity(AI_ORB_LIVE, 12));
|
| AiVecDisc(canvas, cx, cy, R * 0.94, AiColorWithPercentOpacity(AI_ORB_LIVE, 20));
|
| AiVecDisc(canvas, cx, cy, R * 0.88, ColorToARGB(AI_ORB_RIM, 255));
|
| AiVecDisc(canvas, cx, cy, R * 0.82, ColorToARGB(AI_ORB_LIVE, 255));
|
| AiVecDisc(canvas, cx + R * 0.10, cy + R * 0.12, R * 0.74, ColorToARGB(AI_ORB_MID, 255));
|
| AiVecDisc(canvas, cx + R * 0.20, cy + R * 0.24, R * 0.60, ColorToARGB(AI_ORB_DEEP, 255));
|
| AiVecDisc(canvas, cx - R * 0.34, cy - R * 0.36, R * 0.20,
|
| AiColorWithPercentOpacity(clrWhite, 45));
|
| int fs = (int)MathRound(size * 0.34);
|
| if(fs >= 7)
|
| {
|
| int tw = AiTextWidth("AI", "Arial Black", fs);
|
| int th = AiTextHeight("Arial Black", fs);
|
| AiStampTextAA(canvas, (int)MathRound(cx - tw / 2.0), (int)MathRound(cy - th / 2.0),
|
| "AI", "Arial Black", fs, clrWhite);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw close/delete X icon |
|
| //+------------------------------------------------------------------+
|
| void AiDrawIconX(CCanvas &canvas, const int x, const int y, const int size, const uint argb)
|
| {
|
| //--- Span 20%-80% of the box at a flat 2px weight
|
| const double t = 2.0;
|
| AiVecStroke(canvas, x + size * 0.20, y + size * 0.20, x + size * 0.80, y + size * 0.80, t, argb);
|
| AiVecStroke(canvas, x + size * 0.80, y + size * 0.20, x + size * 0.20, y + size * 0.80, t, argb);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw sun icon with disc and spokes |
|
| //+------------------------------------------------------------------+
|
| void AiDrawIconSun(CCanvas &canvas, const int x, const int y, const int size, const uint argb)
|
| {
|
| double cx = x + size / 2.0, cy = y + size / 2.0;
|
| const double t = 2.0;
|
| AiVecDisc(canvas, cx, cy, size * 0.17, argb);
|
| for(int i = 0; i < 8; i++)
|
| {
|
| double a = i * (M_PI / 4.0), cs = MathCos(a), sn = MathSin(a);
|
| AiVecStroke(canvas, cx + cs * size * 0.27, cy + sn * size * 0.27,
|
| cx + cs * size * 0.41, cy + sn * size * 0.41, t, argb);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw crescent moon icon with star |
|
| //+------------------------------------------------------------------+
|
| void AiDrawIconMoon(CCanvas &canvas, const int x, const int y, const int size,
|
| const uint argb, const uint bg)
|
| {
|
| double cx = x + size / 2.0, cy = y + size / 2.0;
|
| double R = MathMax(3.0, size * 0.34);
|
| double carveCx = cx + R * 0.5, carveCy = cy - R * 0.5;
|
| AiVecDisc(canvas, cx, cy, R, argb);
|
| AiVecDisc(canvas, carveCx, carveCy, R, bg);
|
| //--- star nestled in the crescent's opening, one point straight up
|
| double ax = carveCx - cx, ay = carveCy - cy;
|
| double al = MathSqrt(ax * ax + ay * ay);
|
| if(al < 1e-6)
|
| al = 1.0;
|
| double sCx = cx + ax / al * R * 0.60, sCy = cy + ay / al * R * 0.60;
|
| double sOut = R * 0.52, sIn = R * 0.21;
|
| double sx[10], sy[10];
|
| for(int v = 0; v < 10; v++)
|
| {
|
| double rr = (v % 2 == 0) ? sOut : sIn;
|
| double aa = -M_PI / 2.0 + v * (M_PI / 5.0);
|
| sx[v] = sCx + rr * MathCos(aa);
|
| sy[v] = sCy + rr * MathSin(aa);
|
| }
|
| AiVecPoly(canvas, sx, sy, 10, argb);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw magnifier search icon |
|
| //+------------------------------------------------------------------+
|
| void AiDrawIconSearch(CCanvas &canvas, const int x, const int y, const int size, const uint argb)
|
| {
|
| //--- Fill the lens with a 30% translucent wash folded into coverage
|
| double cx = x + size * 0.40, cy = y + size * 0.40;
|
| double r = size * 0.30;
|
| const double t = 2.0;
|
| const double fillPct = 0.30;
|
| //--- glass fill inside the lens
|
| double rIn = r - t * 0.5;
|
| for(int py = (int)MathFloor(cy - rIn - 1); py <= (int)MathCeil(cy + rIn + 1); py++)
|
| for(int px = (int)MathFloor(cx - rIn - 1); px <= (int)MathCeil(cx + rIn + 1); px++)
|
| {
|
| double dx = (px + 0.5) - cx, dy = (py + 0.5) - cy;
|
| double cov = 0.5 - (MathSqrt(dx * dx + dy * dy) - rIn);
|
| if(cov <= 0.0)
|
| continue;
|
| if(cov > 1.0)
|
| cov = 1.0;
|
| AiIconAAPlot(canvas, px, py, cov * fillPct, argb);
|
| }
|
| //--- ring and handle
|
| AiVecRing(canvas, cx, cy, r, t, argb);
|
| double k = 0.7071;
|
| AiVecStroke(canvas, cx + r * k, cy + r * k, x + size * 0.88, y + size * 0.88, t, argb);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw new-chat speech bubble icon |
|
| //+------------------------------------------------------------------+
|
| void AiDrawIconNewChat(CCanvas &canvas, const int x, const int y, const int size, const uint argb)
|
| {
|
| //--- Draw a solid disc with a white speech bubble on top
|
| double cx = x + size / 2.0, cy = y + size / 2.0;
|
| AiVecDisc(canvas, cx, cy, size / 2.0 - 0.5, argb);
|
| uint w = ColorToARGB(clrWhite, 255);
|
| double bw = size * 0.54, bh = size * 0.38;
|
| double bx = cx - bw / 2.0, by = cy - bh / 2.0 - size * 0.04;
|
| AiVecRoundRect(canvas, bx, by, bw, bh, size * 0.11, w);
|
| //--- tapered tail under the bubble's left third
|
| double tipX = bx + bw * 0.30, tipY = by + bh + size * 0.16;
|
| for(double s2 = 0.0; s2 <= 1.0; s2 += 0.05)
|
| {
|
| double ax = bx + bw * 0.18 + (tipX - (bx + bw * 0.18)) * s2;
|
| double bxx = bx + bw * 0.50 + (tipX - (bx + bw * 0.50)) * s2;
|
| double yy = by + bh + (tipY - (by + bh)) * s2;
|
| AiVecStroke(canvas, ax, yy, bxx, yy, 1.2, w);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw trash bin clear icon |
|
| //+------------------------------------------------------------------+
|
| void AiDrawIconClear(CCanvas &canvas, const int x, const int y, const int size, const uint argb)
|
| {
|
| const double t = 2.0;
|
| double cx = x + size / 2.0;
|
| double lidY = y + size * 0.26;
|
| //--- lid and handle
|
| AiVecStroke(canvas, x + size * 0.16, lidY, x + size * 0.84, lidY, t, argb);
|
| AiVecStroke(canvas, cx - size * 0.13, y + size * 0.16, cx + size * 0.13, y + size * 0.16, t, argb);
|
| AiVecStroke(canvas, cx - size * 0.13, y + size * 0.16, cx - size * 0.13, lidY, t, argb);
|
| AiVecStroke(canvas, cx + size * 0.13, y + size * 0.16, cx + size * 0.13, lidY, t, argb);
|
| //--- body: tapered sides plus base
|
| double bTop = lidY + t * 0.5, bBot = y + size * 0.84;
|
| AiVecStroke(canvas, x + size * 0.24, bTop, x + size * 0.30, bBot, t, argb);
|
| AiVecStroke(canvas, x + size * 0.76, bTop, x + size * 0.70, bBot, t, argb);
|
| AiVecStroke(canvas, x + size * 0.30, bBot, x + size * 0.70, bBot, t, argb);
|
| //--- ribs
|
| AiVecStroke(canvas, cx - size * 0.10, bTop + size * 0.10, cx - size * 0.09, bBot - size * 0.08, t, argb);
|
| AiVecStroke(canvas, cx + size * 0.10, bTop + size * 0.10, cx + size * 0.09, bBot - size * 0.08, t, argb);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw clock-with-arrow history icon |
|
| //+------------------------------------------------------------------+
|
| void AiDrawIconHistory(CCanvas &canvas, const int x, const int y, const int size, const uint argb)
|
| {
|
| double cx = x + size * 0.52, cy = y + size * 0.54;
|
| double R = size * 0.32;
|
| const double t = 2.0;
|
| //--- clock face, open at the upper-left so the arrow can enter
|
| AiVecRing(canvas, cx, cy, R, t, argb);
|
| //--- hands: 10 o'clock and 3 o'clock
|
| AiVecStroke(canvas, cx, cy, cx, cy - R * 0.62, t, argb);
|
| AiVecStroke(canvas, cx, cy, cx + R * 0.55, cy + R * 0.10, t, argb);
|
| //--- return arrow head at the upper-left, pointing back into the dial
|
| double ax = x + size * 0.14, ay = y + size * 0.30;
|
| AiVecStroke(canvas, ax, ay, ax + size * 0.16, ay - size * 0.04, t, argb);
|
| AiVecStroke(canvas, ax, ay, ax + size * 0.04, ay + size * 0.16, t, argb);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw sidebar collapse/expand toggle icon |
|
| //+------------------------------------------------------------------+
|
| void AiDrawIconToggle(CCanvas &canvas, const int x, const int y, const int size,
|
| const bool pointLeft, const uint argb)
|
| {
|
| //--- Snap the 1px border to pixel centers so the stroke reads solid
|
| const double t = 1.0;
|
| const double L = MathFloor(x + 1.0) + 0.5, R = MathFloor(x + size - 1.0) + 0.5;
|
| const double T = MathFloor(y + 1.0) + 0.5, B = MathFloor(y + size - 1.0) + 0.5;
|
| const double w = R - L, h = B - T;
|
| if(w <= 6.0 || h <= 6.0)
|
| return;
|
| //--- Fix the corner radius at 4px regardless of icon size
|
| const double rad = MathMin(4.0, MathMin(w, h) * 0.5);
|
| //--- narrow column, on the side the action points to
|
| const double colW = w * 0.32;
|
| const double divX = pointLeft ? (L + colW) : (R - colW);
|
| //--- Fill the column at 30%, folded into coverage since plotting ignores color alpha
|
| const double fillPct = 0.30;
|
| for(int py = (int)MathFloor(T); py <= (int)MathCeil(B); py++)
|
| for(int px = (int)MathFloor(L); px <= (int)MathCeil(R); px++)
|
| {
|
| double sx = px + 0.5, sy = py + 0.5;
|
| if(pointLeft ? (sx > divX) : (sx < divX))
|
| continue;
|
| double qx = MathAbs(sx - (L + w * 0.5)) - (w * 0.5 - rad);
|
| double qy = MathAbs(sy - (T + h * 0.5)) - (h * 0.5 - rad);
|
| double ax = MathMax(qx, 0.0), ay = MathMax(qy, 0.0);
|
| double cov = 0.5 - (MathSqrt(ax * ax + ay * ay) + MathMin(MathMax(qx, qy), 0.0) - rad);
|
| if(cov <= 0.0)
|
| continue;
|
| if(cov > 1.0)
|
| cov = 1.0;
|
| AiIconAAPlot(canvas, px, py, cov * fillPct, argb);
|
| }
|
| //--- 2) frame, single uniform ring
|
| AiVecRoundRectRing(canvas, L, T, w, h, rad, t, argb);
|
| //--- 3) divider
|
| AiVecStroke(canvas, divX, T, divX, B, t, argb);
|
| //--- Define the chevron by arm length and half-angle to set the mouth opening
|
| const double armLen = size * 0.15; // short arms: compact mark
|
| const double halfAngle = 58.0 * M_PI / 180.0; // 116 degree mouth - wide and open
|
| double openL = pointLeft ? divX : L;
|
| double openR = pointLeft ? R : divX;
|
| double ccx = (openL + openR) / 2.0;
|
| double ccy = (T + B) / 2.0;
|
| double s = pointLeft ? 1.0 : -1.0;
|
| double dx = armLen * MathCos(halfAngle);
|
| double dy = armLen * MathSin(halfAngle);
|
| double vx = ccx - s * dx * 0.5; // vertex, nudged so the V sits centered
|
| AiVecStroke(canvas, vx, ccy, vx + s * dx, ccy - dy, t, argb);
|
| AiVecStroke(canvas, vx, ccy, vx + s * dx, ccy + dy, t, argb);
|
| }
|
|
|
|
|
|
|
| //+------------------------------------------------------------------+
|
| //| 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);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw a colored image/picture icon with its own palette |
|
| //+------------------------------------------------------------------+
|
| void AiDrawIconImage(CCanvas &canvas, int x, int y, int size, uint argb)
|
| {
|
| const int L = x + 1, T = y + 2, R = x + size - 1, B = y + size - 2;
|
| const uint frame = ColorToARGB(C'70,130,225', 255); // blue frame
|
| const uint paper = ColorToARGB(C'238,242,250', 255); // light paper
|
| const uint sun = ColorToARGB(C'245,190,60', 255); // yellow sun
|
| const uint mtn = ColorToARGB(C'70,175,110', 255); // green mountain
|
| //--- Paper fill + double-line frame
|
| canvas.FillRectangle(L + 1, T + 1, R - 1, B - 1, paper);
|
| canvas.Rectangle(L, T, R, B, frame);
|
| canvas.Rectangle(L + 1, T + 1, R - 1, B - 1, frame);
|
| //--- Sun (top-left)
|
| const int scx = L + (R - L) / 3;
|
| const int scy = T + (B - T) / 3;
|
| const int sr = MathMax(1, size / 8);
|
| for(int yy = -sr; yy <= sr; yy++)
|
| for(int xx = -sr; xx <= sr; xx++)
|
| if(xx * xx + yy * yy <= sr * sr)
|
| canvas.PixelSet(scx + xx, scy + yy, sun);
|
| //--- Mountain (filled triangle rising to the middle)
|
| const int mBase = B - 2;
|
| const int peakX = L + (R - L) / 2;
|
| const int peakY = T + (B - T) / 2;
|
| for(int yy = peakY; yy <= mBase; yy++)
|
| {
|
| double t = (double)(yy - peakY) / (double)(mBase - peakY + 1);
|
| int half = (int)(t * (double)(R - L) / 2.0);
|
| canvas.LineHorizontal(peakX - half, peakX + half, yy, mtn);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| 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
|
| //+------------------------------------------------------------------+
|