// Copyright (c) Craftwork Games. All rights reserved. // Licensed under the MIT license. // See LICENSE file in the project root for full license information. using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace MonoGame.Extended.Graphics; /// /// Provides extension methods for the class. /// public static class SpriteBatchExtensions { #region ----------------------------NinePatch----------------------------- private static readonly Rectangle[] _patchCache = new Rectangle[9]; private static Rectangle _rect = default; /// /// Draws a nine-patch region to the sprite batch. /// /// The sprite batch. /// The nine-patch region. /// The destination rectangle. /// The color to tint the nine-patch region. /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, NinePatch ninePatchRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null) { CreateDestinationPatches(ninePatchRegion, destinationRectangle); ReadOnlySpan sourcePatches = ninePatchRegion.Patches; for (int i = 0; i < sourcePatches.Length; i++) { Texture2DRegion sourceRegion = sourcePatches[i]; Rectangle destinationRect = _patchCache[i]; if (clippingRectangle.HasValue) { sourceRegion = ClipSourceRegion(sourceRegion, destinationRect, clippingRectangle.Value); destinationRect = ClipDestinationRectangle(destinationRect, clippingRectangle.Value); } if (sourceRegion != null && !destinationRect.IsEmpty) { Draw(spriteBatch, sourceRegion, destinationRect, color); } } } #endregion -------------------------NinePatch----------------------------- #region ----------------------------Sprite----------------------------- /// /// Draws a sprite to the sprite batch. /// /// The sprite to draw. /// The sprite batch. /// The position to draw the sprite. /// The rotation of the sprite. /// The scale of the sprite. public static void Draw(this Sprite sprite, SpriteBatch spriteBatch, Vector2 position, float rotation, Vector2 scale) { Draw(spriteBatch, sprite, position, rotation, scale); } /// /// Draws a sprite to the sprite batch with a transform. /// /// The sprite batch. /// The sprite to draw. /// The transform to apply to the sprite. public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Transform2 transform) { Draw(spriteBatch, sprite, transform.Position, transform.Rotation, transform.Scale); } /// /// Draws a sprite to the sprite batch. /// /// The sprite batch. /// The sprite to draw. /// The position to draw the sprite. /// The rotation of the sprite. public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation = 0) { Draw(spriteBatch, sprite, position, rotation, Vector2.One); } /// /// Draws a sprite to the sprite batch. /// /// The sprite batch. /// The sprite to draw. /// The position to draw the sprite. /// The rotation of the sprite. /// The scale of the sprite. public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation, Vector2 scale) { if (sprite == null) throw new ArgumentNullException(nameof(sprite)); if (sprite.IsVisible) { Draw( spriteBatch, sprite.TextureRegion, position, sprite.Color * sprite.Alpha, rotation, sprite.Origin, scale, sprite.Effect, sprite.Depth ); } } #endregion -------------------------Sprite----------------------------- #region ----------------------------Texture2D----------------------------- /// /// Draws a texture to the sprite batch with optional clipping. /// /// The sprite batch. /// The texture to draw. /// The source rectangle. /// The destination rectangle. /// The color to tint the texture. /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, Texture2D texture, Rectangle sourceRectangle, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle) { if (!ClipRectangles(ref sourceRectangle, ref destinationRectangle, clippingRectangle)) return; if (destinationRectangle.Width > 0 && destinationRectangle.Height > 0) { spriteBatch.Draw(texture, destinationRectangle, sourceRectangle, color); } } #endregion -------------------------Texture2D----------------------------- #region ----------------------------TextureRegion----------------------------- /// /// Draws a texture region to the sprite batch. /// /// The sprite batch. /// The texture region to draw. /// The position to draw the texture region. /// The color to tint the texture region. /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null) { Draw(spriteBatch, textureRegion, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0, clippingRectangle); } /// /// Draws a texture region to the sprite batch with specified parameters. /// /// The sprite batch. /// The texture region to draw. /// The position to draw the texture region. /// The color to tint the texture region. /// The rotation of the texture region. /// The origin of the texture region. /// The scale of the texture region. /// The sprite effects to apply. /// The layer depth. /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth, Rectangle? clippingRectangle = null) { var sourceRectangle = textureRegion.Bounds; var offset = origin - textureRegion.Offset; Vector2 sourceScale = scale; // Handle rotated texture regions if (textureRegion.IsRotated) { var rotatedOrigin = new Vector2(origin.Y, -origin.X); var rotatedTrimOffset = new Vector2(textureRegion.Offset.Y, -textureRegion.Offset.X); var shiftByWidth = new Vector2(textureRegion.Size.Width, 0); offset = rotatedTrimOffset - rotatedOrigin + shiftByWidth; // Swap scale axes and adjust rotation for rotated regions sourceScale = new Vector2(scale.Y, scale.X); rotation -= (float)Math.PI / 2; switch (effects) { case SpriteEffects.FlipHorizontally: effects = SpriteEffects.FlipVertically; break; case SpriteEffects.FlipVertically: effects = SpriteEffects.FlipHorizontally; break; default: break; // nothing to do if flipped in both directions } } if (clippingRectangle.HasValue) { float scaledOffsetX = (origin.X - textureRegion.Offset.X) * scale.X; float scaledOffsetY = (origin.Y - textureRegion.Offset.Y) * scale.Y; var x = (int)(position.X - scaledOffsetX); var y = (int)(position.Y - scaledOffsetY); var width = (int)(textureRegion.Width * sourceScale.X); var height = (int)(textureRegion.Height * sourceScale.Y); if (textureRegion.IsRotated) { (width, height) = (height, width); } var destinationRectangle = new Rectangle(x, y, width, height); if (!ClipRectangles(ref sourceRectangle, ref destinationRectangle, clippingRectangle, textureRegion.IsRotated)) { // Clipped rectangle is empty, nothing to draw return; } if (textureRegion.IsRotated) { offset.X -= (y + height - destinationRectangle.Bottom) / sourceScale.X; offset.Y += (position.X - (destinationRectangle.X + scaledOffsetX)) / sourceScale.Y; } else { offset.X += (position.X - (destinationRectangle.X + scaledOffsetX)) / sourceScale.X; offset.Y += (position.Y - (destinationRectangle.Y + scaledOffsetY)) / sourceScale.Y; } } spriteBatch.Draw(textureRegion.Texture, position, sourceRectangle, color, rotation, offset, sourceScale, effects, layerDepth); } /// /// Draws a texture region to the sprite batch. /// /// The sprite batch. /// The texture region to draw. /// The destination rectangle. /// The color to tint the texture region. /// An optional clipping rectangle. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null) { float scaleX = (float)destinationRectangle.Width / textureRegion.OriginalSize.Width; float scaleY = (float)destinationRectangle.Height / textureRegion.OriginalSize.Height; Draw(spriteBatch, textureRegion, new Vector2(destinationRectangle.X, destinationRectangle.Y), color, 0, Vector2.Zero, new Vector2(scaleX, scaleY), SpriteEffects.None, 0, clippingRectangle); } #endregion -------------------------TextureRegion----------------------------- #region ----------------------------Utilities----------------------------- private static void CreateDestinationPatches(NinePatch ninePatch, Rectangle destinationRect) { destinationRect.Deconstruct(out int x, out int y, out int width, out int height); ninePatch.Padding.Deconstruct(out int topPadding, out int rightPadding, out int bottomPadding, out int leftPadding); int midWidth = width - leftPadding - rightPadding; int midHeight = height - topPadding - bottomPadding; int top = y + topPadding; int right = x + width - rightPadding; int bottom = y + height - bottomPadding; int left = x + leftPadding; _patchCache[NinePatch.TopLeft] = new Rectangle(x, y, leftPadding, topPadding); _patchCache[NinePatch.TopMiddle] = new Rectangle(left, y, midWidth, topPadding); _patchCache[NinePatch.TopRight] = new Rectangle(right, y, rightPadding, topPadding); _patchCache[NinePatch.MiddleLeft] = new Rectangle(x, top, leftPadding, midHeight); _patchCache[NinePatch.Middle] = new Rectangle(left, top, midWidth, midHeight); _patchCache[NinePatch.MiddleRight] = new Rectangle(right, top, rightPadding, midHeight); _patchCache[NinePatch.BottomLeft] = new Rectangle(x, bottom, leftPadding, bottomPadding); _patchCache[NinePatch.BottomMiddle] = new Rectangle(left, bottom, midWidth, bottomPadding); _patchCache[NinePatch.BottomRight] = new Rectangle(right, bottom, rightPadding, bottomPadding); } private static bool ClipRectangles(ref Rectangle sourceRectangle, ref Rectangle destinationRectangle, Rectangle? clippingRectangle, bool rotatedSource = false) { if (!clippingRectangle.HasValue) return true; var originalDestination = destinationRectangle; destinationRectangle = destinationRectangle.Clip(clippingRectangle.Value); if (destinationRectangle == Rectangle.Empty) return false; // Clipped rectangle is empty, nothing to draw int leftDiff = destinationRectangle.Left - originalDestination.Left; int topDiff = destinationRectangle.Top - originalDestination.Top; int bottomDiff = originalDestination.Bottom - destinationRectangle.Bottom; if (rotatedSource) { var scaleX = (float)sourceRectangle.Height / originalDestination.Width; var scaleY = (float)sourceRectangle.Width / originalDestination.Height; sourceRectangle.X += (int)(bottomDiff * scaleY); sourceRectangle.Y += (int)(leftDiff * scaleX); sourceRectangle.Width = (int)(destinationRectangle.Height * scaleY); sourceRectangle.Height = (int)(destinationRectangle.Width * scaleX); } else { var scaleX = (float)sourceRectangle.Width / originalDestination.Width; var scaleY = (float)sourceRectangle.Height / originalDestination.Height; sourceRectangle.X += (int)(leftDiff * scaleX); sourceRectangle.Y += (int)(topDiff * scaleY); sourceRectangle.Width = (int)(destinationRectangle.Width * scaleX); sourceRectangle.Height = (int)(destinationRectangle.Height * scaleY); } return true; } private static Texture2DRegion ClipSourceRegion(Texture2DRegion sourceRegion, Rectangle destinationRectangle, Rectangle clippingRectangle) { var left = (float)(clippingRectangle.Left - destinationRectangle.Left); var right = (float)(destinationRectangle.Right - clippingRectangle.Right); var top = (float)(clippingRectangle.Top - destinationRectangle.Top); var bottom = (float)(destinationRectangle.Bottom - clippingRectangle.Bottom); var x = left > 0 ? left : 0; var y = top > 0 ? top : 0; var w = (right > 0 ? right : 0) + x; var h = (bottom > 0 ? bottom : 0) + y; var scaleX = (float)destinationRectangle.Width / sourceRegion.OriginalSize.Width; var scaleY = (float)destinationRectangle.Height / sourceRegion.OriginalSize.Height; x /= scaleX; y /= scaleY; w /= scaleX; h /= scaleY; return sourceRegion.GetSubregion((int)x, (int)y, (int)(sourceRegion.OriginalSize.Width - w), (int)(sourceRegion.OriginalSize.Height - h)); } private static Rectangle ClipDestinationRectangle(Rectangle destinationRectangle, Rectangle clippingRectangle) { return destinationRectangle.Clip(clippingRectangle); } #endregion -------------------------Utilities----------------------------- }