Article-24337-Working-With-.../OMV Canvas.mqh

1294 lines
52 KiB
MQL5

//+------------------------------------------------------------------+
//| OMV Canvas.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"
#ifndef OMV_CANVAS_MQH
#define OMV_CANVAS_MQH
#include <Canvas/Canvas.mqh>
//+------------------------------------------------------------------+
//| Text metric cache size |
//+------------------------------------------------------------------+
#define OC_CACHE 96 // Measured strings remembered between frames
//+------------------------------------------------------------------+
//| Report how much of a pixel an edge covers |
//+------------------------------------------------------------------+
double OmvCoverage(const double signedDist)
{
//--- Report full cover well inside the edge
if(signedDist <= -0.5)
return(1.0);
//--- Report no cover well outside the edge
if(signedDist >= 0.5)
return(0.0);
//--- Ramp linearly across the one pixel band on the edge
return(0.5 - signedDist);
}
//+------------------------------------------------------------------+
//| Test whether a point falls inside a polygon |
//+------------------------------------------------------------------+
bool OmvPtInPoly(const double &vx[], const double &vy[], const int n, const double px, const double py)
{
//--- Toggle the state on every edge a ray from the point crosses
bool inside = false;
int j = n - 1;
for(int i = 0; i < n; i++)
{
//--- Cross this edge only when it spans the test height
if(((vy[i] > py) != (vy[j] > py)) &&
(px < (vx[j] - vx[i]) * (py - vy[i]) / (vy[j] - vy[i]) + vx[i]))
inside = !inside;
//--- Carry this vertex forward as the previous one
j = i;
}
//--- Report the state the crossings left behind
return inside;
}
//+------------------------------------------------------------------+
//| Drawing surface with blended primitives and cached metrics |
//+------------------------------------------------------------------+
class COmvCanvas : public CCanvas
{
private:
// Remember the font size last applied
int m_fontSize;
// Remember whether the font last applied was bold
bool m_fontBold;
// Store the strings already measured
string m_key[OC_CACHE];
// Store the width measured for each string
int m_wide[OC_CACHE];
// Store the height measured for each string
int m_high[OC_CACHE];
// Count the entries in use
int m_cached;
// Bound the left edge drawing may reach
int m_clipL;
// Bound the top edge drawing may reach
int m_clipT;
// Bound the right edge drawing may reach
int m_clipR;
// Bound the bottom edge drawing may reach
int m_clipB;
public:
COmvCanvas(void);
void SetClip(const int x1, const int y1, const int x2, const int y2);
void NoClip(void);
bool InClip(const int x1, const int y1, const int x2, const int y2) const;
void FillRect(int x1, int y1, int x2, int y2, const uint clr);
void Blend(const int x, const int y, const uint src);
void BlendCov(const int x, const int y, const uint rgb, const double srcAlpha);
void DiscAA(const int cx, const int cy, const int radius, const uint argb);
void CrescentAA(const int cx, const int cy, const double rOut,
const double offX, const double offY, const double rIn,
const uint argb);
void StrokeAA(const double x0, const double y0, const double x1, const double y1,
const int thick, const uint argb);
void PolyAA(const double &vx[], const double &vy[], const int n, const uint argb);
void PathAA(const double &px[], const double &py[], const int n,
const int thick, const uint argb);
void WireAA(const double x0, const double y0,
const double x1, const double y1, const uint argb);
void FlattenTo(double &fx[], double &fy[], int &m,
const double x0, const double y0,
const double cx1, const double cy1,
const double cx2, const double cy2,
const double x1, const double y1);
void SplineAA(const double &px[], const double &py[], const int n,
const int thick, const uint argb);
void CornerQuad(const int cx, const int cy, const int radius, const uint argb,
const int signX, const int signY);
void RoundFill(int x, int y, int w, int h, int r, const uint argb);
void RoundFillSel(int x, int y, int w, int h, int r, const uint argb,
const bool rTL, const bool rTR,
const bool rBL, const bool rBR);
void RoundStroke(int x, int y, int w, int h, int r, const int t, const uint argb);
void Elbow(const double ax1, const double ay1, const double ax2, const double ay2,
const double bend, const double head, const uint argb);
void Font(const int size, const bool bold = false);
int TextWide(const string text);
int TextHigh(const string text);
void TxtL(const int x, const int y, const string text, const uint clr);
void TxtC(const int x, const int y, const string text, const uint clr);
void TxtR(const int x, const int y, const string text, const uint clr);
void TxtM(const int x, const int y, const string text, const uint clr);
void SaveTo(uint &dst[]);
void RestoreFrom(const uint &src[]);
int PixelCount(void) const { return ArraySize(m_pixels); }
int FitFont(const string text, const int maxWide, const int maxHigh,
const int best, const int least, const bool bold);
int WrapText(const string text, const int wide, string &out[],
const int most);
void HRule(const int x1, const int x2, const int y, const uint clr);
void VRule(const int x, const int y1, const int y2, const uint clr);
};
//+------------------------------------------------------------------+
//| Construct with an empty metric cache |
//+------------------------------------------------------------------+
COmvCanvas::COmvCanvas(void)
{
//--- Force the first Font call to apply
m_fontSize = -1;
m_fontBold = false;
//--- Start with nothing measured
m_cached = 0;
//--- Start with the clip wide open
m_clipL = -1000000;
m_clipT = -1000000;
m_clipR = 1000000;
m_clipB = 1000000;
}
//+------------------------------------------------------------------+
//| Restrict drawing to a rectangle |
//+------------------------------------------------------------------+
void COmvCanvas::SetClip(const int x1, const int y1, const int x2, const int y2)
{
//--- Store the bounds every later call is held inside
m_clipL = x1;
m_clipT = y1;
m_clipR = x2;
m_clipB = y2;
}
//+------------------------------------------------------------------+
//| Open the clip back to the whole canvas |
//+------------------------------------------------------------------+
void COmvCanvas::NoClip(void)
{
//--- Push the bounds far outside anything that will be drawn
m_clipL = -1000000;
m_clipT = -1000000;
m_clipR = 1000000;
m_clipB = 1000000;
}
//+------------------------------------------------------------------+
//| Report whether a rectangle lies wholly inside the clip |
//+------------------------------------------------------------------+
bool COmvCanvas::InClip(const int x1, const int y1, const int x2, const int y2) const
{
//--- Report a rectangle no edge of which crosses the bounds
return (x1 >= m_clipL && y1 >= m_clipT && x2 <= m_clipR && y2 <= m_clipB);
}
//+------------------------------------------------------------------+
//| Fill a rectangle clamped to the clip |
//+------------------------------------------------------------------+
void COmvCanvas::FillRect(int x1, int y1, int x2, int y2, const uint clr)
{
//--- Pull every edge back inside the bounds
if(x1 < m_clipL)
x1 = m_clipL;
if(y1 < m_clipT)
y1 = m_clipT;
if(x2 > m_clipR)
x2 = m_clipR;
if(y2 > m_clipB)
y2 = m_clipB;
//--- Skip a rectangle the clip left with no area
if(x2 < x1 || y2 < y1)
return;
//--- Overwrite outright when the color is solid, which is the common case
if(((clr >> 24) & 0xFF) == 255)
{
FillRectangle(x1, y1, x2, y2, clr);
return;
}
//--- Blend a translucent color so whatever lies beneath still shows
for(int y = y1; y <= y2; y++)
for(int x = x1; x <= x2; x++)
Blend(x, y, clr);
}
//+------------------------------------------------------------------+
//| Blend one source pixel over what the buffer already holds |
//+------------------------------------------------------------------+
void COmvCanvas::Blend(const int x, const int y, const uint src)
{
//--- Drop anything outside the buffer or outside the clip
if(x < 0 || x >= m_width || y < 0 || y >= m_height)
return;
if(x < m_clipL || x > m_clipR || y < m_clipT || y > m_clipB)
return;
//--- Locate the pixel in the flat buffer
const int idx = y * m_width + x;
//--- Read the source alpha
const uint sa8 = (src >> 24) & 0xFF;
//--- Leave the buffer untouched for a fully clear source
if(sa8 == 0)
return;
//--- Overwrite outright for a fully opaque source
if(sa8 == 255)
{
m_pixels[idx] = src;
return;
}
//--- Read what the buffer holds underneath
const uint dst = m_pixels[idx];
//--- Overwrite outright when nothing lies underneath
if(((dst >> 24) & 0xFF) == 0)
{
m_pixels[idx] = src;
return;
}
//--- Split both colors into normalized channels
const double sA = ((src >> 24) & 0xFF) / 255.0, sR = ((src >> 16) & 0xFF) / 255.0;
const double sG = ((src >> 8) & 0xFF) / 255.0, sB = (src & 0xFF) / 255.0;
const double dA = ((dst >> 24) & 0xFF) / 255.0, dR = ((dst >> 16) & 0xFF) / 255.0;
const double dG = ((dst >> 8) & 0xFF) / 255.0, dB = (dst & 0xFF) / 255.0;
//--- Combine the two alphas
const double oA = sA + dA * (1.0 - sA);
//--- Clear the pixel when the combination is fully transparent
if(oA == 0.0)
{
m_pixels[idx] = 0;
return;
}
//--- Write the composited result back as one packed value
m_pixels[idx] = ((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);
}
//+------------------------------------------------------------------+
//| Blend one pixel at a fractional coverage |
//+------------------------------------------------------------------+
void COmvCanvas::BlendCov(const int x, const int y, const uint rgb, const double srcAlpha)
{
//--- Skip a coverage too small to see
if(srcAlpha <= 0.0)
return;
//--- Clamp the coverage to a full pixel
double a = srcAlpha;
if(a > 1.0)
a = 1.0;
//--- Blend the color at the alpha the coverage implies
Blend(x, y, (rgb & 0x00FFFFFF) | ((uint)(uchar)(a * 255.0 + 0.5) << 24));
}
//+------------------------------------------------------------------+
//| Fill a supersampled disc |
//+------------------------------------------------------------------+
void COmvCanvas::DiscAA(const int cx, const int cy, const int radius, const uint argb)
{
//--- Skip a disc with no radius
if(radius <= 0)
return;
//--- Split the color so coverage can drive the alpha
const double rd = (double)radius;
const uchar bA = (uchar)((argb >> 24) & 0xFF);
const uint rgb = argb & 0x00FFFFFF;
//--- Sample each edge pixel on an eight by eight grid
const int sub = 8;
const double step = 1.0 / sub;
const int subSq = sub * sub;
//--- Visit every pixel of the bounding box
for(int dy = -radius - 1; dy <= radius + 1; dy++)
for(int dx = -radius - 1; dx <= radius + 1; dx++)
{
//--- Measure this pixel against the disc edge
const double dist = MathSqrt((double)(dx * dx + dy * dy));
//--- Skip a pixel well outside the disc
if(dist > rd + 1.0)
continue;
//--- Write a pixel well inside the disc solid
if(dist <= rd - 1.0)
{
Blend(cx + dx, cy + dy, argb);
continue;
}
//--- Count how many subsamples fall inside the disc
int inside = 0;
for(int sy = 0; sy < sub; sy++)
for(int sx = 0; sx < sub; sx++)
{
//--- Take this subsample against the disc center
const double sdx = (double)dx - 0.5 + (sx + 0.5) * step;
const double sdy = (double)dy - 0.5 + (sy + 0.5) * step;
if(sdx * sdx + sdy * sdy <= rd * rd)
inside++;
}
//--- Skip a pixel the disc never reaches
if(inside == 0)
continue;
//--- Blend the pixel at the coverage the samples measured
BlendCov(cx + dx, cy + dy, rgb, (double)bA / 255.0 * (double)inside / (double)subSq);
}
}
//+------------------------------------------------------------------+
//| Fill a crescent as one shape rather than two discs |
//+------------------------------------------------------------------+
void COmvCanvas::CrescentAA(const int cx, const int cy, const double rOut,
const double offX, const double offY, const double rIn,
const uint argb)
{
//--- Split the color so coverage can drive the alpha
const uint rgb = argb & 0x00FFFFFF;
const double bA = (double)((argb >> 24) & 0xFF) / 255.0;
//--- Bound the pixels the outer disc can reach
const int ext = (int)MathCeil(rOut + 1);
//--- Sample each pixel on an eight by eight grid
const int sub = 8;
const double step = 1.0 / sub;
const int subSq = sub * sub;
//--- Visit every pixel inside those bounds
for(int dy = -ext; dy <= ext; dy++)
for(int dx = -ext; dx <= ext; dx++)
{
//--- Count subsamples inside the outer disc and outside the inner one
int inside = 0;
for(int sy = 0; sy < sub; sy++)
for(int sx = 0; sx < sub; sx++)
{
//--- Take this subsample against the outer disc
const double px = (double)dx - 0.5 + (sx + 0.5) * step;
const double py = (double)dy - 0.5 + (sy + 0.5) * step;
if(px * px + py * py > rOut * rOut)
continue;
//--- Reject it when the inner disc carves it away
const double ix = px - offX, iy = py - offY;
if(ix * ix + iy * iy <= rIn * rIn)
continue;
inside++;
}
//--- Skip a pixel the crescent never reaches
if(inside == 0)
continue;
//--- Blend the pixel at the coverage the samples measured
BlendCov(cx + dx, cy + dy, rgb, bA * (double)inside / (double)subSq);
}
}
//+------------------------------------------------------------------+
//| Draw a smoothed line of a given thickness |
//+------------------------------------------------------------------+
void COmvCanvas::StrokeAA(const double x0, const double y0, const double x1, const double y1,
const int thick, const uint argb)
{
//--- Snap an odd width onto pixel centers, but only for an axis aligned run
const bool level = (MathAbs(y1 - y0) < 0.001);
const bool plumb = (MathAbs(x1 - x0) < 0.001);
const bool snap = (((thick < 1) ? 1 : thick) % 2 == 1) && (level || plumb);
//--- Leave a sloped or curved run on its true coordinates
const double ax = snap ? MathFloor(x0) + 0.5 : x0;
const double ay = snap ? MathFloor(y0) + 0.5 : y0;
const double bx = snap ? MathFloor(x1) + 0.5 : x1;
const double by = snap ? MathFloor(y1) + 0.5 : y1;
//--- Measure the line so its perpendicular distance can be tested
const double dx = bx - ax, dy = by - ay;
const double len = MathSqrt(dx * dx + dy * dy);
//--- Draw a dot when both ends coincide
if(len < 1e-9)
{
DiscAA((int)MathRound(ax), (int)MathRound(ay), (thick > 1) ? thick / 2 : 1, argb);
return;
}
//--- Take the unit direction along the line
const double ux = dx / len, uy = dy / len;
//--- Take half the thickness as the distance the line reaches
const double half = ((thick < 1) ? 1 : thick) * 0.5;
//--- Split the color so coverage can drive the alpha
const uchar bA = (uchar)((argb >> 24) & 0xFF);
const uint rgb = argb & 0x00FFFFFF;
//--- Bound the pixels the line can possibly touch
const int minX = (int)MathFloor(MathMin(ax, bx) - half - 1.0);
const int maxX = (int)MathCeil(MathMax(ax, bx) + half + 1.0);
const int minY = (int)MathFloor(MathMin(ay, by) - half - 1.0);
const int maxY = (int)MathCeil(MathMax(ay, by) + half + 1.0);
//--- Visit every pixel inside those bounds
for(int py = minY; py <= maxY; py++)
for(int px = minX; px <= maxX; px++)
{
//--- Project this pixel onto the line
const double rx = px + 0.5 - ax, ry = py + 0.5 - ay;
double along = rx * ux + ry * uy;
//--- Clamp the projection to the segment ends
if(along < 0.0)
along = 0.0;
if(along > len)
along = len;
//--- Measure how far the pixel sits from the segment
const double ox = rx - ux * along, oy = ry - uy * along;
const double dist = MathSqrt(ox * ox + oy * oy);
//--- Take the coverage the distance implies
const double cov = OmvCoverage(dist - half);
//--- Skip a pixel the line never touches
if(cov <= 0.0)
continue;
//--- Blend the pixel at that coverage
BlendCov(px, py, rgb, (double)bA / 255.0 * cov);
}
}
//+------------------------------------------------------------------+
//| Draw a thin line touching only the pixels it covers |
//+------------------------------------------------------------------+
void COmvCanvas::WireAA(const double x0, const double y0, const double x1, const double y1,
const uint argb)
{
//--- Split the color so the coverage can drive the alpha
const double baseA = (double)((argb >> 24) & 0xFF) / 255.0;
const uint rgb = argb & 0x00FFFFFF;
//--- Nothing to draw once the color is fully clear
if(baseA <= 0.0)
return;
//--- Walk the axis the line travels furthest along
double ax = x0, ay = y0, bx = x1, by = y1;
const bool steep = (MathAbs(by - ay) > MathAbs(bx - ax));
//--- Turn a steep line on its side so the walk is always one step per column
if(steep)
{
double t = ax;
ax = ay;
ay = t;
t = bx;
bx = by;
by = t;
}
//--- Walk from left to right whichever way the caller gave it
if(ax > bx)
{
double t = ax;
ax = bx;
bx = t;
t = ay;
ay = by;
by = t;
}
//--- Take how far the line rises for each column it crosses
const double dx = bx - ax, dy = by - ay;
const double slope = (MathAbs(dx) > 1e-9) ? dy / dx : 0.0;
//--- Bound the walk to the columns the canvas can show
int from = (int)MathFloor(ax), to = (int)MathCeil(bx);
if(to < from)
return;
//--- Step one column at a time, splitting the coverage across two pixels
double at = ay + slope * (from - ax);
for(int px = from; px <= to; px++)
{
//--- Take the two rows this column falls between
const int row = (int)MathFloor(at);
const double part = at - row;
//--- Give each row the share of the pixel the line actually covers
if(steep)
{
BlendCov(row, px, rgb, baseA * (1.0 - part));
BlendCov(row + 1, px, rgb, baseA * part);
}
else
{
BlendCov(px, row, rgb, baseA * (1.0 - part));
BlendCov(px, row + 1, rgb, baseA * part);
}
at += slope;
}
}
//+------------------------------------------------------------------+
//| Stroke a whole path in one pass so joints cannot double |
//+------------------------------------------------------------------+
void COmvCanvas::PathAA(const double &px[], const double &py[], const int n,
const int thick, const uint argb)
{
//--- Nothing to stroke without at least one segment
if(n < 2)
return;
//--- Take half the thickness as the distance the path reaches
const double half = ((thick < 1) ? 1 : thick) * 0.5;
//--- Bound every pixel the path can possibly touch
double minX = px[0], maxX = px[0], minY = py[0], maxY = py[0];
for(int i = 1; i < n; i++)
{
//--- Widen the bounds to hold this point
if(px[i] < minX)
minX = px[i];
if(px[i] > maxX)
maxX = px[i];
if(py[i] < minY)
minY = py[i];
if(py[i] > maxY)
maxY = py[i];
}
//--- Turn the bounds into a pixel rectangle with room for the width
const int x0 = (int)MathFloor(minX - half - 1.0), x1 = (int)MathCeil(maxX + half + 1.0);
const int y0 = (int)MathFloor(minY - half - 1.0), y1 = (int)MathCeil(maxY + half + 1.0);
const int w = x1 - x0 + 1, h = y1 - y0 + 1;
//--- Refuse a rectangle too large to hold a coverage map for
if(w <= 0 || h <= 0 || w * h > 1000000)
return;
//--- Collect the greatest coverage any segment gives each pixel
double cover[];
ArrayResize(cover, w * h);
ArrayInitialize(cover, 0.0);
//--- Visit each segment over its own small neighbourhood
for(int s = 0; s + 1 < n; s++)
{
//--- Take this segment and measure it
const double ax = px[s], ay = py[s], bx = px[s + 1], by = py[s + 1];
const double dx = bx - ax, dy = by - ay;
const double len2 = dx * dx + dy * dy;
//--- Bound the pixels this segment alone can touch
const int sx0 = (int)MathFloor(MathMin(ax, bx) - half - 1.0);
const int sx1 = (int)MathCeil(MathMax(ax, bx) + half + 1.0);
const int sy0 = (int)MathFloor(MathMin(ay, by) - half - 1.0);
const int sy1 = (int)MathCeil(MathMax(ay, by) + half + 1.0);
//--- Measure every pixel in that neighbourhood against the segment
for(int qy = sy0; qy <= sy1; qy++)
for(int qx = sx0; qx <= sx1; qx++)
{
//--- Project the pixel onto the segment
const double rx = qx + 0.5 - ax, ry = qy + 0.5 - ay;
double along = (len2 > 1e-9) ? (rx * dx + ry * dy) / len2 : 0.0;
//--- Clamp the projection to the segment ends
if(along < 0.0)
along = 0.0;
if(along > 1.0)
along = 1.0;
//--- Take the coverage the distance implies
const double ox = rx - dx * along, oy = ry - dy * along;
const double got = OmvCoverage(MathSqrt(ox * ox + oy * oy) - half);
//--- Keep only the greatest coverage, never the sum
const int idx = (qy - y0) * w + (qx - x0);
if(idx >= 0 && idx < w * h && got > cover[idx])
cover[idx] = got;
}
}
//--- Split the color so the collected coverage can drive the alpha
const uchar bA = (uchar)((argb >> 24) & 0xFF);
const uint rgb = argb & 0x00FFFFFF;
//--- Blend every covered pixel exactly once
for(int qy = 0; qy < h; qy++)
for(int qx = 0; qx < w; qx++)
{
//--- Skip a pixel the path never reached
const double got = cover[qy * w + qx];
if(got <= 0.0)
continue;
BlendCov(x0 + qx, y0 + qy, rgb, (double)bA / 255.0 * got);
}
}
//+------------------------------------------------------------------+
//| Fill a supersampled polygon |
//+------------------------------------------------------------------+
void COmvCanvas::PolyAA(const double &vx[], const double &vy[], const int n, const uint argb)
{
//--- Skip anything with too few vertices to enclose an area
if(n < 3)
return;
//--- Bound the pixels the polygon can possibly touch
double minX = vx[0], maxX = vx[0], minY = vy[0], maxY = vy[0];
for(int i = 1; i < n; i++)
{
//--- Widen the bounds to hold this vertex
if(vx[i] < minX)
minX = vx[i];
if(vx[i] > maxX)
maxX = vx[i];
if(vy[i] < minY)
minY = vy[i];
if(vy[i] > maxY)
maxY = vy[i];
}
//--- Split the color so coverage can drive the alpha
const uchar bA = (uchar)((argb >> 24) & 0xFF);
const uint rgb = argb & 0x00FFFFFF;
//--- Sample each pixel on a four by four grid
const int sub = 4;
const double step = 1.0 / sub;
const int subSq = sub * sub;
//--- Visit every pixel inside the bounds
for(int py = (int)MathFloor(minY); py <= (int)MathCeil(maxY); py++)
for(int px = (int)MathFloor(minX); px <= (int)MathCeil(maxX); px++)
{
//--- Count how many subsamples fall inside the polygon
int cnt = 0;
for(int sy = 0; sy < sub; sy++)
for(int sx = 0; sx < sub; sx++)
if(OmvPtInPoly(vx, vy, n, px + (sx + 0.5) * step, py + (sy + 0.5) * step))
cnt++;
//--- Skip a pixel the polygon never reaches
if(cnt == 0)
continue;
//--- Blend the pixel at the coverage the samples measured
BlendCov(px, py, rgb, (double)bA / 255.0 * (double)cnt / (double)subSq);
}
}
//+------------------------------------------------------------------+
//| Fill one supersampled quarter disc around a corner |
//+------------------------------------------------------------------+
void COmvCanvas::CornerQuad(const int cx, const int cy, const int radius, const uint argb,
const int signX, const int signY)
{
//--- Skip a corner with no radius
if(radius <= 0)
return;
//--- Split the color so coverage can drive the alpha
const double rd = (double)radius;
const uchar bA = (uchar)((argb >> 24) & 0xFF);
const uint rgb = argb & 0x00FFFFFF;
//--- Sample each edge pixel on an eight by eight grid
const int sub = 8;
const double step = 1.0 / sub;
const int subSq = sub * sub;
//--- Visit every pixel of the bounding box
for(int dy = -(radius + 1); dy <= (radius + 1); dy++)
for(int dx = -(radius + 1); dx <= (radius + 1); dx++)
{
//--- Keep only the quadrant the caller asked for
const bool inQ = ((signX > 0) ? (dx >= 0) : (dx <= 0)) &&
((signY > 0) ? (dy >= 0) : (dy <= 0));
if(!inQ)
continue;
//--- Measure this pixel against the arc
const double dist = MathSqrt((double)(dx * dx + dy * dy));
//--- Skip a pixel well outside the arc
if(dist > rd + 1.0)
continue;
//--- Write a pixel well inside the arc solid
if(dist <= rd - 1.0)
{
Blend(cx + dx, cy + dy, argb);
continue;
}
//--- Count how many subsamples fall inside the arc
int inside = 0;
for(int sy = 0; sy < sub; sy++)
for(int sx = 0; sx < sub; sx++)
{
//--- Take this subsample against the arc center
const double sdx = (double)dx - 0.5 + (sx + 0.5) * step;
const double sdy = (double)dy - 0.5 + (sy + 0.5) * step;
if(sdx * sdx + sdy * sdy <= rd * rd)
inside++;
}
//--- Skip a pixel the arc never reaches
if(inside == 0)
continue;
//--- Blend the pixel at the coverage the samples measured
BlendCov(cx + dx, cy + dy, rgb, (double)bA / 255.0 * (double)inside / (double)subSq);
}
}
//+------------------------------------------------------------------+
//| Sample a cubic bezier into an existing run of points |
//+------------------------------------------------------------------+
void COmvCanvas::FlattenTo(double &fx[], double &fy[], int &m,
const double x0, const double y0, const double cx1, const double cy1,
const double cx2, const double cy2, const double x1, const double y1)
{
//--- Take enough steps that no step spans more than about two pixels
const double reach = MathAbs(x1 - x0) + MathAbs(y1 - y0) +
MathAbs(cx1 - x0) + MathAbs(cy1 - y0);
int steps = (int)MathCeil(reach / 2.0);
if(steps < 4)
steps = 4;
if(steps > 48)
steps = 48;
//--- Append every sample past the start, which the run already holds
for(int i = 1; i <= steps; i++)
{
//--- Stop once the run is full
if(m >= ArraySize(fx))
return;
//--- Evaluate the curve at this step
const double t = (double)i / steps, u = 1.0 - t;
fx[m] = u * u * u * x0 + 3 * u * u * t * cx1 + 3 * u * t * t * cx2 + t * t * t * x1;
fy[m] = u * u * u * y0 + 3 * u * u * t * cy1 + 3 * u * t * t * cy2 + t * t * t * y1;
m++;
}
}
//+------------------------------------------------------------------+
//| Draw a uniform B spline through a run of points |
//+------------------------------------------------------------------+
void COmvCanvas::SplineAA(const double &px[], const double &py[], const int n,
const int thick, const uint argb)
{
//--- Join the points directly when there are too few to smooth
if(n < 3)
{
//--- Stroke whatever single segment the caller gave
if(n == 2)
PathAA(px, py, 2, thick, argb);
return;
}
//--- Flatten the whole spline into one run of points before stroking
double fx[], fy[];
int m = 0;
ArrayResize(fx, 512);
ArrayResize(fy, 512);
//--- Hold the two points behind the one being placed
double x0 = px[0], y0 = py[0];
double x1 = px[0], y1 = py[0];
double cx = px[0], cy = py[0];
int state = 0;
//--- Walk the run the way a uniform B spline is laid down
for(int i = 0; i < n; i++)
{
//--- Take the next point
const double x = px[i], y = py[i];
//--- The first point only anchors the run
if(state == 0)
{
state = 1;
cx = x;
cy = y;
fx[m] = x;
fy[m] = y;
m++;
}
else
if(state == 1)
state = 2;
else
{
//--- The third point starts the curve a sixth of the way along
if(state == 2)
{
state = 3;
cx = (5 * x0 + x1) / 6;
cy = (5 * y0 + y1) / 6;
if(m < 512)
{
fx[m] = cx;
fy[m] = cy;
m++;
}
}
//--- Sample the span between this point and the last
const double ex = (x0 + 4 * x1 + x) / 6, ey = (y0 + 4 * y1 + y) / 6;
FlattenTo(fx, fy, m, cx, cy, (2 * x0 + x1) / 3, (2 * y0 + y1) / 3,
(x0 + 2 * x1) / 3, (y0 + 2 * y1) / 3, ex, ey);
cx = ex;
cy = ey;
}
//--- Shift the window along
x0 = x1;
y0 = y1;
x1 = x;
y1 = y;
}
//--- Close the run onto its final point
if(state == 3)
FlattenTo(fx, fy, m, cx, cy, (2 * x0 + x1) / 3, (2 * y0 + y1) / 3,
(x0 + 2 * x1) / 3, (y0 + 2 * y1) / 3, x1, y1);
else
if(state == 2 && m < 512)
{
fx[m] = x1;
fy[m] = y1;
m++;
}
//--- Stroke the whole flattened run in one pass
PathAA(fx, fy, m, thick, argb);
}
//+------------------------------------------------------------------+
//| Fill a rounded rectangle with smoothed corners |
//+------------------------------------------------------------------+
void COmvCanvas::RoundFill(int x, int y, int w, int h, int r, const uint argb)
{
//--- Skip a rectangle with no area
if(w <= 0 || h <= 0)
return;
//--- Clamp the radius to what the rectangle can hold
if(r < 0)
r = 0;
if(r > w / 2)
r = w / 2;
if(r > h / 2)
r = h / 2;
//--- Fill the straight middle band across the full height
FillRect(x + r, y, x + w - r - 1, y + h - 1, argb);
//--- Fill the two straight sides between the corners
if(r > 0)
{
FillRect(x, y + r, x + r - 1, y + h - r - 1, argb);
FillRect(x + w - r, y + r, x + w - 1, y + h - r - 1, argb);
}
//--- Stop when no corner remains to smooth
if(r <= 0)
return;
//--- Split the color so coverage can drive the alpha
const uchar bA = (uchar)((argb >> 24) & 0xFF);
const uint rgb = argb & 0x00FFFFFF;
const double rr = (double)r * (double)r;
//--- Sample each corner pixel on a four by four grid
const int sub = 4;
const double step = 1.0 / sub;
const int subSq = sub * sub;
//--- Bound each corner box and name the arc center it curves around
int bx[4], by[4];
double ccx[4], ccy[4];
bx[0] = x;
by[0] = y;
ccx[0] = x + r;
ccy[0] = y + r;
bx[1] = x + w - r;
by[1] = y;
ccx[1] = x + w - r;
ccy[1] = y + r;
bx[2] = x;
by[2] = y + h - r;
ccx[2] = x + r;
ccy[2] = y + h - r;
bx[3] = x + w - r;
by[3] = y + h - r;
ccx[3] = x + w - r;
ccy[3] = y + h - r;
//--- Smooth every pixel inside the four corner boxes
for(int k = 0; k < 4; k++)
for(int py = by[k]; py < by[k] + r; py++)
for(int px = bx[k]; px < bx[k] + r; px++)
{
//--- Count how many subsamples fall inside the arc
int cnt = 0;
for(int sy = 0; sy < sub; sy++)
for(int sx = 0; sx < sub; sx++)
{
//--- Measure this subsample against the arc center
const double fx = px + (sx + 0.5) * step, fy = py + (sy + 0.5) * step;
const double ddx = fx - ccx[k], ddy = fy - ccy[k];
if(ddx * ddx + ddy * ddy <= rr)
cnt++;
}
//--- Skip a pixel the arc never reaches
if(cnt == 0)
continue;
//--- Write it solid when every subsample landed inside
if(cnt == subSq)
Blend(px, py, argb);
else
BlendCov(px, py, rgb, (double)bA / 255.0 * (double)cnt / (double)subSq);
}
}
//+------------------------------------------------------------------+
//| Fill a rectangle rounding only the corners asked for |
//+------------------------------------------------------------------+
void COmvCanvas::RoundFillSel(int x, int y, int w, int h, int r, const uint argb,
const bool rTL, const bool rTR, const bool rBL, const bool rBR)
{
//--- Skip a rectangle with no area
if(w <= 0 || h <= 0)
return;
//--- Clamp the radius to what the rectangle can hold
r = (int)MathMin(r, MathMin(w / 2, h / 2));
//--- Fill a plain rectangle when no corner is rounded
if(r <= 0)
{
FillRect(x, y, x + w - 1, y + h - 1, argb);
return;
}
//--- Fill the straight middle band across the full height
FillRect(x + r, y, x + w - r - 1, y + h - 1, argb);
//--- Fill each side, stopping short only where a corner is rounded
FillRect(x, y + (rTL ? r : 0), x + r - 1, y + h - 1 - (rBL ? r : 0), argb);
FillRect(x + w - r, y + (rTR ? r : 0), x + w - 1, y + h - 1 - (rBR ? r : 0), argb);
//--- Round the top left corner or square it off
if(rTL)
CornerQuad(x + r, y + r, r, argb, -1, -1);
else
FillRect(x, y, x + r - 1, y + r - 1, argb);
//--- Round the top right corner or square it off
if(rTR)
CornerQuad(x + w - 1 - r, y + r, r, argb, 1, -1);
else
FillRect(x + w - r, y, x + w - 1, y + r - 1, argb);
//--- Round the bottom left corner or square it off
if(rBL)
CornerQuad(x + r, y + h - 1 - r, r, argb, -1, 1);
else
FillRect(x, y + h - r, x + r - 1, y + h - 1, argb);
//--- Round the bottom right corner or square it off
if(rBR)
CornerQuad(x + w - 1 - r, y + h - 1 - r, r, argb, 1, 1);
else
FillRect(x + w - r, y + h - r, x + w - 1, y + h - 1, argb);
}
//+------------------------------------------------------------------+
//| Outline a rounded rectangle at a given thickness |
//+------------------------------------------------------------------+
void COmvCanvas::RoundStroke(int x, int y, int w, int h, int r, const int t, const uint argb)
{
//--- Skip a rectangle with no area
if(w <= 0 || h <= 0)
return;
//--- Clamp the radius to what the rectangle can hold
if(r < 0)
r = 0;
if(r > w / 2)
r = w / 2;
if(r > h / 2)
r = h / 2;
//--- Take at least a single pixel of thickness
const double thick = (t < 1) ? 1.0 : (double)t;
//--- Split the color so coverage can drive the alpha
const uchar bA = (uchar)((argb >> 24) & 0xFF);
const uint rgb = argb & 0x00FFFFFF;
//--- Measure the outline as a rounded rectangle a distance from its middle
const double left = x + thick * 0.5, top = y + thick * 0.5;
const double right = x + w - thick * 0.5, bottom = y + h - thick * 0.5;
const double rad = MathMax(0.0, r - thick * 0.5);
//--- Reach only as deep as the corner radius plus the thickness
const int band = (int)MathCeil(MathMax(rad, 0.0) + thick) + 2;
//--- Visit only the four strips the outline can touch, never the interior
for(int py = y; py < y + h; py++)
{
//--- Take the whole row near the top and bottom, the edges elsewhere
const bool wholeRow = (py < y + band || py >= y + h - band);
for(int px = x; px < x + w; px++)
{
//--- Skip the interior of a row that only needs its two edges
if(!wholeRow && px >= x + band && px < x + w - band)
{
px = x + w - band - 1;
continue;
}
//--- Measure how far this pixel sits outside the rounded shape
const double fx = px + 0.5, fy = py + 0.5;
const double qx = MathMax(MathMax(left + rad - fx, fx - (right - rad)), 0.0);
const double qy = MathMax(MathMax(top + rad - fy, fy - (bottom - rad)), 0.0);
const double dist = MathSqrt(qx * qx + qy * qy) - rad;
//--- Take the coverage of a band of the requested thickness
const double cov = OmvCoverage(MathAbs(dist) - thick * 0.5);
//--- Skip a pixel the outline never touches
if(cov <= 0.0)
continue;
//--- Blend the pixel at that coverage
BlendCov(px, py, rgb, (double)bA / 255.0 * cov);
}
}
}
//+------------------------------------------------------------------+
//| Draw a connector that leaves and enters vertically |
//+------------------------------------------------------------------+
void COmvCanvas::Elbow(const double ax1, const double ay1, const double ax2, const double ay2,
const double bend, const double head, const uint argb)
{
//--- Put both anchors on pixel centers so the straight runs stay crisp
const double x1 = MathFloor(ax1) + 0.5, y1 = MathFloor(ay1) + 0.5;
const double x2 = MathFloor(ax2) + 0.5, y2 = MathFloor(ay2) + 0.5;
//--- Stop the route at the notch, which is where the head's tail actually is
const double endY = y2 - head * 0.55;
//--- Route the connection through a handful of points
double px[8], py[8];
int n = 0;
px[n] = x1;
py[n] = y1;
n++;
//--- Step across through a midpoint when the two ends sit in different columns
if(MathAbs(x2 - x1) >= 1.5)
{
//--- Split the vertical gap so the crossing sits between the boxes
const double midY = (y1 + endY) * 0.5;
//--- Hold the bend inside half the drop, or the route would double back
double turn = bend;
const double room = MathAbs(endY - midY);
if(turn > room)
turn = room;
if(turn < 0.0)
turn = 0.0;
px[n] = x1;
py[n] = midY - turn;
n++;
px[n] = x1;
py[n] = midY;
n++;
px[n] = x2;
py[n] = midY;
n++;
px[n] = x2;
py[n] = midY + turn;
n++;
}
px[n] = x2;
py[n] = endY;
n++;
//--- Smooth the route the way the grapher does
SplineAA(px, py, n, 1, argb);
//--- Take the direction the route was travelling as it reached the tip
double ux = 0.0, uy = 1.0;
if(n >= 2)
{
//--- Measure from the leg before the tip, so a short drop still tilts
const double lx = x2 - px[n - 2], ly = y2 - py[n - 2];
const double len = MathSqrt(lx * lx + ly * ly);
//--- Point along it, keeping the downward default when it has no length
if(len > 1e-9)
{
ux = lx / len;
uy = ly / len;
}
}
//--- Take the normal so the head can spread across its own direction
const double nx = -uy, ny = ux;
//--- Step back from the tip along the arrival direction
const double bx = x2 - ux * head, by = y2 - uy * head;
const double cx = x2 - ux * head * 0.6, cy = y2 - uy * head * 0.6;
//--- Fill the head as the notched chevron, turned to face the way it came
const double wide = head * 0.62;
double vx[4], vy[4];
vx[0] = x2;
vy[0] = y2;
vx[1] = bx - nx * wide;
vy[1] = by - ny * wide;
vx[2] = cx;
vy[2] = cy;
vx[3] = bx + nx * wide;
vy[3] = by + ny * wide;
PolyAA(vx, vy, 4, argb);
}
//+------------------------------------------------------------------+
//| Apply a font only when it differs from the current one |
//+------------------------------------------------------------------+
void COmvCanvas::Font(const int size, const bool bold)
{
//--- Skip the call when the font already matches
if(size == m_fontSize && bold == m_fontBold)
return;
//--- Apply the requested face at the requested weight
FontSet("Segoe UI", -size * 10, bold ? FW_MEDIUM : FW_NORMAL);
//--- Remember what is now active
m_fontSize = size;
m_fontBold = bold;
//--- Drop measurements taken under the previous font
m_cached = 0;
}
//+------------------------------------------------------------------+
//| Measure a string width through the cache |
//+------------------------------------------------------------------+
int COmvCanvas::TextWide(const string text)
{
//--- Return a width already measured under this font
for(int i = 0; i < m_cached; i++)
if(m_key[i] == text)
return m_wide[i];
//--- Measure the string
int w = 0, h = 0;
TextSize(text, w, h);
//--- Remember the result while room remains
if(m_cached < OC_CACHE)
{
m_key[m_cached] = text;
m_wide[m_cached] = w;
m_high[m_cached] = h;
m_cached++;
}
//--- Report the measured width
return w;
}
//+------------------------------------------------------------------+
//| Measure a string height through the cache |
//+------------------------------------------------------------------+
int COmvCanvas::TextHigh(const string text)
{
//--- Return a height already measured under this font
for(int i = 0; i < m_cached; i++)
if(m_key[i] == text)
return m_high[i];
//--- Measure the string
int w = 0, h = 0;
TextSize(text, w, h);
//--- Remember the result while room remains
if(m_cached < OC_CACHE)
{
m_key[m_cached] = text;
m_wide[m_cached] = w;
m_high[m_cached] = h;
m_cached++;
}
//--- Report the measured height
return h;
}
//+------------------------------------------------------------------+
//| Break a string into lines that fit a given width |
//+------------------------------------------------------------------+
int COmvCanvas::WrapText(const string text, const int wide, string &out[], const int most)
{
//--- Refuse to wrap into no room at all
ArrayResize(out, 0);
if(wide <= 0 || StringLen(text) == 0 || most <= 0)
return 0;
//--- A string that already fits needs no work
if(TextWide(text) <= wide)
{
ArrayResize(out, 1);
out[0] = text;
return 1;
}
//--- Split the string on spaces so words stay whole where they can
string words[];
const int count = StringSplit(text, ' ', words);
//--- Pack the words into lines, starting a new one when the current fills
string line = "";
int made = 0;
for(int i = 0; i < count && made < most; i++)
{
//--- Try the word on the current line
const string tryLine = (StringLen(line) > 0) ? line + " " + words[i] : words[i];
if(TextWide(tryLine) <= wide)
{
line = tryLine;
continue;
}
//--- Close the current line before starting another
if(StringLen(line) > 0)
{
ArrayResize(out, made + 1);
out[made++] = line;
line = "";
}
//--- A single word too long for the width is broken by character
string piece = words[i];
while(TextWide(piece) > wide && made < most)
{
//--- Take as many characters as the width allows
int fit = StringLen(piece);
while(fit > 1 && TextWide(StringSubstr(piece, 0, fit)) > wide)
fit--;
ArrayResize(out, made + 1);
out[made++] = StringSubstr(piece, 0, fit);
piece = StringSubstr(piece, fit);
}
line = piece;
}
//--- Close whatever the last line still holds
if(StringLen(line) > 0 && made < most)
{
ArrayResize(out, made + 1);
out[made++] = line;
}
//--- Mark the last line when the text ran past what we may show
if(made >= most && made > 0)
{
//--- Trim the line until an ellipsis fits beside it
string last = out[made - 1];
while(StringLen(last) > 1 && TextWide(last + "...") > wide)
last = StringSubstr(last, 0, StringLen(last) - 1);
out[made - 1] = last + "...";
}
//--- Report how many lines the text needed
return made;
}
//+------------------------------------------------------------------+
//| Pick the largest type size that fits a box |
//+------------------------------------------------------------------+
int COmvCanvas::FitFont(const string text, const int maxWide, const int maxHigh,
const int best, const int least, const bool bold)
{
//--- Try every size from the preferred one down to the smallest allowed
for(int size = best; size >= least; size--)
{
//--- Measure the string at this size
Font(size, bold);
if(TextWide(text) <= maxWide && TextHigh(text) <= maxHigh)
return size;
}
//--- Report the smallest size when nothing else fits
Font(least, bold);
return least;
}
//+------------------------------------------------------------------+
//| Draw text from a left anchor |
//+------------------------------------------------------------------+
void COmvCanvas::TxtL(const int x, const int y, const string text, const uint clr)
{
//--- Place the string with its left edge on the anchor
TextOut(x, y, text, clr, TA_LEFT | TA_TOP);
}
//+------------------------------------------------------------------+
//| Draw text from a center anchor |
//+------------------------------------------------------------------+
void COmvCanvas::TxtC(const int x, const int y, const string text, const uint clr)
{
//--- Place the string centered on the anchor
TextOut(x, y, text, clr, TA_CENTER | TA_TOP);
}
//+------------------------------------------------------------------+
//| Draw text from a right anchor |
//+------------------------------------------------------------------+
void COmvCanvas::TxtR(const int x, const int y, const string text, const uint clr)
{
//--- Place the string with its right edge on the anchor
TextOut(x, y, text, clr, TA_RIGHT | TA_TOP);
}
//+------------------------------------------------------------------+
//| Draw text centered on a point in both directions |
//+------------------------------------------------------------------+
void COmvCanvas::TxtM(const int x, const int y, const string text, const uint clr)
{
//--- Let the terminal center the glyphs rather than guessing a baseline
TextOut(x, y, text, clr, TA_CENTER | TA_VCENTER);
}
//+------------------------------------------------------------------+
//| Copy the whole pixel buffer out for caching |
//+------------------------------------------------------------------+
void COmvCanvas::SaveTo(uint &dst[])
{
//--- Size the target and take the buffer in one copy
const int n = ArraySize(m_pixels);
ArrayResize(dst, n);
if(n > 0)
ArrayCopy(dst, m_pixels, 0, 0, n);
}
//+------------------------------------------------------------------+
//| Restore a previously cached pixel buffer |
//+------------------------------------------------------------------+
void COmvCanvas::RestoreFrom(const uint &src[])
{
//--- Put the buffer back only when the cache still matches the canvas
const int n = ArraySize(m_pixels);
if(n > 0 && ArraySize(src) == n)
ArrayCopy(m_pixels, src, 0, 0, n);
}
//+------------------------------------------------------------------+
//| Draw a horizontal rule |
//+------------------------------------------------------------------+
void COmvCanvas::HRule(const int x1, const int x2, const int y, const uint clr)
{
//--- Fill a single pixel tall strip
FillRect(x1, y, x2, y, clr);
}
//+------------------------------------------------------------------+
//| Draw a vertical rule |
//+------------------------------------------------------------------+
void COmvCanvas::VRule(const int x, const int y1, const int y2, const uint clr)
{
//--- Fill a single pixel wide strip
FillRect(x, y1, x, y2, clr);
}
#endif // OMV_CANVAS_MQH
//+------------------------------------------------------------------+