缩放转换
了解用于将对象缩放为各种大小的 SkiaSharp 缩放变换
正如你在平移变换一文中所看到的那样,平移变换可以将图形对象从一个位置移动到另一个位置。 相比之下,缩放变换将改变图形对象的大小:
缩放变换通常还会导致图形坐标随着自身的变大而移动。
前面你看到了两个变换公式,这些公式描述了 dx
和 dy
的平移因子效果:
x' = x + dx
y' = y + dy
sx
和 sy
的缩放因子是乘法因子,而不是加法因子:
x' = sx · x
y' = sy · y
平移因子的默认值都为 0;缩放因子的默认值都为 1。
SKCanvas
类定义了四种 Scale
方法。 第一种 Scale
方法适用于需要相同的水平和垂直缩放因子的情况:
public void Scale (Single s)
这称为各向同性缩放,即缩放在两个方向上均相同。 各向同性缩放保留对象的纵横比。
第二种 Scale
方法可为水平和垂直缩放指定不同的值:
public void Scale (Single sx, Single sy)
这会产生各向异性缩放。
第三种 Scale
方法将两个缩放因子组合在单个 SKPoint
值中:
public void Scale (SKPoint size)
第四种 Scale
方法将在稍后进行介绍。
“基本缩放”页演示了 Scale
方法。 BasicScalePage.xaml 文件包含两个 Slider
元素,可用于选择介于 0 和 10 之间的水平和垂直缩放因子。 BasicScalePage.xaml.cs 代码隐藏文件使用这些值来调用 Scale
,然后显示用虚线绘制的圆角矩形,并调整大小以适应画布左上角的某些文本:
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);
}
}
你可能想知道:缩放因子如何影响从 SKPaint
的 MeasureText
方法返回的值? 答案是:根本不会影响。 Scale
是 SKCanvas
的一种方法。 它不会影响对 SKPaint
对象执行的任何操作,除非使用该对象在画布上呈现内容。
如你所见,调用 Scale
后绘制的所有内容按比例增加:
文本、虚线的宽度、该线中虚线的长度、角的圆度以及画布左边缘和上边缘与圆角矩形之间的 10 像素边距都遵循相同的缩放因子。
重要
通用 Windows 平台无法以各向异性的方式正确呈现缩放文本。
各向异性缩放会导致与水平轴和垂直轴对齐的线条的描边宽度变得不同。 (从本页的第一张图片中也可以明显看出这一点。)如果不希望描边宽度受到缩放因子的影响,请将其设置为 0,无论 Scale
设置如何,它都将始终为 1 像素宽。
缩放是相对于画布的左上角而言的。 这可能正是你想要的效果,但也可能不是。 假设你希望将文本和矩形放置在画布上的其他位置,并且希望相对于其中心进行缩放。 在这种情况下,可以使用 Scale
方法的第四个版本(其中包括两个附加参数)来指定缩放中心:
public void Scale (Single sx, Single sy, Single px, Single py)
px
和 py
参数定义一个点,有时称为缩放中心,但在 SkiaSharp 文档中称为枢轴点。 这是相对于画布左上角的点而言的,不受缩放影响。 所有缩放都相对于该中心进行。
居中缩放介绍了其工作原理。 PaintSurface
处理程序与“基本缩放”程序类似,只是计算 margin
的值是为了使文本水平居中,这意味着该程序在纵向模式下效果最佳:
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);
}
}
圆角矩形的左上角所在位置距画布左侧 margin
像素,距顶部 margin
像素。 Scale
方法的最后两个参数设置为这些值加上文本的宽度和高度,这也是圆角矩形的宽度和高度。 这意味着所有缩放都相对于该矩形的中心进行:
此程序中的 Slider
元素的取值范围为 –10 到 10。 如你所见,垂直缩放的负值(例如在中心的 Android 屏幕上)会使对象围绕穿过缩放中心的水平轴翻转。 水平缩放的负值(例如在右侧的 UWP 屏幕中)会使对象围绕穿过缩放中心的垂直轴翻转。
带有枢轴点的 Scale
方法的版本是对 Translate
和 Scale
的三次调用的快捷方式。 你可能希望通过将“居中缩放”页面中的 Scale
方法替换为以下内容来了解其工作原理:
canvas.Translate(-px, -py);
这些是枢轴点坐标的负值。
现在再次运行程序。 你将看到矩形和文本发生了移动,以便中心位于画布的左上角。 你几乎看不到它。 滑块当然不起作用,因为程序现在根本无法缩放。
现在在 Translate
调用之前添加基本 Scale
调用(没有缩放中心):
canvas.Scale(sx, sy);
canvas.Translate(–px, –py);
如果你熟悉其他图形编程系统中的此练习,你可能会认为这是错误的,但事实并非如此。 Skia 处理连续变换调用的方式与你可能熟悉的方法略有不同。
通过连续的 Scale
和 Translate
调用,圆角矩形的中心仍然位于左上角,但你现在可以相对于画布的左上角(也是圆角矩形的中心)对其进行缩放。
现在,在该 Scale
调用之前,使用居中值添加另一个 Translate
调用:
canvas.Translate(px, py);
canvas.Scale(sx, sy);
canvas.Translate(–px, –py);
这会将缩放结果移回到原始位置。 这三次调用等效于:
canvas.Scale(sx, sy, px, py);
单个变换会进行复合变换,因此总变换公式为:
x' = sx · (x – px) + px
y' = sy · (y – py) + py
请记住,sx
和 sy
的默认值都为 1。 很容易就能说服自己枢轴点 (px, py) 不是通过这些公式变换来的。 它与画布位于同一位置。
合并 Translate
和 Scale
调用时,顺序很重要。 如果 Translate
出现在 Scale
之后,则平移因子将按缩放因子进行有效缩放。 如果 Translate
出现在 Scale
之前,则平移因子不会进行缩放。 当引入变换矩阵的主题时,这个过程会变得更加清晰(尽管更加数学化)。
SKPath
类定义了一个只读 Bounds
属性,该属性返回的 SKRect
可定义路径中坐标的范围。 例如,当从之前创建的十六角星路径获取 Bounds
属性时,矩形的 Left
和 Top
属性约为 –100,Right
和 Bottom
属性约为 100,而 Width
和 Height
属性约为 200。 (大多数实际值要小一些,因为星形的角是由半径为 100 的圆定义的,但只有顶点与水平轴或垂直轴平行。)
如果提供了此信息,则应可以导出适合将路径缩放到画布大小的缩放和平移因子。 “各向异性缩放”页用 11 角星证明了这一点。。 各向异性缩放意味着它在水平和垂直方向上不相等,这意味着星形不会保留其原始的纵横比。 下面是 PaintSurface
处理程序中的相关代码:
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);
}
在此代码顶部附近获得 pathBounds
矩形,然后在 Scale
调用中与画布的宽度和高度一起使用。 该调用本身将在 DrawPath
调用渲染路径时缩放路径的坐标,但星形将位于画布右上角的中心。 它需要向下和向左移动。 可通过 Translate
调用实现。 由于 pathBounds
的这两个属性大约为 –100,因此平移因子约为 100。 由于 Translate
调用是在 Scale
调用之后进行的,因此这些值会通过缩放因子进行有效缩放,因此它们会将星形的中心移动到画布的中心:
考虑 Scale
和 Translate
调用的另一种方法是按相反顺序确定效果:Translate
调用会移动路径,使其完全可见,但定向在画布的左上角。 然后,Scale
方法使该星形相对于左上角更大。
事实上,星形似乎比画布大一点。 问题出在描边宽度。 SKPath
的 Bounds
属性表示路径中编码的坐标的尺寸,也就是程序用来缩放它的尺寸。 使用特定描边宽度渲染路径时,渲染的路径比画布大。
若要解决此问题,需要对此进行补偿。 此程序中的一种简单方法是在调用 Scale
前添加以下语句:
pathBounds.Inflate(strokePaint.StrokeWidth / 2,
strokePaint.StrokeWidth / 2);
这会将 pathBounds
矩形的所有四条边增加 1.5 个单位。 此解决方案仅适用于描边连接是圆角的情况。 斜角连接可能更长,并且难以计算。
还可以对文本使用类似的技术,如“各向异性文本”页面所示。 下面是 AnisotropicTextPage
类中 PaintSurface
处理程序的相关部分:
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);
}
这是类似的逻辑,文本根据从 MeasureText
返回的文本边界矩形扩展到页面的大小(比实际文本稍大):
如果需要保留图形对象的纵横比,则需要使用各向同性缩放。 “各向异性缩放”页用 11 角星证明了这一点。 从概念上讲,以各向同性缩放在页面中心显示图形对象的步骤包括:
- 将图形对象的中心平移到左上角。
- 根据水平和垂直页面尺寸的最小值除以图形对象尺寸来缩放对象。
- 将缩放对象的中心平移到页面的中心。
在显示星形之前,IsotropicScalingPage
将按相反顺序执行这些步骤:
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;
}
}
}
该代码还显示了星形 10 次,每次将缩放系数减小 10%,并逐渐将颜色从红色变为蓝色: