BlurImageControl.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Numerics; 6 using ManagedCommon; 7 using Microsoft.Graphics.Canvas.Effects; 8 using Microsoft.UI; 9 using Microsoft.UI.Composition; 10 using Microsoft.UI.Xaml; 11 using Microsoft.UI.Xaml.Controls; 12 using Microsoft.UI.Xaml.Hosting; 13 using Microsoft.UI.Xaml.Media; 14 using Windows.UI; 15 16 namespace Microsoft.CmdPal.UI.Controls; 17 18 internal sealed partial class BlurImageControl : Control 19 { 20 private const string ImageSourceParameterName = "ImageSource"; 21 22 private const string BrightnessEffectName = "Brightness"; 23 private const string BrightnessOverlayEffectName = "BrightnessOverlay"; 24 private const string BlurEffectName = "Blur"; 25 private const string TintBlendEffectName = "TintBlend"; 26 private const string TintEffectName = "Tint"; 27 28 #pragma warning disable CA1507 // Use nameof to express symbol names ... some of these refer to effect properties that are separate from the class properties 29 private static readonly string BrightnessSource1AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source1Amount"); 30 private static readonly string BrightnessSource2AmountEffectProperty = GetPropertyName(BrightnessEffectName, "Source2Amount"); 31 private static readonly string BrightnessOverlayColorEffectProperty = GetPropertyName(BrightnessOverlayEffectName, "Color"); 32 private static readonly string BlurBlurAmountEffectProperty = GetPropertyName(BlurEffectName, "BlurAmount"); 33 private static readonly string TintColorEffectProperty = GetPropertyName(TintEffectName, "Color"); 34 #pragma warning restore CA1507 35 36 private static readonly string[] AnimatableProperties = [ 37 BrightnessSource1AmountEffectProperty, 38 BrightnessSource2AmountEffectProperty, 39 BrightnessOverlayColorEffectProperty, 40 BlurBlurAmountEffectProperty, 41 TintColorEffectProperty 42 ]; 43 44 public static readonly DependencyProperty ImageSourceProperty = 45 DependencyProperty.Register( 46 nameof(ImageSource), 47 typeof(ImageSource), 48 typeof(BlurImageControl), 49 new PropertyMetadata(null, OnImageChanged)); 50 51 public static readonly DependencyProperty ImageStretchProperty = 52 DependencyProperty.Register( 53 nameof(ImageStretch), 54 typeof(Stretch), 55 typeof(BlurImageControl), 56 new PropertyMetadata(Stretch.UniformToFill, OnImageStretchChanged)); 57 58 public static readonly DependencyProperty ImageOpacityProperty = 59 DependencyProperty.Register( 60 nameof(ImageOpacity), 61 typeof(double), 62 typeof(BlurImageControl), 63 new PropertyMetadata(1.0, OnOpacityChanged)); 64 65 public static readonly DependencyProperty ImageBrightnessProperty = 66 DependencyProperty.Register( 67 nameof(ImageBrightness), 68 typeof(double), 69 typeof(BlurImageControl), 70 new PropertyMetadata(1.0, OnBrightnessChanged)); 71 72 public static readonly DependencyProperty BlurAmountProperty = 73 DependencyProperty.Register( 74 nameof(BlurAmount), 75 typeof(double), 76 typeof(BlurImageControl), 77 new PropertyMetadata(0.0, OnBlurAmountChanged)); 78 79 public static readonly DependencyProperty TintColorProperty = 80 DependencyProperty.Register( 81 nameof(TintColor), 82 typeof(Color), 83 typeof(BlurImageControl), 84 new PropertyMetadata(Colors.Transparent, OnVisualPropertyChanged)); 85 86 public static readonly DependencyProperty TintIntensityProperty = 87 DependencyProperty.Register( 88 nameof(TintIntensity), 89 typeof(double), 90 typeof(BlurImageControl), 91 new PropertyMetadata(0.0, OnVisualPropertyChanged)); 92 93 private Compositor? _compositor; 94 private SpriteVisual? _effectVisual; 95 private CompositionEffectBrush? _effectBrush; 96 private CompositionSurfaceBrush? _imageBrush; 97 98 public BlurImageControl() 99 { 100 this.DefaultStyleKey = typeof(BlurImageControl); 101 this.Loaded += OnLoaded; 102 this.SizeChanged += OnSizeChanged; 103 } 104 105 public ImageSource ImageSource 106 { 107 get => (ImageSource)GetValue(ImageSourceProperty); 108 set => SetValue(ImageSourceProperty, value); 109 } 110 111 public Stretch ImageStretch 112 { 113 get => (Stretch)GetValue(ImageStretchProperty); 114 set => SetValue(ImageStretchProperty, value); 115 } 116 117 public double ImageOpacity 118 { 119 get => (double)GetValue(ImageOpacityProperty); 120 set => SetValue(ImageOpacityProperty, value); 121 } 122 123 public double ImageBrightness 124 { 125 get => (double)GetValue(ImageBrightnessProperty); 126 set => SetValue(ImageBrightnessProperty, Math.Clamp(value, -1, 1)); 127 } 128 129 public double BlurAmount 130 { 131 get => (double)GetValue(BlurAmountProperty); 132 set => SetValue(BlurAmountProperty, value); 133 } 134 135 public Color TintColor 136 { 137 get => (Color)GetValue(TintColorProperty); 138 set => SetValue(TintColorProperty, value); 139 } 140 141 public double TintIntensity 142 { 143 get => (double)GetValue(TintIntensityProperty); 144 set => SetValue(TintIntensityProperty, value); 145 } 146 147 private static void OnImageStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 148 { 149 if (d is BlurImageControl control && control._imageBrush != null) 150 { 151 control._imageBrush.Stretch = ConvertStretch((Stretch)e.NewValue); 152 } 153 } 154 155 private static void OnVisualPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 156 { 157 if (d is BlurImageControl control && control._compositor != null) 158 { 159 control.UpdateEffect(); 160 } 161 } 162 163 private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 164 { 165 if (d is BlurImageControl control && control._effectVisual != null) 166 { 167 control._effectVisual.Opacity = (float)(double)e.NewValue; 168 } 169 } 170 171 private static void OnBlurAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 172 { 173 if (d is BlurImageControl control && control._effectBrush != null) 174 { 175 control.UpdateEffect(); 176 } 177 } 178 179 private static void OnBrightnessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 180 { 181 if (d is BlurImageControl control && control._effectBrush != null) 182 { 183 control.UpdateEffect(); 184 } 185 } 186 187 private void OnLoaded(object sender, RoutedEventArgs e) 188 { 189 InitializeComposition(); 190 } 191 192 private void OnSizeChanged(object sender, SizeChangedEventArgs e) 193 { 194 if (_effectVisual != null) 195 { 196 _effectVisual.Size = new Vector2( 197 (float)Math.Max(1, e.NewSize.Width), 198 (float)Math.Max(1, e.NewSize.Height)); 199 } 200 } 201 202 private static void OnImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 203 { 204 if (d is not BlurImageControl control) 205 { 206 return; 207 } 208 209 control.EnsureEffect(force: true); 210 control.UpdateEffect(); 211 } 212 213 private void InitializeComposition() 214 { 215 var visual = ElementCompositionPreview.GetElementVisual(this); 216 _compositor = visual.Compositor; 217 218 _effectVisual = _compositor.CreateSpriteVisual(); 219 _effectVisual.Size = new Vector2( 220 (float)Math.Max(1, ActualWidth), 221 (float)Math.Max(1, ActualHeight)); 222 _effectVisual.Opacity = (float)ImageOpacity; 223 224 ElementCompositionPreview.SetElementChildVisual(this, _effectVisual); 225 226 UpdateEffect(); 227 } 228 229 private void EnsureEffect(bool force = false) 230 { 231 if (_compositor is null) 232 { 233 return; 234 } 235 236 if (_effectBrush is not null && !force) 237 { 238 return; 239 } 240 241 var imageSource = new CompositionEffectSourceParameter(ImageSourceParameterName); 242 243 // 1) Brightness via ArithmeticCompositeEffect 244 // We blend between the original image and either black or white, 245 // depending on whether we want to darken or brighten. BrightnessEffect isn't supported 246 // in the composition graph. 247 var brightnessEffect = new ArithmeticCompositeEffect 248 { 249 Name = BrightnessEffectName, 250 Source1 = imageSource, // original image 251 Source2 = new ColorSourceEffect 252 { 253 Name = BrightnessOverlayEffectName, 254 Color = Colors.Black, // we'll swap black/white via properties 255 }, 256 257 MultiplyAmount = 0.0f, 258 Source1Amount = 1.0f, // original 259 Source2Amount = 0.0f, // overlay 260 Offset = 0.0f, 261 }; 262 263 // 2) Blur 264 var blurEffect = new GaussianBlurEffect 265 { 266 Name = BlurEffectName, 267 BlurAmount = 0.0f, 268 BorderMode = EffectBorderMode.Hard, 269 Optimization = EffectOptimization.Balanced, 270 Source = brightnessEffect, 271 }; 272 273 // 3) Tint (always in the chain; intensity via alpha) 274 var tintEffect = new BlendEffect 275 { 276 Name = TintBlendEffectName, 277 Background = blurEffect, 278 Foreground = new ColorSourceEffect 279 { 280 Name = TintEffectName, 281 Color = Colors.Transparent, 282 }, 283 Mode = BlendEffectMode.Multiply, 284 }; 285 286 var effectFactory = _compositor.CreateEffectFactory(tintEffect, AnimatableProperties); 287 288 _effectBrush?.Dispose(); 289 _effectBrush = effectFactory.CreateBrush(); 290 291 // Set initial source 292 if (ImageSource is not null) 293 { 294 _imageBrush ??= _compositor.CreateSurfaceBrush(); 295 LoadImageAsync(ImageSource); 296 _effectBrush.SetSourceParameter(ImageSourceParameterName, _imageBrush); 297 } 298 else 299 { 300 _effectBrush.SetSourceParameter(ImageSourceParameterName, _compositor.CreateBackdropBrush()); 301 } 302 303 if (_effectVisual is not null) 304 { 305 _effectVisual.Brush = _effectBrush; 306 } 307 } 308 309 private void UpdateEffect() 310 { 311 if (_compositor is null) 312 { 313 return; 314 } 315 316 EnsureEffect(); 317 if (_effectBrush is null) 318 { 319 return; 320 } 321 322 var props = _effectBrush.Properties; 323 324 // Brightness 325 var b = (float)Math.Clamp(ImageBrightness, -1.0, 1.0); 326 327 float source1Amount; 328 float source2Amount; 329 Color overlayColor; 330 331 if (b >= 0) 332 { 333 // Brighten: blend towards white 334 overlayColor = Colors.White; 335 source1Amount = 1.0f - b; // original image contribution 336 source2Amount = b; // white overlay contribution 337 } 338 else 339 { 340 // Darken: blend towards black 341 overlayColor = Colors.Black; 342 var t = -b; // 0..1 343 source1Amount = 1.0f - t; // original image 344 source2Amount = t; // black overlay 345 } 346 347 props.InsertScalar(BrightnessSource1AmountEffectProperty, source1Amount); 348 props.InsertScalar(BrightnessSource2AmountEffectProperty, source2Amount); 349 props.InsertColor(BrightnessOverlayColorEffectProperty, overlayColor); 350 351 // Blur 352 props.InsertScalar(BlurBlurAmountEffectProperty, (float)BlurAmount); 353 354 // Tint 355 var tintColor = TintColor; 356 var clampedIntensity = (float)Math.Clamp(TintIntensity, 0.0, 1.0); 357 358 var adjustedColor = Color.FromArgb( 359 (byte)(clampedIntensity * 255), 360 tintColor.R, 361 tintColor.G, 362 tintColor.B); 363 364 props.InsertColor(TintColorEffectProperty, adjustedColor); 365 } 366 367 private void LoadImageAsync(ImageSource imageSource) 368 { 369 try 370 { 371 if (imageSource is not Microsoft.UI.Xaml.Media.Imaging.BitmapImage bitmapImage) 372 { 373 return; 374 } 375 376 _imageBrush ??= _compositor?.CreateSurfaceBrush(); 377 if (_imageBrush is null) 378 { 379 return; 380 } 381 382 Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'"); 383 var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource); 384 loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted; 385 SetLoadedSurfaceToBrush(loadedSurface); 386 _effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush); 387 } 388 catch (Exception ex) 389 { 390 Logger.LogError("Failed to load image for BlurImageControl: {0}", ex); 391 } 392 393 return; 394 395 void OnLoadedSurfaceOnLoadCompleted(LoadedImageSurface loadedSurface, LoadedImageSourceLoadCompletedEventArgs e) 396 { 397 switch (e.Status) 398 { 399 case LoadedImageSourceLoadStatus.Success: 400 Logger.LogDebug($"BlurImageControl loaded successfully: has _imageBrush? {_imageBrush != null}"); 401 402 try 403 { 404 SetLoadedSurfaceToBrush(loadedSurface); 405 } 406 catch (Exception ex) 407 { 408 Logger.LogError("Failed to set surface in BlurImageControl", ex); 409 throw; 410 } 411 412 break; 413 case LoadedImageSourceLoadStatus.NetworkError: 414 case LoadedImageSourceLoadStatus.InvalidFormat: 415 case LoadedImageSourceLoadStatus.Other: 416 default: 417 Logger.LogError($"Failed to load image for BlurImageControl: Load status {e.Status}"); 418 break; 419 } 420 } 421 } 422 423 private void SetLoadedSurfaceToBrush(LoadedImageSurface loadedSurface) 424 { 425 var surfaceBrush = _imageBrush; 426 if (surfaceBrush is null) 427 { 428 return; 429 } 430 431 surfaceBrush.Surface = loadedSurface; 432 surfaceBrush.Stretch = ConvertStretch(ImageStretch); 433 surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.Linear; 434 } 435 436 private static CompositionStretch ConvertStretch(Stretch stretch) 437 { 438 return stretch switch 439 { 440 Stretch.None => CompositionStretch.None, 441 Stretch.Fill => CompositionStretch.Fill, 442 Stretch.Uniform => CompositionStretch.Uniform, 443 Stretch.UniformToFill => CompositionStretch.UniformToFill, 444 _ => CompositionStretch.UniformToFill, 445 }; 446 } 447 448 private static string GetPropertyName(string effectName, string propertyName) => $"{effectName}.{propertyName}"; 449 }