Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 18

Camera Control with Affine Transformations

Recommended resources

Hello! Following our last lesson where we learned to draw geometric primitives and complex paths, we're now ready to give our canvas motion. A key feature of any modern whiteboard or design tool is the ability to navigate an infinite canvas, which means we need a "camera" to pan and zoom.

This lesson addresses exactly that. We'll learn how to apply affine transformations—specifically translation, scaling, and rotation—to the SkCanvas itself. This is a powerful and performant technique. Instead of calculating new coordinates for every single point of every shape we draw, we transform the entire coordinate system in one go. This is the foundation of implementing a camera system for your whiteboard, allowing users to fluidly move around and focus on different parts of their work.

The Camera: An Illusion of Movement

First, let's establish a core concept. In most 2D graphics systems, the "camera" doesn't actually exist as an object that moves through a scene. Instead, it's an illusion created by applying the inverse transformations to all the objects in the world. If you want the camera to move right, you move the entire world left. If you want to zoom in, you scale the entire world up.

This short video provides an excellent conceptual overview of this idea.

How does a 2D CAMERA actually work?

This video from Small Hedge Games explains the fundamental principle behind 2D cameras.

Watch the segment from the beginning to understand how moving and scaling objects creates the camera illusion. The key insight is that new_position = object_position - camera_position.


Skia's Coordinate Spaces

Skia formalizes this idea through coordinate spaces. When you call canvas->drawRect(SkRect::MakeXYWH(10, 20, 50, 50), paint), the coordinates (10, 20) are in the local coordinate space. By default, this space maps directly to the device coordinate space (i.e., pixels on the screen).

The key to our camera is that we can manipulate the relationship between these two spaces. By transforming the canvas, we change how local coordinates are mapped to the device.

Skia Coordinate Spaces | Skia

This official Skia documentation explains the distinction between local and device coordinates and how to transform the local space.

Read the Overview and then the section on Local Coordinates and Transforming them. The critical takeaway is that SkCanvas methods like translate and scale modify the canvas's transformation matrix, affecting all subsequent drawing operations.

So, instead of moving an object, we move the canvas's origin. A call to canvas->translate(50, 0) means that the local coordinate (0, 0) is now mapped to the device coordinate (50, 0). Any subsequent drawing will be shifted 50 pixels to the right. This is the mechanism for panning.


Implementing Panning and Zooming

Now let's dive into the practical logic. The process involves maintaining a camera state (offset and scale) and using it to transform world coordinates into screen coordinates. The following video provides a brilliant, from-scratch explanation of the logic, which is universal to any graphics framework.

Programming Panning & Zooming

This video by javidx9 is a masterclass in implementing panning and zooming from first principles. We will focus on the logic, which you will then apply using Skia's API.

Panning Logic: Watch from the explanation of panning. Understand the simple equations screen = world - offset and world = screen + offset. Panning Implementation: Skim through the code implementation from this segment. The key is tracking the mouse delta during a drag and applying it to the world offset. Basic Zooming: Watch from this part to see how a scaling factor is introduced into the transformation equations. Zoom-to-Cursor: This is the most crucial part. Watch carefully from this section. The logic is: Find the world coordinate under the mouse before applying the new zoom level. Find the world coordinate under the mouse after applying the new zoom level. Calculate the difference between these two points. Adjust the world offset by this difference to keep the point under the mouse stationary.

Let's translate this logic to Skia. We'll create a simple "camera" state and apply it in our render loop.

// In your application state
SkPoint camera_offset = {0.0f, 0.0f};
float camera_zoom = 1.0f;

// In your main render function, before any drawing
canvas->save(); // Good practice to save the state before transforming

// Apply the camera transformation
canvas->translate(camera_offset.fX, camera_offset.fY);
canvas->scale(camera_zoom, camera_zoom);

// Now, draw all your world objects (paths, shapes, etc.)
// Their coordinates are in "world space"
drawMyWhiteboardContent(canvas);

canvas->restore(); // Restore to the original state for UI elements, etc.

When handling user input:

  • Panning (mouse drag): Update camera_offset by adding the mouse movement delta. Since the canvas is already scaled, you must divide the mouse delta by the current zoom level to move the correct amount in world space. camera_offset.fX += mouse_delta.x / camera_zoom;
  • Zooming (mouse wheel): Implement the "zoom-to-cursor" logic from the video. You will calculate the pre- and post-zoom world coordinates and adjust camera_offset accordingly while updating camera_zoom.

The Matrix and Order of Operations

Behind the scenes, SkCanvas manages a 3x3 transformation matrix represented by the SkMatrix class. Every call to translate, scale, or rotate modifies this matrix. Your background in computer science and engineering makes it useful to understand that these high-level calls are conveniences for matrix multiplication.

The order in which you apply transformations is critical. Translating then scaling is not the same as scaling then translating.

This image illustrates how applying Scale, Rotate, and Translate in a different order (SRT vs. TRS) produces a completely different result. The top row (SRT) scales and rotates the object at the origin, then translates it. The bottom row (TRS) translates the object, then rotates and scales it around the new, translated origin, causing it to move further away.

For a camera, you typically want to apply scale operations before translations when composing the final view. The "zoom-to-cursor" algorithm neatly handles this by calculating the required translation (offset) that incorporates the effect of scaling.

You can inspect the underlying matrix API in the Skia documentation.

Skia: SkMatrix Class Reference

This documentation details the SkMatrix class, which powers canvas transformations. Understanding this gives you a deeper insight into the system.

Read the Detailed Description to get an overview. Look at the function groups for pre-concatenation and post-concatenation. preConcat applies a new transformation before the existing one (M_new = T * M_old), while postConcat applies it after (M_new = M_old * T). Canvas transforms like canvas->translate() are typically pre-concatenated. This is fundamental to how matrix stacks work in graphics programming.

The SkCanvas methods translate, scale, and rotate are essentially convenient wrappers around canvas->preConcat(SkMatrix::Translate(...)), and so on.


Conclusion

You now have the conceptual and practical tools to build a fully-featured camera system for your whiteboard. By manipulating the SkCanvas transformation matrix, you can efficiently pan and zoom across an infinite canvas, which is a cornerstone of your project's user experience.

Key Takeaways:

  • A 2D camera is an illusion created by applying inverse transformations (translation, scaling) to the world's coordinate system.
  • In Skia, this is achieved by calling SkCanvas::translate(), SkCanvas::scale(), and SkCanvas::rotate() to modify the canvas's internal transformation matrix.
  • Panning is implemented by translating the canvas based on mouse drag input.
  • Zooming requires scaling the canvas. To implement intuitive "zoom-to-cursor" functionality, you must calculate and apply a compensating translation to keep the point under the cursor fixed in screen space.
  • The order of matrix transformations is non-commutative and critically important for achieving the desired visual result.

In our next lesson, we'll start drawing text onto the canvas. Because text is drawn like any other graphical element, it will automatically respect the canvas transformations we've learned about today, allowing you to pan and zoom across text content seamlessly.