Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write custom shaders for circular gauges #602

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ if("${CMAKE_SYSTEM_NAME}" STREQUAL "Emscripten")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
add_compile_definitions(VENUS_WEBASSEMBLY_BUILD)
add_compile_definitions(MQTT_WEBSOCKETS_ENABLED)
find_package(Qt6 6.5.2 COMPONENTS Core Qml Quick Svg Xml LinguistTools Mqtt WebSockets REQUIRED) # require at least qt 6.5.2 for qt_add_qml_module to work properly
find_package(Qt6 6.5.2 COMPONENTS Core Qml Quick Svg Xml LinguistTools Mqtt WebSockets ShaderTools REQUIRED) # require at least qt 6.5.2 for qt_add_qml_module to work properly
else()
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
find_package(Qt6 6.5.2 COMPONENTS Core Qml Quick Svg Xml DBus LinguistTools Mqtt REQUIRED) # require at least qt 6.5.2 for qt_add_qml_module to work properly
find_package(Qt6 6.5.2 COMPONENTS Core Qml Quick Svg Xml DBus LinguistTools Mqtt ShaderTools REQUIRED) # require at least qt 6.5.2 for qt_add_qml_module to work properly
endif()

# This has to go after 'find_package(Qt6 COMPONENTS Core)', and before 'qt_add_qml_module(... QML_FILES ${VENUS_QML_MODULE_SOURCES})'
Expand Down Expand Up @@ -121,6 +121,8 @@ set (VENUS_QML_MODULE_SOURCES
components/RadioButtonControlValue.qml
components/SegmentedButtonRow.qml
components/SeparatorBar.qml
components/ShaderCircularGauge.qml
components/ShaderProgressArc.qml
components/ShinyProgressArc.qml
components/SideGauge.qml
components/SolarDetailBox.qml
Expand Down Expand Up @@ -454,6 +456,17 @@ qt_add_qml_module(VenusQMLModule
OUTPUT_DIRECTORY Victron/VenusOS
QML_FILES ${VENUS_QML_MODULE_SOURCES}
)

qt6_add_shaders(VenusQMLModule "shaders"
BATCHABLE
PRECOMPILE
OPTIMIZED
PREFIX
"/qt/qml/Victron/VenusOS/components"
FILES
"shaders/circulargauge.frag"
"shaders/progressarc.frag"
)
# end VENUS_QML_MODULE

# Dbus_QML_MODULE
Expand Down
73 changes: 44 additions & 29 deletions components/ArcGauge.qml
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,59 @@ import QtQuick.Window
import Victron.VenusOS
import Victron.Gauges

// A progress gauge running an on arc, where 0° is at the top, and positive is clockwise
// A progress gauge running an on arc, where 0° is at the top.
Item {
id: gauge

property alias value: arc.value
implicitWidth: arc.implicitWidth
implicitHeight: arc.implicitHeight

property int valueType: VenusOS.Gauges_ValueType_FallingPercentage
property int alignment: Qt.AlignTop | Qt.AlignLeft
property int direction: ((alignment & Qt.AlignLeft && alignment & Qt.AlignVCenter)
|| (alignment & Qt.AlignLeft && alignment & Qt.AlignTop)
|| (alignment & Qt.AlignRight && alignment & Qt.AlignBottom))
? PathArc.Clockwise : PathArc.Counterclockwise

property alias arcWidth: arc.width
property alias arcHeight: arc.height
property alias arcX: arc.x
property alias arcY: arc.y
property alias value: arc.value
property alias startAngle: arc.startAngle
property alias endAngle: arc.endAngle
property alias radius: arc.radius
property alias useLargeArc: arc.useLargeArc
property alias strokeWidth: arc.strokeWidth
property alias direction: arc.direction
property alias progressColor: arc.progressColor
property alias remainderColor: arc.remainderColor
property alias animationEnabled: arc.animationEnabled
property int alignment: Qt.AlignLeft
property var arcX
property var arcY

Item {
id: antialiased
anchors.fill: parent

// Antialiasing without requiring multisample framebuffers.
layer.enabled: true
layer.smooth: true
layer.textureSize: Qt.size(antialiased.width*2, antialiased.height*2)

ProgressArc {
id: arc

readonly property int status: Gauges.getValueStatus(gauge.value, gauge.valueType)

width: radius*2
height: width
x: arcX !== undefined ? arcX : (gauge.alignment & Qt.AlignRight ? (gauge.width - 2*radius) : 0)
y: arcY !== undefined ? arcY : ((gauge.height - height) / 2)
progressColor: Theme.statusColorValue(status)
remainderColor: Theme.statusColorValue(status, true)
}

readonly property int _status: Gauges.getValueStatus(value, valueType)
readonly property real _maxAngle: alignment & Qt.AlignVCenter
? Theme.geometry.briefPage.largeEdgeGauge.maxAngle
: Theme.geometry.briefPage.smallEdgeGauge.maxAngle

ShaderProgressArc {
id: arc

implicitWidth: Theme.geometry.briefPage.edgeGauge.width
implicitHeight: (alignment & Qt.AlignVCenter)
? Theme.geometry.briefPage.largeEdgeGauge.height
: Theme.geometry.briefPage.smallEdgeGauge.height

x: (alignment & Qt.AlignLeft) ? 0 : (parent.width - width)
y: (alignment & Qt.AlignTop) ? (parent.height - height)
: (alignment & Qt.AlignBottom) ? 0
: (parent.height - height)/2

startAngle: (alignment & Qt.AlignVCenter) ? 270 - _maxAngle/2
: (alignment & Qt.AlignTop) ? 270
: 90
endAngle: startAngle + _maxAngle
radius: Theme.geometry.briefPage.edgeGauge.radius - 2*strokeWidth
strokeWidth: Theme.geometry.arc.strokeWidth
progressColor: Theme.statusColorValue(_status)
remainderColor: Theme.statusColorValue(_status, true)
clockwise: direction === PathArc.Clockwise
}
}
82 changes: 36 additions & 46 deletions components/CircularMultiGauge.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,56 +21,46 @@ Item {
// Step change in the size of the bounding boxes of successive gauges
readonly property real _stepSize: 2 * (strokeWidth + Theme.geometry.circularMultiGauge.spacing)

Item {
id: antialiased
anchors.fill: parent

// Antialiasing without requiring multisample framebuffers.
layer.enabled: true
layer.smooth: true
layer.textureSize: Qt.size(antialiased.width*2, antialiased.height*2)
Repeater {
id: arcRepeater
width: parent.width
delegate: Loader {
id: loader
property int gaugeStatus: Gauges.getValueStatus(model.value, model.valueType)
property real value: model.value
width: parent.width - (index*_stepSize)
height: width
anchors.centerIn: parent
visible: model.index < Theme.geometry.briefPage.centerGauge.maximumGaugeCount
sourceComponent: model.tankType === VenusOS.Tank_Type_Battery ? shinyProgressArc : progressArc
onStatusChanged: if (status === Loader.Error) console.warn("Unable to load circular multi gauge progress arc:", errorString())

Repeater {
id: arcRepeater
width: parent.width
delegate: Loader {
id: loader
property int gaugeStatus: Gauges.getValueStatus(model.value, model.valueType)
property real value: model.value
width: parent.width - (index*_stepSize)
height: width
anchors.centerIn: parent
visible: model.index < Theme.geometry.briefPage.centerGauge.maximumGaugeCount
sourceComponent: model.tankType === VenusOS.Tank_Type_Battery ? shinyProgressArc : progressArc
onStatusChanged: if (status === Loader.Error) console.warn("Unable to load circular multi gauge progress arc:", errorString())

Component {
id: shinyProgressArc
ShinyProgressArc {
radius: width/2
startAngle: 0
endAngle: 270
value: loader.value
progressColor: Theme.statusColorValue(loader.gaugeStatus)
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
strokeWidth: gauges.strokeWidth
animationEnabled: gauges.animationEnabled
shineAnimationEnabled: Global.batteries.system.mode === VenusOS.Battery_Mode_Charging
}
Component {
id: shinyProgressArc
ShaderCircularGauge {
startAngle: 0
endAngle: 270
value: loader.value
progressColor: Theme.statusColorValue(loader.gaugeStatus)
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
strokeWidth: gauges.strokeWidth
animationEnabled: gauges.animationEnabled
shineAnimationEnabled: Global.batteries.system.mode === VenusOS.Battery_Mode_Charging
}
}

Component {
id: progressArc
ProgressArc {
radius: width/2
startAngle: 0
endAngle: 270
value: loader.value
progressColor: Theme.statusColorValue(loader.gaugeStatus)
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
strokeWidth: gauges.strokeWidth
animationEnabled: gauges.animationEnabled
}
Component {
id: progressArc
ShaderCircularGauge {
startAngle: 0
endAngle: 270
value: loader.value
progressColor: Theme.statusColorValue(loader.gaugeStatus)
remainderColor: Theme.statusColorValue(loader.gaugeStatus, true)
strokeWidth: gauges.strokeWidth
animationEnabled: gauges.animationEnabled
shineAnimationEnabled: false
}
}
}
Expand Down
35 changes: 12 additions & 23 deletions components/CircularSingleGauge.qml
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,18 @@ Item {
property alias animationEnabled: arc.animationEnabled
property alias shineAnimationEnabled: arc.shineAnimationEnabled

Item {
id: antialiased
anchors.fill: parent

// Antialiasing without requiring multisample framebuffers.
layer.enabled: true
layer.smooth: true
layer.textureSize: Qt.size(antialiased.width*2, antialiased.height*2)

// The single circular gauge is always the battery gauge :. shiny.
ShinyProgressArc {
id: arc

width: gauges.width
height: width
anchors.centerIn: parent
radius: width/2
startAngle: 0
endAngle: 359 // "Note that a single PathArc cannot be used to specify a circle."
progressColor: Theme.statusColorValue(gauges.status)
remainderColor: Theme.statusColorValue(gauges.status, true)
strokeWidth: Theme.geometry.circularSingularGauge.strokeWidth
}
// The single circular gauge is always the battery gauge :. shiny.
ShaderCircularGauge {
id: arc

width: gauges.width
height: width
anchors.centerIn: parent
startAngle: 0
endAngle: 359 // "Note that a single PathArc cannot be used to specify a circle."
progressColor: Theme.statusColorValue(gauges.status)
remainderColor: Theme.statusColorValue(gauges.status, true)
strokeWidth: Theme.geometry.circularSingularGauge.strokeWidth
}

Column {
Expand Down
81 changes: 81 additions & 0 deletions components/ShaderCircularGauge.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import QtQuick
import Victron.VenusOS

Item {
id: gauge

property real value // from 0.0 to 100.0
property color remainderColor: "gray"
property color progressColor: "blue"
property color shineColor: Qt.rgba(1.0, 1.0, 1.0, 0.80)
property real startAngle: 0
property real endAngle: 270
property real progressAngle: startAngle + ((endAngle - startAngle) * Math.min(Math.max(gauge.value, 0.0), 100.0) / 100.0)
property real strokeWidth: width/25
property real radius: (width - strokeWidth - smoothing)/2
property real smoothing: 1 // how many pixels of antialiasing to apply.
property bool clockwise: true
property bool shineAnimationEnabled: true
property bool animationEnabled: true

property real _normalizedRadiansFactor: (Math.PI/180) / (2*Math.PI)
property real _maxRadius: width/2

onProgressAngleChanged: {
if (!progressAnimator.running) {
progressAnimator.from = shader.progressAngle
progressAnimator.to = gauge.progressAngle * _normalizedRadiansFactor
progressAnimator.start()
}
}

Timer {
running: gauge.shineAnimationEnabled
interval: Theme.animation.briefPage.centerGauge.shine.duration + (Theme.animation.briefPage.centerGauge.shine.duration * Theme.animation.briefPage.centerGauge.shine.pauseRatio)
repeat: true
onTriggered: {
shineAnimator.duration = (gauge.progressAngle / gauge.endAngle) * Theme.animation.briefPage.centerGauge.shine.duration
shineAnimator.from = 0.0
shineAnimator.to = Math.min(gauge.endAngle, gauge.progressAngle+5) * _normalizedRadiansFactor
shineAnimator.start()
}
}

ShaderEffect {
id: shader
anchors.fill: parent
fragmentShader: "shaders/circulargauge.frag.qsb"

property color remainderColor: gauge.remainderColor
property color progressColor: gauge.progressColor
property color shineColor: gauge.shineColor
// transform angles to radians and then normalize
property real startAngle: gauge.startAngle * gauge._normalizedRadiansFactor
property real endAngle: gauge.endAngle * gauge._normalizedRadiansFactor
property real progressAngle: -1.0
property real shineAngle: -1.0
// transform radii to uv coords
property real innerRadius: (gauge.radius - (gauge.strokeWidth/2)) / (gauge._maxRadius)
property real radius: gauge.radius / (gauge._maxRadius)
property real outerRadius: (gauge.radius + (gauge.strokeWidth/2)) / (gauge._maxRadius)
// transform smoothing pixels to uv distance
property real smoothing: gauge.smoothing / gauge._maxRadius
property real clockwise: gauge.clockwise ? 1.0 : 0.0

UniformAnimator {
id: progressAnimator
target: shader
uniform: "progressAngle"
duration: Theme.animation.progressArc.duration
easing.type: Easing.InOutQuad
}

UniformAnimator {
id: shineAnimator
target: shader
uniform: "shineAngle"
easing.type: Easing.InQuad
onRunningChanged: if (!running) shader.shineAngle = -1.0
}
}
}
Loading