Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Matrix-Style Rain in C# with WPF

0.00/5 (No votes)
18 Sep 2019 1  
Matrix digital rain, Matrix code or sometimes green rain, is the computer code featured in the Matrix movies
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, DrawingVisual 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.

Image 1

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:

1. 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:

    1. 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.

      Image 2

      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. For example:

      Image 3
      Frame1

      Image 4
      Frame 2

      Image 5
      Frame 3
      Image 6
      Frame 4
    2. 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:

      Image 7
      Final Frame1 = Black background + Frame1

      Image 8
      Final Frame 2 = Final Frame1 + Frame2

      Image 9
      Final Frame 3 = Final Frame2 + Frame3
      Image 10
      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();
      ...

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);
    }

Points of Interest

Quote:

This is your last chance. After this, there is no turning back. You take the blue pill - the story ends, you wake up in your bed and believe whatever you want to believe. You take the red pill - you stay in Wonderland, and I show you how deep the rabbit hole goes. Remember: all I'm offering is the truth. Nothing more.

The red pill is the DrawingVisual, so make your choice.

P.S.: To generate the letter random, I used a personal "CryptoRandom" class (source included) instead of the canonical Random method. This because Random method generates a 'pseudo random' number. Follow this link if you want to dig in.

If you have any suggestions for modifications, please don't hesitate to contact me.

History

  • Version 1.1.0 - September 2019. Improvements:
    • The application automatically starts at the center of the screen.
    • The maximize button now shows the application fullscreen (Esc button to exit). If you apply special Brush for the letters, the system can start to drop frames because I'm using RenderTargetBitmap which operates in CPU (with big resolution, this can slow down the performance).
  • Version 1.0.0 - August 2019 - First release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here