mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
qs/media: replace FBOs with ClippingRectangle and rewrite seekbar as a
shader
This commit is contained in:
@@ -152,7 +152,8 @@ Item {
|
||||
property real energy: 0
|
||||
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
|
||||
property vector2d bandsB: Qt.vector2d(0, 0)
|
||||
property vector4d fillColor: Qt.vector4d(Theme.primary.r, Theme.primary.g, Theme.primary.b, Theme.primary.a)
|
||||
readonly property color accentColor: MediaAccentService.accent
|
||||
property vector4d fillColor: Qt.vector4d(accentColor.r, accentColor.g, accentColor.b, accentColor.a)
|
||||
|
||||
Behavior on activation {
|
||||
NumberAnimation {
|
||||
@@ -173,7 +174,7 @@ Item {
|
||||
|
||||
imageSource: artUrl || lastValidArtUrl || ""
|
||||
fallbackIcon: "album"
|
||||
border.color: Theme.primary
|
||||
border.color: MediaAccentService.accent
|
||||
border.width: 2
|
||||
|
||||
onImageSourceChanged: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
@@ -65,67 +65,44 @@ Rectangle {
|
||||
border.color: "transparent"
|
||||
border.width: 0
|
||||
|
||||
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
|
||||
AnimatedImage {
|
||||
id: probe
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: false
|
||||
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
|
||||
}
|
||||
radius: Math.min(width, height) / 2
|
||||
color: "transparent"
|
||||
|
||||
// Takes over once the probe settles on a non-animated image, then latches.
|
||||
Image {
|
||||
id: staticImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: false
|
||||
sourceSize.width: Math.max(width * 2, 128)
|
||||
sourceSize.height: Math.max(height * 2, 128)
|
||||
source: {
|
||||
if (!root.shouldProbe)
|
||||
return root.imageSource;
|
||||
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
|
||||
return root.imageSource;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
source: root.activeImage
|
||||
maskEnabled: true
|
||||
maskSource: circularMask
|
||||
visible: root.activeImage.status === Image.Ready && root.imageSource !== ""
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: circularMask
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 4
|
||||
height: parent.height - 4
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
|
||||
AnimatedImage {
|
||||
id: probe
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: root.activeImage === probe && probe.status === Image.Ready && root.imageSource !== ""
|
||||
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
|
||||
}
|
||||
|
||||
// Takes over once the probe settles on a non-animated image, then latches.
|
||||
Image {
|
||||
id: staticImage
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: root.activeImage === staticImage && staticImage.status === Image.Ready && root.imageSource !== ""
|
||||
sourceSize.width: Math.max(width * 2, 128)
|
||||
sourceSize.height: Math.max(height * 2, 128)
|
||||
source: {
|
||||
if (!root.shouldProbe)
|
||||
return root.imageSource;
|
||||
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
|
||||
return root.imageSource;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,8 @@ Item {
|
||||
value: root.value
|
||||
actualValue: root.playerValue
|
||||
showActualPlaybackState: root.isSeeking
|
||||
fillColor: MediaAccentService.accent
|
||||
playheadColor: MediaAccentService.accent
|
||||
actualProgressColor: Theme.onSurface_38
|
||||
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
@@ -179,8 +181,8 @@ Item {
|
||||
Item {
|
||||
property real lineWidth: 3
|
||||
property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40)
|
||||
property color fillColor: Theme.primary
|
||||
property color playheadColor: Theme.primary
|
||||
property color fillColor: MediaAccentService.accent
|
||||
property color playheadColor: MediaAccentService.accent
|
||||
property color actualProgressColor: Theme.onSurface_38
|
||||
readonly property real midY: height / 2
|
||||
|
||||
|
||||
@@ -256,6 +256,11 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
|
||||
function snapIndicator() {
|
||||
indicator.initialSetupComplete = false;
|
||||
updateIndicator();
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
Qt.callLater(updateIndicator);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
|
||||
// Wave progress indicator: track, animated fill, seek preview and playhead are
|
||||
// all drawn in a single fragment shader (Shaders/frag/wave_progress.frag).
|
||||
Item {
|
||||
id: root
|
||||
|
||||
@@ -19,19 +20,6 @@ Item {
|
||||
property color playheadColor: Theme.primary
|
||||
property color actualProgressColor: Theme.onSurface_38
|
||||
|
||||
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 actualX: snap(root.width * root.actualValue)
|
||||
readonly property real midY: snap(height / 2)
|
||||
readonly property bool previewAhead: root.showActualPlaybackState && root.value > root.actualValue
|
||||
readonly property bool previewBehind: root.showActualPlaybackState && root.value < root.actualValue
|
||||
readonly property real previewGapStartX: Math.min(root.playX, root.actualX)
|
||||
readonly property real previewGapEndX: Math.max(root.playX, root.actualX)
|
||||
|
||||
Behavior on currentAmp {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
@@ -39,236 +27,34 @@ Item {
|
||||
}
|
||||
}
|
||||
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
|
||||
Component.onCompleted: currentAmp = isPlaying ? amp : 0
|
||||
|
||||
Shape {
|
||||
id: flatTrack
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
layer.enabled: true
|
||||
blending: true
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.trackColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathMove {
|
||||
id: flatStart
|
||||
x: Math.min(root.width, snap(root.playX + playhead.width / 2))
|
||||
y: root.midY
|
||||
}
|
||||
PathLine {
|
||||
id: flatEnd
|
||||
x: root.width
|
||||
y: root.midY
|
||||
}
|
||||
}
|
||||
readonly property real widthPx: width
|
||||
readonly property real heightPx: height
|
||||
readonly property real value: root.value
|
||||
readonly property real actualValue: root.actualValue
|
||||
readonly property real phase: root.phase
|
||||
readonly property real ampPx: root.currentAmp
|
||||
readonly property real wavelengthPx: root.wavelength
|
||||
readonly property real lineWidthPx: root.lineWidth
|
||||
readonly property real showActual: root.showActualPlaybackState ? 1.0 : 0.0
|
||||
readonly property color fillColor: root.fillColor
|
||||
readonly property color trackColor: root.trackColor
|
||||
readonly property color playheadColor: root.playheadColor
|
||||
readonly property color actualColor: root.actualProgressColor
|
||||
|
||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: waveClip
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
readonly property real startX: snap(root.lineWidth / 2)
|
||||
readonly property real aaBias: (0.25 / root.dpr)
|
||||
readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||
readonly property real gapStartX: root.previewAhead ? Math.max(startX, Math.min(root.actualX + aaBias, width)) : Math.max(startX, Math.min(root.playX + playhead.width / 2, width))
|
||||
readonly property real gapEndX: root.previewAhead ? Math.max(gapStartX, Math.min(root.playX - playhead.width / 2 - aaBias, width)) : Math.max(gapStartX, Math.min(root.actualX - aaBias, width))
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
x: 0
|
||||
width: waveClip.endX
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
Shape {
|
||||
id: waveShape
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width + 4 * root.wavelength
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
x: waveOffsetX
|
||||
|
||||
ShapePath {
|
||||
id: wavePath
|
||||
strokeColor: root.fillColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathSvg {
|
||||
id: waveSvg
|
||||
path: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualMask
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
x: waveClip.gapStartX
|
||||
width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
|
||||
color: "transparent"
|
||||
clip: true
|
||||
visible: (root.previewBehind || root.previewAhead) && width > 0
|
||||
|
||||
Shape {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.width + 4 * root.wavelength
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
x: waveOffsetX
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.actualProgressColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathSvg {
|
||||
path: waveSvg.path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: startCap
|
||||
width: snap(root.lineWidth)
|
||||
height: snap(root.lineWidth)
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.startX - width / 2
|
||||
y: waveY(waveClip.startX) - height / 2
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: endCap
|
||||
width: snap(root.lineWidth)
|
||||
height: snap(root.lineWidth)
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.endX - width / 2
|
||||
y: waveY(waveClip.endX) - height / 2
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualEndCap
|
||||
width: snap(root.lineWidth)
|
||||
height: snap(root.lineWidth)
|
||||
radius: width / 2
|
||||
color: root.actualProgressColor
|
||||
x: waveClip.gapEndX - width / 2
|
||||
y: waveY(waveClip.gapEndX) - height / 2
|
||||
visible: (root.previewBehind || root.previewAhead) && actualMask.width > 0
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualMarker
|
||||
width: 2
|
||||
height: Math.max(root.lineWidth + 4, 10)
|
||||
radius: width / 2
|
||||
color: root.actualProgressColor
|
||||
x: root.actualX - width / 2
|
||||
y: root.midY - height / 2
|
||||
visible: root.showActualPlaybackState
|
||||
z: 2
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
|
||||
function wrapMod(a, m) {
|
||||
let r = a % m;
|
||||
return r < 0 ? r + m : r;
|
||||
}
|
||||
function waveY(x, amplitude = root.currentAmp, phaseOffset = root.phase) {
|
||||
return root.midY + amplitude * Math.sin((x / root.wavelength) * 2 * Math.PI + phaseOffset);
|
||||
}
|
||||
|
||||
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
|
||||
|
||||
FrameAnimation {
|
||||
running: root.visible && (root.isPlaying || root.currentAmp > 0)
|
||||
onTriggered: {
|
||||
if (root.isPlaying)
|
||||
root.phase += 0.03 * frameTime * 60;
|
||||
startCap.y = waveY(waveClip.startX) - startCap.height / 2;
|
||||
endCap.y = waveY(waveClip.endX) - endCap.height / 2;
|
||||
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
|
||||
}
|
||||
}
|
||||
|
||||
function buildStaticWave() {
|
||||
const start = waveClip.startX - 2 * root.wavelength;
|
||||
const end = width + 2 * root.wavelength;
|
||||
if (end <= start) {
|
||||
waveSvg.path = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const kLocal = k;
|
||||
const halfPeriod = root.wavelength / 2;
|
||||
function y0(x) {
|
||||
return root.midY + root.currentAmp * Math.sin(kLocal * x);
|
||||
}
|
||||
function dy0(x) {
|
||||
return root.currentAmp * Math.cos(kLocal * x) * kLocal;
|
||||
}
|
||||
|
||||
let x0 = start;
|
||||
let d = `M ${x0} ${y0(x0)}`;
|
||||
while (x0 < end) {
|
||||
const x1 = Math.min(x0 + halfPeriod, end);
|
||||
const dx = x1 - x0;
|
||||
const yA = y0(x0), yB = y0(x1);
|
||||
const dyA = dy0(x0), dyB = dy0(x1);
|
||||
const c1x = x0 + dx / 3;
|
||||
const c1y = yA + (dyA * dx) / 3;
|
||||
const c2x = x1 - dx / 3;
|
||||
const c2y = yB - (dyB * dx) / 3;
|
||||
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`;
|
||||
x0 = x1;
|
||||
}
|
||||
waveSvg.path = d;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
currentAmp = isPlaying ? amp : 0;
|
||||
buildStaticWave();
|
||||
}
|
||||
onWidthChanged: {
|
||||
flatEnd.x = width;
|
||||
buildStaticWave();
|
||||
}
|
||||
onHeightChanged: buildStaticWave()
|
||||
onCurrentAmpChanged: buildStaticWave()
|
||||
onWavelengthChanged: {
|
||||
k = (2 * Math.PI) / Math.max(1e-6, wavelength);
|
||||
buildStaticWave();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user