Let There Be Shapes!

July 7, 2017 Laszlo Agocs

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!

The post Let There Be Shapes! appeared first on Qt Blog.

Previous
Qt Creator 4.4 Beta released
Qt Creator 4.4 Beta released

We are happy to announce the release of Qt Creator 4.4 Beta! Editing This version of Qt Creator features op...

Next Article
Qt WebGL Streaming merged
Qt WebGL Streaming merged

Some time ago I published a couple of blog posts talking about Qt WebGL Streaming plugin. The time has come...