// Copyright (C) 2026 Danilo M. // // 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 the fill. For Bungee it is // the Regular layer underneath, showing through the stripe Bungee // Inline cuts out of each letter; the Inline layer on top is the fill. 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(label.layered ? canvas.cfg.second : canvas.cfg.fill) } Text { visible: label.layered text: canvas.text font { family: fInline.name; pixelSize: canvas.cfg.size } color: canvas.colour(canvas.cfg.fill) } } // 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) { if (!canvas.grabToImage(r => { if (!r.saveToFile(base + ".png")) { cb(false, `Cannot write ${base}.png`); return; } canvas.maskOnly = true; if (!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))) { canvas.maskOnly = false; cb(false, "Cannot grab the mask"); } }, Qt.size(480, 800))) cb(false, "Cannot grab the canvas"); } }