Following my last blog post about using Rust and Lyon to create custom shapes. I’m happy to announce the creation of Pikasso, a very simple drawing program intended to be used on Plasma Mobile.
Pikasso is very basic and only supports drawing with the mouse/finger and adding rectangles and circles to the scene. An undo feature is also available as well as the possibility to export your beautiful artworks to SVGs. As you can see, Pikasso is not intended to be replacements for Krita. If you want a powerful drawing application just use Krita, it’s awesome. The scope of Pikasso is more similar to Kolourpaint or Paint.exe and intended for children to play a bit with it on Plasma Mobile.
Behind the scene
Behind the scene, Pikasso uses Rust and Lyon to do all the drawing.
The entire drawing area is just one QQuickItem
with many QSGNodes
.
This makes Pikasso hardware accelerated.
When drawing, Pikasso creates DrawEvents
for storing the drawing.
The DrawEvent
contains a QPainterPath
and a few metadata (line width,
pen color and if the QPainterPath
should be filled or not. This is inspired by the
QtWidgets Scribble example
and the Digikam sketch widgets. Using a QPainterPath
replaces the usage of the std::variant
from my
last blog post.
Thanks to Milian on IRC for giving me the hint about just using a QPainterPath.
This has the advantage of integrating better with the rest of the Qt
ecosystem sine QPainterPath is used to store reusable instructions for
QPainter but also supports more features like text rendering. Another
advantage of using a QPainterPath
is that we get an SVG export feature
almost for free with a QSvgGenerator
.
See the MR from Jonah about this feature..
QPainterPath can be natively used with QPainter, but it can also be in
a very simple way transformed to a LyonBuilder
object.
rust::Box<LyonBuilder> painterPathToBuilder(const QPainterPath &path)
{
auto lyonBuilder = new_builder();
for (int i = 0; i < path.elementCount(); i++) {
const auto element = path.elementAt(i);
if (element.isLineTo()) {
lyonBuilder->line_to(LyonPoint { static_cast<float>(element.x), static_cast<float>(element.y) });
} else if (element.isMoveTo()) {
lyonBuilder->move_to(LyonPoint { static_cast<float>(element.x), static_cast<float>(element.y) });
} else if (element.type == QPainterPath::ElementType::CurveToElement) {
// Cubic is encoded with ctrl1 -> CurveToElement, ctrl2 -> CurveToDataElement and to -> CurveToDataElement
Q_ASSERT(i + 2 < path.elementCount() && "CurveToElement doesn't have data");
const auto ctrl1 = path.elementAt(i);
const auto ctrl2 = path.elementAt(i + 1);
const auto to = path.elementAt(i + 2);
lyonBuilder->cubic_bezier_to(
LyonPoint { static_cast<float>(ctrl1.x), static_cast<float>(ctrl1.y) },
LyonPoint { static_cast<float>(ctrl2.x), static_cast<float>(ctrl2.y) },
LyonPoint { static_cast<float>(to.x), static_cast<float>(to.y) }
);
i += 2; // we analysed tree elements instead of just one
}
}
return lyonBuilder;
}
And once we have a LyonBuilder
object we can render our geometries
to the screen. This part didn’t change since the last post.
auto node = new QSGGeometryNode;
auto builder = painterPathToBuilder(drawEvent.path);
LyonGeometry lyonGeometry;
if (!drawEvent.fill) {
lyonGeometry = build_stroke(std::move(builder), drawEvent.penWidth);
} else {
lyonGeometry = build_fill(std::move(builder));
}
QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(),
lyonGeometry.vertices.size(), lyonGeometry.indices.size());
QSGGeometry::Point2D *points = geometry->vertexDataAsPoint2D();
std::size_t i = 0;
for (const auto &vertice: lyonGeometry.vertices) {
points[i].set(vertice.x, vertice.y);
i++;
}
quint16* indices = geometry->indexDataAsUShort();
i = 0;
for (const auto indice: lyonGeometry.indices) {
indices[i] = indice;
i++;
}
Pikasso, needs to add new nodes for each new DrawEvent but only update
the geometry of the last DrawEvent
since it’s the only one who could
have changed. This makes the painting experience very smooth.
For those interested, this is the code for the updatePaintNodeData
method. It’s a bit more complex since Pikasso also
handles undo events and needs to cleanup its removed QSGNodes.
Antialiasing
The last problem to solve for Pikasso was antialiasing since the figures on the
screen looked pixelized. This was easy to solve by enabling Multisample
Antialiasing on my QML DrawingArea. This can be done using the layer.samples
property:
DrawingArea {
anchors.fill: parent
layer.enabled: true
layer.samples: 4
}
Future goals
I think Pikasso is already good enough for its intended usage scope. That I might do in the future is to use a similar technique to add annotation supports to KQuickImageEditor, a library I developed for editing images before sending them in NeoChat. This will need a bit of thinking and a lot of refactoring in KQuickImageEditor first.
Comments
With an account on the Fediverse or Mastodon, you can respond to this post. Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one. Known non-private replies are displayed below.
Learn how this is implemented here.