1using System; 2using System.Drawing; 3using System.Drawing.Drawing2D; 4using System.Drawing.Imaging; 5using WindowCapture.Models; 6 7namespace WindowCapture.Effects 8{ 9 public static class ImageEffects 10 { 11 /// <summary> 12 /// Apply Gaussian blur to a rectangular area of the bitmap. 13 /// </summary> 14 public static void ApplyBlur(Bitmap bmp, Rectangle area, int radius) 15 { 16 if (radius < 1) return; 17 18 // Clamp area to bitmap bounds 19 area = Rectangle.Intersect(area, new Rectangle(0, 0, bmp.Width, bmp.Height)); 20 if (area.Width <= 0 || area.Height <= 0) return; 21 22 // Apply Gaussian blur with cosine-based intensity curve 23 // This gives more natural progression: subtle at low values, stronger at high 24 ApplyGaussianBlur(bmp, area, radius); 25 } 26 27 /// <summary> 28 /// Gaussian blur with proper weighted kernel for smooth, natural-looking blur. 29 /// </summary> 30 private static void ApplyGaussianBlur(Bitmap bmp, Rectangle area, int radius) 31 { 32 if (radius < 1) return; 33 34 // Convert radius to sigma using cosine curve for more natural feel 35 // At radius=1: very subtle, at radius=10: strong but not extreme 36 // Cosine curve: slow start, faster in middle, slows at end 37 float t = (radius - 1) / 9.0f; // normalize to 0-1 38 float cosineT = (float)(1.0 - Math.Cos(t * Math.PI)) / 2.0f; // S-curve 39 float sigma = 0.5f + cosineT * 8.0f; // range 0.5 to 8.5 40 41 // Generate 1D Gaussian kernel 42 int kernelSize = (int)Math.Ceiling(sigma * 3) * 2 + 1; 43 if (kernelSize < 3) kernelSize = 3; 44 float[] kernel = new float[kernelSize]; 45 float sum = 0; 46 int half = kernelSize / 2; 47 48 for (int i = 0; i < kernelSize; i++) 49 { 50 float x = i - half; 51 kernel[i] = (float)Math.Exp(-(x * x) / (2 * sigma * sigma)); 52 sum += kernel[i]; 53 } 54 55 // Normalize kernel 56 for (int i = 0; i < kernelSize; i++) 57 kernel[i] /= sum; 58 59 var data = bmp.LockBits(area, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); 60 try 61 { 62 int stride = data.Stride; 63 int bytes = Math.Abs(stride) * area.Height; 64 byte[] pixels = new byte[bytes]; 65 byte[] temp = new byte[bytes]; 66 67 System.Runtime.InteropServices.Marshal.Copy(data.Scan0, pixels, 0, bytes); 68 69 // Horizontal pass with Gaussian weights 70 for (int y = 0; y < area.Height; y++) 71 { 72 for (int x = 0; x < area.Width; x++) 73 { 74 float rSum = 0, gSum = 0, bSum = 0, aSum = 0; 75 float weightSum = 0; 76 77 for (int k = 0; k < kernelSize; k++) 78 { 79 int nx = x + k - half; 80 if (nx >= 0 && nx < area.Width) 81 { 82 int idx = y * stride + nx * 4; 83 float w = kernel[k]; 84 bSum += pixels[idx] * w; 85 gSum += pixels[idx + 1] * w; 86 rSum += pixels[idx + 2] * w; 87 aSum += pixels[idx + 3] * w; 88 weightSum += w; 89 } 90 } 91 92 int outIdx = y * stride + x * 4; 93 temp[outIdx] = (byte)(bSum / weightSum); 94 temp[outIdx + 1] = (byte)(gSum / weightSum); 95 temp[outIdx + 2] = (byte)(rSum / weightSum); 96 temp[outIdx + 3] = (byte)(aSum / weightSum); 97 } 98 } 99 100 // Vertical pass with Gaussian weights 101 for (int y = 0; y < area.Height; y++) 102 { 103 for (int x = 0; x < area.Width; x++) 104 { 105 float rSum = 0, gSum = 0, bSum = 0, aSum = 0; 106 float weightSum = 0; 107 108 for (int k = 0; k < kernelSize; k++) 109 { 110 int ny = y + k - half; 111 if (ny >= 0 && ny < area.Height) 112 { 113 int idx = ny * stride + x * 4; 114 float w = kernel[k]; 115 bSum += temp[idx] * w; 116 gSum += temp[idx + 1] * w; 117 rSum += temp[idx + 2] * w; 118 aSum += temp[idx + 3] * w; 119 weightSum += w; 120 } 121 } 122 123 int outIdx = y * stride + x * 4; 124 pixels[outIdx] = (byte)(bSum / weightSum); 125 pixels[outIdx + 1] = (byte)(gSum / weightSum); 126 pixels[outIdx + 2] = (byte)(rSum / weightSum); 127 pixels[outIdx + 3] = (byte)(aSum / weightSum); 128 } 129 } 130 131 System.Runtime.InteropServices.Marshal.Copy(pixels, 0, data.Scan0, bytes); 132 } 133 finally { bmp.UnlockBits(data); } 134 } 135 136 /// <summary> 137 /// Apply blur outside the specified rectangle (inverted blur). 138 /// </summary> 139 public static void ApplyInvertedBlur(Bitmap bmp, Rectangle clearArea, int radius) 140 { 141 ApplyInvertedBlur(bmp, null, clearArea, radius); 142 } 143 144 /// <summary> 145 /// Apply blur to the entire bitmap except a rectangular area. 146 /// If original is provided, restore clearArea from original instead of pre-blur state. 147 /// </summary> 148 public static void ApplyInvertedBlur(Bitmap bmp, Bitmap original, Rectangle clearArea, int radius) 149 { 150 // Create a copy of the clear area from original (or bmp if no original) 151 Bitmap source = original ?? bmp; 152 Bitmap clearPart = new Bitmap(clearArea.Width, clearArea.Height); 153 using (var g = Graphics.FromImage(clearPart)) 154 { 155 // Use nearest neighbor to avoid edge artifacts 156 g.InterpolationMode = InterpolationMode.NearestNeighbor; 157 g.PixelOffsetMode = PixelOffsetMode.Half; 158 g.DrawImage(source, 0, 0, clearArea, GraphicsUnit.Pixel); 159 } 160 161 // Blur the entire image 162 ApplyBlur(bmp, new Rectangle(0, 0, bmp.Width, bmp.Height), radius); 163 164 // Restore the clear area from original 165 using (var g = Graphics.FromImage(bmp)) 166 { 167 // Use nearest neighbor to avoid edge artifacts 168 g.InterpolationMode = InterpolationMode.NearestNeighbor; 169 g.PixelOffsetMode = PixelOffsetMode.Half; 170 g.CompositingMode = CompositingMode.SourceCopy; 171 g.DrawImage(clearPart, clearArea.X, clearArea.Y); 172 } 173 174 clearPart.Dispose(); 175 } 176 177 /// <summary> 178 /// Apply horizontal motion blur to a rectangular area. 179 /// </summary> 180 public static void ApplyMotionBlur(Bitmap bmp, Rectangle area, int distance) 181 { 182 if (distance < 1) return; 183 184 // Clamp area to bitmap bounds 185 area = Rectangle.Intersect(area, new Rectangle(0, 0, bmp.Width, bmp.Height)); 186 if (area.Width <= 0 || area.Height <= 0) return; 187 188 var data = bmp.LockBits(area, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); 189 try 190 { 191 int stride = data.Stride; 192 int bytes = Math.Abs(stride) * area.Height; 193 byte[] pixels = new byte[bytes]; 194 byte[] result = new byte[bytes]; 195 196 System.Runtime.InteropServices.Marshal.Copy(data.Scan0, pixels, 0, bytes); 197 198 // Horizontal motion blur 199 for (int y = 0; y < area.Height; y++) 200 { 201 for (int x = 0; x < area.Width; x++) 202 { 203 int rSum = 0, gSum = 0, bSum = 0, aSum = 0; 204 int count = 0; 205 206 for (int dx = -distance; dx <= distance; dx++) 207 { 208 int nx = x + dx; 209 if (nx >= 0 && nx < area.Width) 210 { 211 int idx = y * stride + nx * 4; 212 bSum += pixels[idx]; 213 gSum += pixels[idx + 1]; 214 rSum += pixels[idx + 2]; 215 aSum += pixels[idx + 3]; 216 count++; 217 } 218 } 219 220 int outIdx = y * stride + x * 4; 221 result[outIdx] = (byte)(bSum / count); 222 result[outIdx + 1] = (byte)(gSum / count); 223 result[outIdx + 2] = (byte)(rSum / count); 224 result[outIdx + 3] = (byte)(aSum / count); 225 } 226 } 227 228 System.Runtime.InteropServices.Marshal.Copy(result, 0, data.Scan0, bytes); 229 } 230 finally { bmp.UnlockBits(data); } 231 } 232 233 /// <summary> 234 /// Apply inverted motion blur (blur outside, clear inside). 235 /// </summary> 236 public static void ApplyInvertedMotionBlur(Bitmap bmp, Rectangle clearArea, int distance) 237 { 238 ApplyInvertedMotionBlur(bmp, null, clearArea, distance); 239 } 240 241 /// <summary> 242 /// Apply motion blur outside a rectangular area. 243 /// If original is provided, restore clearArea from original instead of pre-blur state. 244 /// </summary> 245 public static void ApplyInvertedMotionBlur(Bitmap bmp, Bitmap original, Rectangle clearArea, int distance) 246 { 247 // Create a copy of the clear area from original (or bmp if no original) 248 Bitmap source = original ?? bmp; 249 Bitmap clearPart = new Bitmap(clearArea.Width, clearArea.Height); 250 using (var g = Graphics.FromImage(clearPart)) 251 { 252 // Use nearest neighbor to avoid edge artifacts 253 g.InterpolationMode = InterpolationMode.NearestNeighbor; 254 g.PixelOffsetMode = PixelOffsetMode.Half; 255 g.DrawImage(source, 0, 0, clearArea, GraphicsUnit.Pixel); 256 } 257 258 // Apply motion blur to entire image 259 ApplyMotionBlur(bmp, new Rectangle(0, 0, bmp.Width, bmp.Height), distance); 260 261 // Restore the clear area from original 262 using (var g = Graphics.FromImage(bmp)) 263 { 264 // Use nearest neighbor to avoid edge artifacts 265 g.InterpolationMode = InterpolationMode.NearestNeighbor; 266 g.PixelOffsetMode = PixelOffsetMode.Half; 267 g.CompositingMode = CompositingMode.SourceCopy; 268 g.DrawImage(clearPart, clearArea.X, clearArea.Y); 269 } 270 271 clearPart.Dispose(); 272 } 273 274 /// <summary> 275 /// Apply dim (darken) effect to a rectangular area. 276 /// </summary> 277 public static void ApplyDim(Bitmap bmp, Rectangle area, int alpha) 278 { 279 area = Rectangle.Intersect(area, new Rectangle(0, 0, bmp.Width, bmp.Height)); 280 if (area.Width <= 0 || area.Height <= 0) return; 281 282 using (var g = Graphics.FromImage(bmp)) 283 using (var brush = new SolidBrush(Color.FromArgb(alpha, 0, 0, 0))) 284 { 285 g.FillRectangle(brush, area); 286 } 287 } 288 289 /// <summary> 290 /// Apply feathered (soft) edges to a rectangular area. 291 /// </summary> 292 public static void ApplyFeather(Bitmap bmp, Rectangle area, int featherWidth) 293 { 294 if (featherWidth < 1) return; 295 296 area = Rectangle.Intersect(area, new Rectangle(0, 0, bmp.Width, bmp.Height)); 297 if (area.Width <= 0 || area.Height <= 0) return; 298 299 var data = bmp.LockBits(area, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); 300 int stride = data.Stride; 301 int bytes = Math.Abs(stride) * area.Height; 302 byte[] pixels = new byte[bytes]; 303 304 System.Runtime.InteropServices.Marshal.Copy(data.Scan0, pixels, 0, bytes); 305 306 // Apply feather effect - fade alpha near edges 307 for (int y = 0; y < area.Height; y++) 308 { 309 for (int x = 0; x < area.Width; x++) 310 { 311 // Calculate distance to nearest edge 312 int distLeft = x; 313 int distRight = area.Width - 1 - x; 314 int distTop = y; 315 int distBottom = area.Height - 1 - y; 316 int minDist = Math.Min(Math.Min(distLeft, distRight), Math.Min(distTop, distBottom)); 317 318 if (minDist < featherWidth) 319 { 320 float factor = (float)minDist / featherWidth; 321 int idx = y * stride + x * 4; 322 // Blend with transparent 323 pixels[idx + 3] = (byte)(pixels[idx + 3] * factor); 324 } 325 } 326 } 327 328 System.Runtime.InteropServices.Marshal.Copy(pixels, 0, data.Scan0, bytes); 329 bmp.UnlockBits(data); 330 } 331 332 /// <summary> 333 /// Simple box blur implementation. 334 /// </summary> 335 private static void ApplyBoxBlur(Bitmap bmp, Rectangle area, int radius) 336 { 337 if (radius < 1) return; 338 339 var data = bmp.LockBits(area, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); 340 int stride = data.Stride; 341 int bytes = Math.Abs(stride) * area.Height; 342 byte[] pixels = new byte[bytes]; 343 byte[] temp = new byte[bytes]; 344 345 System.Runtime.InteropServices.Marshal.Copy(data.Scan0, pixels, 0, bytes); 346 347 // Horizontal pass 348 for (int y = 0; y < area.Height; y++) 349 { 350 for (int x = 0; x < area.Width; x++) 351 { 352 int rSum = 0, gSum = 0, bSum = 0, aSum = 0; 353 int count = 0; 354 355 for (int dx = -radius; dx <= radius; dx++) 356 { 357 int nx = x + dx; 358 if (nx >= 0 && nx < area.Width) 359 { 360 int idx = y * stride + nx * 4; 361 bSum += pixels[idx]; 362 gSum += pixels[idx + 1]; 363 rSum += pixels[idx + 2]; 364 aSum += pixels[idx + 3]; 365 count++; 366 } 367 } 368 369 int outIdx = y * stride + x * 4; 370 temp[outIdx] = (byte)(bSum / count); 371 temp[outIdx + 1] = (byte)(gSum / count); 372 temp[outIdx + 2] = (byte)(rSum / count); 373 temp[outIdx + 3] = (byte)(aSum / count); 374 } 375 } 376 377 // Vertical pass 378 for (int y = 0; y < area.Height; y++) 379 { 380 for (int x = 0; x < area.Width; x++) 381 { 382 int rSum = 0, gSum = 0, bSum = 0, aSum = 0; 383 int count = 0; 384 385 for (int dy = -radius; dy <= radius; dy++) 386 { 387 int ny = y + dy; 388 if (ny >= 0 && ny < area.Height) 389 { 390 int idx = ny * stride + x * 4; 391 bSum += temp[idx]; 392 gSum += temp[idx + 1]; 393 rSum += temp[idx + 2]; 394 aSum += temp[idx + 3]; 395 count++; 396 } 397 } 398 399 int outIdx = y * stride + x * 4; 400 pixels[outIdx] = (byte)(bSum / count); 401 pixels[outIdx + 1] = (byte)(gSum / count); 402 pixels[outIdx + 2] = (byte)(rSum / count); 403 pixels[outIdx + 3] = (byte)(aSum / count); 404 } 405 } 406 407 System.Runtime.InteropServices.Marshal.Copy(pixels, 0, data.Scan0, bytes); 408 bmp.UnlockBits(data); 409 } 410 411 /// <summary> 412 /// Create a copy of a rectangular portion of a bitmap. 413 /// </summary> 414 public static Bitmap CopyRegion(Bitmap source, Rectangle area) 415 { 416 area = Rectangle.Intersect(area, new Rectangle(0, 0, source.Width, source.Height)); 417 if (area.Width <= 0 || area.Height <= 0) return null; 418 419 var result = new Bitmap(area.Width, area.Height); 420 using (var g = Graphics.FromImage(result)) 421 { 422 g.DrawImage(source, 0, 0, area, GraphicsUnit.Pixel); 423 } 424 return result; 425 } 426 427 /// <summary> 428 /// Restore a region from a backup bitmap. 429 /// </summary> 430 public static void RestoreRegion(Bitmap target, Bitmap backup, Rectangle area) 431 { 432 using (var g = Graphics.FromImage(target)) 433 { 434 g.DrawImage(backup, area.X, area.Y); 435 } 436 } 437 438 /// <summary> 439 /// Apply fade blur effect - gradient blur around the outer edge of highlight area. 440 /// Creates smooth transition from clear center to blurred edge. 441 /// </summary> 442 public static void ApplyFadeBlur(Bitmap bmp, Bitmap original, Rectangle highlightArea, int fadeWidth, int blurRadius) 443 { 444 if (fadeWidth < 1 || blurRadius < 1) return; 445 if (bmp == null || original == null) return; 446 447 // Create a fully blurred version 448 Bitmap blurred = (Bitmap)original.Clone(); 449 ApplyBlur(blurred, new Rectangle(0, 0, blurred.Width, blurred.Height), blurRadius); 450 451 // Now blend: inside highlight = original, outside = blurred, edge = gradient 452 var data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), 453 ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); 454 var origData = original.LockBits(new Rectangle(0, 0, original.Width, original.Height), 455 ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); 456 var blurData = blurred.LockBits(new Rectangle(0, 0, blurred.Width, blurred.Height), 457 ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); 458 459 int stride = data.Stride; 460 int bytes = Math.Abs(stride) * bmp.Height; 461 byte[] pixels = new byte[bytes]; 462 byte[] origPixels = new byte[bytes]; 463 byte[] blurPixels = new byte[bytes]; 464 465 System.Runtime.InteropServices.Marshal.Copy(data.Scan0, pixels, 0, bytes); 466 System.Runtime.InteropServices.Marshal.Copy(origData.Scan0, origPixels, 0, bytes); 467 System.Runtime.InteropServices.Marshal.Copy(blurData.Scan0, blurPixels, 0, bytes); 468 469 // Process all pixels 470 for (int y = 0; y < bmp.Height; y++) 471 { 472 for (int x = 0; x < bmp.Width; x++) 473 { 474 int idx = y * stride + x * 4; 475 476 // Calculate distance from highlight border (negative = inside, positive = outside) 477 int distX = 0, distY = 0; 478 479 if (x < highlightArea.Left) 480 distX = highlightArea.Left - x; 481 else if (x >= highlightArea.Right) 482 distX = x - highlightArea.Right + 1; 483 484 if (y < highlightArea.Top) 485 distY = highlightArea.Top - y; 486 else if (y >= highlightArea.Bottom) 487 distY = y - highlightArea.Bottom + 1; 488 489 // Inside highlight 490 if (distX == 0 && distY == 0) 491 { 492 // Check distance from inner edge 493 int innerDistX = Math.Min(x - highlightArea.Left, highlightArea.Right - 1 - x); 494 int innerDistY = Math.Min(y - highlightArea.Top, highlightArea.Bottom - 1 - y); 495 int innerDist = Math.Min(innerDistX, innerDistY); 496 497 if (innerDist < fadeWidth) 498 { 499 // Gradient from edge to center 500 float factor = (float)innerDist / fadeWidth; 501 // 0 at edge = more blur, 1 at center = original 502 pixels[idx] = (byte)(origPixels[idx] * factor + blurPixels[idx] * (1 - factor)); 503 pixels[idx + 1] = (byte)(origPixels[idx + 1] * factor + blurPixels[idx + 1] * (1 - factor)); 504 pixels[idx + 2] = (byte)(origPixels[idx + 2] * factor + blurPixels[idx + 2] * (1 - factor)); 505 pixels[idx + 3] = (byte)(origPixels[idx + 3] * factor + blurPixels[idx + 3] * (1 - factor)); 506 } 507 else 508 { 509 // Center = original 510 pixels[idx] = origPixels[idx]; 511 pixels[idx + 1] = origPixels[idx + 1]; 512 pixels[idx + 2] = origPixels[idx + 2]; 513 pixels[idx + 3] = origPixels[idx + 3]; 514 } 515 } 516 else 517 { 518 // Outside highlight = blurred 519 pixels[idx] = blurPixels[idx]; 520 pixels[idx + 1] = blurPixels[idx + 1]; 521 pixels[idx + 2] = blurPixels[idx + 2]; 522 pixels[idx + 3] = blurPixels[idx + 3]; 523 } 524 } 525 } 526 527 System.Runtime.InteropServices.Marshal.Copy(pixels, 0, data.Scan0, bytes); 528 bmp.UnlockBits(data); 529 original.UnlockBits(origData); 530 blurred.UnlockBits(blurData); 531 blurred.Dispose(); 532 } 533 534 /// <summary> 535 /// Apply feather effect at the border of a clear area. 536 /// Creates smooth gradient transition from blurred to clear at the edges. 537 /// </summary> 538 public static void ApplyBorderFeather(Bitmap bmp, Rectangle clearArea, int featherWidth) 539 { 540 if (featherWidth < 1) return; 541 if (bmp == null) return; 542 543 // Work with the entire bitmap to blend edges 544 var data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), 545 ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); 546 int stride = data.Stride; 547 int bytes = Math.Abs(stride) * bmp.Height; 548 byte[] pixels = new byte[bytes]; 549 550 System.Runtime.InteropServices.Marshal.Copy(data.Scan0, pixels, 0, bytes); 551 552 // Create a copy for reading original values 553 byte[] original = new byte[bytes]; 554 Array.Copy(pixels, original, bytes); 555 556 // Define the feather zone (expanded area around clearArea) 557 int fLeft = Math.Max(0, clearArea.Left - featherWidth); 558 int fTop = Math.Max(0, clearArea.Top - featherWidth); 559 int fRight = Math.Min(bmp.Width, clearArea.Right + featherWidth); 560 int fBottom = Math.Min(bmp.Height, clearArea.Bottom + featherWidth); 561 562 // Process pixels in the feather zone 563 for (int y = fTop; y < fBottom; y++) 564 { 565 for (int x = fLeft; x < fRight; x++) 566 { 567 // Skip if completely inside clear area 568 if (x >= clearArea.Left && x < clearArea.Right && 569 y >= clearArea.Top && y < clearArea.Bottom) 570 continue; 571 572 // Calculate distance to the clear area border 573 int distX = 0, distY = 0; 574 575 if (x < clearArea.Left) 576 distX = clearArea.Left - x; 577 else if (x >= clearArea.Right) 578 distX = x - clearArea.Right + 1; 579 580 if (y < clearArea.Top) 581 distY = clearArea.Top - y; 582 else if (y >= clearArea.Bottom) 583 distY = y - clearArea.Bottom + 1; 584 585 int dist = (int)Math.Sqrt(distX * distX + distY * distY); 586 587 if (dist < featherWidth) 588 { 589 // Blend factor: 0 at border (fully blurred), 1 at featherWidth (original blur) 590 float factor = (float)dist / featherWidth; 591 int idx = y * stride + x * 4; 592 593 // Blend between clear (original inside clear area) and blurred 594 // For outside areas, darken/fade the effect 595 byte newAlpha = (byte)(pixels[idx + 3] * (0.3f + 0.7f * factor)); 596 pixels[idx + 3] = newAlpha; 597 } 598 } 599 } 600 601 System.Runtime.InteropServices.Marshal.Copy(pixels, 0, data.Scan0, bytes); 602 bmp.UnlockBits(data); 603 } 604 } 605}