• 变换
  • 组合
  • 像素缓存
  • Canvas 绘制
  • 从 HTML5 移植

    变换

    Canvas中的“变形”,主要指的是坐标系的变换,而不是路径的变换。这与 QML 元素变换非常相似,都可以实现坐标系统的scale(缩放)、rotate(旋转)和translate(平移);不同的是,变换的原点是画布原点。例如,如果以一个路径的中心点为定点进行缩放,那么,你需要现将画布原点移动到路径中心点。我们也可以使用变换函数实现复杂的变换。理解“变换是针对坐标系的”这一点非常重要,有时候可以避免很多意外的结果。

    1. import QtQuick 2.0
    2.  
    3. Canvas {
    4. id: root
    5. width: 240; height: 120
    6. onPaint: {
    7. var ctx = getContext("2d")
    8. ctx.strokeStyle = "blue"
    9. ctx.lineWidth = 4
    10.  
    11. ctx.translate(120, 60)
    12. ctx.strokeRect(-20, -20, 40, 40)
    13.  
    14. // draw path now rotated
    15. ctx.strokeStyle = "green"
    16. ctx.rotate(Math.PI / 4)
    17. ctx.strokeRect(-20, -20, 40, 40)
    18. ctx.restore()
    19. }
    20. }

    运行结果如下:画布变换

    通过调用resetTransform()函数,可以将变换矩阵重置为单位矩阵:

    1. ctx.resetTransform()

    组合

    组合意思是,将你绘制的图形与已存在的像素做一些融合操作。canvas支持几种组合方式,使用globalCompositeOperation可以设置组合的模式。如下代码所示,我们可以看到组合的相应表现:

    1. import QtQuick 2.0
    2.  
    3. Canvas {
    4. id: root
    5. width: 600; height: 450
    6. property var operation : [
    7. 'source-over', 'source-in', 'source-over',
    8. 'source-atop', 'destination-over', 'destination-in',
    9. 'destination-out', 'destination-atop', 'lighter',
    10. 'copy', 'xor', 'qt-clear', 'qt-destination',
    11. 'qt-multiply', 'qt-screen', 'qt-overlay', 'qt-darken',
    12. 'qt-lighten', 'qt-color-dodge', 'qt-color-burn',
    13. 'qt-hard-light', 'qt-soft-light', 'qt-difference',
    14. 'qt-exclusion'
    15. ]
    16.  
    17. onPaint: {
    18. var ctx = getContext('2d')
    19.  
    20. for(var i=0; i<operation.length; i++) {
    21. var dx = Math.floor(i%6)*100
    22. var dy = Math.floor(i/6)*100
    23. ctx.save()
    24. ctx.fillStyle = '#33a9ff'
    25. ctx.fillRect(10+dx,10+dy,60,60)
    26. // TODO: does not work yet
    27. ctx.globalCompositeOperation = root.operation[i]
    28. ctx.fillStyle = '#ff33a9'
    29. ctx.globalAlpha = 0.75
    30. ctx.beginPath()
    31. ctx.arc(60+dx, 60+dy, 30, 0, 2*Math.PI)
    32. ctx.closePath()
    33. ctx.fill()
    34. ctx.restore()
    35. }
    36. }
    37. }

    代码运行结果如下:canvas 组合

    像素缓存

    使用canvas,你可以将canvas内容的像素数据读取出来,并且能够针对这些数据做一些操作。

    使用createImageData(sw, sh)getImageData(sx, sy, sw, sh)函数可以读取图像数据。这两个函数都会返回一个ImageData对象,该对象具有widthheightdata等变量。data包含一个以 RGBA 格式存储的像素一维数组,其每一个分量值的范围都是 [0, 255]。如果要设置画布上面的像素,可以使用putImageData(imagedata, dx, dy)函数。

    另外一个获取画布内容的方法是,将数据保存到一个图片。这可以通过Canvas的函数save(path)toDataURL(mimeType)实现,后者会返回一个图像的 URL,可以供Image元素加载图像。

    1. import QtQuick 2.0
    2.  
    3. Rectangle {
    4. width: 240; height: 120
    5. Canvas {
    6. id: canvas
    7. x: 10; y: 10
    8. width: 100; height: 100
    9. property real hue: 0.0
    10. onPaint: {
    11. var ctx = getContext("2d")
    12. var x = 10 + Math.random(80)*80
    13. var y = 10 + Math.random(80)*80
    14. hue += Math.random()*0.1
    15. if(hue > 1.0) { hue -= 1 }
    16. ctx.globalAlpha = 0.7
    17. ctx.fillStyle = Qt.hsla(hue, 0.5, 0.5, 1.0)
    18. ctx.beginPath()
    19. ctx.moveTo(x+5,y)
    20. ctx.arc(x,y, x/10, 0, 360)
    21. ctx.closePath()
    22. ctx.fill()
    23. }
    24. MouseArea {
    25. anchors.fill: parent
    26. onClicked: {
    27. var url = canvas.toDataURL('image/png')
    28. print('image url=', url)
    29. image.source = url
    30. }
    31. }
    32. }
    33.  
    34. Image {
    35. id: image
    36. x: 130; y: 10
    37. width: 100; height: 100
    38. }
    39.  
    40. Timer {
    41. interval: 1000
    42. running: true
    43. triggeredOnStart: true
    44. repeat: true
    45. onTriggered: canvas.requestPaint()
    46. }
    47. }

    在上面的例子中,我们创建了两个画布,左侧的画布每一秒产生一个圆点;鼠标点击会将画布内容保存,并且生成一个图像的 URL,右侧则会显示这个图像。

    Canvas 绘制

    下面我们利用Canvas元素创建一个画板程序。我们程序的运行结果如下所示:canvas 画板窗口上方是调色板,用于设置画笔颜色。色板是一个填充了颜色的矩形,其中覆盖了一个鼠标区域,用于检测鼠标点击事件。

    1. Row {
    2. id: colorTools
    3. anchors {
    4. horizontalCenter: parent.horizontalCenter
    5. top: parent.top
    6. topMargin: 8
    7. }
    8. property color paintColor: "#33B5E5"
    9. spacing: 4
    10. Repeater {
    11. model: ["#33B5E5", "#99CC00", "#FFBB33", "#FF4444"]
    12. ColorSquare {
    13. id: red
    14. color: modelData
    15. active: parent.paintColor === color
    16. onClicked: {
    17. parent.paintColor = color
    18. }
    19. }
    20. }
    21. }

    调色板所支持的颜色保存在一个数组中,画笔的当前颜色则保存在paintColor属性。当用户点击调色板的一个色块,该色块的颜色就会被赋值给paintColor属性。

    为了监听鼠标事件,我们在画布上面覆盖了一个鼠标区域,利用鼠标按下和位置改变的信号处理函数完成绘制:

    1. Canvas {
    2. id: canvas
    3. anchors {
    4. left: parent.left
    5. right: parent.right
    6. top: colorTools.bottom
    7. bottom: parent.bottom
    8. margins: 8
    9. }
    10. property real lastX
    11. property real lastY
    12. property color color: colorTools.paintColor
    13.  
    14. onPaint: {
    15. var ctx = getContext('2d')
    16. ctx.lineWidth = 1.5
    17. ctx.strokeStyle = canvas.color
    18. ctx.beginPath()
    19. ctx.moveTo(lastX, lastY)
    20. lastX = area.mouseX
    21. lastY = area.mouseY
    22. ctx.lineTo(lastX, lastY)
    23. ctx.stroke()
    24. }
    25. MouseArea {
    26. id: area
    27. anchors.fill: parent
    28. onPressed: {
    29. canvas.lastX = mouseX
    30. canvas.lastY = mouseY
    31. }
    32. onPositionChanged: {
    33. canvas.requestPaint()
    34. }
    35. }
    36. }

    鼠标左键按下时,其初始位置保存在lastXlastY两个属性。鼠标位置的改变会请求画布进行重绘,该请求则会调用onPaint()处理函数。

    最后,为了绘制用户笔记,在onPaint()处理函数中,我们首先创建了一个新的路径,将其移动到最后的位置,然后我们从鼠标区域获得新的位置,在最后的位置与新的位置之间绘制直线,同时,将当前鼠标位置(也就是新的位置)设置为新的最后的位置。

    从 HTML5 移植

    由于 QML 的Canvas对象由 HTML 5 的 canvas 标签借鉴而来,将 HTML 5 的 canvas 应用移植到 QML Canvas也是相当容易。我们以 Mozilla 提供的繁华曲线页面为例,演示移植的过程。可以在这里看到该页面的运行结果。下面是 HTML 5 canvas 的脚本部分:

    1. function draw() {
    2. var ctx = document.getElementById('canvas').getContext('2d');
    3. ctx.fillRect(0,0,300,300);
    4. for (var i=0;i<3;i++) {
    5. for (var j=0;j<3;j++) {
    6. ctx.save();
    7. ctx.strokeStyle = "#9CFF00";
    8. ctx.translate(50+j*100,50+i*100);
    9. drawSpirograph(ctx,20*(j+2)/(j+1),-8*(i+3)/(i+1),10);
    10. ctx.restore();
    11. }
    12. }
    13. }
    14. function drawSpirograph(ctx,R,r,O){
    15. var x1 = R-O;
    16. var y1 = 0;
    17. var i = 1;
    18. ctx.beginPath();
    19. ctx.moveTo(x1,y1);
    20. do {
    21. if (i>20000) break;
    22. var x2 = (R+r)*Math.cos(i*Math.PI/72) - (r+O)*Math.cos(((R+r)/r)*(i*Math.PI/72))
    23. var y2 = (R+r)*Math.sin(i*Math.PI/72) - (r+O)*Math.sin(((R+r)/r)*(i*Math.PI/72))
    24. ctx.lineTo(x2,y2);
    25. x1 = x2;
    26. y1 = y2;
    27. i++;
    28. } while (x2 != R-O && y2 != 0 );
    29. ctx.stroke();
    30. }
    31. draw();

    这里我们只解释如何进行移植,有关繁花曲线的算法则不在我们的阐述范围之内。幸运的是,我们需要改变的代码很少,因而这里也会很短。

    HTML 按照顺序执行,draw() 会成为脚本的入口函数。但是在 QML 中,绘制必须在 onPaint 中完成,因此,我们需要将 draw() 函数的调用移至 onPaint。通常我们会在 onPaint 中获取绘制上下文,因此,我们将给 draw() 函数添加一个参数,用于接受Context2D对象。事实上,这就是我们所有的修改。移植之后的 QML 如下所示:

    1. import QtQuick 2.2
    2.  
    3. Canvas {
    4. id: root
    5. width: 300; height: 300
    6.  
    7. onPaint: {
    8. var ctx = getContext("2d");
    9. draw(ctx);
    10. }
    11.  
    12. function draw (ctx) {
    13. ctx.fillRect(0, 0, 300, 300);
    14. for (var i = 0; i < 3; i++) {
    15. for (var j = 0; j < 3; j++) {
    16. ctx.save();
    17. ctx.strokeStyle = "#9CFF00";
    18. ctx.translate(50 + j * 100, 50 + i * 100);
    19. drawSpirograph(ctx, 20 * (j + 2) / (j + 1), -8 * (i + 3) / (i + 1), 10);
    20. ctx.restore();
    21. }
    22. }
    23. }
    24.  
    25. function drawSpirograph (ctx, R, r, O) {
    26. var x1 = R - O;
    27. var y1 = 0;
    28. var i = 1;
    29. ctx.beginPath();
    30. ctx.moveTo(x1, y1);
    31. do {
    32. if (i > 20000) break;
    33. var x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72))
    34. var y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72))
    35. ctx.lineTo(x2, y2);
    36. x1 = x2;
    37. y1 = y2;
    38. i++;
    39. } while (x2 != R-O && y2 != 0 );
    40. ctx.stroke();
    41. }
    42. }

    运行一下这段代码:

    移植自 HTML 5 canvas