diff --git a/Modules/Audio/LinearSpectrum.qml b/Modules/Audio/LinearSpectrum.qml index ea8ef71..2e39e3e 100644 --- a/Modules/Audio/LinearSpectrum.qml +++ b/Modules/Audio/LinearSpectrum.qml @@ -1,6 +1,5 @@ import QtQuick import qs.Commons -import qs.Services Item { id: root @@ -9,38 +8,30 @@ Item { property int strokeWidth: 0 property var values: [] - readonly property real xScale: width / (values.length * 2) + // Pre compute horizontal mirroring + readonly property int valuesCount: values.length + readonly property int totalBars: valuesCount * 2 + readonly property real barSlotWidth: totalBars > 0 ? width / totalBars : 0 Repeater { - model: values.length - Rectangle { - property real amp: values[values.length - 1 - index] + model: root.totalBars - color: fillColor - border.color: strokeColor - border.width: strokeWidth + Rectangle { + // The first half of bars are a mirror image (reversed values array). + // The second half of bars are in normal order. + property int valueIndex: index < root.valuesCount ? root.valuesCount - 1 - index // Mirrored half + : index - root.valuesCount // Normal half + + property real amp: root.values[valueIndex] + + color: root.fillColor + border.color: root.strokeColor + border.width: root.strokeWidth antialiasing: true - width: xScale * 0.5 + width: root.barSlotWidth * 0.5 // Creates a small gap between bars height: Math.max(1, root.height * amp) - x: index * xScale - y: root.height - height - } - } - - Repeater { - model: values.length - Rectangle { - property real amp: values[index] - - color: fillColor - border.color: strokeColor - border.width: strokeWidth - antialiasing: true - - width: xScale * 0.5 - height: Math.max(1, root.height * amp) - x: (values.length + index) * xScale + x: index * root.barSlotWidth y: root.height - height } } diff --git a/Modules/Audio/WaveSpectrum.qml b/Modules/Audio/WaveSpectrum.qml new file mode 100644 index 0000000..596e735 --- /dev/null +++ b/Modules/Audio/WaveSpectrum.qml @@ -0,0 +1,77 @@ +import QtQuick +import qs.Commons + +Item { + id: root + property color fillColor: Color.mPrimary + property color strokeColor: Color.mOnSurface + property int strokeWidth: 0 + property var values: [] + + // Redraw when necessary + onWidthChanged: canvas.requestPaint() + onHeightChanged: canvas.requestPaint() + onValuesChanged: canvas.requestPaint() + onFillColorChanged: canvas.requestPaint() + onStrokeColorChanged: canvas.requestPaint() + + Canvas { + id: canvas + anchors.fill: parent + antialiasing: true + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + if (values.length === 0) { + return + } + + // Create the mirrored values + const partToMirror = values.slice(1).reverse() + const mirroredValues = partToMirror.concat(values) + + if (mirroredValues.length < 2) { + return + } + + ctx.fillStyle = root.fillColor + ctx.strokeStyle = root.strokeColor + ctx.lineWidth = root.strokeWidth + + const count = mirroredValues.length + const stepX = width / (count - 1) + const centerY = height / 2 + const amplitude = height / 2 + + ctx.beginPath() + + // Draw the top half of the waveform from left to right + ctx.moveTo(0, centerY - (mirroredValues[0] * amplitude)) // Move to the first point + for (var i = 1; i < count; i++) { + const x = i * stepX + const y = centerY - (mirroredValues[i] * amplitude) + ctx.lineTo(x, y) + } + + // Draw the bottom half of the waveform from right to left to create a closed shape + for (var i = count - 1; i >= 0; i--) { + const x = i * stepX + const y = centerY + (mirroredValues[i] * amplitude) + // Mirrored across the center + ctx.lineTo(x, y) + } + + ctx.closePath() + + // --- Render the path --- + if (root.fillColor.a > 0) { + ctx.fill() + } + if (root.strokeWidth > 0) { + ctx.stroke() + } + } + } +} diff --git a/Modules/Bar/SystemMonitor.qml b/Modules/Bar/SystemMonitor.qml index 37dcf3c..253c6f8 100644 --- a/Modules/Bar/SystemMonitor.qml +++ b/Modules/Bar/SystemMonitor.qml @@ -54,7 +54,7 @@ Row { Row { id: cpuTempLayout // spacing is thin here to compensate for the vertical thermometer icon - spacing: Style.marginTiniest * scaling + spacing: Style.marginTiniest * scaling NIcon { text: "thermometer" @@ -93,8 +93,7 @@ Row { } } } -} -// Row { +}// Row { // id: layout // anchors.verticalCenter: parent.verticalCenter // spacing: Style.marginSmall * scaling diff --git a/Modules/SettingsPanel/Tabs/AudioTab.qml b/Modules/SettingsPanel/Tabs/AudioTab.qml index b79a943..fb1181c 100644 --- a/Modules/SettingsPanel/Tabs/AudioTab.qml +++ b/Modules/SettingsPanel/Tabs/AudioTab.qml @@ -265,6 +265,10 @@ ColumnLayout { key: "linear" name: "Linear" } + ListElement { + key: "wave" + name: "Wave" + } } currentKey: Settings.data.audio.visualizerType onSelected: function (key) { diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 5cef07b..a0da7a6 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -340,7 +340,20 @@ NBox { width: 300 * scaling height: 80 * scaling values: CavaService.values - fillColor: Color.mOnSurface + fillColor: Color.mPrimary + Layout.alignment: Qt.AlignHCenter + } + } + + Loader { + active: Settings.data.audio.visualizerType == "wave" + Layout.alignment: Qt.AlignHCenter + + sourceComponent: WaveSpectrum { + width: 300 * scaling + height: 80 * scaling + values: CavaService.values + fillColor: Color.mPrimary Layout.alignment: Qt.AlignHCenter } } diff --git a/Services/CavaService.qml b/Services/CavaService.qml index d80c0b6..b58e09a 100644 --- a/Services/CavaService.qml +++ b/Services/CavaService.qml @@ -9,7 +9,7 @@ Singleton { id: root property var values: Array(barsCount).fill(0) - property int barsCount: 20 + property int barsCount: 32 property var config: ({ "general": {