5

The look and feel I'm trying to go for is to have a solid color button, and text on it like "Hello World" where the text is completely transparent, and the background shows through the button.

In other words, having text as a transparency mask on a button element.

dtech
  • 47,916
  • 17
  • 112
  • 190
James
  • 2,742
  • 1
  • 20
  • 43

6 Answers6

7

Here is one way to do it:

// TB.qml
MouseArea {
  width: txt.contentWidth + 20
  height: txt.contentHeight + 10
  property alias text: txt.text
  property alias color: sh.color
  ShaderEffect {
    id: sh
    anchors.fill: parent
    property color color: "red" 
    property var source : ShaderEffectSource {
      sourceRect: Qt.rect(0, 0, sh.width, sh.height)
      sourceItem: Item {
        width: sh.width
        height: sh.height
        Text {
          id: txt
          anchors.centerIn: parent
          font.bold: true
          font.pointSize: 30
          text: "test"
        }
      }
    }
    fragmentShader:
        "varying highp vec2 qt_TexCoord0;
           uniform highp vec4 color;
           uniform sampler2D source;
           void main() {
               gl_FragColor = color * (1.0 - texture2D(source, qt_TexCoord0).w);
           }"
  }
}

Using it:

  TB {
    text: "HELLO WORLD!!!"
    color: "red"
    onClicked: console.log("hi world")
  }

Result:

enter image description here

The button is red, the text is grey from the grey background, and it will accurately show anything that's beneath the button.

Obviously, the button is rudimentary, but the example outta be enough to get you going and implement something according to your needs.

The key element here is the custom shader, which is a very basic one - it colorizes every fragment and applies the mask as alpha. Obviously, you can use ShaderEffectSource to turn any QML Item to a texture, and replace the ShaderEffectSource with another sampler 2D and mix the two textures in any way you want, cut using the alpha channel, or any of the RGB if you are using a grayscale mask. And unlike the rather limited OpacityMask element, this will actually cut through and show anything that is underneath as it is supposed to.

dtech
  • 47,916
  • 17
  • 112
  • 190
7

You can achieve that using layer attached property as follow without using OpacityMask.
Also you does not any limitation and you can use any qml item, use any QtQuick.Controls and style it as usual :)

result

Image {
    id: bk
    source: "http://l7.alamy.com/zooms/7b6f221aadd44ffab6a87c234065b266/sheikh-lotfollah-mosque-at-naqhsh-e-jahan-square-in-isfahan-iran-interior-g07fw2.jpg"
}

Button {
    id: button
    anchors.centerIn: bk
    width: 210; height: 72
    visible: true
    opacity: 0.0
    layer.enabled: true
    layer.smooth: true
    onClicked: console.log("Clicked")
}

Rectangle {
    id: _mask
    anchors.fill: button
    color: "transparent"
    visible: true
    Text {
        font { pointSize: 20; bold: true }
        anchors.centerIn: parent
        text: "Hello World!"
    }

    layer.enabled: true
    layer.samplerName: "maskSource"
    layer.effect: ShaderEffect {
        property variant source: button
        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;
                }
            "
    }
}
S.M.Mousavi
  • 5,013
  • 7
  • 44
  • 59
4

What you want is an OpacityMask that could be inverted. This is planned in a future release of Qt (5.7.1 maybe ?) and in fact you can already take a look at it : https://github.com/qt/qtgraphicaleffects/blob/5.7.1/src/effects/OpacityMask.qml

Meanwhile you could copy the code in a file named MyOpacityMask.qml or something else and use it this way with a Button from Qt Quick Controls 2 :

Button {
    id: button
    text: "Yolo"
    background.visible: false
    contentItem.visible: false
    contentItem.anchors.fill: button //can be avoided at the cost of creating another Item and ShaderEffectSource
    MyOpacityMask {
        anchors.fill: parent
        invert: true
        source: button.background
        maskSource: button.contentItem
    }
}

This has previously been mentioned in Opposite for OpacityMask

Community
  • 1
  • 1
GrecKo
  • 6,615
  • 19
  • 23
2

I think this is what OpacityMask is for.

The sample below seems to illustrate the effect you're looking for. The background is a solid red Rectangle. The foreground is a solid blue Rectangle. A Text object, which black text is on top of the blue foreground. The OpacityMask uses the Text as a mask for the background, which results in red text appearing.

import QtQuick 2.7
import QtGraphicalEffects 1.0

Rectangle {
    width: 500
    height: 500

    Rectangle {
        id: background
        anchors.fill: parent

        Rectangle {
            id: left
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            anchors.left: parent.left
            width: parent.width / 2
            color: "red"

            ColorAnimation on color {
                from: "red"
                to: "green"
                duration: 5000
                loops: Animation.Infinite
            }
        }

        Rectangle {
            id: right 
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            anchors.right: parent.right
            width: parent.width / 2
            color: "red"
        }
    }

    Rectangle {
        id: foreground
        anchors.fill: parent
        color: "blue"
    }

    Text {
        id: txt
        anchors.centerIn: parent
        text: "Test"
        font.pointSize: 60
        color: "black"
    }

    OpacityMask {
        anchors.fill: txt
        source: background
        maskSource: txt
    }
}

Update: Added a more complex background to better illustrate that the OpacityMask is really computing cut through layer. Also added an animation to show the computed cut through changing over time.

Tim
  • 4,560
  • 2
  • 40
  • 64
  • Have you actually tested that? It doesn't look like it will produce the required result, plus you are missing the "" around `black`. – dtech Oct 06 '16 at 23:29
  • Yes, I ran it in `qmlscene`, admittedly from `5.8.0-beta`, but it should be ok in `5.7`. As for the missing quotes, fixed it... interestingly, `qmlscene` didn't complain. – Tim Oct 06 '16 at 23:32
  • It doesn't produce the expected result for me. There is one huge blue rectangle with one huge blurry red Test inside, but the actual background doesn't sow through. – dtech Oct 06 '16 at 23:34
  • I fixed the target of `anchors.flll` for the mask. Now you'll get small red text in centered in the blue foreground. Since the text is red, it is showing that the background has shown through. – Tim Oct 06 '16 at 23:35
  • And it still doesn't work - I get a blue rect with red text, but my background is actually grey. The text is not transparent, it is red. – dtech Oct 06 '16 at 23:35
  • In this case, the `Rectangle` background is layered below foreground, so it is the background. I don't think the `OpacityMask` drills down infinitely, it just computes what it would look like if the `txt` element were transparently overlaid on the `background` element. – Tim Oct 06 '16 at 23:37
  • That's not what the OP wants, he wants the text to "cut through" and show what's below the button, at least that's how I understand it. – dtech Oct 06 '16 at 23:38
  • This does cut through, just in a round-about way. It computes what the cut through might look like, then overlays an opaque version of that on top. I agree it isn't perfect, and I'd love to see a suggestion that does this more intelligently :) – Tim Oct 06 '16 at 23:39
  • It still doesn't show the actual background under the button, just its sibling. – dtech Oct 07 '16 at 00:04
1

If you are using Qt6.5 and don't want to include the Qt5 compatibility module, you need to implement a shader in a separate file:

// ClipMask.frag
#version 440

layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;

layout(binding = 1) uniform sampler2D source;
layout(binding = 2) uniform sampler2D maskSource;

layout(std140, binding = 0) uniform buf {
    mat4 qt_Matrix;
    float qt_Opacity;
    bool invert;
};

void main()
{
    float mask = texture(maskSource, qt_TexCoord0).a;
    if(invert)
        mask = 1.0-mask;

    fragColor = texture(source, qt_TexCoord0) * mask * qt_Opacity;
}

This is compiled into a qsb file with the command qsb --glsl 440 --hlsl 50 --msl 12 -o ClipMask.frag.qsb ClipMask.frag

Then you can make a qml type that links properties to the shader variables:

// ShaderClipMask.qml
import QtQuick 2.15

ShaderEffect {
    id: root
    required property Item sourceItem
    required property Item maskSourceItem

    property var source: ShaderEffectSource{
        sourceItem: root.sourceItem
    }
    property var maskSource: ShaderEffectSource{
        sourceItem:Item{
            id: mask
            height:root.sourceItem.height
            width:root.sourceItem.width
            children: root.maskSourceItem
        }
    }
    property bool invert:false
    fragmentShader: "qrc:/QML/imports/shaders/ClipMask.frag.qsb"
}

Used like this:

Rectangle{
    id: donut
    width: 100
    height: width
    radius: width/2

    layer.enabled: true
    layer.effect:ShaderClipMask{
        id: hole
        sourceItem: donut
        invert: true
        maskSourceItem: Rectangle{
            anchors.fill: parent
            anchors.margins: donut.width/4
            radius:width/2
        }
    }
}
0

There are answers with OpacityMask, ShaderEffects, or layer.effect. There isn't an answer that demonstrates these things can be mixed, so, I thought at least demonstrate that layer.effect can be used with OpacityMask.

In the following example, we define a white Rectangle, but, then, with an OpacityMask defined as a layer.effect we cut out "Hello World!" so that we can see what's happening underneath.

    Rectangle {
        width: 400
        height: 200
        color: "white"
        layer.enabled: true
        layer.effect: OpacityMask {
            invert: true
            maskSource: Item {
                width: 400
                height: 200
                Text {
                    anchors.centerIn: parent
                    text: "Hello World!"
                    font.pixelSize: 60
                    font.bold: true
                }
            }
        }
    }

You can Try it Online!

Stephen Quan
  • 21,481
  • 4
  • 88
  • 75