From 2ec862bafdddf59e2095c4b7290df473208d070f Mon Sep 17 00:00:00 2001 From: ahmed walid Date: Sun, 24 Sep 2023 03:10:49 +0200 Subject: [PATCH] feat(composition): Implement AcrylicBrush! --- .../BasicAcrylicBrushTest.xaml | 4 +- .../CompositionEffectBrush.skia.cs | 4 +- .../CompositionSpriteShape.skia.cs | 1 + .../SkiaCompositionSurface.skia.cs | 5 + src/Uno.UI/Resources/NoiseAsset256x256.png | Bin 0 -> 3241 bytes .../Xaml/Media/AcrylicBrush/AcrylicBrush.cs | 6 +- .../Media/AcrylicBrush/AcrylicBrush.skia.cs | 560 ++++++++++++++++++ src/Uno.UI/UI/Xaml/Media/Brush.skia.cs | 6 +- src/Uno.UI/Uno.UI.Skia.csproj | 4 + 9 files changed, 583 insertions(+), 7 deletions(-) create mode 100644 src/Uno.UI/Resources/NoiseAsset256x256.png create mode 100644 src/Uno.UI/UI/Xaml/Media/AcrylicBrush/AcrylicBrush.skia.cs diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/AcrylicBrushTests/BasicAcrylicBrushTest.xaml b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/AcrylicBrushTests/BasicAcrylicBrushTest.xaml index 30b45ad4a745..acbe967df094 100644 --- a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/AcrylicBrushTests/BasicAcrylicBrushTest.xaml +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media/AcrylicBrushTests/BasicAcrylicBrushTest.xaml @@ -8,7 +8,7 @@ mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> - + @@ -18,7 +18,7 @@ - + diff --git a/src/Uno.UI.Composition/Composition/CompositionEffectBrush.skia.cs b/src/Uno.UI.Composition/Composition/CompositionEffectBrush.skia.cs index dadca0805c59..55400dca2873 100644 --- a/src/Uno.UI.Composition/Composition/CompositionEffectBrush.skia.cs +++ b/src/Uno.UI.Composition/Composition/CompositionEffectBrush.skia.cs @@ -18,6 +18,8 @@ public partial class CompositionEffectBrush : CompositionBrush internal bool HasBackdropBrushInput { get; private set; } + internal bool UseBlurPadding { get; set; } + private SKImageFilter? GenerateEffectFilter(object effect, SKRect bounds) { // TODO: https://user-images.githubusercontent.com/34550324/264485558-d7ee5062-b0e0-4f6e-a8c7-0620ec561d3d.png @@ -71,7 +73,7 @@ public partial class CompositionEffectBrush : CompositionBrush _ = (uint)effectInterop.GetProperty(optProp); // TODO _ = (uint)effectInterop.GetProperty(borderProp); // TODO - return SKImageFilter.CreateBlur(sigma, sigma, sourceFilter, new(bounds)); + return SKImageFilter.CreateBlur(sigma, sigma, sourceFilter, new(UseBlurPadding ? bounds with { Left = -100, Top = -100, Right = bounds.Right + 100, Bottom = bounds.Bottom + 100 } : bounds)); } return null; diff --git a/src/Uno.UI.Composition/Composition/CompositionSpriteShape.skia.cs b/src/Uno.UI.Composition/Composition/CompositionSpriteShape.skia.cs index 3660e7383eff..d34f0d869e3e 100644 --- a/src/Uno.UI.Composition/Composition/CompositionSpriteShape.skia.cs +++ b/src/Uno.UI.Composition/Composition/CompositionSpriteShape.skia.cs @@ -98,6 +98,7 @@ private static SKPaint TryCreateAndClearPaint(in DrawingSession session, ref SKP paint.IsStroke = isStroke; paint.IsAntialias = true; paint.IsAutohinted = true; + paint.FilterQuality = SKFilterQuality.High; } else { diff --git a/src/Uno.UI.Composition/Composition/SkiaCompositionSurface.skia.cs b/src/Uno.UI.Composition/Composition/SkiaCompositionSurface.skia.cs index ff128494c010..272fbdb716ac 100644 --- a/src/Uno.UI.Composition/Composition/SkiaCompositionSurface.skia.cs +++ b/src/Uno.UI.Composition/Composition/SkiaCompositionSurface.skia.cs @@ -41,6 +41,11 @@ internal SkiaCompositionSurface(SKImage image) this.Log().Debug($"Image load result {result}"); } + if (result == SKCodecResult.Success) + { + _image = SKImage.FromBitmap(bitmap); + } + return (result == SKCodecResult.Success || result == SKCodecResult.IncompleteInput, result); } else diff --git a/src/Uno.UI/Resources/NoiseAsset256x256.png b/src/Uno.UI/Resources/NoiseAsset256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..41de173d9014b38f8290acf270c490b37fc79a60 GIT binary patch literal 3241 zcmV;a3|8}rP)QN3MgRZ*%*@Q0nVDv0W-~K00002< zk9uhU01PcjL_t(|+U#A4b|N_pEcy8VUmmMU29f{)!qIKbyj=nf-L`d=I=}Qch#V$^Ji@p%8Ow}aVg{OQfFawvx$7QXHNf;JlH z>=OGpehiMY6qvQ>r+31?Qu`coulkvHr8sf?eJ%Pq`{!L3HPrR)ZPuzD>pymc)ERd) zzk>rC1G3S8Q*}_1#B7CMSB{eOXyX??K=#C&ZSfa;COcp~#v&$=7`2lg#z$%Z{KSqq zKgR=5D%9d=VNhEG;T6Bn*jciRjl(~1ghpC5?^iE1h+v&EM5%$(W^~Kq7Js%C)S$8R z&&Vf{$L|gr4t5WQ47&JVk;>Q~QqTw0G|KqPrW>W$rJ#h6`dG*ziIh6YM-(w-d_ahz ziCLs8i6~hT(>ah5w;KCCXvRidq*{|KIJHcYAbB!zNskc|{4NutczYczbgqZ0iFCnW zsml~G<0~cJm8Lcg-XClfsxBV7RD|?NwHq6y+E5ESTM>I)D+Wc$hvpJtO?p~5Qg6~9 zF8L;*25*`-T_?5T2C1B>DSuAuR1~&MUa-f+18ELrq4EvMW)^~QU_#>sU6Lx3Bulc`iuXz5gSp3Y5ql*T!p+08KROn z54-e)hzb7ZOlVZE<|Csr2oqhdDSf2{^pipyR3pxkltWD5Xrhq_c-ch|MO=ug1cWpm zMxhs>VGW|2LkntvY%7y7lu$m>N(aX-J(NM=KYzXps?fCt@nx8THwa5*vx}3~4o$rY z*BxwSC8jhr15tEmW{a>O_$W=9?m*V!GNcejK^+%5b-~I6@?q_4h-amS(hB(R;QgIi z@7)vt#baDZC-o(z7T`pVIV}lw*6WnMs_#O1^^Y)E;=#H#PJDH9cD9E2mG~$N zOJnZDfpysHK41$2Dg5Q&H8Ok@V z_BIh+f=y1!p>PG0gIdFbEOK8D?rLZAEcmOuk3p?&%Q!F5>Z*^s~XRzn`uvpVHt>ZE@mpO;urk=<&lRTw~623`?ognS}u(o9b+f>U8&Q* zHFOIPUL7H^`w+1nC@$BhkhvD1FZq#*&biTE-Dzw~Bf~QmN{fp=)*-%inhk1JtrPqq z2ft2NUhO*alS*|4IZDh2zt8*dHjg7Lh;v25p(#0-{`m$}Q_aLdBgWYQ*((?!iM%(O zP1or$Q1aSNTyS~r;i)TBr#^)PG;Px{svgv6AycK!aD2E3VMS;#=(W)MbhrmeU=a%B zBxkAvXh$I*KkPeI%D~Is%3iiUOSGmV12c4V@Ca_9h!|Q`4+g;!iY;HoHy;~8S zTP&zCebpfC=*h{G7O58rCu7&qj3>;U@S@u_Ii7NznOW0+@>!;X6mcj>D36U%-?uVC+#}<{>nL%O`i}W9)Cw89T@#oNG=G+Jk zV^b@YAIMNeh9rndV+>dDZ*#W%5OgnEO7U|aE-_lFCRdtJy^a@Shyy?fq( z{aWznz$$i>+W#7GPQPE^jk{AenMj2fq~38(QEgh#w&@%UynFh&wJ1D3F#p)p9t-Sy zUFYvh+So}t-^F&0yG!qWGPoj1`QVEwqgee&&EIm@kUJ=@!pg~<->?1xvTc+5 zo^6Iz10A<&2y(3g{B*yNx_kzG=iI04&?am-o&V*u#7?L2Pn(S*bzyN%0~bttxu=i{ z5?Q>nT*OKhVjsy=!A=f@dYP1AmF;hj+ROj>0VTT#EIqgz{6XYBaaiF; znaq2f3oN}K=7uCABu*5kDr4ot6K-)akRlD_ii?7h{ID@t@^g1ll8XRp@~|-04R+n8 z#mI4_+GPPmma|1KPW94PkjTAftZLWTK4kSZVF@HYZj|v>tWgN#@nu6PCbykcZZj6N6P!OHuA zZdsDXdU;>>#TN1LOvMjxroc*W?IZOsu-|>(&xv6oRa)p7ctGa80!{>MGl%D^IZ*)bj$D|0E`ujBi-XnCoQ6P4EZ2k71eLyR@Ce=u=KIg zH!Ni!$DHqS1}swAzLUjISn}BrRw|YM$eJ`# - /// Currently uses blurring only in Uno. + /// Currently uses blurring only in non-Skia Uno heads. public partial class AcrylicBrush : XamlCompositionBrushBase { /// @@ -116,9 +116,9 @@ public bool AlwaysUseFallback typeof(AcrylicBrush), new FrameworkPropertyMetadata( // Due to the fact that additional subviews are added to acrylic owner views - // on non-WASM platforms, we default to using fallback where not completely safe + // on platforms other than WASM and Skia, we default to using fallback where not completely safe // When this is explicitly set to false, Acrylic will be displayed -#if __WASM__ +#if __WASM__ || __SKIA__ false #else true diff --git a/src/Uno.UI/UI/Xaml/Media/AcrylicBrush/AcrylicBrush.skia.cs b/src/Uno.UI/UI/Xaml/Media/AcrylicBrush/AcrylicBrush.skia.cs new file mode 100644 index 000000000000..65f32910417b --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Media/AcrylicBrush/AcrylicBrush.skia.cs @@ -0,0 +1,560 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Effects; +using Windows.Graphics.Effects; +using Windows.UI.Composition; + +namespace Windows.UI.Xaml.Media +{ + public partial class AcrylicBrush + { + private CompositionSurfaceBrush _noiseBrush; + private CompositionBrush _brush; + private bool _isUsingOpaqueBrush; + private bool _isConnected; + + private const float _blurRadius = 30.0f; + private const float _noiseOpacity = 0.02f; + + protected override void OnConnected() + { + _isConnected = true; + UpdateAcrylicBrush(); + } + + protected override void OnDisconnected() + { + _isConnected = false; + + if (_brush is not null) + { + _brush.Dispose(); + _brush = null; + CompositionBrush = null; + } + + _noiseBrush?.Dispose(); + _noiseBrush = null; + } + + private void UpdateAcrylicBrush() + { + if (_isConnected) + { + //const bool isUsingWindowAcrylic = BackgroundSource() == winrt::AcrylicBackgroundSource::HostBackdrop; + bool shouldUseOpaqueBrush = GetEffectiveTintColor().A == 255; + + // Covers cases where we need a new brush (no animations) + if (_brush is null || // Create brush for the first time + (_isUsingOpaqueBrush != shouldUseOpaqueBrush)) // Recreate the brush with (or without) the opaque tint optimization + { + CreateAcrylicBrush(false /* useCrossFadeEffect */, true /* forceCreateAcrylicBrush */); + } + // Covers cases were we switch between fallback and acrylic (needs animations) + else + { + // TODO: Currently we are doing the same as above because Composition animations aren't implemented yet + CreateAcrylicBrush(false /* useCrossFadeEffect */, true /* forceCreateAcrylicBrush */); + } + } + } + + private void CreateAcrylicBrush(bool useCrossFadeEffect, bool forceCreateAcrylicBrush) + { + Compositor compositor = Window.Current.Compositor; + + //if forceCreateAcrylicBrush=true, _isUsingAcrylicBrush is ignored. + if (forceCreateAcrylicBrush /* || _isUsingAcrylicBrush */) + { + if (!EnsureNoiseBrush()) + { + CreateAcrylicBrush(false, false); + return; + } + + Color tintColor = GetEffectiveTintColor(); + Color luminosityColor = GetEffectiveLuminosityColor(); + + _isUsingOpaqueBrush = tintColor.A == 255; + + var acrylicBrush = CreateAcrylicBrushWorker( + compositor, + /* _isUsingWindowAcrylic */ false, + useCrossFadeEffect, + tintColor, + luminosityColor, + FallbackColor, + _isUsingOpaqueBrush); + + + // Set noise image source + acrylicBrush.SetSourceParameter("Noise", _noiseBrush); + //acrylicBrush.Properties.InsertColor("TintColor.Color", tintColor); + + if (!_isUsingOpaqueBrush) + { + //acrylicBrush.Properties.InsertColor("LuminosityColor.Color", luminosityColor); + } + + if (useCrossFadeEffect) + { + //acrylicBrush.Properties.InsertColor("FallbackColor.Color", FallbackColor); + } + + acrylicBrush.UseBlurPadding = true; + + // Update the AcrylicBrush + _brush = acrylicBrush; + } + else + { + _brush = compositor.CreateColorBrush(FallbackColor); + } + + CompositionBrush = _brush; + } + + private bool EnsureNoiseBrush() + { + if (_noiseBrush is null) + { + Compositor compositor = Window.Current.Compositor; + CompositionSurfaceBrush surfaceBrush = compositor.CreateSurfaceBrush(); + SkiaCompositionSurface surface = new SkiaCompositionSurface(); + using Stream imgStream = GetType().Assembly.GetManifestResourceStream("Uno.UI.Resources.NoiseAsset256x256.png"); + + if (surface.LoadFromStream(256, 256, imgStream).success) + { + surfaceBrush.Surface = surface; + surfaceBrush.Stretch = CompositionStretch.None; + _noiseBrush = surfaceBrush; + + return true; + } + + surfaceBrush.Dispose(); + return false; + } + + return true; + } + + private CompositionEffectBrush CreateAcrylicBrushWorker(Compositor compositor, bool useWindowAcrylic, bool useCrossFadeEffect, Color initialTintColor, Color initialLuminosityColor, Color initialFallbackColor, bool shouldBrushBeOpaque) + { + + var effectFactory = CreateAcrylicBrushCompositionEffectFactory( + compositor, shouldBrushBeOpaque, useWindowAcrylic, useCrossFadeEffect, + initialTintColor, initialLuminosityColor, initialFallbackColor); + + // Create the Comp effect Brush + CompositionEffectBrush acrylicBrush = effectFactory.CreateBrush(); + + // Set the backdrop source + if (!shouldBrushBeOpaque) + { + if (useWindowAcrylic) + { + //var hostBackdropBrush = compositor.CreateHostBackdropBrush(); + var hostBackdropBrush = compositor.CreateBackdropBrush(); // We don't have HostBackdropBrush support yet + acrylicBrush.SetSourceParameter("Backdrop", hostBackdropBrush); + } + else + { + var backdropBrush = compositor.CreateBackdropBrush(); + acrylicBrush.SetSourceParameter("Backdrop", backdropBrush); + } + } + + return acrylicBrush; + } + + private CompositionEffectFactory CreateAcrylicBrushCompositionEffectFactory(Compositor compositor, bool shouldBrushBeOpaque, bool useWindowAcrylic, bool useCrossFadeEffect, Color initialTintColor, Color initialLuminosityColor, Color initialFallbackColor) + { + CompositionEffectFactory effectFactory = null; + + // The part of the effect graph below the noise layer. This is either a semi-transparent tint (common) or an opaque tint (uncommon). + // Opaque tint may be used by apps wishing add the complexity of noise to their brand color, for example. + IGraphicsEffect tintOutput; + + // Tint Color - either used directly or in a Color blend over a blurred backdrop + var tintColorEffect = new ColorSourceEffect(); + tintColorEffect.Name = "TintColor"; + tintColorEffect.Color = initialTintColor; + + List animatedProperties = new() { "TintColor.Color" }; + + if (shouldBrushBeOpaque) + { + tintOutput = tintColorEffect; + } + + else + { + // Load the backdrop in a brush + CompositionEffectSourceParameter backdropEffectSourceParameter = new("Backdrop"); + + // Get a blurred backdrop... + IGraphicsEffectSource blurredSource; + if (useWindowAcrylic) + { + // ...either the shell baked the blur into the backdrop brush, and we use it directly... + blurredSource = backdropEffectSourceParameter; + } + else + { + // ...or we apply the blur ourselves + var gaussianBlurEffect = new GaussianBlurEffect(); + gaussianBlurEffect.Name = "Blur"; + gaussianBlurEffect.BorderMode = EffectBorderMode.Hard; + gaussianBlurEffect.BlurAmount = _blurRadius; + gaussianBlurEffect.Source = backdropEffectSourceParameter; + blurredSource = gaussianBlurEffect; + } + + tintOutput = CombineNoiseWithTintEffect(blurredSource, tintColorEffect, initialLuminosityColor, animatedProperties); + } + + // Create noise with alpha: + CompositionEffectSourceParameter noiseEffectSourceParameter = new("Noise"); + // OpacityEffect applied to wrapped noise + var noiseOpacityEffect = new OpacityEffect(); + noiseOpacityEffect.Name = "NoiseOpacity"; + noiseOpacityEffect.Opacity = _noiseOpacity; + noiseOpacityEffect.Source = noiseEffectSourceParameter; + + // Blend noise on top of tint + var blendEffectOuter = new CompositeEffect(); + blendEffectOuter.Mode = CanvasComposite.SourceOver; + blendEffectOuter.Sources.Add(tintOutput); + blendEffectOuter.Sources.Add(noiseOpacityEffect); + + if (useCrossFadeEffect) + { + // Fallback color + var fallbackColorEffect = new ColorSourceEffect(); + fallbackColorEffect.Name = "FallbackColor"; + fallbackColorEffect.Color = initialFallbackColor; + + // CrossFade with the fallback color. Weight = 0 means full fallback, 1 means full acrylic. + var fadeInOutEffect = new CrossFadeEffect(); + fadeInOutEffect.Name = "FadeInOut"; + fadeInOutEffect.Source1 = fallbackColorEffect; + fadeInOutEffect.Source2 = blendEffectOuter; + fadeInOutEffect.CrossFade = 1.0f; + + animatedProperties.Add("FallbackColor.Color"); + animatedProperties.Add("FadeInOut.CrossFade"); + effectFactory = compositor.CreateEffectFactory(fadeInOutEffect, animatedProperties); + } + else + { + effectFactory = compositor.CreateEffectFactory(blendEffectOuter, animatedProperties); + } + + return effectFactory; + } + + private IGraphicsEffect CombineNoiseWithTintEffect(IGraphicsEffectSource blurredSource, ColorSourceEffect tintColorEffect, Color initialLuminosityColor, IList animatedProperties = null) + { + animatedProperties?.Add("LuminosityColor.Color"); + + // Apply luminosity: + + // Luminosity Color + var luminosityColorEffect = new ColorSourceEffect(); + luminosityColorEffect.Name = "LuminosityColor"; + luminosityColorEffect.Color = initialLuminosityColor; + + // Luminosity blend + var luminosityBlendEffect = new BlendEffect(); + // NOTE: There is currently a bug where the names of BlendEffectMode::Luminosity and BlendEffectMode::Color are flipped. + // This should be changed to Luminosity when/if the bug is fixed. + luminosityBlendEffect.Mode = BlendEffectMode.Color; + luminosityBlendEffect.Background = blurredSource; + luminosityBlendEffect.Foreground = luminosityColorEffect; + + // Apply tint: + + // Color blend + var colorBlendEffect = new BlendEffect(); + // NOTE: There is currently a bug where the names of BlendEffectMode::Luminosity and BlendEffectMode::Color are flipped. + // This should be changed to Color when/if the bug is fixed. + colorBlendEffect.Mode = BlendEffectMode.Luminosity; + colorBlendEffect.Background = luminosityBlendEffect; + colorBlendEffect.Foreground = tintColorEffect; + + return colorBlendEffect; + } + + private Color GetEffectiveLuminosityColor() + { + Color tintColor = TintColor; + + // Purposely leaving out tint opacity modifier here because GetLuminosityColor needs the *original* tint opacity set by the user. + tintColor.A = (byte)Math.Round(tintColor.A * TintOpacity); + + return GetLuminosityColor(tintColor, TintLuminosityOpacity); + } + + private Color GetLuminosityColor(Color tintColor, double? luminosityOpacity) + { + // If luminosity opacity is specified, just use the values as is + if (luminosityOpacity is not null) + { + return tintColor with { A = (byte)(Math.Clamp(luminosityOpacity.Value, 0.0, 1.0) * 255.0f) }; + } + else + { + // To create the Luminosity blend input color without luminosity opacity, + // we're taking the TintColor input, converting to HSV, and clamping the V between these values + const double minHsvV = 0.125; + const double maxHsvV = 0.965; + + Hsv hsvTintColor = RgbToHsv(tintColor); + + var clampedHsvV = Math.Clamp(hsvTintColor.V, minHsvV, maxHsvV); + + Hsv hsvLuminosityColor = new Hsv(hsvTintColor.H, hsvTintColor.S, clampedHsvV); + Rgb rgbLuminosityColor = HsvToRgb(hsvLuminosityColor); + + // Now figure out luminosity opacity + // Map original *tint* opacity to this range + const double minLuminosityOpacity = 0.15; + const double maxLuminosityOpacity = 1.03; + + double luminosityOpacityRangeMax = maxLuminosityOpacity - minLuminosityOpacity; + double mappedTintOpacity = ((tintColor.A / 255.0) * luminosityOpacityRangeMax) + minLuminosityOpacity; + + // Finally, combine the luminosity opacity and the HsvV-clamped tint color + return ((Color)rgbLuminosityColor) with { A = (byte)(Math.Min(mappedTintOpacity, 1.0) * 255.0f) }; + } + + } + + private Color GetEffectiveTintColor() + { + Color tintColor = TintColor; + + // Update tintColor's alpha with the combined opacity value + // If LuminosityOpacity was specified, we don't intervene into users parameters + if (TintLuminosityOpacity is not null) + { + tintColor.A = (byte)Math.Round(tintColor.A * TintOpacity); + } + else + { + double tintOpacityModifier = GetTintOpacityModifier(tintColor); + tintColor.A = (byte)Math.Round(tintColor.A * TintOpacity * tintOpacityModifier); + } + + return tintColor; + } + + private double GetTintOpacityModifier(Color tintColor) + { + const double midPoint = 0.50; + + const double whiteMaxOpacity = 0.45; + const double midPointMaxOpacity = 0.90; + const double blackMaxOpacity = 0.85; + + Hsv hsv = RgbToHsv(tintColor); + + double opacityModifier = midPointMaxOpacity; + + if (hsv.V != midPoint) + { + // Determine maximum suppression amount + double lowestMaxOpacity = midPointMaxOpacity; + double maxDeviation = midPoint; + + if (hsv.V > midPoint) + { + lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV) + maxDeviation = 1 - maxDeviation; + } + else if (hsv.V < midPoint) + { + lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV) + } + + double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity; + + // Determine normalized deviation from the midpoint + double deviation = Math.Abs(hsv.V - midPoint); + double normalizedDeviation = deviation / maxDeviation; + + // If we have saturation, reduce opacity suppression to allow that color to come through more + if (hsv.S > 0) + { + // Dampen opacity suppression based on how much saturation there is + maxOpacitySuppression *= Math.Max(1 - (hsv.S * 2), 0.0); + } + + double opacitySuppression = maxOpacitySuppression * normalizedDeviation; + + opacityModifier = midPointMaxOpacity - opacitySuppression; + } + + return opacityModifier; + } + + #region ColorConversion + Hsv RgbToHsv(Rgb rgb) + { + double hue = 0; + double saturation = 0; + double value = 0; + + double max = rgb.R >= rgb.G ? (rgb.R >= rgb.B ? rgb.R : rgb.B) : (rgb.G >= rgb.B ? rgb.G : rgb.B); + double min = rgb.R <= rgb.G ? (rgb.R <= rgb.B ? rgb.R : rgb.B) : (rgb.G <= rgb.B ? rgb.G : rgb.B); + value = max; + + double chroma = max - min; + + if (chroma == 0) + { + hue = 0.0; + saturation = 0.0; + } + else + { + if (rgb.R == max) + { + hue = 60 * (rgb.G - rgb.B) / chroma; + } + else if (rgb.G == max) + { + hue = 120 + 60 * (rgb.B - rgb.R) / chroma; + } + else + { + hue = 240 + 60 * (rgb.R - rgb.G) / chroma; + } + + if (hue < 0.0) + { + hue += 360.0; + } + + saturation = chroma / value; + } + + return new Hsv(hue, saturation, value); + } + + Rgb HsvToRgb(Hsv hsv) + { + double hue = hsv.H; + double saturation = hsv.S; + double value = hsv.V; + + while (hue >= 360.0) + { + hue -= 360.0; + } + + while (hue < 0.0) + { + hue += 360.0; + } + + // We similarly clamp saturation and value between 0 and 1. + saturation = saturation < 0.0 ? 0.0 : saturation; + saturation = saturation > 1.0 ? 1.0 : saturation; + + value = value < 0.0 ? 0.0 : value; + value = value > 1.0 ? 1.0 : value; + + double chroma = saturation * value; + double min = value - chroma; + + if (chroma == 0) + { + return new Rgb(min, min, min); + } + + int sextant = (int)(hue / 60d); + double intermediateColorPercentage = hue / 60d - sextant; + double max = chroma + min; + + double r = 0; + double g = 0; + double b = 0; + + switch (sextant) + { + case 0: + r = max; + g = min + chroma * intermediateColorPercentage; + b = min; + break; + case 1: + r = min + chroma * (1 - intermediateColorPercentage); + g = max; + b = min; + break; + case 2: + r = min; + g = max; + b = min + chroma * intermediateColorPercentage; + break; + case 3: + r = min; + g = min + chroma * (1 - intermediateColorPercentage); + b = max; + break; + case 4: + r = min + chroma * intermediateColorPercentage; + g = min; + b = max; + break; + case 5: + r = max; + g = min; + b = min + chroma * (1 - intermediateColorPercentage); + break; + } + + return new Rgb(r, g, b); + } + + private struct Rgb + { + public double R; + public double G; + public double B; + + public Rgb(double r, double g, double b) + { + R = r; + G = g; + B = b; + } + + public static implicit operator Rgb(Color color) => new(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f); + public static implicit operator Color(Rgb color) => new(255, (byte)(color.R * 255.0f), (byte)(color.G * 255.0f), (byte)(color.B * 255.0f)); + } + + private struct Hsv + { + public double H; + public double S; + public double V; + + public Hsv(double h, double s, double v) + { + H = h; + S = s; + V = v; + } + } + #endregion + } +} diff --git a/src/Uno.UI/UI/Xaml/Media/Brush.skia.cs b/src/Uno.UI/UI/Xaml/Media/Brush.skia.cs index 08107b828c44..11e97ace9d2c 100644 --- a/src/Uno.UI/UI/Xaml/Media/Brush.skia.cs +++ b/src/Uno.UI/UI/Xaml/Media/Brush.skia.cs @@ -225,7 +225,11 @@ private static IDisposable AssignAndObserveXamlCompositionBrush(XamlCompositionB { var disposables = new CompositeDisposable(); - brush.OnConnectedInternal(); + if (brush.CompositionBrush is null) + { + brush.OnConnectedInternal(); + } + var compositionBrush = brush.CompositionBrush ?? compositor.CreateColorBrush(brush.FallbackColorWithOpacity); brush.RegisterDisposablePropertyChangedCallback( diff --git a/src/Uno.UI/Uno.UI.Skia.csproj b/src/Uno.UI/Uno.UI.Skia.csproj index d9dd56e60a72..a688fb34aebb 100644 --- a/src/Uno.UI/Uno.UI.Skia.csproj +++ b/src/Uno.UI/Uno.UI.Skia.csproj @@ -50,6 +50,10 @@ + + + +