The Scale Transform
Discover the SkiaSharp scale transform for scaling objects to various sizes
As you've seen in The Translate Transform article, the translate transform can move a graphical object from one location to another. In contrast, the scale transform changes the size of the graphical object:
The scale transform also often causes graphics coordinates to move as they are made larger.
Earlier you saw two transform formulas that describe the effects of translation factors of dx
and dy
:
x' = x + dx
y' = y + dy
Scale factors of sx
and sy
are multiplicative rather than additive:
x' = sx · x
y' = sy · y
The default values of the translate factors are 0; the default values of the scale factors are 1.
The SKCanvas
class defines four Scale
methods. The first Scale
method is for cases when you want the same horizontal and vertical scaling factor:
public void Scale (Single s)
This is known as isotropic scaling — scaling that is the same in both directions. Isotropic scaling preserves the object's aspect ratio.
The second Scale
method lets you specify different values for horizontal and vertical scaling:
public void Scale (Single sx, Single sy)
This results in anisotropic scaling.
The third Scale
method combines the two scaling factors in a single SKPoint
value:
public void Scale (SKPoint size)
The fourth Scale
method will be described shortly.
The Basic Scale page demonstrates the Scale
method. The BasicScalePage.xaml file contains two Slider
elements that let you select horizontal and vertical scaling factors between 0 and 10. The BasicScalePage.xaml.cs code-behind file uses those values to call Scale
before displaying a rounded rectangle stroked with a dashed line and sized to fit some text in the upper-left corner of the canvas:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.SkyBlue);
using (SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = 3,
PathEffect = SKPathEffect.CreateDash(new float[] { 7, 7 }, 0)
})
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Blue,
TextSize = 50
})
{
canvas.Scale((float)xScaleSlider.Value,
(float)yScaleSlider.Value);
SKRect textBounds = new SKRect();
textPaint.MeasureText(Title, ref textBounds);
float margin = 10;
SKRect borderRect = SKRect.Create(new SKPoint(margin, margin), textBounds.Size);
canvas.DrawRoundRect(borderRect, 20, 20, strokePaint);
canvas.DrawText(Title, margin, -textBounds.Top + margin, textPaint);
}
}
You might wonder: How do the scaling factors affect the value returned from the MeasureText
method of SKPaint
? The answer is: Not at all. Scale
is a method of SKCanvas
. It does not affect anything you do with an SKPaint
object until you use that object to render something on the canvas.
As you can see, everything drawn after the Scale
call increases proportionally:
The text, the width of the dashed line, the length of the dashes in that line, the rounding of the corners, and the 10-pixel margin between the left and top edges of the canvas and the rounded rectangle are all subject to the same scaling factors.
Important
The Universal Windows Platform does not properly render anisotropicly scaled text.
Anisotropic scaling causes the stroke width to become different for lines aligned with the horizontal and vertical axes. (This is also evident from the first image on this page.) If you don't want the stroke width to be affected by the scaling factors, set it to 0 and it will always be one pixel wide regardless of the Scale
setting.
Scaling is relative to the upper-left corner of the canvas. This might be exactly what you want, but it might not be. Suppose you want to position the text and rectangle somewhere else on the canvas and you want to scale it relative to its center. In that case you can use the fourth version of the Scale
method, which includes two additional parameters to specify the center of scaling:
public void Scale (Single sx, Single sy, Single px, Single py)
The px
and py
parameters define a point that is sometimes called the scaling center but in the SkiaSharp documentation is referred to as a pivot point. This is a point relative to the upper-left corner of the canvas that is not affected by the scaling. All scaling occurs relative to that center.
The Centered Scale page shows how this works. The PaintSurface
handler is similar to the Basic Scale program except that the margin
value is calculated to center the text horizontally, which implies that the program works best in portrait mode:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.SkyBlue);
using (SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = 3,
PathEffect = SKPathEffect.CreateDash(new float[] { 7, 7 }, 0)
})
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Blue,
TextSize = 50
})
{
SKRect textBounds = new SKRect();
textPaint.MeasureText(Title, ref textBounds);
float margin = (info.Width - textBounds.Width) / 2;
float sx = (float)xScaleSlider.Value;
float sy = (float)yScaleSlider.Value;
float px = margin + textBounds.Width / 2;
float py = margin + textBounds.Height / 2;
canvas.Scale(sx, sy, px, py);
SKRect borderRect = SKRect.Create(new SKPoint(margin, margin), textBounds.Size);
canvas.DrawRoundRect(borderRect, 20, 20, strokePaint);
canvas.DrawText(Title, margin, -textBounds.Top + margin, textPaint);
}
}
The upper-left corner of the rounded rectangle is positioned margin
pixels from the left of the canvas and margin
pixels from the top. The last two arguments to the Scale
method are set to those values plus the width and height of the text, which is also the width and height of the rounded rectangle. This means that all scaling is relative to the center of that rectangle:
The Slider
elements in this program have a range of –10 to 10. As you can see,
negative values of vertical scaling (such as on the Android screen in the center) cause objects to flip around the horizontal axis that passes through the center of scaling. Negative values of horizontal scaling (such as in the UWP screen on the right) cause objects to flip around the vertical axis that passes through the center of scaling.
The version of the Scale
method with pivot points is a shortcut for a series of three Translate
and Scale
calls. You might want to see how this works by replacing the Scale
method in the Centered Scale page with the following:
canvas.Translate(-px, -py);
These are the negatives of the pivot point coordinates.
Now run the program again. You'll see that the rectangle and text are shifted so that the center is in the upper-left corner of the canvas. You can barely see it. The sliders don't work of course because now the program doesn't scale at all.
Now add the basic Scale
call (without a scaling center) before that Translate
call:
canvas.Scale(sx, sy);
canvas.Translate(–px, –py);
If you're familiar with this exercise in other graphics programming systems, you might think that's wrong, but it's not. Skia handles successive transform calls a little differently from what you might be familiar with.
With the successive Scale
and Translate
calls, the center of the rounded rectangle is still in the upper-left corner, but you can now scale it relative to the upper-left corner of the canvas, which is also the center of the rounded rectangle.
Now, before that Scale
call, add another Translate
call with the centering values:
canvas.Translate(px, py);
canvas.Scale(sx, sy);
canvas.Translate(–px, –py);
This moves the scaled result back to the original position. Those three calls are equivalent to:
canvas.Scale(sx, sy, px, py);
The individual transforms are compounded so that the total transform formula is:
x' = sx · (x – px) + px
y' = sy · (y – py) + py
Keep in mind that the default values of sx
and sy
are 1. It's easy to convince yourself that the pivot point (px, py) is not transformed by these formulas. It remains in the same location relative to the canvas.
When you combine Translate
and Scale
calls, the order matters. If the Translate
comes after the Scale
, the translation factors are effectively scaled by the scaling factors. If the Translate
comes before the Scale
, the translation factors are not scaled. This process becomes somewhat clearer (albeit more mathematical) when the subject of transform matrices is introduced.
The SKPath
class defines a read-only Bounds
property that returns an SKRect
defining the extent of the coordinates in the path. For example, when the Bounds
property is obtained from the hendecagram path created earlier, the Left
and Top
properties of the rectangle are approximately –100, the Right
and Bottom
properties are approximately 100, and the Width
and Height
properties are approximately 200. (Most of the actual values are a little less because the points of the stars are defined by a circle with a radius of 100 but only the top point is parallel with the horizontal or vertical axes.)
The availability of this information implies that it should be possible to derive scale and translate factors suitable for scaling a path to the size of the canvas. The Anisotropic Scaling page demonstrates this with the 11-pointed star. An anisotropic scale means that it's unequal in the horizontal and vertical directions, which means that the star won't retain its original aspect ratio. Here's the relevant code in the PaintSurface
handler:
SKPath path = HendecagramPage.HendecagramPath;
SKRect pathBounds = path.Bounds;
using (SKPaint fillPaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = SKColors.Pink
})
using (SKPaint strokePaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Blue,
StrokeWidth = 3,
StrokeJoin = SKStrokeJoin.Round
})
{
canvas.Scale(info.Width / pathBounds.Width,
info.Height / pathBounds.Height);
canvas.Translate(-pathBounds.Left, -pathBounds.Top);
canvas.DrawPath(path, fillPaint);
canvas.DrawPath(path, strokePaint);
}
The pathBounds
rectangle is obtained near the top of this code, and then used later with the width and height of the canvas in the Scale
call. That call by itself will scale the coordinates of the path when it's rendered by the DrawPath
call but the star will be centered in the upper-right corner of the canvas. It needs to be shifted down and to the left. This is the job of the Translate
call. Those two properties of pathBounds
are approximately –100, so the translation factors are about 100. Because the Translate
call is after the Scale
call, those values are effectively scaled by the scaling factors, so they move the center of the star to the center of the canvas:
Another way you can think about the Scale
and Translate
calls is to determine the effect in reverse sequence: The Translate
call shifts the path so it becomes fully visible but oriented in the upper-left corner of the canvas. The Scale
method then makes that star larger relative to the upper-left corner.
Actually, it appears that the star is a little larger than the canvas. The problem is the stroke width. The Bounds
property of SKPath
indicates the dimensions of the coordinates encoded in the path, and that's what the program uses to scale it. When the path is rendered with a particular stroke width, the rendered path is larger than the canvas.
To fix this problem you need to compensate for that. One easy approach in this program is to add the following statement right before the Scale
call:
pathBounds.Inflate(strokePaint.StrokeWidth / 2,
strokePaint.StrokeWidth / 2);
This increases the pathBounds
rectangle by 1.5 units on all four sides. This is a reasonable solution only when the stroke join is rounded. A miter join can be longer and is difficult to calculate.
You can also use a similar technique with text, as the Anisotropic Text page demonstrates. Here's the relevant part of the PaintSurface
handler from the AnisotropicTextPage
class:
using (SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Blue,
StrokeWidth = 0.1f,
StrokeJoin = SKStrokeJoin.Round
})
{
SKRect textBounds = new SKRect();
textPaint.MeasureText("HELLO", ref textBounds);
// Inflate bounds by the stroke width
textBounds.Inflate(textPaint.StrokeWidth / 2,
textPaint.StrokeWidth / 2);
canvas.Scale(info.Width / textBounds.Width,
info.Height / textBounds.Height);
canvas.Translate(-textBounds.Left, -textBounds.Top);
canvas.DrawText("HELLO", 0, 0, textPaint);
}
It's similar logic, and the text expands to the size of the page based on the text bounds rectangle returned from MeasureText
(which is a little larger than the actual text):
If you need to preserve the aspect ratio of the graphical objects, you'll want to use isotropic scaling. The Isotropic Scaling page demonstrates this for the 11-pointed star. Conceptually, the steps for displaying a graphical object in the center of the page with isotropic scaling are:
- Translate the center of the graphical object to the upper-left corner.
- Scale the object based on the minimum of the horizontal and vertical page dimensions divided by the graphical object dimensions.
- Translate the center of the scaled object to the center of the page.
The IsotropicScalingPage
performs these steps in reverse order before displaying the star:
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
SKPath path = HendecagramArrayPage.HendecagramPath;
SKRect pathBounds = path.Bounds;
using (SKPaint fillPaint = new SKPaint())
{
fillPaint.Style = SKPaintStyle.Fill;
float scale = Math.Min(info.Width / pathBounds.Width,
info.Height / pathBounds.Height);
for (int i = 0; i <= 10; i++)
{
fillPaint.Color = new SKColor((byte)(255 * (10 - i) / 10),
0,
(byte)(255 * i / 10));
canvas.Save();
canvas.Translate(info.Width / 2, info.Height / 2);
canvas.Scale(scale);
canvas.Translate(-pathBounds.MidX, -pathBounds.MidY);
canvas.DrawPath(path, fillPaint);
canvas.Restore();
scale *= 0.9f;
}
}
}
The code also displays the star 10 more times, each time decreasing the scaling factor by 10% and progressively changing the color from red to blue: