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() 函數以下繪圖操作現在將被此路徑剪輯。通過恢復先前的狀態或將剪輯區域設置為整個畫布,則會禁用剪輯效果。
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()
}
}
除了移動畫布還可以使用 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 元素創建一個小的繪圖應用程序。
為此,我們使用行定位器在場景頂部安排四個彩色方塊。彩色方塊是一個填充顏色的簡單矩形鼠標區域,用于檢測鼠標點擊事件。
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 畫布運行的移植螺旋圖形圖形
是不是很容易?
** 熒光線 **
這是 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);
}
最后的結果將會與此類似。
一些比較有用的參考連接: