Recorte con trazados y regiones
Usar rutas de acceso para recortar gráficos en áreas específicas y crear regiones
A veces es necesario restringir la representación de gráficos a un área determinada. Esto se conoce como recorte. Puedes usar recortes para efectos especiales, como esta imagen de un mono visto a través de un agujero de llaves:
El área de recorte es el área de la pantalla en la que se representan los gráficos. No se representa nada que se muestre fuera del área de recorte. Normalmente, el área de recorte se define mediante un rectángulo o un SKPath
objeto, pero también puede definir un área de recorte mediante un SKRegion
objeto . Estos dos tipos de objetos al principio parecen relacionados porque puede crear una región a partir de una ruta de acceso. Sin embargo, no se puede crear una ruta de acceso desde una región y son muy diferentes internamente: una ruta de acceso consta de una serie de líneas y curvas, mientras que una región se define mediante una serie de líneas de examen horizontales.
La imagen anterior fue creada por la página Monkey a través de Keyhole . La MonkeyThroughKeyholePage
clase define una ruta de acceso mediante datos SVG y usa el constructor para cargar un mapa de bits de los recursos del programa:
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);
}
}
...
}
Aunque el keyholePath
objeto describe el contorno de un agujero de llaves, las coordenadas son completamente arbitrarias y reflejan lo que era conveniente cuando se diseñaron los datos de ruta de acceso. Por este motivo, el PaintSurface
controlador obtiene los límites de esta ruta de acceso y llama Translate
a y Scale
para mover la ruta de acceso al centro de la pantalla y para que sea casi tan alto como la pantalla:
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));
}
}
Pero la ruta de acceso no se representa. En su lugar, después de las transformaciones, la ruta de acceso se usa para establecer un área de recorte con esta instrucción:
canvas.ClipPath(keyholePath);
A PaintSurface
continuación, el controlador restablece las transformaciones con una llamada a ResetMatrix
y dibuja el mapa de bits para extenderse al alto completo de la pantalla. En este código se supone que el mapa de bits es cuadrado, que es este mapa de bits determinado. El mapa de bits solo se representa dentro del área definida por la ruta de recorte:
La ruta de recorte está sujeta a las transformaciones en vigor cuando se llama al ClipPath
método y no a las transformaciones en vigor cuando se muestra un objeto gráfico (como un mapa de bits). La ruta de recorte forma parte del estado del lienzo que se guarda con el Save
método y se restaura con el Restore
método .
Combinación de rutas de recorte
Estrictamente hablando, el área de recorte no está "establecida" por el ClipPath
método . En su lugar, se combina con la ruta de recorte existente, que comienza como un rectángulo igual de tamaño al lienzo. Puede obtener los límites rectangulares del área de recorte mediante la LocalClipBounds
propiedad o la DeviceClipBounds
propiedad . La LocalClipBounds
propiedad devuelve un SKRect
valor que refleja las transformaciones que podrían estar en vigor. La DeviceClipBounds
propiedad devuelve un RectI
valor. Se trata de un rectángulo con dimensiones enteras y describe el área de recorte en dimensiones de píxeles reales.
Cualquier llamada para ClipPath
reducir el área de recorte combinando el área de recorte con un área nueva. Sintaxis completa del ClipPath
método que combina el área de recorte con un rectángulo:
public Void ClipRect(SKRect rect, SKClipOperation operation = SKClipOperation.Intersect, Boolean antialias = false);
De forma predeterminada, el área de recorte resultante es una intersección del área de recorte existente y o SKPath
SKRect
que se especifica en el ClipPath
método o ClipRect
. Esto se muestra en la página De intersección de cuatro círculos. El PaintSurface
controlador de la FourCircleInteresectClipPage
clase reutiliza el mismo SKPath
objeto para crear cuatro círculos superpuestos, cada uno de los cuales reduce el área de recorte a través de llamadas sucesivas a 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);
}
}
}
Lo que queda es la intersección de estos cuatro círculos:
La SKClipOperation
enumeración solo tiene dos miembros:
Difference
quita la ruta de acceso o el rectángulo especificados del área de recorte existente.Intersect
intersecta la ruta de acceso o rectángulo especificados con el área de recorte existente.
Si reemplaza los cuatro SKClipOperation.Intersect
argumentos de la FourCircleIntersectClipPage
clase por SKClipOperation.Difference
, verá lo siguiente:
Se han quitado cuatro círculos superpuestos del área de recorte.
La página Operaciones de recorte ilustra la diferencia entre estas dos operaciones con solo un par de círculos. El primer círculo de la izquierda se agrega al área de recorte con la operación de recorte predeterminada de , mientras que el segundo círculo de la derecha se agrega al área de recorte con la operación de recorte indicada por la etiqueta de Intersect
texto:
La ClipOperationsPage
clase define dos SKPaint
objetos como campos y, a continuación, divide la pantalla en dos áreas rectangulares. Estas áreas son diferentes en función de si el teléfono está en modo vertical o horizontal. A DisplayClipOp
continuación, la clase muestra el texto y llama a ClipPath
con las dos rutas de acceso de círculo para ilustrar cada operación de recorte:
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();
}
Llamar DrawPaint
normalmente hace que todo el lienzo se rellene con ese SKPaint
objeto, pero en este caso, el método simplemente pinta dentro del área de recorte.
Exploración de regiones
También puede definir un área de recorte en términos de un SKRegion
objeto .
Un objeto recién creado SKRegion
describe un área vacía. Normalmente, la primera llamada al objeto es SetRect
para que la región describa un área rectangular. El parámetro para SetRect
es un SKRectI
valor : un rectángulo con coordenadas enteras porque especifica el rectángulo en términos de píxeles. A continuación, puede llamar a SetPath
con un SKPath
objeto . Esto crea una región que es la misma que el interior de la ruta de acceso, pero recortada en la región rectangular inicial.
La región también se puede modificar llamando a una de las sobrecargas del Op
método, como esta:
public Boolean Op(SKRegion region, SKRegionOperation op)
La SKRegionOperation
enumeración es similar a SKClipOperation
pero tiene más miembros:
Difference
Intersect
Union
XOR
ReverseDifference
Replace
La región en la que realiza la Op
llamada se combina con la región especificada como un parámetro basado en el SKRegionOperation
miembro. Cuando finalmente obtenga una región adecuada para el recorte, puede establecerla como área de recorte del lienzo mediante el ClipRegion
método de SKCanvas
:
public void ClipRegion(SKRegion region, SKClipOperation operation = SKClipOperation.Intersect)
En la captura de pantalla siguiente se muestran áreas de recorte basadas en las seis operaciones de región. El círculo izquierdo es la región en la que se llama al Op
método y el círculo derecho es la región que se pasa al Op
método :
¿Estas son todas las posibilidades de combinar estos dos círculos? Considere la imagen resultante como una combinación de tres componentes, que por sí mismos se ven en las Difference
operaciones , Intersect
y ReverseDifference
. El número total de combinaciones es de dos a la tercera potencia, u ocho. Los dos que faltan son la región original (que resulta de no llamar Op
a en absoluto) y una región completamente vacía.
Es más difícil usar regiones para recortar porque primero debe crear una ruta de acceso y, a continuación, una región de esa ruta de acceso y, a continuación, combinar varias regiones. La estructura general de la página Operaciones de región es muy similar a Las operaciones de recorte , pero la RegionOperationsPage
clase divide la pantalla en seis áreas y muestra el trabajo adicional necesario para usar regiones para este trabajo:
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();
}
}
}
Esta es una gran diferencia entre el ClipPath
método y el ClipRegion
método :
Importante
A diferencia del ClipPath
método , el ClipRegion
método no se ve afectado por las transformaciones.
Para comprender la lógica de esta diferencia, resulta útil comprender lo que es una región. Si ha pensado en cómo se pueden implementar internamente las operaciones de recorte o las operaciones de región, es probable que parezca muy complicado. Se combinan varias rutas de acceso potencialmente muy complejas y es probable que el esquema de la ruta resultante sea una pesadilla algorítmica.
Este trabajo se simplifica considerablemente si cada trazado se reduce a una serie de líneas de exploración horizontales, como las de tubos de vacío antiguos. Cada línea de examen es simplemente una línea horizontal con un punto inicial y un punto final. Por ejemplo, un círculo con un radio de 10 píxeles se puede descomponer en 20 líneas de examen horizontales, cada uno de los cuales comienza en la parte izquierda del círculo y termina en la parte derecha. La combinación de dos círculos con cualquier operación de región se vuelve muy sencilla porque es simplemente una cuestión de examinar las coordenadas inicial y final de cada par de líneas de examen correspondientes.
Esto es lo que es una región: una serie de líneas de examen horizontales que definen un área.
Sin embargo, cuando un área se reduce a una serie de líneas de examen, estas líneas de examen se basan en una dimensión de píxel determinada. Estrictamente hablando, la región no es un objeto gráfico vectorial. Está más cerca de la naturaleza a un mapa de bits monocromático comprimido que a una ruta de acceso. Por lo tanto, las regiones no se pueden escalar ni girar sin perder fidelidad y, por este motivo, no se transforman cuando se usan para recortar áreas.
Sin embargo, puede aplicar transformaciones a regiones con fines de pintura. El programa Region Paint muestra vívidamente la naturaleza interna de las regiones. La RegionPaintPage
clase crea un SKRegion
objeto basado en un SKPath
círculo de radio de 10 unidades. Después, una transformación expande ese círculo para rellenar la página:
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);
}
}
}
}
La DrawRegion
llamada rellena la región en naranja, mientras que la DrawPath
llamada tiza la ruta de acceso original en azul para la comparación:
La región es claramente una serie de coordenadas discretas.
Si no es necesario usar transformaciones en relación con las áreas de recorte, puede usar regiones para recortar, como se muestra en la página De conmutación de cuatro hojas . La FourLeafCloverPage
clase construye una región compuesta a partir de cuatro regiones circulares, establece esa región compuesta como área de recorte y, a continuación, dibuja una serie de 360 líneas rectas que proceden del centro de la página:
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);
}
}
}
}
}
}
Realmente no parece un clover de cuatro hojas, pero es una imagen que, de lo contrario, podría ser difícil de representar sin recortar: