Sometimes I need to do something different to my normal day job, doing a bit of escapism, especially during these challenging times with a lot of bad news.

One thing I thought would be cool in the current virtual circumstances is a matrix-like virtual background for my OBS setup. And for I wanted to implement such a matrix-effect myself since I watched the first movie and got into programming (both was quite some time ago), I decided to try.

I chose JavaScript as my implementation language, because it’s very easy to get visual results, my JavaScript is very, very rusty and it might be useful to have a little refresher.

If you don’t want to read on, you can jump directly to the public Git Repository with the results.

First Attempt: HTML Elements

My first attempt was without any performance considerations at all, just to play a bit with the animation capabilities of JavaScript, give some room to experiment and learn.

I didn’t refine that first version, it’s the real, rough, ugly thing I was happy to had running after a loot of googling, trying, exploring.

First attempt

One of the major problems is, that I used HTML elements to animate “particles”. This is incredibly computation expensive and not a good choice. It was a nice first step for me, though, because I learned a lot about the problem domain and the language.

Second Attempt: Canvas Particles

The natural next step for me was to use the Canvas element and its 2D context instead. With the Canvas, you don’t need to deal with HTML elements and all its overhead, but draw directly to a … Canvas.

I created a class MatrixRow that could contain a lot of MatrixChar objects, each of them able to draw and organize themselves. All the MatrixChars would start with 100% opacity and slowly fade themselves out over time.

This technique was a lot more performant and I put in some randomizations in size, velocity etc, which led to a pretty nice outcome.

function render(time) {
    context.fillStyle="#000000";
    context.fillRect(0, 0, wi, hi);

    matrixRows.forEach(mRow => {
        mRow.draw(context, time);
    });

    matrixRows = matrixRows.filter(elem => {return elem.chars.length > 0 || elem.curY < hi});

    if ( time - prev > 50 && matrixRows.length < maxRows ) {
        prev = time;
        matrixRows.push(new MatrixRow(Math.random()*wi-10));
    }
    requestAnimationFrame(render);
}
Second Attmept

So in fact I chose kind of a particle system solution. The problem with that was, that I had a lot of particles (=MatrixChars) to deal with, sometimes 15’000 at a time.

Not very brilliant for performance. This might also be due to non-optimal implementation on my side, I know that “real” particle systems are implemented differently. Again, you will find the rusty, imperfect version in the repository that nonetheless taught me a lot about where I wanted to go.

Besides that, the outcome didn’t exactly look like Matrix-effect.

Third Attempt: Canvas Layer-Fade

My third attempt was inspired by my dear friend Ben, who suggested that instead of drawing all the slightly visible chars all the time with different opacity, I could draw only the first “hit” of each char in a new Y-position and draw a transparent black rectangle over the whole scene every time.

The “new” chars would be most bright, previous chars would have an additional “layer” of black over them every time the scene is drawn.

That way I could achieve a similar effect but only at a fraction of necessary computation.

To look more like the real Matrix-effect, I also rasterized the stage. Chars would only appear in a grid, which would prevent overlapping.

function initCanvas(stage) {
    stage.width = wi;
    stage.height = hi;
    
    gridHorizontal = Math.floor(wi/(fontSize-6));
    gridVertical = Math.floor(hi/(fontSize));

    context.fillStyle="#000000";
    context.fillRect(0, 0, wi, hi);
}

function initChar() {
    var char = {
        x: (Math.floor(Math.random()*gridHorizontal)),
        y: 0,
        tickTime: Math.random()*50+50,
        lastTick: performance.now(),
        char: getRandomHexChar()
    }
    return char;
}

Another thing I did to make the animations more interesting was to randomly increase the brightness of the drawn Char:

function addBrightness( rgb, brightness ) {
    var multiplier = (100+brightness)/100;
    var result = {};
    result.r = rgb.r * multiplier;
    result.g = rgb.g * multiplier;
    result.b = rgb.b * multiplier;
    return result;
}

I also found a very cost-efficient way to manage my array of Chars to keep track of: Instead of creating a new array every time with only the chars that are still visible (have a lower Y than the stage height) I would reorganize the array, moving the valid chars up and cut off all the left over elements of the array.

var iOut = 0;
for ( var i = 0; i < chars.length; i++ ) {
        var c = chars[i];
        if ( c.y < gridVertical ) {
            chars[iOut++] = c;
        }
...
}
chars.length = iOut;

This is possible in JavaScript – in other langauges you might have a hard time to manipulate the length of an Array 😉

The final render-method looks like this:

function render(time) {
    // Draw a transparent, black rect over everything
    // But not each time
    if ( time - prev > 50 ) {
        context.fillStyle="rgba(0,0,0,"+alphaMask+")";
        context.fillRect(0, 0, wi, hi);
        prev = time;
    }
        
    // Setup Context Font-Style
    context.font = 'bold 20px Consolas';
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    
    var iOut = 0;
    for ( var i = 0; i < chars.length; i++ ) {
        var c = chars[i];
        if ( c.y < gridVertical ) { // If Char is still visible
            chars[iOut++] = c; // put it further-up in the array

            // Add a bit more random brightness to the char
            var color = addBrightness({r: 100, g:200, b:100}, Math.random()*70);
            context.fillStyle = "rgb("+color.r+","+color.g+","+color.b+")";
            context.fillText(c.char, c.x*(fontSize-6), c.y*(fontSize));

            // Only move one y-field down if the randomized TickTime is reached
            if ( time - c.lastTick > c.tickTime) {
                c.y++;
                c.lastTick = time;
                // New y-field means new Char, too
                c.char = getRandomHexChar();
            }
        }
    }
    chars.length = iOut; // Adjust array to new length. 
    //Every visible char is moved to a point before this, the rest is cut off
    
    var newChars = 0;
    while (chars.length < maxRunningChars && newChars < 3) {
        chars.push(initChar());
        newChars++;
    }

    requestAnimationFrame(render);
}

Again, a lot of randomization is added and I also included a limitation of maxRunningChars.

But I am very, very satisfied with the current end result:

Attempt Three

Putting it together in OBS

One of the very cool things in OBS is, that you can add a “Browser”-Source. With that, I can put my Matrix-website directly on the stage.

And this is how attempt 3 looks in action:


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.