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}