缩放转换

了解用于将对象缩放为各种大小的 SkiaSharp 缩放变换

正如你在平移变换一文中所看到的那样,平移变换可以将图形对象从一个位置移动到另一个位置。 相比之下,缩放变换将改变图形对象的大小:

按比例缩放的高度较大的词

缩放变换通常还会导致图形坐标随着自身的变大而移动。

前面你看到了两个变换公式,这些公式描述了 dxdy 的平移因子效果:

x' = x + dx

y' = y + dy

sxsy 的缩放因子是乘法因子,而不是加法因子:

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);
    }
}

你可能想知道:缩放因子如何影响从 SKPaintMeasureText 方法返回的值? 答案是:根本不会影响。 ScaleSKCanvas 的一种方法。 它不会影响对 SKPaint 对象执行的任何操作,除非使用该对象在画布上呈现内容。

如你所见,调用 Scale 后绘制的所有内容按比例增加:

“基本缩放”页的三重屏幕截图

文本、虚线的宽度、该线中虚线的长度、角的圆度以及画布左边缘和上边缘与圆角矩形之间的 10 像素边距都遵循相同的缩放因子。

重要

通用 Windows 平台无法以各向异性的方式正确呈现缩放文本。

各向异性缩放会导致与水平轴和垂直轴对齐的线条的描边宽度变得不同。 (从本页的第一张图片中也可以明显看出这一点。)如果不希望描边宽度受到缩放因子的影响,请将其设置为 0,无论 Scale 设置如何,它都将始终为 1 像素宽。

缩放是相对于画布的左上角而言的。 这可能正是你想要的效果,但也可能不是。 假设你希望将文本和矩形放置在画布上的其他位置,并且希望相对于其中心进行缩放。 在这种情况下,可以使用 Scale 方法的第四个版本(其中包括两个附加参数)来指定缩放中心:

public void Scale (Single sx, Single sy, Single px, Single py)

pxpy 参数定义一个点,有时称为缩放中心,但在 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 方法的版本是对 TranslateScale 的三次调用的快捷方式。 你可能希望通过将“居中缩放”页面中的 Scale 方法替换为以下内容来了解​​其工作原理

canvas.Translate(-px, -py);

这些是枢轴点坐标的负值。

现在再次运行程序。 你将看到矩形和文本发生了移动,以便中心位于画布的左上角。 你几乎看不到它。 滑块当然不起作用,因为程序现在根本无法缩放。

现在在 Translate 调用之前添加基本 Scale 调用(没有缩放中心)

canvas.Scale(sx, sy);
canvas.Translate(–px, –py);

如果你熟悉其他图形编程系统中的此练习,你可能会认为这是错误的,但事实并非如此。 Skia 处理连续变换调用的方式与你可能熟悉的方法略有不同。

通过连续的 ScaleTranslate 调用,圆角矩形的中心仍然位于左上角,但你现在可以相对于画布的左上角(也是圆角矩形的中心)对其进行缩放。

现在,在该 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

请记住,sxsy 的默认值都为 1。 很容易就能说服自己枢轴点 (px, py) 不是通过这些公式变换来的。 它与画布位于同一位置。

合并 TranslateScale 调用时,顺序很重要。 如果 Translate 出现在 Scale 之后,则平移因子将按缩放因子进行有效缩放。 如果 Translate 出现在 Scale 之前,则平移因子不会进行缩放。 当引入变换矩阵的主题时,这个过程会变得更加清晰(尽管更加数学化)。

SKPath 类定义了一个只读 Bounds 属性,该属性返回的 SKRect 可定义路径中坐标的范围。 例如,当从之前创建的十六角星路径获取 Bounds 属性时,矩形的 LeftTop 属性约为 –100,RightBottom 属性约为 100,而 WidthHeight 属性约为 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 调用之后进行的,因此这些值会通过缩放因子进行有效缩放,因此它们会将星形的中心移动到画布的中心:

“各向异性缩放”页的三重屏幕截图

考虑 ScaleTranslate 调用的另一种方法是按相反顺序确定效果:Translate 调用会移动路径,使其完全可见,但定向在画布的左上角。 然后,Scale 方法使该星形相对于左上角更大。

事实上,星形似乎比画布大一点。 问题出在描边宽度。 SKPathBounds 属性表示路径中编码的坐标的尺寸,也就是程序用来缩放它的尺寸。 使用特定描边宽度渲染路径时,渲染的路径比画布大。

若要解决此问题,需要对此进行补偿。 此程序中的一种简单方法是在调用 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%,并逐渐将颜色从红色变为蓝色:

“各向同性缩放”页的三重屏幕截图