Math Installment #1: Image layout logic
Turns out most of the code I write that does deal with math is layout code in order to manage how a number of assets, whether it be controls or graphics, is rendered to the screen. Over the past couple of days the kind of code I have been writing is in managing the various kinds of transforms that can be applied to images in order to display them in a number of different ways. Turns out rendering an image has to do with several factors. The first factor is the size of the original image. This will mean the height and the width of the image. The second factor is the size of the target area that you are rendering it into. Based on these factors you get quite a few options for transforming the source into the target. The following diagram shows just a few of the options available (actually most of them).
Let's quickly sum up the options. For the first transformation from a larger image to a smaller area you can either stretch to fit (technically shrink to fit) where the ratio between the height and width may not be preserved, perform an aspect ratio fit where the ratio between the height and width are maintained, create a scrollable view area where only a portion of the image is viewed at a time based on user input, or a fixed offset view where you display a certain portion of the image but don't allow scrolling. This last item, the fixed offset view area is most often realized as a centering option that displays only a center region of the source image in the target.
Going in the opposite direction we have some of the same options, and some different ones. This time the stretch to fit is going in the other way and the image is growing to fit the target region creating. Previously we lost information in the translation. Losing information in working with images is not so bad because you can keep the best information. Creating new information which happens when you grow or stretch the image is bad because you have to interpolate or guess using some functional methods. The chances of guessing wrong is pretty high. Thankfully some techniques exist to fix this problem. In addition to stretch to fit the center option comes back. Now the image really is centered with a border, often called a bezel. The aspect ratio fit also works well.
Time for some math though.
Stretch To Fit
float widthRatio = targetWidth / imageWidth;
float heightRatio = targetWidth / imageWidth;
// Whether growing or shrinking, the ratio will send source metric
// to the target metric through multiplication
targetWidth == widthRatio * imageWidth;
Aspect Ratio Fit
float optimalRatio = Math.Min(widthRatio, heightRatio);
// Taking the smallest of the two ratios we guarantee both
// result metrics are less than or equal to the target metrics
targetWidth >= optimalRatio * imageWidth;
targetHeight >= optimalRatio * imageHeight;
// Normally you center aspect ratio fitted contents. We'll
// see the math for that in the Center section
int left = (int) ((targetWidth - computedWidth) / 2);
int top = (int) ((targetHeight - computedHeight) / 2);
Now that you are introduce to a few of the options, let's check out some of the derivations. First, the ratio that you are using to multiply the source to the target is called the zoom factor. The zoom factor is often a useful display to the user telling them how accurate the display they are seeing is to the original image. If an image is scaled up to 2x or 200% it's normal size then the user might just think the image is bad, but knowing there is a zoom factor the user can quickly determine the display is not the same as the original.
int zoomPercentage = (int) (optimalRatio * 100); // 1.0 would display as 100, .50 would display as 50
One of the most useful display modes is the aspect ratio fit. When images are larger than the target you almost always want to use this method when shrinking them down to ensure the final image that you see is a good representative of the original image. When the images are smaller than the target, even this method creates anti-aliasing or growing distortion even though the aspect ratio is still maintained. Instead what you want is to clamp the aspect ratio fit so that the image will never grow.
Clamped Aspect Ratio Fit
// We've previously computed the optimalRatio, now we perform the clamp operation
optimalRatio = Math.Min(optimalRatio, 1.0f); // MAke sure we don't display the image larger than it was
Clamped Aspect Ratio Fit and Center
Rectangle targetRect = new Rectangle(
(targetWidth - (optimalRatio * imageWidth)) / 2,
(targetHeight - (optimalRatio * imageHeight)) / 2,
optimalRatio * imageWidth,
optimalRatio * imageHeight);
There are many more opportunities for mathematics in image layout. This article only covers some of the basics. One of the more interesting problems is selection rectangles over a zoomed image. You have to convert the region on the screen into an actual region back in the original image. The aspect ratios or zoom factors can be used to perform these conversions. The same applies if you are growing an image and have to update the scrollbars to reflect scrolling the new viewport region. I can probably package up some of this code if people are interested even though the PictureBox in Whidbey has support for most of these features. If you intercept the Image being set on the PictureBox you can dynamically change the settings in order to support all of the features described above.