Layer.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Avalonia;
  5. using Avalonia.Media;
  6. using Avalonia.Media.Imaging;
  7. using AvaloniaWriteableBitmapEx;
  8. using PixiEditor.Models.DataHolders;
  9. using PixiEditor.Models.Position;
  10. using ReactiveUI;
  11. namespace PixiEditor.Models.Layers
  12. {
  13. public class Layer : BasicLayer
  14. {
  15. private const int SizeOfArgb = 4;
  16. public string Name
  17. {
  18. get => _name;
  19. set
  20. {
  21. _name = value;
  22. this.RaisePropertyChanged("Name");
  23. }
  24. }
  25. public bool IsActive
  26. {
  27. get => _isActive;
  28. set
  29. {
  30. _isActive = value;
  31. this.RaisePropertyChanged("IsActive");
  32. }
  33. }
  34. public bool IsVisible
  35. {
  36. get => _isVisible;
  37. set
  38. {
  39. _isVisible = value;
  40. this.RaisePropertyChanged("IsVisible");
  41. }
  42. }
  43. public bool IsRenaming
  44. {
  45. get => _isRenaming;
  46. set
  47. {
  48. _isRenaming = value;
  49. this.RaisePropertyChanged("IsRenaming");
  50. }
  51. }
  52. public WriteableBitmap LayerBitmap
  53. {
  54. get => _layerBitmap;
  55. set
  56. {
  57. _layerBitmap = value;
  58. this.RaisePropertyChanged("LayerBitmap");
  59. }
  60. }
  61. private float _opacity = 1;
  62. public float Opacity
  63. {
  64. get => _opacity;
  65. set
  66. {
  67. _opacity = value;
  68. this.RaisePropertyChanged("Opacity");
  69. }
  70. }
  71. public int OffsetX => (int) Offset.Left;
  72. public int OffsetY => (int) Offset.Top;
  73. private Thickness _offset;
  74. public Thickness Offset
  75. {
  76. get => _offset;
  77. set
  78. {
  79. _offset = value;
  80. this.RaisePropertyChanged("Offset");
  81. }
  82. }
  83. private bool _isActive;
  84. private bool _isRenaming;
  85. private bool _isVisible = true;
  86. private WriteableBitmap _layerBitmap;
  87. private string _name;
  88. private bool _clipRequested;
  89. public int MaxWidth { get; set; } = int.MaxValue;
  90. public int MaxHeight { get; set; } = int.MaxValue;
  91. public Dictionary<Coordinates,Color> LastRelativeCoordinates;
  92. public Layer(string name)
  93. {
  94. Name = name;
  95. LayerBitmap = new WriteableBitmap(new PixelSize(0, 0), new Vector(72, 72));
  96. Width = 0;
  97. Height = 0;
  98. }
  99. public Layer(string name, int width, int height)
  100. {
  101. Name = name;
  102. LayerBitmap = new WriteableBitmap(new PixelSize(width, height), new Vector(72,72));
  103. Width = width;
  104. Height = height;
  105. }
  106. public Layer(string name, WriteableBitmap layerBitmap)
  107. {
  108. Name = name;
  109. LayerBitmap = layerBitmap;
  110. Width = _layerBitmap.PixelSize.Width;
  111. Height = _layerBitmap.PixelSize.Height;
  112. }
  113. /// <summary>
  114. /// Returns clone of layer
  115. /// </summary>
  116. /// <returns></returns>
  117. public Layer Clone()
  118. {
  119. return new Layer(Name, LayerBitmap.Clone())
  120. {
  121. IsVisible = this.IsVisible,
  122. Offset = this.Offset,
  123. MaxHeight = this.MaxHeight,
  124. MaxWidth = this.MaxWidth,
  125. Opacity = this.Opacity,
  126. IsActive = this.IsActive,
  127. IsRenaming = this.IsRenaming
  128. };
  129. }
  130. /// <summary>
  131. /// Resizes bitmap with it's content using NearestNeighbor interpolation
  132. /// </summary>
  133. /// <param name="width">New width</param>
  134. /// <param name="height">New height</param>
  135. /// <param name="newMaxWidth">New layer maximum width, this should be document width</param>
  136. /// <param name="newMaxHeight">New layer maximum height, this should be document height</param>
  137. public void Resize(int width, int height, int newMaxWidth, int newMaxHeight)
  138. {
  139. LayerBitmap = LayerBitmap.Resize(width, height, WriteableBitmapEx.Interpolation.NearestNeighbor);
  140. Width = width;
  141. Height = height;
  142. MaxWidth = newMaxWidth;
  143. MaxHeight = newMaxHeight;
  144. }
  145. /// <summary>
  146. /// Converts coordinates relative to viewport to relative to layer
  147. /// </summary>
  148. /// <param name="cords"></param>
  149. /// <returns></returns>
  150. public Coordinates GetRelativePosition(Coordinates cords)
  151. {
  152. return new Coordinates(cords.X - OffsetX, cords.Y - OffsetY);
  153. }
  154. /// <summary>
  155. /// Returns pixel color of x and y coordinates relative to document using (x - OffsetX) formula.
  156. /// </summary>
  157. /// <param name="x">Viewport relative X</param>
  158. /// <param name="y">Viewport relative Y</param>
  159. /// <returns>Color of a pixel</returns>
  160. public Color GetPixelWithOffset(int x, int y)
  161. {
  162. Coordinates cords = GetRelativePosition(new Coordinates(x, y));
  163. return GetPixel(cords.X, cords.Y);
  164. }
  165. /// <summary>
  166. /// Returns pixel color on x and y.
  167. /// </summary>
  168. /// <param name="x">X coordinate</param>
  169. /// <param name="y">Y Coordinate</param>
  170. /// <returns>Color of pixel, if out of bounds, returns transparent pixel.</returns>
  171. public Color GetPixel(int x, int y)
  172. {
  173. if (x > Width - 1 || x < 0 || y > Height - 1 || y < 0)
  174. {
  175. return Color.FromArgb(0, 0, 0, 0);
  176. }
  177. return LayerBitmap.GetPixel(x, y);
  178. }
  179. /// <summary>
  180. /// Applies pixel to layer
  181. /// </summary>
  182. /// <param name="coordinates">Position of pixel</param>
  183. /// <param name="color">Color of pixel</param>
  184. /// <param name="dynamicResize">Resizes bitmap to fit content</param>
  185. /// <param name="applyOffset">Converts pixels coordinates to relative to bitmap</param>
  186. public void SetPixel(Coordinates coordinates, Color color, bool dynamicResize = true, bool applyOffset = true)
  187. {
  188. SetPixels(BitmapPixelChanges.FromSingleColoredArray(new []{ coordinates }, color), dynamicResize, applyOffset);
  189. }
  190. /// <summary>
  191. /// Applies pixels to layer
  192. /// </summary>
  193. /// <param name="pixels">Pixels to apply</param>
  194. /// <param name="dynamicResize">Resizes bitmap to fit content</param>
  195. /// <param name="applyOffset">Converts pixels coordinates to relative to bitmap</param>
  196. public void SetPixels(BitmapPixelChanges pixels, bool dynamicResize = true, bool applyOffset = true)
  197. {
  198. if (pixels.ChangedPixels == null || pixels.ChangedPixels.Count == 0) return;
  199. if(dynamicResize)
  200. DynamicResize(pixels);
  201. if(applyOffset)
  202. pixels.ChangedPixels = GetRelativePosition(pixels.ChangedPixels);
  203. LastRelativeCoordinates = pixels.ChangedPixels;
  204. using (var ctx = LayerBitmap.GetBitmapContext())
  205. {
  206. foreach (var coords in pixels.ChangedPixels)
  207. {
  208. if (OutOfBounds(coords.Key)) continue;
  209. ctx.WriteableBitmap.SetPixel(coords.Key.X, coords.Key.Y, coords.Value);
  210. }
  211. }
  212. ClipIfNecessary();
  213. }
  214. private Dictionary<Coordinates, Color> GetRelativePosition(Dictionary<Coordinates, Color> changedPixels)
  215. {
  216. return changedPixels.ToDictionary(d => new Coordinates(d.Key.X - OffsetX, d.Key.Y - OffsetY),
  217. d => d.Value);
  218. }
  219. /// <summary>
  220. /// Converts absolute coordinates array to relative to this layer coordinates array.
  221. /// </summary>
  222. /// <param name="nonRelativeCords">absolute coordinates array</param>
  223. /// <returns></returns>
  224. public Coordinates[] ConvertToRelativeCoordinates(Coordinates[] nonRelativeCords)
  225. {
  226. Coordinates[] result = new Coordinates[nonRelativeCords.Length];
  227. for (int i = 0; i < nonRelativeCords.Length; i++)
  228. {
  229. result[i] = new Coordinates(nonRelativeCords[i].X - OffsetX, nonRelativeCords[i].Y - OffsetY);
  230. }
  231. return result;
  232. }
  233. /// <summary>
  234. /// Resizes canvas to fit pixels outside current bounds. Clamped to MaxHeight and MaxWidth
  235. /// </summary>
  236. /// <param name="pixels"></param>
  237. public void DynamicResize(BitmapPixelChanges pixels)
  238. {
  239. if (pixels.ChangedPixels.Count == 0) return;
  240. ResetOffset(pixels);
  241. var borderData = ExtractBorderData(pixels);
  242. DoubleCords minMaxCords = borderData.Item1;
  243. int newMaxX = minMaxCords.Coords2.X - OffsetX;
  244. int newMaxY = minMaxCords.Coords2.Y - OffsetY;
  245. int newMinX = minMaxCords.Coords1.X - OffsetX;
  246. int newMinY = minMaxCords.Coords1.Y - OffsetY;
  247. if (!(pixels.WasBuiltAsSingleColored && pixels.ChangedPixels.First().Value.A == 0))
  248. {
  249. if (newMaxX + 1 > Width || newMaxY + 1 > Height)
  250. {
  251. IncreaseSizeToBottom(newMaxX, newMaxY);
  252. }
  253. if (newMinX < 0 || newMinY < 0)
  254. {
  255. IncreaseSizeToTop(newMinX, newMinY);
  256. }
  257. }
  258. if(borderData.Item2) //if clip is requested
  259. {
  260. _clipRequested = true;
  261. }
  262. }
  263. private Tuple<DoubleCords, bool> ExtractBorderData(BitmapPixelChanges pixels)
  264. {
  265. Coordinates firstCords = pixels.ChangedPixels.First().Key;
  266. int minX = firstCords.X;
  267. int minY = firstCords.Y;
  268. int maxX = minX;
  269. int maxY = minY;
  270. bool clipRequested = false;
  271. foreach (var pixel in pixels.ChangedPixels)
  272. {
  273. if (pixel.Key.X < minX) minX = pixel.Key.X;
  274. else if (pixel.Key.X > maxX) maxX = pixel.Key.X;
  275. if (pixel.Key.Y < minY) minY = pixel.Key.Y;
  276. else if (pixel.Key.Y > maxY) maxY = pixel.Key.Y;
  277. if (clipRequested == false && IsBorderPixel(pixel.Key) && pixel.Value.A == 0)
  278. clipRequested = true;
  279. }
  280. return new Tuple<DoubleCords, bool>(
  281. new DoubleCords(new Coordinates(minX, minY), new Coordinates(maxX, maxY)), clipRequested);
  282. }
  283. private bool IsBorderPixel(Coordinates cords)
  284. {
  285. return cords.X - OffsetX == 0 || cords.Y - OffsetY == 0 || cords.X - OffsetX == Width - 1 ||
  286. cords.Y - OffsetY == Height - 1;
  287. }
  288. private bool OutOfBounds(Coordinates cords)
  289. {
  290. return cords.X < 0 || cords.X > Width - 1 || cords.Y < 0 || cords.Y > Height - 1;
  291. }
  292. private void ClipIfNecessary()
  293. {
  294. if (_clipRequested)
  295. {
  296. ClipCanvas();
  297. _clipRequested = false;
  298. }
  299. }
  300. /// <summary>
  301. /// Changes size of bitmap to fit content
  302. /// </summary>
  303. public void ClipCanvas()
  304. {
  305. var points = GetEdgePoints();
  306. int smallestX = points.Coords1.X;
  307. int smallestY = points.Coords1.Y;
  308. int biggestX = points.Coords2.X;
  309. int biggestY = points.Coords2.Y;
  310. if (smallestX < 0 && smallestY < 0 && biggestX < 0 && biggestY < 0)
  311. return;
  312. int width = biggestX - smallestX + 1;
  313. int height = biggestY - smallestY + 1;
  314. ResizeCanvas(0,0, smallestX, smallestY, width, height);
  315. Offset = new Thickness(OffsetX + smallestX, OffsetY + smallestY, 0, 0);
  316. }
  317. private void IncreaseSizeToBottom(int newMaxX, int newMaxY)
  318. {
  319. if (MaxWidth - OffsetX < 0 || MaxHeight - OffsetY < 0) return;
  320. newMaxX = Math.Clamp(Math.Max(newMaxX + 1, Width), 0, MaxWidth - OffsetX);
  321. newMaxY = Math.Clamp(Math.Max(newMaxY + 1, Height), 0, MaxHeight - OffsetY);
  322. ResizeCanvas(0, 0, 0, 0, newMaxX, newMaxY);
  323. }
  324. private void IncreaseSizeToTop(int newMinX, int newMinY)
  325. {
  326. newMinX = Math.Clamp(Math.Min(newMinX, Width), -OffsetX, 0);
  327. newMinY = Math.Clamp(Math.Min(newMinY, Height), -OffsetY, 0);
  328. Offset = new Thickness(Math.Clamp(OffsetX + newMinX, 0, MaxWidth),
  329. Math.Clamp(OffsetY + newMinY, 0, MaxHeight), 0, 0);
  330. int newWidth = Math.Clamp(Width - newMinX, 0, MaxWidth);
  331. int newHeight = Math.Clamp(Height - newMinY, 0, MaxHeight);
  332. int offsetX = Math.Abs(newWidth - Width);
  333. int offsetY = Math.Abs(newHeight - Height);
  334. ResizeCanvas(offsetX, offsetY, 0, 0, newWidth, newHeight);
  335. }
  336. private DoubleCords GetEdgePoints()
  337. {
  338. Coordinates smallestPixel = CoordinatesCalculator.FindMinEdgeNonTransparentPixel(LayerBitmap);
  339. Coordinates biggestPixel = CoordinatesCalculator.FindMostEdgeNonTransparentPixel(LayerBitmap);
  340. return new DoubleCords(smallestPixel, biggestPixel);
  341. }
  342. private void ResetOffset(BitmapPixelChanges pixels)
  343. {
  344. if (Width == 0 || Height == 0)
  345. {
  346. int offsetX = pixels.ChangedPixels.Min(x => x.Key.X);
  347. int offsetY = pixels.ChangedPixels.Min(x => x.Key.Y);
  348. Offset = new Thickness(offsetX, offsetY, 0,0);
  349. }
  350. }
  351. /// <summary>
  352. /// Clears bitmap
  353. /// </summary>
  354. public void Clear()
  355. {
  356. LayerBitmap.Clear();
  357. }
  358. /// <summary>
  359. /// Converts layer WriteableBitmap to byte array
  360. /// </summary>
  361. /// <returns></returns>
  362. public byte[] ConvertBitmapToBytes()
  363. {
  364. using (LayerBitmap.GetBitmapContext())
  365. {
  366. byte[] byteArray = LayerBitmap.ToByteArray(0, LayerBitmap.PixelSize.Height * LayerBitmap.PixelSize.Width * 4);
  367. return byteArray;
  368. }
  369. }
  370. /// <summary>
  371. /// Resizes canvas to new size with specified offset.
  372. /// </summary>
  373. /// <param name="offsetX"></param>
  374. /// <param name="offsetY"></param>
  375. /// <param name="offsetXSrc"></param>
  376. /// <param name="offsetYSrc"></param>
  377. /// <param name="newWidth"></param>
  378. /// <param name="newHeight"></param>
  379. private void ResizeCanvas(int offsetX, int offsetY, int offsetXSrc, int offsetYSrc, int newWidth, int newHeight)
  380. {
  381. int iteratorHeight = Height > newHeight ? newHeight : Height;
  382. int count = Width > newWidth ? newWidth : Width;
  383. using (var srcContext = LayerBitmap.GetBitmapContext(ReadWriteMode.ReadOnly))
  384. {
  385. var result = BitmapFactory.New(newWidth, newHeight);
  386. using (var destContext = result.GetBitmapContext())
  387. {
  388. for (int line = 0; line < iteratorHeight; line++)
  389. {
  390. var srcOff = ((offsetYSrc + line) * Width + offsetXSrc) * SizeOfArgb;
  391. var dstOff = ((offsetY + line) * newWidth + offsetX) * SizeOfArgb;
  392. BitmapContext.BlockCopy(srcContext, srcOff, destContext, dstOff, count * SizeOfArgb);
  393. }
  394. LayerBitmap = result;
  395. Width = newWidth;
  396. Height = newHeight;
  397. }
  398. }
  399. }
  400. }
  401. }