187 lines
3.8 KiB
QML
187 lines
3.8 KiB
QML
import QtQuick
|
|
|
|
Canvas {
|
|
id: root
|
|
|
|
property rect dirtyRect: Qt.rect(0, 0, 0, 0)
|
|
property bool frameQueued: false
|
|
property bool fullRepaintPending: true
|
|
property point lastPoint: Qt.point(0, 0)
|
|
property real minPointDistance: 2.0
|
|
property color penColor: "white"
|
|
property real penWidth: 4
|
|
property var pendingSegments: []
|
|
property bool strokeActive: false
|
|
property var strokes: []
|
|
|
|
function appendPoint(x, y) {
|
|
if (!strokeActive || strokes.length === 0)
|
|
return;
|
|
const dx = x - lastPoint.x;
|
|
const dy = y - lastPoint.y;
|
|
|
|
if ((dx * dx + dy * dy) < (minPointDistance * minPointDistance))
|
|
return;
|
|
const x1 = lastPoint.x;
|
|
const y1 = lastPoint.y;
|
|
const x2 = x;
|
|
const y2 = y;
|
|
|
|
strokes[strokes.length - 1].push(Qt.point(x2, y2));
|
|
|
|
pendingSegments.push({
|
|
dot: false,
|
|
x1: x1,
|
|
y1: y1,
|
|
x2: x2,
|
|
y2: y2
|
|
});
|
|
|
|
lastPoint = Qt.point(x2, y2);
|
|
queueDirty(segmentDirtyRect(x1, y1, x2, y2));
|
|
}
|
|
|
|
function beginStroke(x, y) {
|
|
const p = Qt.point(x, y);
|
|
strokes.push([p]);
|
|
lastPoint = p;
|
|
strokeActive = true;
|
|
|
|
pendingSegments.push({
|
|
dot: true,
|
|
x: x,
|
|
y: y
|
|
});
|
|
|
|
queueDirty(pointDirtyRect(x, y));
|
|
}
|
|
|
|
function clear() {
|
|
strokes = [];
|
|
pendingSegments = [];
|
|
dirtyRect = Qt.rect(0, 0, 0, 0);
|
|
fullRepaintPending = true;
|
|
markDirty(Qt.rect(0, 0, width, height));
|
|
}
|
|
|
|
function drawDot(ctx, x, y) {
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, penWidth / 2, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
function drawSegment(ctx, x1, y1, x2, y2) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
function endStroke() {
|
|
strokeActive = false;
|
|
}
|
|
|
|
function pointDirtyRect(x, y) {
|
|
const pad = penWidth + 2;
|
|
return Qt.rect(x - pad, y - pad, pad * 2, pad * 2);
|
|
}
|
|
|
|
function queueDirty(r) {
|
|
dirtyRect = unionRects(dirtyRect, r);
|
|
|
|
if (frameQueued)
|
|
return;
|
|
frameQueued = true;
|
|
|
|
requestAnimationFrame(function () {
|
|
frameQueued = false;
|
|
|
|
if (dirtyRect.width > 0 && dirtyRect.height > 0) {
|
|
markDirty(dirtyRect);
|
|
dirtyRect = Qt.rect(0, 0, 0, 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
function replayAll(ctx) {
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
for (const stroke of strokes) {
|
|
if (!stroke || stroke.length === 0)
|
|
continue;
|
|
if (stroke.length === 1) {
|
|
const p = stroke[0];
|
|
drawDot(ctx, p.x, p.y);
|
|
continue;
|
|
}
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(stroke[0].x, stroke[0].y);
|
|
for (let i = 1; i < stroke.length; ++i)
|
|
ctx.lineTo(stroke[i].x, stroke[i].y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function requestFullRepaint() {
|
|
fullRepaintPending = true;
|
|
markDirty(Qt.rect(0, 0, width, height));
|
|
}
|
|
|
|
function segmentDirtyRect(x1, y1, x2, y2) {
|
|
const pad = penWidth + 2;
|
|
const left = Math.min(x1, x2) - pad;
|
|
const top = Math.min(y1, y2) - pad;
|
|
const right = Math.max(x1, x2) + pad;
|
|
const bottom = Math.max(y1, y2) + pad;
|
|
return Qt.rect(left, top, right - left, bottom - top);
|
|
}
|
|
|
|
function unionRects(a, b) {
|
|
if (a.width <= 0 || a.height <= 0)
|
|
return b;
|
|
if (b.width <= 0 || b.height <= 0)
|
|
return a;
|
|
|
|
const left = Math.min(a.x, b.x);
|
|
const top = Math.min(a.y, b.y);
|
|
const right = Math.max(a.x + a.width, b.x + b.width);
|
|
const bottom = Math.max(a.y + a.height, b.y + b.height);
|
|
|
|
return Qt.rect(left, top, right - left, bottom - top);
|
|
}
|
|
|
|
anchors.fill: parent
|
|
contextType: "2d"
|
|
renderStrategy: Canvas.Threaded
|
|
renderTarget: Canvas.Image
|
|
|
|
onHeightChanged: requestFullRepaint()
|
|
onPaint: region => {
|
|
const ctx = getContext("2d");
|
|
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
ctx.lineWidth = penWidth;
|
|
ctx.strokeStyle = penColor;
|
|
ctx.fillStyle = penColor;
|
|
|
|
if (fullRepaintPending) {
|
|
fullRepaintPending = false;
|
|
replayAll(ctx);
|
|
pendingSegments = [];
|
|
return;
|
|
}
|
|
|
|
for (const seg of pendingSegments) {
|
|
if (seg.dot)
|
|
drawDot(ctx, seg.x, seg.y);
|
|
else
|
|
drawSegment(ctx, seg.x1, seg.y1, seg.x2, seg.y2);
|
|
}
|
|
|
|
pendingSegments = [];
|
|
}
|
|
onWidthChanged: requestFullRepaint()
|
|
}
|