2903 lines
117 KiB
MQL5
2903 lines
117 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| AI Canvas Primitives.mqh |
| |||
//| Copyright 2026, Allan Munene Mutiiria. |
| |||
//| https://t.me/Forex_Algo_Trader |
| |||
//+------------------------------------------------------------------+
| |||
#property copyright "Copyright 2026, Allan Munene Mutiiria."
| |||
#property link "https://t.me/Forex_Algo_Trader"
| |||
| |||
//--- Include guard
| |||
#ifndef AI_CANVAS_PRIMITIVES_MQH
| |||
#define AI_CANVAS_PRIMITIVES_MQH
| |||
| |||
//--- Include required libraries
| |||
#include <Canvas/Canvas.mqh>
| |||
#include "AI Canvas Theme.mqh"
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Fast canvas subclass exposing direct pixel buffer access |
| |||
//+------------------------------------------------------------------+
| |||
class CAiCanvasFast : public CCanvas
| |||
{
| |||
public:
| |||
//+---------------------------------------------------------------+
| |||
//| Read pixel directly from buffer |
| |||
//+---------------------------------------------------------------+
| |||
uint GetPixelDirect(int x, int y) const
| |||
{
| |||
//--- Direct row-major buffer read
| |||
return m_pixels[y * Width() + x];
| |||
}
| |||
| |||
//+---------------------------------------------------------------+
| |||
//| Write pixel directly to buffer |
| |||
//+---------------------------------------------------------------+
| |||
void SetPixelDirect(int x, int y, uint v)
| |||
{
| |||
//--- Direct row-major buffer write
| |||
m_pixels[y * Width() + x] = v;
| |||
}
| |||
| |||
//+---------------------------------------------------------------+
| |||
//| Copy rectangular region from source canvas to this canvas |
| |||
//+---------------------------------------------------------------+
| |||
void CopyRectFromCanvas(CCanvas &src, int l, int t, int r, int b)
| |||
{
| |||
//--- Get source and destination dimensions
| |||
const int sw = src.Width();
| |||
const int sh = src.Height();
| |||
const int dw = Width();
| |||
const int dh = Height();
| |||
//--- Clamp rectangle bounds to both canvases
| |||
const int cl = MathMax(0, l);
| |||
const int ct = MathMax(0, t);
| |||
const int cr = MathMin(MathMin(r, sw), dw);
| |||
const int cb = MathMin(MathMin(b, sh), dh);
| |||
//--- Copy row by row
| |||
for(int yy = ct; yy < cb; yy++)
| |||
{
| |||
const int rowBase = yy * dw;
| |||
for(int xx = cl; xx < cr; xx++)
| |||
m_pixels[rowBase + xx] = src.PixelGet(xx, yy);
| |||
}
| |||
}
| |||
| |||
//+---------------------------------------------------------------+
| |||
//| Copy rectangular region from this canvas to destination |
| |||
//+---------------------------------------------------------------+
| |||
void CopyRectToCanvas(CCanvas &dst, int l, int t, int r, int b)
| |||
{
| |||
//--- Get destination and source dimensions
| |||
const int dwOther = dst.Width();
| |||
const int dhOther = dst.Height();
| |||
const int sw = Width();
| |||
const int sh = Height();
| |||
//--- Clamp rectangle bounds
| |||
const int cl = MathMax(0, l);
| |||
const int ct = MathMax(0, t);
| |||
const int cr = MathMin(MathMin(r, sw), dwOther);
| |||
const int cb = MathMin(MathMin(b, sh), dhOther);
| |||
//--- Copy row by row
| |||
for(int yy = ct; yy < cb; yy++)
| |||
{
| |||
const int rowBase = yy * sw;
| |||
for(int xx = cl; xx < cr; xx++)
| |||
dst.PixelSet(xx, yy, m_pixels[rowBase + xx]);
| |||
}
| |||
}
| |||
| |||
//+---------------------------------------------------------------+
| |||
//| 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 centre (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 centres 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 colour 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 centred
| |||
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);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| 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
| |||
//+------------------------------------------------------------------+
|