Qt Quick 3D - Stencil Outline Extension Example
// Copyright (C) 2023 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 // Mirror the internal value until re-bound property color color: root.color onColorChanged: { if (root.color != color) root.color = color; } // When we manually edit a new color // this is the new color proposal signal colorModified(color: color) width: 100 height: 26 Rectangle { id: border color: "transparent" border.width: 2 border.color: colorPicker.isHovered ? palette.dark : palette.alternateBase anchors.fill: parent Image { anchors.fill: parent anchors.margins: 4 source: "images/grid_8x8.png" fillMode: Image.Tile } Rectangle { anchors.fill: parent anchors.margins: 4 color: root.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 Pane { RowLayout { id: root property color color: "red" Item { implicitWidth: 135 implicitHeight: 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 preventStealing: true function handleMouseMove(x : real, y : real, width : real) : vector2d { 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 let color = Qt.hsva(hue, radius, root.color.hsvValue, root.color.a) colorPicker.colorModified(color) root.color = color 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) } onClicked: (mouse) => { let pos = handleMouseMove(mouse.x, mouse.y, hsvWheel.width) reticule.x = pos.x; reticule.y = pos.y; } 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() reticule.reflectColor(root.color.hsvHue, root.color.hsvSaturation) } } Connections { target: colorPicker function onColorChanged() { root.color = colorPicker.color } } 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 { SectionLayout { title: "RGB" id: rgbSection property int redValue: 0 property int greenValue: 0 property int blueValue: 0 implicitWidth: 300 function updateRGB() { let color = Qt.rgba(redValue / 255, greenValue / 255, blueValue / 255, alphaSection.alphaValue / 255) colorPicker.colorModified(color) root.color = color } onRedValueChanged: hexTextField.updateText() onGreenValueChanged: hexTextField.updateText() onBlueValueChanged: hexTextField.updateText() RowLayout { Layout.fillWidth: true 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 (pressed) rgbSection.updateRGB() } } } } SpinBox { Layout.fillWidth: true from: 0 to: 255 value: rgbSection.redValue onValueChanged: { if (value !== rgbSection.redValue) rgbSection.redValue = value } onValueModified: { rgbSection.updateRGB(); } } RowLayout { Layout.fillWidth: true 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 (pressed) rgbSection.updateRGB() } } } } SpinBox { Layout.fillWidth: true from: 0 to: 255 value: rgbSection.greenValue onValueChanged: { if (value !== rgbSection.greenValue) rgbSection.greenValue = value } onValueModified: { rgbSection.updateRGB() } } RowLayout { Layout.fillWidth: true 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 (pressed) rgbSection.updateRGB() } } } } SpinBox { Layout.fillWidth: true from: 0 to: 255 value: rgbSection.blueValue onValueChanged: { if (value !== rgbSection.blueValue) rgbSection.blueValue = value } onValueModified: { rgbSection.updateRGB() } } Label { text: "Hex:" Layout.fillWidth: true } TextField { id: hexTextField maximumLength: 6 Layout.alignment: Qt.AlignRight 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) } } } SectionLayout { title: "HSV" id: hsvSection property int hueValue: 0 property int saturationValue: 0 property int valueValue: 0 implicitWidth: 300 function updateHSV() { let color = Qt.hsva(hueValue / 360, saturationValue / 100, valueValue / 100, alphaSection.alphaValue / 255) colorPicker.colorModified(color) root.color = color } RowLayout { Layout.fillWidth: true 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 (pressed) hsvSection.updateHSV() } } } } SpinBox { Layout.fillWidth: true from: 0 to: 360 value: hsvSection.hueValue onValueChanged: { if (value !== hsvSection.hueValue) hsvSection.hueValue = value } onValueModified: { hsvSection.updateHSV() } } RowLayout { Layout.fillWidth: true 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 (pressed) hsvSection.updateHSV() } } } } SpinBox { Layout.fillWidth: true from: 0 to: 100 value: hsvSection.saturationValue onValueChanged: { if (value !== hsvSection.saturationValue) hsvSection.saturationValue = value } onValueModified: { hsvSection.updateHSV() } } RowLayout { Layout.fillWidth: true 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 (pressed) hsvSection.updateHSV() } } } } SpinBox { Layout.fillWidth: true from: 0 to: 100 value: hsvSection.valueValue onValueChanged: { if (value !== hsvSection.valueValue) hsvSection.valueValue = value } onValueModified: { hsvSection.updateHSV() } } } SectionLayout { title: "Opacity / Alpha" id: alphaSection property int alphaValue: 0 implicitWidth: 300 RowLayout { Layout.fillWidth: true 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 (pressed) hsvSection.updateHSV() } } } } SpinBox { Layout.fillWidth: true from: 0 to: 255 value: alphaSection.alphaValue onValueChanged: { if (value !== alphaSection.alphaValue) alphaSection.alphaValue = value } onValueModified: { hsvSection.updateHSV() } } } } } } } }