aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/sonix/BootlogoCanvas.qml
blob: 309e9ad32658fe61ceb844c2dd9a1365b483ecd2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// 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.

import QtQuick
import "../.."
import "Sonix.js" as S

// The 480x800 boot logo scene: the picture, panned and zoomed by the user,
// and the duotone text. The output is this item, grabbed, so what is seen
// is what the player gets. Scale it for display through a parent, never
// through its own `scale`: grabToImage must see it at 480x800.
Item {
    id: canvas
    width: 480
    height: 800
    clip: true

    property url source: ""
    property var cfg: ({})          // Prefs.bootlogo's shape
    property string text: ""
    property bool moveText: false
    property bool maskOnly: false   // the text alone, for the mask grab
    property var place: ({ x: 0, y: 0, w: 480, h: 800, scale: 1, zoom: 1 })
    readonly property bool ready: photo.status === Image.Ready
    readonly property string loadError: photo.status === Image.Error ? "Cannot read this picture." : ""
    signal textMoved(real x, real y)

    FontLoader { id: fBungee; source: Qt.resolvedUrl("fonts/Bungee-Regular.ttf") }
    FontLoader { id: fInline; source: Qt.resolvedUrl("fonts/BungeeInline-Regular.ttf") }
    FontLoader { id: fMono; source: Qt.resolvedUrl("fonts/MajorMonoDisplay-Regular.ttf") }
    FontLoader { id: fMich; source: Qt.resolvedUrl("fonts/Michroma-Regular.ttf") }
    FontLoader { id: fAudio; source: Qt.resolvedUrl("fonts/Audiowide-Regular.ttf") }
    readonly property var families: ({ bungee: fBungee.name, mono: fMono.name,
                                       michroma: fMich.name, audiowide: fAudio.name })

    function colour(t) { return S.colour(t, Theme.accent); }
    // Tests photo.status, not ready: onStatusChanged can run before the
    // ready binding has caught up, and the picture would stay stretched.
    function recentre() {
        if (photo.status === Image.Ready) canvas.place = S.centred(photo.implicitWidth, photo.implicitHeight, canvas.width, canvas.height);
    }

    Rectangle { anchors.fill: parent; color: "black"; visible: !canvas.maskOnly }

    Image {
        id: photo
        source: canvas.source
        asynchronous: true
        smooth: true
        mipmap: true
        visible: !canvas.maskOnly
        x: canvas.place.x
        y: canvas.place.y
        width: canvas.place.w
        height: canvas.place.h
        onStatusChanged: if (status === Image.Ready) canvas.recentre()
    }

    Item {
        id: label
        readonly property string fam: canvas.families[canvas.cfg.font] || canvas.families.bungee
        readonly property bool layered: canvas.cfg.font === "bungee"
        readonly property var off: S.offsetXY(canvas.cfg.dir, canvas.cfg.offset)
        width: fillText.implicitWidth
        height: fillText.implicitHeight
        x: canvas.cfg.x - width / 2
        y: canvas.cfg.y - height / 2

        // The second colour: an offset copy behind, or Bungee Inline on top.
        Text {
            visible: !label.layered
            x: label.off.x
            y: label.off.y
            text: canvas.text
            font { family: label.fam; pixelSize: canvas.cfg.size }
            color: canvas.colour(canvas.cfg.second)
        }
        Text {
            id: fillText
            text: canvas.text
            font { family: label.fam; pixelSize: canvas.cfg.size }
            color: canvas.colour(canvas.cfg.fill)
        }
        Text {
            visible: label.layered
            text: canvas.text
            font { family: fInline.name; pixelSize: canvas.cfg.size }
            color: canvas.colour(canvas.cfg.second)
        }
    }

    // Drag pans the picture or moves the text; the wheel zooms the picture
    // at the cursor. Coordinates are the canvas's own, unscaled.
    MouseArea {
        anchors.fill: parent
        preventStealing: true
        cursorShape: Qt.SizeAllCursor
        property real lx: 0
        property real ly: 0
        onPressed: m => { lx = m.x; ly = m.y; }
        onPositionChanged: m => {
            const dx = m.x - lx, dy = m.y - ly;
            lx = m.x; ly = m.y;
            if (canvas.moveText) canvas.textMoved(canvas.cfg.x + dx, canvas.cfg.y + dy);
            else if (canvas.ready)
                canvas.place = S.cover(photo.implicitWidth, photo.implicitHeight, canvas.place.zoom,
                                       canvas.place.x + dx, canvas.place.y + dy, canvas.width, canvas.height);
        }
        onWheel: w => {
            w.accepted = true;
            if (!canvas.ready) return;
            const z = canvas.place.zoom * (w.angleDelta.y > 0 ? 1.1 : 1 / 1.1);
            canvas.place = S.zoomAt(photo.implicitWidth, photo.implicitHeight, canvas.place, z,
                                    w.x, w.y, canvas.width, canvas.height);
        }
    }

    // Writes base + ".png" (the logo) and base + "-mask.png" (the text on
    // transparency). cb(ok, error).
    function save(base, cb) {
        canvas.grabToImage(r => {
            if (!r.saveToFile(base + ".png")) { cb(false, `Cannot write ${base}.png`); return; }
            canvas.maskOnly = true;
            canvas.grabToImage(m => {
                canvas.maskOnly = false;
                const ok = m.saveToFile(base + "-mask.png");
                cb(ok, ok ? "" : `Cannot write ${base}-mask.png`);
            }, Qt.size(480, 800));
        }, Qt.size(480, 800));
    }
}