# How to paint smooth rendering of connected QLineF's?

• Hello!

I am painting a series of connected QLineF's with varying vectors and widths. Is there any way to smooth them out to be more vector-ish, so you don't see the jagged transitions between the segments?

Thanks!

• How do you paint them currently?

• If you need control points look at NURBS otherwise a normal cubic spline should do.

• @Asperamanca I am painting them as connected segments using QPainter::drawLine.

@kshegunov That is probably a good solution (although I might not have the math-algorithm brain to implement it), but I don't think that it addresses how to paint it with varying widths. Here is a function I modified from QtCharts which returns a QPainterPath, which of course uses a single pen width:

``````static QVector<qreal> firstControlPoints(const QVector<qreal>& vector)
{
QVector<qreal> result;

int count = vector.count();
result.resize(count);
result = vector / 2.0;

QVector<qreal> temp;
temp.resize(count);
temp = 0;

qreal b = 2.0;

for (int i = 1; i < count; i++) {
temp[i] = 1 / b;
b = (i < count - 1 ? 4.0 : 3.5) - temp[i];
result[i] = (vector[i] - result[i - 1]) / b;
}

for (int i = 1; i < count; i++)
result[count - i - 1] -= temp[count - i] * result[count - i];

return result;
}

/*!
Calculates control points which are needed by QPainterPath.cubicTo function to draw the cubic Bezier cureve between two points.
*/
static QVector<QPointF> calculateControlPoints(const QVector<QPointF> &points)
{
QVector<QPointF> controlPoints;
controlPoints.resize(points.count() * 2 - 2);

int n = points.count() - 1;

if (n == 1) {
//for n==1
controlPoints.setX((2 * points.x() + points.x()) / 3);
controlPoints.setY((2 * points.y() + points.y()) / 3);
controlPoints.setX(2 * controlPoints.x() - points.x());
controlPoints.setY(2 * controlPoints.y() - points.y());
return controlPoints;
}

// Calculate first Bezier control points
// Set of equations for P0 to Pn points.
//
//  |   2   1   0   0   ... 0   0   0   ... 0   0   0   |   |   P1_1    |   |   P0 + 2 * P1             |
//  |   1   4   1   0   ... 0   0   0   ... 0   0   0   |   |   P1_2    |   |   4 * P1 + 2 * P2         |
//  |   0   1   4   1   ... 0   0   0   ... 0   0   0   |   |   P1_3    |   |   4 * P2 + 2 * P3         |
//  |   .   .   .   .   .   .   .   .   .   .   .   .   |   |   ...     |   |   ...                     |
//  |   0   0   0   0   ... 1   4   1   ... 0   0   0   | * |   P1_i    | = |   4 * P(i-1) + 2 * Pi     |
//  |   .   .   .   .   .   .   .   .   .   .   .   .   |   |   ...     |   |   ...                     |
//  |   0   0   0   0   0   0   0   0   ... 1   4   1   |   |   P1_(n-1)|   |   4 * P(n-2) + 2 * P(n-1) |
//  |   0   0   0   0   0   0   0   0   ... 0   2   7   |   |   P1_n    |   |   8 * P(n-1) + Pn         |
//
QVector<qreal> vector;
vector.resize(n);

vector = points.x() + 2 * points.x();

for (int i = 1; i < n - 1; ++i)
vector[i] = 4 * points[i].x() + 2 * points[i + 1].x();

vector[n - 1] = (8 * points[n - 1].x() + points[n].x()) / 2.0;

QVector<qreal> xControl = firstControlPoints(vector);

vector = points.y() + 2 * points.y();

for (int i = 1; i < n - 1; ++i)
vector[i] = 4 * points[i].y() + 2 * points[i + 1].y();

vector[n - 1] = (8 * points[n - 1].y() + points[n].y()) / 2.0;

QVector<qreal> yControl = firstControlPoints(vector);

for (int i = 0, j = 0; i < n; ++i, ++j) {

controlPoints[j].setX(xControl[i]);
controlPoints[j].setY(yControl[i]);

j++;

if (i < n - 1) {
controlPoints[j].setX(2 * points[i + 1].x() - xControl[i + 1]);
controlPoints[j].setY(2 * points[i + 1].y() - yControl[i + 1]);
} else {
controlPoints[j].setX((points[n].x() + xControl[n - 1]) / 2);
controlPoints[j].setY((points[n].y() + yControl[n - 1]) / 2);
}
}
return controlPoints;
}

QPainterPath splineFromPoints(const QVector<QPointF> &points, int penWidth)
{
QPainterPath splinePath;
QVector<QPointF> controlPoints;
if (points.count() >= 2)
controlPoints = calculateControlPoints(points);

if ((points.size() < 2) || (controlPoints.size() < 2)) {
return splinePath;
}

Q_ASSERT(points.count() * 2 - 2 == controlPoints.count());

// Use worst case scenario to determine required margin.
qreal margin = penWidth * 1.42;

splinePath.moveTo(points.at(0));
for (int i = 0; i < points.size() - 1; i++) {
const QPointF &point = points.at(i + 1);
splinePath.cubicTo(controlPoints[2 * i], controlPoints[2 * i + 1], point);
}
return splinePath;
}
``````

• Just a suggestion

• Hi,
There's going to be some linear algebra and/or calculus in the following post, so I hope you bear with me.

If I get your question correctly, you get a set of points in 2D space (x, y coordinates) and one associated value (for each of those points) that's the pressure, correct? From what I could think off the top of my head you'd need to have an estimation for the first derivative of your curve (i.e. the tangents) so you could "thicken" the curve based on the pressure. Actually you need the normals to the "ideal" curve at each point, but that goes through the derivatives all the same.

Consider the following schematic: Suppose the black curve (with black points being your inputs) is the "ideal" curve you have, by "ideal" I mean one you'd get if you pass a smooth (piecewise) spline through the points. If you can get the unit normal vectors respective to the curve at each of your input points (colored arrows, top-left picture) you could scale their length through some function of the pressure to obtain two points positioned on each side of your "ideal" curve. Once you have done that for each of your input points you can pass a spline through them which encloses the "ideal" curve (assuming appropriate ordering - first having all the right-hand points respective to the curve direction, here I assume curve is moving left to right, then having all left-hand points in reverse order). This is illustrated on the second picture to the left - the orange curves is where the enclosing spline would pass; red is shrunk based on the pressure (or a function of the pressure), while green is stretched.

So then the question becomes how to obtain the normals to the "ideal" curve for each of the input points. The straightforward method is to pass a spline (or bezier curve or w/e) through them, and from there to calculate the tangent/normal vectors. Then to normalize them and then scale them with the pressure function. Since you are not dealing with functions as such, calculating the derivative classically may pose a problem, so I've illustrated a simple, albeit naïve, method to estimate the normal vectors without actually passing a curve through the points by means of circles (schematic to the right).

It rests with the fact that each three points will define a circle and the radius of that circle will be exactly the normal vector (which needs to be scaled though). This all is related to the definition of curvature, but with curvatures there exists a requirement that the three points must be very close to each other (so to get a realistic result). As can be seen this simplistic approach works well for those segments that can be reasonably approximated with second order polynomials (red circle, red tangent, red normal vector), but unfortunately fails miserably if the segment can be approximated well only by higher order polynomials (i.e. the green circle, tangent and normal vector).

I hope this helps.
Kind regards.

• Hi! I have a piece of code that takes a `std::vector<QPointF>` of arbitrary length and computes the control points for a polyline that interpolates these points. The line segments are cubic Bezier curves. The segment joints are C2 continious. I can send you a PM with the source if you like. • Sry, when I read your first post I thought you only wanted some sort of spline. I missed the part with the varying pen-widths. But that's easy to achive without complicated geometrical constructions: Instead of using `cubicTo` from `QPainterPath`, just calculate the parameterized Bezier curve yourself, tesselate each curve segment with straight lines and compute the pen-width for each tesselated line-segment with linear interpolation between the given points. For the optimal result you'll have to make the number of tesselation segments adaptive depending on the (pixel-) length of each Bezier-curve.

This is what it looks like when the length of the tesselation segments is too long: And this is with proper distance-adaption: • @Wieland @Wieland That would be great. I don't know how to PM on this forum yet or I would reach out.

• @Wieland Wow, your second post is really intriguing. Do you have code for that? I am not able to follow the technicalities of that one. Also horrible with bezier theory.

• @Wieland @kshegunov it seems like this would be a great feature to have in QPainterPath.

• @patrickkidd I've sent you a chat message.

• @Wieland I was going to send you another PM, but it looks like I am restricted. Would you mind reaching out one more time? Thank you!