В графическом окружении рабочего стола KDE Plasma 5 для дистрибутивов GNU/Linux появилась замечательная возможность создания анимированных обоев с использованием технологии Qt Quick и декларативного скриптового языка QML. Благодаря переходу KDE Plasma 5 на современный и богатый возможностями фреймворк Qt 5, в новой версии Qt Quick 2.0 анимированные обои стали не только бережнее относиться к ресурсам компьютера, но и получили поддержку множества новых переходов, эффектов, шейдеров и даже системы частиц. Помимо этого увеличился FPS и повысилась плавность самой анимации.
Анимированные обои Bezier Clock на рабочем столе KDE Plasma 5.
Так как я уже достаточно давно являюсь счастливым пользователем KDE, я решил реализовать что-нибудь интересное для этого окружения, такое, чтобы радовался глаз. Создать анимированные обои с красивой анимацией было бы весьма кстати, поскольку стандартные совсем скучные и не демонстрируют мощь и функциональность технологии Qt Quick, а я как раз хотел опробовать язык программирования QML в разработке прикладных приложений.
Несколько лет назад, читая популярный в рунете IT-ресурс Хабрахабр я наткнулся на интересный проект: Часы на кривых Безье, где разработчик Jack Frigaard реализовал с помощью библиотеки для языка программирования JavaScript — Processing.js анимацию текущего времени. Используя цифры, отрисованные кривыми Безье и наборы простейших интерполяций, разработчику удалось получить очень интересную и занимательную трансформацию одних чисел в другие.
Сайт Jack’а Frigaard’а с часами на кривых Безье.
Мне настолько сильно понравилась эта анимация, что я вспомнил об этом проекте и решил использовать его в качестве базы для своих анимированных обоев в KDE Plasma 5. К моему огромному сожалению, ссылки из поста на Хабрахабре спустя два года стали мёртвыми, но я нашёл на хостинге Github Pages оригинальный сайт автора и его часы: Bézier Clock. На этой странице щедрый Jack Frigaard поделился с общественностью исходным кодом своего проекта, что несказанно облегчило реализацию моей идеи. По сути конечная цель свелась лишь к портированию Processing-кода на стек технологий Qt Quick и QML, а работающий прототип был создан всего за один вечер и две чашки крепкого кофе.
Содержание:
1. Обзор оригинального кода Bézier Clock
2. Особенности создания анимированных обоев в KDE Plasma 5
3. Портирование Bézier Clock на Qt Quick и QML
4. Создание окна настройки параметров Bezier Clock
5. Создание standalone-приложения Bezier Clock
6. Сборка пакетов для GNU/Linux и их установка
7. Заключение, полезные ссылки и ресурсы
1. Обзор оригинального кода Bézier Clock
Как уже было сказано выше, Jack Frigaard для своего проекта использовал библиотеку Processing.js, которая является портом языка визуализации данных Processing на язык программирования JavaScript. Библиотека интерпретирует код программы в файле с расширением *.pde и отображает требуемое изображение на экране. По заявлению разработчика на его официальном сайте, исходный код Bézier Clock получился очень простым и понятным, что на деле полностью соответствует действительности.
В программе содержится всего два класса: BezierDigit и BezierDigitAnimator. Первый класс описывает цифры и кривые Безье из которых они составлены, а второй отвечает за анимацию. В глобальном методе setup() настраивается контекст, создаются десять экземпляров класса BezierDigit, в соответствии с цифрами от нуля до девяти, затем создаются шесть объектов класса BezierDigitAnimator, по одному на каждую отображаемую цифру. Глобальный метод draw() вызывается каждый раз при обновлении кадра, именно он и формирует конечное изображение на экран. В этом методе в переменную d сохраняется текущая дата и время, из которой выделяются часы, минуты и секунды, потом они передаются объектам-аниматорам в их единственный метод update(). В нём происходит определение типа анимации (линейная, квадратичная, кубическая или синусоидальная) в соответствии с выбранными параметрами, а затем вспомогательным методом bezierVertexFromArrayListsRatios() рисуется текущая кривая Безье на экран. Внутри своего тела этот метод активно использует функцию lerp(), которая занимается линейной интерполяцией переданных в неё координат. Опционально в методах update() объектов класса BezierDigitAnimator рисуется ещё одна кривая Безье для тени и несколько прямых и примитивов для отображения контрольных линий.
Для переключения чисел используется глобальный вспомогательный метод getNextInt(), а метод getAnimStartRatio() предназначен для вычисления начала отсчёта анимации при повторном вызове функции draw().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
float getAnimStartRatio(float totalDuration) { if (animDurationUser > totalDuration) { return 0; } else { return 1.0 - (animDurationUser / totalDuration) } } int getNextInt(int current, int max) { if (current >= max) { return 0; } else { return current + 1; } } |
Методы mousePressed() и keyPressed() отвечают за изменение основных параметров приложения во время работы.
Вся красивая анимация этих часов заключается в своевременной интерполяции пяти контрольных точек кривых Безье между заранее заданными значениями.
2. Особенности создания анимированных обоев в KDE Plasma 5
Для того, чтобы приложение появилось в списке обоев в настройках рабочего стола KDE Plasma 5, каталог проекта нужно поместить в пользовательскую директорию ~/.local/share/plasma/wallpapers/ или системный каталог /usr/share/plasma/wallpapers/. Название директории, в которой находится обоина, должно быть задано в Java-стиле вида domain.organization.name, например, у меня это ru.exlmoto.bezierclock.
Стандартные анимированные обои KDE Plasma 5 доступны в системном каталоге /usr/share/plasma/wallpapers/. Поскольку QML является интерпретируемым языком, их код доступен в открытом и читаемом виде. Структура директории с обоиной довольно простая и выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 |
$ tree . ├── contents │ ├── config │ │ └── main.xml │ └── ui │ ├── config.qml │ └── main.qml ├── metadata.desktop └── plasma-wallpaper-color.desktop 3 directories, 5 files |
В директории contents/config/ находится конфигурационный файл main.xml с дефолтными значениями параметров. В директории contents/ui/ располагаются скрипты на языке программирования QML, из которых config.qml отвечает за содержимое виджета, который будет отображён в настройках рабочего стола при выборе соответствующей обоины, а в main.qml находится точка входа в приложение и, соответственно, все инструкции, которые определяют изображение и анимацию. В файлах с расширением *.desktop содержится различная мета-информация пакета с обоиной.
Структуры директорий с анимированными обоями в системном каталоге KDE Plasma 5, на скриншоте файловый менеджер KDE — Dolphin.
Мой файл с мета-информацией metadata.desktop для Bezier Clock выглядит вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[Desktop Entry] Encoding=UTF-8 Name=Bezier Clock Icon=preferences-system-time Type=Service ServiceTypes=Plasma/Wallpaper X-KDE-PluginInfo-Name=ru.exlmoto.bezierclock X-KDE-PluginInfo-Version=1.0 X-KDE-PluginInfo-Author=Serg Koles X-KDE-PluginInfo-Email=exlmotodev@gmail.com X-KDE-PluginInfo-Website=http://exlmoto.ru/bezier-clock X-KDE-PluginInfo-License=MIT X-Plasma-MainScript=ui/main.qml |
Прочитать настройки из конфигурационного файла достаточно легко, для этого в KDE Plasma 5 имеются специальные системные Qt Quick-пакеты с необходимым набором API.
Например, в конфиге main.xml определён параметр Color типа Color. Тип параметра может быть любым: String, Int, Bool и другие популярные типы.
1 2 3 4 5 6 |
<group name="General"> <entry name="Color" type="Color"> <label>Color of the wallpaper</label> <default>#0000ff</default> </entry> </group> |
Для того, чтобы получить значение параметра из QML, нужно импортировать системный пакет org.kde.plasma.core и использовать элемент wallpaper.configuration для получения необходимых данных:
1 2 3 4 5 6 7 8 9 10 11 |
import QtQuick 2.0 import org.kde.plasma.core 2.0 Rectangle { id: root color: wallpaper.configuration.Color Behavior on color { ColorAnimation { duration: units.longDuration } } } |
Чтобы записать изменяемое значение в конфиг, нужно тоже импортировать пакет org.kde.plasma.core и прокинуть псевдоним на требуемое свойство со специальным префиксом вида cfg_OptionName:
1 2 3 4 5 6 7 8 |
import org.kde.plasma.core 2.0 Column { id: root property alias cfg_Color: colorDialog.color ... } |
Тогда при изменении параметра, он будет автоматически сохранён в пользовательскую кофигурацию.
Следует помнить, что базовые элементы файлов config.qml и main.qml в качестве id должны иметь root, чтобы KDE Plasma 5 мог с ними работать, например, растягивать их контекст на весь экран.
3. Портирование Bézier Clock на Qt Quick и QML
Особенностью языка программирования QML является то, что он был создан декларативным. То есть вместо стандартного императивного подхода, который описывает как нужно решить задачу, в декларативном QML нужно описать что представляет из себя задача и её ожидаемый результат. Главный файл QML-приложения (в моём случае main.qml), определяет взаимодействия и всю иерархию элементов, дерево которых строится при загрузке QML-файла. Для каждого элемента определяется набор его типизированных свойств, в качестве значений которых могут быть использованы не только константы, но и выражения языка программирования JavaScript. Программы на QML могут использовать вспомогательные функции из внешних JavaScript-файлов или библиотек. Если в ООП базовым элементом является класс, по которому строится объект, то в QML базовым элементом будет сам элемент, являющийся строительным блоком QML-программы. Элементы бывают графическими и поведенческими, они объединяются вместе в QML-файлах для построения сложных компонентов. На этом заканчивается мой краткий экскурс в особенности языка программирования QML.
GIF-анимация, демонстрирующая работу Bezier Clock на KDE Plasma 5.
Как было сказано выше, в QML отсутствуют классы, но есть элементы, поэтому я начал портирование с преобразования классов BezierDigit и BezierDigitAnimator в компоненты BezierDigit.qml и BezierAnimator.qml соответственно. В качестве их базового элемента я выбрал компонент Item. QML-элементы внутри можно расширять методами на JavaScript, чем я воспользовался и портировал в них код с Processing. Для файла BezierDigit.qml получилось следующее:
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 |
import QtQuick 2.0 Item { property real vertexX property real vertexY property variant controls: [] function initDigit(control0, control1, control2, control3) { for (var i = 0; i < 4; ++i) { controls[i] = new Array(6); } controls[0] = control0; controls[1] = control1; controls[2] = control2; controls[3] = control3; } function getControl(index) { var scaledControl = new Array(6); for (var i = 0; i < 6; i++) { scaledControl[i] = controls[index][i] * setup.visualScaling; } return scaledControl; } function getVertexX() { return vertexX * setup.visualScaling; } function getVertexY() { return vertexY * setup.visualScaling; } function initialize() { controls = new Array(4); } Component.onCompleted: { initialize(); } } |
Инициализацию этих цифр кривыми Безье я поручил элементу BezierDigits.qml, вот его код:
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 |
import QtQuick 2.0 Item { property BezierDigit digit_0: digit0 property BezierDigit digit_1: digit1 ... BezierDigit { // Digit 0 id: digit0 vertexX: 254.0; vertexY: 47.0; Component.onCompleted: { initDigit([159.0, 84.0, 123.0, 158.0, 131.0, 258.0], [139.0, 358.0, 167.0, 445.0, 256.0, 446.0], [345.0, 447.0, 369.0, 349.0, 369.0, 275.0], [369.0, 201.0, 365.0, 81.0, 231.0, 75.0]); } } BezierDigit { // Digit 1 id: digit1 vertexX: 138.0; vertexY: 180.0; Component.onCompleted: { initDigit([226.0, 99.0, 230.0, 58.0, 243.0, 43.0], [256.0, 28.0, 252.0, 100.0, 253.0, 167.0], [254.0, 234.0, 254.0, 194.0, 255.0, 303.0], [256.0, 412.0, 254.0, 361.0, 255.0, 424.0]); } } ... } |
К сожалению, в официальной документации сказано, что порядок вызова кода у компонентов в свойствах Component.onCompleted не определён. Поэтому я не решился сделать массив variant, содержащий элементы BezierDigit и стал использовать несколько громоздкий прямой доступ к каждому элементу. Если у меня появится свободное время, я уточню этот момент у знакомых Qt Quick/QML-программистов.
Выше я отметил, что запуск анимированной обоины начинается с файла main.qml, он получился у меня достаточно компактным:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import QtQuick 2.0 Rectangle { id: root width: 800 height: 480 color: setup.backgroundColor BezierSettings { id: settings } BezierSetup { id: setup } BezierClock { id: bezierClock anchors.centerIn: parent } } |
Элемент BezierSettings отвечает за чтение настроек из конфига в свои свойства, это сделано специально для разделения кода анимированной обоины для KDE Plasma 5 и standalone-приложения на Qt Quick/QML, о чём я упомяну позже.
1 2 3 4 5 6 7 8 9 10 11 |
import QtQuick 2.0 import org.kde.plasma.core 2.0 // For wallpaper.configuration Item { property color backgroundColor: wallpaper.configuration.BackgroundColor property real visualScaling: wallpaper.configuration.ScalingValue / 10 property real animDurationUser: wallpaper.configuration.DurationAnim / 100 property bool continualAnimation: wallpaper.configuration.ContinualAnimation ... } |
Компонент BezierSetup забирает из свойств BezierSettings необходимые опции и создаёт элемент BezierInit:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Item { property int yOff: 0 property color backgroundColor: settings.backgroundColor property real visualScaling: settings.visualScaling property real animDurationUser: settings.animDurationUser property bool continualAnimation: settings.continualAnimation ... BezierInit { id: _init } } |
BezierInit в своём теле инициализирует элементы-аниматоры и компонент BezierDigits, который обобщает все цифры:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Item { property BezierDigits digits: _digits property BezierAnimator hoursTensAnimator: _hoursTensAnimator property BezierAnimator hoursUnitsAnimator: _hoursUnitsAnimator property BezierAnimator minutesTensAnimator: _minutesTensAnimator property BezierAnimator minutesUnitsAnimator: _minutesUnitsAnimator property BezierAnimator secondsTensAnimator: _secondsTensAnimator property BezierAnimator secondsUnitsAnimator: _secondsUnitsAnimator BezierDigits { id: _digits } BezierAnimator { // X0:00:00 id: _hoursTensAnimator origX: 0.0 * visualScaling origY: yOff * visualScaling animationStartRatio: 35995.0 / (35995.0 + 5.0) } ... } |
Далее в main.qml создаётся главный элемент анимированных обоев — BezierClock, который содержит в себе элемент канваса для отрисовки происходящего на экране, компонент, отвечающий за вывод оверлея счётчика FPS и таймер, в котором происходит обновление канваса в соответствии со значением FPS:
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 |
import QtQuick 2.0 Rectangle { width: 2300 * setup.visualScaling height: 600 * setup.visualScaling BezierCanvas { id: canvas anchors.fill: parent } UiFpsOverlay { id: fps } // Main Timer Timer { id: timer interval: setup.frameRate repeat: true triggeredOnStart: true onTriggered: { fps.framesCount++; fps.framesPerSecond++; canvas.requestPaint(); } } Component.onCompleted: { timer.start(); fps.timer.start(); } } |
Для различных оверлеев я создал базовый элемент UiBaseOverlay, с таким содержанием:
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 |
import QtQuick 2.0 import 'JsCoreFunctions.js' as CoreFunctions // For const Rectangle { id: _base property int gap: 10 property alias textTo: textLabel.text property alias fontTo: textLabel.font.family width: textLabel.width + gap height: textLabel.height + gap color: 'black' opacity: CoreFunctions.DEFAULT_OVERLAY_OPACITY Text { id: textLabel x: gap / 2 y: gap / 2 color: 'white' opacity: parent.opacity font.family : 'monospace' } } |
От него я и унаследовал UiFpsOverlay, который содержит ещё один таймер, тикающий раз в секунду:
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 |
import QtQuick 2.0 UiBaseOverlay { visible: setup.showFps // FPS Area property int framesCount: 0 property int framesPerSecond: 0 property int seconds: 0 property int fps: 0 property int fpsAverage: 0 // Fps Timer property Timer timer: timerOneSec textTo: framesCount + ' Frames in ' + seconds + ' seconds\n' + 'Current FPS: ' + fps + '\n' + 'Average FPS: ' + (fpsAverage / ((seconds > 1) ? seconds - 1 : 1)).toFixed(2); Timer { id: timerOneSec interval: 1000 // 1 Second repeat: true triggeredOnStart: true onTriggered: { if (seconds != 0) { fps = framesPerSecond; fpsAverage += framesPerSecond; framesPerSecond = 0; } seconds++; } } } |
Оба таймера, главный и предназначенный для FPS, запускаются в свойстве Component.onCompleted элемента BezierClock. Это свойство передаёт своё управление JavaScript-коду тогда, когда компонент загрузился и выстроено дерево его элементов. Константы 2300 и 600 являются оригинальным размером контекста часов без какого-либо масштабирования.
В элементе BezierCanvas определён метод render(), являющийся аналогом метода draw() в Processing. Этот метод дёргает функции update() элементов-аниматоров и рисует итоговый кадр на канвас.
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 |
import QtQuick 2.0 import QtQml 2.0 // for new Date() import 'JsCoreFunctions.js' as CoreFunctions import 'JsCanvasFunctions.js' as CanvasFunctions Canvas { id: canvas property date currentDate property int currentSec onPaint: { var context2d = getContext('2d'); render(context2d); } function render(context) { context.save(); // Fill Background CanvasFunctions.fillCanvasByColor(context, setup.backgroundColor); // Translate Context CanvasFunctions.translateContex(context); // Render Seconds currentDate = new Date(); ... } } |
Канвас в QML имеет множество методов, аналогичных канвасу в HTML5, а потому портирование различных приложений с QML на HTML5 и обратно не представляет особой сложности.
Некоторые компоненты я расширил JavaScript-файлами JsCanvasFunctions.js и JsCoreFunctions.js. Первый является компонентно-зависимым, а потому в его функциях можно получать доступ к свойствам QML-элемента. Второй, напротив, является библиотекой, и его можно использовать в различных QML-элементах. Для определения библиотеки имеется специальная директива .pragma library, которую необходимо определить в самом начале файла:
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 |
.pragma library var DEFAULT_OVERLAY_OPACITY = 0.7; function lerp(value1, value2, amount) { // amount = amount < 0 ? 0 : amount; // amount = amount > 1 ? 1 : amount; return ((value2 - value1) * amount) + value1; } function bezierVertexFromArrayListsRatios(context, from, to, ratio, offsetX, offsetY) { context.bezierCurveTo(lerp(from[0], to[0], ratio) + offsetX, lerp(from[1], to[1], ratio) + offsetY, lerp(from[2], to[2], ratio) + offsetX, lerp(from[3], to[3], ratio) + offsetY, lerp(from[4], to[4], ratio) + offsetX, lerp(from[5], to[5], ratio) + offsetY); } function getNextInt(current, max) { if (current >= max) { return 0; } else { return current + 1; } } function getAnimStartRatio(totalDuration, animDurationUser) { if (animDurationUser > totalDuration) { return 0; } else { return 1.0 - (animDurationUser / totalDuration) } } function sq(aNumber) { return aNumber * aNumber; } function roundOne(aNumber) { return (aNumber < 0.5) ? 0.0 : 1.0; } function drawLine(context, startX, startY, endX, endY) { context.beginPath(); context.moveTo(startX, startY); context.lineTo(endX, endY); context.stroke(); } function drawCircle(context, centerX, centerY, radius, fillColor, stroke) { context.beginPath(); context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false); context.fillStyle = fillColor; context.fill(); if (stroke) { context.stroke(); } } function drawSquare(context, x, y, width, fillColor, strokeColor, baseStokeColor) { context.beginPath(); context.rect(x - width / 2, y - width / 2, width, width); context.fillStyle = fillColor; context.fill(); context.strokeStyle = strokeColor; context.stroke(); context.strokeStyle = baseStokeColor; } function getLineCap(aNumber) { switch (aNumber) { default: case 0: return 'butt'; case 1: return 'round'; case 2: return 'square'; } } function getAnimationType(aNumber) { switch (aNumber) { default: case 0: case 1: return 'linear'; case 2: return 'quadratic'; case 3: return 'cubic'; case 4: return 'sinuisoidial'; case 5: return 'no animation'; } } |
В JsCoreFunctions.js я реализовал функции, доступные в Processing, но отсутствующие в JavaScript, такие, как sq() и lerp().
Мои эксперименты показали, что отрисовка канваса размером на весь экран требует значительных ресурсов CPU, поэтому я придумал небольшую хитрость: я рисую часы не в полный экран, а в небольшой прямоугольник, который находится в центре другого статичного полноэкранного прямоугольника. Поскольку цвета у прямоугольников одинаковые, этого не заметно, но зато FPS значительно повысился и перестал проседать.
Зелёный прямоугольник статичен, а на красный рендерится изображение Bezier Clock.
На этом обзорная часть моего кода завершена. В качестве небольшого отличия от оригинала я реализовал между цифрами отрисовку мигающих (в зависимости от желания пользователя) точек, как в самых обычных электронных часах.
4. Создание окна настройки параметров Bezier Clock
Как было отмечено выше, в настройках рабочего стола при выборе необходимой анимированной обоины отображается форма из файла config.qml, внутри которого описаны все необходимые элементы управления. Я решил сделать Bezier Clock максимально настраиваемым и определил более двадцати параметров, которые можно изменить в соответствии со своим вкусом и предпочтениями. Само собой, такое количество опций сильно раздувает окно настройки, но писать интерфейсы на языке программирования QML — одно удовольствие! Кажется, что это идеальный язык для такого рода задач.
Окно настройки параметров Bezier Clock в KDE Plasma 5.
Нужно определить колонки со строками и просто заполнить их компонентами в соответствии с необходимой иерархией. Элементы можно кастомизировать, совмещать и наследовать, в сочетании с мощью QML это даёт огромный простор для полёта фантазии. Например, для создания компонента выбора цвета UiColorBox, который отображает внутри себя кнопку с цветным прямоугольником и строку справа, требуется следующий код:
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 |
import QtQuick 2.0 import QtQuick.Controls 1.0 import QtQuick.Dialogs 1.1 import org.kde.plasma.core 2.0 // For units Item { property string titleDialog property string labelText property alias colorTo: colorDialog.color width: label.width + colorButton.width + units.smallSpacing height: colorButton.height ColorDialog { id: colorDialog modality: Qt.WindowModal showAlphaChannel: true title: titleDialog } Row { spacing: units.smallSpacing Button { id: colorButton width: units.gridUnit * 2 onClicked: colorDialog.open() Rectangle { id: colorRect anchors.centerIn: parent width: parent.width - units.smallSpacing * 4 height: parent.height - units.smallSpacing * 4 color: colorDialog.color } } Label { id: label anchors.verticalCenter: colorButton.verticalCenter text: labelText } } } |
Похожим образом реализованы элементы UiComboBox и UiSpinBox.
Настройки сохраняются в пользовательской конфигурации посредством проброшенных псевдонимов на свойства с префиксом cfg_OptionName, KDE Plasma 5 при изменении настроек на форме тут же перезаписывает их в хранилище и делает доступной кнопку Apply. Часть файла config.qml, демонстрирующая чтение настроек и то, насколько просто описывается отображаемый интерфейс:
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 |
import QtQuick 2.0 import QtQuick.Controls 1.0 import org.kde.plasma.core 2.0 // For units import 'JsConfigUiHelper.js' as ConfigUiHelper Row { id: root // All settings property alias cfg_BackgroundColor: colorBackgroundBox.colorTo property alias cfg_DigitColor: colorDigitBox.colorTo property alias cfg_ShadowColor: colorShadowBox.colorTo property alias cfg_ControlLinesColor: colorLinesBox.colorTo property alias cfg_SquaresColor: colorRectsBox.colorTo ... spacing: units.smallSpacing Column { spacing: units.smallSpacing GroupBox { title: qsTr('Main Settings') id: mainGroupBox // This is element with biggest width Column { spacing: units.smallSpacing UiComboBox { id: animationTypeComboBox labelText: qsTr('Animation') modelTo: [qsTr('Linear'), qsTr('Quadratic'), qsTr('Cubic'), qsTr('Sinuisoidial'), qsTr('No animation')] onComboBoxIndexChanged: { if (comboBoxIndex == 4) { durationsSpinBox.enabled = false } else { durationsSpinBox.enabled = true } } } UiSpinBox { id: scalingSpinBox minValue: 1 maxValue: 20 stepValue: 1 labelText: qsTr('Scaling Value') } UiSpinBox { id: durationsSpinBox minValue: 0 maxValue: 10000 stepValue: 1 labelText: qsTr('Animation Duration') } CheckBox { id: continualAnimationCheckBox text: qsTr('Continual Animation') onCheckedChanged: { if (checked == true) { continualShadowsCheckBox.checked = false; } } } UiColorBox { id: colorBackgroundBox titleDialog: qsTr('Select Background Color') labelText: qsTr('Background Color') } } } } } |
Поскольку вариантов конфигурирования Bezier Clock получилось огромное количество, пользователь может что-то сломать и забыть то, как вернуть всё в первоначальное состояние. Для этой цели я сделал кнопку Reset to Default, а функцию сброса настроек на те, что были по умолчанию, вынес в отдельный файл JsConfigUiHelper.js:
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 |
function resetToDefault() { // Reset all settings to Default values colorBackgroundBox.colorTo = '#FFFFFF'; // White colorDigitBox.colorTo = '#000000'; // Black colorShadowBox.colorTo = '#888888'; // Gray colorLinesBox.colorTo = '#FF0000'; // Red colorRectsBox.colorTo = '#0000FF'; // Blue continualAnimationCheckBox.checked = false; continualShadowsCheckBox.checked = false; controlLinesCheckBox.checked = false; showFpsCheckBox.checked = false; scalingSpinBox.spinBoxValue = 5; durationsSpinBox.spinBoxValue = 100; fpsLimitSpinBox.spinBoxValue = 60; digitsWidthSpinBox.spinBoxValue = 10; shadowsWidthSpinBox.spinBoxValue = 10; linesWidthSpinBox.spinBoxValue = 1; circlesRadiusSpinBox.spinBoxValue = 3; digitCapComboBox.comboBoxIndex = 1; animationTypeComboBox.comboBoxIndex = 0; showDotsCheckBox.checked = true; blinkCheckBox.checked = false; radiusDotsSpinBox.spinBoxValue = 5; } |
В качестве небольшого украшения я добавил на форму собственноручно созданную иконку и информацию о создателях.
5. Создание standalone-приложения Bezier Clock
Программы, созданные с помощью технологии Qt Quick и языка программирования QML, легко переносимы на другие системы. Созданную анимированную обоину можно сделать обычным прикладным приложением и развернуть окно на весь экран там, где нет KDE Plasma 5, но есть Qt 5, например, на MS Windows или macOS. Тот код, который используется только для standalone-приложения, я вынес в отдельный каталог qml/NonKDE/ и использовал разделение. К примеру, отдельной программой используются файлы с постфиксом Qt: BezierSettingsQt.qml и mainQt.qml, а анимированная обоина для KDE Plasma 5 использует BezierSettings.qml и main.qml. Получается, что в случае standalone-приложения настройки в общий элемент BezierSetup загружаются компонентом BezierSettingsQt, а в случае анимированной обоины этой работой занимается BezierSettings. Таким образом можно легко разделять платформозависимый код.
Standalone-приложение Bezier Clock, запущенное на операционной системе MS Windows 10, на скриншоте показан исчезающий оверлей со справкой (превью, увеличение по клику).
Окно с настройкой параметров в standalone-приложении я решил не делать и вместо этого перенес изменение опций на клавиши клавиатуры и кнопки мышки. Для того, чтобы пользователь имел представление о том, каким образом изменился тот или иной параметр, я добавил простейшую систему полупрозрачных исчезающих оверлеев. Их смысл таков: пользователь нажимает на кнопку и видит сообщение о том, какой параметр изменился и как он изменился. Кроме того, с помощью такого же метода я организовал справку по клавишам, которые обрабатывает приложение. Для создания оверлея я просто унаследовался от общего базового элемента UiBaseOverlay и создал компонент UiOptionsOverlay, в котором создал поведенческий элемент NumberAnimation регулирующий свойство opacity и отвечающий за полное исчезновение оверлея с экрана в течении необходимого времени:
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 |
import QtQuick 2.0 import '../' UiBaseOverlay { property bool playAnimation: false property alias animation: _animation fontTo: 'courier new' visible: false NumberAnimation on opacity { id: _animation running: playAnimation to: 0.0 duration: 3000 // 3 seconds } onOpacityChanged: { if (!opacity) { visible = false; } } } |
Файл mainQt.qml, отвечающий за главное окно программы, выглядит следующим образом:
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 |
import QtQuick 2.0 import QtQuick.Window 2.1 import '../' import 'JsMainUiHelper.js' as MainUiHelper import '../JsCoreFunctions.js' as CoreFunctions Window { id: rootWindow visible: true width: 800 height: 480 title: qsTr('Bezier Clock') BezierSettingsQt { id: settings } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: { if (mouse.button == Qt.RightButton) { setup.continualAnimation = !setup.continualAnimation; setup.showContinualShadows = false; MainUiHelper.showOptionsOverlay(qsTr('Main Settings'), qsTr('Continual Animation'), setup.continualAnimation); } else { setup.drawControlLines = !setup.drawControlLines; MainUiHelper.showOptionsOverlay(qsTr('Control Lines'), qsTr('Show Control Lines'), setup.drawControlLines); } } } Rectangle { width: parent.width height: parent.height color: setup.backgroundColor focus: true BezierSetup { id: setup } BezierClock { id: bezierClock anchors.centerIn: parent } UiOptionsOverlay { id: overlayHelp anchors.centerIn: parent animation.duration: 6000 // 6 seconds } UiOptionsOverlay { id: overlayOptions anchors.top: bezierClock.top anchors.right: bezierClock.right } Keys.onPressed: { MainUiHelper.keyPressed(event); } } Component.onCompleted: { MainUiHelper.checkFullscreen(); MainUiHelper.showHelpOverlay(); } Component.onDestruction: { MainUiHelper.saveSettings(); } } |
В элементе MouseArea происходит обработка нажатий на кнопки мышки, а в свойстве Keys.onPressed — обработка клавиш клавиатуры. Различные функции я выделил во вспомогательный JavaScript-файл JsMainUiHelper.js, например, в нём содержатся методы обработки событий, отображения оверлеев overlayOptions и overlayHelp, а также функции генерации случайных цветов. Файл получился весьма громоздким, вот его небольшая часть кода:
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 |
function showOptionsOverlay(option, text, value) { overlayOptions.textTo = option + '\n' + text + ': ' + value; showOverlay(); } function showMessage(value) { overlayOptions.textTo = value; showOverlay(); } function showOverlay() { overlayOptions.visible = true; overlayOptions.opacity = 0.9; overlayOptions.animation.restart(); } function getRandomColor() { return Qt.rgba(Math.random(), Math.random(), Math.random(), 1.0); } function setRandomColors() { setup.backgroundColor = getRandomColor(); setup.digitColor = getRandomColor(); setup.digitColorShadow = getRandomColor(); setup.linesColor = getRandomColor(); setup.rectColor = getRandomColor(); } function checkFullscreen() { if (settings.fullscreen) { rootWindow.visibility = 'FullScreen'; } else { rootWindow.visibility = 'Windowed'; } } function toggleFullscreen() { settings.fullscreen = !settings.fullscreen; checkFullscreen(); } |
Настройки приложения определены в BezierSettingsQt, и по сравнению с BezierSettings расширены опцией, отвечающей за работу окна в полноэкранном режиме. В Qt Quick существует специальный пакет Qt.labs.settings для работы с настройками. Элемент settings обеспечивает кроссплатформенное сохранение и чтение необходимых параметров из внутреннего хранилища системы. Настройки читаются при каждом запуске приложения и сохраняются после выхода из приложения свойством Component.onDestruction корневого элемента mainQt.qml.
В standalone-приложении необходимый исходный код на языках QML и JavaScript вкомпиливается прямо в исполнительный бинарник. Работу по упаковке исходного кода выполняет специальный компилятор ресурсов rcc, который получает список необходимых компонентов из файла qml.qrc. Главный компилируемый файл main.cpp на языке программирования C++ выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QIcon> int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); app.setWindowIcon(QIcon("://images/BC_icon.png")); app.setOrganizationName("EXL\'s Group"); app.setOrganizationDomain("exlmoto.ru"); app.setApplicationName("Bezier Clock"); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:/qml/NonKDE/mainQt.qml"))); return app.exec(); } |
Собрать и запустить standalone-приложение достаточно просто. Необходимо перейти в каталог с исходным кодом и выполнить следующие команды в терминале:
1 2 3 |
$ qmake BezierClock.pro $ make -j9 $ ./BezierClock |
Таким образом развернуть Bezier Clock можно на любых системах, для которых доступен Qt 5.
6. Сборка пакетов для GNU/Linux и их установка
Для удобной установки Bezier Clock в дистрибутивы GNU/Linux, я решил создать два варианта пакетов: первый подходит для любого дистрибутива и уставливает обоину в пользовательскую директорию ~/.local/share/plasma/wallpapers/, а второй является установочным пакетом для дистрибутива Arch Linux и уставливает файлы в системный каталог /usr/share/plasma/wallpapers/.
Первый вариант пакета представляет собой обычный архив формата TAR.XZ и установочный скрипт package.sh, выполнение которого развернёт пакет в пользовательскую директорию. Он выглядит следующим образом:
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 |
#!/bin/bash # Name VERSION="v1.0" PACKAGE_NAME=bezier-clock-$VERSION.tar.xz # Dirs CURRENT_DIR="`pwd`" PACKAGE_DIR="$CURRENT_DIR/ru.exlmoto.bezierclock" UI_DIR="$PACKAGE_DIR/contents/ui" ICON_DIR="$PACKAGE_DIR/contents/icon" CONFIG_DIR="$PACKAGE_DIR/contents/config" DEPLOY_DIR="$HOME/.local/share/plasma/wallpapers" APP_DIR="$DEPLOY_DIR/ru.exlmoto.bezierclock" func_package() { # Create dirs echo -n "Prepare directory $PACKAGE_DIR for package app..." mkdir -p $PACKAGE_DIR mkdir -p $UI_DIR mkdir -p $ICON_DIR mkdir -p $CONFIG_DIR # Copy package files cp metadata.desktop $PACKAGE_DIR cp ../qml/{*.qml,*.js} $UI_DIR cp ../images/BC_icon.png $ICON_DIR cp ../xml/main.xml $CONFIG_DIR echo "done." # Archive files echo -n "Package files to $PACKAGE_NAME..." tar -cJf $PACKAGE_NAME ru.exlmoto.bezierclock echo "done." # Clean echo -n "Cleaning $PACKAGE_DIR..." rm -Rf $PACKAGE_DIR echo "done." } func_install() { if [ ! -f ${PACKAGE_NAME} ]; then echo "$PACKAGE_NAME not found! Create package first with ./package.sh -p" ; exit 1 else if [ -d $APP_DIR ]; then echo "$PACKAGE_NAME already installed! Remove package first with ./package.sh -u" ; exit 1 else # Create dirs mkdir -p $DEPLOY_DIR # Unpack package echo -n "Install package to $DEPLOY_DIR..." tar -xJf $PACKAGE_NAME -C $DEPLOY_DIR echo "done." fi fi } func_uninstall() { # Delete Package if [ -d $APP_DIR ]; then echo -n "Deleting $APP_DIR..." rm -Rf $APP_DIR echo "done." else echo "Package $PACKAGE_NAME already uninstalled." fi } func_usage() { echo -e "\nUsage ./package.sh for install package or ./package.sh <argument> for options:\n" echo -e "\t -i or --install\tfor create package and install it to $DEPLOY_DIR;" echo -e "\t -p or --package\tfor create TAR.XZ-package;" echo -e "\t -u or --uninstall\tfor uninstall package (removing $APP_DIR directory);" echo -e "\t -c or --clean\t\tfor remove package from current directory;" echo -e "\t -h or --help\t\tfor this help.\n" } func_clean() { if [ -f ${PACKAGE_NAME} ]; then echo -n "Deleting $PACKAGE_NAME..." rm $PACKAGE_NAME echo "done." else echo "$PACKAGE_NAME already deleted." fi } if test $# -ne 0; then case "$1" in "") func_usage ;; "-i"|"--install") func_install ;; "-p"|"--package") func_clean ; func_package ;; "-u"|"--uninstall") func_uninstall ;; "-c"|"--clean") func_clean ;; "-h"|"--help") func_usage ;; *) func_usage ;; esac shift else func_install ; exit 1 fi |
Скрипт, будучи запущенный с опцией -u, удаляет файлы Bezier Clock из пользовательской директории. В теории он должен работать на любых дистрибутивах GNU/Linux с KDE Plasma 5. Помимо опций установки и удаления этот скрипт как раз и собирает пакет bezier-clock-v1.0.tar.xz, если его запустить в каталоге исходного кода utils/ с опцией сборки пакета -p.
Поскольку сейчас я использую замечательный дистрибутив GNU/Linux под названием Arch Linux, я решил попрактиковаться в создании пакетов и собрать Bezier Clock именно для этого дистрибутива. Для этого в Arch Linux нужно установить некоторые зависимости и систему сборки abs — Arch Build System, после чего нужно выполнить синхронизацию дерева abs с сервером и можно приступать к созданию пакета.
Update 09-FEB-2018: Система сборки Arch Build System устарела, поэтому устанавливать её для сборки пакетов больше не требуется.
1 2 3 |
$ sudo pacman -S abs $ sudo pacman -S extra-cmake-modules $ sudo abs |
Чтобы в строке Packager при выполнении команды запроса информации о пакете:
1 |
$ sudo pacman -Qi bezier-clock |
Отображалось ваше имя и почта, необходимо задать эти данные в файле /etc/makepkg.conf в переменной PACKAGER раздела PACKAGE OUTPUT.
Для создания пакета потребуется написать лишь два файла: рецепт любой системы сборки и сборочный скрипт PKGBUILD. В качестве сборочной системы я выбрал CMake, так как именно её использует KDE Plasma 5 и в ней имеются все необходимые скрипты и переменные для этого окружения. Пример рецепта CMake для установки своих элементов (обоев, виджетов, апплетов) в KDE Plasma 5 опубликован в официальной документации. Я его немного отредактировал и получилось следующее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
project(bezier-clock) # Set minimum CMake version (required for CMake 3.0 or later) cmake_minimum_required(VERSION 2.8.12) # Use Extra CMake Modules (ECM) for common functionality. # See http://api.kde.org/ecm/manual/ecm.7.html # and http://api.kde.org/ecm/manual/ecm-kde-modules.7.html find_package(ECM REQUIRED NO_MODULE) # Needed by find_package(KF5Plasma) below. set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_MODULE_PATH}) # Locate plasma_install_package macro. find_package(KF5Plasma REQUIRED) # Add installatation target ("make install"). plasma_install_package(bezier-clock ru.exlmoto.bezierclock wallpapers wallpaper) |
Сборочный скрипт PKGBUILD имеет специальные функции prepare(), build() и package(). Первая готовит окружение для сборки, вторая выполняет саму сборку, а третья делает пакет. Мой PKGBUILD выглядит следующим образом:
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 |
pkgname=bezier-clock ver=1.0 pkgver=v$ver pkgrel=1 pkgdesc='Bezier Clock live wallpaper for the Plasma Workspace' arch=('any') url='http://exlmoto.ru/bezier-clock' license=('MIT') depends=('plasma-workspace') source=("https://github.com/EXL/BezierClock/archive/$pkgver.tar.gz") sha256sums=('ca60121292eb78a2143f09651a2c43dbe3fdcd0cda2127603d09f0f3eaffe765') makedepends=('extra-cmake-modules') # Directories base_dir=build-bezier-clock-package build_dir=$base_dir/build app_dir=$base_dir/bezier-clock contents_dir=$app_dir/contents prepare() { mkdir -p $build_dir mkdir -p $contents_dir/{ui,icon,config} cp BezierClock-$ver/utils/*.desktop $app_dir cp BezierClock-$ver/qml/{*.qml,*.js} $contents_dir/ui cp BezierClock-$ver/images/BC_icon.png $contents_dir/icon cp BezierClock-$ver/xml/main.xml $contents_dir/config cp BezierClock-$ver/utils/CMakeLists.txt $base_dir } build() { cd $build_dir cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr \ -DBUILD_TESTING=OFF make } package() { cd $build_dir make DESTDIR="${pkgdir}" install } |
Для сборки пакета достаточно выполнить команду:
1 |
$ makepkg -cf |
В той директории, где находится PKGBUILD. Умный abs сам скачает исходный код, проверит его хеш-сумму, разархивирует, соберёт и создаст пакет bezier-clock-v1.0-1-any.pkg.tar.xz, который будет удобно устанавливать в систему при помощи пакетного менеджера pacman или его фронтеда yaourt:
1 |
$ sudo pacman -U bezier-clock-v1.0-1-any.pkg.tar.xz |
За удаление пакета из системы тоже будет отвечать пакетный менеджер.
1 |
$ sudo pacman -R bezier-clock |
Благодаря системе сборки CMake можно легко собрать пакеты анимированных обоев Bezier Clock и для других дистрибутивов GNU/Linux.
Кроме того, в KDE Plasma 5 Workspace имеется специальный встроенный пакетный менеджер, который называется Plasma Package Manager. Можно установить пакет bezier-clock-v1.0.tar.xz, инструкцию по сборке которого я написал чуть выше, с его помощью следующим образом:
1 |
$ plasmapkg2 -t wallpaperplugin -i bezier-clock-v1.0.tar.xz |
После чего пакет установится аналогино первому способу. Удалить его из системы тоже можно через Plasma Package Manager:
1 |
$ plasmapkg2 -t wallpaperplugin -r ru.exlmoto.bezierclock |
Эта утилита позволяет удобно управлять различными расширениями KDE Plasma 5.
7. Заключение, полезные ссылки и ресурсы
Создание анимированных обоев Bezier Clock для KDE Plasma 5 позволило мне разобраться в написании приложений с использованием технологии Qt Quick на языке программирования QML, узнать о таком языке визуализации данных, как Processing и успешно попрактиковаться в создании установочных пакетов для Arch Linux. Этот процесс дал мне огромное количество ценного опыта, я смог понять механизм работы анимированных обоев в KDE Plasma 5 и разобраться во внутренней кухне сборки пакетов для своего дистрибутива GNU/Linux.
Bezier Clock в качестве анимированных обоев окружения KDE Plasma 5, демонстрация на видеохостинге YouTube.
Размер установочных TAR.XZ-пакетов получился совсем крошечным, всего 15 КБ. Это связано с тем, что QML является интерпретируемым языком, а интерпретатор уже установлен в систему.
Скачать TAR.XZ-пакеты Bezier Clock, готовые для установки в дистрибутивы GNU/Linux, можно по этим ссылкам:
[Скачать Plasma TAR.XZ-пакет Bezier Clock, 13 КБ | Download Plasma Bezier Clock 21 TAR.XZ-package, 13 КB]
[Скачать All TAR.XZ-пакет Bezier Clock, 15 КБ | Download All Bezier Clock 21 TAR.XZ-package, 15 КB]
[Скачать Arch Linux TAR.XZ-пакет Bezier Clock, 15 КБ | Download Arch Linux Bezier Clock 21 TAR.XZ-package, 15 КB]
К сожалению, в последних версиях KDE Plasma 5.7.x внесли баг, из-за которого не читаются и не сохраняются настройки анимированных обоев. Поэтому после установки и активации Bezier Clock можно увидеть чёрный экран. Для исправления этой ошибки нужно просто нажать кнопку Reset to Default, а потом Apply. Этот баг зарепорчен и можно наблюдать за исправлением ситуации здесь и здесь.
Все исходные коды и проект в целом выложен в репозиторий на ресурсе Github. Мои изменения и исходные файлы доступны под лицензией MIT. Ссылка на репозиторий:
https://github.com/EXL/BezierClock
В этой работе я использовал огромное количество материалов, основные из них я выделю в полезных ссылках ниже. Огромное спасибо ресурсам stackoverflow.com и google.com за то, что они есть.
- Сайт с часами на кривых Безье от Jack’а Frigaard’а;
- Исходный код часов на кривых Безье от Jack’а Frigaard’а;
- Документация библиотеки Processing.js;
- Замечательная книга QML Book, посвящённая разработке приложений на Qt Quick/QML, седьмая глава книги посвящена портированию HTML5-приложений на QML;
- Официальная документация по Qt Quick и QML;
- Мануал по созданию своих элементов (обоев, виджетов, апплетов) для KDE Plasma 5;
- Статья на ArchWiki, подробно рассматривающая рецепты создания пакетов для Arch Linux;
- Страничка моего порта Bezier Clock в каталоге приложений KDE Store.
Update 06-MAR-2017: Прочитать о твике Bezier Clock для KDE Plasma 5, который добавляет возможность установки собственного фонового изображения, можно в разделе этой статьи.