Laszlo Agocs

Let There Be Shapes!

Published Friday July 7th, 2017
11 Comments on Let There Be Shapes!
Posted in Declarative UI, Dev Loop, Graphics, OpenGL, Painting, Qt Quick, UI

One of the new features of the upcoming Qt 5.10 is the introduction of the shapes plugin to Qt Quick. This allows adding stroked and filled paths composed of lines, quadratic curves, cubic curves, and arcs into Qt Quick scenes. While this has always been possible to achieve via QQuickPaintedItem or the Canvas type, the Shape type provides a genuine first-class Qt Quick item that from the scene graph’s perspective is backed by either actual geometry or a vendor-specific GPU accelerated path rendering approach (namely, GL_NV_path_rendering).

shape_tiger
The shapes example, running on an Android tablet

Why is This Great?

  • There is no rasterization involved (no QImage, no OpenGL framebuffer object), which is excellent news for those who are looking for shapes spanning a larger area of a possibly high resolution screen, or want to apply potentially animated transformations to the shapes in the scene.
  • The API is fully declarative and every property, including stroke and fill parameters, path element coordinates, control points, etc., can be bound to in QML expressions and can be animated using the usual tools of Qt Quick. Being declarative also means that changing a property leads to recalculating only the affected sets of the underlying data, something that has been traditionally problematic with imperative painting approaches (e.g. QPainter).
  • There are multiple implementations under the hood, with the front Qt Quick item API staying the same. The default, generic solution is to reuse the triangulator from QPainter’s OpenGL backend in QtGui. For NVIDIA GPUs there is an alternative path using the GL_NV_path_rendering OpenGL extension. When using the software renderer of Qt Quick, a simple backend falling back to QPainter will be used. This also leaves the door open to seamlessly adding other path rendering approaches in the future.

Status and Documentation

Right now the feature is merged to the dev branch of qtdeclarative and will be part of 5.10 onces it branches off. The documentation snapshots are online as well:

(due to some minor issues with the documentation system some types in Particles get cross-linked in the Inherited By section and some other places, just ignore this for now)

The canonical example is called shapes and it lives under qtdeclarative/examples/quick as expected.

Let’s See Some Code

Without further ado, let’s look at some code snippets. The path specification reuses existing types from PathView, and should present no surprises. The rest is expected to be fairly self-explanatory. (check the docs above)

1. A simple triangle with animated stroke width and fill color.

shape1

Shape {
    id: tri
    anchors.fill: parent

    ShapePath {
        id: tri_sp
        strokeColor: "red"
        strokeWidth: 4
        SequentialAnimation on strokeWidth {
            running: tri.visible
            NumberAnimation { from: 1; to: 20; duration: 2000 }
            NumberAnimation { from: 20; to: 1; duration: 2000 }
        }
        ColorAnimation on fillColor {
            from: "blue"; to: "cyan"; duration: 2000; running: tri.visible
        }

        startX: 10; startY: 10
        PathLine { x: tri.width - 10; y: tri.height - 10 }
        PathLine { x: 10; y: tri.height - 10 }
        PathLine { x: 10; y: 10 }
    }
}

2. Let’s switch over to dash strokes and disable fill. Unlike with image-backed approaches, applying transformations to shapes are no problem.

shape2

Shape {
    id: tri2
    anchors.fill: parent

    ShapePath {
        strokeColor: "red"
        strokeWidth: 4
        strokeStyle: ShapePath.DashLine
        dashPattern: [ 1, 4 ]
        fillColor: "transparent"

        startX: 10; startY: 10
        PathLine { x: tri2.width - 10; y: tri2.height - 10 }
        PathLine { x: 10; y: tri2.height - 10 }
        PathLine { x: 10; y: 10 }
    }

    SequentialAnimation on scale {
        running: tri2.visible
        NumberAnimation { from: 1; to: 4; duration: 2000; easing.type: Easing.InOutBounce }
        NumberAnimation { from: 4; to: 1; duration: 2000; easing.type: Easing.OutBack }
    }
}

3. Shape comes with full linear gradient support. This works exactly like QLinearGradient in the QPainter world.

shape3-png

Shape {
    id: tri3
    anchors.fill: parent

    ShapePath {
        strokeColor: "transparent"

        fillGradient: LinearGradient {
            x1: 20; y1: 20
            x2: 180; y2: 130
            GradientStop { position: 0; color: "blue" }
            GradientStop { position: 0.2; color: "green" }
            GradientStop { position: 0.4; color: "red" }
            GradientStop { position: 0.6; color: "yellow" }
            GradientStop { position: 1; color: "cyan" }
        }

        startX: 10; startY: 10
        PathLine { x: tri3.width - 10; y: tri3.height - 10 }
        PathLine { x: 10; y: tri3.height - 10 }
        PathLine { x: 10; y: 10 }
    }

    NumberAnimation on rotation {
        from: 0; to: 360; duration: 2000
        running: tri3.visible
    }
}

4. What about circles and ellipses? Just use two arcs. (note: one ShapePath with two PathArcs is sufficient for a typical circle or ellipse, here there are two ShapePath due to the different fill parameters)

shape4

Shape {
    id: circle
    anchors.fill: parent
    property real r: 60

    ShapePath {
        strokeColor: "transparent"
        fillColor: "green"

        startX: circle.width / 2 - circle.r
        startY: circle.height / 2 - circle.r
        PathArc {
            x: circle.width / 2 + circle.r
            y: circle.height / 2 + circle.r
            radiusX: circle.r; radiusY: circle.r
            useLargeArc: true
        }
    }
    ShapePath {
        strokeColor: "transparent"
        fillColor: "red"

        startX: circle.width / 2 + circle.r
        startY: circle.height / 2 + circle.r
        PathArc {
            x: circle.width / 2 - circle.r
            y: circle.height / 2 - circle.r
            radiusX: circle.r; radiusY: circle.r
            useLargeArc: true
        }
    }
}

5. Speaking of arcs, PathArc is modeled after SVG elliptical arcs. Qt 5.10 introduces one missing property, xAxisRotation.

shape5

Repeater {
    model: 2
    Shape {
        anchors.fill: parent

        ShapePath {
            fillColor: "transparent"
            strokeColor: model.index === 0 ? "red" : "blue"
            strokeStyle: ShapePath.DashLine
            strokeWidth: 4

            startX: 50; startY: 100
            PathArc {
                x: 150; y: 100
                radiusX: 50; radiusY: 20
                xAxisRotation: model.index === 0 ? 0 : 45
            }
        }
    }
}

Repeater {
    model: 2
    Shape {
        anchors.fill: parent

        ShapePath {
            fillColor: "transparent"
            strokeColor: model.index === 0 ? "red" : "blue"

            startX: 50; startY: 100
            PathArc {
                x: 150; y: 100
                radiusX: 50; radiusY: 20
                xAxisRotation: model.index === 0 ? 0 : 45
                direction: PathArc.Counterclockwise
            }
        }
    }
}

6. Quadratic and cubic Bezier curves work as expected. Below is a quadratic curve with its control point animated.

shape6

Shape {
    id: quadCurve
    anchors.fill: parent

    ShapePath {
        strokeWidth: 4
        strokeColor: "black"
        fillGradient: LinearGradient {
            x1: 0; y1: 0; x2: 200; y2: 200
            GradientStop { position: 0; color: "blue" }
            GradientStop { position: 1; color: "green" }
        }

        startX: 50
        startY: 150
        PathQuad {
            x: 150; y: 150
            controlX: quadCurveControlPoint.x; controlY: quadCurveControlPoint.y
        }
    }
}

Rectangle {
    id: quadCurveControlPoint
    color: "red"
    width: 10
    height: 10
    y: 20
    SequentialAnimation on x {
        loops: Animation.Infinite
        NumberAnimation {
            from: 0
            to: quadCurve.width - quadCurveControlPoint.width
            duration: 5000
        }
        NumberAnimation {
            from: quadCurve.width - quadCurveControlPoint.width
            to: 0
            duration: 5000
        }
    }
}

7. The usual join and cap styles, that are probably familiar from QPainter and QPen, are available.

shape7

Shape {
    anchors.fill: parent

    ShapePath {
        strokeColor: "red"
        strokeWidth: 20
        fillColor: "transparent"
        joinStyle: ShapePath.RoundJoin

        startX: 20; startY: 20
        PathLine { x: 100; y: 100 }
        PathLine { x: 20; y: 150 }
        PathLine { x: 20; y: 20 }
    }

    ShapePath {
        strokeColor: "black"
        strokeWidth: 20
        capStyle: ShapePath.RoundCap

        startX: 150; startY: 20
        PathCubic {
            x: 150; y: 150; control1X: 120; control1Y: 50; control2X: 200
            SequentialAnimation on control2Y {
                loops: Animation.Infinite
                NumberAnimation { from: 0; to: 200; duration: 5000 }
                NumberAnimation { from: 200; to: 0; duration: 5000 }
            }
        }
    }
}

Any Downsides?

Does this mean the time has finally come to add hundreds of lines and curves and arcs to every Qt Quick scene out there?

Not necessarily.

Please do consider the potential performance implications before designing in a large number of shapes in a user interface. See the notes in the Shape documentation page.

In short, the most obvious gotchas are the following:

  • [generic backend] Shapes with a lot of ShapePath child objects will take longer to generate the geometry. The good news is that this can be mitigated by setting the asynchronous property to true which, as the name suggests, leads to spawning off worker threads without blocking the main UI. This comes at the cost of the shape appearing only after the non-asynchronous UI elements.
  • [GL_NV_path_rendering backend] Geometry generation is a non-issue here, however due to the way the “foreign” rendering commands are integrated with the Qt Quick scene graph, having a large number of Shape items in a scene may not scale very well since, unlike plain geometry-based scenegraph nodes, these involve a larger amount of logic and OpenGL state changes. Note that one Shape with several ShapePath children is not an issue here since that is really just one node in the scenegraph.
  • Antialiasing is currently covered only through multi or super sampling, either for the entire scene or for layers. Note that Qt 5.10 introduces a very handy property here: layer.samples can now be used to enable using multisample renderbuffers, when supported.
  • Shape is not a replacement for rectangles and rounded rectangles provided by Rectangle. Rectangle will always perform better and can provide some level of smoothing even without multisampling enabled.

Nevertheless we expect Shape to be highly useful to a large number of Qt Quick applications. The Qt 5.10 release is due H2 this year, so…stay tuned!

Do you like this? Share it
Share on LinkedInGoogle+Share on FacebookTweet about this on Twitter

Posted in Declarative UI, Dev Loop, Graphics, OpenGL, Painting, Qt Quick, UI

11 comments

HM says:

I hope that the line shape properties will also find their way into the QtLocation map’s items (MapPolyline/MapRoute) in the future.

Vladest says:

is it possible to have a line filled by a pattern?

Laszlo Agocs Laszlo Agocs says:

At the moment fill is limited to a solid color or a linear gradient. The API and feature set is intentionally compact and minimal for now, in order to have a good baseline that can be extended later on. I have now created a Jira issue to keep track of potential enhancements in the future: https://bugreports.qt.io/browse/QTBUG-61857

Dimi says:

This should have been available in 5.9… actually a lot sooner, I guess right from the start. Now people who chose to stick to the LTS release will have to miss that functionality.,

Vladest says:

I believe if one really want it, it can be easily backported to 5.9 (I did it for 5.1)

Luke Parry says:

Should have been released in QtQuick 1.0 – long time coming.

Great to see this addition in QtQuick. Look forward to using this in the next release!

Cheers
Qt

tim blechmann says:

very useful!
though some suggestions of feature which i have implemented in custom scene graph items in the past as our gui designers use them all the time:

* radial and conical gradients (seems that already made it into QTBUG-61857)
* different blend modes (multiply/screen/overlay …)
* some way to import assets from tools that gui designers are familiar with: i’ve ended up with a hack to import the 2d subset of a blender-exported mesh into scenegraph items

Philip S says:

Could you use Qt Quick to render to QImage or PDF?

m][sko says:

Is it possible to draw basic objects with Shape but directly to texture ?
So it will be sharp but still fast to draw.

jani yang says:

Well, it would be nice if there will be qgraphicproxywidget in qml version

Marco Piccolino Marco Piccolino says:

Great work Laszlo!
It would be nice to see in the future a Shape object generator from SVG

Commenting closed.

Get started today with Qt Download now