diff --git a/src/qml/filters/vibrance/icon.webp b/src/qml/filters/vibrance/icon.webp
new file mode 100644
index 0000000000..89e5c0d410
Binary files /dev/null and b/src/qml/filters/vibrance/icon.webp differ
diff --git a/src/qml/filters/vibrance/meta.qml b/src/qml/filters/vibrance/meta.qml
new file mode 100644
index 0000000000..c51032e96d
--- /dev/null
+++ b/src/qml/filters/vibrance/meta.qml
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2024 Meltytech, LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+import QtQuick
+import org.shotcut.qml
+
+Metadata {
+ type: Metadata.Filter
+ name: qsTr('Vibrance')
+ keywords: qsTr('color intensity saturation vibe', 'search keywords for the Vibrance video filter') + ' vibrance'
+ mlt_service: 'avfilter.vibrance'
+ qml: 'ui.qml'
+ icon: 'icon.webp'
+
+ keyframes {
+ allowAnimateIn: true
+ allowAnimateOut: true
+ simpleProperties: ['av.intensity', 'av.rbal', 'av.gbal', 'av.bbal']
+ parameters: [
+ Parameter {
+ name: qsTr('Intensity')
+ property: 'av.intensity'
+ isCurve: true
+ minimum: -2
+ maximum: 2
+ },
+ Parameter {
+ name: qsTr('Red')
+ property: 'av.rbal'
+ isCurve: true
+ minimum: -10
+ maximum: 10
+ },
+ Parameter {
+ name: qsTr('Green')
+ property: 'av.gbal'
+ isCurve: true
+ minimum: -10
+ maximum: 10
+ },
+ Parameter {
+ name: qsTr('Blue')
+ property: 'av.bbal'
+ isCurve: true
+ minimum: -10
+ maximum: 10
+ }
+ ]
+ }
+}
diff --git a/src/qml/filters/vibrance/ui.qml b/src/qml/filters/vibrance/ui.qml
new file mode 100644
index 0000000000..139cbd7877
--- /dev/null
+++ b/src/qml/filters/vibrance/ui.qml
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2024 Meltytech, LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Shotcut.Controls as Shotcut
+
+Shotcut.KeyframableFilter {
+ property double intensityDefault: 0
+ property double redBalanceDefault: 1
+ property double greenBalanceDefault: 1
+ property double blueBalanceDefault: 1
+
+ function setControls() {
+ var position = getPosition();
+ blockUpdate = true;
+ intensitySlider.value = filter.getDouble('av.intensity', position) * 50;
+ intensityKeyframesButton.checked = filter.animateIn <= 0 && filter.animateOut <= 0 && filter.keyframeCount('av.intensity') > 0;
+ redBalanceSlider.value = filter.getDouble('av.rbal', position) * 10;
+ redBalanceKeyframesButton.checked = filter.animateIn <= 0 && filter.animateOut <= 0 && filter.keyframeCount('av.rbal') > 0;
+ greenBalanceSlider.value = filter.getDouble('av.gbal', position) * 10;
+ greenBalanceKeyframesButton.checked = filter.animateIn <= 0 && filter.animateOut <= 0 && filter.keyframeCount('av.gbal') > 0;
+ blueBalanceSlider.value = filter.getDouble('av.bbal', position) * 10;
+ blueBalanceKeyframesButton.checked = filter.animateIn <= 0 && filter.animateOut <= 0 && filter.keyframeCount('av.bbal') > 0;
+ blockUpdate = false;
+ enableControls(isSimpleKeyframesActive());
+ }
+
+ function enableControls(enabled) {
+ intensitySlider.enabled = enabled;
+ redBalanceSlider.enabled = enabled;
+ greenBalanceSlider.enabled = enabled;
+ blueBalanceSlider.enabled = enabled;
+ }
+
+ function updateSimpleKeyframes() {
+ setControls();
+ updateFilter('av.intensity', intensitySlider.value / 50, intensityKeyframesButton, null);
+ updateFilter('av.rbal', redBalanceSlider.value / 10, redBalanceKeyframesButton, null);
+ updateFilter('av.gbal', greenBalanceSlider.value / 10, greenBalanceKeyframesButton, null);
+ updateFilter('av.bbal', blueBalanceSlider.value / 10, blueBalanceKeyframesButton, null);
+ }
+
+ keyframableParameters: ['av.intensity', 'av.rbal', 'av.gbal', 'av.bbal']
+ startValues: [intensityDefault, redBalanceDefault, greenBalanceDefault, blueBalanceDefault]
+ middleValues: [intensityDefault, redBalanceDefault, greenBalanceDefault, blueBalanceDefault]
+ endValues: [intensityDefault, redBalanceDefault, greenBalanceDefault, blueBalanceDefault]
+ width: 200
+ height: 150
+ Component.onCompleted: {
+ if (filter.isNew) {
+ filter.set('av.intensity', intensityDefault);
+ filter.set('av.rbal', redBalanceDefault);
+ filter.set('av.gbal', greenBalanceDefault);
+ filter.set('av.bbal', blueBalanceDefault);
+ filter.savePreset(keyframableParameters);
+ }
+ setControls();
+ }
+
+ GridLayout {
+ columns: 4
+ anchors.fill: parent
+ anchors.margins: 8
+
+ Label {
+ text: qsTr('Preset')
+ Layout.alignment: Qt.AlignRight
+ }
+
+ Shotcut.Preset {
+ id: presetItem
+
+ Layout.columnSpan: 3
+ parameters: keyframableParameters
+ onBeforePresetLoaded: resetSimpleKeyframes()
+ onPresetSelected: {
+ setControls();
+ initializeSimpleKeyframes();
+ }
+ }
+
+ Label {
+ text: qsTr('Intensity')
+ Layout.alignment: Qt.AlignRight
+ }
+
+ Shotcut.SliderSpinner {
+ id: intensitySlider
+
+ minimumValue: -100
+ maximumValue: 100
+ suffix: ' %'
+ onValueChanged: updateFilter('av.intensity', value / 50, intensityKeyframesButton, getPosition())
+ }
+
+ Shotcut.UndoButton {
+ onClicked: intensitySlider.value = 0
+ }
+
+ Shotcut.KeyframesButton {
+ id: intensityKeyframesButton
+
+ onToggled: {
+ enableControls(true);
+ toggleKeyframes(checked, 'av.intensity', intensitySlider.value / 50);
+ }
+ }
+
+ Label {
+ text: qsTr('Red')
+ Layout.alignment: Qt.AlignRight
+ }
+
+ Shotcut.SliderSpinner {
+ id: redBalanceSlider
+
+ minimumValue: -100
+ maximumValue: 100
+ suffix: ' %'
+ onValueChanged: updateFilter('av.rbal', value / 10, redBalanceKeyframesButton, getPosition())
+ }
+
+ Shotcut.UndoButton {
+ onClicked: redBalanceSlider.value = 10
+ }
+
+ Shotcut.KeyframesButton {
+ id: redBalanceKeyframesButton
+
+ onToggled: {
+ enableControls(true);
+ toggleKeyframes(checked, 'av.rbal', redBalanceSlider.value / 10);
+ }
+ }
+
+ Label {
+ text: qsTr('Green')
+ Layout.alignment: Qt.AlignRight
+ }
+
+ Shotcut.SliderSpinner {
+ id: greenBalanceSlider
+
+ minimumValue: -100
+ maximumValue: 100
+ suffix: ' %'
+ onValueChanged: updateFilter('av.gbal', value / 10, greenBalanceKeyframesButton, getPosition())
+ }
+
+ Shotcut.UndoButton {
+ onClicked: greenBalanceSlider.value = 10
+ }
+
+ Shotcut.KeyframesButton {
+ id: greenBalanceKeyframesButton
+
+ onToggled: {
+ enableControls(true);
+ toggleKeyframes(checked, 'av.gbal', greenBalanceSlider.value / 10);
+ }
+ }
+
+ Label {
+ text: qsTr('Blue')
+ Layout.alignment: Qt.AlignRight
+ }
+
+ Shotcut.SliderSpinner {
+ id: blueBalanceSlider
+
+ minimumValue: -100
+ maximumValue: 100
+ suffix: ' %'
+ onValueChanged: updateFilter('av.bbal', value / 10, blueBalanceKeyframesButton, getPosition())
+ }
+
+ Shotcut.UndoButton {
+ onClicked: blueBalanceSlider.value = 10
+ }
+
+ Shotcut.KeyframesButton {
+ id: blueBalanceKeyframesButton
+
+ onToggled: {
+ enableControls(true);
+ toggleKeyframes(checked, 'av.bbal', blueBalanceSlider.value / 10);
+ }
+ }
+
+ Item {
+ Layout.fillHeight: true
+ }
+ }
+
+ Connections {
+ function onChanged() {
+ setControls();
+ }
+
+ function onInChanged() {
+ updateSimpleKeyframes();
+ }
+
+ function onOutChanged() {
+ updateSimpleKeyframes();
+ }
+
+ function onAnimateInChanged() {
+ updateSimpleKeyframes();
+ }
+
+ function onAnimateOutChanged() {
+ updateSimpleKeyframes();
+ }
+
+ function onPropertyChanged(name) {
+ setControls();
+ }
+
+ target: filter
+ }
+
+ Connections {
+ function onPositionChanged() {
+ setControls();
+ }
+
+ target: producer
+ }
+}