3.6 KiB · text History e43831b
import QtQuick
import org.kde.kirigami as Kirigami

import "Lambent.js" as L

// One place (or device) as a dot. Lit, it is cut into wedges, one per
// preview pixel of whatever is driving it, so a rainbow reads as a rainbow
// rather than averaging out to grey; dark, it is a hollow ring. A clamp
// still walking toward its target pulses a halo.
Item {
    id: circle

    property bool lit: false
    property var pixels: []
    property real level: 1          // 0..1, both clamps multiplied
    property bool walking: false
    property bool highlighted: false

    implicitWidth: Kirigami.Units.iconSizes.small
    implicitHeight: implicitWidth

    // Never draw a lit circle fully black: a dim room should still read
    // as "on" next to one that is off.
    readonly property real shown: 0.3 + 0.7 * Math.max(0, Math.min(1, level))

    onPixelsChanged: canvas.requestPaint()
    onLitChanged: canvas.requestPaint()
    onShownChanged: canvas.requestPaint()

    Rectangle {
        id: halo
        anchors.centerIn: parent
        width: parent.width
        height: width
        radius: width / 2
        color: "transparent"
        border.color: Kirigami.Theme.highlightColor
        border.width: 1
        opacity: 0
        visible: circle.walking

        SequentialAnimation on opacity {
            running: halo.visible
            loops: Animation.Infinite
            NumberAnimation { from: 0.8; to: 0; duration: 1200; easing.type: Easing.OutQuad }
        }
        SequentialAnimation on scale {
            running: halo.visible
            loops: Animation.Infinite
            NumberAnimation { from: 0.7; to: 1.3; duration: 1200; easing.type: Easing.OutQuad }
        }
    }

    Canvas {
        id: canvas
        anchors.centerIn: parent
        width: parent.width * (circle.highlighted ? 0.85 : 0.7)
        height: width

        onWidthChanged: requestPaint()

        Behavior on width { NumberAnimation { duration: Kirigami.Units.shortDuration } }

        onPaint: {
            var ctx = getContext("2d")
            ctx.reset()
            var r = width / 2
            if (r <= 0) return

            if (!circle.lit) {
                var lw = Math.max(1.5, width / 6)
                ctx.beginPath()
                ctx.arc(r, r, r - lw / 2, 0, Math.PI * 2)
                ctx.lineWidth = lw
                ctx.strokeStyle = Kirigami.Theme.disabledTextColor
                ctx.stroke()
                return
            }

            var px = circle.pixels || []
            if (px.length === 0) {
                ctx.beginPath()
                ctx.arc(r, r, r, 0, Math.PI * 2)
                var c = Kirigami.Theme.highlightColor
                ctx.fillStyle = L.cssRgb([c.r * 255, c.g * 255, c.b * 255], circle.shown)
                ctx.fill()
                return
            }

            var step = Math.PI * 2 / px.length
            var start = -Math.PI / 2
            for (var i = 0; i < px.length; i++) {
                ctx.beginPath()
                ctx.moveTo(r, r)
                // a hair of overlap so antialiasing leaves no seams
                ctx.arc(r, r, r, start + i * step, start + (i + 1) * step + 0.02)
                ctx.closePath()
                ctx.fillStyle = L.cssRgb(px[i], circle.shown)
                ctx.fill()
            }

            // a thin rim so a near-black frame still has an edge
            ctx.beginPath()
            ctx.arc(r, r, r - 0.5, 0, Math.PI * 2)
            ctx.lineWidth = 1
            ctx.strokeStyle = Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.35)
            ctx.stroke()
        }
    }
}