1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

optional wavy media playback bars

This commit is contained in:
bbedward
2025-09-10 15:52:09 -04:00
parent 4ae157af35
commit d0cbe689f8
5 changed files with 594 additions and 167 deletions

View File

@@ -50,6 +50,7 @@ Singleton {
property int maxWorkspaceIcons: 3
property bool workspacesPerMonitor: true
property var workspaceNameIcons: ({})
property bool waveProgressEnabled: true
property bool clockCompactMode: false
property bool focusedWindowCompactMode: false
property bool runningAppsCompactMode: true
@@ -205,6 +206,7 @@ Singleton {
maxWorkspaceIcons = settings.maxWorkspaceIcons !== undefined ? settings.maxWorkspaceIcons : 3
workspaceNameIcons = settings.workspaceNameIcons !== undefined ? settings.workspaceNameIcons : ({})
workspacesPerMonitor = settings.workspacesPerMonitor !== undefined ? settings.workspacesPerMonitor : true
waveProgressEnabled = settings.waveProgressEnabled !== undefined ? settings.waveProgressEnabled : true
clockCompactMode = settings.clockCompactMode !== undefined ? settings.clockCompactMode : false
focusedWindowCompactMode = settings.focusedWindowCompactMode !== undefined ? settings.focusedWindowCompactMode : false
runningAppsCompactMode = settings.runningAppsCompactMode !== undefined ? settings.runningAppsCompactMode : true
@@ -320,6 +322,7 @@ Singleton {
"maxWorkspaceIcons": maxWorkspaceIcons,
"workspacesPerMonitor": workspacesPerMonitor,
"workspaceNameIcons": workspaceNameIcons,
"waveProgressEnabled": waveProgressEnabled,
"clockCompactMode": clockCompactMode,
"focusedWindowCompactMode": focusedWindowCompactMode,
"runningAppsCompactMode": runningAppsCompactMode,
@@ -391,6 +394,11 @@ Singleton {
saveSettings()
}
function setWaveProgressEnabled(enabled) {
waveProgressEnabled = enabled
saveSettings()
}
function setWorkspaceNameIcon(workspaceName, iconData) {
var iconMap = JSON.parse(JSON.stringify(workspaceNameIcons))
iconMap[workspaceName] = iconData

View File

@@ -42,11 +42,13 @@ Item {
Timer {
id: positionTimer
interval: 300
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressSliderArea.isSeeking
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !isSeeking
repeat: true
onTriggered: activePlayer && activePlayer.positionSupported && activePlayer.positionChanged()
}
property bool isSeeking: false
Timer {
id: cleanupTimer
interval: 2000
@@ -369,106 +371,173 @@ Item {
}
}
// Progress Bar
Item {
id: progressSlider
width: parent.width
Loader {
width: parent.width + 4
height: 20
x: -2
visible: activePlayer?.length > 0
sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent
property real value: ratio
property real lineWidth: 2.5
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
readonly property real midY: height / 2
Component {
id: waveProgressComponent
// Background track
Rectangle {
width: parent.width
height: progressSlider.lineWidth
anchors.verticalCenter: parent.verticalCenter
color: progressSlider.trackColor
radius: height / 2
}
M3WaveProgress {
value: ratio
isPlaying: activePlayer?.playbackState === MprisPlaybackState.Playing
// Filled portion
Rectangle {
width: Math.max(0, Math.min(parent.width, parent.width * progressSlider.value))
height: progressSlider.lineWidth
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: progressSlider.fillColor
radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } }
}
MouseArea {
id: progressSliderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
// Playhead
Rectangle {
id: playhead
width: 2.5
height: Math.max(progressSlider.lineWidth + 8, 12)
radius: width / 2
color: progressSlider.playheadColor
x: Math.max(0, Math.min(progressSlider.width, progressSlider.width * progressSlider.value)) - width / 2
y: progressSlider.midY - height / 2
z: 3
Behavior on x { NumberAnimation { duration: 80 } }
}
property real pendingSeekPosition: -1
MouseArea {
id: progressSliderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
Timer {
id: seekDebounceTimer
interval: 150
onTriggered: {
if (progressSliderArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(progressSliderArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
progressSliderArea.pendingSeekPosition = -1
}
}
}
property bool isSeeking: false
property real pendingSeekPosition: -1
Timer {
id: seekDebounceTimer
interval: 150
onTriggered: {
if (progressSliderArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(progressSliderArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
progressSliderArea.pendingSeekPosition = -1
onPressed: (mouse) => {
root.isSeeking = true
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
}
}
}
onPressed: (mouse) => {
isSeeking = true
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
Component {
id: flatProgressComponent
Item {
property real value: ratio
property real lineWidth: 2.5
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
readonly property real midY: height / 2
Rectangle {
width: parent.width
height: parent.lineWidth
anchors.verticalCenter: parent.verticalCenter
color: parent.trackColor
radius: height / 2
}
}
onReleased: {
isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
Rectangle {
width: Math.max(0, Math.min(parent.width, parent.width * parent.value))
height: parent.lineWidth
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: parent.fillColor
radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } }
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: (mouse) => {
if (pressed && isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
Rectangle {
id: playhead
width: 2.5
height: Math.max(parent.lineWidth + 8, 12)
radius: width / 2
color: parent.playheadColor
x: Math.max(0, Math.min(parent.width, parent.width * parent.value)) - width / 2
y: parent.midY - height / 2
z: 3
Behavior on x { NumberAnimation { duration: 80 } }
}
}
onClicked: (mouse) => {
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
activePlayer.position = r * activePlayer.length
MouseArea {
id: progressSliderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
property real pendingSeekPosition: -1
Timer {
id: seekDebounceTimer
interval: 150
onTriggered: {
if (progressSliderArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(progressSliderArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
progressSliderArea.pendingSeekPosition = -1
}
}
}
onPressed: (mouse) => {
root.isSeeking = true
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
}
}
}

View File

@@ -29,11 +29,13 @@ Card {
Timer {
interval: 300
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
repeat: true
onTriggered: activePlayer?.positionSupported && activePlayer.positionChanged()
}
property bool isSeeking: false
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
@@ -280,101 +282,172 @@ Card {
}
}
Item {
id: progressSlider
width: parent.width
Loader {
width: parent.width + 4
height: 20
x: -2
visible: activePlayer?.length > 0
sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent
property real value: ratio
property real lineWidth: 2.5
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
Component {
id: waveProgressComponent
Rectangle {
width: parent.width
height: progressSlider.lineWidth
anchors.verticalCenter: parent.verticalCenter
color: progressSlider.trackColor
radius: height / 2
}
M3WaveProgress {
value: ratio
isPlaying: activePlayer?.playbackState === MprisPlaybackState.Playing
Rectangle {
width: Math.max(0, Math.min(parent.width, parent.width * progressSlider.value))
height: progressSlider.lineWidth
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: progressSlider.fillColor
radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } }
}
MouseArea {
id: progressMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
Rectangle {
id: playhead
width: 2.5
height: Math.max(progressSlider.lineWidth + 8, 12)
radius: width / 2
color: progressSlider.playheadColor
x: Math.max(0, Math.min(progressSlider.width, progressSlider.width * progressSlider.value)) - width / 2
anchors.verticalCenter: parent.verticalCenter
z: 3
Behavior on x { NumberAnimation { duration: 80 } }
}
property real pendingSeekPosition: -1
MouseArea {
id: progressMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
Timer {
id: seekDebounceTimer
interval: 150
onTriggered: {
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
progressMouseArea.pendingSeekPosition = -1
}
}
}
property bool isSeeking: false
property real pendingSeekPosition: -1
Timer {
id: seekDebounceTimer
interval: 150
onTriggered: {
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
progressMouseArea.pendingSeekPosition = -1
onPressed: (mouse) => {
root.isSeeking = true
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
}
}
}
onPressed: (mouse) => {
isSeeking = true
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
Component {
id: flatProgressComponent
Item {
property real value: ratio
property real lineWidth: 2.5
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
Rectangle {
width: parent.width
height: parent.lineWidth
anchors.verticalCenter: parent.verticalCenter
color: parent.trackColor
radius: height / 2
}
}
onReleased: {
isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
Rectangle {
width: Math.max(0, Math.min(parent.width, parent.width * parent.value))
height: parent.lineWidth
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: parent.fillColor
radius: height / 2
Behavior on width { NumberAnimation { duration: 80 } }
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: (mouse) => {
if (pressed && isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
Rectangle {
id: playhead
width: 2.5
height: Math.max(parent.lineWidth + 8, 12)
radius: width / 2
color: parent.playheadColor
x: Math.max(0, Math.min(parent.width, parent.width * parent.value)) - width / 2
anchors.verticalCenter: parent.verticalCenter
z: 3
Behavior on x { NumberAnimation { duration: 80 } }
}
}
onClicked: (mouse) => {
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / progressSlider.width))
activePlayer.position = r * activePlayer.length
MouseArea {
id: progressMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer ? (activePlayer.canSeek && activePlayer.length > 0) : false
property real pendingSeekPosition: -1
Timer {
id: seekDebounceTimer
interval: 150
onTriggered: {
if (progressMouseArea.pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(progressMouseArea.pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
progressMouseArea.pendingSeekPosition = -1
}
}
}
onPressed: (mouse) => {
root.isSeeking = true
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onReleased: {
root.isSeeking = false
seekDebounceTimer.stop()
if (pendingSeekPosition >= 0 && activePlayer?.canSeek && activePlayer?.length > 0) {
const clamped = Math.min(pendingSeekPosition, activePlayer.length * 0.99)
activePlayer.position = clamped
pendingSeekPosition = -1
}
displayPosition = Qt.binding(() => currentPosition)
}
onPositionChanged: (mouse) => {
if (pressed && root.isSeeking && activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
pendingSeekPosition = r * activePlayer.length
displayPosition = pendingSeekPosition
seekDebounceTimer.restart()
}
}
onClicked: (mouse) => {
if (activePlayer?.length > 0 && activePlayer?.canSeek) {
const r = Math.max(0, Math.min(1, mouse.x / parent.width))
activePlayer.position = r * activePlayer.length
}
}
}
}
}

View File

@@ -297,6 +297,55 @@ Item {
}
}
StyledRect {
width: parent.width
height: mediaSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: mediaSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "music_note"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Media Player Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Wave Progress Bars"
description: "Use animated wave progress bars for media playback"
checked: SettingsData.waveProgressEnabled
onToggled: checked => {
return SettingsData.setWaveProgressEnabled(checked)
}
}
}
}
StyledRect {
width: parent.width
height: runningAppsSection.implicitHeight + Theme.spacingL * 2

228
Widgets/M3WaveProgress.qml Normal file
View File

@@ -0,0 +1,228 @@
import QtQuick
import QtQuick.Shapes
import qs.Common
Item {
id: root
property real value: 0
property real lineWidth: 2
property real wavelength: 20
property real amp: 1.6
property real phase: 0.0
property bool isPlaying: false
property real currentAmp: 1.6
property int samples: Math.max(48, Math.round(width / 3))
property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
function snap(v) { return Math.round(v * dpr) / dpr }
readonly property real playX: snap(root.width * root.value)
readonly property real midY: snap(height / 2)
readonly property real capPad: Math.ceil(lineWidth / 2)
function yWave(x) {
return midY + currentAmp * Math.sin((x / wavelength) * 2 * Math.PI + phase)
}
Behavior on currentAmp {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
onIsPlayingChanged: {
currentAmp = isPlaying ? amp : 0
}
Shape {
id: flatTrack
anchors.fill: parent
antialiasing: true
asynchronous: false
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.smooth: true
layer.samples: 8
layer.textureSize: Qt.size(Math.ceil(width * dpr * 2), Math.ceil(height * dpr * 2))
ShapePath {
id: flatPath
strokeColor: root.trackColor
strokeWidth: snap(root.lineWidth)
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathMove {
id: flatStart
x: 0
y: root.midY
}
PathLine {
id: flatEnd
x: root.width
y: root.midY
}
}
}
Item {
id: waveContainer
anchors.fill: parent
clip: true
width: Math.max(0, Math.min(parent.width, root.playX))
Shape {
id: waveShape
anchors.fill: parent
antialiasing: true
asynchronous: false
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.smooth: true
layer.samples: 8
layer.textureSize: Qt.size(Math.ceil(width * dpr * 2), Math.ceil(height * dpr * 2))
ShapePath {
id: wavePath
strokeColor: root.fillColor
strokeWidth: snap(root.lineWidth)
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
}
}
}
property var cubics: []
property real startY: root.midY + root.currentAmp * Math.sin(root.phase)
property real endY: root.midY + root.currentAmp * Math.sin((root.playX / root.wavelength) * 2 * Math.PI + root.phase)
Component {
id: moveComp
PathMove {}
}
Component {
id: cubicComp
PathCubic {}
}
function buildWave() {
wavePath.pathElements = []
cubics = []
wavePath.pathElements.push(moveComp.createObject(wavePath))
for (let i = 0; i < samples - 1; ++i) {
const cubic = cubicComp.createObject(wavePath)
wavePath.pathElements.push(cubic)
cubics.push(cubic)
}
updateWave()
}
function updateWave() {
if (cubics.length === 0) return
const step = root.width / (samples - 1)
const startX = snap(root.lineWidth / 2)
const r = root.lineWidth / 2
const aaBias = 0.25 / dpr
function y(x) { return yWave(x) }
function dy(x) {
return currentAmp * (2 * Math.PI / wavelength) * Math.cos((x / wavelength) * 2 * Math.PI + phase)
}
const m = wavePath.pathElements[0]
m.x = startX
m.y = y(startX)
for (let i = 0; i < cubics.length; ++i) {
const x0 = startX + i * step
const x1 = startX + (i + 1) * step
// Stop exactly at playX
if (x0 >= root.playX) {
// This segment is entirely past the playhead - collapse it
const seg = cubics[i]
seg.control1X = seg.control2X = seg.x = root.playX
const py = y(root.playX)
seg.control1Y = seg.control2Y = seg.y = py
continue
}
const xe = Math.min(x1, root.playX - r - aaBias)
const p0x = x0, p0y = y(x0)
const p1x = xe, p1y = y(xe)
const dx = xe - x0
if (dx <= 0) {
// Zero-length segment
const seg = cubics[i]
seg.control1X = seg.control2X = seg.x = root.playX
const py = y(root.playX)
seg.control1Y = seg.control2Y = seg.y = py
continue
}
const c1x = p0x + dx / 4
const c1y = p0y + (dx * dy(x0)) / 4
const c2x = p1x - dx / 4
const c2y = p1y - (dx * dy(xe)) / 4
const seg = cubics[i]
seg.control1X = c1x
seg.control1Y = c1y
seg.control2X = c2x
seg.control2Y = c2y
seg.x = p1x
seg.y = p1y
}
flatStart.x = 0
flatStart.y = midY
flatEnd.x = width
flatEnd.y = midY
}
Rectangle {
id: playhead
width: 3.5
height: Math.max(root.lineWidth + 12, 16)
radius: width / 2
color: root.playheadColor
x: root.playX - width / 2
y: root.midY - height / 2
z: 3
Behavior on x {
NumberAnimation {
duration: 80
}
}
}
FrameAnimation {
running: root.visible && (root.isPlaying || root.currentAmp > 0)
onTriggered: {
if (root.isPlaying) {
root.phase += 0.03 * frameTime * 60
}
root.updateWave()
}
}
Component.onCompleted: {
currentAmp = isPlaying ? amp : 0
buildWave()
}
onWidthChanged: buildWave()
onSamplesChanged: buildWave()
onCurrentAmpChanged: updateWave()
}