Search code examples
python-2.7qtpyqt4

Is there an easy way to connect the tangents of two overlapping Bezier curves?


I'm trying to simulate a stroke from a pen. I've created a QPainterPath that consists of two identical Bezier curve paths offset from each other, connected at the ends. The code is roughly this:

path1 = QPainterPath(curvePath)
path2 = QPainterPath(curvePath).toReversed()
path1.translate(5, -5)
path2.translate(-5, 5)

strokeShape.addPath(path1)
strokeShape.connectPath(path2)
strokeShape.closeSubpath()

gc.drawPath(strokeShape)

It works fine for straight and certain curves:

nice curves

However, if the curve is more extreme, the path "splits open" and it looks bad:

that space tho

In an earlier version I drew a bunch of little quads along the path but I was hoping that I could get smoother curves (and simpler code):

ugly and jaggy

I can't seem to find any magical Qt solution. Qt doesn't seem to allow you to stroke an arbitrary shape along a path, unless I'm missing it.

So two questions:

  1. can you stroke a custom shape along a QPainterPath?
  2. failing #1, is there an easy way/algorithm to find the tangents at the extremes of a cubic Bezier curve in Qt, so I can connect the path via a line or another curve?

Thanks!!


Solution

  • Well, I found a couple of solutions, but neither is perfect.

    1. Drawing a line with a pixmap brush in Qt?

    Using this method, I created a 2-pixel-tall pixmap and rotated it to the same angle as my "pen", then drew it repeatedly along the line. Unfortunately to look good it needs to be at least a resolution of 1000 points:

    penXformed = penPixmap.transformed(QtGui.QTransform().rotate(-40), QtCore.Qt.SmoothTransformation)
    for i in range(0, 1000):
        pct = float(i) / 1000.0
    
        gc.drawPixmap(curvePath.pointAtPercent(pct) - QtCore.QPoint(10, 10), penXformed)
    

    but it doesn't look smooth, and the alpha seems to apply to the whole shape so you can't see the overlap. There's probably some blending mode that will work better, but I don't really want this solution at the moment:

    enter image description here

    However, it's good to know, in case I want to ever do something like a chalk look, or paintbrush, perhaps.

    1. There's probably a fancier algorithm, but checking the tangents and splitting the curve into segments whenever the tangent is close to the angle I want (40 degrees) basically works:

    sorta, almost

    However, again I need to check the percentage in 1000 intervals to get enough tangents to match. Ideally this curve would only be 3 segments so I must have a bug in my code somewhere here because I get unnecessary segments:

    lines

    This solution required more code, one set to find the tangents:

    for i in range(0, 1000):
        pct = float(i) / 1000.0
        angle = int(curvePath.angleAtPercent(pct))
        if angle >= 39 and angle <= 41:
    
            # l = to the left of pct, r is to the right of pct  
            (l, r) = sliceBezier(verts, pct)
    
            curCurve = QtGui.QPainterPath()
            curCurve.moveTo(l[0][0], l[0][1])
            curCurve.cubicTo(l[1][0], l[1][1], l[2][0], l[2][1], l[3][0], l[3][1])
            newCurves.append(curCurve)
            verts = r
    
    # once we found all the segments above, make the remaining control points into curves (if any)
    while (len(verts) > 3):
        curCurve = QtGui.QPainterPath()
        curCurve.moveTo(verts[0][0], verts[0][1])
        curCurve.cubicTo(verts[1][0], verts[1][1], verts[2][0], verts[2][1], verts[3][0], verts[3][1])
        newCurves.append(curCurve)
        verts = verts[3:]
    

    and the "sliceBezier" mentioned in the code is code I found on StackOverflow to split a Bezier curve in two at a given 't' value, and return the control points for the new left (i.e. less than 't') and right sections (i.e. greater than 't'):

    def sliceBezier(points, t):
        if len(points) < 4:
            return points, []
    
        x1, y1 = points[0] 
        x2, y2 = points[1]
        x3, y3 = points[2]
        x4, y4 = points[3]
    
        x12 = (x2-x1)*t+x1
        y12 = (y2-y1)*t+y1
    
        x23 = (x3-x2)*t+x2
        y23 = (y3-y2)*t+y2
    
        x34 = (x4-x3)*t+x3
        y34 = (y4-y3)*t+y3
    
        x123 = (x23-x12)*t+x12
        y123 = (y23-y12)*t+y12
    
        x234 = (x34-x23)*t+x23
        y234 = (y34-y23)*t+y23
    
        x1234 = (x234-x123)*t+x123
        y1234 = (y234-y123)*t+y123
    
        return [(x1, y1), (x12, y12), (x123, y123), (x1234, y1234)], [(x1234,y1234),(x234,y234),(x34,y34),(x4,y4)]
    

    ...and then to render it, I just loop through the new set of curve segments the same way I did for a single curve in the original post.

    It doesn't work for every curve so it's not a perfect solution, but it might at least be workable for now.