Qt Quick 3D - Scene Effects Example

 // Copyright (C) 2022 The Qt Company Ltd.
 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

 import QtQuick
 import QtQuick.Controls
 import QtQuick.Layouts

 Item {
     id: colorPicker
     property bool isHovered: false
     property alias color: root.color

     width: 100
     height: 26

     Rectangle {
         id: border
         color: "transparent"
         border.width: 2
         border.color: isHovered ? palette.dark : palette.alternateBase
         anchors.fill: parent

         Image {
             anchors.fill: parent
             anchors.margins: 4
             source: "qrc:/images/grid_8x8.png"
             fillMode: Image.Tile
         }

         Rectangle {
             anchors.fill: parent
             anchors.margins: 4
             color: colorPicker.color
         }

         MouseArea {
             anchors.fill: parent
             hoverEnabled: true
             onEntered: colorPicker.isHovered = true
             onExited: colorPicker.isHovered = false
             onClicked: {
                 colorPickerPopup.open()
             }
         }
     }

     Dialog {
         id: colorPickerPopup
         title: "Color Picker"

         anchors.centerIn: Overlay.overlay
         modal: true
         focus: true
         closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent

         RowLayout {
             anchors.centerIn: parent
             id: root
             property color color: "red"
             spacing: 0

             Item {
                 width: 135
                 height: 135
                 ShaderEffect {
                     id: hsvWheel
                     anchors.centerIn: parent
                     blending: true
                     width: 128
                     height: 128
                     property real value: 1.0

                     fragmentShader: "qrc:/shaders/huesaturation.frag.qsb"

                     Item {
                         id: reticule

                         function reflectColor(hue, saturation) {
                             let angleDegrees = hue * 360
                             if (angleDegrees > 180)
                                 angleDegrees = angleDegrees - 360
                             let angleRadians = angleDegrees * (Math.PI / 180)
                             let vector = Qt.vector2d(Math.cos(angleRadians), Math.sin(angleRadians)).normalized().times(0.5).times(saturation)
                             vector = vector.plus(Qt.vector2d(0.5, 0.5))
                             reticule.x = vector.x * hsvWheel.width
                             reticule.y = vector.y * hsvWheel.width
                         }

                         Rectangle {
                             x: -5
                             y: -5
                             width: 10
                             height: 10
                             radius: 5
                             color: "transparent"
                             border.width: 1
                             border.color: "black"

                             Rectangle {
                                 x: 0.5
                                 y: 0.5
                                 width: 9
                                 height: 9
                                 radius: 4.5
                                 color: root.color
                                 border.width: 1
                                 border.color: "white"
                             }
                         }
                     }

                     MouseArea {
                         anchors.fill: parent

                         function handleMouseMove(x, y, width) {
                             let normalizedX = x / width - 0.5;
                             let normalizedY = y / width - 0.5;
                             let angle = Math.atan2(normalizedY, normalizedX);
                             let toCenter = Qt.vector2d(normalizedX, normalizedY);
                             let radius = toCenter.length() * 2.0;
                             let degrees = angle * (180 / Math.PI)
                             if (degrees < 0)
                                 degrees = 360 + degrees
                             let hue = degrees / 360

                             root.color = Qt.hsva(hue, radius, root.color.hsvValue, root.color.a)

                             if (radius <= 1.0)
                                 return Qt.vector2d(x, y)
                             // Limit to radius of 1.0
                             toCenter = toCenter.normalized();
                             let halfWidth = width * 0.5;
                             let newX = halfWidth * toCenter.x + halfWidth
                             let newY = halfWidth * toCenter.y + halfWidth
                             return Qt.vector2d(newX, newY)
                         }

                         onPositionChanged: (mouse) => {
                                                let pos = handleMouseMove(mouse.x, mouse.y, hsvWheel.width)
                                                reticule.x = pos.x;
                                                reticule.y = pos.y;
                                            }
                     }
                 }
             }

             Component.onCompleted: {
                 updateColorSections()
                 reticule.reflectColor(root.color.hsvHue, root.color.hsvSaturation)
             }

             Connections {
                 target: root
                 function onColorChanged() {
                     root.updateColorSections()
                 }
             }

             function updateColorSections() {
                 rgbSection.redValue = root.color.r * 255
                 rgbSection.greenValue = root.color.g * 255
                 rgbSection.blueValue = root.color.b * 255
                 hsvSection.hueValue = root.color.hsvHue * 360
                 hsvSection.saturationValue = root.color.hsvSaturation * 100
                 hsvSection.valueValue = root.color.hsvValue  * 100
                 alphaSection.alphaValue = root.color.a * 255
             }
             ColumnLayout {
                 width: 250
                 SectionLayout {
                     title: "RGB"
                     id: rgbSection
                     property int redValue: 0
                     property int greenValue: 0
                     property int blueValue: 0

                     function updateRGB() {
                         root.color = Qt.rgba(redValue / 255, greenValue / 255, blueValue / 255, alphaSection.alphaValue / 255)
                         reticule.reflectColor(root.color.hsvHue, root.color.hsvSaturation)
                     }

                     RowLayout {
                         Label {
                             text: "R:"
                         }
                         Slider {
                             id: redSlider
                             Layout.fillWidth: true
                             from: 0
                             to: 255
                             value: rgbSection.redValue
                             onValueChanged: {
                                 if (value !== rgbSection.redValue) {
                                     rgbSection.redValue = value
                                     if (activeFocus)
                                         rgbSection.updateRGB()
                                 }
                             }
                         }
                         SpinBox {
                             from: 0
                             to: 255
                             value: rgbSection.redValue
                             onValueChanged: {
                                 if (value !== rgbSection.redValue) {
                                     rgbSection.redValue = value
                                     if (activeFocus)
                                         rgbSection.updateRGB()
                                 }
                             }
                         }
                     }
                     RowLayout {
                         Label {
                             text: "G:"
                         }
                         Slider {
                             id: greenSlider
                             Layout.fillWidth: true
                             from: 0
                             to: 255
                             value: rgbSection.greenValue
                             onValueChanged: {
                                 if (value !== rgbSection.greenValue) {
                                     rgbSection.greenValue = value
                                     if (activeFocus)
                                         rgbSection.updateRGB()
                                 }
                             }
                         }
                         SpinBox {
                             from: 0
                             to: 255
                             value: rgbSection.greenValue
                             onValueChanged: {
                                 if (value !== rgbSection.greenValue) {
                                     rgbSection.greenValue = value
                                     if (activeFocus)
                                         rgbSection.updateRGB()
                                 }
                             }
                         }
                     }
                     RowLayout {
                         Label {
                             text: "B:"
                         }
                         Slider {
                             id: blueSlider
                             Layout.fillWidth: true
                             from: 0
                             to: 255
                             value: rgbSection.blueValue
                             onValueChanged: {
                                 if (value !== rgbSection.blueValue) {
                                     rgbSection.blueValue = value
                                     if (activeFocus)
                                         rgbSection.updateRGB()
                                 }
                             }
                         }
                         SpinBox {
                             from: 0
                             to: 255
                             value: rgbSection.blueValue
                             onValueChanged: {
                                 if (value !== rgbSection.blueValue) {
                                     rgbSection.blueValue = value
                                     if (activeFocus)
                                         rgbSection.updateRGB()
                                 }
                             }
                         }
                     }
                     RowLayout {
                         Label {
                             text: "Hex:"
                             Layout.fillWidth: true
                         }
                         TextField {
                             id: hexTextField
                             maximumLength: 6
                             implicitWidth: 75

                             function updateText() {
                                 if (activeFocus)
                                     return

                                 let redText = rgbSection.redValue.toString(16).toUpperCase()
                                 let greenText = rgbSection.greenValue.toString(16).toUpperCase()
                                 let blueText = rgbSection.blueValue.toString(16).toUpperCase()
                                 if (redText.length == 1)
                                     redText = "0" + redText
                                 if (greenText.length == 1)
                                     greenText = "0" + greenText
                                 if (blueText.length == 1)
                                     blueText = "0" + blueText
                                 text = redText + greenText + blueText;
                             }

                             function expandText(text) {
                                 let newText = text.toUpperCase()
                                 let expandLength = 6 - newText.length
                                 for (let i = 0; i < expandLength; ++i)
                                     newText = "0" + newText
                                 return newText
                             }

                             Component.onCompleted: updateText()
                             validator: RegularExpressionValidator {
                                 regularExpression: /^[0-9A-Fa-f]{0,6}$/
                             }
                             onTextChanged: {
                                 if (!acceptableInput)
                                     return;
                                 let colorText = expandText(text)
                                 rgbSection.redValue = parseInt(colorText.substr(0, 2), 16)
                                 rgbSection.greenValue = parseInt(colorText.substr(2, 2), 16)
                                 rgbSection.blueValue = parseInt(colorText.substr(4, 2), 16)
                                 if (activeFocus)
                                     rgbSection.updateRGB()
                             }

                             onAccepted: {
                                 text = expandText(text)
                             }

                         }
                         Connections {
                             target: rgbSection
                             function onRedValueChanged() {
                                 hexTextField.updateText()
                             }
                             function onGreenValueChanged() {
                                 hexTextField.updateText()
                             }
                             function onBlueValueChanged() {
                                 hexTextField.updateText()
                             }
                         }
                     }
                 }

                 SectionLayout {
                     title: "HSV"
                     id: hsvSection
                     property int hueValue: 0
                     property int saturationValue: 0
                     property int valueValue: 0

                     function updateHSV() {
                         root.color = Qt.hsva(hueValue / 360, saturationValue / 100, valueValue / 100, alphaSection.alphaValue / 255)
                         reticule.reflectColor(root.color.hsvHue, root.color.hsvSaturation)
                     }

                     RowLayout {
                         Label {
                             text: "H:"
                         }
                         Slider {
                             id: hueSlider
                             Layout.fillWidth: true
                             from: 0
                             to: 360
                             value: hsvSection.hueValue
                             onValueChanged: {
                                 if (value !== hsvSection.hueValue) {
                                     hsvSection.hueValue = value
                                     if (activeFocus)
                                         hsvSection.updateHSV()
                                 }
                             }
                         }
                         SpinBox {
                             from: 0
                             to: 360
                             value: hsvSection.hueValue
                             onValueChanged: {
                                 if (value !== hsvSection.hueValue) {
                                     hsvSection.hueValue = value
                                     if (activeFocus)
                                         hsvSection.updateHSV()
                                 }
                             }
                         }
                     }

                     RowLayout {
                         Label {
                             text: "S:"
                         }
                         Slider {
                             id: saturationSlider
                             Layout.fillWidth: true
                             from: 0
                             to: 100
                             value: hsvSection.saturationValue
                             onValueChanged: {
                                 if (value !== hsvSection.saturationValue) {
                                     hsvSection.saturationValue = value
                                     if (activeFocus)
                                         hsvSection.updateHSV()
                                 }
                             }
                         }
                         SpinBox {
                             from: 0
                             to: 100
                             value: hsvSection.saturationValue
                             onValueChanged: {
                                 if (value !== hsvSection.saturationValue) {
                                     hsvSection.saturationValue = value
                                     if (activeFocus)
                                         hsvSection.updateHSV()
                                 }
                             }
                         }
                     }

                     RowLayout {
                         Label {
                             text: "V:"
                         }
                         Slider {
                             id: valueSlider
                             Layout.fillWidth: true
                             from: 0
                             to: 100
                             value: hsvSection.valueValue
                             onValueChanged: {
                                 if (value !== hsvSection.valueValue) {
                                     hsvSection.valueValue = value
                                     if (activeFocus)
                                         hsvSection.updateHSV()
                                 }
                             }
                         }
                         SpinBox {
                             from: 0
                             to: 100
                             value: hsvSection.valueValue
                             onValueChanged: {
                                 if (value !== hsvSection.valueValue) {
                                     hsvSection.valueValue = value
                                     if (activeFocus)
                                         hsvSection.updateHSV()
                                 }
                             }
                         }
                     }
                 }

                 SectionLayout {
                     title: "Opacity / Alpha"
                     id: alphaSection
                     property int alphaValue: 0

                     RowLayout {
                         Label {
                             text: "V:"
                         }
                         Slider {
                             id: alphaSlider
                             Layout.fillWidth: true
                             from: 0
                             to: 255
                             value: alphaSection.alphaValue
                             onValueChanged: {
                                 if (value !== alphaSection.alphaValue) {
                                     alphaSection.alphaValue = value
                                     if (activeFocus)
                                         hsvSection.updateHSV()
                                 }
                             }
                         }
                         SpinBox {
                             from: 0
                             to: 255
                             value: alphaSection.alphaValue
                             onValueChanged: {
                                 if (value !== alphaSection.alphaValue) {
                                     alphaSection.alphaValue = value
                                     if (activeFocus)
                                         hsvSection.updateHSV()
                                 }
                             }
                         }
                     }
                 }
             }
         }
     }
 }