From 13713aa5d74eec0dc2b2f624e67e60d9550e17fa Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:53:15 +1000 Subject: [PATCH] feat: add wavy circular indicator --- components/controls/CircularIndicator.qml | 2 +- components/controls/CircularProgress.qml | 105 ++++++++++++------- modules/dashboard/dash/Media.qml | 67 +++--------- plugin/src/Caelestia/Components/wavyline.cpp | 4 +- 4 files changed, 86 insertions(+), 92 deletions(-) diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml index 987664c5..0c11594a 100644 --- a/components/controls/CircularIndicator.qml +++ b/components/controls/CircularIndicator.qml @@ -20,7 +20,7 @@ BusyIndicator { } property real implicitSize: Tokens.font.body.medium.pointSize * 3 - property real strokeWidth: Tokens.padding.small * 0.8 + property real strokeWidth: Tokens.padding.extraSmall property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml index 65625de3..efeb1b4b 100644 --- a/components/controls/CircularProgress.qml +++ b/components/controls/CircularProgress.qml @@ -1,68 +1,101 @@ import QtQuick import QtQuick.Shapes +import Caelestia.Components import Caelestia.Config import qs.components import qs.services -Shape { +Item { id: root property real value property int startAngle: -90 + property int sweepAngle: 360 property int strokeWidth: Tokens.padding.small property int padding: 0 property int spacing: Tokens.spacing.small property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer + property bool wavy: false + property alias waveFrequency: wave.frequency + property alias waveAmplitude: wave.amplitudeMultiplier + property alias wavePaused: waveProgAnim.paused + property alias waveDuration: waveProgAnim.duration + readonly property real size: Math.min(width, height) - readonly property real arcRadius: (size - padding - strokeWidth) / 2 - readonly property real vValue: value || 1 / 360 + readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2 + readonly property real clampedVal: Math.max(1 / 360, Math.min(1, value)) readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI) - preferredRendererType: Shape.CurveRenderer - asynchronous: true + readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2 // For consumers + property real implicitSize - ShapePath { - fillColor: "transparent" - strokeColor: root.bgColour - strokeWidth: root.strokeWidth - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + implicitWidth: implicitSize + implicitHeight: implicitSize - PathAngleArc { - startAngle: root.startAngle + 360 * root.vValue + root.gapAngle - sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2) - radiusX: root.arcRadius - radiusY: root.arcRadius - centerX: root.size / 2 - centerY: root.size / 2 - } + Shape { + preferredRendererType: Shape.CurveRenderer + asynchronous: true + opacity: Math.min(1, remainingArc.sweepAngle) - Behavior on strokeColor { - CAnim { - duration: Tokens.anim.durations.large + ShapePath { + fillColor: "transparent" + strokeColor: root.bgColour + strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth + capStyle: ShapePath.RoundCap + + PathAngleArc { + id: remainingArc + + radiusX: root.arcRadius + radiusY: root.arcRadius + centerX: root.size / 2 + centerY: root.size / 2 + startAngle: root.startAngle + root.clampedVal * root.sweepAngle + root.gapAngle + sweepAngle: Math.max(1 / 360, root.sweepAngle * (1 - root.clampedVal) - root.gapAngle * 2) + } + + Behavior on strokeColor { + CAnim {} } } } - ShapePath { - fillColor: "transparent" - strokeColor: root.fgColour - strokeWidth: root.strokeWidth - capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + WavyLine { + id: wave - PathAngleArc { - startAngle: root.startAngle - sweepAngle: 360 * root.vValue - radiusX: root.arcRadius - radiusY: root.arcRadius - centerX: root.size / 2 - centerY: root.size / 2 + anchors.fill: parent + anchors.margins: -lineWidth * amplitudeMultiplier + + lineWidth: root.strokeWidth + color: root.fgColour + pathType: WavyLine.Arc + radius: root.arcRadius + startAngle: root.startAngle + fullAngle: root.sweepAngle + value: root.clampedVal + frequency: 8 + amplitudeMultiplier: root.wavy ? 0.5 : 0 + + Anim on waveProgress { + id: waveProgAnim + + running: true + from: 0 + to: 1 + duration: 2000 + easing.type: Easing.Linear + loops: Animation.Infinite } - Behavior on strokeColor { - CAnim { - duration: Tokens.anim.durations.large + Behavior on color { + CAnim {} + } + + Behavior on amplitudeMultiplier { + Anim { + type: Anim.DefaultEffects } } } diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 2bdbac70..88216280 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -19,9 +19,7 @@ Item { return active?.length ? (active.position % active.length) / active.length : 0; } - readonly property real arcCoverGap: Tokens.spacing.small - readonly property real arcRadius: (cover.width + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + arcCoverGap - readonly property real arcGap: (Tokens.spacing.extraSmall + Tokens.sizes.dashboard.mediaProgressThickness) / arcRadius * 180 / Math.PI + readonly property real arcCoverGap: Tokens.spacing.extraSmall anchors.top: parent.top anchors.bottom: parent.bottom @@ -45,59 +43,22 @@ Item { service: Audio.beatTracker } - Shape { - preferredRendererType: Shape.CurveRenderer - opacity: Math.min(1, remainingArc.sweepAngle) + CircularProgress { + id: prog - ShapePath { - fillColor: "transparent" - strokeColor: Colours.palette.m3secondaryContainer - strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.Tokens.sizes.dashboard.mediaProgressThickness - capStyle: ShapePath.RoundCap + anchors.centerIn: cover + implicitSize: cover.width + root.arcCoverGap + thickness * 2 - PathAngleArc { - id: remainingArc - - centerX: cover.x + cover.width / 2 - centerY: cover.y + cover.height / 2 - radiusX: root.arcRadius - radiusY: root.arcRadius - startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 + root.playerProgress * root.Tokens.sizes.dashboard.mediaProgressSweep + root.arcGap - sweepAngle: Math.max(0.1, root.Tokens.sizes.dashboard.mediaProgressSweep * (1 - root.playerProgress) - root.arcGap) - } - - Behavior on strokeColor { - CAnim {} - } - } - } - - WavyLine { - anchors.fill: cover - anchors.margins: -lineWidth * amplitudeMultiplier * 2 - lineWidth - root.arcCoverGap - - lineWidth: Tokens.sizes.dashboard.mediaProgressThickness - color: Colours.palette.m3primary - pathType: WavyLine.Arc - radius: root.arcRadius - frequency: 8 - startAngle: -fullAngle / 2 - fullAngle: Tokens.sizes.dashboard.mediaProgressSweep + fgColour: Colours.palette.m3primary + strokeWidth: Tokens.sizes.dashboard.mediaProgressThickness + startAngle: -90 - sweepAngle / 2 + sweepAngle: Tokens.sizes.dashboard.mediaProgressSweep value: root.playerProgress - Anim on waveProgress { - running: true - paused: !Players.active?.isPlaying - from: 0 - to: 1 - duration: 2000 - easing.type: Easing.Linear - loops: Animation.Infinite - } - - Behavior on color { - CAnim {} - } + wavy: true + waveFrequency: 8 + waveDuration: 2000 + wavePaused: !Players.active?.isPlaying } Item { @@ -106,7 +67,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Tokens.padding.large + Tokens.sizes.dashboard.mediaProgressThickness + root.arcCoverGap + anchors.margins: Tokens.padding.medium + root.arcCoverGap + prog.thickness implicitHeight: width // Slight glow to separate from bg diff --git a/plugin/src/Caelestia/Components/wavyline.cpp b/plugin/src/Caelestia/Components/wavyline.cpp index 7114e573..e61b896f 100644 --- a/plugin/src/Caelestia/Components/wavyline.cpp +++ b/plugin/src/Caelestia/Components/wavyline.cpp @@ -19,7 +19,7 @@ WavyLine::WavyLine(QQuickItem* parent) , m_fullAngle(360) , m_radius(-1) , m_value(1) - , m_startAngleRad(-M_PI / 2.0) + , m_startAngleRad(0) , m_fullAngleRad(2 * M_PI) { setAntialiasing(true); } @@ -127,7 +127,7 @@ qreal WavyLine::startAngle() const { void WavyLine::setStartAngle(qreal startAngle) { if (!qFuzzyCompare(m_startAngle + 1.0, startAngle + 1.0)) { m_startAngle = startAngle; - m_startAngleRad = startAngle * M_PI / 180.0 - M_PI / 2.0; + m_startAngleRad = startAngle * M_PI / 180.0; emit startAngleChanged(); update(); }