6.8 KiB · text History c2e4eb1
// 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" (moire over halftone), "bokeh",
    // "stars", or "alternate": fading between fadeFrom and fadeTo, which
    // are any of the others, every few minutes.
    property string pattern: "both"
    property string fadeFrom: "moire"
    property string fadeTo: "halftone"
    property int bokehLights: 14
    // Now and then, a few large defocused discs drifting past in front.
    property bool foreground: false
    property bool glints: false
    property bool breathing: false
    property bool grain: false
    property bool dither: true

    // Counts frames, to move the grain.
    property int frame: 0
    // Multiplies how fast everything drifts; 0 holds the picture still.
    property real speed: 1
    property int frameRate: 30
    // "NEIAM" in the bottom right, in the ink colour.
    property bool wordmark: false

    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

    // How much of each layer a pattern shows.
    function layers(name) {
        return {
            moire: name === "moire" || name === "both" ? 1 : 0,
            halftone: name === "halftone" || name === "both" ? 1 : 0,
            bokeh: name === "bokeh" ? 1 : 0,
            stars: name === "stars" ? 1 : 0,
        }
    }

    function amount(layer) {
        if (pattern !== "alternate")
            return layers(pattern)[layer]
        var t = 0.5 + 0.5 * sweep(360, 0)
        return layers(fadeFrom)[layer] * (1 - t) + layers(fadeTo)[layer] * t
    }

    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)
        // Breathing swells the pattern's reach and strength together over
        // sixteen seconds, eased in and out by breathe.
        property real breathe: field.breathing ? 1 : 0
        Behavior on breathe { NumberAnimation { duration: 1500 } }
        property real breath: 1 - 0.2 * breathe * (0.5 - 0.5 * field.sweep(16, 0))
        property real reach: 0.95 + 0.06 * field.sweep(43, 0.7) + 0.08 * breathe * field.sweep(16, 0)
        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)

        // One bokeh cycle is ten minutes; the fastest disc climbs the
        // screen three times in it.
        property real drift: (field.clock / 600) % 1

        // Amounts ease so a pattern change fades rather than cuts; while
        // alternating they are already continuous, so easing would lag.
        property real moire: field.amount("moire")
        property real halftone: field.amount("halftone")
        property real bokeh: field.amount("bokeh")
        property real stars: field.amount("stars")
        property real lights: field.bokehLights
        property real flare: field.foreground ? 1 : 0
        Behavior on flare { NumberAnimation { duration: 1500 } }

        property real twinkle: (field.clock / 12) % 1
        property real glints: field.glints ? 1 : 0
        property real glintPhase: (field.clock / 90) % 1
        property real grain: field.grain ? 1 : 0
        property real dither: field.dither ? 1 : 0
        property real seed: (field.frame * 0.618034) % 1
        Behavior on glints { NumberAnimation { duration: 1500 } }
        Behavior on grain { NumberAnimation { duration: 1500 } }
        Behavior on moire { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } }
        Behavior on halftone { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } }
        Behavior on bokeh { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } }
        Behavior on stars { 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
    }

    // B612 Mono comes from fonts-b612 / firstyear-b612-fonts, which the
    // package depends on.
    Text {
        visible: field.wordmark
        anchors { right: parent.right; bottom: parent.bottom; margins: field.height * 0.04 }
        text: "NEIAM"
        color: field.ink
        font { family: "B612 Mono"; bold: true; pixelSize: Math.max(1, Math.round(field.height * 0.12)) }
        // Sit the capitals, not the line box, on the margin.
        anchors.bottomMargin: field.height * 0.04 - fontMetrics.descent
        FontMetrics { id: fontMetrics; font: parent.font }
    }

    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
            field.frame = (field.frame + 1) % 100000
        }
    }
}