SpriteBatch.Extensions.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. // Copyright (c) Craftwork Games. All rights reserved.
  2. // Licensed under the MIT license.
  3. // See LICENSE file in the project root for full license information.
  4. using System;
  5. using Microsoft.Xna.Framework;
  6. using Microsoft.Xna.Framework.Graphics;
  7. namespace MonoGame.Extended.Graphics;
  8. /// <summary>
  9. /// Provides extension methods for the <see cref="SpriteBatch"/> class.
  10. /// </summary>
  11. public static class SpriteBatchExtensions
  12. {
  13. #region ----------------------------NinePatch-----------------------------
  14. private static readonly Rectangle[] _patchCache = new Rectangle[9];
  15. private static Rectangle _rect = default;
  16. /// <summary>
  17. /// Draws a nine-patch region to the sprite batch.
  18. /// </summary>
  19. /// <param name="spriteBatch">The sprite batch.</param>
  20. /// <param name="ninePatchRegion">The nine-patch region.</param>
  21. /// <param name="destinationRectangle">The destination rectangle.</param>
  22. /// <param name="color">The color to tint the nine-patch region.</param>
  23. /// <param name="clippingRectangle">An optional clipping rectangle.</param>
  24. public static void Draw(this SpriteBatch spriteBatch, NinePatch ninePatchRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null)
  25. {
  26. CreateDestinationPatches(ninePatchRegion, destinationRectangle);
  27. ReadOnlySpan<Texture2DRegion> sourcePatches = ninePatchRegion.Patches;
  28. for (int i = 0; i < sourcePatches.Length; i++)
  29. {
  30. Texture2DRegion sourceRegion = sourcePatches[i];
  31. Rectangle destinationRect = _patchCache[i];
  32. if (clippingRectangle.HasValue)
  33. {
  34. sourceRegion = ClipSourceRegion(sourceRegion, destinationRect, clippingRectangle.Value);
  35. destinationRect = ClipDestinationRectangle(destinationRect, clippingRectangle.Value);
  36. }
  37. if (sourceRegion != null && !destinationRect.IsEmpty)
  38. {
  39. Draw(spriteBatch, sourceRegion, destinationRect, color);
  40. }
  41. }
  42. }
  43. #endregion -------------------------NinePatch-----------------------------
  44. #region ----------------------------Sprite-----------------------------
  45. /// <summary>
  46. /// Draws a sprite to the sprite batch.
  47. /// </summary>
  48. /// <param name="sprite">The sprite to draw.</param>
  49. /// <param name="spriteBatch">The sprite batch.</param>
  50. /// <param name="position">The position to draw the sprite.</param>
  51. /// <param name="rotation">The rotation of the sprite.</param>
  52. /// <param name="scale">The scale of the sprite.</param>
  53. public static void Draw(this Sprite sprite, SpriteBatch spriteBatch, Vector2 position, float rotation, Vector2 scale)
  54. {
  55. Draw(spriteBatch, sprite, position, rotation, scale);
  56. }
  57. /// <summary>
  58. /// Draws a sprite to the sprite batch with a transform.
  59. /// </summary>
  60. /// <param name="spriteBatch">The sprite batch.</param>
  61. /// <param name="sprite">The sprite to draw.</param>
  62. /// <param name="transform">The transform to apply to the sprite.</param>
  63. public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Transform2 transform)
  64. {
  65. Draw(spriteBatch, sprite, transform.Position, transform.Rotation, transform.Scale);
  66. }
  67. /// <summary>
  68. /// Draws a sprite to the sprite batch.
  69. /// </summary>
  70. /// <param name="spriteBatch">The sprite batch.</param>
  71. /// <param name="sprite">The sprite to draw.</param>
  72. /// <param name="position">The position to draw the sprite.</param>
  73. /// <param name="rotation">The rotation of the sprite.</param>
  74. public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation = 0)
  75. {
  76. Draw(spriteBatch, sprite, position, rotation, Vector2.One);
  77. }
  78. /// <summary>
  79. /// Draws a sprite to the sprite batch.
  80. /// </summary>
  81. /// <param name="spriteBatch">The sprite batch.</param>
  82. /// <param name="sprite">The sprite to draw.</param>
  83. /// <param name="position">The position to draw the sprite.</param>
  84. /// <param name="rotation">The rotation of the sprite.</param>
  85. /// <param name="scale">The scale of the sprite.</param>
  86. public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation, Vector2 scale)
  87. {
  88. if (sprite == null) throw new ArgumentNullException(nameof(sprite));
  89. if (sprite.IsVisible)
  90. {
  91. Draw(
  92. spriteBatch,
  93. sprite.TextureRegion,
  94. position,
  95. sprite.Color * sprite.Alpha,
  96. rotation,
  97. sprite.Origin,
  98. scale,
  99. sprite.Effect,
  100. sprite.Depth
  101. );
  102. }
  103. }
  104. #endregion -------------------------Sprite-----------------------------
  105. #region ----------------------------Texture2D-----------------------------
  106. /// <summary>
  107. /// Draws a texture to the sprite batch with optional clipping.
  108. /// </summary>
  109. /// <param name="spriteBatch">The sprite batch.</param>
  110. /// <param name="texture">The texture to draw.</param>
  111. /// <param name="sourceRectangle">The source rectangle.</param>
  112. /// <param name="destinationRectangle">The destination rectangle.</param>
  113. /// <param name="color">The color to tint the texture.</param>
  114. /// <param name="clippingRectangle">An optional clipping rectangle.</param>
  115. public static void Draw(this SpriteBatch spriteBatch, Texture2D texture, Rectangle sourceRectangle, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle)
  116. {
  117. if (!ClipRectangles(ref sourceRectangle, ref destinationRectangle, clippingRectangle))
  118. return;
  119. if (destinationRectangle.Width > 0 && destinationRectangle.Height > 0)
  120. {
  121. spriteBatch.Draw(texture, destinationRectangle, sourceRectangle, color);
  122. }
  123. }
  124. #endregion -------------------------Texture2D-----------------------------
  125. #region ----------------------------TextureRegion-----------------------------
  126. /// <summary>
  127. /// Draws a texture region to the sprite batch.
  128. /// </summary>
  129. /// <param name="spriteBatch">The sprite batch.</param>
  130. /// <param name="textureRegion">The texture region to draw.</param>
  131. /// <param name="position">The position to draw the texture region.</param>
  132. /// <param name="color">The color to tint the texture region.</param>
  133. /// <param name="clippingRectangle">An optional clipping rectangle.</param>
  134. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null)
  135. {
  136. Draw(spriteBatch, textureRegion, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0, clippingRectangle);
  137. }
  138. /// <summary>
  139. /// Draws a texture region to the sprite batch with specified parameters.
  140. /// </summary>
  141. /// <param name="spriteBatch">The sprite batch.</param>
  142. /// <param name="textureRegion">The texture region to draw.</param>
  143. /// <param name="position">The position to draw the texture region.</param>
  144. /// <param name="color">The color to tint the texture region.</param>
  145. /// <param name="rotation">The rotation of the texture region.</param>
  146. /// <param name="origin">The origin of the texture region.</param>
  147. /// <param name="scale">The scale of the texture region.</param>
  148. /// <param name="effects">The sprite effects to apply.</param>
  149. /// <param name="layerDepth">The layer depth.</param>
  150. /// <param name="clippingRectangle">An optional clipping rectangle.</param>
  151. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Vector2 position, Color color,
  152. float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth, Rectangle? clippingRectangle = null)
  153. {
  154. var sourceRectangle = textureRegion.Bounds;
  155. var offset = origin - textureRegion.Offset;
  156. Vector2 sourceScale = scale;
  157. // Handle rotated texture regions
  158. if (textureRegion.IsRotated)
  159. {
  160. var rotatedOrigin = new Vector2(origin.Y, -origin.X);
  161. var rotatedTrimOffset = new Vector2(textureRegion.Offset.Y, -textureRegion.Offset.X);
  162. var shiftByWidth = new Vector2(textureRegion.Size.Width, 0);
  163. offset = rotatedTrimOffset - rotatedOrigin + shiftByWidth;
  164. // Swap scale axes and adjust rotation for rotated regions
  165. sourceScale = new Vector2(scale.Y, scale.X);
  166. rotation -= (float)Math.PI / 2;
  167. switch (effects)
  168. {
  169. case SpriteEffects.FlipHorizontally: effects = SpriteEffects.FlipVertically; break;
  170. case SpriteEffects.FlipVertically: effects = SpriteEffects.FlipHorizontally; break;
  171. default: break; // nothing to do if flipped in both directions
  172. }
  173. }
  174. if (clippingRectangle.HasValue)
  175. {
  176. float scaledOffsetX = (origin.X - textureRegion.Offset.X) * scale.X;
  177. float scaledOffsetY = (origin.Y - textureRegion.Offset.Y) * scale.Y;
  178. var x = (int)(position.X - scaledOffsetX);
  179. var y = (int)(position.Y - scaledOffsetY);
  180. var width = (int)(textureRegion.Width * sourceScale.X);
  181. var height = (int)(textureRegion.Height * sourceScale.Y);
  182. if (textureRegion.IsRotated)
  183. {
  184. (width, height) = (height, width);
  185. }
  186. var destinationRectangle = new Rectangle(x, y, width, height);
  187. if (!ClipRectangles(ref sourceRectangle, ref destinationRectangle, clippingRectangle, textureRegion.IsRotated))
  188. {
  189. // Clipped rectangle is empty, nothing to draw
  190. return;
  191. }
  192. if (textureRegion.IsRotated)
  193. {
  194. offset.X -= (y + height - destinationRectangle.Bottom) / sourceScale.X;
  195. offset.Y += (position.X - (destinationRectangle.X + scaledOffsetX)) / sourceScale.Y;
  196. }
  197. else
  198. {
  199. offset.X += (position.X - (destinationRectangle.X + scaledOffsetX)) / sourceScale.X;
  200. offset.Y += (position.Y - (destinationRectangle.Y + scaledOffsetY)) / sourceScale.Y;
  201. }
  202. }
  203. spriteBatch.Draw(textureRegion.Texture, position, sourceRectangle, color, rotation, offset, sourceScale, effects, layerDepth);
  204. }
  205. /// <summary>
  206. /// Draws a texture region to the sprite batch.
  207. /// </summary>
  208. /// <param name="spriteBatch">The sprite batch.</param>
  209. /// <param name="textureRegion">The texture region to draw.</param>
  210. /// <param name="destinationRectangle">The destination rectangle.</param>
  211. /// <param name="color">The color to tint the texture region.</param>
  212. /// <param name="clippingRectangle">An optional clipping rectangle.</param>
  213. public static void Draw(this SpriteBatch spriteBatch, Texture2DRegion textureRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null)
  214. {
  215. float scaleX = (float)destinationRectangle.Width / textureRegion.OriginalSize.Width;
  216. float scaleY = (float)destinationRectangle.Height / textureRegion.OriginalSize.Height;
  217. Draw(spriteBatch, textureRegion, new Vector2(destinationRectangle.X, destinationRectangle.Y), color, 0, Vector2.Zero, new Vector2(scaleX, scaleY), SpriteEffects.None, 0, clippingRectangle);
  218. }
  219. #endregion -------------------------TextureRegion-----------------------------
  220. #region ----------------------------Utilities-----------------------------
  221. private static void CreateDestinationPatches(NinePatch ninePatch, Rectangle destinationRect)
  222. {
  223. destinationRect.Deconstruct(out int x, out int y, out int width, out int height);
  224. ninePatch.Padding.Deconstruct(out int topPadding, out int rightPadding, out int bottomPadding, out int leftPadding);
  225. int midWidth = width - leftPadding - rightPadding;
  226. int midHeight = height - topPadding - bottomPadding;
  227. int top = y + topPadding;
  228. int right = x + width - rightPadding;
  229. int bottom = y + height - bottomPadding;
  230. int left = x + leftPadding;
  231. _patchCache[NinePatch.TopLeft] = new Rectangle(x, y, leftPadding, topPadding);
  232. _patchCache[NinePatch.TopMiddle] = new Rectangle(left, y, midWidth, topPadding);
  233. _patchCache[NinePatch.TopRight] = new Rectangle(right, y, rightPadding, topPadding);
  234. _patchCache[NinePatch.MiddleLeft] = new Rectangle(x, top, leftPadding, midHeight);
  235. _patchCache[NinePatch.Middle] = new Rectangle(left, top, midWidth, midHeight);
  236. _patchCache[NinePatch.MiddleRight] = new Rectangle(right, top, rightPadding, midHeight);
  237. _patchCache[NinePatch.BottomLeft] = new Rectangle(x, bottom, leftPadding, bottomPadding);
  238. _patchCache[NinePatch.BottomMiddle] = new Rectangle(left, bottom, midWidth, bottomPadding);
  239. _patchCache[NinePatch.BottomRight] = new Rectangle(right, bottom, rightPadding, bottomPadding);
  240. }
  241. private static bool ClipRectangles(ref Rectangle sourceRectangle, ref Rectangle destinationRectangle, Rectangle? clippingRectangle, bool rotatedSource = false)
  242. {
  243. if (!clippingRectangle.HasValue)
  244. return true;
  245. var originalDestination = destinationRectangle;
  246. destinationRectangle = destinationRectangle.Clip(clippingRectangle.Value);
  247. if (destinationRectangle == Rectangle.Empty)
  248. return false; // Clipped rectangle is empty, nothing to draw
  249. int leftDiff = destinationRectangle.Left - originalDestination.Left;
  250. int topDiff = destinationRectangle.Top - originalDestination.Top;
  251. int bottomDiff = originalDestination.Bottom - destinationRectangle.Bottom;
  252. if (rotatedSource)
  253. {
  254. var scaleX = (float)sourceRectangle.Height / originalDestination.Width;
  255. var scaleY = (float)sourceRectangle.Width / originalDestination.Height;
  256. sourceRectangle.X += (int)(bottomDiff * scaleY);
  257. sourceRectangle.Y += (int)(leftDiff * scaleX);
  258. sourceRectangle.Width = (int)(destinationRectangle.Height * scaleY);
  259. sourceRectangle.Height = (int)(destinationRectangle.Width * scaleX);
  260. }
  261. else
  262. {
  263. var scaleX = (float)sourceRectangle.Width / originalDestination.Width;
  264. var scaleY = (float)sourceRectangle.Height / originalDestination.Height;
  265. sourceRectangle.X += (int)(leftDiff * scaleX);
  266. sourceRectangle.Y += (int)(topDiff * scaleY);
  267. sourceRectangle.Width = (int)(destinationRectangle.Width * scaleX);
  268. sourceRectangle.Height = (int)(destinationRectangle.Height * scaleY);
  269. }
  270. return true;
  271. }
  272. private static Texture2DRegion ClipSourceRegion(Texture2DRegion sourceRegion, Rectangle destinationRectangle, Rectangle clippingRectangle)
  273. {
  274. var left = (float)(clippingRectangle.Left - destinationRectangle.Left);
  275. var right = (float)(destinationRectangle.Right - clippingRectangle.Right);
  276. var top = (float)(clippingRectangle.Top - destinationRectangle.Top);
  277. var bottom = (float)(destinationRectangle.Bottom - clippingRectangle.Bottom);
  278. var x = left > 0 ? left : 0;
  279. var y = top > 0 ? top : 0;
  280. var w = (right > 0 ? right : 0) + x;
  281. var h = (bottom > 0 ? bottom : 0) + y;
  282. var scaleX = (float)destinationRectangle.Width / sourceRegion.OriginalSize.Width;
  283. var scaleY = (float)destinationRectangle.Height / sourceRegion.OriginalSize.Height;
  284. x /= scaleX;
  285. y /= scaleY;
  286. w /= scaleX;
  287. h /= scaleY;
  288. return sourceRegion.GetSubregion((int)x, (int)y, (int)(sourceRegion.OriginalSize.Width - w), (int)(sourceRegion.OriginalSize.Height - h));
  289. }
  290. private static Rectangle ClipDestinationRectangle(Rectangle destinationRectangle, Rectangle clippingRectangle)
  291. {
  292. return destinationRectangle.Clip(clippingRectangle);
  293. }
  294. #endregion -------------------------Utilities-----------------------------
  295. }