Обрезка изображения по границам области с помощью путей
Использование путей для клипа графики для определенных областей и создания регионов
Иногда необходимо ограничить отрисовку графики определенной областью. Это называется вырезка. Вы можете использовать вырезку для специальных эффектов, таких как это изображение обезьяны, замеченное через замочную скважину:
Область вырезки — это область экрана, в которой отрисовываются графические элементы. Все, что отображается вне области вырезки, не отображается. Область вырезки обычно определяется прямоугольником или 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);
По умолчанию результирующая область вырезки является пересечением существующей области вырезки и SKRect
SKPath
указанной в методе.ClipRect
ClipPath
Это показано на странице "Четыре круга" "Клип ". Обработчик 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
:
Это все возможности объединения этих двух кругов? Рассмотрим результирующий образ как сочетание трех компонентов, которые сами по себе рассматриваются в Difference
Intersect
операциях и 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);
}
}
}
}
}
}
Он действительно не выглядит как четырехконечный кловер, но это изображение, которое может быть трудно отрисовки без вырезки: