aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/sonix
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/sonix')
-rw-r--r--desktop/modules/sonix/BootlogoCanvas.qml139
1 files changed, 139 insertions, 0 deletions
diff --git a/desktop/modules/sonix/BootlogoCanvas.qml b/desktop/modules/sonix/BootlogoCanvas.qml
new file mode 100644
index 0000000..309e9ad
--- /dev/null
+++ b/desktop/modules/sonix/BootlogoCanvas.qml
@@ -0,0 +1,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));
+ }
+}