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
|
|
//+------------------------------------------------------------------+
|