feat: add wavy circular indicator
This commit is contained in:
parent
eed9e4f8a0
commit
13713aa5d7
4 changed files with 86 additions and 92 deletions
|
|
@ -20,7 +20,7 @@ BusyIndicator {
|
||||||
}
|
}
|
||||||
|
|
||||||
property real implicitSize: Tokens.font.body.medium.pointSize * 3
|
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 fgColour: Colours.palette.m3primary
|
||||||
property color bgColour: Colours.palette.m3secondaryContainer
|
property color bgColour: Colours.palette.m3secondaryContainer
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,101 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
|
import Caelestia.Components
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
||||||
Shape {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property real value
|
property real value
|
||||||
property int startAngle: -90
|
property int startAngle: -90
|
||||||
|
property int sweepAngle: 360
|
||||||
property int strokeWidth: Tokens.padding.small
|
property int strokeWidth: Tokens.padding.small
|
||||||
property int padding: 0
|
property int padding: 0
|
||||||
property int spacing: Tokens.spacing.small
|
property int spacing: Tokens.spacing.small
|
||||||
property color fgColour: Colours.palette.m3primary
|
property color fgColour: Colours.palette.m3primary
|
||||||
property color bgColour: Colours.palette.m3secondaryContainer
|
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 size: Math.min(width, height)
|
||||||
readonly property real arcRadius: (size - padding - strokeWidth) / 2
|
readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2
|
||||||
readonly property real vValue: value || 1 / 360
|
readonly property real clampedVal: Math.max(1 / 360, Math.min(1, value))
|
||||||
readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)
|
readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)
|
||||||
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2 // For consumers
|
||||||
asynchronous: true
|
property real implicitSize
|
||||||
|
|
||||||
ShapePath {
|
implicitWidth: implicitSize
|
||||||
fillColor: "transparent"
|
implicitHeight: implicitSize
|
||||||
strokeColor: root.bgColour
|
|
||||||
strokeWidth: root.strokeWidth
|
|
||||||
capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
|
|
||||||
|
|
||||||
PathAngleArc {
|
Shape {
|
||||||
startAngle: root.startAngle + 360 * root.vValue + root.gapAngle
|
preferredRendererType: Shape.CurveRenderer
|
||||||
sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2)
|
asynchronous: true
|
||||||
radiusX: root.arcRadius
|
opacity: Math.min(1, remainingArc.sweepAngle)
|
||||||
radiusY: root.arcRadius
|
|
||||||
centerX: root.size / 2
|
|
||||||
centerY: root.size / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on strokeColor {
|
ShapePath {
|
||||||
CAnim {
|
fillColor: "transparent"
|
||||||
duration: Tokens.anim.durations.large
|
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 {
|
WavyLine {
|
||||||
fillColor: "transparent"
|
id: wave
|
||||||
strokeColor: root.fgColour
|
|
||||||
strokeWidth: root.strokeWidth
|
|
||||||
capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
|
|
||||||
|
|
||||||
PathAngleArc {
|
anchors.fill: parent
|
||||||
startAngle: root.startAngle
|
anchors.margins: -lineWidth * amplitudeMultiplier
|
||||||
sweepAngle: 360 * root.vValue
|
|
||||||
radiusX: root.arcRadius
|
lineWidth: root.strokeWidth
|
||||||
radiusY: root.arcRadius
|
color: root.fgColour
|
||||||
centerX: root.size / 2
|
pathType: WavyLine.Arc
|
||||||
centerY: root.size / 2
|
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 {
|
Behavior on color {
|
||||||
CAnim {
|
CAnim {}
|
||||||
duration: Tokens.anim.durations.large
|
}
|
||||||
|
|
||||||
|
Behavior on amplitudeMultiplier {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@ Item {
|
||||||
return active?.length ? (active.position % active.length) / active.length : 0;
|
return active?.length ? (active.position % active.length) / active.length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property real arcCoverGap: Tokens.spacing.small
|
readonly property real arcCoverGap: Tokens.spacing.extraSmall
|
||||||
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
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
|
|
@ -45,59 +43,22 @@ Item {
|
||||||
service: Audio.beatTracker
|
service: Audio.beatTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
Shape {
|
CircularProgress {
|
||||||
preferredRendererType: Shape.CurveRenderer
|
id: prog
|
||||||
opacity: Math.min(1, remainingArc.sweepAngle)
|
|
||||||
|
|
||||||
ShapePath {
|
anchors.centerIn: cover
|
||||||
fillColor: "transparent"
|
implicitSize: cover.width + root.arcCoverGap + thickness * 2
|
||||||
strokeColor: Colours.palette.m3secondaryContainer
|
|
||||||
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.Tokens.sizes.dashboard.mediaProgressThickness
|
|
||||||
capStyle: ShapePath.RoundCap
|
|
||||||
|
|
||||||
PathAngleArc {
|
fgColour: Colours.palette.m3primary
|
||||||
id: remainingArc
|
strokeWidth: Tokens.sizes.dashboard.mediaProgressThickness
|
||||||
|
startAngle: -90 - sweepAngle / 2
|
||||||
centerX: cover.x + cover.width / 2
|
sweepAngle: Tokens.sizes.dashboard.mediaProgressSweep
|
||||||
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
|
|
||||||
value: root.playerProgress
|
value: root.playerProgress
|
||||||
|
|
||||||
Anim on waveProgress {
|
wavy: true
|
||||||
running: true
|
waveFrequency: 8
|
||||||
paused: !Players.active?.isPlaying
|
waveDuration: 2000
|
||||||
from: 0
|
wavePaused: !Players.active?.isPlaying
|
||||||
to: 1
|
|
||||||
duration: 2000
|
|
||||||
easing.type: Easing.Linear
|
|
||||||
loops: Animation.Infinite
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
CAnim {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|
@ -106,7 +67,7 @@ Item {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
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
|
implicitHeight: width
|
||||||
|
|
||||||
// Slight glow to separate from bg
|
// Slight glow to separate from bg
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ WavyLine::WavyLine(QQuickItem* parent)
|
||||||
, m_fullAngle(360)
|
, m_fullAngle(360)
|
||||||
, m_radius(-1)
|
, m_radius(-1)
|
||||||
, m_value(1)
|
, m_value(1)
|
||||||
, m_startAngleRad(-M_PI / 2.0)
|
, m_startAngleRad(0)
|
||||||
, m_fullAngleRad(2 * M_PI) {
|
, m_fullAngleRad(2 * M_PI) {
|
||||||
setAntialiasing(true);
|
setAntialiasing(true);
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +127,7 @@ qreal WavyLine::startAngle() const {
|
||||||
void WavyLine::setStartAngle(qreal startAngle) {
|
void WavyLine::setStartAngle(qreal startAngle) {
|
||||||
if (!qFuzzyCompare(m_startAngle + 1.0, startAngle + 1.0)) {
|
if (!qFuzzyCompare(m_startAngle + 1.0, startAngle + 1.0)) {
|
||||||
m_startAngle = startAngle;
|
m_startAngle = startAngle;
|
||||||
m_startAngleRad = startAngle * M_PI / 180.0 - M_PI / 2.0;
|
m_startAngleRad = startAngle * M_PI / 180.0;
|
||||||
emit startAngleChanged();
|
emit startAngleChanged();
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue