Wednesday, 18 March 2020

Matrix-Style Rain in C# with WPF




This article is intended for those people who want to understand how DrawingVisual works in WPF. I assume the reader knows WPF dispatcher, and provide a sample made up of two projects that I run through step by step.

Introduction

As described by MSDN, Drawing Visual is a lightweight drawing class that is used to render shapes, images, or text. This class is considered lightweight because it does not provide layout, input, focus, or event handling, which improves its performance.

Background

Before I started coding, I consulted MSDN page to understand the basic of DrawingVisual Objects and WPF Graphics Rendering Overview.

Many of the elements/controls that we commonly use in WPF like Button, ComboBox, Shape, and others have these characteristics:
  • Can be composed by multiple elements, each of the composing elements provide focus method, event handling and many features which allow us to have a lot of freedom of programming but with a lot of "overhead" if we just need to perform some drawing.
  • Extend common objects which are not optimized for a specific purpose but for generic service.

The scope of DrawingVisual is to propose a lightweight approach to object drawing.

Regarding the matrix rain effect, I take some ideas on how to develop it from CodePen, which is an online community for testing and showcasing user-created HTML, CSS and JavaScript code snippets.

I assume the reader knows WPF dispatcher. Briefly, when you execute a WPF application, it automatically creates a new Dispatcher object and calls its Run method. All the visual elements will be created by the dispatcher thread and all the modification to visual elements must be executed on Dispatcher thread.

Using the Code

My sample is made up of two projects:

A. MatrixRain

This is the core of the solution. This project implements a UserControl that simulates the Matrix digital rain effect. The UserControl can be used in any Window/Page, etc.
  1. Set up parameter.
The SetParameter method allows to set up some animation parameter:
...
public void SetParameter(int framePerSecond = 0, FontFamily fontFamily = null,
                         int fontSize = 0, Brush backgroundBrush = null,
                         Brush textBrush = null, String characterToDisplay = "")
...

  • framePerSecond: Frame per second refresh (this parameter affect the "speed" of the rain)
  • fontFamily: Font family used
  • fontSize: Dimension of the font used
  • backgroundBrush: Brush used for the background
  • textBrush: Brush used for the text
  • characterToDisplay: The character used for the rain will be randomly chosen from this string
   2. Start the animation.
The Start and Stop methods allow to start and stop the animation:

public void Start() {
    _DispatcherTimer.Start();
}
public void Stop() {
    _DispatcherTimer.Stop();
}
...

The animation is controlled through System.Timers.Timer. I prefer this solution over System.Windows.Threading.DispatcherTimer because the DispatcherTimer is re-evaluated at the top of every Dispatcher loop and the timer is not guaranteed to execute exactly when the time interval occurs.
Every tick, the method _DispatcherTimerTick(object sender, EventArgs e) is called.
This method is not executed on the Dispatcher thread so the first thing is to sync the call on the Dispatcher thread because we need to work with some resources accessible only by the main thread.
...

private void _DispatcherTimerTick(object sender, EventArgs e)
{
    if (!Dispatcher.CheckAccess()) {
        //synchronize on main thread
        System.Timers.ElapsedEventHandler dt = _DispatcherTimerTick;
        Dispatcher.Invoke(dt,sender,e);
        return;
    }
    ....
}

     3. Draw the new frame.
Once the call from the timer is on the dispatcher thread, it performs two operations:
  • Design the new frame
The frame is created by the method _RenderDrops(). Here is a new DrawingVisual and its DrawingContext are created to draw objects. The drawing context allows drawing line, ellipse, geometry, images and many more.

DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();

First, the method creates a black background with a 10% of opacity (I will explain later why I put 10% opacity).
After this, we scroll through an array called _Drops.
This array represents the column along which the letters are drawn (see the red column in the image). The value of the array represents the row (see the blue circle in the image) where a new letter must be drawn. When the value of the drop reaches the 'bottom' of the image, the drop re-starts from the top immediately or randomly after a series of cycle.

...
//looping over drops
for (var i = 0; i < _Drops.Length; i++) {
    // new drop position
    double x = _BaselineOrigin.X + _LetterAdvanceWidth * i;
    double y = _BaselineOrigin.Y + _LetterAdvanceHeight * _Drops[i];

    // check if new letter does not goes outside the image
    if (y + _LetterAdvanceHeight < _CanvasRect.Height) {
        // add new letter to the drawing
        var glyphIndex = _GlyphTypeface.CharacterToGlyphMap[_AvaiableLetterChars[
                         _CryptoRandom.Next(0, _AvaiableLetterChars.Length - 1)]];
        glyphIndices.Add(glyphIndex);
        advancedWidths.Add(0);
        glyphOffsets.Add(new Point(x, -y));
    }

    //sending the drop back to the top randomly after it has crossed the image
    //adding a randomness to the reset to make the drops scattered on the Y axis
    if (_Drops[i] * _LetterAdvanceHeight > _CanvasRect.Height && 
                                           _CryptoRandom.NextDouble() > 0.775) {
        _Drops[i] = 0;
    }
    //incrementing Y coordinate
    _Drops[i]++;
}
// add glyph on drawing context
if (glyphIndices.Count > 0) {
    GlyphRun glyphRun = new GlyphRun(_GlyphTypeface,0,false,_RenderingEmSize,
                        glyphIndices,_BaselineOrigin,advancedWidths,glyphOffsets,
                        null,null,null,null,null);
    drawingContext.DrawGlyphRun(_TextBrush, glyphRun);
}
...

To recap the method, _RenderDrops() generates DrawingVisual that contains a background with opacity and the new drops letters.

  • Copy the new frame over the previous one
As seen before, the new frame only generates the "new" letter, but how can we fade away the previous letters?
This is performed by the background of the frame which is black with 10% opacity. When we copy a new frame over the previous frame, the blending makes the trick. The "copy over" weakens the previous letters luminance as shown in this example:

Final Frame1 = Black background + Frame1

Final Frame 2 = Final Frame1 + Frame2

Final Frame 3 = Final Frame2 + Frame3

Final Frame 4 = Final Frame3 + Frame4
P.S.: I render the Drawing Visual on a RenderTargetBitmap. I could apply this directly on my image:

_MyImage.Source = _RenderTargetBitmap 

The problem with this solution is that at every cycle, this operation allocates a lot of memory at every cycle. To overlap this problem, I use WriteableBitmap which is allocated in memory only once in the initialization code.

...
_WriteableBitmap.Lock();
_RenderTargetBitmap.CopyPixels(new Int32Rect(0, 0, _RenderTargetBitmap.PixelWidth,
                                            _RenderTargetBitmap.PixelHeight), 
                               _WriteableBitmap.BackBuffer, 
                               _WriteableBitmap.BackBufferStride * 
                               _WriteableBitmap.PixelHeight,
                               _WriteableBitmap.BackBufferStride);
_WriteableBitmap.AddDirtyRect(new Int32Rect(0, 0, _RenderTargetBitmap.PixelWidth, 
                                            _RenderTargetBitmap.PixelHeight));
_WriteableBitmap.Unlock();
...


2. MatrixRainWpfApp


This project references MatrixRain and showcases the potentiality of MatrixRain user control. The code is not commented, because it is so simple that does not need to be.
  1. In the MainWindow.xaml, a MatrixRain control is added to the window:
    ...
    xmlns:MatrixRain="clr-namespace:MatrixRain;assembly=MatrixRain"
    ...
    <MatrixRain:MatrixRain x:Name="mRain" HorizontalAlignment="Left" Height="524" 
    
                           Margin="10,35,0,0" VerticalAlignment="Top" Width="1172"/>
    ...
     
  2. During Initialization, I read a special font from the embedded resources and pass it to MatrixRain control:
    FontFamily rfam = new FontFamily(new Uri("pack://application:,,,"), 
                                     "./font/#Matrix Code NFI");
    mRain.SetParameter(fontFamily: rfam);
     
    Please pay attention to the font. This is the link where I found it: https://www.1001fonts.com/matrix-code-nfi-font.html. This is free to use only for personal purposes.

  3. Two buttons: Start and Stop; command the animation:
    private void _StartButtonClick(object sender, RoutedEventArgs e)
    {
        mRain.Start();
    }
    
    private void _StopButtonClick(object sender, RoutedEventArgs e)
    {
        mRain.Stop();
    }
  4. Two buttons: Set1 and Set2; command the text color:
    private void _ChangeColorButtonClick(object sender, RoutedEventArgs e)
    {
        mRain.SetParameter(textBrush: ((Button)sender).Background);
    }

No comments:

Post a comment