up su Gitea

This commit is contained in:
2026-04-19 17:07:18 +02:00
parent e78ce720bb
commit fe54b28378
298 changed files with 23460 additions and 0 deletions
@@ -0,0 +1,150 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Modules.DesktopWidgets
import qs.Services.Media
DraggableDesktopWidget {
id: root
property var pluginApi: null
implicitWidth: Math.round(300 * widgetScale)
implicitHeight: Math.round(300 * widgetScale)
showBackground: false
// Scaled dimensions
readonly property int scaledRadiusL: Math.round(Style.radiusL * widgetScale)
// Settings from plugin
readonly property real sensitivity: widgetData?.sensitivity ?? pluginApi?.pluginSettings?.sensitivity ?? pluginApi?.manifest?.metadata?.defaultSettings?.sensitivity
readonly property real rotationSpeed: widgetData?.rotationSpeed ?? pluginApi?.pluginSettings?.rotationSpeed ?? pluginApi?.manifest?.metadata?.defaultSettings?.rotationSpeed
readonly property real barWidth: widgetData?.barWidth ?? pluginApi?.pluginSettings?.barWidth ?? pluginApi?.manifest?.metadata?.defaultSettings?.barWidth
readonly property real ringOpacity: widgetData?.ringOpacity ?? pluginApi?.pluginSettings?.ringOpacity ?? pluginApi?.manifest?.metadata?.defaultSettings?.ringOpacity
readonly property real bloomIntensity: widgetData?.bloomIntensity ?? pluginApi.pluginSettings?.bloomIntensity ?? pluginApi?.manifest?.metadata?.defaultSettings?.bloomIntensity
readonly property int visualizationMode: widgetData?.visualizationMode ?? pluginApi?.pluginSettings?.visualizationMode ?? pluginApi?.manifest?.metadata?.defaultSettings?.visualizationMode ?? 3
readonly property real waveThickness: widgetData?.waveThickness ?? pluginApi?.pluginSettings?.waveThickness ?? pluginApi?.manifest?.metadata?.defaultSettings?.waveThickness ?? 1.0
readonly property real innerDiameter: widgetData?.innerDiameter ?? pluginApi?.pluginSettings?.innerDiameter ?? pluginApi?.manifest?.metadata?.defaultSettings?.innerDiameter ?? 0.7
readonly property bool fadeWhenIdle: widgetData?.fadeWhenIdle ?? pluginApi?.pluginSettings?.fadeWhenIdle ?? false
readonly property bool useCustomColors: widgetData?.useCustomColors ?? pluginApi?.pluginSettings?.useCustomColors ?? false
readonly property color customPrimaryColor: widgetData?.customPrimaryColor ?? pluginApi?.pluginSettings?.customPrimaryColor ?? "#6750A4"
readonly property color customSecondaryColor: widgetData?.customSecondaryColor ?? pluginApi?.pluginSettings?.customSecondaryColor ?? "#625B71"
// Animation time for shader (0 to 3600, 1 hour cycle)
property real shaderTime: 0
NumberAnimation on shaderTime {
loops: Animation.Infinite
from: 0
to: 3600
duration: 3600000
running: !SpectrumService.isIdle
}
// Hidden canvas that encodes audio data as a 32x1 texture
Canvas {
id: audioCanvas
width: 32
height: 1
visible: false
onPaint: {
var ctx = getContext("2d");
var values = SpectrumService.values;
if (!values || values.length === 0) {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 32, 1);
return;
}
for (var i = 0; i < 32; i++) {
var v = values[i] || 0;
// Encode amplitude in grayscale (R=G=B=amplitude)
var c = Math.floor(v * 255);
ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
ctx.fillRect(i, 0, 1, 1);
}
}
}
// Trigger canvas repaint when audio data changes
Connections {
target: SpectrumService
function onValuesChanged() {
if (!SpectrumService.isIdle) {
audioCanvas.requestPaint();
}
}
}
// Unique instance ID for SpectrumService registration
// This prevents the old widget's destruction from unregistering the new widget
readonly property string spectrumInstanceId: "plugin:fancy-audiovisualizer:" + Date.now() + Math.random()
// Register with SpectrumService when pluginApi becomes available
onPluginApiChanged: {
if (pluginApi) {
SpectrumService.registerComponent(spectrumInstanceId);
audioCanvas.requestPaint();
}
}
Component.onDestruction: {
SpectrumService.unregisterComponent(spectrumInstanceId);
}
// Audio texture source (outside ShaderEffect to avoid 'source' property warning)
ShaderEffectSource {
id: audioTextureSource
sourceItem: audioCanvas
live: true
hideSource: true
}
// The shader effect visualization
ShaderEffect {
id: visualizer
anchors.fill: parent
visible: pluginApi !== null
opacity: (root.fadeWhenIdle && SpectrumService.isIdle) ? 0 : 1
Behavior on opacity {
NumberAnimation { duration: 500; easing.type: Easing.InOutQuad }
}
// Audio texture - named 'source' to match ShaderEffectSource's property and avoid warning
property var source: audioTextureSource
// Uniforms passed to shader
property real time: root.shaderTime
property real itemWidth: visualizer.width
property real itemHeight: visualizer.height
property color primaryColor: root.useCustomColors ? root.customPrimaryColor : Color.mPrimary
property color secondaryColor: root.useCustomColors ? root.customSecondaryColor : Color.mSecondary
property real sensitivity: root.sensitivity
property real rotationSpeed: root.rotationSpeed
property real barWidth: root.barWidth
property real ringOpacity: root.ringOpacity
property real cornerRadius: scaledRadiusL
property real bloomIntensity: root.bloomIntensity
property real visualizationMode: root.visualizationMode
property real waveThickness: root.waveThickness
property real innerDiameter: root.innerDiameter
fragmentShader: pluginApi ? Qt.resolvedUrl(pluginApi.pluginDir + "/shaders/visualizer.frag.qsb") : ""
}
// Fallback when shader not loaded
Rectangle {
anchors.fill: parent
color: Color.mSurface
radius: scaledRadiusL
visible: !visualizer.visible || visualizer.fragmentShader === ""
Text {
anchors.centerIn: parent
text: "Loading..."
color: Color.mOnSurface
font.pointSize: Math.round(Style.fontSizeM * widgetScale)
}
}
}
@@ -0,0 +1,244 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
property var pluginApi: null
property var widgetSettings: null
property var screen: null
spacing: Style.marginM
// Local state for editing
property real valueSensitivity: widgetSettings?.data?.sensitivity ?? pluginApi?.pluginSettings?.sensitivity ?? 1.5
property real valueRotationSpeed: widgetSettings?.data?.rotationSpeed ?? pluginApi?.pluginSettings?.rotationSpeed ?? 0.5
property real valueBarWidth: widgetSettings?.data?.barWidth ?? pluginApi?.pluginSettings?.barWidth ?? 0.6
property real valueRingOpacity: widgetSettings?.data?.ringOpacity ?? pluginApi?.pluginSettings?.ringOpacity ?? 0.8
property real valueBloomIntensity: widgetSettings?.data?.bloomIntensity ?? pluginApi?.pluginSettings?.bloomIntensity ?? 0.5
property int valueVisualizationMode: widgetSettings?.data?.visualizationMode ?? pluginApi?.pluginSettings?.visualizationMode ?? 3
property real valueWaveThickness: widgetSettings?.data?.waveThickness ?? pluginApi?.pluginSettings?.waveThickness ?? 1.0
property real valueInnerDiameter: widgetSettings?.data?.innerDiameter ?? pluginApi?.pluginSettings?.innerDiameter ?? 0.7
property bool valueFadeWhenIdle: widgetSettings?.data?.fadeWhenIdle ?? pluginApi?.pluginSettings?.fadeWhenIdle ?? true
property bool valueUseCustomColors: widgetSettings?.data?.useCustomColors ?? pluginApi?.pluginSettings?.useCustomColors ?? false
property color valueCustomPrimaryColor: widgetSettings?.data?.customPrimaryColor ?? pluginApi?.pluginSettings?.customPrimaryColor ?? "#6750A4"
property color valueCustomSecondaryColor: widgetSettings?.data?.customSecondaryColor ?? pluginApi?.pluginSettings?.customSecondaryColor ?? "#625B71"
// Mode helpers
readonly property bool modeHasBars: valueVisualizationMode === 0 || valueVisualizationMode === 3 || valueVisualizationMode === 5
readonly property bool modeHasWave: valueVisualizationMode === 1 || valueVisualizationMode === 4 || valueVisualizationMode === 5
readonly property bool modeHasRings: valueVisualizationMode >= 2
NHeader {
label: pluginApi?.tr("settings.title") ?? "Visualizer Settings"
description: pluginApi?.tr("settings.description") ?? "Configure the audio visualizer appearance"
}
// Visualization mode selector
NComboBox {
Layout.fillWidth: true
label: pluginApi?.tr("settings.visualizationMode") ?? "Visualization Mode"
description: pluginApi?.tr("settings.visualizationMode-description") ?? "Choose visualization style"
model: [
{"key": "0", "name": pluginApi?.tr("settings.mode.bars") ?? "Bars"},
{"key": "1", "name": pluginApi?.tr("settings.mode.wave") ?? "Wave"},
{"key": "2", "name": pluginApi?.tr("settings.mode.rings") ?? "Rings"},
{"key": "3", "name": pluginApi?.tr("settings.mode.barsRings") ?? "Bars + Rings"},
{"key": "4", "name": pluginApi?.tr("settings.mode.waveRings") ?? "Wave + Rings"},
{"key": "5", "name": pluginApi?.tr("settings.mode.all") ?? "All"}
]
currentKey: String(root.valueVisualizationMode)
onSelected: key => {
root.valueVisualizationMode = parseInt(key);
root.saveSettings();
}
}
// Wave thickness slider (shown when mode includes wave)
NValueSlider {
Layout.fillWidth: true
visible: root.modeHasWave
label: pluginApi?.tr("settings.waveThickness") ?? "Wave Thickness"
value: root.valueWaveThickness
from: 0.3
to: 2.0
stepSize: 0.1
onMoved: value => {
root.valueWaveThickness = value;
root.saveSettings();
}
}
// Sensitivity slider
NValueSlider {
Layout.fillWidth: true
label: pluginApi?.tr("settings.sensitivity") ?? "Sensitivity"
value: root.valueSensitivity
from: 0.5
to: 3.0
stepSize: 0.1
onMoved: value => {
root.valueSensitivity = value;
root.saveSettings();
}
}
// Rotation speed slider
NValueSlider {
Layout.fillWidth: true
label: pluginApi?.tr("settings.rotationSpeed") ?? "Rotation Speed"
value: root.valueRotationSpeed
from: 0.0
to: 2.0
stepSize: 0.1
onMoved: value => {
root.valueRotationSpeed = value;
root.saveSettings();
}
}
// Bar width slider (shown when mode includes bars)
NValueSlider {
Layout.fillWidth: true
visible: root.modeHasBars
label: pluginApi?.tr("settings.barWidth") ?? "Bar Width"
value: root.valueBarWidth
from: 0.2
to: 1.0
stepSize: 0.1
onMoved: value => {
root.valueBarWidth = value;
root.saveSettings();
}
}
// Ring opacity slider (shown when mode includes rings)
NValueSlider {
Layout.fillWidth: true
visible: root.modeHasRings
label: pluginApi?.tr("settings.ringOpacity") ?? "Ring Opacity"
value: root.valueRingOpacity
from: 0.0
to: 1.0
stepSize: 0.1
onMoved: value => {
root.valueRingOpacity = value;
root.saveSettings();
}
}
// Base diameter slider
NValueSlider {
Layout.fillWidth: true
label: pluginApi?.tr("settings.innerDiameter") ?? "Inner Diameter"
value: root.valueInnerDiameter
from: 0
to: 1
stepSize: 0.05
onMoved: value => {
root.valueInnerDiameter = value;
root.saveSettings();
}
}
// Bloom intensity slider
NValueSlider {
Layout.fillWidth: true
label: pluginApi?.tr("settings.bloomIntensity") ?? "Bloom Intensity"
value: root.valueBloomIntensity
from: 0.0
to: 1.0
stepSize: 0.05
onMoved: value => {
root.valueBloomIntensity = value;
root.saveSettings();
}
}
// Fade when idle toggle
NToggle {
label: pluginApi?.tr("settings.fadeWhenIdle") ?? "Fade When Idle"
description: pluginApi?.tr("settings.fadeWhenIdle-description") ?? "Fade out visualizer when no audio is playing"
checked: root.valueFadeWhenIdle
onToggled: checked => {
root.valueFadeWhenIdle = checked;
root.saveSettings();
}
}
// Use custom colors toggle
NToggle {
label: pluginApi?.tr("settings.useCustomColors") ?? "Use Custom Colors"
description: pluginApi?.tr("settings.useCustomColors-description") ?? "Override theme colors with custom colors"
checked: root.valueUseCustomColors
onToggled: checked => {
root.valueUseCustomColors = checked;
root.saveSettings();
}
}
// Custom primary color picker
RowLayout {
Layout.fillWidth: true
visible: root.valueUseCustomColors
spacing: Style.marginM
NText {
text: pluginApi?.tr("settings.customPrimaryColor") ?? "Primary Color"
Layout.fillWidth: true
}
NColorPicker {
screen: Screen
selectedColor: root.valueCustomPrimaryColor
onColorSelected: color => {
root.valueCustomPrimaryColor = color;
root.saveSettings();
}
}
}
// Custom secondary color picker
RowLayout {
Layout.fillWidth: true
visible: root.valueUseCustomColors
spacing: Style.marginM
NText {
text: pluginApi?.tr("settings.customSecondaryColor") ?? "Secondary Color"
Layout.fillWidth: true
}
NColorPicker {
screen: Screen
selectedColor: root.valueCustomSecondaryColor
onColorSelected: color => {
root.valueCustomSecondaryColor = color;
root.saveSettings();
}
}
}
// Called when user clicks Apply/Save
function saveSettings() {
if (!widgetSettings)
return;
widgetSettings.data.sensitivity = root.valueSensitivity;
widgetSettings.data.rotationSpeed = root.valueRotationSpeed;
widgetSettings.data.barWidth = root.valueBarWidth;
widgetSettings.data.ringOpacity = root.valueRingOpacity;
widgetSettings.data.bloomIntensity = root.valueBloomIntensity;
widgetSettings.data.visualizationMode = root.valueVisualizationMode;
widgetSettings.data.waveThickness = root.valueWaveThickness;
widgetSettings.data.innerDiameter = root.valueInnerDiameter;
widgetSettings.data.fadeWhenIdle = root.valueFadeWhenIdle;
widgetSettings.data.useCustomColors = root.valueUseCustomColors;
widgetSettings.data.customPrimaryColor = root.valueCustomPrimaryColor.toString();
widgetSettings.data.customSecondaryColor = root.valueCustomSecondaryColor.toString();
widgetSettings.save();
}
}
@@ -0,0 +1,38 @@
# Fancy Audiovisualizer
A circular audio visualizer desktop widget for Noctalia Shell with shader-based rendering and multiple visualization modes.
## Features
- **Multiple Visualization Modes**: Bars, Wave, Rings, or combinations (Bars+Rings, Wave+Rings, All)
- **Shader-Based Rendering**: Smooth, GPU-accelerated visualization using custom fragment shaders
- **Theme Integration**: Automatically uses Noctalia theme colors, with optional custom color override
- **Configurable Appearance**: Adjust sensitivity, rotation speed, bar width, ring opacity, bloom intensity, and more
- **Idle Fade**: Optional fade-out when no audio is playing
## Installation
This plugin is part of the `noctalia-plugins` repository.
## Configuration
Access the plugin settings in Noctalia to configure the following options:
- **Visualization Mode**: Choose between Bars, Wave, Rings, Bars+Rings, Wave+Rings, or All
- **Wave Thickness**: Thickness of the wave visualization (when enabled)
- **Sensitivity**: Audio response sensitivity (0.5 - 3.0)
- **Rotation Speed**: Speed of visualization rotation (0.0 - 2.0)
- **Bar Width**: Width of audio bars (when enabled)
- **Ring Opacity**: Opacity of background rings (when enabled)
- **Inner Diameter**: Size of the inner empty area
- **Bloom Intensity**: Glow/bloom effect strength
- **Fade When Idle**: Fade out when no audio is playing
- **Custom Colors**: Override theme colors with custom primary and secondary colors
## Usage
Add the widget to your desktop via the Noctalia desktop widgets interface. The visualizer responds to system audio.
## Requirements
- Noctalia 3.7.2 or later
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Balkenbreite",
"bloomIntensity": "Blütenintensität",
"customPrimaryColor": "Primärfarbe",
"customSecondaryColor": "Sekundärfarbe",
"description": "Audiovisualisierungsdarstellung konfigurieren",
"fadeWhenIdle": "Ausblenden bei Inaktivität",
"fadeWhenIdle-description": "Visualisierer ausblenden, wenn keine Audioausgabe erfolgt",
"innerDiameter": "Innendurchmesser",
"mode": {
"all": "Alle",
"bars": "Bars",
"barsRings": "Barren + Ringe",
"rings": "Ringe",
"wave": "Welle",
"waveRings": "Welle + Ringe"
},
"ringOpacity": "Ring-Opazität",
"rotationSpeed": "Drehzahl",
"sensitivity": "Empfindlichkeit",
"title": "Visualisierungseinstellungen",
"useCustomColors": "Benutzerdefinierte Farben verwenden",
"useCustomColors-description": "Themenfarben mit benutzerdefinierten Farben überschreiben",
"visualizationMode": "Visualisierungsmodus",
"visualizationMode-description": "Visualisierungsstil wählen",
"waveThickness": "Wellendicke"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Bar Width",
"bloomIntensity": "Bloom Intensity",
"customPrimaryColor": "Primary Color",
"customSecondaryColor": "Secondary Color",
"description": "Configure the audio visualizer appearance",
"fadeWhenIdle": "Fade When Idle",
"fadeWhenIdle-description": "Fade out visualizer when no audio is playing",
"innerDiameter": "Inner Diameter",
"mode": {
"all": "All",
"bars": "Bars",
"barsRings": "Bars + Rings",
"rings": "Rings",
"wave": "Wave",
"waveRings": "Wave + Rings"
},
"ringOpacity": "Ring Opacity",
"rotationSpeed": "Rotation Speed",
"sensitivity": "Sensitivity",
"title": "Visualizer Settings",
"useCustomColors": "Use Custom Colors",
"useCustomColors-description": "Override theme colors with custom colors",
"visualizationMode": "Visualization Mode",
"visualizationMode-description": "Choose visualization style",
"waveThickness": "Wave Thickness"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Ancho de barra",
"bloomIntensity": "Intensidad de floración",
"customPrimaryColor": "Color primario",
"customSecondaryColor": "Color secundario",
"description": "Configurar la apariencia del visualizador de audio",
"fadeWhenIdle": "Desvanecer al estar inactivo",
"fadeWhenIdle-description": "Atenuar el visualizador cuando no se reproduce audio.",
"innerDiameter": "Diámetro interior",
"mode": {
"all": "Todo",
"bars": "Barras",
"barsRings": "Barras + Anillas",
"rings": "Anillos",
"wave": "Ola",
"waveRings": "Onda + Anillos"
},
"ringOpacity": "Opacidad del anillo",
"rotationSpeed": "Velocidad de rotación",
"sensitivity": "Sensibilidad",
"title": "Ajustes del Visualizador",
"useCustomColors": "Usar colores personalizados",
"useCustomColors-description": "Anular los colores del tema con colores personalizados",
"visualizationMode": "Modo de visualización",
"visualizationMode-description": "Elegir estilo de visualización",
"waveThickness": "Espesor de la onda"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Largeur de la barre",
"bloomIntensity": "Intensité de l'éclat",
"customPrimaryColor": "Couleur primaire",
"customSecondaryColor": "Couleur secondaire",
"description": "Configurer l'apparence du visualiseur audio",
"fadeWhenIdle": "Estomper en cas d'inactivité",
"fadeWhenIdle-description": "Estomper le visualiseur en l'absence de son.",
"innerDiameter": "Diamètre intérieur",
"mode": {
"all": "Tout",
"bars": "Barres",
"barsRings": "Barres + Anneaux",
"rings": "Anneaux",
"wave": "Vague",
"waveRings": "Onde + Anneaux"
},
"ringOpacity": "Opacité de l'anneau",
"rotationSpeed": "Vitesse de rotation",
"sensitivity": "Sensibilité",
"title": "Paramètres du visualiseur",
"useCustomColors": "Utiliser des couleurs personnalisées",
"useCustomColors-description": "Remplacer les couleurs du thème par des couleurs personnalisées",
"visualizationMode": "Mode de visualisation",
"visualizationMode-description": "Choisir le style de visualisation",
"waveThickness": "Épaisseur de la vague"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Sávszélesség",
"bloomIntensity": "Virágzás intenzitása",
"customPrimaryColor": "Elsődleges szín",
"customSecondaryColor": "Másodlagos szín",
"description": "Az audio vizualizáló megjelenésének beállítása",
"fadeWhenIdle": "Halványítás tétlenség esetén",
"fadeWhenIdle-description": "A vizualizáció elhalványítása, ha nincs hang lejátszva",
"innerDiameter": "Belső átmérő",
"mode": {
"all": "Összes",
"bars": "Sávok",
"barsRings": "Sávok + Gyűrűk",
"rings": "Gyűrűk",
"wave": "Hullám",
"waveRings": "Hullám + Gyűrűk"
},
"ringOpacity": "Gyűrű átlátszósága",
"rotationSpeed": "Forgási sebesség",
"sensitivity": "Érzékenység",
"title": "Vizualizációs beállítások",
"useCustomColors": "Egyéni színek használata",
"useCustomColors-description": "A téma színeinek felülírása egyéni színekkel",
"visualizationMode": "Megjelenítési mód",
"visualizationMode-description": "Válassz megjelenítési stílust",
"waveThickness": "Hullámvastagság"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Breite der Leiste",
"bloomIntensity": "Intensità Bloom",
"customPrimaryColor": "Ngjyra parësore",
"customSecondaryColor": "Ngjyra dytësore",
"description": "Konfigurieren Sie das Aussehen des Audio-Visualisierers.",
"fadeWhenIdle": "Verblassen im Leerlauf",
"fadeWhenIdle-description": "Vizualizátor eltüntetése, ha nincs hang lejátszva.",
"innerDiameter": "Innendurchmesser",
"mode": {
"all": "Kaikki",
"bars": "Pritličja",
"barsRings": "Stangen + Ringe",
"rings": "Hringar",
"wave": "Val",
"waveRings": "Welle + Ringe"
},
"ringOpacity": "Neprůhlednost prstence",
"rotationSpeed": "Schnelllaufdrehzahl",
"sensitivity": "Ndjeshmëri",
"title": "Mga Setting ng Visualizer",
"useCustomColors": "Përdorni Ngjyra të Personalizuara",
"useCustomColors-description": "Ngjyrat e personalizuara mbivendosin ngjyrat e temës",
"visualizationMode": "Módszer megjelenítése",
"visualizationMode-description": "Zgjidhni stilin e vizualizimit",
"waveThickness": "Onda-lodiera"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "バーの幅",
"bloomIntensity": "開花強度",
"customPrimaryColor": "原色",
"customSecondaryColor": "二次色",
"description": "オーディオビジュアライザーの外観を設定します。",
"fadeWhenIdle": "アイドル時にフェード",
"fadeWhenIdle-description": "音声が再生されていないときは、ビジュアライザーをフェードアウトする",
"innerDiameter": "内径",
"mode": {
"all": "すべて",
"bars": "バー",
"barsRings": "鉄棒と輪",
"rings": "リング",
"wave": "波",
"waveRings": "波紋 + リング"
},
"ringOpacity": "リングの不透明度",
"rotationSpeed": "回転速度",
"sensitivity": "感度",
"title": "ビジュアライザー設定",
"useCustomColors": "カスタムカラーを使用する",
"useCustomColors-description": "カスタムカラーでテーマの色をオーバーライドする",
"visualizationMode": "可視化モード",
"visualizationMode-description": "視覚化のスタイルを選択",
"waveThickness": "波厚"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Firehiya Barê",
"bloomIntensity": "Zehfîya Gurtî",
"customPrimaryColor": "Rengê Seretayî",
"customSecondaryColor": "Rengê Duyemîn",
"description": "Xuyakirina dîmenê bihîstwerî yê dengî",
"fadeWhenIdle": "Dema de bêdengiyê vemirîne",
"fadeWhenIdle-description": "Dîmenkerê dema ku tu deng lê nabe, hêdî hêdî winda bike",
"innerDiameter": "Dirêjbûna Hundir",
"mode": {
"all": "Hemû",
"bars": "Bars",
"barsRings": "Bars + Rings",
"rings": "Xelqet",
"wave": "Pêl",
"waveRings": "Pêl + Xelek"
},
"ringOpacity": "Ronahiya Zengil",
"rotationSpeed": "Leza Şewitandinê",
"sensitivity": "Hesasiyet",
"title": "Mîhengên Dîmenderê",
"useCustomColors": "Rengên Xweser Bikar Bîne",
"useCustomColors-description": "Rengên mijarê bi rengên xwerû biguherîne",
"visualizationMode": "Şêwaza Dîmenkirinê",
"visualizationMode-description": "Şêwaza dîtinê hilbijêre",
"waveThickness": "Qalinîya Pêlê"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Balkbreedte",
"bloomIntensity": "Bloei Intensiteit",
"customPrimaryColor": "Primaire kleur",
"customSecondaryColor": "Secundaire kleur",
"description": "Configureer de weergave van de audio visualisatie.",
"fadeWhenIdle": "Vervagen bij inactiviteit",
"fadeWhenIdle-description": "Visualiseerder laten vervagen wanneer er geen audio wordt afgespeeld.",
"innerDiameter": "Binnendiameter",
"mode": {
"all": "Alles",
"bars": "Bars",
"barsRings": "Rekstok + Ringen",
"rings": "Ringen",
"wave": "Golf",
"waveRings": "Golf + Ringen"
},
"ringOpacity": "Ringondoorzichtigheid",
"rotationSpeed": "Rotatiesnelheid",
"sensitivity": "Gevoeligheid",
"title": "Visualisatie-instellingen",
"useCustomColors": "Aangepaste kleuren gebruiken",
"useCustomColors-description": "Themakleuren overschrijven met aangepaste kleuren",
"visualizationMode": "Visualisatiemodus",
"visualizationMode-description": "Kies visualisatiestijl",
"waveThickness": "Golflaagdikte"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Szerokość paska",
"bloomIntensity": "Intensywność Rozkwitu",
"customPrimaryColor": "Kolor podstawowy",
"customSecondaryColor": "Kolor dodatkowy",
"description": "Skonfiguruj wygląd wizualizatora dźwięku",
"fadeWhenIdle": "Przyciemnij przy bezczynności",
"fadeWhenIdle-description": "Wycisz wizualizator, gdy nie jest odtwarzany dźwięk",
"innerDiameter": "Średnica wewnętrzna",
"mode": {
"all": "Wszystkie",
"bars": "Paski",
"barsRings": "Paski + Pierścienie",
"rings": "Pierścienie",
"wave": "Fala",
"waveRings": "Fala + Pierścienie"
},
"ringOpacity": "Krycie pierścienia",
"rotationSpeed": "Prędkość obrotu",
"sensitivity": "Czułość",
"title": "Ustawienia wizualizatora",
"useCustomColors": "Użyj własnych kolorów",
"useCustomColors-description": "Zastąp kolory motywu własnymi kolorami",
"visualizationMode": "Tryb wizualizacji",
"visualizationMode-description": "Wybierz styl wizualizacji",
"waveThickness": "Grubość fali"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Largura da Barra",
"bloomIntensity": "Intensidade do Bloom",
"customPrimaryColor": "Cor primária",
"customSecondaryColor": "Cor secundária",
"description": "Configurar a aparência do visualizador de áudio",
"fadeWhenIdle": "Desaparecer quando inativo",
"fadeWhenIdle-description": "Desvanecer o visualizador quando nenhum áudio estiver sendo reproduzido.",
"innerDiameter": "Diâmetro interno",
"mode": {
"all": "Tudo",
"bars": "Barras",
"barsRings": "Barras + Argolas",
"rings": "Anéis",
"wave": "Onda",
"waveRings": "Onda + Anéis"
},
"ringOpacity": "Opacidade do Anel",
"rotationSpeed": "Velocidade de rotação",
"sensitivity": "Sensibilidade",
"title": "Configurações do Visualizador",
"useCustomColors": "Usar Cores Personalizadas",
"useCustomColors-description": "Substituir as cores do tema por cores personalizadas",
"visualizationMode": "Modo de Visualização",
"visualizationMode-description": "Escolha o estilo de visualização",
"waveThickness": "Espessura da onda"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Ширина полосы",
"bloomIntensity": "Интенсивность свечения",
"customPrimaryColor": "Основной цвет",
"customSecondaryColor": "Вторичный цвет",
"description": "Настроить внешний вид визуализатора звука",
"fadeWhenIdle": "Затухать в режиме ожидания",
"fadeWhenIdle-description": "Затухать визуализатору при отсутствии воспроизведения аудио.",
"innerDiameter": "Внутренний диаметр",
"mode": {
"all": "Всё",
"bars": "Бары",
"barsRings": "Турники + Кольца",
"rings": "Кольца",
"wave": "Волна",
"waveRings": "Волна + Кольца"
},
"ringOpacity": "Прозрачность кольца",
"rotationSpeed": "Скорость вращения",
"sensitivity": "Чувствительность",
"title": "Настройки визуализатора",
"useCustomColors": "Использовать пользовательские цвета",
"useCustomColors-description": "Переопределить цвета темы пользовательскими цветами",
"visualizationMode": "Режим визуализации",
"visualizationMode-description": "Выберите стиль визуализации",
"waveThickness": "Толщина волны"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Çubuk Genişliği",
"bloomIntensity": "Çiçeklenme Yoğunluğu",
"customPrimaryColor": "Ana Renk",
"customSecondaryColor": "İkincil Renk",
"description": "Ses görselleştirici görünümünü yapılandır",
"fadeWhenIdle": "Boşta Kalınca Karart",
"fadeWhenIdle-description": "Ses çalmadığında görselleştiriciyi soldur.",
"innerDiameter": "İç Çap",
"mode": {
"all": "Tüm",
"bars": "Barlar",
"barsRings": "Barfiks Demiri + Halkalar",
"rings": "Yüzükler",
"wave": "Dalga",
"waveRings": "Dalga + Halkalar"
},
"ringOpacity": "Halka Opaklığı",
"rotationSpeed": "Dönme Hızı",
"sensitivity": "Hassasiyet",
"title": "Görselleştirici Ayarları",
"useCustomColors": "Özel Renkleri Kullan",
"useCustomColors-description": "Tema renklerini özel renklerle geçersiz kıl",
"visualizationMode": "Görselleştirme Modu",
"visualizationMode-description": "Görselleştirme stilini seçin",
"waveThickness": "Dalga Kalınlığı"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Ширина бруска",
"bloomIntensity": "Інтенсивність світіння",
"customPrimaryColor": "Основний колір",
"customSecondaryColor": "Вторинний колір",
"description": "Налаштуйте вигляд аудіовізуалізатора",
"fadeWhenIdle": "Згасати в режимі очікування",
"fadeWhenIdle-description": "Затемнювати візуалізатор, коли не відтворюється звук.",
"innerDiameter": "Внутрішній діаметр",
"mode": {
"all": "Все",
"bars": "Бари",
"barsRings": "Бруси + Кільця",
"rings": "Кільця",
"wave": "Хвиля",
"waveRings": "Хвиля + Кільця"
},
"ringOpacity": "Прозорість кільця",
"rotationSpeed": "Швидкість обертання",
"sensitivity": "Чутливість",
"title": "Налаштування візуалізатора",
"useCustomColors": "Використовувати власні кольори",
"useCustomColors-description": "Перевизначити кольори теми власними кольорами",
"visualizationMode": "Режим візуалізації",
"visualizationMode-description": "Виберіть стиль візуалізації",
"waveThickness": "Товщина хвилі"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "Độ rộng thanh",
"bloomIntensity": "Cường độ hiệu ứng bloom",
"customPrimaryColor": "Màu chính tùy chỉnh",
"customSecondaryColor": "Màu phụ tùy chỉnh",
"description": "Cấu hình giao diện trình hiển thị âm thanh",
"fadeWhenIdle": "Làm mờ khi không hoạt động",
"fadeWhenIdle-description": "Làm mờ trình hiển thị khi không có âm thanh",
"innerDiameter": "Đường kính bên trong",
"mode": {
"all": "Tất cả",
"bars": "Thanh",
"barsRings": "Thanh + Vòng",
"rings": "Vòng",
"wave": "Sóng",
"waveRings": "Sóng + Vòng"
},
"ringOpacity": "Độ mờ vòng",
"rotationSpeed": "Tốc độ xoay",
"sensitivity": "Độ nhạy",
"title": "Cài đặt trình hiển thị",
"useCustomColors": "Dùng màu tùy chỉnh",
"useCustomColors-description": "Ghi đè màu giao diện bằng màu tùy chỉnh",
"visualizationMode": "Chế độ hiển thị",
"visualizationMode-description": "Chọn kiểu hiển thị",
"waveThickness": "Độ dày sóng"
}
}
@@ -0,0 +1,29 @@
{
"settings": {
"barWidth": "条宽",
"bloomIntensity": "光晕强度",
"customPrimaryColor": "原色",
"customSecondaryColor": "间色",
"description": "配置音频可视化效果外观",
"fadeWhenIdle": "空闲时淡出",
"fadeWhenIdle-description": "当没有音频播放时淡出可视化工具",
"innerDiameter": "内径",
"mode": {
"all": "全部",
"bars": "酒吧",
"barsRings": "单杠 + 吊环",
"rings": "戒指",
"wave": "波浪",
"waveRings": "波浪 + 环"
},
"ringOpacity": "环透明度",
"rotationSpeed": "转速",
"sensitivity": "敏感性",
"title": "可视化设置",
"useCustomColors": "使用自定义颜色",
"useCustomColors-description": "使用自定义颜色覆盖主题颜色",
"visualizationMode": "可视化模式",
"visualizationMode-description": "选择可视化风格",
"waveThickness": "波浪厚度"
}
}
@@ -0,0 +1,38 @@
{
"id": "fancy-audiovisualizer",
"name": "Fancy Audiovisualizer",
"version": "1.1.2",
"minNoctaliaVersion": "4.6.6",
"author": "Noctalia Team",
"official": true,
"license": "MIT",
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
"description": "Lemmy's fancy circular audio visualizer.",
"tags": [
"Desktop",
"Audio"
],
"entryPoints": {
"desktopWidget": "DesktopWidget.qml",
"desktopWidgetSettings": "DesktopWidgetSettings.qml"
},
"dependencies": {
"plugins": []
},
"metadata": {
"defaultSettings": {
"sensitivity": 1.5,
"rotationSpeed": 0.5,
"barWidth": 0.6,
"ringOpacity": 0.8,
"bloomIntensity": 0.5,
"visualizationMode": 3,
"waveThickness": 1.0,
"innerDiameter": 0.7,
"fadeWhenIdle": false,
"useCustomColors": false,
"customPrimaryColor": "#6750A4",
"customSecondaryColor": "#625B71"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

@@ -0,0 +1,559 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float time;
float itemWidth;
float itemHeight;
vec4 primaryColor;
vec4 secondaryColor;
float sensitivity;
float rotationSpeed;
float barWidth;
float ringOpacity;
float cornerRadius;
float bloomIntensity;
float visualizationMode; // 0=bars, 1=wave, 2=rings, 3=bars+rings, 4=wave+rings, 5=all
float waveThickness;
float innerDiameter;
} ubuf;
// Mode helper functions
bool hasRings() { return ubuf.visualizationMode >= 2.0; }
bool hasBars() { return ubuf.visualizationMode == 0.0 || ubuf.visualizationMode == 3.0 || ubuf.visualizationMode >= 5.0; }
bool hasWave() { return ubuf.visualizationMode == 1.0 || ubuf.visualizationMode == 4.0 || ubuf.visualizationMode >= 5.0; }
layout(binding = 1) uniform sampler2D source;
#define TWOPI 6.28318530718
#define PI 3.14159265359
#define NBARS 32
// Sample audio amplitude at normalized position (0.0-1.0)
float getAudio(float pos) {
return texture(source, vec2(clamp(pos, 0.0, 1.0), 0.5)).r;
}
// Smoothed audio sampling with interpolation
float smoothAudio(float pos) {
float idx = pos * float(NBARS - 1);
float frac = fract(idx);
float i0 = floor(idx) / float(NBARS - 1);
float i1 = ceil(idx) / float(NBARS - 1);
return mix(getAudio(i0), getAudio(i1), frac) * ubuf.sensitivity;
}
// Frequency band helpers
float getBass() { return smoothAudio(0.05); }
float getMid() { return smoothAudio(0.3); }
float getHighMid() { return smoothAudio(0.6); }
float getTreble() { return smoothAudio(0.9); }
// SDF for rounded rectangle
float roundedBoxSDF(vec2 center, vec2 size, float radius) {
vec2 q = abs(center) - size + radius;
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
}
// Compute polar wave visualization
vec4 computePolarWave(vec2 uv, float iTime, float bass, float mid, float highMid, float treble) {
float aspect = ubuf.itemWidth / ubuf.itemHeight;
vec2 centered = (uv - 0.5) * 2.0;
centered.x *= aspect;
float theta = atan(centered.y, centered.x);
float d = length(centered);
float innerRadius = ubuf.innerDiameter / 2.0;
float baseRadius = 0.35; // Fixed reference for outer extent
vec4 color = vec4(0.0);
// RING SYSTEM
if (hasRings()) {
// Center Waves
if (d < innerRadius * 0.6) {
float wave = mid * 0.8;
float ripple = sin(d * 25.0 + wave * 15.0 - iTime * 2.0);
if (ripple > 0.7) {
float intensity = clamp(mid * 0.6, 0.0, 0.4);
vec4 waveColor = ubuf.secondaryColor * intensity * ubuf.ringOpacity;
color = max(color, waveColor);
}
}
// Energy Ring
float energyRad = innerRadius * 0.65;
float energyThickness = 0.015 + clamp(highMid * 0.02, 0.0, 0.03);
if (d > energyRad - energyThickness && d < energyRad + energyThickness) {
float segmentAngle = theta * 8.0 + highMid * 3.0 + iTime;
if (mod(segmentAngle, 1.0) < 0.6) {
float alpha = clamp(highMid * 2.0, 0.3, 1.0) * ubuf.ringOpacity;
vec4 energyColor = mix(ubuf.primaryColor, ubuf.secondaryColor, 0.5) * alpha;
color = max(color, energyColor);
}
}
// Particle Ring
float particleRad = innerRadius * 0.75;
if (d > particleRad - 0.02 && d < particleRad + 0.02) {
float particleAngle = theta + treble * 2.0 + iTime * 0.5;
float particleSpacing = TWOPI / 16.0;
if (mod(particleAngle, particleSpacing) < 0.15) {
float brightness = 0.5 + clamp(treble * 1.5, 0.0, 0.5);
vec4 particleColor = ubuf.secondaryColor * brightness * ubuf.ringOpacity;
color = max(color, particleColor);
}
}
// Tech Grid Ring
float gridRad = innerRadius * 0.85;
if (d > gridRad - 0.012 && d < gridRad + 0.012) {
float gridAngle = theta + iTime * ubuf.rotationSpeed;
float gridDensity = 0.08 + clamp(mid * 0.05, 0.0, 0.1);
if (mod(gridAngle, 0.2) < gridDensity) {
vec4 gridColor = ubuf.primaryColor * 0.7 * ubuf.ringOpacity;
gridColor.rgb += vec3(0.1, 0.15, 0.2) * clamp(mid, 0.0, 0.8);
color = max(color, gridColor);
}
}
// Accent Ring
float accentRad = innerRadius * 0.92;
float pulse = clamp(bass * 0.08, 0.0, 0.05);
if (d > accentRad - pulse - 0.008 && d < accentRad + pulse + 0.015) {
float colorShift = clamp(bass * 0.5, 0.0, 1.0);
vec4 ringColor = mix(ubuf.secondaryColor * 0.7, ubuf.primaryColor, colorShift);
ringColor.a = ubuf.ringOpacity;
ringColor.rgb *= 1.0 + bass * 0.3;
color = max(color, ringColor);
}
// Outer Ring
float outerRad = innerRadius + bass * 0.05;
if (d > outerRad - 0.008 && d < outerRad + 0.008) {
vec4 outerColor = ubuf.primaryColor * ubuf.ringOpacity;
outerColor.rgb += vec3(0.2, 0.3, 0.4) * clamp(treble * 0.5, 0.0, 0.3);
outerColor.rgb *= 1.0 + bass * 0.4;
color = max(color, outerColor);
}
}
// POLAR WAVE - filled shape with mirrored bands (64 visual bands from 32 samples)
if (hasWave()) {
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
// Map angle to 0-1 range around the full circle
float normalizedAngle = mod(adjustedTheta, TWOPI) / TWOPI;
// Mirror: first half (0-0.5) maps to bands 0->31, second half (0.5-1) maps back 31->0
float mirroredPos = normalizedAngle < 0.5 ? normalizedAngle * 2.0 : (1.0 - normalizedAngle) * 2.0;
// Catmull-Rom spline interpolation for smooth curve through mirrored bands
float bandPos = mirroredPos * float(NBARS - 1);
float fband1 = floor(bandPos);
float fband0 = max(fband1 - 1.0, 0.0);
float fband2 = min(fband1 + 1.0, float(NBARS - 1));
float fband3 = min(fband1 + 2.0, float(NBARS - 1));
float t = fract(bandPos);
// Sample the 4 control points
float p0 = getAudio(fband0 / float(NBARS - 1)) * ubuf.sensitivity;
float p1 = getAudio(fband1 / float(NBARS - 1)) * ubuf.sensitivity;
float p2 = getAudio(fband2 / float(NBARS - 1)) * ubuf.sensitivity;
float p3 = getAudio(fband3 / float(NBARS - 1)) * ubuf.sensitivity;
// Catmull-Rom spline interpolation
float t2 = t * t;
float t3 = t2 * t;
float smoothedAudio = 0.5 * (
(2.0 * p1) +
(-p0 + p2) * t +
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
);
smoothedAudio = max(smoothedAudio, 0.0);
// Calculate wave radius at this angle
float waveDisplacement = smoothedAudio * 0.5;
float waveRadius = baseRadius + waveDisplacement; // Fixed outer extent
// Fill the entire area from inner to wave edge
if (d >= innerRadius && d <= waveRadius) {
float fillFactor = (d - innerRadius) / max(waveRadius - innerRadius, 0.001);
// Gradient from primary at base to accent at edge
vec3 fillColor = mix(ubuf.primaryColor.rgb * 0.8, ubuf.secondaryColor.rgb, fillFactor);
// Boost brightness with bass
fillColor *= 1.0 + bass * 0.3;
// Alpha: stronger near the edge, fades toward center
float fillAlpha = mix(0.4, 1.0, fillFactor) * ubuf.waveThickness;
fillAlpha = clamp(fillAlpha, 0.0, 1.0);
vec4 fill = vec4(fillColor, fillAlpha);
color = max(color, fill);
}
// Bright edge line at the wave boundary
float edgeThickness = ubuf.waveThickness * 0.025;
float distToEdge = abs(d - waveRadius);
if (distToEdge < edgeThickness) {
float edgeFactor = 1.0 - smoothstep(0.0, edgeThickness, distToEdge);
vec3 edgeColor = ubuf.secondaryColor.rgb * (1.2 + smoothedAudio * 0.5);
// Add highlight at peaks
if (smoothedAudio > 0.5) {
edgeColor += vec3(0.3, 0.4, 0.5) * (smoothedAudio - 0.5);
}
vec4 edge = vec4(edgeColor, edgeFactor);
color = max(color, edge);
}
}
return color;
}
// Compute visualization color at given UV coordinates
// Returns vec4 with RGB color and alpha
vec4 computeVisualization(vec2 uv, float iTime, float bass, float mid, float highMid, float treble) {
float aspect = ubuf.itemWidth / ubuf.itemHeight;
vec2 centered = (uv - 0.5) * 2.0;
centered.x *= aspect;
float theta = atan(centered.y, centered.x);
float d = length(centered);
float innerRadius = ubuf.innerDiameter / 2.0;
float baseRadius = 0.35; // Fixed reference for outer extent
vec4 color = vec4(0.0);
// RING SYSTEM
if (hasRings()) {
// Center Waves
if (d < innerRadius * 0.6) {
float wave = mid * 0.8;
float ripple = sin(d * 25.0 + wave * 15.0 - iTime * 2.0);
if (ripple > 0.7) {
float intensity = clamp(mid * 0.6, 0.0, 0.4);
vec4 waveColor = ubuf.secondaryColor * intensity * ubuf.ringOpacity;
color = max(color, waveColor);
}
}
// Energy Ring
float energyRad = innerRadius * 0.65;
float energyThickness = 0.015 + clamp(highMid * 0.02, 0.0, 0.03);
if (d > energyRad - energyThickness && d < energyRad + energyThickness) {
float segmentAngle = theta * 8.0 + highMid * 3.0 + iTime;
if (mod(segmentAngle, 1.0) < 0.6) {
float alpha = clamp(highMid * 2.0, 0.3, 1.0) * ubuf.ringOpacity;
vec4 energyColor = mix(ubuf.primaryColor, ubuf.secondaryColor, 0.5) * alpha;
color = max(color, energyColor);
}
}
// Particle Ring
float particleRad = innerRadius * 0.75;
if (d > particleRad - 0.02 && d < particleRad + 0.02) {
float particleAngle = theta + treble * 2.0 + iTime * 0.5;
float particleSpacing = TWOPI / 16.0;
if (mod(particleAngle, particleSpacing) < 0.15) {
float brightness = 0.5 + clamp(treble * 1.5, 0.0, 0.5);
vec4 particleColor = ubuf.secondaryColor * brightness * ubuf.ringOpacity;
color = max(color, particleColor);
}
}
// Tech Grid Ring
float gridRad = innerRadius * 0.85;
if (d > gridRad - 0.012 && d < gridRad + 0.012) {
float gridAngle = theta + iTime * ubuf.rotationSpeed;
float gridDensity = 0.08 + clamp(mid * 0.05, 0.0, 0.1);
if (mod(gridAngle, 0.2) < gridDensity) {
vec4 gridColor = ubuf.primaryColor * 0.7 * ubuf.ringOpacity;
gridColor.rgb += vec3(0.1, 0.15, 0.2) * clamp(mid, 0.0, 0.8);
color = max(color, gridColor);
}
}
// Accent Ring
float accentRad = innerRadius * 0.92;
float pulse = clamp(bass * 0.08, 0.0, 0.05);
if (d > accentRad - pulse - 0.008 && d < accentRad + pulse + 0.015) {
float colorShift = clamp(bass * 0.5, 0.0, 1.0);
vec4 ringColor = mix(ubuf.secondaryColor * 0.7, ubuf.primaryColor, colorShift);
ringColor.a = ubuf.ringOpacity;
ringColor.rgb *= 1.0 + bass * 0.3;
color = max(color, ringColor);
}
// Outer Ring
float outerRad = innerRadius + bass * 0.05;
if (d > outerRad - 0.008 && d < outerRad + 0.008) {
vec4 outerColor = ubuf.primaryColor * ubuf.ringOpacity;
outerColor.rgb += vec3(0.2, 0.3, 0.4) * clamp(treble * 0.5, 0.0, 0.3);
outerColor.rgb *= 1.0 + bass * 0.4;
color = max(color, outerColor);
}
}
// CIRCULAR AUDIO BARS (64 bars, mirrored from 32 audio samples)
if (hasBars() && d > innerRadius) {
// Double the visual bars by using NBARS * 2
float section = TWOPI / float(NBARS * 2);
float center = section / 2.0;
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
float m = mod(adjustedTheta, section);
float ym = d * sin(center - m);
float barW = ubuf.barWidth * 0.015;
if (abs(ym) < barW) {
// Calculate position in the circle (0.0 to 1.0)
float circlePos = mod(adjustedTheta, TWOPI) / TWOPI;
// Mirror: first half (0-0.5) maps to 0-1, second half (0.5-1) maps back 1-0
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
float v = smoothAudio(mirroredPos);
float wave = sin(theta * 3.0 + mid * 5.0) * clamp(mid * 0.03, 0.0, 0.05);
v += wave;
v = max(v, 0.0);
float barStart = innerRadius;
float barEnd = baseRadius + v * 0.5; // Fixed outer extent
if (d >= barStart && d <= barEnd) {
float heightFactor = (d - barStart) / max(barEnd - barStart, 0.001);
vec3 bottomColor = ubuf.primaryColor.rgb * 0.6;
vec3 middleColor = ubuf.primaryColor.rgb;
vec3 topColor = ubuf.secondaryColor.rgb;
vec3 barColor;
if (heightFactor < 0.5) {
barColor = mix(bottomColor, middleColor, heightFactor * 2.0);
} else {
barColor = mix(middleColor, topColor, (heightFactor - 0.5) * 2.0);
}
barColor *= 1.0 + bass * 0.4;
if (heightFactor > 0.85) {
barColor += vec3(0.3, 0.4, 0.5) * clamp(treble * 0.8, 0.0, 0.5);
}
float edgeFactor = 1.0 - smoothstep(barW * 0.7, barW, abs(ym));
vec4 finalBarColor = vec4(barColor, edgeFactor);
color = max(color, finalBarColor);
}
}
}
return color;
}
void main() {
// ============================================
// DYNAMIC CONTENT SCALING
// ============================================
// Calculate max possible extent from current settings to guarantee
// nothing ever reaches the widget border.
//
// Max visualization radius in centered space [-1,1]:
// Bars/Wave: baseRadius(0.35) + sensitivity * 0.5
// Rings: innerDiameter/2 + sensitivity * 0.05 (always smaller)
//
// Bloom reach: exp(-dist * minDecayRate / bloomIntensity) decays to
// < 1/255 at dist ≈ bloomIntensity * 1.0 (from solving exp decay
// with worst-case amplitude * multiplier chain)
//
// contentScale maps the widget edge to ±contentScale in centered space,
// so setting it to maxTotalRadius ensures everything fits.
float maxContentRadius = 0.35 + ubuf.sensitivity * 0.5;
float maxBloomReach = ubuf.bloomIntensity * 1.0;
float maxTotalRadius = maxContentRadius + maxBloomReach;
float contentScale = max(maxTotalRadius * 1.05, 1.0); // 5% safety margin
vec2 uv = (qt_TexCoord0 - 0.5) * contentScale + 0.5;
// Convert linear time (0-3600) to smooth oscillation for seamless looping
// sin() ensures perfect continuity when QML wraps from 3600 back to 0
float iTime = sin(ubuf.time * TWOPI / 3600.0) * 1800.0 + 1800.0;
// Frequency analysis
float bass = getBass();
float mid = getMid();
float highMid = getHighMid();
float treble = getTreble();
// Get base visualization color based on mode
// Mode 0: bars only, Mode 1: wave only, Mode 2: rings only
// Mode 3: bars+rings, Mode 4: wave+rings, Mode 5: all
vec4 color;
if (hasWave() && !hasBars()) {
// Wave only or wave+rings (modes 1, 4)
color = computePolarWave(uv, iTime, bass, mid, highMid, treble);
} else if (hasBars() && !hasWave()) {
// Bars only or bars+rings (modes 0, 3)
color = computeVisualization(uv, iTime, bass, mid, highMid, treble);
} else if (hasWave() && hasBars()) {
// All mode (5) - combine both
vec4 barsColor = computeVisualization(uv, iTime, bass, mid, highMid, treble);
vec4 waveColor = computePolarWave(uv, iTime, bass, mid, highMid, treble);
color = max(barsColor, waveColor);
} else {
// Rings only (mode 2) - still need to call one of them for ring rendering
color = computeVisualization(uv, iTime, bass, mid, highMid, treble);
}
// ============================================
// BLOOM EFFECT - Glow based on distance to geometry
// ============================================
if (ubuf.bloomIntensity > 0.01 && color.a < 0.01) {
// Only apply bloom where there's no geometry (empty space)
// Find distance to nearest bright element
float aspect = ubuf.itemWidth / ubuf.itemHeight;
vec2 centered = (uv - 0.5) * 2.0;
centered.x *= aspect;
float d = length(centered);
float theta = atan(centered.y, centered.x);
float innerRadius = ubuf.innerDiameter / 2.0;
float baseRadius = 0.35; // Fixed reference for outer extent
float glowAmount = 0.0;
vec3 glowColor = vec3(0.0);
// Glow from rings (if enabled)
if (hasRings()) {
// Outer ring glow
float outerRad = innerRadius + bass * 0.05;
float ringDist = abs(d - outerRad);
float ringGlow = exp(-ringDist * 8.0 / ubuf.bloomIntensity) * (1.0 + bass * 0.5);
glowColor += ubuf.primaryColor.rgb * ringGlow;
glowAmount = max(glowAmount, ringGlow);
// Accent ring glow
float accentRad = innerRadius * 0.92;
float accentDist = abs(d - accentRad);
float accentGlow = exp(-accentDist * 10.0 / ubuf.bloomIntensity) * (0.7 + bass * 0.3);
glowColor += mix(ubuf.secondaryColor.rgb, ubuf.primaryColor.rgb, 0.5) * accentGlow;
glowAmount = max(glowAmount, accentGlow);
}
// Glow from visualization (bars or polar wave)
if ((hasBars() || hasWave()) && d > innerRadius * 0.8) {
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
float circlePos = mod(adjustedTheta, TWOPI) / TWOPI;
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
float v = smoothAudio(mirroredPos);
if (hasWave()) {
// Polar wave bloom - Catmull-Rom spline with mirroring matching main render
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
float bandPos = mirroredPos * float(NBARS - 1);
float fband1 = floor(bandPos);
float fband0 = max(fband1 - 1.0, 0.0);
float fband2 = min(fband1 + 1.0, float(NBARS - 1));
float fband3 = min(fband1 + 2.0, float(NBARS - 1));
float t = fract(bandPos);
float p0 = getAudio(fband0 / float(NBARS - 1)) * ubuf.sensitivity;
float p1 = getAudio(fband1 / float(NBARS - 1)) * ubuf.sensitivity;
float p2 = getAudio(fband2 / float(NBARS - 1)) * ubuf.sensitivity;
float p3 = getAudio(fband3 / float(NBARS - 1)) * ubuf.sensitivity;
float t2 = t * t;
float t3 = t2 * t;
float smoothedAudio = 0.5 * (
(2.0 * p1) +
(-p0 + p2) * t +
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
);
smoothedAudio = max(smoothedAudio, 0.0);
float waveRadius = baseRadius + smoothedAudio * 0.5;
// Glow from the filled area and edge
float distToWave = abs(d - waveRadius);
float waveGlow = exp(-distToWave * 8.0 / ubuf.bloomIntensity) * smoothedAudio * 2.5;
vec3 waveGlowColor = mix(ubuf.primaryColor.rgb, ubuf.secondaryColor.rgb, smoothedAudio);
glowColor += waveGlowColor * waveGlow;
glowAmount = max(glowAmount, waveGlow);
}
if (hasBars()) {
// Bars bloom
float section = TWOPI / float(NBARS * 2);
float m = mod(adjustedTheta, section);
float center = section / 2.0;
float barAngleDist = min(abs(m - center), section - abs(m - center));
float barEnd = baseRadius + v * 0.5; // Fixed outer extent
float radialDist = 0.0;
if (d < innerRadius) {
radialDist = innerRadius - d;
} else if (d > barEnd) {
radialDist = d - barEnd;
}
float totalDist = length(vec2(barAngleDist * d, radialDist));
float barGlow = exp(-totalDist * 15.0 / ubuf.bloomIntensity) * v * 2.0;
float heightFactor = clamp((d - innerRadius) / max(barEnd - innerRadius, 0.001), 0.0, 1.0);
vec3 barGlowColor = mix(ubuf.primaryColor.rgb, ubuf.secondaryColor.rgb, heightFactor);
glowColor += barGlowColor * barGlow;
glowAmount = max(glowAmount, barGlow);
}
}
// Apply bloom
float bloomMult = ubuf.bloomIntensity * (1.0 + bass * 0.5);
color.rgb = glowColor * bloomMult;
color.a = glowAmount * bloomMult * 0.6;
// Clamp to reasonable values
color.rgb = min(color.rgb, vec3(1.5));
color.a = min(color.a, 0.8);
}
// ============================================
// EDGE FADE - radial falloff that only affects the bloom zone
// ============================================
// Fade starts just past where main content ends (maxContentRadius)
// and reaches zero at the widget edge (contentScale) in centered space.
// This catches bloom tails without affecting the main visualization.
vec2 fromCenter = (qt_TexCoord0 - 0.5) * 2.0; // -1 to 1 in widget space
float edgeProximity = max(abs(fromCenter.x), abs(fromCenter.y)); // box distance
float fadeStart = maxContentRadius / contentScale; // where content ends in widget space
float edgeFade = 1.0 - smoothstep(fadeStart, 1.0, edgeProximity);
color *= edgeFade;
// ============================================
// CORNER MASKING
// ============================================
vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight);
vec2 centerPos = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5;
vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5;
float dist = roundedBoxSDF(centerPos, halfSize, ubuf.cornerRadius);
float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist);
// Final output with premultiplied alpha
float finalAlpha = color.a * ubuf.qt_Opacity * cornerMask;
fragColor = vec4(color.rgb * finalAlpha, finalAlpha);
}