Giter VIP home page Giter VIP logo

qmlcurvedclippping's Introduction

Clipping along round edges

QML supports clipping along the edges of a rectangular item. It is very easy to use (just setting property clip to true) and pretty helpful. But it also has two major disadvantages:

  1. Clipping does not produce smooth edges (e.g. when clipping in rotated items)
  2. Even when creating Rectangles with rounded corners clipping does not work on these visual edges but only on the Items real edges that is always a rectangle

This little project demonstrates how you can visually clip your items very easy to the edges of an other item. The downside is that you can still click MouseAreas that should be clipped away.
We'll create a roundly gauge element in 6 easy to follow steps. At the end of each step I'll name the commit you can see the changeds I've made.

TL;DR

  • enable layering of your Item that should be clipped (layer.enable: true)
  • make a OpacityMask be the layer.effect of that item
  • set the OpacityMask's maskSource property to the clipping Item

Create a QML project

The first step is to create a new QML project. I just used the Qt Quick UI project the QtCreator can create out of the box:

  1. Click 'File' > 'New File or Project...'
  2. Make sure to select All Templates from the drop down in the top-right corner
  3. Select 'Application' > 'Qt Quick UI'

Now we have a simple application that shows us the message "Hello World" when we hit run.

See: 64b949b (Initial commit)

Add the basic layout of the gauge element

To make our element reusable we put it into an separate file (Gauge.qml).
As root element we keep the plain Item and place 3 nested Rectangles in it:

 Rectangle {
    id: outerBackground
    anchors.fill: parent
    radius: Math.max(width, height)
    color: root.backgroundColor
    Rectangle {
        id: progressBarTrace
        anchors.fill: parent
        anchors.margins: progressBarWidth
        radius:  Math.max(width, height)
        color: root.traceColor
        Rectangle {
            id: innerBackground
            anchors.fill: parent
            anchors.margins: progressBarWidth
            radius: Math.max(width, height)
            color: root.backgroundColor
        }
    }
}

I also placed some text inside the element to show the value as string.

In the file MainForm.ui.qml I replaced the Text element by the newly created Gauge:

Gauge {
    anchors.fill: parent
    anchors.margins: progressBarWidth
}

See: 40b06aa (Add gauge layout)

Add rectangles used to display the progress bar

We create a new item that acts as indicator. What it does is it rotates a rectangle into the visible area of it's parent. Input value range is from 0 (fully rotated out of view) to 90 (fully visible):

Item {
    id: root
    width: 150
    height: 150
    clip: true

    property real angle: 33.3
    property color color: "royalblue"

    Rectangle {
        id: indicator
        transform: Rotation {
            angle: -90 + Math.max(0, Math.min(root.angle, 90))
            origin.y: indicator.height
        }
        width: Math.max(parent.width, parent.height)
        height: width
        y: parent.height - height
        color: root.color
    }
}

In our gauge we use 4 of those items each rotated and positioned in one quadrant of the gauge element. Also the input values are adjusted by subtracting the indicators rotation:

Item {
        id: indicatorContainer
        anchors.fill: parent
        anchors.margins: progressBarWidth

        property real normalizedValue: (root.value - root.minValue) / (root.maxValue - root.minValue)

        Indicator {
            id: indicatorTopRight
            anchors.left: parent.horizontalCenter
            anchors.bottom: parent.verticalCenter
            width: Math.max(parent.width, parent.height) / 2
            height: width
            angle: indicatorContainer.normalizedValue * 360 - rotation
        }
        Indicator {
            id: indicatorBottomRight
            anchors.left: parent.horizontalCenter
            anchors.top: parent.verticalCenter
            width: Math.max(parent.width, parent.height) / 2
            height: width
            angle: indicatorContainer.normalizedValue * 360 - rotation
            rotation: 90
        }
        Indicator {
            id: indicatorBottomLeft
            anchors.right: parent.horizontalCenter
            anchors.top: parent.verticalCenter
            width: Math.max(parent.width, parent.height) / 2
            height: width
            angle: indicatorContainer.normalizedValue * 360 - rotation
            rotation: 180
        }
        Indicator {
            id: indicatorTopLeft
            anchors.right: parent.horizontalCenter
            anchors.bottom: parent.verticalCenter
            width: Math.max(parent.width, parent.height) / 2
            height: width
            angle: indicatorContainer.normalizedValue * 360 - rotation
            rotation: 270
       }
    }

See: 5b2319d (Add indicator rectangles)

Breathing life into it

For a better understanding and to detect errors faster we add an animation of the gauge value. Therefor we'll use a infinite PropertyAnimation:

    PropertyAnimation {
        target: gauge
        property: "value"
        from: 0
        to: 100
        loops: Animation.Infinite
        duration: 5000
        easing.type: Easing.InOutCubic
        running: true
    }

See: 4e52f97 (Animate the gauge value)

Clipping indicator into a circle shape

Clipping the outer edges of our indicators is a very easy step. Therefor we use the OpacityMask form Qt's QtGraphicalEffects module and apply as layer effect of the inicatorContainer item:

        layer.enabled: true
        layer.effect: OpacityMask {
            maskSource: innerBackground
        }

What it does is it uses the maskSource item's opacity and applies it to the target item.


NOTE: Using this technique to cut away unwanted visual parts is easy and fast but is no real clipping. That means mouse events can still trigger MouseAreas positioned in areas that should be cut away!


See: ea32c58 (Add OpacityMask layer)

Finally: Clipping indicator into the shape of the progress bar

We could omit the last step if we'd move the indicatorContainer item below the innerBackground item. That would cover the inner part of the indicator and just show our bar instead of the full disc.
For the reason to show that we not only can clip the outer parts but also can punch holes into items we'll add an additional shader.

At first we wrap the innerBackground item in an additional Item (innerMaskSource) that fills it's parent (progressBarTrace).
The reason for that additional item is, that we want to use the innerBackground as mask for the indicator items. Because a shader does not know about pixels in a texture and doesn't care about sizes of textures it would "stretch" the mask to fit the target texture. That would lead to that we try to punch a hole into the item that is the same size as the item itself.
Now that we have wrapped the innerBackground and use the innerMaskSource as maskSource we have in additional margin added to our mask texture.

Item {
    id: innerMaskSource
    anchors.fill: parent
    layer.enabled: true
    Rectangle {
        id: innerBackground
        anchors.fill: parent
        anchors.margins: progressBarWidth
        radius: Math.max(width, height)
        color: root.backgroundColor
    }
}

Finally we wrap the 4 Indicator items into an additional Item. That additional item has layering enabled and as layer.effect we define a new shader.
The shader nearly does the same as the OpacityMask except for it multiplies the output by the negated value of maskSource alpha value:

Item {
    anchors.fill: parent
    layer.enabled: true
    layer.effect: ShaderEffect {
        property variant source
        property variant maskSource: innerMaskSource

        fragmentShader: "
            varying highp vec2 qt_TexCoord0;
            uniform highp float qt_Opacity;
            uniform lowp sampler2D source;
            uniform lowp sampler2D maskSource;
            void main(void) {
                gl_FragColor = texture2D(source, qt_TexCoord0.st) * (1.0 - texture2D(maskSource, qt_TexCoord0.st).a) * qt_Opacity;
            }
        "
    }
    Indicator {
//        ...
    }
    Indicator {
//        ...
    }
    Indicator {
//        ...
    }
    Indicator {
//        ...
    }
}

See: 99d7080 (Add inverted OpacityMask layer to clip inner circle)

qmlcurvedclippping's People

Contributors

e1sbaer avatar

Watchers

James Cloos avatar wangyyovo avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.