- PVSM.RU - https://www.pvsm.ru -

Работа с QML Canvas

Работа с QML Canvas В последнее время на хабре было много хороших постов, раскрывающих аспекты работы с QML: XMLHTTPRequest [1], Loader [2], GLSL [3], но до сих пор никто не упоминал, что Qt Quick 2.0 содержит также компонент Canvas [4], который даёт нам возможность (сюрприз!) рисовать. Синтаксис использования тот же, что и у HTML5 Canvas, но лично мне, как человеку, далекому от разработки для веба, это ни о чём не говорило.
Продемонстрировать работу с ним я хочу на примере создания каркаса для игры, который, при желании, легко можно будет переделать либо в старую добрую Snake [5], либо во что-то вроде Achtung, die Kurve! [6]

В проекте у нас будет два компонента: сцена (Scene.qml, корневой элемент) и игрок (Player.qml).
Начнём с игрока. Игрок в любой момент времени будет представлять собой движущийся объект, за котором будет оставаться линия. Движение характеризуется тремя параметрами: углом (так как действие происходит на плоскости, то он задаёт направление движения), скоростью движения и скоростью поворота. Так и запишем:

import QtQuick 2.0

Item {
    width: 30;
    height: 30;
    property int angle: 0;
    property real linearSpeed: 1.5; // скорость движения
    property real angularSpeed: 2.0; // скорость поворота (не угловая)
    function step() {
        x += Math.cos(angle*Math.PI/180)*linearSpeed;
        y += Math.sin(angle*Math.PI/180)*linearSpeed;
    }
    Rectangle {
        anchors.fill: parent;
        color: "black";
        antialiasing: true;
        radius: width/2;
    }
}

Двигать игрока теперь можно простым вызовом функции step(). Хорошо, но как насчёт управления? Чтобы не зависеть от какого-то одного способа ввода, поступим следующим образом: добавим игроку ещё два свойства

    property bool turnLeft: false;
    property bool turnRight: false;

А функцию step() дополним этой строкой:

    if (turnLeft) angle -= angularSpeed; else if (turnRight) angle += angularSpeed;

Само же управление повесим на клавиши:

    Keys.onPressed: {
        switch (event.key) {
        case Qt.Key_Left: turnLeft = true; break;
        case Qt.Key_Right: turnRight = true; break;
        }
    }
    Keys.onReleased: {
        switch (event.key) {
        case Qt.Key_Left: turnLeft = false; break;
        case Qt.Key_Right: turnRight = false; break;
        }
    }
    focus: true;

Самое время проверить, работает ли оно так, как задумывалось.
Начнём разрабатывать сцену. Тут всё просто — возьмём компонент Canvas и разместим на нём нашего игрока. Пока без всякого рисования.

import QtQuick 2.0

Canvas {
    id: canvas;
    width: 640;
    height: 480;
    antialiasing: true;
    Player {
        id: player;
    }
    Timer {
        id: timer;
        interval: 16;
        running: true;
        repeat: true
        onTriggered: {
            player.step();
        }
    }
}

Работает? Здорово. Теперь нужно начать каким-то образом оставлять за собой следы. Пусть игрок занимается этим самостоятельно. Добавим в самый конец нашего «игрового цикла» (timer.onTriggered) запрос на рисование.

    canvas.requestPaint();

Рисование происходит на неком контексте [7]. Контекст мы получаем в обработчике события рисования и передаём нашему игроку:

    onPaint: {
        var ctx = canvas.getContext("2d");
        player.draw(ctx);
    }

В документации Qt вы не найдёте описания функций работы с Canvas (что, в общем-то, логично), поэтому обратимся к сторонним источникам [8] (первый результат гугла по запросу «canvas functions»).
Для минимизации вычислительных затрат на каждой итерации мы будем лишь «дорисовывать» линию за игроком. То есть, придётся сохранять ещё и прошлые координаты игрока. Конечно, визуальная гладкость линии, нарисованной подобным образом, будет зависеть от точности представления вещественных чисел и может быть разной на различных платформах (я предупредил!). Простой пример работы с контекстом есть в описании функции lineTo() [9]. То, что нужно, теперь мы можем доработать игрока. Следите за руками.

    property color lineColor: "green";
    property real lastX: 0;
    property real lastY: 0;
    function step() {
        if (turnLeft) angle -= angularSpeed; else if (turnRight) angle += angularSpeed;
        lastX = x;
        lastY = y;
        x += Math.cos(angle*Math.PI/180)*linearSpeed;
        y += Math.sin(angle*Math.PI/180)*linearSpeed;
    }
    function draw(ctx) {
        ctx.beginPath();
        ctx.strokeStyle = lineColor;
        ctx.lineWidth = 5;
        var radius = width/2;
        ctx.moveTo(lastX+radius, lastY+radius);
        ctx.lineTo(x+radius, y+radius);
        ctx.stroke();
    }

В сумме у вас должно было получиться следующее:

Player.qml

import QtQuick 2.0

Item {
    width: 30;
    height: 30;
    property real angle: 0;
    property real linearSpeed: 2.0;
    property real angularSpeed: 2.0;
    property bool turnLeft: false;
    property bool turnRight: false;
    property color lineColor: "green";
    property real lastX: 0;
    property real lastY: 0;
    function step() {
        if (turnLeft) angle -= angularSpeed; else if (turnRight) angle += angularSpeed;
        lastX = x;
        lastY = y;
        x += Math.cos(angle*Math.PI/180)*linearSpeed;
        y += Math.sin(angle*Math.PI/180)*linearSpeed;
    }
    function draw(ctx) {
        ctx.beginPath();
        ctx.strokeStyle = lineColor;
        ctx.lineWidth = 5;
        var radius = width/2;
        ctx.moveTo(lastX+radius, lastY+radius);
        ctx.lineTo(x+radius, y+radius);
        ctx.stroke();
    }
    Keys.onPressed: {
        switch (event.key) {
        case Qt.Key_Left: turnLeft = true; break;
        case Qt.Key_Right: turnRight = true; break;
        }
    }
    Keys.onReleased: {
        switch (event.key) {
        case Qt.Key_Left: turnLeft = false; break;
        case Qt.Key_Right: turnRight = false; break;
        }
    }
    focus: true;
    Rectangle {
        anchors.fill: parent;
        color: "black";
        antialiasing: true;
        radius: width/2;
    }
}
Scene.qml

import QtQuick 2.0

Canvas {
    id: canvas;
    width: 640;
    height: 480;
    antialiasing: true;
    onPaint: {
        var ctx = canvas.getContext("2d");
        player.draw(ctx);
    }
    Player {
        id: player;
    }
    Timer {
        id: timer;
        interval: 16;
        running: true;
        repeat: true
        onTriggered: {
            player.step();
            canvas.requestPaint();
        }
    }
}

По умолчанию рисование происходит в кадровый буфер OpenGL, поэтому, если вы хотите более-менее одинакового отображения на различных устройствах (например, сглаживание не будет работать, если видеокарта не поддерживает Sample Buffers), рекомендую установить свойство Canvas renderTarget [10] в значение Canvas.Image. Кроме этого, немаловажную роль играет свойство renderStrategy [11].

Работа с QML Canvas

Если вы захотите написать клона игры Zatacka (Achtung, die Kurve!) [12], то вам нужно будет определять пересечения игроков с линией. Делается это достаточно легко: функция getImageData [13] возвращает объект типа CanvasImageData [14], содержащий свойство data. Этот одномерный массив содержит информацию о цвете заданных пикселей (подробнее [15]). Для определения пересечения остаётся лишь проверить пиксели в текущих координатах игрока на прозрачность (учтите, что вызовы getImageData довольно затратны, особенно, если рендеринг осуществляется в кадровый буфер). Функция могла бы выглядеть так:

    function check(ctx) {
        var c = ctx.getImageData(x+xSpeed*2-1, y+ySpeed*2-1, 2, 2).data;
        if (c[3]||c[7]||c[11]||c[15]) lose();
    }

Очистка игрового поля может производиться следующим образом:

    function clear() {
        var ctx = getContext("2d");
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

Если только одно ограничение — производительность. Прямо к холсту мы можем применить какой-нибудь шейдерный эффект:

import QtQuick 2.0
import QtGraphicalEffects 1.0

Item {
    width: 640;
    height: 480;
    Canvas {
        id: canvas;
        ...
    }
    DirectionalBlur {
        anchors.fill: canvas;
        source: canvas;
        angle: 90;
        length: 32;
        samples: 24;
    }
}

Работа с QML Canvas [16]

Спасибо за внимание.

Автор: epicfailguy93

Источник [17]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/qt-quick/33184

Ссылки в тексте:

[1] XMLHTTPRequest: http://habrahabr.ru/post/176665/

[2] Loader: http://habrahabr.ru/post/172509/

[3] GLSL: http://habrahabr.ru/post/166813/

[4] Canvas: http://qt-project.org/doc/qt-5.0/qtquick/qml-qtquick2-canvas.html

[5] Snake: http://en.wikipedia.org/wiki/Snake_%28game%29

[6] Achtung, die Kurve!: http://en.wikipedia.org/wiki/Zatacka

[7] контексте: http://qt-project.org/doc/qt-5.0/qtquick/qml-qtquick2-canvas.html#context-prop

[8] источникам: http://www.w3schools.com/tags/ref_canvas.asp

[9] lineTo(): http://www.w3schools.com/tags/canvas_lineto.asp

[10] renderTarget: http://qt-project.org/doc/qt-5.0/qtquick/qml-qtquick2-canvas.html#renderTarget-prop

[11] renderStrategy: http://qt-project.org/doc/qt-5.0/qtquick/qml-qtquick2-canvas.html#renderStrategy-prop

[12] Zatacka (Achtung, die Kurve!): http://zatacka.sourceforge.net/

[13] getImageData: http://qt-project.org/doc/qt-5.0/qtquick/qml-qtquick2-canvas.html#getImageData-method

[14] CanvasImageData: http://qt-project.org/doc/qt-5.0/qtquick/qml-qtquick2-canvasimagedata.html

[15] подробнее: http://www.w3schools.com/tags/canvas_imagedata_data.asp

[16] Image: http://storage1.static.itmages.com/i/13/0427/h_1367041234_5849419_d41d8cd98f.png

[17] Источник: http://habrahabr.ru/post/178151/