diff options
Diffstat (limited to 'desktop/modules/sonix/BootlogoCanvas.qml')
| -rw-r--r-- | desktop/modules/sonix/BootlogoCanvas.qml | 139 |
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)); + } +} |
