Files
komplex/package/contents/ui/KomplexModel.qml
2025-08-15 09:33:12 -04:00

254 lines
10 KiB
QML

/*
* Komplex Wallpaper Engine
* Copyright (C) 2025 @DigitalArtifex | github.com/DigitalArtifex
*
* WallpaperModel.qml
*
* This component provides a model for parsing and managing wallpaper configurations from
* a JSON file, specifically designed for the Komplex Wallpaper Engine.
*
* This enables infinite complexity in shader configurations
*
* 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 <https://www.gnu.org/licenses/>
*/
import QtCore
import QtQuick
import com.github.digitalartifex.komplex 1.0 as Komplex
Item
{
property var screenGeometry
property real pixelRatio: 1 //This will (hopefully) be set to PlasmaCore.Units.devicePixelRatio in onCompleted
property vector3d iResolution: Qt.vector3d(wallpaper.configuration.resolution_x ? wallpaper.configuration.resolution_x : 1920, wallpaper.configuration.resolution_y ? wallpaper.configuration.resolution_y : 1080, 1)//width, height, pixel aspect ratio
property real iTime: 0 //used by most motion shaders
property real iTimeDelta: iTime
property var iChannelTime: [iTime, iTime, iTime, iTime] //individual channel time values
property real iSampleRate: 44100 //used by audio shaders
property int iFrame: 0
property real iFrameRate: wallpaper.configuration.framerate_limit ? wallpaper.configuration.framerate_limit : 60 // Default frame rate for the shader
property vector4d iMouse
property var iDate
property bool running: true
// Individual channel resolutions to customize performance and quality
property var iChannelResolution: [Qt.vector3d(wallpaper.configuration.iChannel0_resolution_x, wallpaper.configuration.iChannel0_resolution_y, pixelRatio),
Qt.vector3d(wallpaper.configuration.iChannel1_resolution_x, wallpaper.configuration.iChannel1_resolution_y, pixelRatio),
Qt.vector3d(wallpaper.configuration.iChannel2_resolution_x, wallpaper.configuration.iChannel2_resolution_y, pixelRatio),
Qt.vector3d(wallpaper.configuration.iChannel3_resolution_x, wallpaper.configuration.iChannel3_resolution_y, pixelRatio)]
property string iChannel0: ""
property string iChannel1: ""
property string iChannel2: ""
property string iChannel3: ""
id: mainItem
Item
{
id: data
property var channels: []
}
Komplex.ShaderPackModel
{
id: shaderPackModel
onJsonChanged: () =>
{
// clean up old channels
while(data.channels.length > 0)
data.channels.pop().destroy()
// Handle the JSON change if needed
mainItem.parsePack(shaderPackModel.json);
}
}
// The WindowModel is used to manage the interaction with the desktop environment
WindowModel
{
id: windowModel
screenGeometry: mainItem.screenGeometry
}
Rectangle
{
anchors.fill: parent
color: "black"
// The output channel that combines all the input channels and displays the final shader output
// This channel must be set to a shader source file that has been pre-compiled to a QSB Fragment Shader
ShaderChannel
{
iTime: mainItem.iTime
iMouse: mainItem.iMouse
iResolution: mainItem.iResolution
id: channelOutput
anchors.fill: parent
type: ShaderChannel.Type.ShaderChannel
visible: true // Set to true to display the output
z: 9000
}
// To save on performance, just use one timer for all channels and shaders
// they will need to be bound to the mainItem properties they need to access
Timer
{
id: channelTimer
//Not entirely sure if this will actually limit the frame rate
interval: (1 / mainItem.iFrameRate) * 1000 //fps to ms cycles :: fps = 60 = 1 / 60 = 0.01666 * 1000 = 16
repeat: true
running: true //wallpaper.configuration.running ? mainItem.running : true
triggeredOnStart: true
onTriggered:
{
var date = new Date();
var startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
var secondsSinceMidnight = (date - startOfDay) / 1000;
mainItem.iTime += (interval / 1000) * (wallpaper.configuration.shaderSpeed ? wallpaper.configuration.shaderSpeed : 1.0)
mainItem.iDate = Qt.vector4d(date.getFullYear(), date.getMonth() + 1, date.getDate(), secondsSinceMidnight)
mainItem.iFrame += 1
}
}
}
// Load the default shader pack configuration on component completion
Component.onCompleted:
{
if(wallpaper.configuration.shader_package)
shaderPackModel.loadJson(wallpaper.configuration.shader_package);
else
shaderPackModel.loadJson("/home/parametheus/kde/src/komplex/pack.json"); // Load a default shader pack if none is specified
}
// Function to parse the pack.json file and set the properties of the ShaderChannel
function parsePack(json)
{
var pack = JSON.parse(json);
var currentChannel = null;
if (pack.channel0)
channelOutput.iChannel0 = parseChannel(pack.channel0, channelOutput);
if (pack.channel1)
channelOutput.iChannel1 = parseChannel(pack.channel1, channelOutput);
if (pack.channel2)
channelOutput.iChannel2 = parseChannel(pack.channel2, channelOutput);
if (pack.channel3)
channelOutput.iChannel3 = parseChannel(pack.channel3, channelOutput);
channelOutput.source = getFilePath(pack.source); // Set the shader source file
channelOutput.type = ShaderChannel.Type.ShaderChannel; // Set the shader
if(pack.mouse_scale)
channelOutput.mouseBias = pack.mouse_scale
if(pack.speed)
channelOutput.iTimeScale = pack.speed
}
// Recursive helper function to parse channels
function parseChannel(channel, parent)
{
if (!channel) return;
var source = getFilePath(channel.source)
// Qt.createQmlObject() method was not working the same inside plasma
// as it was inside a QMLEngine instance. This resulted in the Loader
// object being undefined and thus breaking the Komplex wallpaper mode. (oops)
// Instead, use Qt.createComponent() then manually setup bindings
var component = Qt.createComponent("./ShaderChannel.qml")
var result
if (component.status === Component.Ready) {
result = component.createObject(mainItem, { x: 100, y: 100 });
}
result.frameBufferChannel = channel.frameBufferChannel !== undefined ? channel.frameBufferChannel : -1
result.type = channel.type
result.anchors.fill = mainItem
result.visible = false
result.iMouse = Qt.binding(() => { return mainItem.iMouse; })
result.iTime = Qt.binding(() => { return mainItem.iTime; })
result.iResolution = Qt.binding(() => { return Qt.vector3d(channel.resolution_x || mainItem.width, channel.resolution_y || mainItem.height, 1.0); })
result.mouseBias = channel.mouse_scale ? channel.mouse_scale : 1.0
result.iTimeScale = channel.time_scale ? channel.time_scale : 1.0
result.iTimeDelta = Qt.binding(() => { return mainItem.iTimeDelta; })
result.iChannelResolution = Qt.binding(() => {
return [
Qt.vector3d(channel.resolution_x || mainItem.width, channel.resolution_y || mainItem.height, 1.0),
Qt.vector3d(channel.resolution_x || mainItem.width, channel.resolution_y || mainItem.height, 1.0),
Qt.vector3d(channel.resolution_x || mainItem.width, channel.resolution_y || mainItem.height, 1.0),
Qt.vector3d(channel.resolution_x || mainItem.width, channel.resolution_y || mainItem.height, 1.0)
];
});
result.iChannelTime = Qt.binding(() => {
return [
mainItem.iTime * result.iTimeScale,
mainItem.iTime * result.iTimeScale,
mainItem.iTime * result.iTimeScale,
mainItem.iTime * result.iTimeScale
];
});
result.iFrameRate = Qt.binding(() => { return mainItem.iFrameRate; })
result.iFrame = Qt.binding(() => { return mainItem.iFrame; })
result.invert = channel.invert ? channel.invert : false
result.source = source
if (channel.channel0)
result.iChannel0 = parseChannel(channel.channel0, result);
if (channel.channel1)
result.iChannel1 = parseChannel(channel.channel1, result);
if (channel.channel2)
result.iChannel2 = parseChannel(channel.channel2, result);
if (channel.channel3)
result.iChannel3 = parseChannel(channel.channel3, result);
data.channels.push(result) // save for destroying
return result;
}
function getFilePath(source)
{
if(source === undefined || source === "")
return "";
// Ensure the source path is correctly resolved for relative paths
if(source.startsWith("./"))
{
var temp = source.replace("./", "file://" + shaderPackModel.shaderPackPath + "/")
return temp;
}
else if(source.startsWith("$/"))
{
var temp = source.replace("$/", "file://" + StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/komplex/")
return temp;
}
else if(!source.startsWith("file://"))
return "file://" + source;
}
}