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

一些比较有用的参考连接:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容