My website went down for a few hours today when my hosting company unexpectedly changed Apache’s root http folder to a new one containing a symbolic link. This change broke some of my PHP scripts because __DIR__ suddenly had a base path that was different than DOCUMENT_ROOT, and the str_replace method I was using to determine the directory URL assumed they would always be the same. If you Google “how to get the URL of the current directory with PHP”, you’ll probably find recommendations to use code like this:
// WARNING: DON'T USE THIS CODE!
$folderUrl='http://'.$_SERVER['HTTP_HOST'].str_replace($_SERVER['DOCUMENT_ROOT'], '', __DIR__);
Let’s look at what these variables resolve to on my site:
Comparing __DIR__ with DOCUMENT_ROOT, notice that without real path resolution the base paths are different! This caused the string replace method to abruptly fail on my website, taking several sites down for a few hours. I’ll accept it as a rookie mistake on my part, and I’m sharing what I learned here in case it helps others in the future.
The Solution is to ensure all paths are converted to canonicalized absolute paths using realpath(). This will protect you from unexpected symbolic links. Notice this code also adds the appropriate HTTP or HTTPS prefix.
// This script displays the URL of the current folder
$realDocRoot= realpath($_SERVER['DOCUMENT_ROOT']);
$realDirPath= realpath(__DIR__);
$suffix= str_replace($realDocRoot, '', $realDirPath);
$prefix= isset($_SERVER['HTTPS']) ?'https://':'http://';
$folderUrl=$prefix.$_SERVER['HTTP_HOST'] .$suffix;
echo$folderUrl;
This page contains notes about the analytical methods LJPcalc uses to calculate LJP from ion tables
as well as notes for experimenters about what causes LJP and how to compensate for it in electrophysiology experiments.
💡 Use LJPcalc to calculate Liquid Junction Potential (LJP) in your browser
LJPcalc calculates the liquid junction potential according to the stationary Nernst-Planck equation which is typically regarded as superior to the simpler Henderson equation used by most commercial LJP calculators. Both equations produce nearly identical LJPs, but the Henderson equation becomes inaccurate as ion concentrations increase, and also when calculating LJP for solutions containing polyvalent ions.
The ion mobility table is stored in Markdown format. Not only does Markdown make it easy to display the table nicely in a browser,
but it also makes the table easy to edit in any text editor. Users desiring to use their own ion mobilities or add new ions to the table
can do so by editing the IonTable.md file adjacent to LJPcalc.exe as needed.
💡 LJPcalc automatically sorts the ion table into an ideal sequence prior to solving for LJP. Attention only needs to be paid to the ion sequence if automatic sorting is disabled.
When calculating LJP for a set of ions it is important to consider the sequence in which they are listed. Additional information can be found in Marino et al., 2014 which describes the exact computational methods employed by LJPcalc.
The last ion’s c0 may be overridden to achieve electroneutrality on the c0 side. This will not occur if the sum of charge on the c0 side is zero.
cL for most ions will be slightly adjusted to achieve electroneutrality on the cL side. The second-to-last ion’s cL (which cannot equal its c0) will remain fixed, while the last cL will be adjusted to achieve electroneutrality. During the solving process all cL values (but the second-from-last) will be slightly adjusted. The adjustments are likely negligible experimentally, but this is why cL values in the output table slightly differ from those given for inputs.
The LJP is temperature dependent. There are two sources of temperature-dependent variation: the Einstein relation and the conductivity table. The former can be easily defined at calculation time, while the latter requires modifying conductances in the ion mobility table. These modifications typically have a small effect on the LJP, so standard temperature (25C) can be assumed for most applications.
The ion conductivity table is temperature-specific. Ion conductivity was measured experimentally and varies with temperature. The ion conductivity table here assumes standard temperature (25C), but ion conductivity values can be found for many ions at nonstandard temperatures. LJPcalc users desiring to perform LJP calculations at nonstandard temperatures are encouraged to build their own temperature-specific ion tables.
Patch-clamp electrophysiologists impale cells with glass microelectrodes to measure or clamp their voltage.
Amplifier offset voltage is adjusted to achieve a reading of zero volts when the pipette is in open-tip configuration with the bath,
but this voltage includes offset for a liquid junction potential (LJP) caused by the free exchange of ions with different mobilities between the pipette and bath solutions.
Whole-cell patch-clamp experiments typically fill the pipette with large anions like gluconate, aspartate, or methanesulfonate, and their low mobility
(relative to ions like K, Na, and Cl) causes them to accumulate in the pipette and produce a LJP (where the bath is more positive than then pipette).
After establishment of whole-cell configuration, ions no longer freely move between pipette and bath solutions (they are separated by the cell membrane), so
there is effectively no LJP but the offset voltage is still offsetting as if LJP were present. By knowing the LJP, the scientist can
adjust offset voltage to compensate for it, resulting in more accurate measured and clamped voltages.
Vmeter = Vcell + LJP
To correct for LJP, the electrophysiologist must calculate LJP mathematically (using software like LJPcalc) or estimate it experimentally (see the section on this topic below). Once the LJP is known it can be compensated for experimentally to improve accuracy of recorded and clamped voltages.
Vcell = Vmeter - LJP
⚠️ This method assumes that the amplifier voltage was zeroed at the start of the experiment when the pipette was in open-tip configuration
with the bath, and that concentration of chloride (if using Ag/AgCl electrodes) in the internal and bath solutions are stable throughout experiments.
This ion set came from in Figl et al., 2003 Page 8. They have been loaded into LJPcalc such that the pipette solution is c0 and the bath solution is cL. Note that the order of ions has been adjusted to place the most abundant two ions at the bottom. This is ideal for LJPcalc’s analytical method.
Name
Charge
pipette (mM)
bath (mM)
K
+1
145
2.8
Na
+1
13
145
Mg
+2
1
2
Ca
+2
0
1
HEPES
-1
5
5
Gluconate
-1
145
0
Cl
-1
10
148.8
Loading this table into LJPcalc produces the following output:
Values for cL were adjusted to achieve electro-neutrality:
Name | Charge | Conductivity (E-4) | C0 (mM) | CL (mM)
--------------------|--------|--------------------|--------------|--------------
K | +1 | 73.5 | 145 | 2.8098265
Na | +1 | 50.11 | 13 | 144.9794365
Mg | +2 | 53.06 | 1 | 1.9998212
Ca | +2 | 59.5 | 0 | 0.9999109
HEPES | -1 | 22.05 | 5 | 4.9990023
Gluconate | -1 | 24.255 | 145 | 0
Cl | -1 | 76.31 | 10 | 148.789725
Equations were solved in 88.91 ms
LJP at 20 C (293.15 K) = 16.052319631180264 mV
💡 Figl et al., 2003 Page 8 calculated a LJP of 15.6 mV for this ion set (720 µV lesser magnitude than our calculated LJP). As discussed above, differences in ion mobility table values and use of the Nernst-Planck vs. Henderson equation can cause commercial software to report values slightly different than LJPcalc. Experimentally these small differences are negligible, but values produced by LJPcalc are assumed to be more accurate. See Marino et al., 2014 for discussion.
If we have patch-clamp data that indicates a neuron rests at -48.13 mV, what is its true resting potential? Now that we know the LJP, we can subtract it from our measurement:
The patch-clamp amplifier is typically zeroed at the start of every experiment when the patch pipette is in open-tip configuration
with the bath solution. An offset voltage (Voffset) is applied such that the Vmeasured is zero.
This process incorporates 3 potentials into the offset voltage:
liquid junction potential (LJP) between the pipette solution and the bath solution (mostly from small mobile ions)
half-cell potential (HCP) between the reference electrode and the bath solution (mostly from Cl)
half-cell potential (HCP) between the recording electrode and the pipette solution (mostly from Cl)
When the amplifier is zeroed before to experiments, all 3 voltages are incorporated into the offset voltage.
Since the LJP is the only one that changes after forming whole-cell configuration with a patched cell (it is eliminated),
it is the only one that needs to be known and compensated for to achieve a true zero offset (the rest remain constant).
However, if the [Cl] of the internal or bath solutions change during the course of an experiment
(most likely to occur when an Ag/AgCl pellet is immersed in a flowing bath solution),
the half-cell potentials become significant and affect Vmeasured as they change.
This is why agar bridge references are preferred over Ag/AgCl pellets.
See Figl et al., 2003
for more information about LJPs as they relate to electrophysiological experiments.
It is possible to measure LJP experimentally, but this technique is often discouraged because issues with KCl reference electrodes make it difficult
to accurately measure (Barry and Diamond, 1970). However, experimental measurement
may be the only option to calculate LJP for solutions containing ions with unknown mobilities.
To measure LJP Experimentally:
• Step 1: Zero the amplifier with intracellular solution in the bath and in your pipette
• Step 2: Replace the bath with extracellular solution
• Step 3: The measured voltage is the negative LJP (invert its sign to get LJP)
✔️ Confirm no drift is present by replacing the bath with intracellular solution after step 3 to verify the reading is 0.
If it is not 0, some type of drift is occurring and the measured LJP is not accurate.
❓ Why invert the sign of the LJP? The LJP measured in step 2 is the LJP of the pipette relative to
the bath, but in electrophysiology experiments convention is to refer to LJP as that of the bath relative to the pipette.
LJPs for experiments using typical ACSF bath and physiological K-gluconate pipette solutions are usually near +15 mV.
⚠️ Do not measure LJP using a Ag/AgCl reference electrode! because mobility will be low when the bath is filled with intracellular solution (physiological intracellular solutions have low [Cl]).
Use a 3M KCl reference electrode instead, allowing high [K] mobility in intracellular solution and high [Cl] mobility in extracellular solution.
Marino et al. (2014) - describes a computational method to calculate LJP according to the stationary Nernst-Planck equation. The JAVA software described in this manuscript is open-source and now on GitHub (JLJP). Figure 1 directly compares LJP calculated by the Nernst-Planck vs. Henderson equation.
Perram and Stiles (2006) - A review of several methods used to calculate liquid junction potential. This manuscript provides excellent context for the history of LJP calculations and describes the advantages and limitations of each.
Shinagawa (1980)“Invalidity of the Henderson diffusion equation shown by the exact solution of the Nernst-Planck equations” - a manuscript which argues that the Henderson equation is inferior to solved Nernst-Planck-Poisson equations due to how it accounts for ion flux in the charged diffusion zone.
Lin (2011)“The Poisson The Poisson-Nernst-Planck (PNP) system for ion transport (PNP) system for ion transport” - a PowerPoint presentation which reviews mathematical methods to calculate LJP with notes related to its application in measuring voltage across cell membranes.
EGTA charge and pH - Empirical determination of EGTA charge state distribution as a function of pH.
LJPCalcWin - A Program for Calculating Liquid Junction Potentials
LJP Corrections (Axon Instruments Application Note) describes how to calculate LJP using ClampEx and LJPCalcWin and also summarizes how to measure LJP experimentally
LJP Corrections (Figl et al., AxoBits 39) summarizes LJP and discusses measurement and calculation with ClampEx
Google Charts is a free interactive JavaScript charting framework. The Google Chart Gallery showcases many of the available chart types and options. This project shows how to create data from C# functions in Blazor, then use JavaScript interop to pass arrays into JavaScript for display with Google Charts.
This page contains a div where the chart will be displayed, controls for customizing settings, and a few functions in the code-behind to call a JavaScript function that updates the chart.
@page "/"
@inject IJSRuntime JsRuntime
<!-- this is where the Google Chart is displayed --><divid="chart_div"style="height: 400px;"></div>
<!-- buttons call C# functions --><button@onclick="PlotSin">Sin</button>
<button@onclick="PlotRandom">Random</button>
<button@onclick="PlotWalk">Walk</button>
<button@onclick="PlotRandomXY">RandomXY</button>
<!-- a slider bound to a C# field --><inputtype="range"@bind="PointCount"@bind:event="oninput">
@code{
privateint PointCount = 123;
Random Rand = new Random();
privatevoid PlotData(double[] xs, double[] ys)
{
// This function calls a JavaScript function to update the chart.// Notice how multiple parameters are passed in. JsRuntime.InvokeVoidAsync("createNewChart", new { xs, ys });
}
privatevoid PlotSin()
{
double[] xs = Enumerable.Range(0, PointCount).Select(x => (double)x).ToArray();
double[] ys = xs.Select(x => Math.Sin(x / 10)).ToArray();
PlotData(xs, ys);
}
privatevoid PlotRandom()
{
double[] xs = Enumerable.Range(0, PointCount).Select(x => (double)x).ToArray();
double[] ys = xs.Select(x => (Rand.NextDouble() - .5) * 1000).ToArray();
PlotData(xs, ys);
}
privatevoid PlotWalk()
{
double[] xs = Enumerable.Range(0, PointCount).Select(x => (double)x).ToArray();
double[] ys = newdouble[PointCount];
for (int i = 1; i < ys.Length; i++)
ys[i] = ys[i - 1] + Rand.NextDouble() - .5;
PlotData(xs, ys);
}
privatevoid PlotRandomXY()
{
double[] xs = Enumerable.Range(0, PointCount).Select(x => Rand.NextDouble()).ToArray();
double[] ys = Enumerable.Range(0, PointCount).Select(x => Rand.NextDouble()).ToArray();
PlotData(xs, ys);
}
}
These JavaScript blocks were added just before the closing </body> tag
<script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script><script> google.charts.load('current', { packages: ['corechart', 'line'] });
// draw an empty chart when the page first loads
google.charts.setOnLoadCallback(initChart);
function initChart() {
var xs = [];
var ys = [];
window.createNewChart({xs, ys});
}
// draw a new chart given X/Y values
window.createNewChart = (params) => {
var xs = params.xs;
var ys = params.ys;
var data =new google.visualization.DataTable();
data.addColumn('number', 'X');
data.addColumn('number', 'Y');
for (var i =0; i < ys.length; i++) {
data.addRow([xs[i], ys[i]]);
}
var options = {
hAxis: { title:'Horizontal Axis Label' },
vAxis: { title:'Vertical Axis Label' },
title:'This is a Google Chart in Blazor',
legend: { position:'none' },
};
var chart =new google.visualization.LineChart(document.getElementById('chart_div'));
chart.draw(data, options);
};
</script>
Plots look bad when the window is resized because Google Charts adopts a fixed size on each render. To give the appearance of fluid charts (that resize to fit their container as its size changes), add some JavaScript to re-render on every resize.
window.onresize =function () { initChart(); };
We can’t call our existing createNewChart() method because that expects data (from C#/Blazor) passed-in as a parameter. To support this type of resizing, the call from C# must be modified to store data arrays at the window level so they can be later accessed when the chart is plotted again. This would take some re-structuring of this project, but it’s possible.
Google Charts is a straightforward way to display data in mouse-interactive graphs on the web.
JS interop allows Blazor to pass data to a JavaScript function that can plot it on a Google Chart.
Extra steps are required to automatically resize a Google Chart when its container size changes.
For interactive graphs in desktop applications, check out ScottPlot (an open-source plotting library for .NET that makes it easy to interactively display large datasets).
The project simulates the classic Mystify your Mind screensaver from Windows 3 using client-side Blazor. The graphics model logic is entirely C# and the web interface has user-facing options (razor components) to customize its behavior in real time. To render a frame JavaScript calls a C# function that returns JSON containing an array of line details (colors and coordinates) so a JavaScript render function can draw them on a HTML canvas. While .NET APIs for drawing on canvases exist, I find this method to be a bit more versatile because it does not require any external libraries, and it only makes a single JS interop call on each render so it is appreciably faster.
The point of this article is to discuss strategies for rendering graphics using client-side Blazor and WebAssembly, so I won’t detail how the polygons are tracked here. Source code for these classes can be viewed on GitHub (MystifyBlazor/Models) or downloaded with this project (blazor-mystify.zip).
Models/Color.cs - Represents a color and has methods to help with hue-shifting
Models/Corner.cs - Represents a corner of a polygon (position, direction, and velocity) and has logic to move forward in time and bounce off walls
Models/Polygon.cs - A polygon is just a color and a list of corners that are connected by a single line.
Models/Shape.cs - A Shape is a moving polygon, and this class contains a list of polygons (a historical record of a single polygon’s configuration as it changed over time).
Models/Field.cs - Represents the rectangular render area and holds multiple shapes.
This is the only class Blazor directly interacts with3
Configuration options (like rainbow mode) are public fields that can be bound to inputs on Razor pages
This class can return rendering instructions as JSON. This is just an array of lines, each with a color and an array of X/Y points.
This is the HTML that holds the HTML canvas. By using CSS to make the canvas background black I don’t have to concern myself with manually drawing a black background on every new frame.
Note that I’m using Bootstrap 5 (not the Bootstrap 4 that currently comes with new Blazor projects), so the classes may be slightly different.
The only goals here are to start the JavaScript render loop and provide a method JavaScript can call to get line data in JSON format. Notice the method called by JavaScript accepts width and height arguments so the data model can properly adjust to canvases that change size as the browser window changes dimensions.
This file contains JavaScript to start the renderer (an infinite loop) and request line data as JSON from a C# function in each render.
Notice that the aspect ratio of the canvas is maintained by setting its with to be a fraction of its height. The canvas width and height are passed as arguments to the C# function which updates the model.
<script>
function renderJS() {
// resize the canvas to fit its parent (resizing clears the canvas too)
var holder =document.getElementById('myCanvasHolder');
var canvas =document.getElementById('myCanvas');
canvas.width = holder.clientWidth;
canvas.height = canvas.width * .6;
// tell C# about the latest dimensions, advance the model, and parse the new data
var dataString =window.theInstance.invokeMethod('UpdateModel', canvas.width, canvas.height);
var polys = JSON.parse(dataString);
// render each polygon
var lineCount =0;
var ctx =document.getElementById('myCanvas').getContext('2d');
ctx.lineWidth =2;
for (var i =0; i < polys.length; i++) {
var poly = polys[i];
var color = poly.shift();
ctx.beginPath();
for (var j =0; j < poly.length; j++) {
x =parseFloat(poly[j][0]);
y =parseFloat(poly[j][1]);
if (j ==0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.strokeStyle = color;
ctx.closePath();
ctx.stroke();
lineCount +=1;
}
window.requestAnimationFrame(renderJS);
}
window.initRenderJS = (instance) => {
window.theInstance = instance;
window.requestAnimationFrame(renderJS);
};
</script>
The JSON that C# passes to the JavaScript renderer is just an array of lines, each with a color and an array of X/Y points. This is an example JSON packet to render a single frame:
There are wrappers to let you draw on a HTML canvas from .NET, allowing you to write your render function in C# instead of JavaScript. It is a bit of added complexity (requires a package) and is a bit slower (additional JavaScript interop calls), so I did not choose to use it for this project.
Vanilla JavaScript will probably always be faster at rendering graphics in the browser than Blazor. If your application requires fast graphics, you’ll have to write all your graphics model logic and render system in JavaScript. The great advantage of Blazor is that existing C# code that has already been developed (and extensively tested) can be used in the browser. At the time of writing, Speed is not Blazor’s strong suit.
This system starts to choke when the model represents more than a few lines. The bottleneck seems to be encoding all the data as JSON on the C# side.
I tried using StringBuilder everywhere and also using int instead of float, but it did not produce large improvements.
I also tired using the Blazor.Extensions.Canvas package so I could make all the rendering calls from C# (eliminating the conversion to JSON step). Because this required so many individual interop calls (even with batching), it was about twice as slow as the JS renderer implemented in the code above.
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.
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,
A BoidField has a width, height, and array of Boids
Each Boid has a position, direction, and velocity
Each Boid can advance itself by considering the positions of all the other boids:
Rule 1: boids steer toward the center of mass of nearby boids
Rule 2: boids adjust direction to match nearby boids
Rule 3: boids adjust speed to match a match a target
Rule 4: boids steer away from very close boids
Rule 5: boids steer away from boids marked as predators
Rule 6: boids are repelled by the edge of the window
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.
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.
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:
A canvas is created inside a div, each with a unique ID so they can be referenced from JavaScript.
A IJSRuntime is injected so JS functions can be called from C#. Specifically, the initRenderJS is called when the component is first rendered.
A NavigationManager is injected to let us easily work with query strings (Note that QueryHelpers requires the Microsoft.AspNetCore.WebUtilities NuGet package).
A boidField graphics model is created as a private field and initialized OnAfterRender using query strings to customize the initializer parameters.
The UpdateModel() method is decorated with JSInvokable so it can be called from JavaScript. It accepts a width and height (which may change if the browser size changes) and can resize the boidField as needed. It advances the model then converts the positions and directions of all boids to JSON and returns it as a string.
@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;
protectedoverrideasync Task OnAfterRenderAsync(bool firstRender)
{
// call the initialization JavaScript functionawait JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
awaitbase.OnInitializedAsync();
// use a query string to customize the number of boidsint boidCount = 75;
var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("boids", outvar boidCountString))
if (int.TryParse(boidCountString, outint parsedBoidCount))
boidCount = parsedBoidCount;
// use a query string to customize the number of predatorsint predatorCount = 3;
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("predators", outvar predatorCountString))
if (int.TryParse(predatorCountString, outint parsedPredatorCount))
predatorCount = parsedPredatorCount;
// create the model using the custom settings boidField = new Models.Field(800, 600, boidCount, predatorCount);
}
[JSInvokable]publicstring UpdateModel(double width, double height)
{
if (boidField isnull)
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();
}
}
renderJS() - this function calls C# to update the model and request the latest data, then calls itself to create an infinite render loop. Each time it executes it:
resizes the canvas to fit the div it’s inside of
calls the C# UpdateModel() method (passing in the latest canvas size)
parses the JSON the C# method returns to obtain an array of boid details
clears the canvas by filling it with a blue color
renders each boid (by translating and rotating the canvas, not the boid)
requests itself be rendered again so rendering continues infinitely
initRenderJS - this function is called from Blazor so the running instance can be referenced in the future. It also starts the infinite render loop.
<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>
Draw Animated Graphics in the Browser with Blazor WebAssembly uses a render loop written in C# (instead of JavaScript) using Blazor.Extensions.Canvas. It’s a bit slower as a result, but for simple models it has the advantage of not having to write a JavaScript rendering method.
Boids in C# - a Windows application that runs much faster than this Blazor app. It has controls to customize flocking parameters. This was the source of the C# model I used for this Blazor app.
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.