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