SWHarden.com

The personal website of Scott W Harden

Boids in your Browser with Blazor

This project implements the Boids flocking algorithm in C# and uses Blazor WebAssembly to render it in the browser. Drone birds (bird-oids, or boids) follow a simple set of rules to determine their angle and velocity. When many boids are placed in a field together, interesting group behaviors emerge. Details about the boid flocking algorithm are discussed in depth elsewhere. This article summarizes how to configure a Blazor application to model graphics with C# and render them with JavaScript.

Live Demo

The C# Boids Model

The code that manages the field of boids is entirely written in C#. It tracks the positions of boids and advances their positions and directions over time, but it does not manage rendering. Briefly,

Model with C#, Render with JavaScript

At the time of writing, the most performant way to render high frame rates in the browser is to write your model and business logic entirely in JavaScript. The purpose of this project is not to make the most performant model possible, but rather to explore what can be done with client-side Blazor and WebAssembly. There is a strong advantage to being able to keep keeping your graphics model and business logic in C#, especially if it is already written and extensively tested.

In my Draw Animated Graphics in the Browser with Blazor WebAssembly article we used Blazor.Extensions.Canvas to draw on a HTML Canvas from C# code. Although this worked, it was limited by the fact that interop calls can be slow. Even though they can be batched, they still represent a significant bottleneck when thousands of calls need to be made on every frame.

In this project I used C# to model the field of boids, converted the field to JSON (with each boid having a X/Y position and a rotation), and had JavaScript parse the boid array and render each boid using a rendering function inside JavaScript.

Source Code

The full source code can be downloaded here (blazor-boids.zip) or navigated on GitHub C# Data Visualization: Blazor Boids. The C# code for Boid and BoidField can be found there. What I’ll focus on here is the key Blazor code required to manage the model and render it using JavaScript.

index.razor

There are a lot of different things going on here. Most of them can be figured out with a little visual inspection. Here are the highlights:

@page "/"
@using System.Text;
@inject IJSRuntime JsRuntime
@inject NavigationManager NavManager
@using Microsoft.AspNetCore.WebUtilities

<div id="boidsHolder" style="position: fixed; width: 100%; height: 100%">
    <canvas id="boidsCanvas"></canvas>
</div>

@code{
    private Random rand = new Random();
    private Models.Field boidField;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // call the initialization JavaScript function
        await JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
        await base.OnInitializedAsync();

        // use a query string to customize the number of boids
        int boidCount = 75;
        var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
        if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("boids", out var boidCountString))
            if (int.TryParse(boidCountString, out int parsedBoidCount))
                boidCount = parsedBoidCount;

        // use a query string to customize the number of predators
        int predatorCount = 3;
        if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("predators", out var predatorCountString))
            if (int.TryParse(predatorCountString, out int parsedPredatorCount))
                predatorCount = parsedPredatorCount;

        // create the model using the custom settings
        boidField = new Models.Field(800, 600, boidCount, predatorCount);
    }

    [JSInvokable]
    public string UpdateModel(double width, double height)
    {
        if (boidField is null)
            return "";

        boidField.Resize(width, height);
        boidField.Advance(bounceOffWalls: true, wrapAroundEdges: false);

        StringBuilder sb = new StringBuilder("[");
        foreach (var boid in boidField.Boids)
        {
            double x = boid.X;
            double y = boid.Y;
            double r = boid.GetAngle() / 360;
            sb.Append($"[{x:0.0},{y:0.0},{r:0.000}],");
        }

        sb.Append(boidField.PredatorCount.ToString());
        sb.Append("]");
        return sb.ToString();
    }
}

index.html

Two JavaScript functions are added to the HTML:

<script>

    function renderJS() {

        // resize the canvas to fit its parent (resizing clears the canvas too)
        var holder = document.getElementById('boidsHolder');
        var canvas = document.getElementById('boidsCanvas');
        canvas.width = holder.clientWidth;
        canvas.height = holder.clientHeight;

        // tell C# about the latest dimensions, advance the model, and parse the new data
        var boidsString = window.theInstance.invokeMethod('UpdateModel', canvas.width, canvas.height);
        var boids = JSON.parse(boidsString);
        var predatorCount = boids.pop();

        // render each boid
		var ctx = document.getElementById('boidsCanvas').getContext('2d');
        for (var i = 0; i < boids.length; i++) {
            var predator = i < predatorCount;
            var boid = boids[i];
            var x = boid[0];
            var y = boid[1];
            var rotation = boid[2];
            ctx.save();
            ctx.translate(x, y);
            ctx.rotate(rotation * 2 * Math.PI);
            ctx.beginPath();
            ctx.moveTo(0, 0);
            ctx.lineTo(4, -2);
            ctx.lineTo(0, 10);
            ctx.lineTo(-4, -2);
            ctx.lineTo(0, 0);
            ctx.closePath();
            ctx.fillStyle = predator ? '#FFFF00' : '#FFFFFF';
            ctx.fill();
            ctx.restore();
        }

        // call this same function to render the next frame
        window.requestAnimationFrame(renderJS);
    }

    window.initRenderJS = (instance) => {
        window.theInstance = instance;
        window.requestAnimationFrame(renderJS);
    };

</script>

Resources

Blazor Boids

Blazor Source Code

Boids in C# (Windows Application)

JavaScript Boids Simulators

Obviously a native JavaScript Boids simulator will be much faster. Implementing this Blazor app in JavaScript would have meant translating the model from C# to JavaScript. For performance-critical rendering-intensive applications, this is the way to go at the time of writing.

Literature