Welcome back. In the last lesson, we successfully captured raw user input into a Stroke data structure, a crucial first step. However, a sequence of raw (x, y, pressure) points is just data; it's not yet a visually pleasing line. If we were to simply connect these points with straight lines, the result would be jagged and unnatural, betraying the fluid motion of the user's hand.
This lesson bridges that gap between raw data and polished rendering. We will tackle the core challenge of real-time graphics: transforming a discrete series of input samples into a continuous, smooth, and expressive curve. You will learn how to generate a smooth, variable-width SkPath from the raw points and pressure values stored in your Stroke object. This involves a fascinating blend of interpolation mathematics and practical rendering techniques, perfectly suited for building a high-performance drawing application.
From Jagged Points to Smooth Curves
The raw points from a touch device or stylus are often noisy and unevenly spaced. Directly connecting them with SkPath::lineTo creates a polyline, which looks amateurish and fails to capture the artist's intent.

To create a smooth curve, we must use an interpolation technique. A popular and effective method for this is using Catmull-Rom splines. A key property that makes them ideal for this task is that the resulting curve passes through each of the input points, making them feel intuitive and predictable for the user.
A Catmull-Rom spline generates a curve segment between two points, say P1 and P2, by using two additional "control" points, P0 and P3, to define the tangents at P1 and P2. This creates a smooth transition from one segment to the next.
The following video provides an excellent, practical introduction to Catmull-Rom splines, including an intuitive explanation and a C++ implementation.
Programming & Using Splines - Part#1
This video from javidx9 is a great primer on Catmull-Rom splines. It builds the concept from the ground up, which will be valuable for implementing our path-smoothing algorithm.
Please watch the following sections: The introduction, which explains what a Catmull-Rom spline is and the four-point system (P0, P1, P2, P3). The explanation using "influential circles," which provides a great non-mathematical intuition for how the curve is shaped. The C++ implementation of the getSplinePoint function. This demonstrates how to translate the spline equations into code. The explanation of how to chain multiple segments together to form a continuous path from a list of points.
Implementing the Spline in C++
Inspired by the video, we can write a function that calculates a point on the spline. For any segment between P1 and P2, we can find the interpolated point at a parameter t (where ) using the Catmull-Rom basis functions.
Here is a C++ function that does this, using Skia's SkPoint for convenience:
// Calculates a point on the Catmull-Rom spline.
// p0, p1, p2, p3 are the four control points.
// t is the interpolation factor, from 0.0 to 1.0.
SkPoint getSplinePoint(SkPoint p0, SkPoint p1, SkPoint p2, SkPoint p3, float t) {
const float t2 = t * t;
const float t3 = t2 * t;
const float q0 = -t3 + 2.0f * t2 - t;
const float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f;
const float q2 = -3.0f * t3 + 4.0f * t2 + t;
const float q3 = t3 - t2;
const float tx = 0.5f * (p0.x() * q0 + p1.x() * q1 + p2.x() * q2 + p3.x() * q3);
const float ty = 0.5f * (p0.y() * q0 + p1.y() * q1 + p2.y() * q2 + p3.y() * q3);
return {tx, ty};
}
To create a continuous curve from our list of InputPoints, we iterate through them. For each point p[i], we treat it as P1 and p[i+1] as P2. The control points P0 and P3 would then be p[i-1] and p[i+2].
Handling Endpoints: What about the very first and last segments, where p[i-1] or p[i+2] don't exist? A common and effective trick is to duplicate the endpoint.
- For the first segment (from
p[0]top[1]), we can usep[0]asP0. So the points are(p[0], p[0], p[1], p[2]). - For the last segment (from
p[n-2]top[n-1]), we can usep[n-1]asP3. So the points are(p[n-3], p[n-2], p[n-1], p[n-1]).
By sampling t at small increments (e.g., 0.1) for each segment and connecting the resulting points with SkPath::lineTo, we can build a smooth SkPath representing the centerline of our stroke.
From Centerline to Variable-Width Stroke
We now have a smooth centerline. The next challenge is to give it a variable thickness based on the stylus pressure. A naive approach of drawing many circles along the path is inefficient and produces ugly, overlapping artifacts.
A more robust and performant technique used in professional graphics is stroke expansion. This involves constructing a new filled shape that represents the thickened stroke. The most direct way to implement this for real-time rendering is to generate a triangle strip.
A triangle strip is a mesh of connected triangles that follows our centerline. We calculate two vertices for each point on our centerline, offset perpendicularly by an amount proportional to the pressure.

The process looks like this:
- Interpolate Pressure: Just as we interpolated the
xandycoordinates, we can apply the same Catmull-Rom math to thepressurevalues from ourInputPoints to get a smooth pressure value for any pointtalong the curve. - Calculate Normals: For each point on the smooth centerline, we need to find the direction perpendicular to the curve—the normal vector. We can approximate the tangent at a point
P_iby taking the vector from the previous point to the next (P_{i+1} - P_{i-1}). The normal is then found by rotating this tangent vector by 90 degrees. - Generate Vertices: At each centerline point
P_i, with its interpolated pressure giving awidth_i, we calculate two new points for our outline:Vertex_A = P_i + Normal_i * (width_i / 2)Vertex_B = P_i - Normal_i * (width_i / 2)
- Build the Mesh: These
Vertex_AandVertex_Bpairs form a sequence of quads along the path. This can be efficiently represented and drawn as a triangle strip.
Skia provides the SkVertices class to handle this kind of custom mesh rendering. You can use an SkVertices::Builder in kTriangleStrip_VertexMode to construct the geometry, which you can then draw to the canvas in a single, efficient call.
This method gives you direct, low-level control over the geometry being sent to the GPU, aligning perfectly with your goal of building a high-performance system.
Advanced Topic: Stroke Expansion via Path Offsetting
While our triangle strip approach is performant and practical, it's worth knowing that graphics libraries like Skia often solve this problem by generating a new, closed SkPath that represents the stroke's outline. This is a significantly more complex problem, especially when dealing with sharp corners and high curvatures.
The following video from the High-Performance Graphics conference discusses the deep theory behind a GPU-friendly algorithm for this, giving you a taste of the state-of-the-art.
This video discusses advanced techniques for stroke expansion. We won't be implementing this, but it provides excellent context on the complexity of "correctly" rendering strokes as filled paths.
Watch these two short segments to appreciate the problem space: The introduction to stroke expansion, explaining how it turns a vector path into a filled path. The discussion on "strongly correct" rendering, which shows how simple parallel curves can fail at high curvatures and require "evolute" sections.
This glimpse into advanced rendering theory highlights why our triangle strip method is such a powerful and pragmatic choice for your application. It bypasses many of these complexities while still producing high-quality, performant results.
Bringing It All Together
To integrate this into your project, you'll create a function that takes a Stroke object and generates an SkVertices object for rendering.
Here is a high-level outline of the function:
// Generates a renderable SkVertices object from a stroke.
sk_sp<SkVertices> generateStrokeMesh(const Stroke& stroke) {
if (stroke.points.size() < 2) {
return nullptr;
}
// 1. Generate a list of finely-sampled points for the centerline
// by iterating through the stroke's InputPoints and using getSplinePoint.
// Also interpolate pressure values at each sampled point.
std::vector<SkPoint> centerlinePoints;
std::vector<float> widths;
// ... logic to populate these vectors ...
// 2. Build the vertex list for the triangle strip.
SkVertices::Builder builder(SkVertices::kTriangleStrip_VertexMode,
centerlinePoints.size() * 2, // vertex count
0, // index count
0); // texCoord count
for (size_t i = 0; i < centerlinePoints.size(); ++i) {
// 3. Calculate the tangent and normal at centerlinePoints[i].
// Handle endpoints carefully.
// 4. Calculate the two outline vertices using the normal and width[i].
SkPoint vertexA = ...;
SkPoint vertexB = ...;
// 5. Add them to the builder.
builder.positions()[i * 2] = vertexA;
builder.positions()[i * 2 + 1] = vertexB;
}
// 6. Return the final mesh.
return builder.detach();
}
This generated SkVertices mesh can then be cached or regenerated as needed and drawn to your SkCanvas with canvas->drawVertices().
Conclusion
In this lesson, you've learned the theory and practical steps to transform raw input into beautifully rendered strokes. This is a fundamental skill in graphics programming and a major step forward for your whiteboard application.
Key Takeaways:
- Raw input points must be interpolated to create smooth curves. Catmull-Rom splines are an excellent choice as they pass through the control points.
- A variable-width stroke can be efficiently rendered by generating a triangle strip mesh along the smoothed centerline.
- The width of the strip at any point is determined by interpolating the
pressurevalues from the original input. - The vertices of the strip are calculated by offsetting points along the centerline in the direction of the normal vector.
- Skia's
SkVertices::BuilderandSkCanvas::drawVerticesare the tools for creating and rendering this custom geometry.
In our next lesson, we will focus on interactivity. We will design a Command interface and a history manager to track user actions, laying the groundwork for a robust Undo/Redo system—an essential feature for any creation tool.