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