From 10e70ee9458a128807161435120b746c407ec5dd Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Fri, 8 Dec 2023 22:21:08 +1000 Subject: [PATCH] Write custom shaders for circular gauges and arc gauges Animating the Shape-based gauges is too expensive. So reimplement them as single-pass shaders. --- CMakeLists.txt | 17 ++++- components/ArcGauge.qml | 73 ++++++++++++-------- components/CircularMultiGauge.qml | 82 ++++++++++------------ components/CircularSingleGauge.qml | 35 ++++------ components/ShaderCircularGauge.qml | 81 ++++++++++++++++++++++ components/ShaderProgressArc.qml | 78 +++++++++++++++++++++ components/SideGauge.qml | 15 ---- components/SolarYieldGauge.qml | 11 +-- pages/BriefPage.qml | 5 +- shaders/circulargauge.frag | 106 +++++++++++++++++++++++++++++ shaders/progressarc.frag | 106 +++++++++++++++++++++++++++++ src/main.cpp | 1 + themes/animation/Animation.json | 2 +- 13 files changed, 484 insertions(+), 128 deletions(-) create mode 100644 components/ShaderCircularGauge.qml create mode 100644 components/ShaderProgressArc.qml create mode 100644 shaders/circulargauge.frag create mode 100644 shaders/progressarc.frag diff --git a/CMakeLists.txt b/CMakeLists.txt index 2be0a82a8..c05802b14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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})' @@ -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 @@ -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 diff --git a/components/ArcGauge.qml b/components/ArcGauge.qml index 30cb7ad04..1f572b16d 100644 --- a/components/ArcGauge.qml +++ b/components/ArcGauge.qml @@ -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 } } diff --git a/components/CircularMultiGauge.qml b/components/CircularMultiGauge.qml index 72d20d122..27e66d94f 100644 --- a/components/CircularMultiGauge.qml +++ b/components/CircularMultiGauge.qml @@ -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 } } } diff --git a/components/CircularSingleGauge.qml b/components/CircularSingleGauge.qml index ab62e73a2..5c93eab4b 100644 --- a/components/CircularSingleGauge.qml +++ b/components/CircularSingleGauge.qml @@ -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 { diff --git a/components/ShaderCircularGauge.qml b/components/ShaderCircularGauge.qml new file mode 100644 index 000000000..81792f902 --- /dev/null +++ b/components/ShaderCircularGauge.qml @@ -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 + } + } +} diff --git a/components/ShaderProgressArc.qml b/components/ShaderProgressArc.qml new file mode 100644 index 000000000..df0ba7bd8 --- /dev/null +++ b/components/ShaderProgressArc.qml @@ -0,0 +1,78 @@ +/* +** Copyright (C) 2023 Victron Energy B.V. +** See LICENSE.txt for license information. +*/ + +import QtQuick +import Victron.VenusOS + +Item { + id: gauge + + property real value // from 0.0 to 100.0 + property color remainderColor: Theme.color.darkOk + property color progressColor: Theme.color.ok + property real startAngle: 270 + property real endAngle: startAngle + Theme.geometry.briefPage.smallEdgeGauge.maxAngle + 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: Theme.geometry.briefPage.edgeGauge.radius + property real smoothing: 1 // how many pixels of antialiasing to apply. + property bool clockwise: true + property bool animationEnabled: true + + // the radius of the outermost part of the arc we draw. + // we scale everything to uv coordinates with respect to + // the origin of a circle with this radius. + readonly property real _maxRadius: gauge.radius + gauge.strokeWidth/2 + 2*gauge.smoothing + // when we calculate the uv coordinate transformations, + // leave just enough of the opposite quadrant visible + // so that we can draw the startcap. + readonly property real _startcapAmount: ((strokeWidth/2 + 2*smoothing)/_maxRadius) + property real _normalizedRadiansFactor: (Math.PI/180) / (2*Math.PI) + + onProgressAngleChanged: { + if (!progressAnimator.running) { + progressAnimator.from = shader.progressAngle + progressAnimator.to = gauge.progressAngle * gauge._normalizedRadiansFactor + progressAnimator.start() + } + } + + ShaderEffect { + id: shader + anchors.fill: parent + fragmentShader: "shaders/progressarc.frag.qsb" + + property color remainderColor: gauge.remainderColor + property color progressColor: gauge.progressColor + // 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 + // transform radii to uv coords + property real innerRadius: (gauge.radius - gauge.strokeWidth/2 - gauge.smoothing)/gauge._maxRadius + property real radius: gauge.radius/gauge._maxRadius + property real outerRadius: (gauge.radius + gauge.strokeWidth/2 + gauge.smoothing)/gauge._maxRadius + // transform smoothing pixels to uv distance + property real smoothing: gauge.smoothing / gauge._maxRadius + property real clockwise: gauge.clockwise ? 1.0 : 0.0 + // perform some uv transformations to "clip" the viewport to the section of the arc. + property real xscale: (gauge.width/gauge._maxRadius) + property real xtranslate: ((gauge.clockwise && (gauge.startAngle > 180.0)) || (!gauge.clockwise && gauge.startAngle < 180)) + ? -1.0 // left gauges + : (1-(gauge.width/gauge._maxRadius)) // right gauges + property real yscale: (gauge.height/gauge._maxRadius) + property real ytranslate: (gauge.startAngle < 180.0) ? (0.0 - gauge._startcapAmount) // small gauges, in bottom left or bottom right quadrants + : (gauge.startAngle < 270.0) ? -((gauge.height/gauge._maxRadius)/2) // large gauges + : ((-gauge.height/gauge._maxRadius) + gauge._startcapAmount) // small gauges, in top left or top right quadrants + + UniformAnimator { + id: progressAnimator + target: shader + uniform: "progressAngle" + duration: Theme.animation.progressArc.duration + easing.type: Easing.InOutQuad + } + } +} diff --git a/components/SideGauge.qml b/components/SideGauge.qml index ee7cd5d1c..52bf47d82 100644 --- a/components/SideGauge.qml +++ b/components/SideGauge.qml @@ -14,21 +14,6 @@ ArcGauge { property alias icon: quantityLabel.icon property alias quantityLabel: quantityLabel.quantityLabel - readonly property int _maxArcHeight: Math.sin(Utils.degreesToRadians(_maxAngle)) * radius - readonly property int _arcOffset: -(radius - root.height) - strokeWidth / 2 - readonly property real _maxAngle: alignment & Qt.AlignVCenter ? Theme.geometry.briefPage.largeEdgeGauge.maxAngle : Theme.geometry.briefPage.smallEdgeGauge.maxAngle - - implicitWidth: Theme.geometry.briefPage.edgeGauge.width - implicitHeight: alignment & Qt.AlignVCenter ? Theme.geometry.briefPage.largeEdgeGauge.height : Theme.geometry.briefPage.smallEdgeGauge.height - alignment: Qt.AlignTop | Qt.AlignLeft - direction: PathArc.Counterclockwise - startAngle: alignment & Qt.AlignTop ? 90 : alignment & Qt.AlignVCenter ? 90 + _maxAngle/2 : 90 + _maxAngle - endAngle: direction === PathArc.Counterclockwise ? startAngle - _maxAngle : startAngle + _maxAngle - radius: Theme.geometry.briefPage.edgeGauge.radius - useLargeArc: false - strokeWidth: Theme.geometry.arc.strokeWidth - arcY: alignment & Qt.AlignTop ? _arcOffset : alignment & Qt.AlignVCenter ? undefined : _arcOffset - _maxArcHeight - ArcGaugeQuantityLabel { id: quantityLabel diff --git a/components/SolarYieldGauge.qml b/components/SolarYieldGauge.qml index 3ac967cb1..4f64d59ac 100644 --- a/components/SolarYieldGauge.qml +++ b/components/SolarYieldGauge.qml @@ -14,7 +14,6 @@ Item { property alias alignment: quantityLabel.alignment property alias label: quantityLabel property bool animationEnabled - readonly property int _maxAngle: alignment & Qt.AlignVCenter ? Theme.geometry.briefPage.largeEdgeGauge.maxAngle : Theme.geometry.briefPage.smallEdgeGauge.maxAngle implicitHeight: alignment & Qt.AlignVCenter ? Theme.geometry.briefPage.largeEdgeGauge.height : Theme.geometry.briefPage.smallEdgeGauge.height @@ -26,16 +25,12 @@ Item { delegate: ArcGauge { animationEnabled: root.animationEnabled width: Theme.geometry.briefPage.edgeGauge.width - x: index*strokeWidth + arcX: index*strokeWidth + //arcY: (root.alignment & Qt.AlignVCenter) ? 0 : (-radius + strokeWidth/2) opacity: 1.0 - index * 0.3 height: root.height - startAngle: root.alignment & Qt.AlignVCenter ? 270 + _maxAngle/2 : 270 - endAngle: startAngle - _maxAngle + alignment: root.alignment radius: Theme.geometry.briefPage.edgeGauge.radius - index*strokeWidth - useLargeArc: false - direction: PathArc.Counterclockwise - strokeWidth: Theme.geometry.arc.strokeWidth - arcY: root.alignment & Qt.AlignVCenter ? undefined : -radius + strokeWidth/2 value: { if (!visible || solarMeasurements.maxPower == 0) { // No useful max yet, so show a full gauge diff --git a/pages/BriefPage.qml b/pages/BriefPage.qml index 61d9a0339..0a0cd8b4a 100644 --- a/pages/BriefPage.qml +++ b/pages/BriefPage.qml @@ -102,9 +102,6 @@ Page { sourceComponent: SideGauge { alignment: Qt.AlignLeft | (leftLower.active ? Qt.AlignTop : Qt.AlignVCenter) - arcX: leftLower.active ? undefined : 10 - direction: PathArc.Clockwise - startAngle: leftLower.active ? 270 : (270 - Theme.geometry.briefPage.largeEdgeGauge.maxAngle / 2) animationEnabled: root.animationEnabled // Gauge color changes only apply when there is a maximum value. @@ -131,7 +128,7 @@ Page { : Global.acInputs.current : Units.sumRealNumbers(Global.acInputs.power, Global.dcInputs.power) - value: !visible ? 0 : inputsRange.valueAsRatio * 100 + value: visible ? inputsRange.valueAsRatio * 100 : 0 ValueRange { id: inputsRange diff --git a/shaders/circulargauge.frag b/shaders/circulargauge.frag new file mode 100644 index 000000000..0b0c4f476 --- /dev/null +++ b/shaders/circulargauge.frag @@ -0,0 +1,106 @@ +#version 440 +#define CONSTANT_PI 3.141592653589793 +#define CONSTANT_TAU 6.283185307179586 +layout(location = 0) in vec2 coord; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + + vec4 remainderColor; + vec4 progressColor; + vec4 shineColor; + float startAngle; + float endAngle; + float progressAngle; + float shineAngle; + float innerRadius; + float radius; + float outerRadius; + float smoothing; + float clockwise; +} ubuf; + +// atan2 which isn't undefined at x == 0 +float my_atan2(float y, float x) { + return x == 0.0 ? sign(y) * CONSTANT_PI : atan(y, x); +} + +// angle expressed in normal radians, but with orientation. +float denormalizedAngle(float a, float clockwise) { + float angle = clockwise*(1.0 - a) + (1.0 - clockwise)*a; + return (angle * CONSTANT_TAU) - CONSTANT_PI; +} + +// angle must be in radians [-PI, PI]. +// return value is normalized to [0.0, 1.0], +// inverted if necessary to adjust for clockwise vs anticlockwise. +float normalizedAngle(float angle, float clockwise) { + float a = (angle + CONSTANT_PI) / CONSTANT_TAU; + a = clockwise*(1.0 - a) + (1.0 - clockwise)*a; + return a; +} + +// converts from cartesian to polar coordinates (angle, radius). +// angle is normalized to [0.0, 1.0]. +vec2 toPolar(vec2 point, float clockwise) { + // note that we have flipped y/x to x/y to get angle from y axis (i.e. vertical). + return vec2(normalizedAngle(atan(point.x, point.y), clockwise), length(point)); +} + +// converts from polar to cartesian coordinates (x, y). +vec2 toCartesian(vec2 polar, float clockwise) { + float angle = denormalizedAngle(polar.x, clockwise); + // note that we have flipped cos/sin to sin/cos since angle is from y axis (i.e. vertical). + return vec2(polar.y * sin(angle), polar.y * cos(angle)); +} + +// if you move the specified distance along the arc, how much angle has been traversed? +// the error grows larger and larger the bigger distanceMoved is (since we assume straight-line movement)... +float angleDelta(float startAngle, float endAngle, float radius, float distanceMoved) { + float arcLength = (endAngle - startAngle) * CONSTANT_TAU * radius; + return distanceMoved / arcLength; +} + +void main() { + vec2 uv = coord * 2.0 - 1.0; + float uvDistance = length(uv); // distance from the center + + // note: we want angle from y axis rather than x axis, so flip args of atan2. + float uvAngle = normalizedAngle(my_atan2(uv.x, uv.y), ubuf.clockwise); + float withinGaugeAngle = (uvAngle < ubuf.startAngle || uvAngle > ubuf.endAngle) ? 0.0 : 1.0; + + // calculate the rounded caps. + float capRadius = ubuf.outerRadius - ubuf.radius; + float capAngleDelta = angleDelta(ubuf.startAngle, ubuf.endAngle, ubuf.radius, capRadius); + + // the startAngle cap. + vec2 startCapCenter = toCartesian(vec2(ubuf.startAngle, ubuf.radius), ubuf.clockwise); + float startCapAlpha = 1.0 - smoothstep(capRadius, capRadius + ubuf.smoothing, distance(uv, startCapCenter)); + + // the endAngle cap. + vec2 endCapCenter = toCartesian(vec2(ubuf.endAngle, ubuf.radius), ubuf.clockwise); + float endCapAlpha = 1.0 - smoothstep(capRadius, capRadius + ubuf.smoothing, distance(uv, endCapCenter)); + + // the progress cap. + vec2 progressCapCenter = toCartesian(vec2(ubuf.progressAngle, ubuf.radius), ubuf.clockwise); + float progressCapMix = 1.0 - smoothstep(capRadius, capRadius + ubuf.smoothing, distance(uv, progressCapCenter)); + + // calculate shine animation. + float adjustedAngle = (uvAngle > 0.92 && ubuf.shineAngle < 0.08 && startCapAlpha > 0.0) ? (uvAngle - 1.0) : uvAngle; + float shineMix = smoothstep(ubuf.shineAngle - 0.08, ubuf.shineAngle, adjustedAngle) + * (1.0 - smoothstep(ubuf.shineAngle, ubuf.shineAngle + 0.02, adjustedAngle)); + + // antialiasing. + float gaugeAngleAlpha = max(endCapAlpha, max(startCapAlpha, withinGaugeAngle)); + float gaugeStrokeAlpha = smoothstep(ubuf.innerRadius - ubuf.smoothing, ubuf.innerRadius, uvDistance) + * (1.0 - smoothstep(ubuf.outerRadius, ubuf.outerRadius + ubuf.smoothing, uvDistance)); + + float isProgressBar = ((startCapAlpha > 0.0 && ubuf.progressAngle > 0.0) + || (uvAngle <= ubuf.progressAngle)) ? 1.0 : 0.0; + float progressColorMix = max(isProgressBar, progressCapMix); + + // Qt expects pre-multiplied output, so don't just set w-channel. + fragColor = mix(ubuf.remainderColor, mix(ubuf.progressColor, ubuf.shineColor, shineMix), progressColorMix) * gaugeAngleAlpha * gaugeStrokeAlpha * ubuf.qt_Opacity; +} diff --git a/shaders/progressarc.frag b/shaders/progressarc.frag new file mode 100644 index 000000000..b432983d0 --- /dev/null +++ b/shaders/progressarc.frag @@ -0,0 +1,106 @@ +#version 440 +#define CONSTANT_PI 3.141592653589793 +#define CONSTANT_TAU 6.283185307179586 +layout(location = 0) in vec2 coord; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + + vec4 remainderColor; + vec4 progressColor; + float startAngle; + float endAngle; + float progressAngle; + float innerRadius; + float radius; + float outerRadius; + float smoothing; + float clockwise; + float xscale; + float xtranslate; + float yscale; + float ytranslate; +} ubuf; + +// atan2 which isn't undefined at x == 0 +float my_atan2(float y, float x) { + return x == 0.0 ? sign(y) * CONSTANT_PI : atan(y, x); +} + +// angle expressed in normal radians, but with orientation. +float denormalizedAngle(float a, float clockwise) { + float angle = clockwise*(1.0 - a) + (1.0 - clockwise)*a; + return (angle * CONSTANT_TAU) - CONSTANT_PI; +} + +// angle must be in radians [-PI, PI]. +// return value is normalized to [0.0, 1.0], +// inverted if necessary to adjust for clockwise vs anticlockwise. +float normalizedAngle(float angle, float clockwise) { + float a = (angle + CONSTANT_PI) / CONSTANT_TAU; + a = clockwise*(1.0 - a) + (1.0 - clockwise)*a; + return a; +} + +// converts from cartesian to polar coordinates (angle, radius). +// angle is normalized to [0.0, 1.0]. +vec2 toPolar(vec2 point, float clockwise) { + // note that we have flipped y/x to x/y to get angle from y axis (i.e. vertical). + return vec2(normalizedAngle(atan(point.x, point.y), clockwise), length(point)); +} + +// converts from polar to cartesian coordinates (x, y). +vec2 toCartesian(vec2 polar, float clockwise) { + float angle = denormalizedAngle(polar.x, clockwise); + // note that we have flipped cos/sin to sin/cos since angle is from y axis (i.e. vertical). + return vec2(polar.y * sin(angle), polar.y * cos(angle)); +} + +// if you move the specified distance along the arc, how much angle has been traversed? +// the error grows larger and larger the bigger distanceMoved is (since we assume straight-line movement)... +float angleDelta(float startAngle, float endAngle, float radius, float distanceMoved) { + float arcLength = (endAngle - startAngle) * CONSTANT_TAU * radius; + return distanceMoved / arcLength; +} + +void main() { + // first transform uv so that we "clip" to the section of the arc we want. + // that is: notionally (0,0) is the origin of the unit circle, + // but we might be displaying one small segment of the arc, + // so (0,0) might not be represented at all in any of our fragments. + vec2 uv = vec2(coord.x*ubuf.xscale + ubuf.xtranslate, coord.y*ubuf.yscale + ubuf.ytranslate); + float uvDistance = length(uv); // distance from the center + + // note: we want angle from y axis rather than x axis, so flip args of atan2. + float uvAngle = normalizedAngle(my_atan2(uv.x, uv.y), ubuf.clockwise); + float withinGaugeAngle = (uvAngle < ubuf.startAngle || uvAngle > ubuf.endAngle) ? 0.0 : 1.0; + + // calculate the rounded caps. + float capRadius = ubuf.outerRadius - ubuf.radius; + float capAngleDelta = angleDelta(ubuf.startAngle, ubuf.endAngle, ubuf.radius, capRadius); + + // the startAngle cap. + vec2 startCapCenter = toCartesian(vec2(ubuf.startAngle, ubuf.radius), ubuf.clockwise); + float startCapAlpha = 1.0 - smoothstep(capRadius, capRadius + ubuf.smoothing, distance(uv, startCapCenter)); + + // the endAngle cap. + vec2 endCapCenter = toCartesian(vec2(ubuf.endAngle, ubuf.radius), ubuf.clockwise); + float endCapAlpha = 1.0 - smoothstep(capRadius, capRadius + ubuf.smoothing, distance(uv, endCapCenter)); + + // the progress cap. + vec2 progressCapCenter = toCartesian(vec2(ubuf.progressAngle, ubuf.radius), ubuf.clockwise); + float progressCapMix = 1.0 - smoothstep(capRadius, capRadius + ubuf.smoothing, distance(uv, progressCapCenter)); + + // antialiasing. + float gaugeAngleAlpha = max(endCapAlpha, max(startCapAlpha, withinGaugeAngle)); + float gaugeStrokeAlpha = smoothstep(ubuf.innerRadius - ubuf.smoothing, ubuf.innerRadius, uvDistance) + * (1.0 - smoothstep(ubuf.outerRadius, ubuf.outerRadius + ubuf.smoothing, uvDistance)); + + float isProgressBar = ((uvAngle > (ubuf.endAngle + capAngleDelta)) || uvAngle <= ubuf.progressAngle) ? 1.0 : 0.0; + float progressColorMix = max(isProgressBar, progressCapMix); + + // Qt expects pre-multiplied output, so don't just set w-channel. + fragColor = mix(ubuf.remainderColor, ubuf.progressColor, progressColorMix) * gaugeAngleAlpha * gaugeStrokeAlpha * ubuf.qt_Opacity; +} diff --git a/src/main.cpp b/src/main.cpp index 0f34bdb0f..e690cc35e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -343,6 +343,7 @@ int main(int argc, char *argv[]) engine.setProperty("screenSize", (round(screenDiagonalMm / 10 / 2.5) == 7) ? Victron::VenusOS::Theme::SevenInch : Victron::VenusOS::Theme::FiveInch); +engine.setProperty("screenSize", Victron::VenusOS::Theme::SevenInch); #else engine.setProperty("screenSize", Victron::VenusOS::Theme::SevenInch); #endif diff --git a/themes/animation/Animation.json b/themes/animation/Animation.json index 9b2b04b33..c34acc52c 100644 --- a/themes/animation/Animation.json +++ b/themes/animation/Animation.json @@ -4,7 +4,7 @@ "animation.progressArc.duration": 600, "animation.progressBar.duration": 1000, "animation.briefPage.centerGauge.shine.pauseRatio": 2.0, - "animation.briefPage.centerGauge.shine.duration": 1500, + "animation.briefPage.centerGauge.shine.duration": 1200, "animation.briefPage.edgeGauge.fade.duration": 200, "animation.briefPage.sidePanel.slide.duration": 350, "animation.briefPage.sidePanel.sliderValueChange.duration": 200,