3.7 KiB · text History f629a67
// The animated picture, independent of Plasma so it can be rendered
// and tested on its own (tools/moire-preview.qml does).
//
// Motion is a handful of slow sines at unrelated periods, so it never
// visibly repeats. It is advanced here in JavaScript, in double
// precision, and handed to the shader as positions and a wrapped
// phase: the shader holds no clock that could lose precision.
import QtQuick 2.15

Item {
    id: field

    // "moire", "halftone", "both", or "alternate" (fading between the
    // two every few minutes).
    property string pattern: "both"
    // Multiplies how fast everything drifts; 0 holds the picture still.
    property real speed: 1
    property int frameRate: 30

    property color base: "#661e1e"
    property color deep: "#470005"
    property color ink: "#fdf2c6"
    property color tone: "#b6797a"

    // Theme changes arrive once a minute at most; ease into them.
    Behavior on base { ColorAnimation { duration: 4000 } }
    Behavior on deep { ColorAnimation { duration: 4000 } }
    Behavior on ink { ColorAnimation { duration: 4000 } }
    Behavior on tone { ColorAnimation { duration: 4000 } }

    // Seconds of motion so far, scaled by speed. A fresh start picks a
    // random point in the motion so two screens don't mirror each other.
    property real clock: Math.random() * 10000

    readonly property real aspect: height > 0 ? width / height : 16 / 9

    function sweep(period, phase) {
        return Math.sin(2 * Math.PI * (clock / period + phase))
    }

    ShaderEffect {
        anchors.fill: parent
        fragmentShader: "field.frag.qsb"

        property size size: Qt.size(field.width, field.height)

        // The hub, where the pattern is strongest, wanders around the
        // right of centre; the ring centres circle it at a breathing
        // distance, which walks the moire's beat around the rings.
        property point hub: Qt.point(0.62 * field.aspect + 0.05 * field.sweep(97, 0),
                                       0.46 + 0.04 * field.sweep(131, 0.3))
        readonly property real spread: 0.03 * (1 + 0.35 * field.sweep(71, 0.1))
        readonly property real turn: 2 * Math.PI * field.clock / 53
        property point ringA: Qt.point(hub.x - spread * Math.cos(turn) / 2,
                                       hub.y - spread * Math.sin(turn) / 2)
        property point ringB: Qt.point(hub.x + spread * Math.cos(turn) / 2,
                                       hub.y + spread * Math.sin(turn) / 2)
        property real reach: 0.95 + 0.06 * field.sweep(43, 0.7)
        property real period: 1 / 110

        property real cell: 1 / 70
        property real angle: 0.26 + 0.03 * field.sweep(211, 0.2)
        property real wave: (2 * Math.PI * field.clock / 16) % (2 * Math.PI)

        // Amounts ease so a pattern change fades rather than cuts.
        readonly property real alternate: 0.5 + 0.5 * field.sweep(360, 0)
        property real moire: field.pattern === "halftone" ? 0
                           : field.pattern === "alternate" ? alternate : 1
        property real halftone: field.pattern === "moire" ? 0
                              : field.pattern === "alternate" ? 1 - alternate : 1
        Behavior on moire { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } }
        Behavior on halftone { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } }

        property color base: field.base
        property color deep: field.deep
        property color ink: field.ink
        property color tone: field.tone
    }

    Timer {
        running: field.speed > 0 && field.visible
        repeat: true
        interval: Math.round(1000 / Math.max(1, field.frameRate))
        onTriggered: field.clock += field.speed * interval / 1000
    }
}