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