windowcapture
исходный код / Effects/EffectLayer.cs

EffectLayer.cs

242 строк · 8,530 байт · модуль Effects
  1using System;
  2using System.Drawing;
  3using System.Drawing.Drawing2D;
  4using System.Drawing.Imaging;
  5using WindowCapture.Helpers;
  6using WindowCapture.Models;
  7
  8namespace WindowCapture.Effects
  9{
 10    public enum EffectType
 11    {
 12        None,
 13        BlurInside,
 14        BlurOutside,
 15        MotionBlurInside,
 16        MotionBlurOutside
 17    }
 18
 19    /// <summary>
 20    /// Non-destructive effect layer that follows highlight movement.
 21    /// Caches rendered result for performance.
 22    /// </summary>
 23    public class EffectLayer : IDisposable
 24    {
 25        public int HighlightId;
 26        public EffectType Type = EffectType.None;
 27        public Rectangle TargetRect;
 28        public int Intensity = 10;
 29        public int FeatherWidth = 0;
 30
 31        private Bitmap cachedResult;
 32        private Rectangle cachedRect;
 33        private EffectType cachedType;
 34        private int cachedIntensity;
 35        private int cachedFeather;
 36        private bool disposed;
 37
 38        public EffectLayer(int highlightId)
 39        {
 40            HighlightId = highlightId;
 41        }
 42
 43        public bool NeedsRecalculation
 44        {
 45            get
 46            {
 47                return cachedResult == null ||
 48                       cachedRect != TargetRect ||
 49                       cachedType != Type ||
 50                       cachedIntensity != Intensity ||
 51                       cachedFeather != FeatherWidth;
 52            }
 53        }
 54
 55        public void Invalidate()
 56        {
 57            if (cachedResult != null)
 58            {
 59                cachedResult.Dispose();
 60                cachedResult = null;
 61            }
 62        }
 63
 64        public Bitmap GetCachedResult(Bitmap original)
 65        {
 66            if (original == null) return null;
 67            if (NeedsRecalculation)
 68            {
 69                Recalculate(original);
 70            }
 71            return cachedResult;
 72        }
 73
 74        private void Recalculate(Bitmap original)
 75        {
 76            if (cachedResult != null)
 77            {
 78                cachedResult.Dispose();
 79                cachedResult = null;
 80            }
 81
 82            if (Type == EffectType.None || original == null)
 83            {
 84                return;
 85            }
 86
 87            try
 88            {
 89                // Clone original bitmap for non-destructive editing
 90                cachedResult = BitmapHelper.Clone32(original);
 91
 92                // Apply effect based on type
 93                switch (Type)
 94                {
 95                    case EffectType.BlurInside:
 96                        ImageEffects.ApplyBlur(cachedResult, TargetRect, Intensity);
 97                        break;
 98
 99                    case EffectType.BlurOutside:
100                        // 1. Blur the entire image first
101                        ImageEffects.ApplyBlur(cachedResult, new Rectangle(0, 0, cachedResult.Width, cachedResult.Height), Intensity);
102                        // 2. Apply dim to the blurred result
103                        using (var g = Graphics.FromImage(cachedResult))
104                        {
105                            using (var dimBrush = new SolidBrush(Color.FromArgb(Settings.DimAlpha, 0, 0, 0)))
106                            {
107                                g.FillRectangle(dimBrush, 0, 0, cachedResult.Width, cachedResult.Height);
108                            }
109                        }
110                        // 3. Restore the clear area from original using direct pixel copy
111                        CopyPixels(original, cachedResult, TargetRect);
112                        break;
113
114                    case EffectType.MotionBlurInside:
115                        ImageEffects.ApplyMotionBlur(cachedResult, TargetRect, Intensity);
116                        break;
117
118                    case EffectType.MotionBlurOutside:
119                        // 1. Apply motion blur to the entire image first
120                        ImageEffects.ApplyMotionBlur(cachedResult, new Rectangle(0, 0, cachedResult.Width, cachedResult.Height), Intensity);
121                        // 2. Apply dim to the blurred result
122                        using (var g = Graphics.FromImage(cachedResult))
123                        {
124                            using (var dimBrush = new SolidBrush(Color.FromArgb(Settings.DimAlpha, 0, 0, 0)))
125                            {
126                                g.FillRectangle(dimBrush, 0, 0, cachedResult.Width, cachedResult.Height);
127                            }
128                        }
129                        // 3. Restore the clear area from original using direct pixel copy
130                        CopyPixels(original, cachedResult, TargetRect);
131                        break;
132                }
133
134                // Feather effect removed - was causing visible border artifacts
135
136                // Store cached parameters
137                cachedRect = TargetRect;
138                cachedType = Type;
139                cachedIntensity = Intensity;
140                cachedFeather = FeatherWidth;
141            }
142            catch
143            {
144                if (cachedResult != null)
145                {
146                    cachedResult.Dispose();
147                    cachedResult = null;
148                }
149            }
150        }
151
152        public void Dispose()
153        {
154            Dispose(true);
155            GC.SuppressFinalize(this);
156        }
157
158        protected virtual void Dispose(bool disposing)
159        {
160            if (!disposed)
161            {
162                if (disposing)
163                {
164                    if (cachedResult != null)
165                    {
166                        cachedResult.Dispose();
167                        cachedResult = null;
168                    }
169                }
170                disposed = true;
171            }
172        }
173
174        ~EffectLayer()
175        {
176            Dispose(false);
177        }
178
179        /// <summary>
180        /// Copy pixels directly from source to target in a specific rectangle.
181        /// Uses LockBits for pixel-perfect copying without any GDI+ artifacts.
182        /// </summary>
183        private static void CopyPixels(Bitmap source, Bitmap target, Rectangle rect)
184        {
185            // Clamp rect to bitmap bounds
186            rect = Rectangle.Intersect(rect, new Rectangle(0, 0, source.Width, source.Height));
187            rect = Rectangle.Intersect(rect, new Rectangle(0, 0, target.Width, target.Height));
188            if (rect.Width <= 0 || rect.Height <= 0) return;
189
190            var srcData = source.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
191            BitmapData dstData = null;
192            try
193            {
194                dstData = target.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
195
196                int srcStride = srcData.Stride;
197                int dstStride = dstData.Stride;
198                int bytesPerRow = rect.Width * 4;
199
200                unsafe
201                {
202                    byte* srcPtr = (byte*)srcData.Scan0;
203                    byte* dstPtr = (byte*)dstData.Scan0;
204
205                    for (int y = 0; y < rect.Height; y++)
206                    {
207                        Buffer.MemoryCopy(srcPtr, dstPtr, bytesPerRow, bytesPerRow);
208                        srcPtr += srcStride;
209                        dstPtr += dstStride;
210                    }
211                }
212            }
213            finally
214            {
215                // Always unlock both bitmaps; a leaked lock permanently breaks all later drawing/disposal.
216                source.UnlockBits(srcData);
217                if (dstData != null) target.UnlockBits(dstData);
218            }
219        }
220
221        /// <summary>
222        /// Apply dim effect to area outside the highlight rectangle.
223        /// </summary>
224        private void ApplyDimOutside(Bitmap target, Bitmap original, Rectangle highlightRect)
225        {
226            using (var g = Graphics.FromImage(target))
227            {
228                // Dim the whole bitmap
229                using (var dimBrush = new SolidBrush(Color.FromArgb(Settings.DimAlpha, 0, 0, 0)))
230                {
231                    g.FillRectangle(dimBrush, 0, 0, target.Width, target.Height);
232                }
233                // Restore highlight area from original (clear, no dim)
234                // Use nearest neighbor and SourceCopy to avoid edge artifacts
235                g.InterpolationMode = InterpolationMode.NearestNeighbor;
236                g.PixelOffsetMode = PixelOffsetMode.Half;
237                g.CompositingMode = CompositingMode.SourceCopy;
238                g.DrawImage(original, highlightRect, highlightRect, GraphicsUnit.Pixel);
239            }
240        }
241    }
242}