microsoft/WindowsCompositionSamples

[Request] Color Bloom to Pictures

Closed this issue · 4 comments

Hi,

is it possible to adapt the Color Bloom Transition Animation in such a way, that instead of radial expanding a single color one can radial expand an image the same way?

The idea behind is, that I have a Hamburger Button on my page. When I click it, I want to create a temporal image of the background grid, apply a gaussian blur-filter on it and then use this image to be radial expanded into the Sidebar Panel View.

Currently I have only a blurred Sidebar (done in the steps above), that has a Transition in x-direction and it already looks amazing. The radial expansion would definitely give it a more special touch.

To better understand your feedback, I'd like to confirm that I understand your desired experience properly. From what I understand, what you would like to be able to achieve is the following flow:

  1. User clicks on the hamburger button on the corner of your page.
  2. After the click, what the user sees is that a blurred version of the background radiates (radial animation) from the hamburger menu button.

The way you are attempting to achieve this is that you have a static picture of the background, which you blur and want to reveal radially. The gradual revealing of the picture makes it look like a blur effect is happening to the actual background.

Are these assumptions correct?

@msftdavid this is correct. This is exactly what I'm trying to achieve.

The blur I currently do the following way:

private async void PrjTestBlur_SizeChanged(object sender, SizeChangedEventArgs e)
{
    // fix height and width of blurImage
    blurImage.Width = e.NewSize.Width;
    blurImage.Height = e.NewSize.Height;
    if (blurImage.Visibility == Visibility.Visible || blurImage.Source == null)
    {
        using (var stream = await mainGrid.RenderToRandomAccessStream())
        {
            var img = new BitmapImage();
            await img.SetSourceAsync(stream);
            blurImage.Source = img;
        }

        SpriteVisual blurVisual = (SpriteVisual)ElementCompositionPreview.GetElementChildVisual(blurImage);
        if (blurVisual != null)
        {
            blurVisual.Size = e.NewSize.ToVector2();
        }
    }
}

The RenderToRandomAccessStream function I got from here
and calling the blur with

private async void backgroundBlur(bool show = true)
{
    // show blurred image
    blurImage.Visibility = Visibility.Visible;

    // failsafe
    if (_compositor == null)
    {
        _compositor = ElementCompositionPreview.GetElementVisual(blurImage).Compositor;

        // Create a chained effect graph using a BlendEffect, blending color and blur
        GaussianBlurEffect blurEffect = new GaussianBlurEffect()
        {
            Name = "Blur",
            BlurAmount = show ? 0f : 9f,
            BorderMode = EffectBorderMode.Hard,
            Optimization = EffectOptimization.Balanced,
            Source = new CompositionEffectSourceParameter("Backdrop")
        };
        var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect, new[] { "Blur.BlurAmount" });

        // Create EffectBrush, BackdropBrush and SpriteVisual
        _brush = blurEffectFactory.CreateBrush();
    }

    // show animation
    StartBlurAnimation(show ? 9.0f : 0f);

    var destinationBrush = _compositor.CreateBackdropBrush();
    _brush.SetSourceParameter("Backdrop", destinationBrush);

    var blurSprite = _compositor.CreateSpriteVisual();
    blurSprite.Size = new Vector2((float)blurImage.Width, (float)blurImage.Height);
    blurSprite.Brush = _brush;

    ElementCompositionPreview.SetElementChildVisual(blurImage, blurSprite);

    if (!show)
    {
        await Task.Delay(1500);
        blurImage.Visibility = Visibility.Collapsed;
    }
}

private void StartBlurAnimation(float blur)
{
    ScalarKeyFrameAnimation blurAnimation = _compositor.CreateScalarKeyFrameAnimation();
    blurAnimation.InsertKeyFrame(1f, blur);
    blurAnimation.Duration = TimeSpan.FromSeconds(2);
    _brush.StartAnimation("Blur.BlurAmount", blurAnimation);
}

With the SizeChanged I get an updated version of the background, so the blur always matches the background content. It could be optimized by only updating, when a blurred content is shown. The function backgroundBlur here is used to blur the whole screen, but when the Image blurImage is set to the SplitView - element (Hamburgermenu) it would have the desired effect without the radial animation. Global variables for that page are CompositionEffectBrush _brush; and Compositor _compositor;.

Are you expecting a behavior like this?

bloom

If yes, then you can achieve this by using CompositionProToolkit

I have created a UserControl called ImageBloomControl, which you can put in each of your pages ( corresponding to each hamburger item). You can set the following properties

  • ImageSource - Uri of the image to be displayed
  • BlurAmount - Blur radius of the Gaussian blur to be applied on the image.
  • Duration - Duration of the bloom animation.
  • OffsetX - X Offset of the center of the bloom animation.
  • OffsetY - Y Offset of the center of the bloom animation.

When each of your page loads, you can call the Bloom() method of the ImageBloomControl to start the bloom animation.
Note: Don't forget to call the Dispose() method of the ImageBloomControl in the Page's Unloaded event to dispose the objects safely.

Here is the code for ImageBloomControl

ImageBloomControl.xaml

<UserControl
    x:Class="TestSurfaceImage.ImageBloomControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:xaml="using:Microsoft.Graphics.Canvas.UI.Xaml"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <Grid>
        <Grid x:Name="CompositionGrid"></Grid>
        <xaml:CanvasAnimatedControl x:Name="CanvasCtrl"
                                    Draw="OnDrawCanvas"></xaml:CanvasAnimatedControl>
    </Grid>
</UserControl>

ImageBloomControl.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Windows.UI.Composition;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Hosting;
using CompositionProToolkit;
using CompositionProToolkit.Expressions;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Graphics.Canvas.UI.Xaml;

// The User Control item template is documented at http://go.microsoft.com/fwlink/?LinkId=234236

namespace TestSurfaceImage
{
    public sealed partial class ImageBloomControl : UserControl, IDisposable
    {
        #region Fields

        private Compositor _compositor;
        private ICompositionGenerator _generator;
        private IMaskSurface _opacityMaskSurface;
        private LayerVisual _layer;
        private float _radiusFrom;
        private float _radiusTo;
        private float _currentRadius;
        private bool _bloomInProgress;
        private float _maxFrames;
        private float _currFrame;
        private Vector2 _bloomCenter;

        #endregion

        #region Dependency Properties

        #region ImageSource

        /// <summary>
        /// ImageSource Dependency Property
        /// </summary>
        public static readonly DependencyProperty ImageSourceProperty =
            DependencyProperty.Register("ImageSource", typeof(Uri), typeof(ImageBloomControl),
                new PropertyMetadata(null));

        /// <summary>
        /// Gets or sets the ImageSource property. This dependency property 
        /// indicates the Uri of the image to be displayed.
        /// </summary>
        public Uri ImageSource
        {
            get { return (Uri)GetValue(ImageSourceProperty); }
            set { SetValue(ImageSourceProperty, value); }
        }

        #endregion

        #region BlurAmount

        /// <summary>
        /// BlurAmount Dependency Property
        /// </summary>
        public static readonly DependencyProperty BlurAmountProperty =
            DependencyProperty.Register("BlurAmount", typeof(double), typeof(ImageBloomControl),
                new PropertyMetadata(10));

        /// <summary>
        /// Gets or sets the BlurAmount property. This dependency property 
        /// indicates the blur radius of the backdrop brush.
        /// </summary>
        public double BlurAmount
        {
            get { return (double)GetValue(BlurAmountProperty); }
            set { SetValue(BlurAmountProperty, value); }
        }

        #endregion

        #region Duration

        /// <summary>
        /// Duration Dependency Property
        /// </summary>
        public static readonly DependencyProperty DurationProperty =
            DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(ImageBloomControl),
                new PropertyMetadata(TimeSpan.FromSeconds(1)));

        /// <summary>
        /// Gets or sets the Duration property. This dependency property 
        /// indicates the duration of the bloom animation.
        /// </summary>
        public TimeSpan Duration
        {
            get { return (TimeSpan)GetValue(DurationProperty); }
            set { SetValue(DurationProperty, value); }
        }

        #endregion

        #region OffsetX

        /// <summary>
        /// OffsetX Dependency Property
        /// </summary>
        public static readonly DependencyProperty OffsetXProperty =
            DependencyProperty.Register("OffsetX", typeof(double), typeof(ImageBloomControl),
                new PropertyMetadata(0));

        /// <summary>
        /// Gets or sets the OffsetX property. This dependency property 
        /// indicates the offset of the center of the bloom on the X-axis.
        /// </summary>
        public double OffsetX
        {
            get { return (double)GetValue(OffsetXProperty); }
            set { SetValue(OffsetXProperty, value); }
        }

        #endregion

        #region OffsetY

        /// <summary>
        /// OffsetY Dependency Property
        /// </summary>
        public static readonly DependencyProperty OffsetYProperty =
            DependencyProperty.Register("OffsetY", typeof(double), typeof(ImageBloomControl),
                new PropertyMetadata(0));

        /// <summary>
        /// Gets or sets the OffsetY property. This dependency property 
        /// indicates the offset of the center of the bloom on the Y-axis.
        /// </summary>
        public double OffsetY
        {
            get { return (double)GetValue(OffsetYProperty); }
            set { SetValue(OffsetYProperty, value); }
        }

        #endregion

        #endregion

        #region Construction / Initialization

        public ImageBloomControl()
        {
            InitializeComponent();
            Loaded += OnLoaded;
        }

        #endregion

        #region APIs

        public void Bloom()
        {
            if (_bloomInProgress)
                return;

            var x = OffsetX.Single();
            var y = OffsetY.Single();
            _bloomCenter = new Vector2(x, y);
            var circleGeometry = CanvasGeometry.CreateCircle(_generator.Device, _bloomCenter, 0);
            _opacityMaskSurface.Redraw(circleGeometry);

            _currentRadius = 0;
            _radiusFrom = 0;
            // Calculate the max radius
            // Max radius will be the largest distance from the bloom center
            // to the corners of the CompositionGrid
            var distances = new List<float>();
            distances.Add(Vector2.Distance(_bloomCenter, new Vector2(0, 0)));
            distances.Add(Vector2.Distance(_bloomCenter, new Vector2(CompositionGrid.ActualWidth.Single(), 0)));
            distances.Add(Vector2.Distance(_bloomCenter, new Vector2(0, CompositionGrid.ActualHeight.Single())));
            distances.Add(Vector2.Distance(_bloomCenter, new Vector2(CompositionGrid.ActualWidth.Single(), CompositionGrid.ActualHeight.Single())));
            _radiusTo = distances.Max();

            // Calculate the number of frames required to animate
            var duration = Duration;
            _maxFrames = (float)Math.Floor((float)duration.TotalMilliseconds / (float)CanvasCtrl.TargetElapsedTime.TotalMilliseconds);
            _currFrame = 0f;

            _bloomInProgress = true;
        }

        public void Hide()
        {
            _bloomInProgress = false;
            var circleGeometry = CanvasGeometry.CreateCircle(_generator.Device, _bloomCenter, 0);
            _opacityMaskSurface.Redraw(circleGeometry);
            _currFrame = 0;
        }

        public void Dispose()
        {
            var visual = _layer.Children.ElementAt(0) as SpriteVisual;
            visual?.Dispose();
            _layer.Dispose();
            _layer = null;
            _opacityMaskSurface.Dispose();
            _opacityMaskSurface = null;
            _generator.Dispose();
            _generator = null;
            _compositor = null;
        }

        #endregion

        #region Event Handlers

        private async void OnLoaded(Object sender, RoutedEventArgs e)
        {
            if (_compositor == null)
            {
                _compositor = ElementCompositionPreview.GetElementVisual(CompositionGrid).Compositor;
                _generator = CompositionGeneratorFactory.GetCompositionGenerator(_compositor);

                _layer = _compositor.CreateLayerVisual();
                _layer.Size = new Vector2(CompositionGrid.ActualWidth.Single(), CompositionGrid.ActualHeight.Single());

                // Create the effect to create the opacity mask
                var opacityMaskEffect = new CompositeEffect
                {
                    // CanvasComposite.DestinationIn - Intersection of source and mask. 
                    // Equation: O = MA * S
                    // where O - Output pixel, MA - Mask Alpha, S - Source pixel.
                    Mode = CanvasComposite.DestinationIn,
                    Sources =
                        {
                            new CompositionEffectSourceParameter("source"),
                            new CompositionEffectSourceParameter("mask")
                        }
                };

                var opacityMaskEffectFactory = _compositor.CreateEffectFactory(opacityMaskEffect);
                var opacityMaskBrush = opacityMaskEffectFactory.CreateBrush();

                // The mask for the layer
                var circleGeometry = CanvasGeometry.CreateCircle(_generator.Device, Vector2.Zero, 0);
                _opacityMaskSurface = _generator.CreateMaskSurface(_layer.Size.ToSize(), circleGeometry);
                opacityMaskBrush.SetSourceParameter("mask", _compositor.CreateSurfaceBrush(_opacityMaskSurface.Surface));

                // Apply the opacity mask brush to the layer
                _layer.Effect = opacityMaskBrush;

                // Create the blur effect
                var blurAmount = BlurAmount;
                var blurEffect = new GaussianBlurEffect
                {
                    Name = "Blur",
                    BlurAmount = blurAmount.Single(),
                    BorderMode = EffectBorderMode.Hard,
                    Source = new CompositionEffectSourceParameter("image")
                };

                var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect, () => blurEffect.BlurAmount);
                var blurBrush = blurEffectFactory.CreateBrush();

                // Load the image
                var uri = ImageSource;
                var imageSurface =
                    await _generator.CreateImageSurfaceAsync(uri, _layer.Size.ToSize(), ImageSurfaceOptions.Default);
                blurBrush.SetSourceParameter("image", _compositor.CreateSurfaceBrush(imageSurface.Surface));

                var visual = _compositor.CreateSpriteVisual();
                visual.Size = _layer.Size;
                visual.Brush = blurBrush;

                _layer.Children.InsertAtTop(visual);

                ElementCompositionPreview.SetElementChildVisual(CompositionGrid, _layer);
            }
        }

        private void OnDrawCanvas(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
        {
            if (!_bloomInProgress)
                return;

            if (_currFrame > _maxFrames)
            {
                _bloomInProgress = false;
                return;
            }

            // ================================================================================================
            // Cubic Ease Out
            // ================================================================================================
            var t = (++_currFrame) / _maxFrames;
            var ts = t * t;
            var tc = ts * t;
            // Cubic ease out fn => b + c * (tc - (3 * ts) + (3 * t))
            _currentRadius = _radiusFrom + (_radiusTo - _radiusFrom) * (tc - (3 * ts) + (3 * t));
            // ================================================================================================

            var circleGeometry = CanvasGeometry.CreateCircle(_generator.Device, _bloomCenter, _currentRadius);
            _opacityMaskSurface.Redraw(circleGeometry);
        }

        #endregion
    }
}

Since Composition APIs cannot be used to animate non-Composition objects, I am using my own animation logic which is invoked periodically using CanvasAnimatedControl's Draw method.

Usage

<local:ImageBloomControl x:Name="BloomControl"
                                 ImageSource="ms-appx:///Assets/Images/Image3.jpg"
                                 BlurAmount="4"
                                 Duration="00:00:02"
                                 OffsetX="0"
                                 OffsetY="100"></local:ImageBloomControl>

@ratishphilip wow! this is exactly what I was looking for. And then with a full code example - couldn't hope for more. Thank you very very much.

Also I have never heard from CompositionProToolkit, it seams to have quite some interesting functions.