1294 lines
52 KiB
MQL5
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
| |||
//+------------------------------------------------------------------+
|