翻译转换

了解如何使用平移变换来移动 SkiaSharp 图形

SkiaSharp 中最简单的变换类型是平移或平移变换。 此变换会朝水平和垂直方向移动图形对象。 从某种意义上讲,平移是最不必要的变换,因为通常你只需更改绘图函数中使用的坐标即可实现相同的效果。 但是,在呈现路径时,所有坐标都封装在路径中,因此应用平移变换来移动整个路径要容易得多。

平移对于动画和简单文本效果也很有用:

使用平移时的文本阴影、雕刻和浮雕

SKCanvas 中的 Translate 方法有两个参数,它们会导致后续绘制的图形对象水平和垂直移动:

public void Translate (Single dx, Single dy)

这些参数可为负。 第二个 Translate 方法将两个平移值合并到一个 SKPoint 值中:

public void Translate (SKPoint point)

示例程序的“累积平移”页演示了对 Translate 方法的多次调用是累积性的AccumulatedTranslatePage 类显示同一矩形的 20 个版本,每个版本都与前一个矩形有足够的偏移,以便它们沿着对角线拉伸。 下面是 PaintSurface 事件处理程序:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    using (SKPaint strokePaint = new SKPaint())
    {
        strokePaint.Color = SKColors.Black;
        strokePaint.Style = SKPaintStyle.Stroke;
        strokePaint.StrokeWidth = 3;

        int rectangleCount = 20;
        SKRect rect = new SKRect(0, 0, 250, 250);
        float xTranslate = (info.Width - rect.Width) / (rectangleCount - 1);
        float yTranslate = (info.Height - rect.Height) / (rectangleCount - 1);

        for (int i = 0; i < rectangleCount; i++)
        {
            canvas.DrawRect(rect, strokePaint);
            canvas.Translate(xTranslate, yTranslate);
        }
    }
}

连续矩形沿着页面向下缓慢移动:

“累积平移”页的三屏幕截图

如果累积平移因子为 dxdy,并且在绘图函数中指定的点为 (x, y),则图形对象将在点 (x', y') 处呈现,其中:

x' = x + dx

y' = y + dy

这些公式称为平移的变换公式。 新 SKCanvasdxdy 默认值为 0。

通常我们会将平移变换用于阴影效果和类似技术,如“平移文本效果”页所示。 下面是 TranslateTextEffectsPage 类中 PaintSurface 处理程序的相关部分:

float textSize = 150;

using (SKPaint textPaint = new SKPaint())
{
    textPaint.Style = SKPaintStyle.Fill;
    textPaint.TextSize = textSize;
    textPaint.FakeBoldText = true;

    float x = 10;
    float y = textSize;

    // Shadow
    canvas.Translate(10, 10);
    textPaint.Color = SKColors.Black;
    canvas.DrawText("SHADOW", x, y, textPaint);
    canvas.Translate(-10, -10);
    textPaint.Color = SKColors.Pink;
    canvas.DrawText("SHADOW", x, y, textPaint);

    y += 2 * textSize;

    // Engrave
    canvas.Translate(-5, -5);
    textPaint.Color = SKColors.Black;
    canvas.DrawText("ENGRAVE", x, y, textPaint);
    canvas.ResetMatrix();
    textPaint.Color = SKColors.White;
    canvas.DrawText("ENGRAVE", x, y, textPaint);

    y += 2 * textSize;

    // Emboss
    canvas.Save();
    canvas.Translate(5, 5);
    textPaint.Color = SKColors.Black;
    canvas.DrawText("EMBOSS", x, y, textPaint);
    canvas.Restore();
    textPaint.Color = SKColors.White;
    canvas.DrawText("EMBOSS", x, y, textPaint);
}

在这三个示例中,均调用了 Translate 来显示文本,以将其从 xy 变量指定的位置偏移。 然后文本再次以另一种颜色显示,且没有平移效果:

“平移文本效果”页的三屏幕截图

这三个示例均显示了否定 Translate 调用的不同方式:

第一个示例只是再次调用 Translate,但使用负值。 由于 Translate 调用是累积性的,因此该调用序列只是将总变换还原为默认值零。

第二个示例调用 ResetMatrix。 这会导致所有变换还原到其默认状态。

第三个示例通过调用 Save 来保存 SKCanvas 对象的状态,然后通过调用 Restore 来还原状态。 这是操控一系列绘图操作的变换的最通用方式。 这些 SaveRestore 调用的功能类似于堆栈:可以多次调用 Save,然后按相反顺序调用 Restore 以还原到之前的状态。 Save 方法返回一个整数,你可以将该整数传递给 RestoreToCount 以有效地多次调用 RestoreSaveCount 属性返回当前保存在堆栈中的状态数。

还可以使用 SKAutoCanvasRestore 类还原画布状态。 此类的构造函数需在 using 语句中调用;画布状态会在 using 块的末尾自动还原。

不过,不必担心从 PaintSurface 处理程序的一次调用到下一次调用的变换。 对 PaintSurface 的每次新调用都会传递一个具有默认变换的全新 SKCanvas 对象。

Translate 变换的另一个常见用途是呈现视觉对象,该对象最初是使用便于绘制的坐标创建的。 例如,你可能想要指定以点 (0, 0) 为中心的模拟时钟的坐标。 然后,可以使用变换将时钟显示在所需位置。 [Hendecagram 数组] 页演示了此方法HendecagramArrayPage 类首先为 11 角星创建一个 SKPath 对象。 HendecagramPath 对象定义为公共、静态和只读对象,以便可以从其他演示程序访问它。 它是在静态构造函数中创建的:

public class HendecagramArrayPage : ContentPage
{
    ...
    public static readonly SKPath HendecagramPath;

    static HendecagramArrayPage()
    {
        // Create 11-pointed star
        HendecagramPath = new SKPath();
        for (int i = 0; i < 11; i++)
        {
            double angle = 5 * i * 2 * Math.PI / 11;
            SKPoint pt = new SKPoint(100 * (float)Math.Sin(angle),
                                    -100 * (float)Math.Cos(angle));
            if (i == 0)
            {
                HendecagramPath.MoveTo(pt);
            }
            else
            {
                HendecagramPath.LineTo(pt);
            }
        }
        HendecagramPath.Close();
    }
}

如果星形的中心是点 (0, 0),则星形的所有点都在围绕该点的圆上。 每个点都是角度的正弦值和余弦值的组合,该角度以 360 度的 5/11 增加。 (也可以通过按圆的 2/11、3/11 或 4/11 增加角度来创建 11 角星。)该圆的半径设置为 100。

如果在不进行任何变换的情况下呈现此路径,则中心将位于 SKCanvas 的左上角,并且只有四分之一可见。 HendecagramPagePaintSurface 处理程序改用 Translate 在画布上平铺星形的多个副本,每个副本的颜色是随机的:

public class HendecagramArrayPage : ContentPage
{
    Random random = new Random();
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        using (SKPaint paint = new SKPaint())
        {
            for (int x = 100; x < info.Width + 100; x += 200)
                for (int y = 100; y < info.Height + 100; y += 200)
                {
                    // Set random color
                    byte[] bytes = new byte[3];
                    random.NextBytes(bytes);
                    paint.Color = new SKColor(bytes[0], bytes[1], bytes[2]);

                    // Display the hendecagram
                    canvas.Save();
                    canvas.Translate(x, y);
                    canvas.DrawPath(HendecagramPath, paint);
                    canvas.Restore();
                }
        }
    }
}

结果如下:

“Hendecagram 阵列”页的三屏幕截图

动画通常涉及到变换。 “十六角星动画”页将 11 角星绕圆移动HendecagramAnimationPage 类以某些字段以及 OnAppearingOnDisappearing 方法的重写开头,以启动和停止 Xamarin.Forms 计时器:

public class HendecagramAnimationPage : ContentPage
{
    const double cycleTime = 5000;      // in milliseconds

    SKCanvasView canvasView;
    Stopwatch stopwatch = new Stopwatch();
    bool pageIsActive;
    float angle;

    public HendecagramAnimationPage()
    {
        Title = "Hedecagram Animation";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();
        pageIsActive = true;
        stopwatch.Start();

        Device.StartTimer(TimeSpan.FromMilliseconds(33), () =>
        {
            double t = stopwatch.Elapsed.TotalMilliseconds % cycleTime / cycleTime;
            angle = (float)(360 * t);
            canvasView.InvalidateSurface();

            if (!pageIsActive)
            {
                stopwatch.Stop();
            }

            return pageIsActive;
        });
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();
        pageIsActive = false;
    }
    ...
}

angle 字段每隔 5 秒从 0 度动画变换为 360 度。 PaintSurface 处理程序以两种方式使用 angle 属性:在 SKColor.FromHsl 方法中指定颜色的色调,以及作为 Math.SinMath.Cos 方法的参数来控制星形的位置:

public class HendecagramAnimationPage : ContentPage
{
    ...
    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.Translate(info.Width / 2, info.Height / 2);
        float radius = (float)Math.Min(info.Width, info.Height) / 2 - 100;

        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Fill;
            paint.Color = SKColor.FromHsl(angle, 100, 50);

            float x = radius * (float)Math.Sin(Math.PI * angle / 180);
            float y = -radius * (float)Math.Cos(Math.PI * angle / 180);
            canvas.Translate(x, y);
            canvas.DrawPath(HendecagramPage.HendecagramPath, paint);
        }
    }
}

PaintSurface 处理程序调用 Translate 方法两次,首先变换到画布的中心,然后变换到以 (0, 0) 为中心的圆的周长。 圆的半径设置为尽可能大,同时仍将星形保持在页面范围内:

“Hendecagram 动画”页的三屏幕截图

请注意,星形围绕页面中心旋转时保持相同的方向。 它根本不会旋转。 旋转是旋转变换的任务。