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
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 组织的另一个更复杂的接口。原来漂亮的发光线条有一些很不错的方面,这使得移植更具挑战性。
Pretty Glowing Lines
在 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);
}
最后的结果将会与此类似。
一些比较有用的参考连接:
- W3C HTML Canvas 2D Context Specification
- Mozilla Canvas Documentation
- HTML5 Canvas Tutorial