mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
qs: numerous performance optimizations
- ClippingRectangle usages - Asynchronous loader usages - Replace cava visualizers with shaders
This commit is contained in:
+122
-107
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -15,6 +14,36 @@ Item {
|
||||
property bool showAnimation: true
|
||||
property real animationScale: 1.0
|
||||
|
||||
readonly property real blobBaseRadiusFactor: 0.43
|
||||
readonly property real blobAmplitudeFactor: 0.115
|
||||
readonly property real blobOvershoot: 1.15
|
||||
readonly property real blobEnergySensitivity: 1.15
|
||||
readonly property real cavaFullScale: 45
|
||||
readonly property real blobAttack: 0.75
|
||||
readonly property real blobRelease: 0.2
|
||||
readonly property real blobBeatBoost: 2.5
|
||||
readonly property real blobBeatKick: 4
|
||||
readonly property real blobOnsetThreshold: 1.4
|
||||
readonly property real blobSpringStiffness: 220
|
||||
readonly property real blobSpringDamping: 19
|
||||
readonly property real blobMorphSpeed: 0.05
|
||||
readonly property real blobMorphBoost: 1.7
|
||||
readonly property real blobSpinSpeed: 0.03
|
||||
|
||||
readonly property bool blobActive: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation && albumArtStatus === Image.Ready
|
||||
property var smoothedBands: [0, 0, 0, 0, 0, 0]
|
||||
property var slowBands: [0, 0, 0, 0, 0, 0]
|
||||
property var bandTargets: [0, 0, 0, 0, 0, 0]
|
||||
property var bandDisplay: [0, 0, 0, 0, 0, 0]
|
||||
property var prevLevels: [0, 0, 0, 0, 0, 0]
|
||||
readonly property var fluxWeights: [1.0, 1.0, 0.6, 0.6, 0.35, 0.35]
|
||||
property real fluxAvg: 0.02
|
||||
property real loudCtx: 0.1
|
||||
property int beatCooldown: 0
|
||||
property real energyTarget: 0
|
||||
property real energyPos: 0
|
||||
property real energyVel: 0
|
||||
|
||||
onActivePlayerChanged: {
|
||||
lastValidArtUrl = "";
|
||||
}
|
||||
@@ -25,6 +54,62 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function updateBands() {
|
||||
const vals = CavaService.values;
|
||||
if (!vals || vals.length < 6)
|
||||
return;
|
||||
|
||||
const s = smoothedBands;
|
||||
const slow = slowBands;
|
||||
const out = bandTargets;
|
||||
const prev = prevLevels;
|
||||
const w = fluxWeights;
|
||||
let flux = 0;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const level = Math.min(Math.max(vals[i], 0), cavaFullScale) / cavaFullScale;
|
||||
flux += Math.max(0, level - prev[i]) * w[i];
|
||||
prev[i] = level;
|
||||
const alpha = level > s[i] ? blobAttack : blobRelease;
|
||||
s[i] += alpha * (level - s[i]);
|
||||
slow[i] += 0.05 * (level - slow[i]);
|
||||
const punch = Math.max(0, s[i] - slow[i]) * blobBeatBoost;
|
||||
out[i] = Math.min(1, (0.55 * s[i] + punch) * blobEnergySensitivity);
|
||||
}
|
||||
|
||||
const ratio = flux / Math.max(fluxAvg, 0.004);
|
||||
fluxAvg += 0.06 * (flux - fluxAvg);
|
||||
if (beatCooldown > 0) {
|
||||
beatCooldown--;
|
||||
} else if (ratio > blobOnsetThreshold && flux > 0.008) {
|
||||
energyVel += blobBeatKick * Math.min(2.5, ratio - 1);
|
||||
beatCooldown = 3;
|
||||
}
|
||||
|
||||
const loud = 0.7 * Math.max(prev[0], prev[1]) + 0.3 * Math.max(prev[2], prev[3]);
|
||||
loudCtx += 0.03 * (loud - loudCtx);
|
||||
const surge = Math.max(0, loud / Math.max(loudCtx, 0.05) - 1);
|
||||
energyTarget = Math.min(1, 0.5 * loud + 0.6 * Math.min(1, surge));
|
||||
}
|
||||
|
||||
function stepBlob(dt) {
|
||||
energyVel += (blobSpringStiffness * (energyTarget - energyPos) - blobSpringDamping * energyVel) * dt;
|
||||
energyPos = Math.max(0, Math.min(blobOvershoot, energyPos + energyVel * dt));
|
||||
blobEffect.energy = energyPos;
|
||||
|
||||
const d = bandDisplay;
|
||||
const t = bandTargets;
|
||||
const f = Math.min(1, dt * 14);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
d[i] += f * (t[i] - d[i]);
|
||||
}
|
||||
blobEffect.bandsA = Qt.vector4d(d[0], d[1], d[2], d[3]);
|
||||
blobEffect.bandsB = Qt.vector2d(d[4], d[5]);
|
||||
|
||||
const speed = 1 + energyPos * blobMorphBoost;
|
||||
blobEffect.phase = (blobEffect.phase + dt * blobMorphSpeed * speed) % 1;
|
||||
blobEffect.spin = (blobEffect.spin + dt * blobSpinSpeed) % 6.28318530718;
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
|
||||
sourceComponent: Component {
|
||||
@@ -34,119 +119,49 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: morphingBlob
|
||||
width: parent.width * 1.1
|
||||
height: parent.height * 1.1
|
||||
Connections {
|
||||
target: CavaService
|
||||
enabled: blobEffect.visible
|
||||
function onValuesChanged() {
|
||||
root.updateBands();
|
||||
}
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
running: blobEffect.visible
|
||||
onTriggered: root.stepBlob(Math.min(frameTime, 0.05))
|
||||
}
|
||||
|
||||
ShaderEffect {
|
||||
id: blobEffect
|
||||
|
||||
readonly property real span: Math.min(root.width, root.height)
|
||||
|
||||
width: span * (root.blobBaseRadiusFactor + root.blobAmplitudeFactor * root.blobOvershoot) * 2 * root.animationScale + 4
|
||||
height: width
|
||||
anchors.centerIn: parent
|
||||
visible: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
|
||||
asynchronous: false
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
z: 0
|
||||
layer.enabled: false
|
||||
visible: root.blobActive || activation > 0.004
|
||||
|
||||
readonly property real centerX: width / 2
|
||||
readonly property real centerY: height / 2
|
||||
readonly property real baseRadius: Math.min(width, height) * 0.41 * root.animationScale
|
||||
readonly property int segments: 28
|
||||
property real phase: 0
|
||||
property real spin: 0
|
||||
property real sizePx: width
|
||||
property real baseRadiusPx: span * root.blobBaseRadiusFactor * root.animationScale
|
||||
property real amplitudePx: span * root.blobAmplitudeFactor * root.animationScale
|
||||
property real activation: root.blobActive ? 1 : 0
|
||||
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)
|
||||
|
||||
property var audioLevels: {
|
||||
if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
|
||||
return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6];
|
||||
}
|
||||
return CavaService.values;
|
||||
}
|
||||
|
||||
property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]
|
||||
property var cubics: []
|
||||
|
||||
Connections {
|
||||
target: CavaService
|
||||
function onValuesChanged() {
|
||||
if (morphingBlob.visible) {
|
||||
morphingBlob.updatePath();
|
||||
}
|
||||
Behavior on activation {
|
||||
NumberAnimation {
|
||||
duration: 550
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: cubicSegment
|
||||
PathCubic {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pathMoveComp
|
||||
PathMove {}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
shapePath.pathElements.push(pathMoveComp.createObject(shapePath));
|
||||
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const seg = cubicSegment.createObject(shapePath);
|
||||
shapePath.pathElements.push(seg);
|
||||
cubics.push(seg);
|
||||
}
|
||||
|
||||
updatePath();
|
||||
}
|
||||
|
||||
function updatePath() {
|
||||
if (cubics.length === 0)
|
||||
return;
|
||||
|
||||
const alpha = 0.35;
|
||||
const minLen = Math.min(smoothedLevels.length, audioLevels.length);
|
||||
for (let i = 0; i < minLen; i++) {
|
||||
smoothedLevels[i] += alpha * (audioLevels[i] - smoothedLevels[i]);
|
||||
}
|
||||
|
||||
const angleStep = 2 * Math.PI / segments;
|
||||
const tension3 = 0.16666667;
|
||||
const startMove = shapePath.pathElements[0];
|
||||
|
||||
const points = new Array(segments);
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const angle = i * angleStep;
|
||||
const audioIndex = i % 10;
|
||||
const rawLevel = smoothedLevels[audioIndex] || 0;
|
||||
const clampedLevel = rawLevel < 0 ? 0 : (rawLevel > 100 ? 100 : rawLevel);
|
||||
const audioLevel = Math.max(0.15, Math.sqrt(clampedLevel * 0.01)) * 0.5;
|
||||
const radius = baseRadius * (1.0 + audioLevel);
|
||||
points[i] = {
|
||||
x: centerX + Math.cos(angle) * radius,
|
||||
y: centerY + Math.sin(angle) * radius
|
||||
};
|
||||
}
|
||||
|
||||
startMove.x = points[0].x;
|
||||
startMove.y = points[0].y;
|
||||
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const p0 = points[(i + segments - 1) % segments];
|
||||
const p1 = points[i];
|
||||
const p2 = points[(i + 1) % segments];
|
||||
const p3 = points[(i + 2) % segments];
|
||||
|
||||
const seg = cubics[i];
|
||||
seg.control1X = p1.x + (p2.x - p0.x) * tension3;
|
||||
seg.control1Y = p1.y + (p2.y - p0.y) * tension3;
|
||||
seg.control2X = p2.x - (p3.x - p1.x) * tension3;
|
||||
seg.control2Y = p2.y - (p3.y - p1.y) * tension3;
|
||||
seg.x = p2.x;
|
||||
seg.y = p2.y;
|
||||
}
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
id: shapePath
|
||||
fillColor: Theme.primary
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillRule: ShapePath.WindingFill
|
||||
}
|
||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/blob.frag.qsb")
|
||||
}
|
||||
|
||||
DankCircularImage {
|
||||
|
||||
@@ -13,9 +13,17 @@ Rectangle {
|
||||
property bool cacheImages: true
|
||||
property bool hasImage: imageSource !== ""
|
||||
readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
|
||||
// Probe with AnimatedImage first; once loaded, check frameCount to decide.
|
||||
readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
|
||||
readonly property var activeImage: isAnimated ? probe : staticImage
|
||||
readonly property bool probeSettled: probe.status === Image.Ready || probe.status === Image.Error
|
||||
readonly property var activeImage: {
|
||||
if (isAnimated)
|
||||
return probe;
|
||||
if (staticImage.status === Image.Ready)
|
||||
return staticImage;
|
||||
if (probe.status === Image.Ready && probe.source !== "")
|
||||
return probe;
|
||||
return staticImage;
|
||||
}
|
||||
property int imageStatus: activeImage.status
|
||||
|
||||
signal imageSaved(string filePath)
|
||||
@@ -57,7 +65,7 @@ Rectangle {
|
||||
border.color: "transparent"
|
||||
border.width: 0
|
||||
|
||||
// Probe: loads as AnimatedImage to detect frame count.
|
||||
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
|
||||
AnimatedImage {
|
||||
id: probe
|
||||
anchors.fill: parent
|
||||
@@ -68,10 +76,10 @@ Rectangle {
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: false
|
||||
source: root.shouldProbe ? root.imageSource : ""
|
||||
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
|
||||
}
|
||||
|
||||
// Static fallback: used once probe confirms the image is not animated.
|
||||
// Takes over once the probe settles on a non-animated image, then latches.
|
||||
Image {
|
||||
id: staticImage
|
||||
anchors.fill: parent
|
||||
@@ -84,38 +92,12 @@ Rectangle {
|
||||
visible: false
|
||||
sourceSize.width: Math.max(width * 2, 128)
|
||||
sourceSize.height: Math.max(height * 2, 128)
|
||||
source: !root.shouldProbe ? root.imageSource : ""
|
||||
}
|
||||
|
||||
// Once the probe loads, if not animated, hand off to Image and unload probe.
|
||||
Connections {
|
||||
target: probe
|
||||
function onStatusChanged() {
|
||||
source: {
|
||||
if (!root.shouldProbe)
|
||||
return;
|
||||
switch (probe.status) {
|
||||
case Image.Ready:
|
||||
if (probe.frameCount <= 1) {
|
||||
staticImage.source = root.imageSource;
|
||||
probe.source = "";
|
||||
}
|
||||
break;
|
||||
case Image.Error:
|
||||
staticImage.source = root.imageSource;
|
||||
probe.source = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If imageSource changes, reset: re-probe with AnimatedImage.
|
||||
onImageSourceChanged: {
|
||||
if (root.shouldProbe) {
|
||||
staticImage.source = "";
|
||||
probe.source = root.imageSource;
|
||||
} else {
|
||||
probe.source = "";
|
||||
staticImage.source = root.imageSource;
|
||||
return root.imageSource;
|
||||
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
|
||||
return root.imageSource;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1249,14 +1249,14 @@ Item {
|
||||
// Fast fade duration for superseded close.
|
||||
readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible
|
||||
readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0))
|
||||
property real publishedOpacity: _targetOpacity
|
||||
readonly property real publishedOpacity: opacity
|
||||
|
||||
opacity: _targetOpacity
|
||||
visible: _renderActive
|
||||
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr)
|
||||
x: Theme.snap(contentContainer.animX, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY, root.dpr)
|
||||
|
||||
layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1)
|
||||
layer.smooth: false
|
||||
@@ -1276,15 +1276,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on publishedOpacity {
|
||||
enabled: contentWrapper._fadeWithOpacity
|
||||
NumberAnimation {
|
||||
duration: contentWrapper._supersededFade ? Theme.shorterDuration : Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
|
||||
@@ -220,6 +220,31 @@ Item {
|
||||
onTriggered: root._bgCommitWindow = false
|
||||
}
|
||||
|
||||
// An idle layer surface won't commit the cleared blur region on auto-close, so the
|
||||
// blur sticks; pulse updatesEnabled false->true to force a re-commit.
|
||||
property bool _blurCommitSuppress: false
|
||||
|
||||
Timer {
|
||||
id: blurCommitPulseTimer
|
||||
interval: 16
|
||||
onTriggered: root._blurCommitSuppress = false
|
||||
}
|
||||
|
||||
function _pulseBlurCommit() {
|
||||
if (!backgroundWindow.visible)
|
||||
return;
|
||||
_blurCommitSuppress = true;
|
||||
blurCommitPulseTimer.restart();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: overlayLoader.item
|
||||
ignoreUnknownSignals: true
|
||||
function onOverlayBlurActiveChanged() {
|
||||
root._pulseBlurCommit();
|
||||
}
|
||||
}
|
||||
|
||||
function _setSurfaceGeometry(bodyX, bodyY, bodyW, bodyH) {
|
||||
const newX = Theme.snap(bodyX, dpr);
|
||||
const newY = Theme.snap(bodyY, dpr);
|
||||
@@ -540,7 +565,7 @@ Item {
|
||||
// Skip buffer updates when there's nothing to render. Briefly flipped
|
||||
// true via _bgCommitWindow when _surfaceBodyW/H changes so the
|
||||
// contentHoleRect mask carve-out actually commits to the compositor.
|
||||
updatesEnabled: root.overlayContent !== null || root._bgCommitWindow
|
||||
updatesEnabled: !root._blurCommitSuppress && (root.overlayContent !== null || root._bgCommitWindow)
|
||||
|
||||
WlrLayershell.namespace: root.layerNamespace + ":background"
|
||||
WlrLayershell.layer: root.effectivePopoutLayer
|
||||
|
||||
Reference in New Issue
Block a user