Поделиться через


Обрезка изображения по границам области с помощью путей

Использование путей для клипа графики для определенных областей и создания регионов

Иногда необходимо ограничить отрисовку графики определенной областью. Это называется вырезка. Вы можете использовать вырезку для специальных эффектов, таких как это изображение обезьяны, замеченное через замочную скважину:

Обезьяна через замышку

Область вырезки — это область экрана, в которой отрисовываются графические элементы. Все, что отображается вне области вырезки, не отображается. Область вырезки обычно определяется прямоугольником или SKPath объектом, но можно также определить область вырезки с помощью SKRegion объекта. Эти два типа объектов сначала кажутся связанными, так как можно создать регион из пути. Однако невозможно создать путь из региона, и они очень отличаются внутри: путь состоит из ряда линий и кривых, а область определяется рядом горизонтальных линий сканирования.

Изображение выше было создано обезьяной на странице "Замок". Класс MonkeyThroughKeyholePage определяет путь с помощью данных SVG и использует конструктор для загрузки растрового изображения из программных ресурсов:

public class MonkeyThroughKeyholePage : ContentPage
{
    SKBitmap bitmap;
    SKPath keyholePath = SKPath.ParseSvgPathData(
        "M 300 130 L 250 350 L 450 350 L 400 130 A 70 70 0 1 0 300 130 Z");

    public MonkeyThroughKeyholePage()
    {
        Title = "Monkey through Keyhole";

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

        string resourceID = "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        using (Stream stream = assembly.GetManifestResourceStream(resourceID))
        {
            bitmap = SKBitmap.Decode(stream);
        }
    }
    ...
}

keyholePath Несмотря на то, что объект описывает контур замочной скважины, координаты полностью произвольны и отражают то, что было удобно при разработке данных пути. По этой причине PaintSurface обработчик получает границы этого пути и вызовов Translate , а Scale также перемещает путь к центру экрана и делает его почти столь высоким, как экран:

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

        canvas.Clear();

        // Set transform to center and enlarge clip path to window height
        SKRect bounds;
        keyholePath.GetTightBounds(out bounds);

        canvas.Translate(info.Width / 2, info.Height / 2);
        canvas.Scale(0.98f * info.Height / bounds.Height);
        canvas.Translate(-bounds.MidX, -bounds.MidY);

        // Set the clip path
        canvas.ClipPath(keyholePath);

        // Reset transforms
        canvas.ResetMatrix();

        // Display monkey to fill height of window but maintain aspect ratio
        canvas.DrawBitmap(bitmap,
            new SKRect((info.Width - info.Height) / 2, 0,
                       (info.Width + info.Height) / 2, info.Height));
    }
}

Но путь не отображается. Вместо этого, следуя преобразованиям, путь используется для задания области вырезки с помощью этой инструкции:

canvas.ClipPath(keyholePath);

Затем PaintSurface обработчик сбрасывает преобразования с вызовом ResetMatrix и рисует растровое изображение для расширения до полной высоты экрана. В этом коде предполагается, что растровое изображение является квадратным, что такое конкретное растровое изображение. Растровое изображение отрисовывается только в пределах области, определенной путем обрезки:

Тройной снимок экрана: страница

Путь обрезки подвергается преобразованиям, которые применяются при ClipPath вызове метода, а не преобразованиям при отображении графического объекта (например, растрового изображения). Путь обрезки является частью состояния холста, сохраненного с Save помощью метода и восстановленного Restore с помощью метода.

Объединение путей обрезки

Строго говоря, область вырезки не задана методом ClipPath . Вместо этого он сочетается с существующим контуром вырезки, который начинается как прямоугольник, равный размеру холста. Прямоугольные границы области вырезки можно получить с помощью LocalClipBounds свойства или DeviceClipBounds свойства. Свойство LocalClipBounds возвращает SKRect значение, которое отражает любые преобразования, которые могут быть в действии. Свойство DeviceClipBounds возвращает RectI значение. Это прямоугольник с целыми измерениями и описывает область вырезки в фактических измерениях пикселей.

Любой вызов для ClipPath уменьшения области вырезки путем объединения области вырезки с новой областью. Полный синтаксис метода, объединяющего ClipPath область вырезки с прямоугольником:

public Void ClipRect(SKRect rect, SKClipOperation operation = SKClipOperation.Intersect, Boolean antialias = false);

По умолчанию результирующая область вырезки является пересечением существующей области вырезки и SKRectSKPath указанной в методе.ClipRectClipPath Это показано на странице "Четыре круга" "Клип ". Обработчик PaintSurface в FourCircleInteresectClipPage классе повторно использует один и тот же SKPath объект для создания четырех перекрывающихся кругов, каждый из которых уменьшает область вырезки через последовательные вызовы ClipPath:

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

    canvas.Clear();

    float size = Math.Min(info.Width, info.Height);
    float radius = 0.4f * size;
    float offset = size / 2 - radius;

    // Translate to center
    canvas.Translate(info.Width / 2, info.Height / 2);

    using (SKPath path = new SKPath())
    {
        path.AddCircle(-offset, -offset, radius);
        canvas.ClipPath(path, SKClipOperation.Intersect);

        path.Reset();
        path.AddCircle(-offset, offset, radius);
        canvas.ClipPath(path, SKClipOperation.Intersect);

        path.Reset();
        path.AddCircle(offset, -offset, radius);
        canvas.ClipPath(path, SKClipOperation.Intersect);

        path.Reset();
        path.AddCircle(offset, offset, radius);
        canvas.ClipPath(path, SKClipOperation.Intersect);

        using (SKPaint paint = new SKPaint())
        {
            paint.Style = SKPaintStyle.Fill;
            paint.Color = SKColors.Blue;
            canvas.DrawPaint(paint);
        }
    }
}

Что осталось, является пересечением этих четырех кругов:

Снимок экрана с тройным снимок экрана страницы

Перечисление SKClipOperation содержит только два элемента:

  • Difference Удаляет указанный путь или прямоугольник из существующей области вырезки

  • Intersect пересекает указанный путь или прямоугольник с существующей областью вырезки

При замене четырех SKClipOperation.Intersect аргументов в FourCircleIntersectClipPage классе SKClipOperation.Differenceвы увидите следующее:

Тройной снимок экрана: страница

Четыре перекрывающихся круга были удалены из области вырезки.

Страница "Операции клипа" иллюстрирует разницу между этими двумя операциями только с парой кругов. Первый круг слева добавляется в область вырезки с операцией Intersectклипа по умолчанию, а второй круг справа добавляется в область вырезки с операцией клипа, указанной текстовой меткой:

Тройной снимок экрана: страница

Класс ClipOperationsPage определяет два SKPaint объекта в виде полей, а затем делит экран на две прямоугольные области. Эти области различаются в зависимости от того, находится ли телефон в книжном или альбомном режиме. Затем DisplayClipOp класс отображает текст и вызовы ClipPath с двумя путями круга, чтобы проиллюстрировать каждую операцию клипа:

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

    canvas.Clear();

    float x = 0;
    float y = 0;

    foreach (SKClipOperation clipOp in Enum.GetValues(typeof(SKClipOperation)))
    {
        // Portrait mode
        if (info.Height > info.Width)
        {
            DisplayClipOp(canvas, new SKRect(x, y, x + info.Width, y + info.Height / 2), clipOp);
            y += info.Height / 2;
        }
        // Landscape mode
        else
        {
            DisplayClipOp(canvas, new SKRect(x, y, x + info.Width / 2, y + info.Height), clipOp);
            x += info.Width / 2;
        }
    }
}

void DisplayClipOp(SKCanvas canvas, SKRect rect, SKClipOperation clipOp)
{
    float textSize = textPaint.TextSize;
    canvas.DrawText(clipOp.ToString(), rect.MidX, rect.Top + textSize, textPaint);
    rect.Top += textSize;

    float radius = 0.9f * Math.Min(rect.Width / 3, rect.Height / 2);
    float xCenter = rect.MidX;
    float yCenter = rect.MidY;

    canvas.Save();

    using (SKPath path1 = new SKPath())
    {
        path1.AddCircle(xCenter - radius / 2, yCenter, radius);
        canvas.ClipPath(path1);

        using (SKPath path2 = new SKPath())
        {
            path2.AddCircle(xCenter + radius / 2, yCenter, radius);
            canvas.ClipPath(path2, clipOp);

            canvas.DrawPaint(fillPaint);
        }
    }

    canvas.Restore();
}

Вызов DrawPaint обычно приводит к заполнению всего холста этим SKPaint объектом, но в этом случае метод просто красит в области вырезки.

Изучение регионов

Вы также можете определить область вырезки с точки зрения SKRegion объекта.

Созданный SKRegion объект описывает пустую область. Обычно первый вызов объекта заключается SetRect в том, что регион описывает прямоугольную область. Параметр SetRect является значением SKRectI — прямоугольник с целыми координатами, так как он задает прямоугольник с точки зрения пикселей. Затем можно вызвать SetPath объект SKPath . При этом создается регион, который совпадает с интерьером пути, но обрезается к исходной прямоугольной области.

Кроме того, регион можно изменить, вызвав одну из Op перегрузок метода, например следующую:

public Boolean Op(SKRegion region, SKRegionOperation op)

Перечисление SKRegionOperation аналогично SKClipOperation , но имеет больше элементов:

  • Difference

  • Intersect

  • Union

  • XOR

  • ReverseDifference

  • Replace

Регион, на который выполняется Op звонок, объединяется с регионом, указанным в качестве параметра на SKRegionOperation основе элемента. Когда вы, наконец, получите регион, подходящий для вырезки, вы можете задать его в качестве области вырезки холста с помощью ClipRegion метода SKCanvas:

public void ClipRegion(SKRegion region, SKClipOperation operation = SKClipOperation.Intersect)

На следующем снимка экрана показана вырезка областей на основе шести операций региона. Левый круг — это область Op , в которую вызывается метод, а правый круг — регион, переданный методу Op :

Тройной снимок экрана: страница

Это все возможности объединения этих двух кругов? Рассмотрим результирующий образ как сочетание трех компонентов, которые сами по себе рассматриваются в DifferenceIntersectоперациях и ReverseDifference операциях. Общее количество комбинаций — два до третьего или восемь. Те два, которые отсутствуют, являются исходным регионом (который приводит к тому, что не вызывается Op вообще) и полностью пустым регионом.

Труднее использовать регионы для вырезки, так как сначала необходимо создать путь, а затем регион из этого пути, а затем объединить несколько регионов. Общая структура страницы операций региона очень похожа на операции клипа, но RegionOperationsPage класс делит экран на шесть областей и показывает дополнительную работу, необходимую для использования регионов для этого задания:

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

    canvas.Clear();

    float x = 0;
    float y = 0;
    float width = info.Height > info.Width ? info.Width / 2 : info.Width / 3;
    float height = info.Height > info.Width ? info.Height / 3 : info.Height / 2;

    foreach (SKRegionOperation regionOp in Enum.GetValues(typeof(SKRegionOperation)))
    {
        DisplayClipOp(canvas, new SKRect(x, y, x + width, y + height), regionOp);

        if ((x += width) >= info.Width)
        {
            x = 0;
            y += height;
        }
    }
}

void DisplayClipOp(SKCanvas canvas, SKRect rect, SKRegionOperation regionOp)
{
    float textSize = textPaint.TextSize;
    canvas.DrawText(regionOp.ToString(), rect.MidX, rect.Top + textSize, textPaint);
    rect.Top += textSize;

    float radius = 0.9f * Math.Min(rect.Width / 3, rect.Height / 2);
    float xCenter = rect.MidX;
    float yCenter = rect.MidY;

    SKRectI recti = new SKRectI((int)rect.Left, (int)rect.Top,
                                (int)rect.Right, (int)rect.Bottom);

    using (SKRegion wholeRectRegion = new SKRegion())
    {
        wholeRectRegion.SetRect(recti);

        using (SKRegion region1 = new SKRegion(wholeRectRegion))
        using (SKRegion region2 = new SKRegion(wholeRectRegion))
        {
            using (SKPath path1 = new SKPath())
            {
                path1.AddCircle(xCenter - radius / 2, yCenter, radius);
                region1.SetPath(path1);
            }

            using (SKPath path2 = new SKPath())
            {
                path2.AddCircle(xCenter + radius / 2, yCenter, radius);
                region2.SetPath(path2);
            }

            region1.Op(region2, regionOp);

            canvas.Save();
            canvas.ClipRegion(region1);
            canvas.DrawPaint(fillPaint);
            canvas.Restore();
        }
    }
}

Вот большая разница между ClipPath методом и методом ClipRegion :

Внимание

ClipPath В отличие от метода, ClipRegion метод не влияет на преобразования.

Чтобы понять обоснование этой разницы, полезно понять, что такое регион. Если вы думали о том, как операции клипа или региональные операции могут быть реализованы внутренне, вероятно, это очень сложно. Несколько потенциально очень сложных путей объединяются, и контур результирующий путь, скорее всего, алгоритмический кошмар.

Это задание значительно упрощается, если каждый путь уменьшается до ряда горизонтальных линий сканирования, таких как те, что в старых вакуумных трубных телевизорах. Каждая линия сканирования — это просто горизонтальная линия с начальной точкой и конечной точкой. Например, круг с радиусом 10 пикселей можно разложить на 20 горизонтальных линий сканирования, каждый из которых начинается в левой части круга и заканчивается в правой части. Объединение двух кругов с любой операцией региона становится очень простым, так как это просто вопрос проверки координат начала и конца каждой пары соответствующих строк сканирования.

Это то, что такое регион: ряд горизонтальных линий сканирования, определяющих область.

Однако, если область уменьшается до ряда линий сканирования, эти линии сканирования основаны на определенном измерении пикселей. Строго говоря, область не является векторным графическим объектом. Он ближе к сжатой монохромной растровой карте, чем к пути. Следовательно, области нельзя масштабировать или повернуть без потери точности, и по этой причине они не преобразуются при использовании для вырезки областей.

Однако для рисования можно применять преобразования к регионам. Программа «Краска региона» ярко демонстрирует внутреннюю природу регионов. Класс RegionPaintPage создает SKRegion объект на SKPath основе круга радиуса 10 единиц. Затем преобразование расширяет этот круг, чтобы заполнить страницу:

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

    canvas.Clear();

    int radius = 10;

    // Create circular path
    using (SKPath circlePath = new SKPath())
    {
        circlePath.AddCircle(0, 0, radius);

        // Create circular region
        using (SKRegion circleRegion = new SKRegion())
        {
            circleRegion.SetRect(new SKRectI(-radius, -radius, radius, radius));
            circleRegion.SetPath(circlePath);

            // Set transform to move it to center and scale up
            canvas.Translate(info.Width / 2, info.Height / 2);
            canvas.Scale(Math.Min(info.Width / 2, info.Height / 2) / radius);

            // Fill region
            using (SKPaint fillPaint = new SKPaint())
            {
                fillPaint.Style = SKPaintStyle.Fill;
                fillPaint.Color = SKColors.Orange;

                canvas.DrawRegion(circleRegion, fillPaint);
            }

            // Stroke path for comparison
            using (SKPaint strokePaint = new SKPaint())
            {
                strokePaint.Style = SKPaintStyle.Stroke;
                strokePaint.Color = SKColors.Blue;
                strokePaint.StrokeWidth = 0.1f;

                canvas.DrawPath(circlePath, strokePaint);
            }
        }
    }
}

Вызов DrawRegion заполняет регион оранжевым цветом, а DrawPath вызов обнажает исходный путь синим цветом для сравнения:

Тройной снимок экрана страницы

Регион, очевидно, является рядом дискретных координат.

Если вам не нужно использовать преобразования в связи с областями вырезки, можно использовать регионы для вырезки, как показано на странице "Четырехконечная кловер ". Класс FourLeafCloverPage создает составной регион из четырех циклических регионов, задает этот составной регион в качестве области вырезки, а затем рисует ряд 360 прямых линий, исходящих из центра страницы:

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

    canvas.Clear();

    float xCenter = info.Width / 2;
    float yCenter = info.Height / 2;
    float radius = 0.24f * Math.Min(info.Width, info.Height);

    using (SKRegion wholeScreenRegion = new SKRegion())
    {
        wholeScreenRegion.SetRect(new SKRectI(0, 0, info.Width, info.Height));

        using (SKRegion leftRegion = new SKRegion(wholeScreenRegion))
        using (SKRegion rightRegion = new SKRegion(wholeScreenRegion))
        using (SKRegion topRegion = new SKRegion(wholeScreenRegion))
        using (SKRegion bottomRegion = new SKRegion(wholeScreenRegion))
        {
            using (SKPath circlePath = new SKPath())
            {
                // Make basic circle path
                circlePath.AddCircle(xCenter, yCenter, radius);

                // Left leaf
                circlePath.Transform(SKMatrix.MakeTranslation(-radius, 0));
                leftRegion.SetPath(circlePath);

                // Right leaf
                circlePath.Transform(SKMatrix.MakeTranslation(2 * radius, 0));
                rightRegion.SetPath(circlePath);

                // Make union of right with left
                leftRegion.Op(rightRegion, SKRegionOperation.Union);

                // Top leaf
                circlePath.Transform(SKMatrix.MakeTranslation(-radius, -radius));
                topRegion.SetPath(circlePath);

                // Combine with bottom leaf
                circlePath.Transform(SKMatrix.MakeTranslation(0, 2 * radius));
                bottomRegion.SetPath(circlePath);

                // Make union of top with bottom
                bottomRegion.Op(topRegion, SKRegionOperation.Union);

                // Exclusive-OR left and right with top and bottom
                leftRegion.Op(bottomRegion, SKRegionOperation.XOR);

                // Set that as clip region
                canvas.ClipRegion(leftRegion);

                // Set transform for drawing lines from center
                canvas.Translate(xCenter, yCenter);

                // Draw 360 lines
                for (double angle = 0; angle < 360; angle++)
                {
                    float x = 2 * radius * (float)Math.Cos(Math.PI * angle / 180);
                    float y = 2 * radius * (float)Math.Sin(Math.PI * angle / 180);

                    using (SKPaint strokePaint = new SKPaint())
                    {
                        strokePaint.Color = SKColors.Green;
                        strokePaint.StrokeWidth = 2;

                        canvas.DrawLine(0, 0, x, y, strokePaint);
                    }
                }
            }
        }
    }
}

Он действительно не выглядит как четырехконечный кловер, но это изображение, которое может быть трудно отрисовки без вырезки:

Тройной снимок экрана страницы четырехконечного кловера