When I started writing Code Canvas back in 2008, I quickly realized that using a simple ScaleTransform on an existing Canvas would not produce the experience that I wanted. This was mostly because the elements on my canvas have many fine details such as the dog-ears on files, the icons next to identifiers, and the arrowheads on relationship links. But many of these small details quickly became hard to see when scaled down linearly by the ScaleTransform applied to the canvas. I also had the usual problem of the relationship links (which were simple Paths) becoming too thin to see, even though the links were still long enough to cover much of the screen. The problem of thin Paths is easily overcome by increasing the StrokeThickness by the inverse of the scale, but unfortunately this trick doesn’t work as well for the arrow heads on the ends of the paths. The stroke thickness will stay the same size, but the area of the arrow head keeps getting smaller, eventually resulting in a tiny blob. Linear ScaleTransforms also don’t work well for text labels and small detailed icons since the text quickly becomes too small to read and the details of the icons become too small to see. The inverse is also true: there is often no point in scaling text above 100% even when zoomed in very close unless you are giving a demonstration on a projector or you’re visually impaired.
For all of these scenarios, I wanted to something other than a linear ScaleTransform. For dog-ears on files, I wanted them to stay a fixed size until the zoom level was less than around 30%, at which point I wanted them to start becoming smaller so they didn’t cover the entire file. For the icons next to identifiers, I wanted them to stay the same size and stay next to their target identifier, no matter how far you were zoomed out. For the labels of the identifiers, I wanted the text to remain a fixed size and eventually become truncated (with ellipsis) when they started to collide with other objects when zooming out. For the arrow heads on the end of links, I wanted them to become smaller at a slower rate than the rest of the canvas, and this required changing the Path’s actual geometry dynamically as the user zooms in and out. All of these details are very subtle, but they’re just the beginning of what’s known as semantic zoom.
In the following blog posts I’m going to introduce the ZoomableCanvas class and its source code. ZoomableCanvas was written while I was developing Code Canvas, but it’s not specific to that application. It’s a general-purpose WPF component which can be thought of like a Canvas that understands the concept of Scale (for zooming), Offset (for panning), and Virtualization (for not instantiating UIElements when they’re off the screen). It supports both linear ScaleTransform–based zooming and also non-linear semantic zooming, which basically just positions the elements in the appropriate spot on the screen but leaves the shape-changing up to the elements themselves. The virtualization uses a spatial index based on the quad-tree in Chris Lovett’s VirtualCanvas, and is extensible to support data-virtualization as well. I’m working with the WPF team to try and make ZoomableCanvas available as part of the WPF Toolkit, but I will post source code to my blog as well.