Skip to content

Instantly share code, notes, and snippets.

@namikiri
Created January 10, 2026 12:40
Show Gist options
  • Select an option

  • Save namikiri/84b092891c2717ca70402054968c3b15 to your computer and use it in GitHub Desktop.

Select an option

Save namikiri/84b092891c2717ca70402054968c3b15 to your computer and use it in GitHub Desktop.
Pixel sorting effect for Shotcut
import QtQuick
import org.shotcut.qml
Metadata {
type: Metadata.Filter
name: qsTr("Pixels0rt")
keywords: qsTr('glitch pixelsort', 'search keywords for the video filter') + ' pixels0rt'
mlt_service: "frei0r.pixels0rt"
qml: "ui.qml"
icon: 'icon.webp'
keyframes {
allowAnimateIn: true
allowAnimateOut: true
simpleProperties: ['0', '1', '2', '3']
parameters: [
Parameter {
name: qsTr('Threshold')
property: '0'
isCurve: true
minimum: 0
maximum: 1
},
Parameter {
name: qsTr('Direction')
property: '1'
isCurve: false
minimum: 0
maximum: 1
},
Parameter {
name: qsTr('Reversed')
property: '2'
isCurve: false
minimum: 0
maximum: 1
},
Parameter {
name: qsTr('Transparency threshold')
property: '3'
isCurve: true
minimum: 0
maximum: 1
}
]
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Shotcut.Controls as Shotcut
Shotcut.KeyframableFilter {
property string threshold: '0'
property string direction: '1'
property string reversed: '2'
property string alphaThr: '3'
property double thresholdDefault: 0.4
property double directionDefault: 0
property double reversedDefault: 0
property double alphaThrDefault: 1.0
function setControls() {
var position = getPosition();
blockUpdate = true;
thresholdSlider.value = filter.getDouble(threshold, position) * thresholdSlider.maximumValue;
thresholdKeyframesButton.checked = filter.animateIn <= 0 && filter.animateOut <= 0 && filter.keyframeCount(threshold) > 0;
alphaThrSlider.value = filter.getDouble(alphaThr, position) * alphaThrSlider.maximumValue;
alphaThrKeyframesButton.checked = filter.animateIn <= 0 && filter.animateOut <= 0 && filter.keyframeCount(alphaThr) > 0;
blockUpdate = false;
enableControls(isSimpleKeyframesActive());
}
function enableControls(enabled) {
thresholdSlider.enabled = enabled;
directionCombo.enabled = enabled;
reversedCheckbox.enabled = enabled;
alphaThrSlider.enabled = enabled;
}
function updateSimpleKeyframes() {
setControls();
updateFilter(threshold, thresholdSlider.value / thresholdSlider.maximumValue, thresholdKeyframesButton, null);
updateFilter(alphaThr, alphaThrSlider.value / alphaThrSlider.maximumValue, alphaThrKeyframesButton, null);
}
keyframableParameters: [threshold, alphaThr]
startValues: [0.5]
middleValues: [thresholdDefault, alphaThrDefault]
endValues: [0.5]
width: 350
height: 100
Component.onCompleted: {
if (filter.isNew) {
filter.set(threshold, thresholdDefault);
filter.set(direction, directionDefault / 4);
filter.set(reversed, reversedDefault);
filter.set(alphaThr, alphaThrDefault);
filter.savePreset(preset.parameters);
}
setControls();
}
GridLayout {
anchors.fill: parent
anchors.margins: 8
columns: 4
Label {
text: qsTr('Preset')
Layout.alignment: Qt.AlignRight
}
Shotcut.Preset {
id: preset
parameters: [threshold, direction]
Layout.columnSpan: 3
onBeforePresetLoaded: resetSimpleKeyframes()
onPresetSelected: {
setControls();
initializeSimpleKeyframes();
}
}
Label {
text: qsTr('Threshold')
Layout.alignment: Qt.AlignRight
}
Shotcut.SliderSpinner {
id: thresholdSlider
minimumValue: 0
maximumValue: 100
stepSize: 0.1
decimals: 1
suffix: ' %'
onValueChanged: updateFilter(threshold, value / maximumValue, thresholdKeyframesButton, getPosition())
}
Shotcut.UndoButton {
onClicked: thresholdSlider.value = thresholdDefault * thresholdSlider.maximumValue
}
Shotcut.KeyframesButton {
id: thresholdKeyframesButton
onToggled: {
enableControls(true);
toggleKeyframes(checked, threshold, thresholdSlider.value / thresholdSlider.maximumValue);
}
}
Label {
text: qsTr('Direction')
Layout.alignment: Qt.AlignRight
}
Shotcut.ComboBox {
id: directionCombo
Layout.columnSpan: 2
Layout.fillWidth: true
function setCurrentValue(value) {
var modelIndex = indexOfValue(value);
if (modelIndex >= 0) {
currentIndex = modelIndex;
} else {
console.log("Invalid value for direction", value);
currentIndex = 0;
}
}
onCurrentIndexChanged: filter.set(direction, directionModel.get(currentIndex).value / 4)
model: ListModel {
id: directionModel
ListElement {
text: qsTr('Top to bottom')
value: 0
}
ListElement {
text: qsTr('Bottom to top')
value: 1
}
ListElement {
text: qsTr('Left to right')
value: 2
}
ListElement {
text: qsTr('Right to left')
value: 3
}
}
textRole: "text"
valueRole: "value"
}
Shotcut.UndoButton {
onClicked: directionCombo.currentIndex = directionDefault
}
Item {
width: 1
}
CheckBox {
id: reversedCheckbox
Layout.columnSpan: 2
text: qsTr('Reversed')
onCheckedChanged: filter.set(reversed, checked ? 1.0 : 0.0)
}
Shotcut.UndoButton {
onClicked: reversedCheckbox.checked = false
}
Label {
text: qsTr('Transparency threshold')
Layout.alignment: Qt.AlignRight
}
Shotcut.SliderSpinner {
id: alphaThrSlider
minimumValue: 0
maximumValue: 100
stepSize: 0.1
decimals: 1
suffix: ' %'
onValueChanged: updateFilter(alphaThr, value / maximumValue, alphaThrKeyframesButton, getPosition())
}
Shotcut.UndoButton {
onClicked: alphaThrSlider.value = alphaThrDefault * alphaThrSlider.maximumValue
}
Shotcut.KeyframesButton {
id: alphaThrKeyframesButton
onToggled: {
enableControls(true);
toggleKeyframes(checked, alphaThr, alphaThrSlider.value / alphaThrSlider.maximumValue);
}
}
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
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment