Портирование из элемента canvas HTML5 в QML
Перенос с элемента 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);
}
Окончательный результат будет выглядеть примерно так.