Saturday, October 10, 2009

Rasterizing a vector brush for fast scaling animation

When animating (scaling) a complex WPF vector brush, 100% of my CPU is used. The animation also looks jerky. To speed things up, I rasterized the vector brush into a bitmap brush. The CPU load decreases below 30% and the animation became much smoother.

So how do I create a bitmap brush? There is no meaningful properties or methods to override in the Brush class, as most of the workings of brush are marked as internal. To overcome the problem, the brush is implemented as a markup extension.

To use the code, pass your vector brush into the RasterizeBrush class.

<Button
    x:Name="helloButton"
    Background="{app:RasterizeBrush {StaticResource HelloBrush}}"
    >
        <TextBlock>Hello</TextBlock>
</Button>

And place the following code inside your project.

/-----------------------------------------------------------------------
// <copyright file="RasterizeBrushExtension.cs" company="Jeow Li Huan">
// Copyright (c) Jeow Li Huan. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
 
namespace Huan.Windows.Markup
{
    using System;
    using System.Windows;
    using System.Windows.Markup;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
 
    /// <summary>
    /// Converts a vector brush into a bitmap brush to speed up scaling.
    /// </summary>
    [MarkupExtensionReturnType(typeof(ImageBrush))]
    public class RasterizeBrushExtension : MarkupExtension
    {
        /// <summary>
        /// Object used to synchronize access to the static properties <see cref="P:DefaultWidth"/> and <see cref="P:DefaultHeight"/>.
        /// </summary>
        private static object sync = new object();
 
        /// <summary>
        /// Backing field for the <see cref="P:DefaultWidth"/> property.
        /// </summary>
        private static int defaultWidth = 64;
 
        /// <summary>
        /// Backing field for the <see cref="P:DefaultHeight"/> property.
        /// </summary>
        private static int defaultHeight = 64;
 
        /// <summary>
        /// Backing field for the <see cref="P:OriginalBrush"/> property.
        /// </summary>
        private Brush originalBrush;
 
        /// <summary>
        /// The converted bitmap brush.
        /// </summary>
        private ImageBrush rasteredBrush;
 
        /// <summary>
        /// Backing field for the <see cref="P:Width"/> property.
        /// </summary>
        private int width;
 
        /// <summary>
        /// Backing field for the <see cref="P:Height"/> property.
        /// </summary>
        private int height;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="RasterizeBrushExtension"/> class.
        /// </summary>
        public RasterizeBrushExtension()
        {
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="RasterizeBrushExtension"/> class.
        /// </summary>
        /// <param name="originalBrush">The original brush that is to be converted into a bitmap brush.</param>
        public RasterizeBrushExtension(Brush originalBrush)
            : this()
        {
            this.originalBrush = originalBrush;
        }
 
        /// <summary>
        /// Gets or sets the default number of horizontal pixels for the bitmap to render on.
        /// </summary>
        /// <value>The default number of horizontal pixels for the bitmap to render on.</value>
        public static int DefaultWidth
        {
            get
            {
                lock (sync)
                    return defaultWidth;
            }
 
            set
            {
                lock (sync)
                    defaultWidth = value;
            }
        }
 
        /// <summary>
        /// Gets or sets the default number of vertical pixels for the bitmap to render on.
        /// </summary>
        /// <value>The default number of vertical pixels for the bitmap to render on.</value>
        public static int DefaultHeight
        {
            get
            {
                lock (sync)
                    return defaultHeight;
            }
 
            set
            {
                lock (sync)
                    defaultHeight = value;
            }
        }
 
        /// <summary>
        /// Gets or sets the original brush that is to be converted into a bitmap brush.
        /// </summary>
        /// <value>The original brush that is to be converted into a bitmap brush.</value>
        [ConstructorArgument("originalBrush")]
        public Brush OriginalBrush
        {
            get
            {
                return this.originalBrush;
            }
 
            set
            {
                if (this.originalBrush != value)
                {
                    this.rasteredBrush = null;
                    this.originalBrush = value;
                }
            }
        }
 
        /// <summary>
        /// Gets or sets the number of horizontal pixels for the bitmap to render on.
        /// </summary>
        /// <value>The number of horizontal pixels for the bitmap to render on.</value>
        public int Width
        {
            get
            {
                return this.width;
            }
 
            set
            {
                ifthis.width != value)
                {
                    this.rasteredBrush = null;
                    this.width = value;
                }
            }
        }
 
        /// <summary>
        /// Gets or sets the number of vertical pixels for the bitmap to render on.
        /// </summary>
        /// <value>The number of vertical pixels for the bitmap to render on.</value>
        public int Height
        {
            get
            {
                return this.height;
            }
 
            set
            {
                if (this.height != value)
                {
                    this.rasteredBrush = null;
                    this.height = value;
                }
            }
        }
 
        /// <summary>
        /// Returns the converted bitmap brush.
        /// </summary>
        /// <param name="serviceProvider">Not used.</param>
        /// <returns>
        /// The converted bitmap brush.
        /// </returns>
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            int width = (this.width == 0) ? RasterizeBrushExtension.defaultWidth : this.width;
            int height = (this.height == 0) ? RasterizeBrushExtension.defaultHeight : this.height;
            if (this.originalBrush == null || width == 0 || height == 0)
                return null;
 
            if (this.rasteredBrush == null)
            {
                RenderTargetBitmap targetBitmap = new RenderTargetBitmap(width, height, 96.0, 96.0, PixelFormats.Default);
                Rectangle rectangle = new Rectangle();
                rectangle.Width = width;
                rectangle.Height = height;
                rectangle.Fill = this.originalBrush;
 
                rectangle.Measure(new Size(width, height));
                rectangle.Arrange(new Rect(0, 0, width, height));
 
                targetBitmap.Render(rectangle);
                targetBitmap.Freeze();
                this.rasteredBrush = new ImageBrush(targetBitmap);
 
                TileBrush tileBrush = this.originalBrush as TileBrush;
                if (tileBrush != null)
                {
                    this.rasteredBrush.AlignmentX = tileBrush.AlignmentX;
                    this.rasteredBrush.AlignmentY = tileBrush.AlignmentY;
                    this.rasteredBrush.Stretch = tileBrush.Stretch;
                    this.rasteredBrush.TileMode = tileBrush.TileMode;
                    this.rasteredBrush.Viewbox = tileBrush.Viewbox;
                    this.rasteredBrush.ViewboxUnits = tileBrush.ViewboxUnits;
                    this.rasteredBrush.Viewport = tileBrush.Viewport;
                    this.rasteredBrush.ViewportUnits = tileBrush.ViewportUnits;
                }
            }
 
            return this.rasteredBrush;
        }
    }
}

No comments: