Портирование из элемента canvas HTML5 в QML

Добавлено 9 мая 2022 в 21:30

Перенос с элемента canvas HTML5 на элемент Canvas QML довольно прост. В этой главе мы рассмотрим пару примеров и выполним преобразование.

Спирограф

В качестве основы мы используем пример спирографа из проекта Mozilla. Исходный HTML5 был опубликован как часть руководства по canvas.

Нам нужно было изменить несколько строк:

  • Qt Quick требует, чтобы вы объявили переменную, поэтому нам нужно было добавить несколько объявлений var.
    for (var i=0;i<3;i++) {
        ...
    }
  • Мы адаптировали метод draw для получения объекта Context2D.
    function draw(ctx) {
        ...
    }
  • Нам нужно было адаптировать перенос для каждого узора из-за разных размеров.
    ctx.translate(20+j*50,20+i*50);
  • Наконец, мы завершили наш обработчик onPaint. Внутри мы получаем контекст и вызываем нашу функцию рисования.
    onPaint: {
        var ctx = getContext("2d");
        draw(ctx);
    }

Результатом является портированная графика спирографа, работающая с использованием холста QML.

Результат
Результат

Как видите, перенос с HTML5 на QML возможен без изменений фактической логики и с относительно небольшими изменениями самого кода.

Светящиеся линии

Вот еще один, более сложный пример для портирования от организации W3C. Исходные красивые светящиеся линии имеют несколько особенностей, что усложняет портирование.

Исходные светящиеся линии
Исходные светящиеся линии
<!DOCTYPE HTML>
<html lang="en">
<head>
    <title>Pretty Glowing Lines</title>
</head>
<body>

<canvas width="800" height="450"></canvas>
<script>
var context = document.getElementsByTagName('canvas')[0].getContext('2d');

// начальная позиция
var lastX = context.canvas.width * Math.random();
var lastY = context.canvas.height * Math.random();
var hue = 0;

// функция для рисования
// случайная кривая Безье со случайным цветом и эффектом свечения
function line() {

    context.save();

    // масштабирование с коэффициентом 0,9 вокруг центра холста
    context.translate(context.canvas.width/2, context.canvas.height/2);
    context.scale(0.9, 0.9);
    context.translate(-context.canvas.width/2, -context.canvas.height/2);

    context.beginPath();
    context.lineWidth = 5 + Math.random() * 10;

    // наша начальная позиция
    context.moveTo(lastX, lastY);

    // наша новая конечная позиция
    lastX = context.canvas.width * Math.random();
    lastY = context.canvas.height * Math.random();

    // случайная кривая Безье, которая заканчивается на lastX, lastY
    context.bezierCurveTo(context.canvas.width * Math.random(),
    context.canvas.height * Math.random(),
    context.canvas.width * Math.random(),
    context.canvas.height * Math.random(),
    lastX, lastY);

    // эффект свечения
    hue = hue + 10 * Math.random();
    context.strokeStyle = 'hsl(' + hue + ', 50%, 50%)';
    context.shadowColor = 'white';
    context.shadowBlur = 10;
    // обводим кривую
    context.stroke();
    context.restore();
}

// вызываем функцию line каждые 50 мс
setInterval(line, 50);

function blank() {
    // при каждом вызове делает фон на 10% темнее
    context.fillStyle = 'rgba(0,0,0,0.1)';
    context.fillRect(0, 0, context.canvas.width, context.canvas.height);
}

// вызываем функцию blank каждые 50 мс
setInterval(blank, 40);

</script>
</body>
</html>

В HTML5 объект Context2D может рисовать на холсте в любое время. В QML он может рисовать только внутри обработчика onPaint. Таймер, используемый с setInterval, запускает в HTML5 отрисовку линии или очистку экрана. Из-за отличающейся обработки в QML просто вызвать эти функции невозможно потому, что нам нужно пройти через обработчик onPaint. Кроме того, должны быть адаптированы цветовые представления. Давайте пошагово пройдемся по изменениям.

Всё начинается с элемента холста. Для простоты мы просто используем элемент Canvas в качестве корневого элемента нашего файла QML.

import QtQuick

Canvas {
   id: canvas
   width: 800; height: 450

   ...
}

Чтобы распутать прямой вызов функций через setInterval, мы заменяем вызовы setInterval двумя таймерами, которые будут запрашивать перерисовку. Таймер срабатывает через короткий интервал и позволяет нам выполнить некоторый код. Поскольку мы не можем указать функции рисования, какую операцию мы хотим инициировать, мы определяем для каждой операции логический флаг, запрашивающий операцию, а затем запускаем запрос на перерисовку.

Вот код операции строки. Операция очистки аналогична.

...
property bool requestLine: false

Timer {
    id: lineTimer
    interval: 40
    repeat: true
    triggeredOnStart: true
    onTriggered: {
        canvas.requestLine = true
        canvas.requestPaint()
    }
}

Component.onCompleted: {
    lineTimer.start()
}
...

Теперь у нас есть указание, какую операцию (линия или очистка, или даже обе) нам нужно выполнить во время операции onPaint. Поскольку мы вводим обработчик onPaint для каждого запроса на рисование, нам нужно извлечь инициализацию переменной в элемент холста.

Canvas {
    ...
    property real hue: 0
    property real lastX: width * Math.random();
    property real lastY: height * Math.random();
    ...
}

Теперь наша функция рисования должна выглядеть так:

onPaint: {
    var context = getContext('2d')
    if(requestLine) {
        line(context)
        requestLine = false
    }
    if(requestBlank) {
        blank(context)
        requestBlank = false
    }
}

Функция line была извлечена для холста в качестве аргумента.

function line(context) {
    context.save();
    context.translate(canvas.width/2, canvas.height/2);
    context.scale(0.9, 0.9);
    context.translate(-canvas.width/2, -canvas.height/2);
    context.beginPath();
    context.lineWidth = 5 + Math.random() * 10;
    context.moveTo(lastX, lastY);
    lastX = canvas.width * Math.random();
    lastY = canvas.height * Math.random();
    context.bezierCurveTo(canvas.width * Math.random(),
        canvas.height * Math.random(),
        canvas.width * Math.random(),
        canvas.height * Math.random(),
        lastX, lastY);

    hue += Math.random()*0.1
    if(hue > 1.0) {
        hue -= 1
    }
    context.strokeStyle = Qt.hsla(hue, 0.5, 0.5, 1.0);
    // context.shadowColor = 'white';
    // context.shadowBlur = 10;
    context.stroke();
    context.restore();
}

Самым большим изменением стало использование функций QML Qt.rgba() и Qt.hsla(), которые требовали адаптации значений к используемому в QML диапазону 0,0 … 1,0.

То же самое относится и к функции blank.

function blank(context) {
    context.fillStyle = Qt.rgba(0,0,0,0.1)
    context.fillRect(0, 0, canvas.width, canvas.height);
}

Окончательный результат будет выглядеть примерно так.

Окончательный результат
Окончательный результат

Теги

CanvasGUI / Графический интерфейс пользователяHTML5JavaScriptQMLQtQtQuickПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.