QML Book 第七章 畫布(Canvas)元素 2

7.4 圖片繪制

QML 畫布支持來自多個來源的圖像繪制。要在畫布中使用圖像,需要首先加載圖像。我們將使用 Component.onCompleted 處理程序在我們的示例中加載映像。

    onPaint: {
        var ctx = getContext("2d")


        // draw an image
        ctx.drawImage('assets/ball.png', 10, 10)

        // store current context setup
        ctx.save()
        ctx.strokeStyle = '#ff2a68'
        // create a triangle as clip region
        ctx.beginPath()
        ctx.moveTo(110,10)
        ctx.lineTo(155,10)
        ctx.lineTo(135,55)
        ctx.closePath()
        // translate coordinate system
        ctx.clip()  // create clip from the path
        // draw image with clip applied
        ctx.drawImage('assets/ball.png', 100, 10)
        // draw stroke around path
        ctx.stroke()
        // restore previous context
        ctx.restore()

    }

    Component.onCompleted: {
        loadImage("assets/ball.png")
    }

左側顯示我們的球圖像畫在 10x10 的左上角位置。右側的圖像顯示了應用了剪輯路徑效果的球。 圖像和任何其他路徑可以使用路徑剪輯。通過定義路徑并調用 clip() 函數來應用剪輯。所有 clip() 函數以下繪圖操作現在將被此路徑剪輯。通過恢復先前的狀態或將剪輯區域設置為整個畫布,則會禁用剪輯效果。

canvas_image

7.5 轉換效果

畫布允許我們以多種方式轉換坐標系。這與 QML 項目提供的轉換非常相似。 我們可以縮放(scale),旋轉(rotate),移動(translate)。與 QML 不同的是,轉換的原點始終是畫布原點。例如,如果要圍繞它的中心擴展路徑,我們需要將畫布原點映射到路徑的中心。我們也可以使用 transform 方法應用更復雜的形變。

// transform.qml

import QtQuick 2.5

Canvas {
    id: root
    width: 240; height: 120
    onPaint: {
        var ctx = getContext("2d")
        ctx.strokeStyle = "blue"
        ctx.lineWidth = 4

        ctx.beginPath()
        ctx.rect(-20, -20, 40, 40)
        ctx.translate(120,60)
        ctx.stroke()

        // draw path now rotated
        ctx.strokeStyle = "green"
        ctx.rotate(Math.PI/4)
        ctx.stroke()
    }
}
transform.png

除了移動畫布還可以使用 scale(x,y) 圍繞 x 和 y 軸的刻度進行縮放,使用 rotate(angle) 旋轉,其中角度以半徑(360度= 2 * Math.PI)給出,并使用 setTransform(m11,m12,m21,m22,dx,dy) 進行矩陣變換。

** 注意: **
要重置任何轉換,您可以調用 resetTransform() 函數將轉換矩陣設置回單位矩陣:

ctx.resetTransform()

7.6 組合模式

組合允許我們繪制一個形狀并將其與現有圖像進行混合。畫布支持使用 globalCompositeOperation(mode) 操作的多個組合模式:

  • source-over
  • source-in
  • source-out
  • source-atop
    onPaint: {
        var ctx = getContext("2d")
        ctx.globalCompositeOperation = "xor"
        ctx.fillStyle = "#33a9ff"

        for(var i=0; i<40; i++) {
            ctx.beginPath()
            ctx.arc(Math.random()*400, Math.random()*200, 20, 0, 2*Math.PI)
            ctx.closePath()
            ctx.fill()
        }
    }

這個小例子遍歷了復合模式的列表,并生成一個帶圓的矩形。

    property var operation : [
        'source-over', 'source-in', 'source-over',
        'source-atop', 'destination-over', 'destination-in',
        'destination-out', 'destination-atop', 'lighter',
        'copy', 'xor', 'qt-clear', 'qt-destination',
        'qt-multiply', 'qt-screen', 'qt-overlay', 'qt-darken',
        'qt-lighten', 'qt-color-dodge', 'qt-color-burn',
        'qt-hard-light', 'qt-soft-light', 'qt-difference',
        'qt-exclusion'
        ]

    onPaint: {
        var ctx = getContext('2d')

        for(var i=0; i<operation.length; i++) {
            var dx = Math.floor(i%6)*100
            var dy = Math.floor(i/6)*100
            ctx.save()
            ctx.fillStyle = '#33a9ff'
            ctx.fillRect(10+dx,10+dy,60,60)
            // TODO: does not work yet
            ctx.globalCompositeOperation = root.operation[i]
            ctx.fillStyle = '#ff33a9'
            ctx.globalAlpha = 0.75
            ctx.beginPath()
            ctx.arc(60+dx, 60+dy, 30, 0, 2*Math.PI)
            ctx.closePath()
            ctx.fill()
            ctx.restore()
        }
    }

7.7 像素緩沖

使用畫布時,我們可以從畫布中檢索像素數據來讀取或操作畫布的像素。要讀取圖像數據,請使用 createImageData(sw,sh) 或getImageData(sx,sy,sw,sh)。兩個函數都返回一個帶寬度,高度和數據變量的 ImageData 對象。數據變量包含以 RGBA 格式檢索的像素數據的一維數組,其中每個值在 0 到 255 的范圍內變化。要在畫布上設置像素,可以使用 putImageData(imagedata ,, dx,dy) 功能。

檢索畫布內容的另一種方法是將數據存儲到圖像中。 這可以通過 Canvas 函數 save(path) 或 toDataURL(mimeType) 來實現,后者函數返回一個可以被 Image 元素加載的圖像 url。

import QtQuick 2.5

Rectangle {
    width: 240; height: 120
    Canvas {
        id: canvas
        x: 10; y: 10
        width: 100; height: 100
        property real hue: 0.0
        onPaint: {
            var ctx = getContext("2d")
            var x = 10 + Math.random(80)*80
            var y = 10 + Math.random(80)*80
            hue += Math.random()*0.1
            if(hue > 1.0) { hue -= 1 }
            ctx.globalAlpha = 0.7
            ctx.fillStyle = Qt.hsla(hue, 0.5, 0.5, 1.0)
            ctx.beginPath()
            ctx.moveTo(x+5,y)
            ctx.arc(x,y, x/10, 0, 360)
            ctx.closePath()
            ctx.fill()
        }
        MouseArea {
            anchors.fill: parent
            onClicked: {
                var url = canvas.toDataURL('image/png')
                print('image url=', url)
                image.source = url
            }
        }
    }

    Image {
        id: image
        x: 130; y: 10
        width: 100; height: 100
    }

    Timer {
        interval: 1000
        running: true
        triggeredOnStart: true
        repeat: true
        onTriggered: canvas.requestPaint()
    }
}

在我們的小例子中,我們每秒在畫布左側畫一個小圓。當鼠標點擊時,畫布內容會被存儲并且作為圖像 url 賦值給在我們示例的右側的圖像并顯示。

7.8 畫布繪圖

在這個例子中,我們想使用 Canvas 元素創建一個小的繪圖應用程序。

canvaspaint

為此,我們使用行定位器在場景頂部安排四個彩色方塊。彩色方塊是一個填充顏色的簡單矩形鼠標區域,用于檢測鼠標點擊事件。

    Row {
        id: colorTools
        anchors {
            horizontalCenter: parent.horizontalCenter
            top: parent.top
            topMargin: 8
        }
        property variant activeSquare: red
        property color paintColor: "#33B5E5"
        spacing: 4
        Repeater {
            model: ["#33B5E5", "#99CC00", "#FFBB33", "#FF4444"]
            ColorSquare {
                id: red
                color: modelData
                active: parent.paintColor == color
                onClicked: {
                    parent.paintColor = color
                }
            }
        }
    }

顏色存儲在一個顏色數組作為模型數據和同時也將作為繪圖的顏色。當用戶點擊一個正方形時,方形的顏色被分配給名為colorTools 的對象的 paintColor 屬性。

為了能夠跟蹤畫布上的鼠標事件,我們有一個 MouseArea 覆蓋了 canvas 元素,并且連接了鼠標按下和位置改變的信號處理程序。

    Canvas {
        id: canvas
        anchors {
            left: parent.left
            right: parent.right
            top: colorTools.bottom
            bottom: parent.bottom
            margins: 8
        }
        property real lastX
        property real lastY
        property color color: colorTools.paintColor

        onPaint: {
            var ctx = getContext('2d')
            ctx.lineWidth = 1.5
            ctx.strokeStyle = canvas.color
            ctx.beginPath()
            ctx.moveTo(lastX, lastY)
            lastX = area.mouseX
            lastY = area.mouseY
            ctx.lineTo(lastX, lastY)
            ctx.stroke()
        }
        MouseArea {
            id: area
            anchors.fill: parent
            onPressed: {
                canvas.lastX = mouseX
                canvas.lastY = mouseY
            }
            onPositionChanged: {
                canvas.requestPaint()
            }
        }
    }

鼠標按鍵將最初的鼠標位置存儲在 lastX 和 lastY 屬性中。鼠標位置上的每一個變化觸發畫布上的繪制請求,這將導致調用 onPaint 處理程序。

為了最終繪制用戶筆畫,在 onPaint 處理程序中,我們開始一個新的路徑并移動到最后一個位置。然后,我們從鼠標區域收集新的位置,并將所選顏色的線條繪制到新位置。鼠標位置存儲為新的最后一個位置。

7.9 從 HTML5 移植畫布

將 HTML5 畫布圖形移植到 QML 畫布上是相當容易的。從成千上萬的例子中,我們選擇了一個并進行了嘗試。

** 螺旋圖 **

我們使用 Mozilla 項目的 螺旋圖 示例作為我們的基礎。原始的HTML5被作為畫布教程的一部分發布。

在這里我們需要改變幾行:

  • 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 畫布運行的移植螺旋圖形圖形

spirograph

是不是很容易?

** 熒光線 **

這是 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');

// initial start position
var lastX = context.canvas.width * Math.random();
var lastY = context.canvas.height * Math.random();
var hue = 0;

// closure function to draw
// a random bezier curve with random color with a glow effect
function line() {

    context.save();

    // scale with factor 0.9 around the center of canvas
    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;

    // our start position
    context.moveTo(lastX, lastY);

    // our new end position
    lastX = context.canvas.width * Math.random();
    lastY = context.canvas.height * Math.random();

    // random bezier curve, which ends on 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);

    // glow effect
    hue = hue + 10 * Math.random();
    context.strokeStyle = 'hsl(' + hue + ', 50%, 50%)';
    context.shadowColor = 'white';
    context.shadowBlur = 10;
    // stroke the curve
    context.stroke();
    context.restore();
}

// call line function every 50msecs
setInterval(line, 50);

function blank() {
    // makes the background 10% darker on each call
    context.fillStyle = 'rgba(0,0,0,0.1)';
    context.fillRect(0, 0, context.canvas.width, context.canvas.height);
}

// call blank function every 50msecs
setInterval(blank, 40);

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

在 HTML5 中,Context2D 對象可以在畫布上隨時繪制。在 QML 中,它只能指向 onPaint 處理程序。使用 setInterval 的定時器會在 HTML5 中觸發該行的行程或將屏幕空白。由于在 QML 中的不同處理,不可能只調用這些函數,因為我們需要通過 onPaint 處理程序。還需要調整顏色聲明。我們來看一下變化吧。

一切都從 canvas 元素開始。為了簡單起見,我們只使用 Canvas 元素作為 QML 文件的根元素。

import QtQuick 2.5

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

   ...
}

要通過 setInterval 解開對函數的直接調用,我們將使用兩個定時器替換 setInterval 調用,這些計時器將請求重新繪制。在短時間間隔后觸發定時器,并允許我們執行一些代碼。因為我們不能告訴 paint 函數我們想要觸發哪個操作,我們為每個操作定義一個 bool 標志來請求一個操作并觸發一個重繪請求。

這是線操作的代碼??瞻撞僮魇窍嗨频?。

...
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 元素中。

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
    }
}

為畫布提取線函數作為參數。

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 范圍。

同樣適用于空白功能。

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

最后的結果將會與此類似。

glowlines

一些比較有用的參考連接:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容