From 90e69092cebfe727da9f731c95c73ef65267964b Mon Sep 17 00:00:00 2001 From: Arlo Date: Thu, 13 Jul 2023 15:44:29 -0500 Subject: [PATCH] Ported source code for CommunityToolkit.WinUI.Media (#113) * Initial port of CommunityToolkit.WinUI.Media to 8.x * Unport Geometry * Add missing file header * Updated tooling submodule, suppress CS0067 * Removed RadialGradientBrush * Update submodule, testing hotfixes * Update tooling to latest main * Fixed null handling * Fixed version number * Use parameter validation for FontIconExtension * Add ThrowIfNull polyfill to extensions * Move internal ArgumentNullExceptionExtensions from Animations to Extensions * Expose internals to CommunityToolkit.WinUI.Media --- ...KeyFrameAnimationBuilder{T}.Composition.cs | 4 +- .../CommunityToolkit.WinUI.Animations.csproj | 3 + .../Abstract/Animation{TValue,TKeyFrame}.cs | 4 +- .../CustomAnimation{TValue,TKeyFrame}.cs | 4 +- .../ImplicitAnimation{TValue,TKeyFrame}.cs | 2 + .../src/Xaml/Default/ClipAnimation.cs | 4 +- .../src/ArgumentNullExceptionExtensions.cs | 65 ++ .../CommunityToolkit.WinUI.Extensions.csproj | 10 + .../src/Markup/FontIconExtension.cs | 2 + components/Media/OpenSolution.bat | 3 + components/Media/samples/Assets/icon.png | Bin 0 -> 6192 bytes components/Media/samples/Dependencies.props | 31 + components/Media/samples/Media.Samples.csproj | 8 + .../Media/src/AdditionalAssemblyInfo.cs | 13 + ...fectAnimation{TEffect,TValue,TKeyFrame}.cs | 82 +++ .../src/Animations/BlurEffectAnimation.cs | 22 + .../src/Animations/ColorEffectAnimation.cs | 23 + .../Animations/CrossFadeEffectAnimation.cs | 22 + .../src/Animations/ExposureEffectAnimation.cs | 22 + .../Animations/HueRotationEffectAnimation.cs | 22 + .../src/Animations/OpacityEffectAnimation.cs | 22 + .../Animations/SaturationEffectAnimation.cs | 22 + .../src/Animations/SepiaEffectAnimation.cs | 22 + components/Media/src/Brushes/AcrylicBrush.cs | 222 ++++++ .../Media/src/Brushes/BackdropBlurBrush.cs | 64 ++ .../src/Brushes/BackdropGammaTransferBrush.cs | 406 ++++++++++ .../Media/src/Brushes/BackdropInvertBrush.cs | 21 + .../src/Brushes/BackdropSaturationBrush.cs | 75 ++ .../Media/src/Brushes/BackdropSepiaBrush.cs | 75 ++ .../Base/XamlCompositionEffectBrushBase.cs | 143 ++++ .../Media/src/Brushes/CanvasBrushBase.cs | 149 ++++ .../Media/src/Brushes/ImageBlendBrush.cs | 214 ++++++ components/Media/src/Brushes/PipelineBrush.cs | 69 ++ components/Media/src/Brushes/TilesBrush.cs | 75 ++ .../Media/src/Brushes/XamlCompositionBrush.cs | 93 +++ .../src/CommunityToolkit.WinUI.Media.csproj | 17 + components/Media/src/Dependencies.props | 22 + .../Abstract/ImageSourceBaseExtension.cs | 29 + .../src/Effects/Abstract/PipelineEffect.cs | 41 ++ components/Media/src/Effects/BlendEffect.cs | 66 ++ components/Media/src/Effects/BlurEffect.cs | 47 ++ .../Media/src/Effects/CrossFadeEffect.cs | 82 +++ .../Media/src/Effects/ExposureEffect.cs | 47 ++ .../Extensions/AcrylicSourceExtension.cs | 69 ++ .../Extensions/BackdropSourceExtension.cs | 33 + .../Extensions/ImageSourceExtension.cs | 20 + .../Extensions/SolidColorSourceExtension.cs | 26 + .../Effects/Extensions/TileSourceExtension.cs | 21 + .../Media/src/Effects/GrayscaleEffect.cs | 20 + .../Media/src/Effects/HueRotationEffect.cs | 41 ++ .../src/Effects/Interfaces/IPipelineEffect.cs | 39 + components/Media/src/Effects/InvertEffect.cs | 20 + .../src/Effects/LuminanceToAlphaEffect.cs | 20 + components/Media/src/Effects/OpacityEffect.cs | 47 ++ .../Media/src/Effects/SaturationEffect.cs | 47 ++ components/Media/src/Effects/SepiaEffect.cs | 47 ++ components/Media/src/Effects/ShadeEffect.cs | 36 + .../src/Effects/TemperatureAndTintEffect.cs | 42 ++ components/Media/src/Effects/TintEffect.cs | 42 ++ components/Media/src/Enums/AlphaMode.cs | 21 + components/Media/src/Enums/CacheMode.cs | 26 + components/Media/src/Enums/DpiMode.cs | 31 + components/Media/src/Enums/ImageBlendMode.cs | 61 ++ .../Media/src/Enums/InnerContentClipMode.cs | 32 + components/Media/src/Enums/Placement.cs | 23 + .../GenericExtensions.cs | 53 ++ .../System.Threading.Tasks/AsyncMutex.cs | 58 ++ .../src/Extensions/System/UriExtensions.cs | 47 ++ .../src/Extensions/UIElementExtensions.cs | 65 ++ .../CompositionObjectExtensions.cs | 91 +++ .../CompositionObjectCache{TKey,TValue}.cs | 76 ++ .../Cache/CompositionObjectCache{T}.cs | 50 ++ .../src/Helpers/SurfaceLoader.Instance.cs | 229 ++++++ components/Media/src/Helpers/SurfaceLoader.cs | 150 ++++ components/Media/src/MultiTarget.props | 9 + .../Media/src/Pipelines/BrushProvider.cs | 67 ++ .../PipelineBuilder.Effects.Internals.cs | 218 ++++++ .../src/Pipelines/PipelineBuilder.Effects.cs | 693 ++++++++++++++++++ .../PipelineBuilder.Initialization.cs | 327 +++++++++ .../src/Pipelines/PipelineBuilder.Merge.cs | 160 ++++ .../src/Pipelines/PipelineBuilder.Prebuilt.cs | 214 ++++++ .../Media/src/Pipelines/PipelineBuilder.cs | 224 ++++++ .../src/Visuals/AttachedVisualFactoryBase.cs | 24 + .../src/Visuals/PipelineVisualFactory.cs | 79 ++ .../src/Visuals/PipelineVisualFactoryBase.cs | 37 + components/Media/tests/Media.Tests.projitems | 11 + components/Media/tests/Media.Tests.shproj | 13 + tooling | 2 +- 88 files changed, 6022 insertions(+), 8 deletions(-) create mode 100644 components/Extensions/src/ArgumentNullExceptionExtensions.cs create mode 100644 components/Media/OpenSolution.bat create mode 100644 components/Media/samples/Assets/icon.png create mode 100644 components/Media/samples/Dependencies.props create mode 100644 components/Media/samples/Media.Samples.csproj create mode 100644 components/Media/src/AdditionalAssemblyInfo.cs create mode 100644 components/Media/src/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs create mode 100644 components/Media/src/Animations/BlurEffectAnimation.cs create mode 100644 components/Media/src/Animations/ColorEffectAnimation.cs create mode 100644 components/Media/src/Animations/CrossFadeEffectAnimation.cs create mode 100644 components/Media/src/Animations/ExposureEffectAnimation.cs create mode 100644 components/Media/src/Animations/HueRotationEffectAnimation.cs create mode 100644 components/Media/src/Animations/OpacityEffectAnimation.cs create mode 100644 components/Media/src/Animations/SaturationEffectAnimation.cs create mode 100644 components/Media/src/Animations/SepiaEffectAnimation.cs create mode 100644 components/Media/src/Brushes/AcrylicBrush.cs create mode 100644 components/Media/src/Brushes/BackdropBlurBrush.cs create mode 100644 components/Media/src/Brushes/BackdropGammaTransferBrush.cs create mode 100644 components/Media/src/Brushes/BackdropInvertBrush.cs create mode 100644 components/Media/src/Brushes/BackdropSaturationBrush.cs create mode 100644 components/Media/src/Brushes/BackdropSepiaBrush.cs create mode 100644 components/Media/src/Brushes/Base/XamlCompositionEffectBrushBase.cs create mode 100644 components/Media/src/Brushes/CanvasBrushBase.cs create mode 100644 components/Media/src/Brushes/ImageBlendBrush.cs create mode 100644 components/Media/src/Brushes/PipelineBrush.cs create mode 100644 components/Media/src/Brushes/TilesBrush.cs create mode 100644 components/Media/src/Brushes/XamlCompositionBrush.cs create mode 100644 components/Media/src/CommunityToolkit.WinUI.Media.csproj create mode 100644 components/Media/src/Dependencies.props create mode 100644 components/Media/src/Effects/Abstract/ImageSourceBaseExtension.cs create mode 100644 components/Media/src/Effects/Abstract/PipelineEffect.cs create mode 100644 components/Media/src/Effects/BlendEffect.cs create mode 100644 components/Media/src/Effects/BlurEffect.cs create mode 100644 components/Media/src/Effects/CrossFadeEffect.cs create mode 100644 components/Media/src/Effects/ExposureEffect.cs create mode 100644 components/Media/src/Effects/Extensions/AcrylicSourceExtension.cs create mode 100644 components/Media/src/Effects/Extensions/BackdropSourceExtension.cs create mode 100644 components/Media/src/Effects/Extensions/ImageSourceExtension.cs create mode 100644 components/Media/src/Effects/Extensions/SolidColorSourceExtension.cs create mode 100644 components/Media/src/Effects/Extensions/TileSourceExtension.cs create mode 100644 components/Media/src/Effects/GrayscaleEffect.cs create mode 100644 components/Media/src/Effects/HueRotationEffect.cs create mode 100644 components/Media/src/Effects/Interfaces/IPipelineEffect.cs create mode 100644 components/Media/src/Effects/InvertEffect.cs create mode 100644 components/Media/src/Effects/LuminanceToAlphaEffect.cs create mode 100644 components/Media/src/Effects/OpacityEffect.cs create mode 100644 components/Media/src/Effects/SaturationEffect.cs create mode 100644 components/Media/src/Effects/SepiaEffect.cs create mode 100644 components/Media/src/Effects/ShadeEffect.cs create mode 100644 components/Media/src/Effects/TemperatureAndTintEffect.cs create mode 100644 components/Media/src/Effects/TintEffect.cs create mode 100644 components/Media/src/Enums/AlphaMode.cs create mode 100644 components/Media/src/Enums/CacheMode.cs create mode 100644 components/Media/src/Enums/DpiMode.cs create mode 100644 components/Media/src/Enums/ImageBlendMode.cs create mode 100644 components/Media/src/Enums/InnerContentClipMode.cs create mode 100644 components/Media/src/Enums/Placement.cs create mode 100644 components/Media/src/Extensions/System.Collections.Generic/GenericExtensions.cs create mode 100644 components/Media/src/Extensions/System.Threading.Tasks/AsyncMutex.cs create mode 100644 components/Media/src/Extensions/System/UriExtensions.cs create mode 100644 components/Media/src/Extensions/UIElementExtensions.cs create mode 100644 components/Media/src/Extensions/Windows.UI.Composition/CompositionObjectExtensions.cs create mode 100644 components/Media/src/Helpers/Cache/CompositionObjectCache{TKey,TValue}.cs create mode 100644 components/Media/src/Helpers/Cache/CompositionObjectCache{T}.cs create mode 100644 components/Media/src/Helpers/SurfaceLoader.Instance.cs create mode 100644 components/Media/src/Helpers/SurfaceLoader.cs create mode 100644 components/Media/src/MultiTarget.props create mode 100644 components/Media/src/Pipelines/BrushProvider.cs create mode 100644 components/Media/src/Pipelines/PipelineBuilder.Effects.Internals.cs create mode 100644 components/Media/src/Pipelines/PipelineBuilder.Effects.cs create mode 100644 components/Media/src/Pipelines/PipelineBuilder.Initialization.cs create mode 100644 components/Media/src/Pipelines/PipelineBuilder.Merge.cs create mode 100644 components/Media/src/Pipelines/PipelineBuilder.Prebuilt.cs create mode 100644 components/Media/src/Pipelines/PipelineBuilder.cs create mode 100644 components/Media/src/Visuals/AttachedVisualFactoryBase.cs create mode 100644 components/Media/src/Visuals/PipelineVisualFactory.cs create mode 100644 components/Media/src/Visuals/PipelineVisualFactoryBase.cs create mode 100644 components/Media/tests/Media.Tests.projitems create mode 100644 components/Media/tests/Media.Tests.shproj diff --git a/components/Animations/src/Builders/NormalizedKeyFrameAnimationBuilder{T}.Composition.cs b/components/Animations/src/Builders/NormalizedKeyFrameAnimationBuilder{T}.Composition.cs index b09c3287..5fc551d7 100644 --- a/components/Animations/src/Builders/NormalizedKeyFrameAnimationBuilder{T}.Composition.cs +++ b/components/Animations/src/Builders/NormalizedKeyFrameAnimationBuilder{T}.Composition.cs @@ -2,9 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if WINAPPSDK +#if WINUI3 using Microsoft.UI.Composition; -#else +#elif WINUI2 using Windows.UI.Composition; #endif diff --git a/components/Animations/src/CommunityToolkit.WinUI.Animations.csproj b/components/Animations/src/CommunityToolkit.WinUI.Animations.csproj index 196bf0ef..fde8d32e 100644 --- a/components/Animations/src/CommunityToolkit.WinUI.Animations.csproj +++ b/components/Animations/src/CommunityToolkit.WinUI.Animations.csproj @@ -10,7 +10,10 @@ + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/components/Animations/src/Xaml/Abstract/Animation{TValue,TKeyFrame}.cs b/components/Animations/src/Xaml/Abstract/Animation{TValue,TKeyFrame}.cs index 667f3e64..cc98af6e 100644 --- a/components/Animations/src/Xaml/Abstract/Animation{TValue,TKeyFrame}.cs +++ b/components/Animations/src/Xaml/Abstract/Animation{TValue,TKeyFrame}.cs @@ -86,11 +86,13 @@ public IList> KeyFrames /// /// Gets the explicit target for the animation. This is the primary target property that is animated. /// - protected abstract string ExplicitTarget { get; } + protected abstract string? ExplicitTarget { get; } /// public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeSpan? delayHint, TimeSpan? durationHint, EasingType? easingTypeHint, EasingMode? easingModeHint) { + default(ArgumentNullException).ThrowIfNull(ExplicitTarget); + return builder.NormalizedKeyFrames This, EasingType? EasingTypeHint, EasingMode? EasingModeHint)>( property: ExplicitTarget, state: (this, easingTypeHint, easingModeHint), diff --git a/components/Animations/src/Xaml/Abstract/CustomAnimation{TValue,TKeyFrame}.cs b/components/Animations/src/Xaml/Abstract/CustomAnimation{TValue,TKeyFrame}.cs index ef1bef0b..dde31d2b 100644 --- a/components/Animations/src/Xaml/Abstract/CustomAnimation{TValue,TKeyFrame}.cs +++ b/components/Animations/src/Xaml/Abstract/CustomAnimation{TValue,TKeyFrame}.cs @@ -34,11 +34,13 @@ public abstract class CustomAnimation : ImplicitAnimation - protected override string ExplicitTarget => Target!; + protected override string? ExplicitTarget => Target; /// public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeSpan? delayHint, TimeSpan? durationHint, EasingType? easingTypeHint, EasingMode? easingModeHint) { + default(ArgumentNullException).ThrowIfNull(ExplicitTarget); + return builder.NormalizedKeyFrames This, EasingType? EasingTypeHint, EasingMode? EasingModeHint)>( property: ExplicitTarget, state: (this, easingTypeHint, easingModeHint), diff --git a/components/Animations/src/Xaml/Abstract/ImplicitAnimation{TValue,TKeyFrame}.cs b/components/Animations/src/Xaml/Abstract/ImplicitAnimation{TValue,TKeyFrame}.cs index 2380e79c..f5c1fb77 100644 --- a/components/Animations/src/Xaml/Abstract/ImplicitAnimation{TValue,TKeyFrame}.cs +++ b/components/Animations/src/Xaml/Abstract/ImplicitAnimation{TValue,TKeyFrame}.cs @@ -46,6 +46,8 @@ protected ImplicitAnimation() /// public CompositionAnimation GetAnimation(UIElement element, out string? target) { + default(ArgumentNullException).ThrowIfNull(ExplicitTarget); + NormalizedKeyFrameAnimationBuilder.Composition builder = new( ExplicitTarget, Delay ?? DefaultDelay, diff --git a/components/Animations/src/Xaml/Default/ClipAnimation.cs b/components/Animations/src/Xaml/Default/ClipAnimation.cs index 69cb97a7..9f8330ea 100644 --- a/components/Animations/src/Xaml/Default/ClipAnimation.cs +++ b/components/Animations/src/Xaml/Default/ClipAnimation.cs @@ -12,9 +12,7 @@ namespace CommunityToolkit.WinUI.Animations; public sealed class ClipAnimation : Animation { /// -#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations - protected override string ExplicitTarget => throw new NotImplementedException(); -#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations + protected override string? ExplicitTarget => null; /// public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeSpan? delayHint, TimeSpan? durationHint, EasingType? easingTypeHint, EasingMode? easingModeHint) diff --git a/components/Extensions/src/ArgumentNullExceptionExtensions.cs b/components/Extensions/src/ArgumentNullExceptionExtensions.cs new file mode 100644 index 00000000..545654bf --- /dev/null +++ b/components/Extensions/src/ArgumentNullExceptionExtensions.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System; + +/// +/// Throw helper extensions for . +/// +internal static class ArgumentNullExceptionExtensions +{ + /// + /// Throws an for a given parameter name. + /// + /// Dummy value to invoke the extension upon (always pass . + /// The name of the parameter to report in the exception. + /// Thrown with . + [DoesNotReturn] + public static void Throw(this ArgumentNullException? _, string? parameterName) + { + throw new ArgumentNullException(parameterName); + } + + /// + /// Throws an if is . + /// + /// Dummy value to invoke the extension upon (always pass . + /// The reference type argument to validate as non-. + /// The name of the parameter with which corresponds. + /// Thrown if is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfNull(this ArgumentNullException? _, [NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? parameterName = null) + { + if (argument is null) + { + Throw(parameterName); + } + } + + /// + /// Throws an if is . + /// + /// Dummy value to invoke the extension upon (always pass . + /// The pointer argument to validate as non-. + /// The name of the parameter with which corresponds. + /// Thrown if is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void ThrowIfNull(this ArgumentNullException? _, [NotNull] void* argument, [CallerArgumentExpression(nameof(argument))] string? parameterName = null) + { + if (argument is null) + { + Throw(parameterName); + } + } + + /// + [DoesNotReturn] + private static void Throw(string? parameterName) + { + throw new ArgumentNullException(parameterName); + } +} diff --git a/components/Extensions/src/CommunityToolkit.WinUI.Extensions.csproj b/components/Extensions/src/CommunityToolkit.WinUI.Extensions.csproj index dceba6eb..e4df1fff 100644 --- a/components/Extensions/src/CommunityToolkit.WinUI.Extensions.csproj +++ b/components/Extensions/src/CommunityToolkit.WinUI.Extensions.csproj @@ -6,11 +6,21 @@ CommunityToolkit.WinUI.ExtensionsRns + true + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/components/Extensions/src/Markup/FontIconExtension.cs b/components/Extensions/src/Markup/FontIconExtension.cs index 4afac400..4a090833 100644 --- a/components/Extensions/src/Markup/FontIconExtension.cs +++ b/components/Extensions/src/Markup/FontIconExtension.cs @@ -23,6 +23,8 @@ public class FontIconExtension : TextIconExtension /// protected override object ProvideValue() { + default(ArgumentNullException).ThrowIfNull(Glyph); + var fontIcon = new FontIcon { Glyph = Glyph, diff --git a/components/Media/OpenSolution.bat b/components/Media/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/Media/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/Media/samples/Assets/icon.png b/components/Media/samples/Assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5f574ceca233597ecc5d775fd2f516c0d9685cfa GIT binary patch literal 6192 zcmV-07|-X4P)(A#0FSU0V~+BLKO>0TE|I) z+7v-aRYc9N|<-MF- zi1b%r@3F{l^-$8!0R-@AuNN68cbxCgtQtH~7r^fAZ0@y8>k33}5Dv?5~4wfAB#-0ASbA8OC7v zdvB0BF+HH3LzI&{1NSS$#1$BHAZ^$ImXFJa&|Ht|TD5@H>%^+bwcx_$VDO2p zAaz>-D&y|21)R8kV4+z03WI%ydM}-j-7mfjOROZC6tq#GA7cP2XmPy#5KOZbFFF}0 zr3Ka&NUurFCG<@*2IF)y2RX4cOt$+&PKovnH#fh$6^|io=hR&>XIoJJJV);FqQA5o zLW5>th!&1EfZj1V_(VC#W#`8^R2dX&5+8ZE34m>w*KxJuRz^CAe430IF_=m@M;pqm zy8@&K3w4$LKrL0l2OuvS3}*P%@NyPL~F(Dl+_< zxE_IpRuhh~AociWTqVbp)MpH~EQ+x;SqKRF;bDj9$``VkRBAOE;PDNO*!l{` zLQ(o5@|0LA)8l-i)a$ej4mv_*J9c1UnpjR1VkUsw!Yj~@)paq4%sdfRc?+9(j7Y#m z>=+@tw$QndMVh1{Sz$D0CaM_y;uFrqY@nixtL)orhC`$z6t{RSQ`v9danJrO0wwZW*r)G4nrFBGDzsLFQJ^Q+qY}aE}mInA2zy2XCo0~lF ziTsYzYzmtVWx5JJR}<+PryeS`6qsxRUHJf(7e~Fp_a+DZ3EMGvmCx(MX)Ca>^R??a zePE~&@Cp*a;+PNSG1Iss7<4u-a@*w+p^-98h$VB>mxPYUcc916mka;zale4o#uit^(5Kx1N>kkR zS72rT+%{IREFcce-&5%`&nJvK5NPL8L!>P$+~|&o8CaEC$A$wIBT$R&Ls-fF9aE z4<~--SK;nw{~OM|!v}Rk^C@V~He9ou-FX5a2dNG~xMx|et;(=jKnAAHGsko#Er7ZjJR#Gkpl|r@6XVkpJ(l14I(muTv-~!iTe)Y^1{Hlh65-#WXzcx z-H_~k$6o`IFX~2oDu*P0MlviMf*ywMruGzRBZFBOoQgr}PGgYTv?|_SplYp~PM(4>o+p}HqH|>d?l}Ge zui%AD-G>>ZllLEjWsV3om>9JcBtvxu=v1%XtJK3gvLbFiv}K63_LnMXLiy37n6 zCi7n7uo7_2zB5FcnMWGTdIU6*AeuBOzQ{oNPbUKj^fdvZK)o5b6`6jcVpL7WX(_>% z8IGAr3((Cj;`-(y-F{3!x|qiAFl)rA2M)u^98X%pP3t&|m=aktn5*|M>2dVCCW!(J zGM-vJpbj~}apu6;cbQEydx?g8&~Rv=2ZJDd6mHR)Sbf5&7zEz%4)kTYOPDAhYc&2U zs5(D{!3c_ErnyC2+n8r9_;+PeyhM|pf9R)4K!Om^2p_i>Rhw)qQTE93%gn%2M-TFK zM#-GDH!r~*Pkx`)-WwClEHRsBd8#8dAeO~KOAAKdCRvlf5cWU67K5O%5foiP3x^^e zWar`9>D;TqL9I^yQf!IKr8^b zk@JPfjAohx%$yG^b81+Fe-M8|1<0OakU>l)Xr%_BmObW>mjp#kA=Rv90pT@JfHnm{;9DalJ&BpK z;%ONvMK#XBFpyMzl&mOgl7$=bO0Th*yzTLCFq1C0K8Pj_@#F&sQ8Y$DGV#!CcY4*B zW1$($`97?_Ct@)E&1Bguqb zI<_>Q3oBa%=ncu7$&?{hvKY&Iqz3g|X+s|MdF}prv@ueUz(5*|86>U|7^JgT7U1^J zeN)Y3T$`OSp8BnipgCl4=3LgW5^!*Kfvb7RRwGYQ*rsT3!4)oJFv6;NZOg<10hnia zd6A8BfZYKKI#e(cFu%SYl(kqEp7CBY8F?KC;f&oba|J(OO_8?6jDW;t$>+Y+YfT7a zTISrL%$Z>mxPYQLQspOUbR1Q4G6g#o!X$>FQ;eF8$(#g4@jaCf0^Mcw{BEo7#bhbD zSQL{mis>^pY$C0&Es;YFJm1#z>qxd?$IWF4A=gZ=eJ~Hd|CKY;&pP(?2VeaGSt?(U zeh}YgXg;eIM@zC;nWRFaU%?_DYrd~xKyFt^cU5tkhMDJZvTvAfp}0y%YV1v>DL*3o zO7mPwlq@=xiOgwW*@tVULpSY(V-Nol_VvPkKDK%*EZ?}qrQTSbr>suvSx_&C%!@&S_qTTPgq8$PDQ0)+%chXASF z-B@O=Ir-?vVP)TJf>T=*Y$o@O@x-Ga<4)noI<~2bfG)@$>)w8WcxXw&e9~i(i=~H> zTOk+Fz|}%F+m;a&`AcdR3`TFWlasQ`it0X29UlfmlK*87wv`JSdZ6e*wkaC0EL7}>_ zzbim4y3PRXiFAyuXlJe!a4>^dFxnXI_e7h(qcF}@=X`SY0JSZPH5TN%j(rziJYV3R z$GIcOwN?oC3N!8N58R9g7Z{+jfxVTgF;2mxU_z7PLzK?{2+p`zft}Mab z$G*+GCDj51NbIFL{e`NV|M*t2Pp9X7EGf-$qtW<`dRTfr^p&1(yoh? zrm$3b;FXrJI>|!Ingc<739GxGI>UwMn52gc(k5kpB=r@HM!ZiZKyg{ZX7c{y-+@-R zCHsB#(>H0&CQPzNXCl?Q08`HcIqfTO$r#FPGo6!e9)vXZK=>HQR)=BG4#08(HEB~H zfE9t|>~o5l6Ag9&$HMB2^mDYz#}V&}aI?!*60OHQvQ`N*X?0LL zw;vD8Y13S_NL_NN&b(d4+#QIG$oxACm{O$O?4{SJJE3*cFS2R%8CjFaV~CqH}LHOyZr@ zzq@xoO1KG=s4C^IBE+Iz^8T_$7)IIR8~xUVE4ezxyom~O zfHRp?=G^;*S9ISBx!HvZ&e(^F=RGn;=2_C=dGR-v0zS~gxN%Ojkdwz3?9{%DP z4Y%-R2Uzo-IdU@`nBSt4&ZueaL zR4J+a$WyNX3o9&c2rbo?YUVtn#f`z@kom+Qsco|l$m=Ih*)>zy^V5>$sI7A}i*Vm2!-@xaue3?uW&p;s z*bT<6S|XlOKW&P!V+Xtm^298EXAD?W#q7y@&O)8`YU7xRdK@V$P-1^_1-%Xvyt#+Q z?O1l<=RcT*N5Awkz5do^X!WrI5Z+{Fp`n`~rC*OxKbsfT`_b0G8(`}v1j)Ut@0IKdlZDz&qw}ReYqb@`R`C9i^ z|HBtQ$05IE>dFFd(npy@Q`KTo6{|-Gk)$e|wiW~Uvy=a$DRv^eq0NBWLC2O$>21u} zSDY6Q7;Jsepw}(f@_A)o|F|}+g5ek6{Q#c+)(`ooK^tqBKFG6Jhp^-R{0@>>@1GfT9-(+QV-CB<0!wwRd+9XLXY8iEx@GeFldM z$Zl|eHFv{%#j;|ZeW!S?#O$%L!;D%TL+FQ<1ek}Dfn&zUhvR<~tkVww8;}JPY(Ec0 zkursMLj)u)SHwm&-Ojpf=EIjza2G$q{dkk zZ2kmDQ>5>7qWw8LMXvQf=85~ajc_Lwc;?A9%yoCA)o1ZBcTj{iomvdAXDdrOW~sx_ z`?fCHQvgKC*%ak0UhQuU&HGDZi}$Hmj=)=K83y(PswqQ%@C|xK%u08vIoMkiMjgvK zrCIGLUj0uKzzrW^^jO=)1z}$cF;bqNJi+VL1wBD zXvFjtU>rDXq(+-e#PS+<+lJ z1xhVODF#oTUQ15Yq{zz~dl`u7$MWe6zc*wrDSA6TS&P0xlqNBgM7xWMO-;7_wNpRn zRT~gM)fJ+cG^L@VoNSjBxVJYQ;8azg5JvY+B_#HdDyYypUcaCI#Tj@*-h`TBC&5V- zyBQ3&F)eNiuKEx@DhpxEFxf(x6T1U6gh!1<0fJGlx>f%SvPtA8mXJe?!ZRkFjHxcN z+5If(Vy!TRtU;>m5m78!=mkafPxVPCs#y~p)L7hlPuEn}5DJDPUhKNL&+HD+XFu^8 zY{D<{=_lHYH$q+ex>VJ-*^{!R(}uXc@8Y}!Cl<9>+sreRT&L@Wpg$D`)Z*(v84EhB zV5PsIge#I<1SG}%ZQ*qpF7W>^47%B0c>jq{o`vhL>#yst>#v`n*Z%>kPgzNgtS&GB O0000 + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Media/samples/Media.Samples.csproj b/components/Media/samples/Media.Samples.csproj new file mode 100644 index 00000000..ce5843af --- /dev/null +++ b/components/Media/samples/Media.Samples.csproj @@ -0,0 +1,8 @@ + + + Media + + + + + diff --git a/components/Media/src/AdditionalAssemblyInfo.cs b/components/Media/src/AdditionalAssemblyInfo.cs new file mode 100644 index 00000000..7e58d70e --- /dev/null +++ b/components/Media/src/AdditionalAssemblyInfo.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +// These `InternalsVisibleTo` calls are intended to make it easier for +// for any internal code to be testable in all the different test projects +// used with the Labs infrastructure. +[assembly: InternalsVisibleTo("Media.Tests.Uwp")] +[assembly: InternalsVisibleTo("Media.Tests.WinAppSdk")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/Media/src/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs b/components/Media/src/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs new file mode 100644 index 00000000..4663e338 --- /dev/null +++ b/components/Media/src/Animations/Abstract/EffectAnimation{TEffect,TValue,TKeyFrame}.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; +using static CommunityToolkit.WinUI.Animations.AnimationExtensions; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Animations; + +/// +/// A custom animation targeting a property on an instance. +/// +/// The type of effect to animate. +/// +/// The type to use for the public and +/// properties. This can differ from to facilitate XAML parsing. +/// +/// The actual type of keyframe values in use. +public abstract class EffectAnimation : Animation + where TEffect : class, IPipelineEffect + where TKeyFrame : unmanaged +{ + /// + /// Gets or sets the linked instance to animate. + /// + public TEffect? Target + { + get => (TEffect?)GetValue(TargetProperty); + set => SetValue(TargetProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TargetProperty = DependencyProperty.Register( + nameof(Target), + typeof(TEffect), + typeof(EffectAnimation), + new PropertyMetadata(null)); + + /// + public override AnimationBuilder AppendToBuilder(AnimationBuilder builder, TimeSpan? delayHint, TimeSpan? durationHint, EasingType? easingTypeHint, EasingMode? easingModeHint) + { + if (Target is not TEffect target) + { + static AnimationBuilder ThrowArgumentNullException() => throw new ArgumentNullException("The target effect is null, make sure to set the Target property"); + + return ThrowArgumentNullException(); + } + + if (ExplicitTarget is not string explicitTarget) + { + static AnimationBuilder ThrowArgumentNullException() + { + throw new ArgumentNullException( + "The target effect cannot be animated at this time. If you're targeting one of the " + + "built-in effects, make sure that the PipelineEffect.IsAnimatable property is set to true."); + } + + return ThrowArgumentNullException(); + } + + NormalizedKeyFrameAnimationBuilder.Composition keyFrameBuilder = new( + explicitTarget, + Delay ?? delayHint ?? DefaultDelay, + Duration ?? durationHint ?? DefaultDuration, + Repeat, + DelayBehavior); + + AppendToBuilder(keyFrameBuilder, easingTypeHint, easingModeHint); + + CompositionAnimation animation = keyFrameBuilder.GetAnimation(target.Brush!, out _); + + return builder.ExternalAnimation(target.Brush!, animation); + } +} diff --git a/components/Media/src/Animations/BlurEffectAnimation.cs b/components/Media/src/Animations/BlurEffectAnimation.cs new file mode 100644 index 00000000..dd44d3c5 --- /dev/null +++ b/components/Media/src/Animations/BlurEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// +/// An effect animation that targets . +/// +public sealed class BlurEffectAnimation : EffectAnimation +{ + /// + protected override string? ExplicitTarget => Target?.Id; + + /// + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/ColorEffectAnimation.cs b/components/Media/src/Animations/ColorEffectAnimation.cs new file mode 100644 index 00000000..c3aaffd6 --- /dev/null +++ b/components/Media/src/Animations/ColorEffectAnimation.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Animations; + +/// +/// An effect animation that targets . +/// +public sealed class ColorEffectAnimation : EffectAnimation +{ + /// + protected override string? ExplicitTarget => Target?.Id; + + /// + protected override (Color?, Color?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/CrossFadeEffectAnimation.cs b/components/Media/src/Animations/CrossFadeEffectAnimation.cs new file mode 100644 index 00000000..1ee76d92 --- /dev/null +++ b/components/Media/src/Animations/CrossFadeEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// +/// An effect animation that targets . +/// +public sealed class CrossFadeEffectAnimation : EffectAnimation +{ + /// + protected override string? ExplicitTarget => Target?.Id; + + /// + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/ExposureEffectAnimation.cs b/components/Media/src/Animations/ExposureEffectAnimation.cs new file mode 100644 index 00000000..7bd45706 --- /dev/null +++ b/components/Media/src/Animations/ExposureEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// +/// An effect animation that targets . +/// +public sealed class ExposureEffectAnimation : EffectAnimation +{ + /// + protected override string? ExplicitTarget => Target?.Id; + + /// + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/HueRotationEffectAnimation.cs b/components/Media/src/Animations/HueRotationEffectAnimation.cs new file mode 100644 index 00000000..723806c1 --- /dev/null +++ b/components/Media/src/Animations/HueRotationEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// +/// An effect animation that targets . +/// +public sealed class HueRotationEffectAnimation : EffectAnimation +{ + /// + protected override string? ExplicitTarget => Target?.Id; + + /// + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/OpacityEffectAnimation.cs b/components/Media/src/Animations/OpacityEffectAnimation.cs new file mode 100644 index 00000000..e4f76b5b --- /dev/null +++ b/components/Media/src/Animations/OpacityEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// +/// An effect animation that targets . +/// +public sealed class OpacityEffectAnimation : EffectAnimation +{ + /// + protected override string? ExplicitTarget => Target?.Id; + + /// + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/SaturationEffectAnimation.cs b/components/Media/src/Animations/SaturationEffectAnimation.cs new file mode 100644 index 00000000..014a8648 --- /dev/null +++ b/components/Media/src/Animations/SaturationEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// +/// An effect animation that targets . +/// +public sealed class SaturationEffectAnimation : EffectAnimation +{ + /// + protected override string? ExplicitTarget => Target?.Id; + + /// + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Animations/SepiaEffectAnimation.cs b/components/Media/src/Animations/SepiaEffectAnimation.cs new file mode 100644 index 00000000..f32d62e3 --- /dev/null +++ b/components/Media/src/Animations/SepiaEffectAnimation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media; + +namespace CommunityToolkit.WinUI.Animations; + +/// +/// An effect animation that targets . +/// +public sealed class SepiaEffectAnimation : EffectAnimation +{ + /// + protected override string? ExplicitTarget => Target?.Id; + + /// + protected override (double?, double?) GetParsedValues() + { + return (To, From); + } +} diff --git a/components/Media/src/Brushes/AcrylicBrush.cs b/components/Media/src/Brushes/AcrylicBrush.cs new file mode 100644 index 00000000..ba95daea --- /dev/null +++ b/components/Media/src/Brushes/AcrylicBrush.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINUI2 +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.UI; +using Windows.UI.Composition; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A that implements an acrylic effect with customizable parameters +/// +public sealed class AcrylicBrush : XamlCompositionEffectBrushBase +{ + /// + /// The instance in use to set the blur amount + /// + /// This is only set when is + private EffectSetter? blurAmountSetter; + + /// + /// The instance in use to set the tint color + /// + private EffectSetter? tintColorSetter; + + /// + /// The instance in use to set the tint mix amount + /// + private EffectSetter? tintOpacitySetter; + + /// + /// Gets or sets the background source mode for the effect (the default is ). + /// + public AcrylicBackgroundSource BackgroundSource + { + get => (AcrylicBackgroundSource)GetValue(BackgroundSourceProperty); + set => SetValue(BackgroundSourceProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty BackgroundSourceProperty = DependencyProperty.Register( + nameof(BackgroundSource), + typeof(AcrylicBackgroundSource), + typeof(AcrylicBrush), + new PropertyMetadata(AcrylicBackgroundSource.Backdrop, OnSourcePropertyChanged)); + + /// + /// Updates the UI when changes + /// + /// The current instance + /// The instance for + private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AcrylicBrush brush && + brush.CompositionBrush != null) + { + brush.OnDisconnected(); + brush.OnConnected(); + } + } + + /// + /// Gets or sets the blur amount for the effect (must be a positive value) + /// + /// This property is ignored when the active mode is + public double BlurAmount + { + get => (double)GetValue(BlurAmountProperty); + set => SetValue(BlurAmountProperty, Math.Max(value, 0)); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty BlurAmountProperty = DependencyProperty.Register( + nameof(BlurAmount), + typeof(double), + typeof(AcrylicBrush), + new PropertyMetadata(0.0, OnBlurAmountPropertyChanged)); + + /// + /// Updates the UI when changes + /// + /// The current instance + /// The instance for + private static void OnBlurAmountPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AcrylicBrush brush && + brush.BackgroundSource != AcrylicBackgroundSource.HostBackdrop && // Blur is fixed by OS when using HostBackdrop source. + brush.CompositionBrush is CompositionBrush target) + { + brush.blurAmountSetter?.Invoke(target, (float)(double)e.NewValue); + } + } + + /// + /// Gets or sets the tint for the effect + /// + public Color TintColor + { + get => (Color)GetValue(TintColorProperty); + set => SetValue(TintColorProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TintColorProperty = DependencyProperty.Register( + nameof(TintColor), + typeof(Color), + typeof(AcrylicBrush), + new PropertyMetadata(default(Color), OnTintColorPropertyChanged)); + + /// + /// Updates the UI when changes + /// + /// The current instance + /// The instance for + private static void OnTintColorPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AcrylicBrush brush && + brush.CompositionBrush is CompositionBrush target) + { + brush.tintColorSetter?.Invoke(target, (Color)e.NewValue); + } + } + + /// + /// Gets or sets the tint opacity factor for the effect (default is 0.5, must be in the [0, 1] range) + /// + public double TintOpacity + { + get => (double)GetValue(TintOpacityProperty); + set => SetValue(TintOpacityProperty, Math.Clamp(value, 0, 1)); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TintOpacityProperty = DependencyProperty.Register( + nameof(TintOpacity), + typeof(double), + typeof(AcrylicBrush), + new PropertyMetadata(0.5, OnTintOpacityPropertyChanged)); + + /// + /// Updates the UI when changes + /// + /// The current instance + /// The instance for + private static void OnTintOpacityPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AcrylicBrush brush && + brush.CompositionBrush is CompositionBrush target) + { + brush.tintOpacitySetter?.Invoke(target, (float)(double)e.NewValue); + } + } + + /// + /// Gets or sets the for the texture to use + /// + public Uri TextureUri + { + get => (Uri)GetValue(TextureUriProperty); + set => SetValue(TextureUriProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TextureUriProperty = DependencyProperty.Register( + nameof(TextureUri), + typeof(Uri), + typeof(AcrylicBrush), + new PropertyMetadata(default, OnTextureUriPropertyChanged)); + + /// + /// Updates the UI when changes + /// + /// The current instance + /// The instance for + private static void OnTextureUriPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is AcrylicBrush brush && + brush.CompositionBrush != null) + { + brush.OnDisconnected(); + brush.OnConnected(); + } + } + + /// + protected override PipelineBuilder OnPipelineRequested() + { + switch (BackgroundSource) + { + case AcrylicBackgroundSource.Backdrop: + return PipelineBuilder.FromBackdropAcrylic( + TintColor, + out this.tintColorSetter, + (float)TintOpacity, + out this.tintOpacitySetter, + (float)BlurAmount, + out blurAmountSetter, + TextureUri); + case AcrylicBackgroundSource.HostBackdrop: + return PipelineBuilder.FromHostBackdropAcrylic( + TintColor, + out this.tintColorSetter, + (float)TintOpacity, + out this.tintOpacitySetter, + TextureUri); + default: throw new ArgumentOutOfRangeException(nameof(BackgroundSource), $"Invalid acrylic source: {BackgroundSource}"); + } + } +} +#endif diff --git a/components/Media/src/Brushes/BackdropBlurBrush.cs b/components/Media/src/Brushes/BackdropBlurBrush.cs new file mode 100644 index 00000000..b6159604 --- /dev/null +++ b/components/Media/src/Brushes/BackdropBlurBrush.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//// Example brush from https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.xamlcompositionbrushbase + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// The is a that blurs whatever is behind it in the application. +/// +public class BackdropBlurBrush : XamlCompositionEffectBrushBase +{ + /// + /// The instance currently in use + /// + private EffectSetter? amountSetter; + + /// + /// Gets or sets the amount of gaussian blur to apply to the background. + /// + public double Amount + { + get => (double)GetValue(AmountProperty); + set => SetValue(AmountProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty AmountProperty = DependencyProperty.Register( + nameof(Amount), + typeof(double), + typeof(BackdropBlurBrush), + new PropertyMetadata(0.0, new PropertyChangedCallback(OnAmountChanged))); + + /// + /// Updates the UI when changes + /// + /// The current instance + /// The instance for + private static void OnAmountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is BackdropBlurBrush brush && + brush.CompositionBrush is CompositionBrush target) + { + brush.amountSetter?.Invoke(target, (float)brush.Amount); + } + } + + /// + protected override PipelineBuilder OnPipelineRequested() + { + return PipelineBuilder.FromBackdrop().Blur((float)Amount, out this.amountSetter); + } +} diff --git a/components/Media/src/Brushes/BackdropGammaTransferBrush.cs b/components/Media/src/Brushes/BackdropGammaTransferBrush.cs new file mode 100644 index 00000000..1b7de60b --- /dev/null +++ b/components/Media/src/Brushes/BackdropGammaTransferBrush.cs @@ -0,0 +1,406 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Graphics.Canvas.Effects; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A brush which alters the colors of whatever is behind it in the application by applying a per-channel gamma transfer function. See https://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_GammaTransferEffect.htm. +/// +public class BackdropGammaTransferBrush : XamlCompositionBrushBase +{ + /// + /// Gets or sets the amount of scale to apply to the alpha chennel. + /// + public double AlphaAmplitude + { + get => (double)GetValue(AlphaAmplitudeProperty); + set => SetValue(AlphaAmplitudeProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty AlphaAmplitudeProperty = DependencyProperty.Register( + nameof(AlphaAmplitude), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(AlphaAmplitude)))); + + /// + /// Gets or sets a value indicating whether to disable alpha transfer. + /// + public bool AlphaDisable + { + get => (bool)GetValue(AlphaDisableProperty); + set => SetValue(AlphaDisableProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty AlphaDisableProperty = DependencyProperty.Register( + nameof(AlphaDisable), + typeof(bool), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(false, OnBooleanPropertyChangedHelper(nameof(AlphaDisable)))); + + /// + /// Gets or sets the amount of scale to apply to the alpha chennel. + /// + public double AlphaExponent + { + get => (double)GetValue(AlphaExponentProperty); + set => SetValue(AlphaExponentProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty AlphaExponentProperty = DependencyProperty.Register( + nameof(AlphaExponent), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(AlphaExponent)))); + + /// + /// Gets or sets the amount of scale to apply to the alpha chennel. + /// + public double AlphaOffset + { + get => (double)GetValue(AlphaOffsetProperty); + set => SetValue(AlphaOffsetProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty AlphaOffsetProperty = DependencyProperty.Register( + nameof(AlphaOffset), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(0.0, OnScalarPropertyChangedHelper(nameof(AlphaOffset)))); + + /// + /// Gets or sets the amount of scale to apply to the Blue chennel. + /// + public double BlueAmplitude + { + get => (double)GetValue(BlueAmplitudeProperty); + set => SetValue(BlueAmplitudeProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty BlueAmplitudeProperty = DependencyProperty.Register( + nameof(BlueAmplitude), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(BlueAmplitude)))); + + /// + /// Gets or sets a value indicating whether to disable Blue transfer. + /// + public bool BlueDisable + { + get => (bool)GetValue(BlueDisableProperty); + set => SetValue(BlueDisableProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty BlueDisableProperty = DependencyProperty.Register( + nameof(BlueDisable), + typeof(bool), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(false, OnBooleanPropertyChangedHelper(nameof(BlueDisable)))); + + /// + /// Gets or sets the amount of scale to apply to the Blue chennel. + /// + public double BlueExponent + { + get => (double)GetValue(BlueExponentProperty); + set => SetValue(BlueExponentProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty BlueExponentProperty = DependencyProperty.Register( + nameof(BlueExponent), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(BlueExponent)))); + + /// + /// Gets or sets the amount of scale to apply to the Blue chennel. + /// + public double BlueOffset + { + get => (double)GetValue(BlueOffsetProperty); + set => SetValue(BlueOffsetProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty BlueOffsetProperty = DependencyProperty.Register( + nameof(BlueOffset), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(0.0, OnScalarPropertyChangedHelper(nameof(BlueOffset)))); + + /// + /// Gets or sets the amount of scale to apply to the Green chennel. + /// + public double GreenAmplitude + { + get => (double)GetValue(GreenAmplitudeProperty); + set => SetValue(GreenAmplitudeProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty GreenAmplitudeProperty = DependencyProperty.Register( + nameof(GreenAmplitude), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(GreenAmplitude)))); + + /// + /// Gets or sets a value indicating whether to disable Green transfer. + /// + public bool GreenDisable + { + get => (bool)GetValue(GreenDisableProperty); + set => SetValue(GreenDisableProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty GreenDisableProperty = DependencyProperty.Register( + nameof(GreenDisable), + typeof(bool), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(false, OnBooleanPropertyChangedHelper(nameof(GreenDisable)))); + + /// + /// Gets or sets the amount of scale to apply to the Green chennel. + /// + public double GreenExponent + { + get => (double)GetValue(GreenExponentProperty); + set => SetValue(GreenExponentProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty GreenExponentProperty = DependencyProperty.Register( + nameof(GreenExponent), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(GreenExponent)))); + + /// + /// Gets or sets the amount of scale to apply to the Green chennel. + /// + public double GreenOffset + { + get => (double)GetValue(GreenOffsetProperty); + set => SetValue(GreenOffsetProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty GreenOffsetProperty = DependencyProperty.Register( + nameof(GreenOffset), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(0.0, OnScalarPropertyChangedHelper(nameof(GreenOffset)))); + + /// + /// Gets or sets the amount of scale to apply to the Red chennel. + /// + public double RedAmplitude + { + get => (double)GetValue(RedAmplitudeProperty); + set => SetValue(RedAmplitudeProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty RedAmplitudeProperty = DependencyProperty.Register( + nameof(RedAmplitude), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(RedAmplitude)))); + + /// + /// Gets or sets a value indicating whether to disable Red transfer. + /// + public bool RedDisable + { + get => (bool)GetValue(RedDisableProperty); + set => SetValue(RedDisableProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty RedDisableProperty = DependencyProperty.Register( + nameof(RedDisable), + typeof(bool), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(false, OnBooleanPropertyChangedHelper(nameof(RedDisable)))); + + /// + /// Gets or sets the amount of scale to apply to the Red chennel. + /// + public double RedExponent + { + get => (double)GetValue(RedExponentProperty); + set => SetValue(RedExponentProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty RedExponentProperty = DependencyProperty.Register( + nameof(RedExponent), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(1.0, OnScalarPropertyChangedHelper(nameof(RedExponent)))); + + /// + /// Gets or sets the amount of scale to apply to the Red chennel. + /// + public double RedOffset + { + get => (double)GetValue(RedOffsetProperty); + set => SetValue(RedOffsetProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty RedOffsetProperty = DependencyProperty.Register( + nameof(RedOffset), + typeof(double), + typeof(BackdropGammaTransferBrush), + new PropertyMetadata(0.0, OnScalarPropertyChangedHelper(nameof(RedOffset)))); + + private static PropertyChangedCallback OnScalarPropertyChangedHelper(string propertyname) + { + return (d, e) => + { + var brush = (BackdropGammaTransferBrush)d; + + // Unbox and set a new blur amount if the CompositionBrush exists. + brush.CompositionBrush?.Properties.InsertScalar("GammaTransfer." + propertyname, (float)(double)e.NewValue); + }; + } + + private static PropertyChangedCallback OnBooleanPropertyChangedHelper(string propertyname) + { + return (d, e) => + { + var brush = (BackdropGammaTransferBrush)d; + + // We can't animate our boolean properties so recreate our internal brush. + brush.OnDisconnected(); + brush.OnConnected(); + }; + } + + /// + protected override void OnConnected() + { + // Delay creating composition resources until they're required. + if (CompositionBrush == null) + { +#if WINUI2 + var compositionCapabilities = CompositionCapabilities.GetForCurrentView(); +#else + var compositionCapabilities = new CompositionCapabilities(); +#endif + // Abort if effects aren't supported. + if (!compositionCapabilities.AreEffectsSupported()) + { + return; + } + + var backdrop = Window.Current.Compositor.CreateBackdropBrush(); + + // Use a Win2D blur affect applied to a CompositionBackdropBrush. + var graphicsEffect = new GammaTransferEffect + { + Name = "GammaTransfer", + AlphaAmplitude = (float)AlphaAmplitude, + AlphaDisable = AlphaDisable, + AlphaExponent = (float)AlphaExponent, + AlphaOffset = (float)AlphaOffset, + RedAmplitude = (float)RedAmplitude, + RedDisable = RedDisable, + RedExponent = (float)RedExponent, + RedOffset = (float)RedOffset, + GreenAmplitude = (float)GreenAmplitude, + GreenDisable = GreenDisable, + GreenExponent = (float)GreenExponent, + GreenOffset = (float)GreenOffset, + BlueAmplitude = (float)BlueAmplitude, + BlueDisable = BlueDisable, + BlueExponent = (float)BlueExponent, + BlueOffset = (float)BlueOffset, + Source = new CompositionEffectSourceParameter("backdrop") + }; + + var effectFactory = Window.Current.Compositor.CreateEffectFactory(graphicsEffect, new[] + { + "GammaTransfer.AlphaAmplitude", + "GammaTransfer.AlphaExponent", + "GammaTransfer.AlphaOffset", + "GammaTransfer.RedAmplitude", + "GammaTransfer.RedExponent", + "GammaTransfer.RedOffset", + "GammaTransfer.GreenAmplitude", + "GammaTransfer.GreenExponent", + "GammaTransfer.GreenOffset", + "GammaTransfer.BlueAmplitude", + "GammaTransfer.BlueExponent", + "GammaTransfer.BlueOffset", + }); + var effectBrush = effectFactory.CreateBrush(); + + effectBrush.SetSourceParameter("backdrop", backdrop); + + CompositionBrush = effectBrush; + } + } + + /// + protected override void OnDisconnected() + { + // Dispose of composition resources when no longer in use. + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + } +} diff --git a/components/Media/src/Brushes/BackdropInvertBrush.cs b/components/Media/src/Brushes/BackdropInvertBrush.cs new file mode 100644 index 00000000..b5cc729e --- /dev/null +++ b/components/Media/src/Brushes/BackdropInvertBrush.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//// Example brush from https://blogs.windows.com/buildingapps/2017/07/18/working-brushes-content-xaml-visual-layer-interop-part-one/#z70vPv1QMAvZsceo.97 + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// The is a which inverts whatever is behind it in the application. +/// +public class BackdropInvertBrush : XamlCompositionEffectBrushBase +{ + /// + protected override PipelineBuilder OnPipelineRequested() + { + return PipelineBuilder.FromBackdrop().Invert(); + } +} diff --git a/components/Media/src/Brushes/BackdropSaturationBrush.cs b/components/Media/src/Brushes/BackdropSaturationBrush.cs new file mode 100644 index 00000000..f965bb2e --- /dev/null +++ b/components/Media/src/Brushes/BackdropSaturationBrush.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// Brush which applies a SaturationEffect to the Backdrop. http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_SaturationEffect.htm +/// +public class BackdropSaturationBrush : XamlCompositionEffectBrushBase +{ + /// + /// The instance currently in use + /// + private EffectSetter? setter; + + /// + /// Gets or sets the amount of gaussian blur to apply to the background. + /// + public double Saturation + { + get => (double)GetValue(SaturationProperty); + set => SetValue(SaturationProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty SaturationProperty = DependencyProperty.Register( + nameof(Saturation), + typeof(double), + typeof(BackdropSaturationBrush), + new PropertyMetadata(0.5, new PropertyChangedCallback(OnSaturationChanged))); + + /// + /// Updates the UI when changes + /// + /// The current instance + /// The instance for + private static void OnSaturationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (BackdropSaturationBrush)d; + + // Clamp Value as per docs http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_SaturationEffect.htm + var value = (float)(double)e.NewValue; + if (value > 1.0) + { + brush.Saturation = 1.0; + } + else if (value < 0.0) + { + brush.Saturation = 0.0; + } + + // Unbox and set a new blur amount if the CompositionBrush exists + if (brush.CompositionBrush is CompositionBrush target) + { + brush.setter?.Invoke(target, (float)brush.Saturation); + } + } + + /// + protected override PipelineBuilder OnPipelineRequested() + { + return PipelineBuilder.FromBackdrop().Saturation((float)Saturation, out setter); + } +} diff --git a/components/Media/src/Brushes/BackdropSepiaBrush.cs b/components/Media/src/Brushes/BackdropSepiaBrush.cs new file mode 100644 index 00000000..73f28a36 --- /dev/null +++ b/components/Media/src/Brushes/BackdropSepiaBrush.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// Brush which applies a SepiaEffect to the Backdrop. http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_SepiaEffect.htm +/// +public class BackdropSepiaBrush : XamlCompositionEffectBrushBase +{ + /// + /// The instance currently in use + /// + private EffectSetter? setter; + + /// + /// Gets or sets the amount of gaussian blur to apply to the background. + /// + public double Intensity + { + get => (double)GetValue(IntensityProperty); + set => SetValue(IntensityProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IntensityProperty = DependencyProperty.Register( + nameof(Intensity), + typeof(double), + typeof(BackdropSepiaBrush), + new PropertyMetadata(0.5, new PropertyChangedCallback(OnIntensityChanged))); + + /// + /// Updates the UI when changes + /// + /// The current instance + /// The instance for + private static void OnIntensityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (BackdropSepiaBrush)d; + + // Clamp Value as per docs http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_SepiaEffect.htm + var value = (float)(double)e.NewValue; + if (value > 1.0) + { + brush.Intensity = 1.0; + } + else if (value < 0.0) + { + brush.Intensity = 0.0; + } + + // Unbox and set a new blur amount if the CompositionBrush exists. + if (brush.CompositionBrush is CompositionBrush target) + { + brush.setter?.Invoke(target, (float)brush.Intensity); + } + } + + /// + protected override PipelineBuilder OnPipelineRequested() + { + return PipelineBuilder.FromBackdrop().Sepia((float)Intensity, out setter); + } +} diff --git a/components/Media/src/Brushes/Base/XamlCompositionEffectBrushBase.cs b/components/Media/src/Brushes/Base/XamlCompositionEffectBrushBase.cs new file mode 100644 index 00000000..e4b6f921 --- /dev/null +++ b/components/Media/src/Brushes/Base/XamlCompositionEffectBrushBase.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A custom that's ready to be used with a custom pipeline. +/// +public abstract class XamlCompositionEffectBrushBase : XamlCompositionBrushBase +{ + /// + /// The initialization instance. + /// + private readonly AsyncMutex connectedMutex = new AsyncMutex(); + + /// + /// A method that builds and returns the pipeline to use in the current instance. + /// This method can also be used to store any needed or + /// instances in local fields, for later use (they will need to be called upon ). + /// + /// A instance to create the brush to display. + protected abstract PipelineBuilder OnPipelineRequested(); + + private bool isEnabled = true; + + /// + /// Gets or sets a value indicating whether the current brush is using the provided pipeline, or the fallback color. + /// + public bool IsEnabled + { + get => this.isEnabled; + set => this.OnEnabledToggled(value); + } + + /// + protected override async void OnConnected() + { + using (await this.connectedMutex.LockAsync()) + { + if (CompositionBrush == null) + { +#if WINUI2 + var compositionCapabilities = CompositionCapabilities.GetForCurrentView(); +#else + var compositionCapabilities = new CompositionCapabilities(); +#endif + // Abort if effects aren't supported. + if (!compositionCapabilities.AreEffectsSupported()) + { + return; + } + + if (this.isEnabled) + { + CompositionBrush = await OnPipelineRequested().BuildAsync(); + } + else + { + CompositionBrush = await PipelineBuilder.FromColor(FallbackColor).BuildAsync(); + } + + OnCompositionBrushUpdated(); + } + } + + base.OnConnected(); + } + + /// + protected override async void OnDisconnected() + { + using (await this.connectedMutex.LockAsync()) + { + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + + OnCompositionBrushUpdated(); + } + } + + base.OnDisconnected(); + } + + /// + /// Updates the property depending on the input value. + /// + /// The new value being set to the property. + protected async void OnEnabledToggled(bool value) + { + using (await this.connectedMutex.LockAsync()) + { + if (this.isEnabled == value) + { + return; + } + + this.isEnabled = value; + + if (CompositionBrush != null) + { +#if WINUI2 + var compositionCapabilities = CompositionCapabilities.GetForCurrentView(); +#else + var compositionCapabilities = new CompositionCapabilities(); +#endif + // Abort if effects aren't supported. + if (!compositionCapabilities.AreEffectsSupported()) + { + return; + } + + if (this.isEnabled) + { + CompositionBrush = await OnPipelineRequested().BuildAsync(); + } + else + { + CompositionBrush = await PipelineBuilder.FromColor(FallbackColor).BuildAsync(); + } + + OnCompositionBrushUpdated(); + } + } + } + + /// + /// Invoked whenever the property is updated. + /// + protected virtual void OnCompositionBrushUpdated() + { + } +} diff --git a/components/Media/src/Brushes/CanvasBrushBase.cs b/components/Media/src/Brushes/CanvasBrushBase.cs new file mode 100644 index 00000000..55cd1be0 --- /dev/null +++ b/components/Media/src/Brushes/CanvasBrushBase.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.UI.Composition; + +#if WINUI2 +using Windows.UI.Composition; +using Windows.Graphics.DirectX; +#elif WINUI3 +using Microsoft.UI.Composition; +using Microsoft.Graphics.DirectX; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// Helper Brush class to interop with Win2D Canvas calls. +/// +public abstract class CanvasBrushBase : XamlCompositionBrushBase +{ + private CompositionSurfaceBrush? _surfaceBrush; + + /// + /// Gets or sets the internal surface render width. Modify during construction. + /// + protected float SurfaceWidth { get; set; } + + /// + /// Gets or sets the internal surface render height. Modify during construction. + /// + protected float SurfaceHeight { get; set; } + + private CanvasDevice? _device; + + private CompositionGraphicsDevice? _graphics; + + /// + /// Implemented by parent class and called when canvas is being constructed for brush. + /// + /// Canvas device. + /// Canvas drawing session. + /// Size of surface to draw on. + /// True if drawing was completed and the brush is ready, otherwise return False to not create brush yet. + protected abstract bool OnDraw(CanvasDevice device, CanvasDrawingSession session, Vector2 size); + + /// + /// Initializes the Composition Brush. + /// + protected override void OnConnected() + { + base.OnConnected(); + + if (_device != null) + { + _device.DeviceLost -= CanvasDevice_DeviceLost; + } + + _device = CanvasDevice.GetSharedDevice(); + _device.DeviceLost += CanvasDevice_DeviceLost; + + if (_graphics != null) + { + _graphics.RenderingDeviceReplaced -= CanvasDevice_RenderingDeviceReplaced; + } + + _graphics = CanvasComposition.CreateCompositionGraphicsDevice(Window.Current.Compositor, _device); + _graphics.RenderingDeviceReplaced += CanvasDevice_RenderingDeviceReplaced; + + // Delay creating composition resources until they're required. + if (CompositionBrush == null) + { +#if WINUI2 + var compositionCapabilities = CompositionCapabilities.GetForCurrentView(); +#else + var compositionCapabilities = new CompositionCapabilities(); +#endif + // Abort if effects aren't supported. + if (!compositionCapabilities.AreEffectsSupported()) + { + return; + } + + var size = new Vector2(SurfaceWidth, SurfaceHeight); + var surface = _graphics.CreateDrawingSurface(size.ToSize(), DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); + + using (var session = CanvasComposition.CreateDrawingSession(surface)) + { + // Call Implementor to draw on session. + if (!OnDraw(_device, session, size)) + { + return; + } + } + + _surfaceBrush = Window.Current.Compositor.CreateSurfaceBrush(surface); + _surfaceBrush.Stretch = CompositionStretch.Fill; + + CompositionBrush = _surfaceBrush; + } + } + + private void CanvasDevice_RenderingDeviceReplaced(CompositionGraphicsDevice sender, object args) + { + OnDisconnected(); + OnConnected(); + } + + private void CanvasDevice_DeviceLost(CanvasDevice sender, object args) + { + OnDisconnected(); + OnConnected(); + } + + /// + /// Deconstructs the Composition Brush. + /// + protected override void OnDisconnected() + { + base.OnDisconnected(); + + if (_device != null) + { + _device.DeviceLost -= CanvasDevice_DeviceLost; + _device = null; + } + + if (_graphics != null) + { + _graphics.RenderingDeviceReplaced -= CanvasDevice_RenderingDeviceReplaced; + _graphics = null; + } + + // Dispose of composition resources when no longer in use. + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + + if (_surfaceBrush != null) + { + _surfaceBrush.Dispose(); + _surfaceBrush = null; + } + } +} diff --git a/components/Media/src/Brushes/ImageBlendBrush.cs b/components/Media/src/Brushes/ImageBlendBrush.cs new file mode 100644 index 00000000..4f5123ba --- /dev/null +++ b/components/Media/src/Brushes/ImageBlendBrush.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//// Image loading reference from https://blogs.windows.com/buildingapps/2017/07/18/working-brushes-content-xaml-visual-layer-interop-part-one/#MA0k4EYWzqGKV501.97 + +using Microsoft.Graphics.Canvas.Effects; +using CanvasBlendEffect = Microsoft.Graphics.Canvas.Effects.BlendEffect; + +#if WINUI3 +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Media.Imaging; +#elif WINUI2 +using Windows.UI.Composition; +using Windows.UI.Xaml.Media.Imaging; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// Brush which blends a to the Backdrop in a given mode. See http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_BlendEffect.htm. +/// +public class ImageBlendBrush : XamlCompositionBrushBase +{ + private LoadedImageSurface? _surface; + private CompositionSurfaceBrush? _surfaceBrush; + + /// + /// Gets or sets the source of the image to composite. + /// + public ImageSource Source + { + get => (ImageSource)GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty SourceProperty = DependencyProperty.Register( + nameof(Source), + typeof(ImageSource), // We use ImageSource type so XAML engine will automatically construct proper object from String. + typeof(ImageBlendBrush), + new PropertyMetadata(null, OnImageSourceChanged)); + + /// + /// Gets or sets how to stretch the image within the brush. + /// + public Stretch Stretch + { + get => (Stretch)GetValue(StretchProperty); + set => SetValue(StretchProperty, value); + } + + /// + /// Identifies the dependency property. + /// Requires 16299 or higher for modes other than None. + /// + public static readonly DependencyProperty StretchProperty = DependencyProperty.Register( + nameof(Stretch), + typeof(Stretch), + typeof(ImageBlendBrush), + new PropertyMetadata(Stretch.None, OnStretchChanged)); + + /// + /// Gets or sets how to blend the image with the backdrop. + /// + public ImageBlendMode Mode + { + get => (ImageBlendMode)GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ModeProperty = DependencyProperty.Register( + nameof(Mode), + typeof(ImageBlendMode), + typeof(ImageBlendBrush), + new PropertyMetadata(ImageBlendMode.Multiply, OnModeChanged)); + + private static void OnImageSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (ImageBlendBrush)d; + + // Unbox and update surface if CompositionBrush exists + if (brush._surfaceBrush != null) + { + // If UriSource is invalid, StartLoadFromUri will return a blank texture. + var uri = (e.NewValue as BitmapImage)?.UriSource ?? new Uri("ms-appx:///"); + var newSurface = LoadedImageSurface.StartLoadFromUri(uri); + + brush._surface = newSurface; + brush._surfaceBrush.Surface = newSurface; + } + else + { + // If we didn't initially have a valid surface, we need to recreate our effect now. + brush.OnDisconnected(); + brush.OnConnected(); + } + } + + private static void OnStretchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (ImageBlendBrush)d; + + // Unbox and update surface if CompositionBrush exists + if (brush._surfaceBrush != null) + { + // Modify the stretch property on our brush. + brush._surfaceBrush.Stretch = CompositionStretchFromStretch((Stretch)e.NewValue); + } + } + + private static void OnModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var brush = (ImageBlendBrush)d; + + // We can't animate our enum properties so recreate our internal brush. + brush.OnDisconnected(); + brush.OnConnected(); + } + + /// + protected override void OnConnected() + { + // Delay creating composition resources until they're required. + if (CompositionBrush == null && Source != null && Source is BitmapImage bitmap) + { + // Use LoadedImageSurface API to get ICompositionSurface from image uri provided + // If UriSource is invalid, StartLoadFromUri will return a blank texture. + _surface = LoadedImageSurface.StartLoadFromUri(bitmap.UriSource); + + // Load Surface onto SurfaceBrush + _surfaceBrush = Window.Current.Compositor.CreateSurfaceBrush(_surface); + _surfaceBrush.Stretch = CompositionStretchFromStretch(Stretch); + +#if WINUI2 + var compositionCapabilities = CompositionCapabilities.GetForCurrentView(); +#else + var compositionCapabilities = new CompositionCapabilities(); +#endif + // Abort if effects aren't supported. + if (!compositionCapabilities.AreEffectsSupported()) + { + // Just use image straight-up, if we don't support effects. + CompositionBrush = _surfaceBrush; + return; + } + + var backdrop = Window.Current.Compositor.CreateBackdropBrush(); + + // Use a Win2D invert affect applied to a CompositionBackdropBrush. + var graphicsEffect = new CanvasBlendEffect + { + Name = "Invert", + Mode = (BlendEffectMode)(int)Mode, + Background = new CompositionEffectSourceParameter("backdrop"), + Foreground = new CompositionEffectSourceParameter("image") + }; + + var effectFactory = Window.Current.Compositor.CreateEffectFactory(graphicsEffect); + var effectBrush = effectFactory.CreateBrush(); + + effectBrush.SetSourceParameter("backdrop", backdrop); + effectBrush.SetSourceParameter("image", _surfaceBrush); + + CompositionBrush = effectBrush; + } + } + + /// + protected override void OnDisconnected() + { + // Dispose of composition resources when no longer in use. + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + + if (_surfaceBrush != null) + { + _surfaceBrush.Dispose(); + _surfaceBrush = null; + } + + if (_surface != null) + { + _surface.Dispose(); + _surface = null; + } + } + + //// Helper to allow XAML developer to use XAML stretch property rather than another enum. + private static CompositionStretch CompositionStretchFromStretch(Stretch value) + { + switch (value) + { + case Stretch.None: + return CompositionStretch.None; + case Stretch.Fill: + return CompositionStretch.Fill; + case Stretch.Uniform: + return CompositionStretch.Uniform; + case Stretch.UniformToFill: + return CompositionStretch.UniformToFill; + } + + return CompositionStretch.None; + } +} diff --git a/components/Media/src/Brushes/PipelineBrush.cs b/components/Media/src/Brushes/PipelineBrush.cs new file mode 100644 index 00000000..8267c7dd --- /dev/null +++ b/components/Media/src/Brushes/PipelineBrush.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A that renders a customizable Composition/Win2D effects pipeline +/// +[ContentProperty(Name = nameof(Effects))] +public sealed class PipelineBrush : XamlCompositionEffectBrushBase +{ + /// + /// Gets or sets the source for the current pipeline (defaults to a with source). + /// + public PipelineBuilder? Source { get; set; } + + /// + /// Gets or sets the collection of effects to use in the current pipeline. + /// + public IList Effects + { + get + { + if (GetValue(EffectsProperty) is not IList effects) + { + effects = new List(); + + SetValue(EffectsProperty, effects); + } + + return effects; + } + set => SetValue(EffectsProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty EffectsProperty = DependencyProperty.Register( + nameof(Effects), + typeof(IList), + typeof(PipelineBrush), + new PropertyMetadata(null)); + + /// + protected override PipelineBuilder OnPipelineRequested() + { + PipelineBuilder builder = Source ?? PipelineBuilder.FromBackdrop(); + + foreach (IPipelineEffect effect in Effects) + { + builder = effect.AppendToBuilder(builder); + } + + return builder; + } + + /// + protected override void OnCompositionBrushUpdated() + { + foreach (IPipelineEffect effect in Effects) + { + effect.NotifyCompositionBrushInUse(CompositionBrush); + } + } +} diff --git a/components/Media/src/Brushes/TilesBrush.cs b/components/Media/src/Brushes/TilesBrush.cs new file mode 100644 index 00000000..894e98ab --- /dev/null +++ b/components/Media/src/Brushes/TilesBrush.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A that displays a tiled image +/// +public sealed class TilesBrush : XamlCompositionEffectBrushBase +{ + /// + /// Gets or sets the to the texture to use + /// + public Uri TextureUri + { + get => (Uri)GetValue(TextureUriProperty); + set => SetValue(TextureUriProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TextureUriProperty = DependencyProperty.Register( + nameof(TextureUri), + typeof(Uri), + typeof(TilesBrush), + new PropertyMetadata(default, OnDependencyPropertyChanged)); + + /// + /// Gets or sets the DPI mode used to render the texture (the default is ) + /// + public DpiMode DpiMode + { + get => (DpiMode)GetValue(DpiModeProperty); + set => SetValue(DpiModeProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DpiModeProperty = DependencyProperty.Register( + nameof(DpiMode), + typeof(DpiMode), + typeof(TilesBrush), + new PropertyMetadata(DpiMode.DisplayDpiWith96AsLowerBound, OnDependencyPropertyChanged)); + + /// + /// Updates the UI when either or changes + /// + /// The current instance + /// The instance for or + private static void OnDependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TilesBrush brush && + brush.CompositionBrush != null) + { + brush.OnDisconnected(); + brush.OnConnected(); + } + } + + /// + protected override PipelineBuilder OnPipelineRequested() + { + if (TextureUri is Uri uri) + { + return PipelineBuilder.FromTiles(uri, DpiMode); + } + + return PipelineBuilder.FromColor(default); + } +} \ No newline at end of file diff --git a/components/Media/src/Brushes/XamlCompositionBrush.cs b/components/Media/src/Brushes/XamlCompositionBrush.cs new file mode 100644 index 00000000..2b507da9 --- /dev/null +++ b/components/Media/src/Brushes/XamlCompositionBrush.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.System; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A that represents a custom effect setter that can be applied to a instance +/// +/// The type of property value to set +/// The effect target value +public delegate void XamlEffectSetter(T value) + where T : unmanaged; + +/// +/// A that represents a custom effect animation that can be applied to a instance +/// +/// The type of property value to animate +/// The animation target value +/// The animation duration +/// A that completes when the target animation completes +public delegate Task XamlEffectAnimation(T value, TimeSpan duration) + where T : unmanaged; + +/// +/// A simple that can be used to quickly create XAML brushes from arbitrary pipelines +/// +public sealed class XamlCompositionBrush : XamlCompositionEffectBrushBase +{ + /// + /// Gets the pipeline for the current instance + /// + public PipelineBuilder Pipeline { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The instance to create the effect + public XamlCompositionBrush(PipelineBuilder pipeline) => this.Pipeline = pipeline; + + /// + /// Binds an to the composition brush in the current instance + /// + /// The type of property value to set + /// The input setter + /// The resulting setter + /// The current instance + [Pure] + public XamlCompositionBrush Bind(EffectSetter setter, out XamlEffectSetter bound) + where T : unmanaged + { + bound = value => setter(this.CompositionBrush, value); + + return this; + } + + /// + /// Binds an to the composition brush in the current instance + /// + /// The type of property value to animate + /// The input animation + /// The resulting animation + /// The current instance + [Pure] + public XamlCompositionBrush Bind(EffectAnimation animation, out XamlEffectAnimation bound) + where T : unmanaged + { + bound = (value, duration) => animation(this.CompositionBrush, value, duration); + + return this; + } + + /// + protected override PipelineBuilder OnPipelineRequested() => this.Pipeline; + + /// + /// Clones the current instance by rebuilding the source . Use this method to reuse the same effects pipeline on a different + /// + /// + /// If your code is already on the same thread, you can just assign this brush to an arbitrary number of controls and it will still work correctly. + /// This method is only meant to be used to create a new instance of this brush using the same pipeline, on threads that can't access the current instance, for example in secondary app windows. + /// + /// A instance using the current effects pipeline + [Pure] + public XamlCompositionBrush Clone() + { + return new XamlCompositionBrush(this.Pipeline); + } +} \ No newline at end of file diff --git a/components/Media/src/CommunityToolkit.WinUI.Media.csproj b/components/Media/src/CommunityToolkit.WinUI.Media.csproj new file mode 100644 index 00000000..dc295949 --- /dev/null +++ b/components/Media/src/CommunityToolkit.WinUI.Media.csproj @@ -0,0 +1,17 @@ + + + Media + This package contains Media. + 8.0.0-beta.1 + + + CommunityToolkit.WinUI.MediaRns + + + + + + + + + diff --git a/components/Media/src/Dependencies.props b/components/Media/src/Dependencies.props new file mode 100644 index 00000000..07187681 --- /dev/null +++ b/components/Media/src/Dependencies.props @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/components/Media/src/Effects/Abstract/ImageSourceBaseExtension.cs b/components/Media/src/Effects/Abstract/ImageSourceBaseExtension.cs new file mode 100644 index 00000000..011b6c0c --- /dev/null +++ b/components/Media/src/Effects/Abstract/ImageSourceBaseExtension.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An image based effect that loads an image at the specified location +/// +[MarkupExtensionReturnType(ReturnType = typeof(PipelineBuilder))] +public abstract class ImageSourceBaseExtension : MarkupExtension +{ + /// + /// Gets or sets the for the image to load + /// + public Uri? Uri { get; set; } + + /// + /// Gets or sets the DPI mode used to render the image (the default is ) + /// + public DpiMode DpiMode { get; set; } = DpiMode.DisplayDpiWith96AsLowerBound; + + /// + /// Gets or sets the cache mode to use when loading the image (the default is ) + /// + public CacheMode CacheMode { get; set; } = CacheMode.Default; +} diff --git a/components/Media/src/Effects/Abstract/PipelineEffect.cs b/components/Media/src/Effects/Abstract/PipelineEffect.cs new file mode 100644 index 00000000..0a59447f --- /dev/null +++ b/components/Media/src/Effects/Abstract/PipelineEffect.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A base pipeline effect. +/// +public abstract class PipelineEffect : DependencyObject, IPipelineEffect +{ + /// + public CompositionBrush? Brush { get; private set; } + + /// + /// Gets or sets a value indicating whether the effect can be animated. + /// + public bool IsAnimatable { get; set; } + + /// + public abstract PipelineBuilder AppendToBuilder(PipelineBuilder builder); + + /// + public virtual void NotifyCompositionBrushInUse(CompositionBrush brush) + { + Brush = brush; + } +} diff --git a/components/Media/src/Effects/BlendEffect.cs b/components/Media/src/Effects/BlendEffect.cs new file mode 100644 index 00000000..04933d3d --- /dev/null +++ b/components/Media/src/Effects/BlendEffect.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Graphics.Canvas.Effects; +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A blend effect that merges the current builder with an input one +/// +/// This effect maps to the Win2D effect +[ContentProperty(Name = nameof(Effects))] +public sealed class BlendEffect : PipelineEffect +{ + /// + /// Gets or sets the input to merge with the current instance (defaults to a with source). + /// + public PipelineBuilder? Source { get; set; } + + /// + /// Gets or sets the effects to apply to the input to merge with the current instance + /// + public List Effects { get; set; } = new List(); + + /// + /// Gets or sets the blending mode to use (the default mode is ) + /// + public ImageBlendMode Mode { get; set; } + + /// + /// Gets or sets the placement of the input builder with respect to the current one (the default is ) + /// + public Placement Placement { get; set; } = Placement.Foreground; + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + PipelineBuilder inputBuilder = Source ?? PipelineBuilder.FromBackdrop(); + + foreach (IPipelineEffect effect in Effects) + { + inputBuilder = effect.AppendToBuilder(inputBuilder); + } + + return builder.Blend(inputBuilder, (BlendEffectMode)Mode, Placement); + } + + /// + public override void NotifyCompositionBrushInUse(CompositionBrush brush) + { + base.NotifyCompositionBrushInUse(brush); + + foreach (IPipelineEffect effect in Effects) + { + effect.NotifyCompositionBrushInUse(brush); + } + } +} diff --git a/components/Media/src/Effects/BlurEffect.cs b/components/Media/src/Effects/BlurEffect.cs new file mode 100644 index 00000000..42284934 --- /dev/null +++ b/components/Media/src/Effects/BlurEffect.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A gaussian blur effect +/// +/// This effect maps to the Win2D effect +public sealed class BlurEffect : PipelineEffect +{ + private double amount; + + /// + /// Gets or sets the blur amount for the effect (must be a positive value) + /// + public double Amount + { + get => this.amount; + set => this.amount = Math.Max(value, 0); + } + + /// + /// Gets the unique id for the effect, if is set. + /// + internal string? Id { get; private set; } + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Blur((float)Amount, out string id); + + Id = id; + + return builder; + } + + return builder.Blur((float)Amount); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/CrossFadeEffect.cs b/components/Media/src/Effects/CrossFadeEffect.cs new file mode 100644 index 00000000..84210ab0 --- /dev/null +++ b/components/Media/src/Effects/CrossFadeEffect.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A blend effect that merges the current builder with an input one +/// +/// This effect maps to the Win2D effect +[ContentProperty(Name = nameof(Effects))] +public sealed class CrossFadeEffect : PipelineEffect +{ + /// + /// Gets or sets the input to merge with the current instance (defaults to a with source). + /// + public PipelineBuilder? Source { get; set; } + + /// + /// Gets or sets the effects to apply to the input to merge with the current instance + /// + public List Effects { get; set; } = new List(); + + private double factor = 0.5; + + /// + /// Gets or sets the The cross fade factor to blend the input effects (default to 0.5, should be in the [0, 1] range) + /// + public double Factor + { + get => this.factor; + set => this.factor = Math.Clamp(value, 0, 1); + } + + /// + /// Gets the unique id for the effect, if is set. + /// + internal string? Id { get; private set; } + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + PipelineBuilder inputBuilder = Source ?? PipelineBuilder.FromBackdrop(); + + foreach (IPipelineEffect effect in Effects) + { + inputBuilder = effect.AppendToBuilder(inputBuilder); + } + + if (IsAnimatable) + { + builder = builder.CrossFade(inputBuilder, (float)Factor, out string id); + + Id = id; + + return builder; + } + + return builder.CrossFade(inputBuilder, (float)Factor); + } + + /// + public override void NotifyCompositionBrushInUse(CompositionBrush brush) + { + base.NotifyCompositionBrushInUse(brush); + + foreach (IPipelineEffect effect in Effects) + { + effect.NotifyCompositionBrushInUse(brush); + } + } +} diff --git a/components/Media/src/Effects/ExposureEffect.cs b/components/Media/src/Effects/ExposureEffect.cs new file mode 100644 index 00000000..5f19ac8e --- /dev/null +++ b/components/Media/src/Effects/ExposureEffect.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An exposure effect +/// +/// This effect maps to the Win2D effect +public sealed class ExposureEffect : PipelineEffect +{ + private double amount; + + /// + /// Gets or sets the amount of exposure to apply to the background (defaults to 0, should be in the [-2, 2] range). + /// + public double Amount + { + get => this.amount; + set => this.amount = Math.Clamp(value, -2, 2); + } + + /// + /// Gets the unique id for the effect, if is set. + /// + internal string? Id { get; private set; } + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Exposure((float)Amount, out string id); + + Id = id; + + return builder; + } + + return builder.Exposure((float)Amount); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/Extensions/AcrylicSourceExtension.cs b/components/Media/src/Effects/Extensions/AcrylicSourceExtension.cs new file mode 100644 index 00000000..cb555ab2 --- /dev/null +++ b/components/Media/src/Effects/Extensions/AcrylicSourceExtension.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINUI2 + +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A custom acrylic effect that can be inserted into a pipeline +/// +/// This effect mirrors the look of the default implementation +[MarkupExtensionReturnType(ReturnType = typeof(PipelineBuilder))] +public sealed class AcrylicSourceExtension : MarkupExtension +{ + /// + /// Gets or sets the background source mode for the effect (the default is ). + /// + public AcrylicBackgroundSource BackgroundSource { get; set; } = AcrylicBackgroundSource.Backdrop; + + private double blurAmount; + + /// + /// Gets or sets the blur amount for the effect (must be a positive value) + /// + /// This property is ignored when the active mode is + public double BlurAmount + { + get => this.blurAmount; + set => this.blurAmount = Math.Max(value, 0); + } + + /// + /// Gets or sets the tint for the effect + /// + public Color TintColor { get; set; } + + private double tintOpacity = 0.5f; + + /// + /// Gets or sets the color for the tint effect (default is 0.5, must be in the [0, 1] range) + /// + public double TintOpacity + { + get => this.tintOpacity; + set => this.tintOpacity = Math.Clamp(value, 0, 1); + } + + /// + /// Gets or sets the to the texture to use + /// + public Uri? TextureUri { get; set; } + + /// + protected override object ProvideValue() + { + return BackgroundSource switch + { + AcrylicBackgroundSource.Backdrop => PipelineBuilder.FromBackdropAcrylic(this.TintColor, (float)this.TintOpacity, (float)BlurAmount, TextureUri), + AcrylicBackgroundSource.HostBackdrop => PipelineBuilder.FromHostBackdropAcrylic(this.TintColor, (float)this.TintOpacity, TextureUri), + _ => throw new ArgumentException($"Invalid source mode for acrylic effect: {BackgroundSource}") + }; + } +} + +#endif diff --git a/components/Media/src/Effects/Extensions/BackdropSourceExtension.cs b/components/Media/src/Effects/Extensions/BackdropSourceExtension.cs new file mode 100644 index 00000000..6ed06966 --- /dev/null +++ b/components/Media/src/Effects/Extensions/BackdropSourceExtension.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINDOWS_UWP + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A backdrop effect that can sample from a specified source +/// +[MarkupExtensionReturnType(ReturnType = typeof(PipelineBuilder))] +public sealed class BackdropSourceExtension : MarkupExtension +{ + /// + /// Gets or sets the background source mode for the effect (the default is ). + /// + public AcrylicBackgroundSource BackgroundSource { get; set; } = AcrylicBackgroundSource.Backdrop; + + /// + protected override object ProvideValue() + { + return BackgroundSource switch + { + AcrylicBackgroundSource.Backdrop => PipelineBuilder.FromBackdrop(), + AcrylicBackgroundSource.HostBackdrop => PipelineBuilder.FromHostBackdrop(), + _ => throw new ArgumentException($"Invalid source for backdrop effect: {BackgroundSource}") + }; + } +} +#endif diff --git a/components/Media/src/Effects/Extensions/ImageSourceExtension.cs b/components/Media/src/Effects/Extensions/ImageSourceExtension.cs new file mode 100644 index 00000000..8c04b7a9 --- /dev/null +++ b/components/Media/src/Effects/Extensions/ImageSourceExtension.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An image effect, which displays an image loaded as a Win2D surface +/// +public sealed class ImageSourceExtension : ImageSourceBaseExtension +{ + /// + protected override object ProvideValue() + { + default(ArgumentNullException).ThrowIfNull(Uri); + return PipelineBuilder.FromImage(Uri, DpiMode, CacheMode); + } +} diff --git a/components/Media/src/Effects/Extensions/SolidColorSourceExtension.cs b/components/Media/src/Effects/Extensions/SolidColorSourceExtension.cs new file mode 100644 index 00000000..1f713f44 --- /dev/null +++ b/components/Media/src/Effects/Extensions/SolidColorSourceExtension.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An effect that renders a standard 8bit SDR color on the available surface +/// +[MarkupExtensionReturnType(ReturnType = typeof(PipelineBuilder))] +public sealed class SolidColorSourceExtension : MarkupExtension +{ + /// + /// Gets or sets the color to display + /// + public Color Color { get; set; } + + /// + protected override object ProvideValue() + { + return PipelineBuilder.FromColor(Color); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/Extensions/TileSourceExtension.cs b/components/Media/src/Effects/Extensions/TileSourceExtension.cs new file mode 100644 index 00000000..a68956ee --- /dev/null +++ b/components/Media/src/Effects/Extensions/TileSourceExtension.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An effect that loads an image and replicates it to cover all the available surface area +/// +/// This effect maps to the Win2D effect +public sealed class TileSourceExtension : ImageSourceBaseExtension +{ + /// + protected override object ProvideValue() + { + default(ArgumentNullException).ThrowIfNull(Uri); + return PipelineBuilder.FromTiles(Uri, DpiMode, CacheMode); + } +} diff --git a/components/Media/src/Effects/GrayscaleEffect.cs b/components/Media/src/Effects/GrayscaleEffect.cs new file mode 100644 index 00000000..c9f5a943 --- /dev/null +++ b/components/Media/src/Effects/GrayscaleEffect.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A grayscale effect +/// +/// This effect maps to the Win2D effect +public sealed class GrayscaleEffect : PipelineEffect +{ + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + return builder.Grayscale(); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/HueRotationEffect.cs b/components/Media/src/Effects/HueRotationEffect.cs new file mode 100644 index 00000000..2b4d80b3 --- /dev/null +++ b/components/Media/src/Effects/HueRotationEffect.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A hue rotation effect +/// +/// This effect maps to the Win2D effect +public sealed class HueRotationEffect : PipelineEffect +{ + /// + /// Gets or sets the angle to rotate the hue, in radians + /// + public double Angle { get; set; } + + /// + /// Gets the unique id for the effect, if is set. + /// + internal string? Id { get; private set; } + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.HueRotation((float)Angle, out string id); + + Id = id; + + return builder; + } + + return builder.HueRotation((float)Angle); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/Interfaces/IPipelineEffect.cs b/components/Media/src/Effects/Interfaces/IPipelineEffect.cs new file mode 100644 index 00000000..e93a5a00 --- /dev/null +++ b/components/Media/src/Effects/Interfaces/IPipelineEffect.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// +/// The base for all the builder effects to be used in a . +/// +public interface IPipelineEffect +{ + /// + /// Gets the current instance, if one is in use. + /// + CompositionBrush? Brush { get; } + + /// + /// Appends the current effect to the input instance. + /// + /// The source instance to add the effect to. + /// A new with the new effects added to it. + PipelineBuilder AppendToBuilder(PipelineBuilder builder); + + /// + /// Notifies that a given is now in use. + /// + /// The in use. + void NotifyCompositionBrushInUse(CompositionBrush brush); +} diff --git a/components/Media/src/Effects/InvertEffect.cs b/components/Media/src/Effects/InvertEffect.cs new file mode 100644 index 00000000..9ec5437b --- /dev/null +++ b/components/Media/src/Effects/InvertEffect.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An color inversion effect +/// +/// This effect maps to the Win2D effect +public sealed class InvertEffect : PipelineEffect +{ + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + return builder.Invert(); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/LuminanceToAlphaEffect.cs b/components/Media/src/Effects/LuminanceToAlphaEffect.cs new file mode 100644 index 00000000..789f9829 --- /dev/null +++ b/components/Media/src/Effects/LuminanceToAlphaEffect.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A luminance to alpha effect +/// +/// This effect maps to the Win2D effect +public sealed class LuminanceToAlphaEffect : PipelineEffect +{ + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + return builder.LuminanceToAlpha(); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/OpacityEffect.cs b/components/Media/src/Effects/OpacityEffect.cs new file mode 100644 index 00000000..e4696415 --- /dev/null +++ b/components/Media/src/Effects/OpacityEffect.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An opacity effect +/// +/// This effect maps to the Win2D effect +public sealed class OpacityEffect : PipelineEffect +{ + private double value = 1; + + /// + /// Gets or sets the opacity value to apply to the background (defaults to 1, should be in the [0, 1] range). + /// + public double Value + { + get => this.value; + set => this.value = Math.Clamp(value, 0, 1); + } + + /// + /// Gets the unique id for the effect, if is set. + /// + internal string? Id { get; private set; } + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Opacity((float)Value, out string id); + + Id = id; + + return builder; + } + + return builder.Opacity((float)Value); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/SaturationEffect.cs b/components/Media/src/Effects/SaturationEffect.cs new file mode 100644 index 00000000..83ac5d18 --- /dev/null +++ b/components/Media/src/Effects/SaturationEffect.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A saturation effect +/// +/// This effect maps to the Win2D effect +public sealed class SaturationEffect : PipelineEffect +{ + private double value = 1; + + /// + /// Gets or sets the saturation amount to apply to the background (defaults to 1, should be in the [0, 1] range). + /// + public double Value + { + get => this.value; + set => this.value = Math.Clamp(value, 0, 1); + } + + /// + /// Gets the unique id for the effect, if is set. + /// + internal string? Id { get; private set; } + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Saturation((float)Value, out string id); + + Id = id; + + return builder; + } + + return builder.Saturation((float)Value); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/SepiaEffect.cs b/components/Media/src/Effects/SepiaEffect.cs new file mode 100644 index 00000000..e393ea67 --- /dev/null +++ b/components/Media/src/Effects/SepiaEffect.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A sepia effect +/// +/// This effect maps to the Win2D effect +public sealed class SepiaEffect : PipelineEffect +{ + private double intensity = 0.5; + + /// + /// Gets or sets the intensity of the effect (defaults to 0.5, should be in the [0, 1] range). + /// + public double Intensity + { + get => this.intensity; + set => this.intensity = Math.Clamp(value, 0, 1); + } + + /// + /// Gets the unique id for the effect, if is set. + /// + internal string? Id { get; private set; } + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Sepia((float)Intensity, out string id); + + Id = id; + + return builder; + } + + return builder.Sepia((float)Intensity); + } +} diff --git a/components/Media/src/Effects/ShadeEffect.cs b/components/Media/src/Effects/ShadeEffect.cs new file mode 100644 index 00000000..e56169b6 --- /dev/null +++ b/components/Media/src/Effects/ShadeEffect.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An effect that overlays a color layer over the current builder, with a specified intensity +/// +public sealed class ShadeEffect : PipelineEffect +{ + /// + /// Gets or sets the color to use + /// + public Color Color { get; set; } + + private double intensity = 0.5; + + /// + /// Gets or sets the intensity of the color layer (default to 0.5, should be in the [0, 1] range) + /// + public double Intensity + { + get => this.intensity; + set => this.intensity = Math.Clamp(value, 0, 1); + } + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + return builder.Shade(Color, (float)Intensity); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/TemperatureAndTintEffect.cs b/components/Media/src/Effects/TemperatureAndTintEffect.cs new file mode 100644 index 00000000..385fb237 --- /dev/null +++ b/components/Media/src/Effects/TemperatureAndTintEffect.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A temperature and tint effect +/// +/// This effect maps to the Win2D effect +public sealed class TemperatureAndTintEffect : PipelineEffect +{ + private double temperature; + + /// + /// Gets or sets the value of the temperature for the current effect (defaults to 0, should be in the [-1, 1] range) + /// + public double Temperature + { + get => this.temperature; + set => this.temperature = Math.Clamp(value, -1, 1); + } + + private double tint; + + /// + /// Gets or sets the value of the tint for the current effect (defaults to 0, should be in the [-1, 1] range) + /// + public double Tint + { + get => this.tint; + set => this.tint = Math.Clamp(value, -1, 1); + } + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + return builder.TemperatureAndTint((float)Temperature, (float)Tint); + } +} \ No newline at end of file diff --git a/components/Media/src/Effects/TintEffect.cs b/components/Media/src/Effects/TintEffect.cs new file mode 100644 index 00000000..e986481e --- /dev/null +++ b/components/Media/src/Effects/TintEffect.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; +using Windows.UI; + +#nullable enable + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A tint effect +/// +/// This effect maps to the Win2D effect +public sealed class TintEffect : PipelineEffect +{ + /// + /// Gets or sets the int color to use + /// + public Color Color { get; set; } + + /// + /// Gets the unique id for the effect, if is set. + /// + internal string? Id { get; private set; } + + /// + public override PipelineBuilder AppendToBuilder(PipelineBuilder builder) + { + if (IsAnimatable) + { + builder = builder.Tint(Color, out string id); + + Id = id; + + return builder; + } + + return builder.Tint(Color); + } +} \ No newline at end of file diff --git a/components/Media/src/Enums/AlphaMode.cs b/components/Media/src/Enums/AlphaMode.cs new file mode 100644 index 00000000..1cfa2b31 --- /dev/null +++ b/components/Media/src/Enums/AlphaMode.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Media; + +/// +/// Specifies the way in which an alpha channel affects color channels. +/// +public enum AlphaMode +{ + /// + /// Provides better transparent effects without a white bloom. + /// + Premultiplied = 0, + + /// + /// WPF default handling of alpha channel during transparent blending. + /// + Straight = 1, +} \ No newline at end of file diff --git a/components/Media/src/Enums/CacheMode.cs b/components/Media/src/Enums/CacheMode.cs new file mode 100644 index 00000000..57153b0b --- /dev/null +++ b/components/Media/src/Enums/CacheMode.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Media; + +/// +/// Indicates the cache mode to use when loading a Win2D image +/// +public enum CacheMode +{ + /// + /// The default behavior, the cache is enabled + /// + Default, + + /// + /// Reload the target image and overwrite the cached entry, if it exists + /// + Overwrite, + + /// + /// The cache is disabled and new images are always reloaded + /// + Disabled +} \ No newline at end of file diff --git a/components/Media/src/Enums/DpiMode.cs b/components/Media/src/Enums/DpiMode.cs new file mode 100644 index 00000000..4167eac9 --- /dev/null +++ b/components/Media/src/Enums/DpiMode.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Media; + +/// +/// Indicates the DPI mode to use to load an image +/// +public enum DpiMode +{ + /// + /// Uses the original DPI settings of the loaded image + /// + UseSourceDpi, + + /// + /// Uses the default value of 96 DPI + /// + Default96Dpi, + + /// + /// Overrides the image DPI settings with the current screen DPI value + /// + DisplayDpi, + + /// + /// Overrides the image DPI settings with the current screen DPI value and ensures the resulting value is at least 96 + /// + DisplayDpiWith96AsLowerBound +} \ No newline at end of file diff --git a/components/Media/src/Enums/ImageBlendMode.cs b/components/Media/src/Enums/ImageBlendMode.cs new file mode 100644 index 00000000..28d997f5 --- /dev/null +++ b/components/Media/src/Enums/ImageBlendMode.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//// Composition supported version of http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_BlendEffectMode.htm. + +using Microsoft.Graphics.Canvas.Effects; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - see http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_BlendEffectMode.htm. + +namespace CommunityToolkit.WinUI.Media; + +/// +/// Blend mode to use when compositing effects. +/// See http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_BlendEffectMode.htm for details. +/// Dissolve is not supported. +/// +public enum ImageBlendMode +{ + Multiply = BlendEffectMode.Multiply, + Screen = BlendEffectMode.Screen, + Darken = BlendEffectMode.Darken, + Lighten = BlendEffectMode.Lighten, + ColorBurn = BlendEffectMode.ColorBurn, + LinearBurn = BlendEffectMode.LinearBurn, + DarkerColor = BlendEffectMode.DarkerColor, + LighterColor = BlendEffectMode.LighterColor, + ColorDodge = BlendEffectMode.ColorDodge, + LinearDodge = BlendEffectMode.LinearDodge, + Overlay = BlendEffectMode.Overlay, + SoftLight = BlendEffectMode.SoftLight, + HardLight = BlendEffectMode.HardLight, + VividLight = BlendEffectMode.VividLight, + LinearLight = BlendEffectMode.LinearLight, + PinLight = BlendEffectMode.PinLight, + HardMix = BlendEffectMode.HardMix, + Difference = BlendEffectMode.Difference, + Exclusion = BlendEffectMode.Exclusion, + + /// + /// Hue blend mode. + /// + Hue = BlendEffectMode.Hue, + + /// + /// Saturation blend mode. + /// + Saturation = BlendEffectMode.Saturation, + + /// + /// Color blend mode. + /// + Color = BlendEffectMode.Color, + + /// + /// Luminosity blend mode. + /// + Luminosity = BlendEffectMode.Luminosity, + Subtract = BlendEffectMode.Subtract, + Division = BlendEffectMode.Division, +} \ No newline at end of file diff --git a/components/Media/src/Enums/InnerContentClipMode.cs b/components/Media/src/Enums/InnerContentClipMode.cs new file mode 100644 index 00000000..a154ebb6 --- /dev/null +++ b/components/Media/src/Enums/InnerContentClipMode.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Media; + +/// +/// The method that each instance of uses when clipping its inner content. +/// +public enum InnerContentClipMode +{ + /// + /// Do not clip inner content. + /// + None, + + /// + /// Use to clip inner content. + /// + /// + /// This mode has better performance than . + /// + CompositionMaskBrush, + + /// + /// Use to clip inner content. + /// + /// + /// Content clipped in this mode will have smoother corners than when using . + /// + CompositionGeometricClip +} diff --git a/components/Media/src/Enums/Placement.cs b/components/Media/src/Enums/Placement.cs new file mode 100644 index 00000000..b2c6854c --- /dev/null +++ b/components/Media/src/Enums/Placement.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.Graphics.Effects; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An used to modify the default placement of the input instance in a blend operation +/// +public enum Placement +{ + /// + /// The instance used to call the blend method is placed on top of the other + /// + Foreground, + + /// + /// The instance used to call the blend method is placed behind the other + /// + Background +} \ No newline at end of file diff --git a/components/Media/src/Extensions/System.Collections.Generic/GenericExtensions.cs b/components/Media/src/Extensions/System.Collections.Generic/GenericExtensions.cs new file mode 100644 index 00000000..ce09a05f --- /dev/null +++ b/components/Media/src/Extensions/System.Collections.Generic/GenericExtensions.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An extension for the +/// +internal static class GenericExtensions +{ + /// + /// Merges the two input instances and makes sure no duplicate keys are present + /// + /// The type of keys in the input dictionaries + /// The type of values in the input dictionaries + /// The first to merge + /// The second to merge + /// An instance with elements from both and + [Pure] + public static IReadOnlyDictionary Merge( + this IReadOnlyDictionary a, + IReadOnlyDictionary b) + where TKey : notnull + { + if (a.Keys.FirstOrDefault(b.ContainsKey) is TKey key) + { + throw new InvalidOperationException($"The key {key} already exists in the current pipeline"); + } + + return new Dictionary(a.Concat(b)); + } + + /// + /// Merges the two input instances and makes sure no duplicate items are present + /// + /// The type of elements in the input collections + /// The first to merge + /// The second to merge + /// An instance with elements from both and + [Pure] + public static IReadOnlyCollection Merge(this IReadOnlyCollection a, IReadOnlyCollection b) + { + if (a.Any(b.Contains)) + { + throw new InvalidOperationException("The input collection has at least an item already present in the second collection"); + } + + return a.Concat(b).ToArray(); + } +} diff --git a/components/Media/src/Extensions/System.Threading.Tasks/AsyncMutex.cs b/components/Media/src/Extensions/System.Threading.Tasks/AsyncMutex.cs new file mode 100644 index 00000000..d30e29ac --- /dev/null +++ b/components/Media/src/Extensions/System.Threading.Tasks/AsyncMutex.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An implementation that can be easily used inside a block +/// +#pragma warning disable CA1001 // Types that own disposable fields should be disposable +internal sealed class AsyncMutex +#pragma warning restore CA1001 // Types that own disposable fields should be disposable +{ + /// + /// The underlying instance in use + /// + private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); + + /// + /// Acquires a lock for the current instance, that is automatically released outside the block + /// + /// A that returns an instance to release the lock + public async Task LockAsync() + { + await this.semaphore.WaitAsync().ConfigureAwait(false); + + return new Lock(this.semaphore); + } + + /// + /// Private class that implements the automatic release of the semaphore + /// + private sealed class Lock : IDisposable + { + /// + /// The instance of the parent class + /// + private readonly SemaphoreSlim semaphore; + + /// + /// Initializes a new instance of the class. + /// + /// The instance of the parent class + public Lock(SemaphoreSlim semaphore) + { + this.semaphore = semaphore; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void IDisposable.Dispose() + { + this.semaphore.Release(); + } + } +} diff --git a/components/Media/src/Extensions/System/UriExtensions.cs b/components/Media/src/Extensions/System/UriExtensions.cs new file mode 100644 index 00000000..71c20e80 --- /dev/null +++ b/components/Media/src/Extensions/System/UriExtensions.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An extension for the type +/// +internal static class UriExtensions +{ + /// + /// Returns an that starts with the ms-appx:// prefix + /// + /// The input to process + /// A equivalent to the first but relative to ms-appx:// + /// This is needed because the XAML converter doesn't use the ms-appx:// prefix + [Pure] + public static Uri ToAppxUri(this Uri uri) + { + if (uri.Scheme.Equals("ms-resource")) + { + string path = uri.AbsolutePath.StartsWith("/Files") + ? uri.AbsolutePath.Replace("/Files", string.Empty) + : uri.AbsolutePath; + + return new Uri($"ms-appx://{path}"); + } + + return uri; + } + + /// + /// Returns an that starts with the ms-appx:// prefix + /// + /// The input relative path to convert + /// A with relative to ms-appx:// + [Pure] + public static Uri ToAppxUri(this string path) + { + string prefix = $"ms-appx://{(path.StartsWith('/') ? string.Empty : "/")}"; + + return new Uri($"{prefix}{path}"); + } +} \ No newline at end of file diff --git a/components/Media/src/Extensions/UIElementExtensions.cs b/components/Media/src/Extensions/UIElementExtensions.cs new file mode 100644 index 00000000..569fa16f --- /dev/null +++ b/components/Media/src/Extensions/UIElementExtensions.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; + +#if WINUI3 +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Hosting; +#elif WINUI2 +using Windows.UI.Composition; +using Windows.UI.Xaml.Hosting; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// Attached properties to support attaching custom pipelines to UI elements. +/// +public static class UIElementExtensions +{ + /// + /// Identifies the VisualFactory XAML attached property. + /// + public static readonly DependencyProperty VisualFactoryProperty = DependencyProperty.RegisterAttached( + "VisualFactory", + typeof(AttachedVisualFactoryBase), + typeof(UIElementExtensions), + new PropertyMetadata(null, OnVisualFactoryPropertyChanged)); + + /// + /// Gets the value of . + /// + /// The to get the value for. + /// The retrieved item. + public static AttachedVisualFactoryBase GetVisualFactory(UIElement element) + { + return (AttachedVisualFactoryBase)element.GetValue(VisualFactoryProperty); + } + + /// + /// Sets the value of . + /// + /// The to set the value for. + /// The value to set. + public static void SetVisualFactory(UIElement element, AttachedVisualFactoryBase value) + { + element.SetValue(VisualFactoryProperty, value); + } + + /// + /// Callback to apply the visual for . + /// + /// The target object the property was changed for. + /// The instance for the current event. + private static async void OnVisualFactoryPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + UIElement element = (UIElement)d; + Visual attachedVisual = await ((AttachedVisualFactoryBase)e.NewValue).GetAttachedVisualAsync(element); + + attachedVisual.RelativeSizeAdjustment = Vector2.One; + + ElementCompositionPreview.SetElementChildVisual(element, attachedVisual); + } +} diff --git a/components/Media/src/Extensions/Windows.UI.Composition/CompositionObjectExtensions.cs b/components/Media/src/Extensions/Windows.UI.Composition/CompositionObjectExtensions.cs new file mode 100644 index 00000000..195b0ce3 --- /dev/null +++ b/components/Media/src/Extensions/Windows.UI.Composition/CompositionObjectExtensions.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using Windows.UI; + +#if WINUI3 +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Hosting; +#elif WINUI2 +using Windows.UI.Composition; +using Windows.UI.Xaml.Hosting; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// An extension for the type +/// +internal static class CompositionObjectExtensions +{ + /// + /// Starts an to keep the size of the source in sync with the target + /// + /// The to start the animation on + /// The target to read the size updates from + public static void BindSize(this Visual source, UIElement target) + { + var visual = ElementCompositionPreview.GetElementVisual(target); + var bindSizeAnimation = source.Compositor.CreateExpressionAnimation($"{nameof(visual)}.Size"); + + bindSizeAnimation.SetReferenceParameter(nameof(visual), visual); + + // Start the animation + source.StartAnimation("Size", bindSizeAnimation); + } + + /// + /// Starts an animation on the given property of a + /// + /// The type of the property to animate + /// The target + /// The name of the property to animate + /// The final value of the property + /// The animation duration + /// A that completes when the created animation completes + public static Task StartAnimationAsync(this CompositionObject target, string property, T value, TimeSpan duration) + where T : unmanaged + { + // Stop previous animations + target.StopAnimation(property); + + // Setup the animation to run + KeyFrameAnimation animation; + switch (value) + { + case float f: + var scalarAnimation = target.Compositor.CreateScalarKeyFrameAnimation(); + scalarAnimation.InsertKeyFrame(1f, f); + animation = scalarAnimation; + break; + case Color c: + var colorAnimation = target.Compositor.CreateColorKeyFrameAnimation(); + colorAnimation.InsertKeyFrame(1f, c); + animation = colorAnimation; + break; + case Vector4 v4: + var vector4Animation = target.Compositor.CreateVector4KeyFrameAnimation(); + vector4Animation.InsertKeyFrame(1f, v4); + animation = vector4Animation; + break; + default: throw new ArgumentException($"Invalid animation type: {typeof(T)}", nameof(value)); + } + + animation.Duration = duration; + + // Get the batch and start the animations + var batch = target.Compositor.CreateScopedBatch(CompositionBatchTypes.Animation); + + var tcs = new TaskCompletionSource(); + + batch.Completed += (s, e) => tcs.SetResult(null); + + target.StartAnimation(property, animation); + + batch.End(); + + return tcs.Task; + } +} diff --git a/components/Media/src/Helpers/Cache/CompositionObjectCache{TKey,TValue}.cs b/components/Media/src/Helpers/Cache/CompositionObjectCache{TKey,TValue}.cs new file mode 100644 index 00000000..7846b220 --- /dev/null +++ b/components/Media/src/Helpers/Cache/CompositionObjectCache{TKey,TValue}.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Helpers.Cache; + +/// +/// A used to cache reusable instances with an associated key +/// +/// The type of key to classify the items in the cache +/// The type of items stored in the cache +internal sealed class CompositionObjectCache + where TValue : CompositionObject + where TKey : notnull +{ + /// + /// The cache of weak references of type to instances, to avoid memory leaks + /// + private readonly ConditionalWeakTable>> cache = new ConditionalWeakTable>>(); + + /// + /// Tries to retrieve a valid instance from the cache, and uses the provided factory if an existing item is not found + /// + /// The current instance to get the value for + /// The key to look for + /// The resulting value, if existing + /// if the target value has been found, otherwise + public bool TryGetValue(Compositor compositor, TKey key, out TValue? result) + { + lock (this.cache) + { + if (this.cache.TryGetValue(compositor, out var map) && + map.TryGetValue(key, out var reference) && + reference.TryGetTarget(out result)) + { + return true; + } + + result = null; + return false; + } + } + + /// + /// Adds or updates a value with the specified key to the cache + /// + /// The current instance to get the value for + /// The key of the item to add + /// The value to add + public void AddOrUpdate(Compositor compositor, TKey key, TValue value) + { + lock (this.cache) + { + if (this.cache.TryGetValue(compositor, out var map)) + { + _ = map.Remove(key); + + map.Add(key, new WeakReference(value)); + } + else + { + map = new Dictionary> { [key] = new WeakReference(value) }; + + this.cache.Add(compositor, map); + } + } + } +} diff --git a/components/Media/src/Helpers/Cache/CompositionObjectCache{T}.cs b/components/Media/src/Helpers/Cache/CompositionObjectCache{T}.cs new file mode 100644 index 00000000..6465b941 --- /dev/null +++ b/components/Media/src/Helpers/Cache/CompositionObjectCache{T}.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +#if WINUI2 +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Helpers.Cache; + +/// +/// A used to cache reusable instances in each UI thread +/// +/// The type of instances to cache +internal sealed class CompositionObjectCache + where T : CompositionObject +{ + /// + /// The cache of weak references of type , to avoid memory leaks + /// + private readonly ConditionalWeakTable> cache = new ConditionalWeakTable>(); + + /// + /// Tries to retrieve a valid instance from the cache, and uses the provided factory if an existing item is not found + /// + /// The current instance to get the value for + /// A instance used to produce a instance + /// A instance that is linked to + public T GetValue(Compositor compositor, Func producer) + { + lock (cache) + { + if (this.cache.TryGetValue(compositor, out var reference) && + reference.TryGetTarget(out var instance)) + { + return instance; + } + + // Create a new instance when needed + var fallback = producer(compositor); + this.cache.AddOrUpdate(compositor, new WeakReference(fallback)); + + return fallback; + } + } +} diff --git a/components/Media/src/Helpers/SurfaceLoader.Instance.cs b/components/Media/src/Helpers/SurfaceLoader.Instance.cs new file mode 100644 index 00000000..013e4aad --- /dev/null +++ b/components/Media/src/Helpers/SurfaceLoader.Instance.cs @@ -0,0 +1,229 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Text; +using Microsoft.Graphics.Canvas.UI.Composition; +using Windows.UI; + +#if WINUI2 +using Windows.Graphics.DirectX; +using Windows.UI.Composition; +#elif WINUI3 +using Microsoft.Graphics.DirectX; +using Microsoft.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Helpers; + +/// +/// A delegate for load time effects. +/// +/// The bitmap. +/// The device. +/// The size target. +/// A CompositeDrawingSurface +public delegate CompositionDrawingSurface LoadTimeEffectHandler(CanvasBitmap bitmap, CompositionGraphicsDevice device, Size sizeTarget); + +/// +/// A that can load and draw images and other objects to Win2D surfaces and brushes +/// +public sealed partial class SurfaceLoader : IDisposable +{ + /// + /// The cache of instances currently available + /// + private static readonly ConditionalWeakTable Instances = new ConditionalWeakTable(); + + /// + /// Gets a instance for the of the current window + /// + /// A instance to use in the current window + public static SurfaceLoader GetInstance() + { + return GetInstance(Window.Current.Compositor); + } + + /// + /// Gets a instance for a given + /// + /// The input object to use + /// A instance associated with + public static SurfaceLoader GetInstance(Compositor compositor) + { + lock (Instances) + { + if (Instances.TryGetValue(compositor, out var instance)) + { + return instance; + } + + instance = new SurfaceLoader(compositor); + + Instances.Add(compositor, instance); + + return instance; + } + } + + /// + /// The instance in use. + /// + private readonly Compositor compositor; + + /// + /// The instance in use. + /// + private CanvasDevice? canvasDevice; + + /// + /// The instance to determine which GPU is handling the request. + /// + private CompositionGraphicsDevice? compositionDevice; + + /// + /// Initializes a new instance of the class. + /// + /// The instance to use + private SurfaceLoader(Compositor compositor) + { + this.compositor = compositor; + + this.InitializeDevices(); + } + + /// + /// Reloads the and fields. + /// + private void InitializeDevices() + { + if (!(this.canvasDevice is null)) + { + this.canvasDevice.DeviceLost -= CanvasDevice_DeviceLost; + } + + if (!(this.compositionDevice is null)) + { + this.compositionDevice.RenderingDeviceReplaced -= CompositionDevice_RenderingDeviceReplaced; + } + + this.canvasDevice = new CanvasDevice(); + this.compositionDevice = CanvasComposition.CreateCompositionGraphicsDevice(this.compositor, this.canvasDevice); + + this.canvasDevice.DeviceLost += CanvasDevice_DeviceLost; + this.compositionDevice.RenderingDeviceReplaced += CompositionDevice_RenderingDeviceReplaced; + } + + /// + /// Invokes when the current is lost. + /// + private void CanvasDevice_DeviceLost(CanvasDevice sender, object args) + { + InitializeDevices(); + } + + /// + /// Invokes when the current changes rendering device. + /// + private void CompositionDevice_RenderingDeviceReplaced(CompositionGraphicsDevice sender, RenderingDeviceReplacedEventArgs args) + { + InitializeDevices(); + } + + /// + /// Loads an image from the URI. + /// + /// The URI. + /// + public async Task LoadFromUri(Uri uri) + { + return await LoadFromUri(uri, Size.Empty); + } + + /// + /// Loads an image from URI with a specified size. + /// + /// The URI. + /// The size target. + /// + public async Task LoadFromUri(Uri uri, Size sizeTarget) + { + default(ArgumentNullException).ThrowIfNull(compositionDevice); + + var bitmap = await CanvasBitmap.LoadAsync(canvasDevice, uri); + var sizeSource = bitmap.Size; + + if (sizeTarget.IsEmpty) + { + sizeTarget = sizeSource; + } + + var surface = compositionDevice.CreateDrawingSurface( + sizeTarget, + DirectXPixelFormat.B8G8R8A8UIntNormalized, + DirectXAlphaMode.Premultiplied); + + using (var ds = CanvasComposition.CreateDrawingSession(surface)) + { + ds.Clear(Color.FromArgb(0, 0, 0, 0)); + ds.DrawImage(bitmap, new Rect(0, 0, sizeTarget.Width, sizeTarget.Height), new Rect(0, 0, sizeSource.Width, sizeSource.Height)); + } + + return surface; + } + + /// + /// Loads the text on to a . + /// + /// The text. + /// The size target. + /// The text format. + /// Color of the text. + /// Color of the bg. + /// + public CompositionDrawingSurface LoadText(string text, Size sizeTarget, CanvasTextFormat textFormat, Color textColor, Color bgColor) + { + default(ArgumentNullException).ThrowIfNull(compositionDevice); + + var surface = compositionDevice.CreateDrawingSurface( + sizeTarget, + DirectXPixelFormat.B8G8R8A8UIntNormalized, + DirectXAlphaMode.Premultiplied); + + using (var ds = CanvasComposition.CreateDrawingSession(surface)) + { + ds.Clear(bgColor); + ds.DrawText(text, new Rect(0, 0, sizeTarget.Width, sizeTarget.Height), textColor, textFormat); + } + + return surface; + } + + /// + /// Loads an image from URI, with a specified size. + /// + /// The URI. + /// The size target. + /// The load effect handler callback. + /// + public async Task LoadFromUri(Uri uri, Size sizeTarget, LoadTimeEffectHandler loadEffectHandler) + { + default(ArgumentNullException).ThrowIfNull(compositionDevice); + + if (loadEffectHandler != null) + { + var bitmap = await CanvasBitmap.LoadAsync(canvasDevice, uri); + return loadEffectHandler(bitmap, compositionDevice, sizeTarget); + } + + return await LoadFromUri(uri, sizeTarget); + } + + public void Dispose() + { + compositionDevice?.Dispose(); + canvasDevice?.Dispose(); + } +} diff --git a/components/Media/src/Helpers/SurfaceLoader.cs b/components/Media/src/Helpers/SurfaceLoader.cs new file mode 100644 index 00000000..ea40b913 --- /dev/null +++ b/components/Media/src/Helpers/SurfaceLoader.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.UI.Composition; +using CommunityToolkit.WinUI.Media.Helpers.Cache; +using Windows.Graphics.Display; +using Windows.Graphics.Imaging; +using Windows.UI; + +#if WINUI2 +using Windows.UI.Composition; +using Windows.Graphics.DirectX; +#elif WINUI3 +using Microsoft.UI.Composition; +using Microsoft.Graphics.DirectX; +#endif + +namespace CommunityToolkit.WinUI.Media.Helpers; + +/// +/// A that can load and draw images and other objects to Win2D surfaces and brushes +/// +public sealed partial class SurfaceLoader +{ + /// + /// Synchronization mutex to access the cache and load Win2D images concurrently + /// + private static readonly AsyncMutex Win2DMutex = new AsyncMutex(); + + /// + /// Gets the local cache mapping for previously loaded Win2D images + /// + private static readonly CompositionObjectCache Cache = new CompositionObjectCache(); + + /// + /// Loads a instance with the target image from the shared instance + /// + /// The path to the image to load + /// Indicates the desired DPI mode to use when loading the image + /// Indicates the cache option to use to load the image + /// A that returns the loaded instance + public static async Task LoadImageAsync(Uri uri, DpiMode dpiMode, CacheMode cacheMode = CacheMode.Default) + { + var compositor = Window.Current.Compositor; + + // Lock and check the cache first + using (await Win2DMutex.LockAsync()) + { + uri = uri.ToAppxUri(); + + if (cacheMode == CacheMode.Default && + Cache.TryGetValue(compositor, uri, out var cached)) + { + return cached; + } + + // Load the image + CompositionBrush? brush; + try + { + // This will throw and the canvas will re-initialize the Win2D device if needed + var sharedDevice = CanvasDevice.GetSharedDevice(); + brush = await LoadSurfaceBrushAsync(sharedDevice, compositor, uri, dpiMode); + } + catch + { + // Device error + brush = null; + } + + // Cache when needed and return the result + if (brush != null && + cacheMode != CacheMode.Disabled) + { + Cache.AddOrUpdate(compositor, uri, brush); + } + + return brush; + } + } + + /// + /// Loads a from the input , and prepares it to be used in a tile effect + /// + /// The device to use to process the Win2D image + /// The compositor instance to use to create the final brush + /// The path to the image to load + /// Indicates the desired DPI mode to use when loading the image + /// A that returns the loaded instance + private static async Task LoadSurfaceBrushAsync( + CanvasDevice canvasDevice, + Compositor compositor, + Uri uri, + DpiMode dpiMode) + { + var displayInformation = DisplayInformation.GetForCurrentView(); + float dpi = displayInformation.LogicalDpi; + + // Load the bitmap with the appropriate settings + using CanvasBitmap bitmap = dpiMode switch + { + DpiMode.UseSourceDpi => await CanvasBitmap.LoadAsync(canvasDevice, uri), + DpiMode.Default96Dpi => await CanvasBitmap.LoadAsync(canvasDevice, uri, 96), + DpiMode.DisplayDpi => await CanvasBitmap.LoadAsync(canvasDevice, uri, dpi), + DpiMode.DisplayDpiWith96AsLowerBound => await CanvasBitmap.LoadAsync(canvasDevice, uri, dpi >= 96 ? dpi : 96), + _ => throw new ArgumentOutOfRangeException(nameof(dpiMode), dpiMode, $"Invalid DPI mode: {dpiMode}") + }; + + // Calculate the surface size + Size + size = bitmap.Size, + sizeInPixels = new Size(bitmap.SizeInPixels.Width, bitmap.SizeInPixels.Height); + + // Get the device and the target surface + using CompositionGraphicsDevice graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice); + + // Create the drawing surface + var drawingSurface = graphicsDevice.CreateDrawingSurface( + sizeInPixels, + DirectXPixelFormat.B8G8R8A8UIntNormalized, + DirectXAlphaMode.Premultiplied); + + // Create a drawing session for the target surface + using (var drawingSession = CanvasComposition.CreateDrawingSession(drawingSurface, new Rect(0, 0, sizeInPixels.Width, sizeInPixels.Height), dpi)) + { + // Fill the target surface + drawingSession.Clear(Color.FromArgb(0, 0, 0, 0)); + drawingSession.DrawImage(bitmap, new Rect(0, 0, size.Width, size.Height), new Rect(0, 0, size.Width, size.Height)); + drawingSession.EffectTileSize = new BitmapSize { Width = (uint)size.Width, Height = (uint)size.Height }; + } + + // Setup the effect brush to use + var surfaceBrush = compositor.CreateSurfaceBrush(drawingSurface); + surfaceBrush.Stretch = CompositionStretch.None; + + double pixels = displayInformation.RawPixelsPerViewPixel; + + // Adjust the scale if the DPI scaling is greater than 100% + if (pixels > 1) + { + surfaceBrush.Scale = new Vector2((float)(1 / pixels)); + surfaceBrush.BitmapInterpolationMode = CompositionBitmapInterpolationMode.NearestNeighbor; + } + + return surfaceBrush; + } +} diff --git a/components/Media/src/MultiTarget.props b/components/Media/src/MultiTarget.props new file mode 100644 index 00000000..67f1c274 --- /dev/null +++ b/components/Media/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk; + + diff --git a/components/Media/src/Pipelines/BrushProvider.cs b/components/Media/src/Pipelines/BrushProvider.cs new file mode 100644 index 00000000..56572dc2 --- /dev/null +++ b/components/Media/src/Pipelines/BrushProvider.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; + +#if WINUI3 +using Microsoft.UI.Composition; +#elif WINUI2 +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// +/// A simple container used to store info on a custom composition effect to create +/// +public sealed class BrushProvider +{ + /// + /// Gets the name of the target + /// + internal string Name { get; } + + /// + /// Gets the stored effect initializer + /// + internal Func> Initializer { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the target + /// The stored effect initializer + private BrushProvider(string name, Func> initializer) + { + this.Name = name; + this.Initializer = initializer; + } + + /// + /// Creates a new instance with the info on a given to initialize + /// + /// The target effect name + /// A to use to initialize the effect + /// A instance with the input initializer + [Pure] + public static BrushProvider New(string name, CompositionBrush brush) => new BrushProvider(name, () => new ValueTask(brush)); + + /// + /// Creates a new instance with the info on a given to initialize + /// + /// The target effect name + /// A instance that will produce the to use to initialize the effect + /// A instance with the input initializer + [Pure] + public static BrushProvider New(string name, Func factory) => new BrushProvider(name, () => new ValueTask(factory())); + + /// + /// Creates a new instance with the info on a given to initialize + /// + /// The target effect name + /// An asynchronous instance that will produce the to use to initialize the effect + /// A instance with the input initializer + [Pure] + public static BrushProvider New(string name, Func> factory) => new BrushProvider(name, () => new ValueTask(factory())); +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.Effects.Internals.cs b/components/Media/src/Pipelines/PipelineBuilder.Effects.Internals.cs new file mode 100644 index 00000000..a93932f1 --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.Effects.Internals.cs @@ -0,0 +1,218 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using Microsoft.Graphics.Canvas.Effects; +using CommunityToolkit.WinUI.Animations; +using Windows.Graphics.Effects; +using Windows.UI; +using CanvasCrossFadeEffect = Microsoft.Graphics.Canvas.Effects.CrossFadeEffect; +using CanvasExposureEffect = Microsoft.Graphics.Canvas.Effects.ExposureEffect; +using CanvasHueRotationEffect = Microsoft.Graphics.Canvas.Effects.HueRotationEffect; +using CanvasOpacityEffect = Microsoft.Graphics.Canvas.Effects.OpacityEffect; +using CanvasSaturationEffect = Microsoft.Graphics.Canvas.Effects.SaturationEffect; +using CanvasSepiaEffect = Microsoft.Graphics.Canvas.Effects.SepiaEffect; +using CanvasTintEffect = Microsoft.Graphics.Canvas.Effects.TintEffect; + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// +/// A that allows to build custom effects pipelines and create instances from them +/// +public sealed partial class PipelineBuilder +{ + /// + /// Adds a new to the current pipeline + /// + /// The blur amount to apply + /// The target property to animate the resulting effect. + /// The parameter for the effect, defaults to + /// The parameter to use, defaults to + /// A new instance to use to keep adding new effects + [Pure] + internal PipelineBuilder Blur( + float blur, + out string target, + EffectBorderMode mode = EffectBorderMode.Hard, + EffectOptimization optimization = EffectOptimization.Balanced) + { + string name = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{name}.{nameof(GaussianBlurEffect.BlurAmount)}"; + + async ValueTask Factory() => new GaussianBlurEffect + { + BlurAmount = blur, + BorderMode = mode, + Optimization = optimization, + Source = await this.sourceProducer(), + Name = name + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// + /// Cross fades two pipelines using an instance + /// + /// The second instance to cross fade + /// The cross fade factor to blend the input effects (should be in the [0, 1] range) + /// The target property to animate the resulting effect. + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder CrossFade(PipelineBuilder pipeline, float factor, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasCrossFadeEffect.CrossFade)}"; + + async ValueTask Factory() => new CanvasCrossFadeEffect + { + CrossFade = factor, + Source1 = await this.sourceProducer(), + Source2 = await pipeline.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(Factory, this, pipeline, new[] { target }); + } + + /// + /// Applies an exposure effect on the current pipeline + /// + /// The initial exposure of tint to apply over the current effect (should be in the [-2, 2] range) + /// The target property to animate the resulting effect. + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Exposure(float amount, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasExposureEffect.Exposure)}"; + + async ValueTask Factory() => new CanvasExposureEffect + { + Exposure = amount, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// + /// Applies a hue rotation effect on the current pipeline + /// + /// The angle to rotate the hue, in radians + /// The target property to animate the resulting effect. + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder HueRotation(float angle, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasHueRotationEffect.Angle)}"; + + async ValueTask Factory() => new CanvasHueRotationEffect + { + Angle = angle, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The opacity value to apply to the pipeline + /// The target property to animate the resulting effect. + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Opacity(float opacity, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasOpacityEffect.Opacity)}"; + + async ValueTask Factory() => new CanvasOpacityEffect + { + Opacity = opacity, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The initial saturation amount for the new effect (should be in the [0, 1] range) + /// The target property to animate the resulting effect. + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Saturation(float saturation, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasSaturationEffect.Saturation)}"; + + async ValueTask Factory() => new CanvasSaturationEffect + { + Saturation = saturation, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The sepia effect intensity for the new effect + /// The target property to animate the resulting effect. + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Sepia(float intensity, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasSepiaEffect.Intensity)}"; + + async ValueTask Factory() => new CanvasSepiaEffect + { + Intensity = intensity, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } + + /// + /// Applies a tint effect on the current pipeline + /// + /// The color to use + /// The target property to animate the resulting effect. + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Tint(Color color, out string target) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + target = $"{id}.{nameof(CanvasTintEffect.Color)}"; + + async ValueTask Factory() => new CanvasTintEffect + { + Color = color, + Source = await this.sourceProducer(), + Name = id + }; + + return new PipelineBuilder(this, Factory, new[] { target }); + } +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.Effects.cs b/components/Media/src/Pipelines/PipelineBuilder.Effects.cs new file mode 100644 index 00000000..a65ca17a --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.Effects.cs @@ -0,0 +1,693 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using Microsoft.Graphics.Canvas.Effects; +using CommunityToolkit.WinUI.Animations; +using Windows.Graphics.Effects; +using Windows.UI; +using Windows.UI.Composition; +using CanvasExposureEffect = Microsoft.Graphics.Canvas.Effects.ExposureEffect; +using CanvasGrayscaleEffect = Microsoft.Graphics.Canvas.Effects.GrayscaleEffect; +using CanvasHueRotationEffect = Microsoft.Graphics.Canvas.Effects.HueRotationEffect; +using CanvasInvertEffect = Microsoft.Graphics.Canvas.Effects.InvertEffect; +using CanvasLuminanceToAlphaEffect = Microsoft.Graphics.Canvas.Effects.LuminanceToAlphaEffect; +using CanvasOpacityEffect = Microsoft.Graphics.Canvas.Effects.OpacityEffect; +using CanvasSaturationEffect = Microsoft.Graphics.Canvas.Effects.SaturationEffect; +using CanvasSepiaEffect = Microsoft.Graphics.Canvas.Effects.SepiaEffect; +using CanvasTemperatureAndTintEffect = Microsoft.Graphics.Canvas.Effects.TemperatureAndTintEffect; +using CanvasTintEffect = Microsoft.Graphics.Canvas.Effects.TintEffect; + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// +/// A that allows to build custom effects pipelines and create instances from them +/// +public sealed partial class PipelineBuilder +{ + /// + /// Adds a new to the current pipeline + /// + /// The blur amount to apply + /// The parameter for the effect, defaults to + /// The parameter to use, defaults to + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Blur(float blur, EffectBorderMode mode = EffectBorderMode.Hard, EffectOptimization optimization = EffectOptimization.Balanced) + { + async ValueTask Factory() => new GaussianBlurEffect + { + BlurAmount = blur, + BorderMode = mode, + Optimization = optimization, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Adds a new to the current pipeline + /// + /// The initial blur amount + /// The optional blur setter for the effect + /// The parameter for the effect, defaults to + /// The parameter to use, defaults to + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Blur(float blur, out EffectSetter setter, EffectBorderMode mode = EffectBorderMode.Hard, EffectOptimization optimization = EffectOptimization.Balanced) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new GaussianBlurEffect + { + BlurAmount = blur, + BorderMode = mode, + Optimization = optimization, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(GaussianBlurEffect.BlurAmount)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(GaussianBlurEffect.BlurAmount)}" }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The initial blur amount + /// The optional blur animation for the effect + /// The parameter for the effect, defaults to + /// The parameter to use, defaults to + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Blur(float blur, out EffectAnimation animation, EffectBorderMode mode = EffectBorderMode.Hard, EffectOptimization optimization = EffectOptimization.Balanced) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new GaussianBlurEffect + { + BlurAmount = blur, + BorderMode = mode, + Optimization = optimization, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(GaussianBlurEffect.BlurAmount)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(GaussianBlurEffect.BlurAmount)}" }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The saturation amount for the new effect (should be in the [0, 1] range) + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Saturation(float saturation) + { + async ValueTask Factory() => new CanvasSaturationEffect + { + Saturation = saturation, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Adds a new to the current pipeline + /// + /// The initial saturation amount for the new effect (should be in the [0, 1] range) + /// The optional saturation setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Saturation(float saturation, out EffectSetter setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasSaturationEffect + { + Saturation = saturation, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasSaturationEffect.Saturation)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasSaturationEffect.Saturation)}" }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The initial saturation amount for the new effect (should be in the [0, 1] range) + /// The optional saturation animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Saturation(float saturation, out EffectAnimation animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasSaturationEffect + { + Saturation = saturation, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasSaturationEffect.Saturation)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasSaturationEffect.Saturation)}" }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The sepia effect intensity for the new effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Sepia(float intensity) + { + async ValueTask Factory() => new CanvasSepiaEffect + { + Intensity = intensity, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Adds a new to the current pipeline + /// + /// The sepia effect intensity for the new effect + /// The optional sepia intensity setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Sepia(float intensity, out EffectSetter setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasSepiaEffect + { + Intensity = intensity, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasSepiaEffect.Intensity)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasSepiaEffect.Intensity)}" }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The sepia effect intensity for the new effect + /// The sepia intensity animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Sepia(float intensity, out EffectAnimation animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasSepiaEffect + { + Intensity = intensity, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasSepiaEffect.Intensity)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasSepiaEffect.Intensity)}" }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The opacity value to apply to the pipeline + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Opacity(float opacity) + { + async ValueTask Factory() => new CanvasOpacityEffect + { + Opacity = opacity, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Adds a new to the current pipeline + /// + /// The opacity value to apply to the pipeline + /// The optional opacity setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Opacity(float opacity, out EffectSetter setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasOpacityEffect + { + Opacity = opacity, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasOpacityEffect.Opacity)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasOpacityEffect.Opacity)}" }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The opacity value to apply to the pipeline + /// The optional opacity animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Opacity(float opacity, out EffectAnimation animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasOpacityEffect + { + Opacity = opacity, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasOpacityEffect.Opacity)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasOpacityEffect.Opacity)}" }); + } + + /// + /// Applies an exposure effect on the current pipeline + /// + /// The amount of exposure to apply over the current effect (should be in the [-2, 2] range) + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Exposure(float amount) + { + async ValueTask Factory() => new CanvasExposureEffect + { + Exposure = amount, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Applies an exposure effect on the current pipeline + /// + /// The initial exposure of tint to apply over the current effect (should be in the [-2, 2] range) + /// The optional amount setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Exposure(float amount, out EffectSetter setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasExposureEffect + { + Exposure = amount, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasExposureEffect.Exposure)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasExposureEffect.Exposure)}" }); + } + + /// + /// Applies an exposure effect on the current pipeline + /// + /// The initial exposure of tint to apply over the current effect (should be in the [-2, 2] range) + /// The optional amount animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Exposure(float amount, out EffectAnimation animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasExposureEffect + { + Exposure = amount, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasExposureEffect.Exposure)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasExposureEffect.Exposure)}" }); + } + + /// + /// Applies a hue rotation effect on the current pipeline + /// + /// The angle to rotate the hue, in radians + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder HueRotation(float angle) + { + async ValueTask Factory() => new CanvasHueRotationEffect + { + Angle = angle, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Applies a hue rotation effect on the current pipeline + /// + /// The angle to rotate the hue, in radians + /// The optional rotation angle setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder HueRotation(float angle, out EffectSetter setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasHueRotationEffect + { + Angle = angle, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasHueRotationEffect.Angle)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasHueRotationEffect.Angle)}" }); + } + + /// + /// Applies a hue rotation effect on the current pipeline + /// + /// The angle to rotate the hue, in radians + /// The optional rotation angle animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder HueRotation(float angle, out EffectAnimation animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasHueRotationEffect + { + Angle = angle, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasHueRotationEffect.Angle)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasHueRotationEffect.Angle)}" }); + } + + /// + /// Applies a tint effect on the current pipeline + /// + /// The color to use + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Tint(Color color) + { + async ValueTask Factory() => new CanvasTintEffect + { + Color = color, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Applies a tint effect on the current pipeline + /// + /// The color to use + /// The optional color setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Tint(Color color, out EffectSetter setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasTintEffect + { + Color = color, + Source = await this.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertColor($"{id}.{nameof(CanvasTintEffect.Color)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasTintEffect.Color)}" }); + } + + /// + /// Applies a tint effect on the current pipeline + /// + /// The color to use + /// The optional color animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Tint(Color color, out EffectAnimation animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasTintEffect + { + Color = color, + Source = await this.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasTintEffect.Color)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasTintEffect.Color)}" }); + } + + /// + /// Applies a temperature and tint effect on the current pipeline + /// + /// The temperature value to use (should be in the [-1, 1] range) + /// The tint value to use (should be in the [-1, 1] range) + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder TemperatureAndTint(float temperature, float tint) + { + async ValueTask Factory() => new CanvasTemperatureAndTintEffect + { + Temperature = temperature, + Tint = tint, + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Applies a temperature and tint effect on the current pipeline + /// + /// The temperature value to use (should be in the [-1, 1] range) + /// The optional temperature setter for the effect + /// The tint value to use (should be in the [-1, 1] range) + /// The optional tint setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder TemperatureAndTint( + float temperature, + out EffectSetter temperatureSetter, + float tint, + out EffectSetter tintSetter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasTemperatureAndTintEffect + { + Temperature = temperature, + Tint = tint, + Source = await this.sourceProducer(), + Name = id + }; + + temperatureSetter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasTemperatureAndTintEffect.Temperature)}", value); + + tintSetter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasTemperatureAndTintEffect.Tint)}", value); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasTemperatureAndTintEffect.Temperature)}", $"{id}.{nameof(CanvasTemperatureAndTintEffect.Tint)}" }); + } + + /// + /// Applies a temperature and tint effect on the current pipeline + /// + /// The temperature value to use (should be in the [-1, 1] range) + /// The optional temperature animation for the effect + /// The tint value to use (should be in the [-1, 1] range) + /// The optional tint animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder TemperatureAndTint( + float temperature, + out EffectAnimation temperatureAnimation, + float tint, + out EffectAnimation tintAnimation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasTemperatureAndTintEffect + { + Temperature = temperature, + Tint = tint, + Source = await this.sourceProducer(), + Name = id + }; + + temperatureAnimation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasTemperatureAndTintEffect.Temperature)}", value, duration); + + tintAnimation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasTemperatureAndTintEffect.Tint)}", value, duration); + + return new PipelineBuilder(this, Factory, new[] { $"{id}.{nameof(CanvasTemperatureAndTintEffect.Temperature)}", $"{id}.{nameof(CanvasTemperatureAndTintEffect.Tint)}" }); + } + + /// + /// Applies a shade effect on the current pipeline + /// + /// The color to use + /// The amount of mix to apply over the current effect (must be in the [0, 1] range) + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Shade(Color color, float mix) + { + return FromColor(color).CrossFade(this, mix); + } + + /// + /// Applies a shade effect on the current pipeline + /// + /// The color to use + /// The optional color setter for the effect + /// The initial amount of mix to apply over the current effect (must be in the [0, 1] range) + /// The optional mix setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Shade( + Color color, + out EffectSetter colorSetter, + float mix, + out EffectSetter mixSetter) + { + return FromColor(color, out colorSetter).CrossFade(this, mix, out mixSetter); + } + + /// + /// Applies a shade effect on the current pipeline + /// + /// The color to use + /// The optional color animation for the effect + /// The initial amount of mix to apply over the current effect (must be in the [0, 1] range) + /// The optional mix animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Shade( + Color color, + out EffectAnimation colorAnimation, + float mix, + out EffectAnimation mixAnimation) + { + return FromColor(color, out colorAnimation).CrossFade(this, mix, out mixAnimation); + } + + /// + /// Applies a luminance to alpha effect on the current pipeline + /// + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder LuminanceToAlpha() + { + async ValueTask Factory() => new CanvasLuminanceToAlphaEffect + { + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Applies an invert effect on the current pipeline + /// + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Invert() + { + async ValueTask Factory() => new CanvasInvertEffect + { + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Applies a grayscale on the current pipeline + /// + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Grayscale() + { + async ValueTask Factory() => new CanvasGrayscaleEffect + { + Source = await this.sourceProducer() + }; + + return new PipelineBuilder(this, Factory); + } + + /// + /// Applies a custom effect to the current pipeline + /// + /// A that takes the current instance and produces a new effect to display + /// The list of optional animatable properties in the returned effect + /// The list of source parameters that require deferred initialization (see for more info) + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Effect( + Func factory, + IEnumerable? animations = null, + IEnumerable? initializers = null) + { + async ValueTask Factory() => factory(await this.sourceProducer()); + + return new PipelineBuilder(this, Factory, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } + + /// + /// Applies a custom effect to the current pipeline + /// + /// An asynchronous that takes the current instance and produces a new effect to display + /// The list of optional animatable properties in the returned effect + /// The list of source parameters that require deferred initialization (see for more info) + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Effect( + Func> factory, + IEnumerable? animations = null, + IEnumerable? initializers = null) + { + async ValueTask Factory() => await factory(await this.sourceProducer()); + + return new PipelineBuilder(this, Factory, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.Initialization.cs b/components/Media/src/Pipelines/PipelineBuilder.Initialization.cs new file mode 100644 index 00000000..8997d397 --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.Initialization.cs @@ -0,0 +1,327 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using System.Numerics; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Effects; +using CommunityToolkit.WinUI.Animations; +using CommunityToolkit.WinUI.Media.Helpers; +using CommunityToolkit.WinUI.Media.Helpers.Cache; +using Windows.Graphics.Effects; +using Windows.UI; + +#if WINUI3 +using Microsoft.UI.Xaml.Hosting; +using Microsoft.UI.Composition; +#elif WINUI2 +using Windows.UI.Xaml.Hosting; +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// +/// A that allows to build custom effects pipelines and create instances from them +/// +public sealed partial class PipelineBuilder +{ + /// + /// The cache manager for backdrop brushes + /// + private static readonly CompositionObjectCache BackdropBrushCache = new CompositionObjectCache(); + + /// + /// The cache manager for host backdrop brushes + /// + private static readonly CompositionObjectCache HostBackdropBrushCache = new CompositionObjectCache(); + + /// + /// Starts a new pipeline from the returned by + /// + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromBackdrop() + { + ValueTask Factory() + { + var brush = BackdropBrushCache.GetValue(Window.Current.Compositor, c => c.CreateBackdropBrush()); + + return new ValueTask(brush); + } + + return new PipelineBuilder(Factory); + } + +#if WINUI2 + /// + /// Starts a new pipeline from the returned by + /// + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromHostBackdrop() + { + ValueTask Factory() + { + var brush = HostBackdropBrushCache.GetValue(Window.Current.Compositor, c => c.CreateHostBackdropBrush()); + + return new ValueTask(brush); + } + + return new PipelineBuilder(Factory); + } +#endif + + /// + /// Starts a new pipeline from a solid with the specified color + /// + /// The desired color for the initial + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromColor(Color color) + { + return new PipelineBuilder(() => new ValueTask(new ColorSourceEffect { Color = color })); + } + + /// + /// Starts a new pipeline from a solid with the specified color + /// + /// The desired color for the initial + /// The optional color setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromColor(Color color, out EffectSetter setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + ValueTask Factory() => new ValueTask(new ColorSourceEffect + { + Color = color, + Name = id + }); + + setter = (brush, value) => brush.Properties.InsertColor($"{id}.{nameof(ColorSourceEffect.Color)}", value); + + return new PipelineBuilder(Factory, new[] { $"{id}.{nameof(ColorSourceEffect.Color)}" }); + } + + /// + /// Starts a new pipeline from a solid with the specified color + /// + /// The desired color for the initial + /// The optional color animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromColor(Color color, out EffectAnimation animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + ValueTask Factory() => new ValueTask(new ColorSourceEffect + { + Color = color, + Name = id + }); + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(ColorSourceEffect.Color)}", value, duration); + + return new PipelineBuilder(Factory, new[] { $"{id}.{nameof(ColorSourceEffect.Color)}" }); + } + + /// + /// Starts a new pipeline from a solid with the specified color + /// + /// The desired color for the initial + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromHdrColor(Vector4 color) + { + return new PipelineBuilder(() => new ValueTask(new ColorSourceEffect { ColorHdr = color })); + } + + /// + /// Starts a new pipeline from a solid with the specified color + /// + /// The desired color for the initial + /// The optional color setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromHdrColor(Vector4 color, out EffectSetter setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + ValueTask Factory() => new ValueTask(new ColorSourceEffect + { + ColorHdr = color, + Name = id + }); + + setter = (brush, value) => brush.Properties.InsertVector4($"{id}.{nameof(ColorSourceEffect.ColorHdr)}", value); + + return new PipelineBuilder(Factory, new[] { $"{id}.{nameof(ColorSourceEffect.ColorHdr)}" }); + } + + /// + /// Starts a new pipeline from a solid with the specified color + /// + /// The desired color for the initial + /// The optional color animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromHdrColor(Vector4 color, out EffectAnimation animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + ValueTask Factory() => new ValueTask(new ColorSourceEffect + { + ColorHdr = color, + Name = id + }); + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(ColorSourceEffect.ColorHdr)}", value, duration); + + return new PipelineBuilder(Factory, new[] { $"{id}.{nameof(ColorSourceEffect.ColorHdr)}" }); + } + + /// + /// Starts a new pipeline from the input instance + /// + /// A instance to start the pipeline + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromBrush(CompositionBrush brush) + { + return new PipelineBuilder(() => new ValueTask(brush)); + } + + /// + /// Starts a new pipeline from the input instance + /// + /// A that synchronously returns a instance to start the pipeline + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromBrush(Func factory) + { + return new PipelineBuilder(() => new ValueTask(factory())); + } + + /// + /// Starts a new pipeline from the input instance + /// + /// A that asynchronously returns a instance to start the pipeline + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromBrush(Func> factory) + { + async ValueTask Factory() => await factory(); + + return new PipelineBuilder(Factory); + } + + /// + /// Starts a new pipeline from the input instance + /// + /// A instance to start the pipeline + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromEffect(IGraphicsEffectSource effect) + { + return new PipelineBuilder(() => new ValueTask(effect)); + } + + /// + /// Starts a new pipeline from the input instance + /// + /// A that synchronously returns a instance to start the pipeline + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromEffect(Func factory) + { + return new PipelineBuilder(() => new ValueTask(factory())); + } + + /// + /// Starts a new pipeline from the input instance + /// + /// A that asynchronously returns a instance to start the pipeline + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromEffect(Func> factory) + { + async ValueTask Factory() => await factory(); + + return new PipelineBuilder(Factory); + } + + /// + /// Starts a new pipeline from a Win2D image + /// + /// The relative path for the image to load (eg. "/Assets/image.png") + /// Indicates the desired DPI mode to use when loading the image + /// The cache mode to use to load the image + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromImage(string relativePath, DpiMode dpiMode = DpiMode.DisplayDpiWith96AsLowerBound, CacheMode cacheMode = CacheMode.Default) + { + return FromImage(relativePath.ToAppxUri(), dpiMode, cacheMode); + } + + /// + /// Starts a new pipeline from a Win2D image + /// + /// The path for the image to load + /// Indicates the desired DPI mode to use when loading the image + /// The cache mode to use to load the image + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromImage(Uri uri, DpiMode dpiMode = DpiMode.DisplayDpiWith96AsLowerBound, CacheMode cacheMode = CacheMode.Default) + { + return new PipelineBuilder(() => new ValueTask(SurfaceLoader.LoadImageAsync(uri, dpiMode, cacheMode)!)); + } + + /// + /// Starts a new pipeline from a Win2D image tiled to cover the available space + /// + /// The relative path for the image to load (eg. "/Assets/image.png") + /// Indicates the desired DPI mode to use when loading the image + /// The cache mode to use to load the image + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromTiles(string relativePath, DpiMode dpiMode = DpiMode.DisplayDpiWith96AsLowerBound, CacheMode cacheMode = CacheMode.Default) + { + return FromTiles(relativePath.ToAppxUri(), dpiMode, cacheMode); + } + + /// + /// Starts a new pipeline from a Win2D image tiled to cover the available space + /// + /// The path for the image to load + /// Indicates the desired DPI mode to use when loading the image + /// The cache mode to use to load the image + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromTiles(Uri uri, DpiMode dpiMode = DpiMode.DisplayDpiWith96AsLowerBound, CacheMode cacheMode = CacheMode.Default) + { + var image = FromImage(uri, dpiMode, cacheMode); + + async ValueTask Factory() => new BorderEffect + { + ExtendX = CanvasEdgeBehavior.Wrap, + ExtendY = CanvasEdgeBehavior.Wrap, + Source = await image.sourceProducer() + }; + + return new PipelineBuilder(image, Factory); + } + + /// + /// Starts a new pipeline from the returned by on the input + /// + /// The source to use to create the pipeline + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromUIElement(UIElement element) + { + return new PipelineBuilder(() => new ValueTask(ElementCompositionPreview.GetElementVisual(element).Compositor.CreateBackdropBrush())); + } +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.Merge.cs b/components/Media/src/Pipelines/PipelineBuilder.Merge.cs new file mode 100644 index 00000000..5fa5db08 --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.Merge.cs @@ -0,0 +1,160 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using Microsoft.Graphics.Canvas.Effects; +using CommunityToolkit.WinUI.Animations; +using Windows.Graphics.Effects; +using CanvasBlendEffect = Microsoft.Graphics.Canvas.Effects.BlendEffect; +using CanvasCrossFadeEffect = Microsoft.Graphics.Canvas.Effects.CrossFadeEffect; + +#if WINUI3 +using Microsoft.UI.Composition; +#elif WINUI2 +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// +/// A that allows to build custom effects pipelines and create instances from them +/// +public sealed partial class PipelineBuilder +{ + /// + /// Blends two pipelines using a instance with the specified mode + /// + /// The second instance to blend + /// The desired to use to blend the input pipelines + /// The placemeht to use with the two input pipelines (the default is ) + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Blend(PipelineBuilder pipeline, BlendEffectMode mode, Placement placement = Placement.Foreground) + { + var (foreground, background) = placement switch + { + Placement.Foreground => (pipeline, this), + Placement.Background => (this, pipeline), + _ => throw new ArgumentException($"Invalid placement value: {placement}") + }; + + async ValueTask Factory() => new CanvasBlendEffect + { + Foreground = await foreground.sourceProducer(), + Background = await background.sourceProducer(), + Mode = mode + }; + + return new PipelineBuilder(Factory, foreground, background); + } + + /// + /// Cross fades two pipelines using an instance + /// + /// The second instance to cross fade + /// The cross fade factor to blend the input effects (default is 0.5, must be in the [0, 1] range) + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder CrossFade(PipelineBuilder pipeline, float factor = 0.5f) + { + async ValueTask Factory() => new CanvasCrossFadeEffect + { + CrossFade = factor, + Source1 = await this.sourceProducer(), + Source2 = await pipeline.sourceProducer() + }; + + return new PipelineBuilder(Factory, this, pipeline); + } + + /// + /// Cross fades two pipelines using an instance + /// + /// The second instance to cross fade + /// The cross fade factor to blend the input effects (should be in the [0, 1] range) + /// The optional blur setter for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder CrossFade(PipelineBuilder pipeline, float factor, out EffectSetter setter) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasCrossFadeEffect + { + CrossFade = factor, + Source1 = await this.sourceProducer(), + Source2 = await pipeline.sourceProducer(), + Name = id + }; + + setter = (brush, value) => brush.Properties.InsertScalar($"{id}.{nameof(CanvasCrossFadeEffect.CrossFade)}", value); + + return new PipelineBuilder(Factory, this, pipeline, new[] { $"{id}.{nameof(CanvasCrossFadeEffect.CrossFade)}" }); + } + + /// + /// Cross fades two pipelines using an instance + /// + /// The second instance to cross fade + /// The cross fade factor to blend the input effects (should be in the [0, 1] range) + /// The optional blur animation for the effect + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder CrossFade(PipelineBuilder pipeline, float factor, out EffectAnimation animation) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + async ValueTask Factory() => new CanvasCrossFadeEffect + { + CrossFade = factor, + Source1 = await this.sourceProducer(), + Source2 = await pipeline.sourceProducer(), + Name = id + }; + + animation = (brush, value, duration) => brush.StartAnimationAsync($"{id}.{nameof(CanvasCrossFadeEffect.CrossFade)}", value, duration); + + return new PipelineBuilder(Factory, this, pipeline, new[] { $"{id}.{nameof(CanvasCrossFadeEffect.CrossFade)}" }); + } + + /// + /// Blends two pipelines using the provided to do so + /// + /// The blend function to use + /// The background pipeline to blend with the current instance + /// The list of optional animatable properties in the returned effect + /// The list of source parameters that require deferred initialization (see for more info) + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Merge( + Func factory, + PipelineBuilder background, + IEnumerable? animations = null, + IEnumerable? initializers = null) + { + async ValueTask Factory() => factory(await this.sourceProducer(), await background.sourceProducer()); + + return new PipelineBuilder(Factory, this, background, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } + + /// + /// Blends two pipelines using the provided asynchronous to do so + /// + /// The asynchronous blend function to use + /// The background pipeline to blend with the current instance + /// The list of optional animatable properties in the returned effect + /// The list of source parameters that require deferred initialization (see for more info) + /// A new instance to use to keep adding new effects + [Pure] + public PipelineBuilder Merge( + Func> factory, + PipelineBuilder background, + IEnumerable? animations = null, + IEnumerable? initializers = null) + { + async ValueTask Factory() => await factory(await this.sourceProducer(), await background.sourceProducer()); + + return new PipelineBuilder(Factory, this, background, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.Prebuilt.cs b/components/Media/src/Pipelines/PipelineBuilder.Prebuilt.cs new file mode 100644 index 00000000..bb95863e --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.Prebuilt.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using Microsoft.Graphics.Canvas.Effects; +using Windows.UI; + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// +/// A that allows to build custom effects pipelines and create instances from them +/// +public sealed partial class PipelineBuilder +{ +#if WINUI2 + /// + /// Returns a new instance that implements the host backdrop acrylic effect + /// + /// The tint color to use + /// The amount of tint to apply over the current effect + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromHostBackdropAcrylic( + Color tintColor, + float tintOpacity, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = + FromHostBackdrop() + .LuminanceToAlpha() + .Opacity(0.4f) + .Blend(FromHostBackdrop(), BlendEffectMode.Multiply) + .Shade(tintColor, tintOpacity); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } + + /// + /// Returns a new instance that implements the host backdrop acrylic effect + /// + /// The tint color to use + /// The optional tint color setter for the effect + /// The amount of tint to apply over the current effect + /// The optional tint mix setter for the effect + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromHostBackdropAcrylic( + Color tintColor, + out EffectSetter tintColorSetter, + float tintOpacity, + out EffectSetter tintOpacitySetter, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = + FromHostBackdrop() + .LuminanceToAlpha() + .Opacity(0.4f) + .Blend(FromHostBackdrop(), BlendEffectMode.Multiply) + .Shade(tintColor, out tintColorSetter, tintOpacity, out tintOpacitySetter); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } + + /// + /// Returns a new instance that implements the host backdrop acrylic effect + /// + /// The tint color to use + /// The optional tint color animation for the effect + /// The amount of tint to apply over the current effect + /// The optional tint mix animation for the effect + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromHostBackdropAcrylic( + Color tintColor, + out EffectAnimation tintColorAnimation, + float tintOpacity, + out EffectAnimation tintOpacityAnimation, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = + FromHostBackdrop() + .LuminanceToAlpha() + .Opacity(0.4f) + .Blend(FromHostBackdrop(), BlendEffectMode.Multiply) + .Shade(tintColor, out tintColorAnimation, tintOpacity, out tintOpacityAnimation); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } +#endif + + /// + /// Returns a new instance that implements the in-app backdrop acrylic effect + /// + /// The tint color to use + /// The amount of tint to apply over the current effect (must be in the [0, 1] range) + /// The amount of blur to apply to the acrylic brush + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromBackdropAcrylic( + Color tintColor, + float tintOpacity, + float blurAmount, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = FromBackdrop().Shade(tintColor, tintOpacity).Blur(blurAmount); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } + + /// + /// Returns a new instance that implements the in-app backdrop acrylic effect + /// + /// The tint color to use + /// The optional tint color setter for the effect + /// The amount of tint to apply over the current effect + /// The optional tint mix setter for the effect + /// The amount of blur to apply to the acrylic brush + /// The optional blur setter for the effect + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromBackdropAcrylic( + Color tintColor, + out EffectSetter tintColorSetter, + float tintOpacity, + out EffectSetter tintOpacitySetter, + float blurAmount, + out EffectSetter blurAmountSetter, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = + FromBackdrop() + .Shade(tintColor, out tintColorSetter, tintOpacity, out tintOpacitySetter) + .Blur(blurAmount, out blurAmountSetter); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } + + /// + /// Returns a new instance that implements the in-app backdrop acrylic effect + /// + /// The tint color to use + /// The optional tint color animation for the effect + /// The amount of tint to apply over the current effect + /// The optional tint mix animation for the effect + /// The amount of blur to apply to the acrylic brush + /// The optional blur animation for the effect + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + /// A new instance to use to keep adding new effects + [Pure] + public static PipelineBuilder FromBackdropAcrylic( + Color tintColor, + out EffectAnimation tintAnimation, + float tintOpacity, + out EffectAnimation tintOpacityAnimation, + float blurAmount, + out EffectAnimation blurAmountAnimation, + Uri? noiseUri, + CacheMode cacheMode = CacheMode.Default) + { + var pipeline = + FromBackdrop() + .Shade(tintColor, out tintAnimation, tintOpacity, out tintOpacityAnimation) + .Blur(blurAmount, out blurAmountAnimation); + + if (noiseUri != null) + { + return pipeline.Blend(FromTiles(noiseUri, cacheMode: cacheMode), BlendEffectMode.Overlay); + } + + return pipeline; + } +} diff --git a/components/Media/src/Pipelines/PipelineBuilder.cs b/components/Media/src/Pipelines/PipelineBuilder.cs new file mode 100644 index 00000000..4057c7ea --- /dev/null +++ b/components/Media/src/Pipelines/PipelineBuilder.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using System.Numerics; +using CommunityToolkit.WinUI.Animations; +using Windows.Graphics.Effects; + +#if WINUI2 +using Windows.UI.Composition; +using Windows.UI.Xaml.Hosting; +#elif WINUI3 +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Hosting; +#endif + +namespace CommunityToolkit.WinUI.Media.Pipelines; + +/// +/// A that represents a custom effect property setter that can be applied to a +/// +/// The type of property value to set +/// The target instance to target +/// The property value to set +public delegate void EffectSetter(CompositionBrush brush, T value) + where T : unmanaged; + +/// +/// A that represents a custom effect property animation that can be applied to a +/// +/// The type of property value to animate +/// The target instance to use to start the animation +/// The animation target value +/// The animation duration +/// A that completes when the target animation completes +public delegate Task EffectAnimation(CompositionBrush brush, T value, TimeSpan duration) + where T : unmanaged; + +/// +/// A that allows to build custom effects pipelines and create instances from them +/// +public sealed partial class PipelineBuilder +{ + /// + /// The instance used to produce the output for this pipeline + /// + private readonly Func> sourceProducer; + + /// + /// The collection of animation properties present in the current pipeline + /// + private readonly IReadOnlyCollection animationProperties; + + /// + /// The collection of info on the parameters that need to be initialized after creating the final + /// + private readonly IReadOnlyDictionary>> lazyParameters; + + /// + /// Initializes a new instance of the class. + /// + /// A instance that will return the initial + private PipelineBuilder(Func> factory) + { + string id = Guid.NewGuid().ToUppercaseAsciiLetters(); + + this.sourceProducer = () => new ValueTask(new CompositionEffectSourceParameter(id)); + this.animationProperties = Array.Empty(); + this.lazyParameters = new Dictionary>> { { id, factory } }; + } + + /// + /// Initializes a new instance of the class. + /// + /// A instance that will return the initial + private PipelineBuilder(Func> factory) + : this( + factory, + Array.Empty(), + new Dictionary>>()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A instance that will produce the new to add to the pipeline + /// The collection of animation properties for the new effect + private PipelineBuilder( + Func> factory, + IReadOnlyCollection animations) + : this( + factory, + animations, + new Dictionary>>()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A instance that will produce the new to add to the pipeline + /// The collection of animation properties for the new effect + /// The collection of instances that needs to be initialized for the new effect + private PipelineBuilder( + Func> factory, + IReadOnlyCollection animations, + IReadOnlyDictionary>> lazy) + { + this.sourceProducer = factory; + this.animationProperties = animations; + this.lazyParameters = lazy; + } + + /// + /// Initializes a new instance of the class. + /// + /// The source pipeline to attach the new effect to + /// A instance that will produce the new to add to the pipeline + /// The collection of animation properties for the new effect + /// The collection of instances that needs to be initialized for the new effect + private PipelineBuilder( + PipelineBuilder source, + Func> factory, + IReadOnlyCollection? animations = null, + IReadOnlyDictionary>>? lazy = null) + : this( + factory, + animations?.Merge(source.animationProperties) ?? source.animationProperties, + lazy?.Merge(source.lazyParameters) ?? source.lazyParameters) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A instance that will produce the new to add to the pipeline + /// The first pipeline to merge + /// The second pipeline to merge + /// The collection of animation properties for the new effect + /// The collection of instances that needs to be initialized for the new effect + private PipelineBuilder( + Func> factory, + PipelineBuilder a, + PipelineBuilder b, + IReadOnlyCollection? animations = null, + IReadOnlyDictionary>>? lazy = null) + : this( + factory, + animations?.Merge(a.animationProperties.Merge(b.animationProperties)) ?? a.animationProperties.Merge(b.animationProperties), + lazy?.Merge(a.lazyParameters.Merge(b.lazyParameters)) ?? a.lazyParameters.Merge(b.lazyParameters)) + { + } + + /// + /// Builds a instance from the current effects pipeline + /// + /// A that returns the final instance to use + [Pure] + public async Task BuildAsync() + { + var effect = await this.sourceProducer() as IGraphicsEffect; + + // Validate the pipeline + if (effect is null) + { + throw new InvalidOperationException("The pipeline doesn't contain a valid effects sequence"); + } + + // Build the effects factory + var factory = this.animationProperties.Count > 0 + ? Window.Current.Compositor.CreateEffectFactory(effect, this.animationProperties) + : Window.Current.Compositor.CreateEffectFactory(effect); + + // Create the effect factory and apply the final effect + var effectBrush = factory.CreateBrush(); + foreach (var pair in this.lazyParameters) + { + effectBrush.SetSourceParameter(pair.Key, await pair.Value()); + } + + return effectBrush; + } + + /// + /// Builds the current pipeline and creates a that is applied to the input + /// + /// The target to apply the brush to + /// An optional to use to bind the size of the created brush + /// A that returns the final instance to use + public async Task AttachAsync(UIElement target, UIElement? reference = null) + { + SpriteVisual visual = Window.Current.Compositor.CreateSpriteVisual(); + + visual.Brush = await BuildAsync(); + + ElementCompositionPreview.SetElementChildVisual(target, visual); + + if (reference != null) + { + if (reference == target) + { + visual.RelativeSizeAdjustment = Vector2.One; + } + else + { + visual.BindSize(reference); + } + } + + return visual; + } + + /// + /// Creates a new from the current effects pipeline + /// + /// A instance ready to be displayed + [Pure] + public XamlCompositionBrush AsBrush() + { + return new XamlCompositionBrush(this); + } +} diff --git a/components/Media/src/Visuals/AttachedVisualFactoryBase.cs b/components/Media/src/Visuals/AttachedVisualFactoryBase.cs new file mode 100644 index 00000000..c2d4eaae --- /dev/null +++ b/components/Media/src/Visuals/AttachedVisualFactoryBase.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINUI3 +using Microsoft.UI.Composition; +#elif WINUI2 +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A type responsible for creating instances to attach to target elements. +/// +public abstract class AttachedVisualFactoryBase : DependencyObject +{ + /// + /// Creates a to attach to the target element. + /// + /// The target the visual will be attached to. + /// A instance that the caller will attach to the target element. + public abstract ValueTask GetAttachedVisualAsync(UIElement element); +} diff --git a/components/Media/src/Visuals/PipelineVisualFactory.cs b/components/Media/src/Visuals/PipelineVisualFactory.cs new file mode 100644 index 00000000..494fc44b --- /dev/null +++ b/components/Media/src/Visuals/PipelineVisualFactory.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI3 +using Microsoft.UI.Composition; +#elif WINUI2 +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A builder type for instance to apply to UI elements. +/// +[ContentProperty(Name = nameof(Effects))] +public sealed class PipelineVisualFactory : PipelineVisualFactoryBase +{ + /// + /// Gets or sets the source for the current pipeline (defaults to a with source). + /// + public PipelineBuilder? Source { get; set; } + + /// + /// Gets or sets the collection of effects to use in the current pipeline. + /// + public IList Effects + { + get + { + if (GetValue(EffectsProperty) is not IList effects) + { + effects = new List(); + + SetValue(EffectsProperty, effects); + } + + return effects; + } + set => SetValue(EffectsProperty, value); + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty EffectsProperty = DependencyProperty.Register( + nameof(Effects), + typeof(IList), + typeof(PipelineVisualFactory), + new PropertyMetadata(null)); + + /// + public override async ValueTask GetAttachedVisualAsync(UIElement element) + { + var visual = (SpriteVisual)await base.GetAttachedVisualAsync(element); + + foreach (IPipelineEffect effect in Effects) + { + effect.NotifyCompositionBrushInUse(visual.Brush); + } + + return visual; + } + + /// + protected override PipelineBuilder OnPipelineRequested() + { + PipelineBuilder builder = Source ?? PipelineBuilder.FromBackdrop(); + + foreach (IPipelineEffect effect in Effects) + { + builder = effect.AppendToBuilder(builder); + } + + return builder; + } +} diff --git a/components/Media/src/Visuals/PipelineVisualFactoryBase.cs b/components/Media/src/Visuals/PipelineVisualFactoryBase.cs new file mode 100644 index 00000000..938546c4 --- /dev/null +++ b/components/Media/src/Visuals/PipelineVisualFactoryBase.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Media.Pipelines; + +#if WINUI3 +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Hosting; +#elif WINUI2 +using Windows.UI.Composition; +using Windows.UI.Xaml.Hosting; +#endif + +namespace CommunityToolkit.WinUI.Media; + +/// +/// A base class that extends by leveraging the APIs. +/// +public abstract class PipelineVisualFactoryBase : AttachedVisualFactoryBase +{ + /// + public override async ValueTask GetAttachedVisualAsync(UIElement element) + { + var visual = ElementCompositionPreview.GetElementVisual(element).Compositor.CreateSpriteVisual(); + + visual.Brush = await OnPipelineRequested().BuildAsync(); + + return visual; + } + + /// + /// A method that builds and returns the pipeline to use in the current instance. + /// + /// A instance to create the to display. + protected abstract PipelineBuilder OnPipelineRequested(); +} diff --git a/components/Media/tests/Media.Tests.projitems b/components/Media/tests/Media.Tests.projitems new file mode 100644 index 00000000..f6bb5968 --- /dev/null +++ b/components/Media/tests/Media.Tests.projitems @@ -0,0 +1,11 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 29B0EAEF-DDC3-461E-BE63-4052976510E8 + + + MediaExperiment.Tests + + \ No newline at end of file diff --git a/components/Media/tests/Media.Tests.shproj b/components/Media/tests/Media.Tests.shproj new file mode 100644 index 00000000..a14f5f00 --- /dev/null +++ b/components/Media/tests/Media.Tests.shproj @@ -0,0 +1,13 @@ + + + + 29B0EAEF-DDC3-461E-BE63-4052976510E8 + 14.0 + + + + + + + + diff --git a/tooling b/tooling index a852f23d..38728ba6 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit a852f23dabb110b7a51c068662309d00834d90a1 +Subproject commit 38728ba616661853b3bbcec9269ab1e362daa72a