mirror of https://github.com/SixLabors/ImageSharp
7 changed files with 611 additions and 165 deletions
@ -0,0 +1,239 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.ParallelUtils; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Memory; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// Provides the base methods to perform affine transforms on an image.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal class AffineTransformProcessorOld<TPixel> : InterpolatedTransformProcessorBase<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AffineTransformProcessorOld{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="matrix">The transform matrix</param>
|
|||
/// <param name="sampler">The sampler to perform the transform operation.</param>
|
|||
/// <param name="targetDimensions">The target dimensions to constrain the transformed image to.</param>
|
|||
public AffineTransformProcessorOld(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) |
|||
: base(sampler) |
|||
{ |
|||
this.TransformMatrix = matrix; |
|||
this.TargetDimensions = targetDimensions; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the matrix used to supply the affine transform
|
|||
/// </summary>
|
|||
public Matrix3x2 TransformMatrix { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the target dimensions to constrain the transformed image to
|
|||
/// </summary>
|
|||
public Size TargetDimensions { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle) |
|||
{ |
|||
// We will always be creating the clone even for mutate because we may need to resize the canvas
|
|||
IEnumerable<ImageFrame<TPixel>> frames = |
|||
source.Frames.Select(x => new ImageFrame<TPixel>(source.GetConfiguration(), this.TargetDimensions, x.MetaData.DeepClone())); |
|||
|
|||
// Use the overload to prevent an extra frame being added
|
|||
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.DeepClone(), frames); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply( |
|||
ImageFrame<TPixel> source, |
|||
ImageFrame<TPixel> destination, |
|||
Rectangle sourceRectangle, |
|||
Configuration configuration) |
|||
{ |
|||
int height = this.TargetDimensions.Height; |
|||
int width = this.TargetDimensions.Width; |
|||
|
|||
Rectangle sourceBounds = source.Bounds(); |
|||
var targetBounds = new Rectangle(0, 0, width, height); |
|||
|
|||
// Since could potentially be resizing the canvas we might need to re-calculate the matrix
|
|||
Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds); |
|||
|
|||
// Convert from screen to world space.
|
|||
Matrix3x2.Invert(matrix, out matrix); |
|||
|
|||
if (this.Sampler is NearestNeighborResampler) |
|||
{ |
|||
ParallelHelper.IterateRows( |
|||
targetBounds, |
|||
configuration, |
|||
rows => |
|||
{ |
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
Span<TPixel> destRow = destination.GetPixelRowSpan(y); |
|||
|
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
var point = Point.Transform(new Point(x, y), matrix); |
|||
if (sourceBounds.Contains(point.X, point.Y)) |
|||
{ |
|||
destRow[x] = source[point.X, point.Y]; |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
|
|||
return; |
|||
} |
|||
|
|||
int maxSourceX = source.Width - 1; |
|||
int maxSourceY = source.Height - 1; |
|||
(float radius, float scale, float ratio) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); |
|||
(float radius, float scale, float ratio) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); |
|||
float xScale = xRadiusScale.scale; |
|||
float yScale = yRadiusScale.scale; |
|||
var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius); |
|||
IResampler sampler = this.Sampler; |
|||
var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY); |
|||
int xLength = (int)MathF.Ceiling((radius.X * 2) + 2); |
|||
int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2); |
|||
|
|||
MemoryAllocator memoryAllocator = configuration.MemoryAllocator; |
|||
|
|||
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height)) |
|||
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height)) |
|||
{ |
|||
ParallelHelper.IterateRows( |
|||
targetBounds, |
|||
configuration, |
|||
rows => |
|||
{ |
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); |
|||
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); |
|||
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y)); |
|||
|
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
// Use the single precision position to calculate correct bounding pixels
|
|||
// otherwise we get rogue pixels outside of the bounds.
|
|||
var point = Vector2.Transform(new Vector2(x, y), matrix); |
|||
|
|||
// Clamp sampling pixel radial extents to the source image edges
|
|||
Vector2 maxXY = point + radius; |
|||
Vector2 minXY = point - radius; |
|||
|
|||
// max, maxY, minX, minY
|
|||
var extents = new Vector4( |
|||
MathF.Floor(maxXY.X + .5F), |
|||
MathF.Floor(maxXY.Y + .5F), |
|||
MathF.Ceiling(minXY.X - .5F), |
|||
MathF.Ceiling(minXY.Y - .5F)); |
|||
|
|||
int right = (int)extents.X; |
|||
int bottom = (int)extents.Y; |
|||
int left = (int)extents.Z; |
|||
int top = (int)extents.W; |
|||
|
|||
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); |
|||
|
|||
int maxX = (int)extents.X; |
|||
int maxY = (int)extents.Y; |
|||
int minX = (int)extents.Z; |
|||
int minY = (int)extents.W; |
|||
|
|||
if (minX == maxX || minY == maxY) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// It appears these have to be calculated on-the-fly.
|
|||
// Precalculating transformed weights would require prior knowledge of every transformed pixel location
|
|||
// since they can be at sub-pixel positions on both axis.
|
|||
// I've optimized where I can but am always open to suggestions.
|
|||
if (yScale > 1 && xScale > 1) |
|||
{ |
|||
CalculateWeightsDown( |
|||
top, |
|||
bottom, |
|||
minY, |
|||
maxY, |
|||
point.Y, |
|||
sampler, |
|||
yScale, |
|||
ref ySpanRef, |
|||
yLength); |
|||
|
|||
CalculateWeightsDown( |
|||
left, |
|||
right, |
|||
minX, |
|||
maxX, |
|||
point.X, |
|||
sampler, |
|||
xScale, |
|||
ref xSpanRef, |
|||
xLength); |
|||
} |
|||
else |
|||
{ |
|||
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); |
|||
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); |
|||
} |
|||
|
|||
// Now multiply the results against the offsets
|
|||
Vector4 sum = Vector4.Zero; |
|||
for (int yy = 0, j = minY; j <= maxY; j++, yy++) |
|||
{ |
|||
float yWeight = Unsafe.Add(ref ySpanRef, yy); |
|||
|
|||
for (int xx = 0, i = minX; i <= maxX; i++, xx++) |
|||
{ |
|||
float xWeight = Unsafe.Add(ref xSpanRef, xx); |
|||
|
|||
// Values are first premultiplied to prevent darkening of edge pixels
|
|||
var current = source[i, j].ToVector4(); |
|||
Vector4Utils.Premultiply(ref current); |
|||
sum += current * xWeight * yWeight; |
|||
} |
|||
} |
|||
|
|||
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); |
|||
|
|||
// Reverse the premultiplication
|
|||
Vector4Utils.UnPremultiply(ref sum); |
|||
dest.FromVector4(sum); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a transform matrix adjusted for final processing based upon the target image bounds.
|
|||
/// </summary>
|
|||
/// <param name="sourceRectangle">The source image bounds.</param>
|
|||
/// <param name="destinationRectangle">The destination image bounds.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="Matrix3x2"/>.
|
|||
/// </returns>
|
|||
protected virtual Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) |
|||
=> this.TransformMatrix; |
|||
} |
|||
} |
|||
@ -0,0 +1,188 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Numerics; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Primitives; |
|||
|
|||
// TODO: It would be great if we could somehow optimize this to calculate the weights once.
|
|||
// currently we cannot do that as we are calulating the weight of the transformed point dimension
|
|||
// not the point in the original image.
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// Contains the methods required to calculate kernel sampling weights on-the-fly.
|
|||
/// </summary>
|
|||
internal class TransformKernelMap : IDisposable |
|||
{ |
|||
private readonly Buffer2D<float> yBuffer; |
|||
private readonly Buffer2D<float> xBuffer; |
|||
private readonly float yScale; |
|||
private readonly float xScale; |
|||
private readonly int yLength; |
|||
private readonly int xLength; |
|||
private readonly Vector2 extents; |
|||
private Vector4 maxSourceExtents; |
|||
private readonly IResampler sampler; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TransformKernelMap"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="source">The source size.</param>
|
|||
/// <param name="destination">The destination size.</param>
|
|||
/// <param name="sampler">The sampler.</param>
|
|||
public TransformKernelMap(Configuration configuration, Size source, Size destination, IResampler sampler) |
|||
{ |
|||
this.sampler = sampler; |
|||
(float radius, float scale) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); |
|||
(float radius, float scale) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); |
|||
|
|||
this.yScale = yRadiusScale.scale; |
|||
this.xScale = xRadiusScale.scale; |
|||
this.extents = new Vector2(xRadiusScale.radius, yRadiusScale.radius); |
|||
this.xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2); |
|||
this.yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2); |
|||
|
|||
// We use 2D buffers so that we can access the weight spans in parallel.
|
|||
this.yBuffer = configuration.MemoryAllocator.Allocate2D<float>(this.yLength, destination.Height); |
|||
this.xBuffer = configuration.MemoryAllocator.Allocate2D<float>(this.xLength, destination.Height); |
|||
|
|||
int maxX = source.Width - 1; |
|||
int maxY = source.Height - 1; |
|||
this.maxSourceExtents = new Vector4(maxX, maxY, maxX, maxY); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a reference to the first item of the y window.
|
|||
/// </summary>
|
|||
/// <returns>The reference to the first item of the window.</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public ref float GetYStartReference(int y) |
|||
=> ref MemoryMarshal.GetReference(this.yBuffer.GetRowSpan(y)); |
|||
|
|||
/// <summary>
|
|||
/// Gets a reference to the first item of the x window.
|
|||
/// </summary>
|
|||
/// <returns>The reference to the first item of the window.</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public ref float GetXStartReference(int y) |
|||
=> ref MemoryMarshal.GetReference(this.xBuffer.GetRowSpan(y)); |
|||
|
|||
public void Convolve<TPixel>( |
|||
Vector2 transformedPoint, |
|||
int column, |
|||
ref float ySpanRef, |
|||
ref float xSpanRef, |
|||
Buffer2D<TPixel> sourcePixels, |
|||
Span<Vector4> targetRow) |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
// Clamp sampling pixel radial extents to the source image edges
|
|||
Vector2 minXY = transformedPoint - this.extents; |
|||
Vector2 maxXY = transformedPoint + this.extents; |
|||
|
|||
// minX, minY, maxX, maxY
|
|||
var extents = new Vector4( |
|||
MathF.Ceiling(minXY.X - .5F), |
|||
MathF.Ceiling(minXY.Y - .5F), |
|||
MathF.Floor(maxXY.X + .5F), |
|||
MathF.Floor(maxXY.Y + .5F)); |
|||
|
|||
int left = (int)extents.X; |
|||
int top = (int)extents.Y; |
|||
int right = (int)extents.Z; |
|||
int bottom = (int)extents.W; |
|||
|
|||
extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents); |
|||
|
|||
int minX = (int)extents.X; |
|||
int minY = (int)extents.Y; |
|||
int maxX = (int)extents.Z; |
|||
int maxY = (int)extents.W; |
|||
|
|||
if (minX == maxX || minY == maxY) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// TODO: Get Anton to use his superior brain on this one.
|
|||
// It looks to me like we're calculating the same weights over and over again
|
|||
// since min(X+Y) and max(X+Y) are the same distance apart.
|
|||
this.CalculateWeights(minY, maxY, maxY - minY, transformedPoint.Y, ref ySpanRef); |
|||
this.CalculateWeights(minX, maxX, maxX - minX, transformedPoint.X, ref xSpanRef); |
|||
|
|||
Vector4 sum = Vector4.Zero; |
|||
for (int kernelY = 0, y = minY; y <= maxY; y++, kernelY++) |
|||
{ |
|||
float yWeight = Unsafe.Add(ref ySpanRef, kernelY); |
|||
|
|||
for (int kernelX = 0, x = minX; x <= maxX; x++, kernelX++) |
|||
{ |
|||
float xWeight = Unsafe.Add(ref xSpanRef, kernelX); |
|||
|
|||
// Values are first premultiplied to prevent darkening of edge pixels.
|
|||
var current = sourcePixels[x, y].ToVector4(); |
|||
Vector4Utils.Premultiply(ref current); |
|||
sum += current * xWeight * yWeight; |
|||
} |
|||
} |
|||
|
|||
// Reverse the premultiplication
|
|||
Vector4Utils.UnPremultiply(ref sum); |
|||
targetRow[column] = sum; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculated the normalized weights for the given point.
|
|||
/// </summary>
|
|||
/// <param name="min">The minimum sampling offset</param>
|
|||
/// <param name="max">The maximum sampling offset</param>
|
|||
/// <param name="length">The length of the weights collection</param>
|
|||
/// <param name="point">The transformed point dimension</param>
|
|||
/// <param name="weightsRef">The reference to the collection of weights</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private void CalculateWeights(int min, int max, int length, float point, ref float weightsRef) |
|||
{ |
|||
float sum = 0; |
|||
for (int x = 0, i = min; i <= max; i++, x++) |
|||
{ |
|||
float weight = this.sampler.GetValue(i - point); |
|||
sum += weight; |
|||
Unsafe.Add(ref weightsRef, x) = this.sampler.GetValue(i - point); |
|||
} |
|||
|
|||
// TODO: Do we need this? Check what happens when we scale an image down.
|
|||
// if (sum > 0)
|
|||
// {
|
|||
// for (int i = 0; i < length; i++)
|
|||
// {
|
|||
// ref float wRef = ref Unsafe.Add(ref weightsRef, i);
|
|||
// wRef /= sum;
|
|||
// }
|
|||
// }
|
|||
} |
|||
|
|||
private (float radius, float scale) GetSamplingRadius(int sourceSize, int destinationSize) |
|||
{ |
|||
float scale = (float)sourceSize / destinationSize; |
|||
|
|||
if (scale < 1F) |
|||
{ |
|||
scale = 1F; |
|||
} |
|||
|
|||
return (MathF.Ceiling(scale * this.sampler.Radius), scale); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
this.yBuffer?.Dispose(); |
|||
this.xBuffer?.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Numerics; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|||
{ |
|||
/// <summary>
|
|||
/// Contains utility methods for working with transforms.
|
|||
/// </summary>
|
|||
public static class TransformUtils |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a centered rotation matrix using the given rotation in degrees and the source size.
|
|||
/// </summary>
|
|||
/// <param name="degrees">The amount of rotation, in degrees.</param>
|
|||
/// <param name="size">The source image size.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/>.</returns>
|
|||
public static Matrix3x2 CreateCenteredRotationMatrixDegrees(float degrees, Size size) |
|||
=> CreateCenteredTransformMatrix( |
|||
new Rectangle(Point.Empty, size), |
|||
Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty)); |
|||
|
|||
/// <summary>
|
|||
/// Gets the centered transform matrix based upon the source and destination rectangles.
|
|||
/// </summary>
|
|||
/// <param name="sourceRectangle">The source image bounds.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/></returns>
|
|||
public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix) |
|||
{ |
|||
Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix); |
|||
|
|||
// We invert the matrix to handle the transformation from screen to world space.
|
|||
// This ensures scaling matrices are correct.
|
|||
Matrix3x2.Invert(matrix, out Matrix3x2 inverted); |
|||
|
|||
var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); |
|||
var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); |
|||
|
|||
// Translate back to world space.
|
|||
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); |
|||
|
|||
return centered; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the centered transform matrix based upon the source and destination rectangles.
|
|||
/// </summary>
|
|||
/// <param name="sourceRectangle">The source image bounds.</param>
|
|||
/// <param name="destinationRectangle">The destination image bounds.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>The <see cref="Matrix3x2"/></returns>
|
|||
public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix) |
|||
{ |
|||
// We invert the matrix to handle the transformation from screen to world space.
|
|||
// This ensures scaling matrices are correct.
|
|||
Matrix3x2.Invert(matrix, out Matrix3x2 inverted); |
|||
|
|||
var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F); |
|||
var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F); |
|||
|
|||
// Translate back to world space.
|
|||
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered); |
|||
|
|||
return centered; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the rectangle bounds relative to the source for the given transformation matrix.
|
|||
/// </summary>
|
|||
/// <param name="rectangle">The source rectangle.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="Rectangle"/>.
|
|||
/// </returns>
|
|||
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix) |
|||
{ |
|||
Rectangle transformed = GetTransformedRectangle(rectangle, matrix); |
|||
|
|||
// TODO: Check this.
|
|||
return new Rectangle(0, 0, transformed.Width, transformed.Height); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the rectangle relative to the source for the given transformation matrix.
|
|||
/// </summary>
|
|||
/// <param name="rectangle">The source rectangle.</param>
|
|||
/// <param name="matrix">The transformation matrix.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="Rectangle"/>.
|
|||
/// </returns>
|
|||
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix) |
|||
{ |
|||
if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) |
|||
{ |
|||
return rectangle; |
|||
} |
|||
|
|||
var tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); |
|||
var tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); |
|||
var bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); |
|||
var br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); |
|||
|
|||
return GetBoundingRectangle(tl, tr, bl, br); |
|||
} |
|||
|
|||
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br) |
|||
{ |
|||
// Find the minimum and maximum "corners" based on the given vectors
|
|||
float left = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); |
|||
float top = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); |
|||
float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); |
|||
float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); |
|||
|
|||
return Rectangle.Round(RectangleF.FromLTRB(left, top, right, bottom)); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue