carlfranklin / BlazorCanvas

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Blazor Canvas

The Canvas is built in to all modern browsers that support HTML5 or higher. It's for drawing graphics and doing graphical animations. If you're new to the Canvas, check out the W3Schools Canvas Tutorial, and for fun check out 25 Ridiculously Impressive HTML5 Canvas Experiments, a collection of Canvas demos done in JavaScript.

This demo builds on from Scott Harden's EXCELLENT blog post, Draw Animated Graphics in the Browser with Blazor WebAssembly, which uses the OSS Blazor.Extensions.Canvas component to draw on the canvas, but also includes JavaScript to help with animations, illustrating the real power of the HTML Canvas element. Click here for a tutorial on the HTML Canvas element.

I took it one step beyond by encapsulating the JavaScript required to do animations in a Razor Class Library, which I call AvnCanvasHelper. At some point, I will create a repo just for AvnCanvasHelper because it can be used to do any kind of Canvas animation.

AvnCanvasHelper

To use AvnCanvasHelper, place the BECanvas inside it as child content like so:

<CanvasHelper 
    @ref="CanvasHelper"
    CanvasResized="CanvasResized" 
    RenderFrame="RenderFrame"
    MouseDown="MouseDown"
    MouseUp="MouseUp"
    MouseMove="MouseMove">

        <BECanvas Width="600" Height="400" @ref="CanvasReference"></BECanvas>

</CanvasHelper>

Then, you'll need to hold references to the AvnCanvasHelper as well as the Context and the Canvas itself:

private Canvas2DContext Ctx;
private BECanvasComponent CanvasReference;
private CanvasHelper CanvasHelper;

Create your canvas reference in OnAfterRenderAsync and initialize the AvnCanvasHelper

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        // Create the canvas and context
        Ctx = await CanvasReference.CreateCanvas2DAsync();
        // Initialize the helper
        await CanvasHelper.Initialize();
    }
}

Now you can handle the RenderFrame event to draw the next frame. Here's an example from the demo:

public async Task RenderFrame(double fps)
{
    // update the Frames Per Second measurement
    FPS = fps;

    // The following code is adapted from Scott Harden's EXCELLENT blog post, 
    // "Draw Animated Graphics in the Browser with Blazor WebAssembly"
    // https://swharden.com/blog/2021-01-07-blazor-canvas-animated-graphics/

    if (BallField.Balls.Count == 0)
        BallField.AddRandomBalls(50);

    BallField.StepForward();

    await this.Ctx.BeginBatchAsync();

    await this.Ctx.ClearRectAsync(0, 0, BallField.Width, BallField.Height);
    await this.Ctx.SetFillStyleAsync("#003366");
    await this.Ctx.FillRectAsync(0, 0, BallField.Width, BallField.Height);

    await this.Ctx.SetFontAsync("26px Segoe UI");
    await this.Ctx.SetFillStyleAsync("#FFFFFF");
    await this.Ctx.FillTextAsync("Blazor Canvas", 10, 30);

    await this.Ctx.SetFontAsync("16px consolas");
    await this.Ctx.FillTextAsync($"FPS: {fps:0.000}", 10, 50);

    await this.Ctx.SetStrokeStyleAsync("#FFFFFF");
    foreach (var ball in BallField.Balls)
    {
        await this.Ctx.BeginPathAsync();
        await this.Ctx.ArcAsync(ball.X, ball.Y, ball.R, 0, 2 * Math.PI, false);
        await this.Ctx.SetFillStyleAsync(ball.Color);
        await this.Ctx.FillAsync();
        await this.Ctx.StrokeAsync();
    }

    await this.Ctx.EndBatchAsync();
}

Methods

AvnCanvasHelper exposes the following methods;

Method Description
Initialize Call this in your Blazor app's OnAfterRenderAsync method when firstRender is true

Events

AvnCanvasHelper exposes the following events:

Event Description
CanvasResized When the browser, and therefore the canvas, is resized
RenderFrame When a frame is ready to be drawn
MouseDown The user clicked a mouse button
MouseUp The user released a mouse button
MouseMove The user moved the mouse

CanvasMouseArgs

The DOM holds a lot of information for the mouse. I've extracted the basic properties into CanvasMouseArgs which is passed into the MouseDown, MouseUp, and MouseMove events. For more information check out https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent

public class CanvasMouseArgs
{
    public int ScreenX { get; set; }
    public int ScreenY { get; set; }
    public int ClientX { get; set; }
    public int ClientY { get; set; }
    public int MovementX { get; set; }
    public int MovementY { get; set; }
    public int OffsetX { get; set; }
    public int OffsetY { get; set; }
    public bool AltKey { get; set; }
    public bool CtrlKey { get; set; }
    public bool Bubbles { get; set; }
    public int Buttons { get; set; }
    public int Button { get; set; }
}

BlazorCanvas Demo

The demo is a Blazor Server application that replicates the code Scott Harden's EXCELLENT blog post, Draw Animated Graphics in the Browser with Blazor WebAssembly, which uses the OSS Blazor.Extensions.Canvas component to draw on the canvas, but using a helper component so we never have to touch JavaScript.

Here are the steps to reproduce the demo:

Create a Global WebAssembly Blazor Web App in Visual Studio called BlazorCanvas

Add the AvnCanvasHelper project from this repo to the solution (and a project reference from both projects), or add the AvnCanvasHelper NuGet package to both projects.

The rest of the steps are all in the BlazorCanvas project.

Add the following to the .csproj file:

<ItemGroup>
    <PackageReference Include="Blazor.Extensions.Canvas" Version="1.1.1" />
</ItemGroup>

Add the following to App.Razor below the existing <script> tag:

<script src="_content/Blazor.Extensions.Canvas/blazor.extensions.canvas.js"></script>

Add the following to _Imports.razor in both projects :

@using Blazor.Extensions
@using Blazor.Extensions.Canvas
@using Blazor.Extensions.Canvas.Canvas2D
@using Blazor.Extensions.Canvas.WebGL
@using AvnCanvasHelper
@using System.Drawing

Replace Shared/MainLayout.razor with the following:

@inherits LayoutComponentBase
@Body

Add a Models folder to the client project, and add these two classes:

Ball.cs:

/*
 * Adapted from Scott Harden's EXCELLENT blog post, 
 * "Draw Animated Graphics in the Browser with Blazor WebAssembly"
 * https://swharden.com/blog/2021-01-07-blazor-canvas-animated-graphics/
 */
public class Ball
{
    public double X { get; private set; }
    public double Y { get; private set; }
    public double XVel { get; private set; }
    public double YVel { get; private set; }
    public double R { get; private set; }
    public string Color { get; private set; }

    public Ball(double x, double y, double xVel, double yVel, 
                double radius, string color)
    {
        (X, Y, XVel, YVel, R, Color) = (x, y, xVel, yVel, radius, color);
    }

    public void StepForward(double width, double height)
    {
        X += XVel;
        Y += YVel;
        if (X < 0 || X > width)
            XVel *= -1;
        if (Y < 0 || Y > height)
            YVel *= -1;

        if (X < 0)
            X += 0 - X;
        else if (X > width)
            X -= X - width;

        if (Y < 0)
            Y += 0 - Y;
        if (Y > height)
            Y -= Y - height;
    }
}

Field.cs:

/*
 * Adapted from Scott Harden's EXCELLENT blog post, 
 * "Draw Animated Graphics in the Browser with Blazor WebAssembly"
 * https://swharden.com/blog/2021-01-07-blazor-canvas-animated-graphics/
 */
public class Field
{
    public readonly List<Ball> Balls = new List<Ball>();
    public double Width { get; private set; }
    public double Height { get; private set; }

    public void Resize(double width, double height) =>
        (Width, Height) = (width, height);

    public void StepForward()
    {
        foreach (Ball ball in Balls)
            ball.StepForward(Width, Height);
    }

    private double RandomVelocity(Random rand, double min, double max)
    {
        double v = min + (max - min) * rand.NextDouble();
        if (rand.NextDouble() > .5)
            v *= -1;
        return v;
    }

    public void AddRandomBalls(int count = 10)
    {
        double minSpeed = .5;
        double maxSpeed = 5;
        double radius = 10;
        Random rand = new Random();

        for (int i = 0; i < count; i++)
        {
            Balls.Add(
                new Ball(
                    x: Width * rand.NextDouble(),
                    y: Height * rand.NextDouble(),
                    xVel: RandomVelocity(rand, minSpeed, maxSpeed),
                    yVel: RandomVelocity(rand, minSpeed, maxSpeed),
                    radius: radius,
                    color: string.Format("#{0:X6}", rand.Next(0xFFFFFF))
                )
            );
        }
    }
}

Replace Home.razor with the following:

@page "/"

<PageTitle>Index</PageTitle>

<CanvasHelper 
    @ref="CanvasHelper"
    CanvasResized="CanvasResized" 
    RenderFrame="RenderFrame"
    MouseDown="MouseDown"
    MouseUp="MouseUp"
    MouseMove="MouseMove">

        <BECanvas Width="600" Height="400" @ref="CanvasReference"></BECanvas>

</CanvasHelper>

@code {

    private Size Size = new Size();
    private double FPS;
    private Canvas2DContext Ctx;
    private BECanvasComponent CanvasReference;
    private CanvasHelper CanvasHelper;
    private Field BallField = new Field();

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Create the canvas and context
            Ctx = await CanvasReference.CreateCanvas2DAsync();
            // Initialize the helper
            await CanvasHelper.Initialize();
        }
    }

    /// <summary>
    /// Called by CanvasHelper whenever we are ready to render a frame
    /// </summary>
    /// <param name="fps"></param>
    /// <returns></returns>
    public async Task RenderFrame(double fps)
    {
        // update the Frames Per Second measurement
        FPS = fps;

        // The following code is adapted from Scott Harden's EXCELLENT blog post, 
        // "Draw Animated Graphics in the Browser with Blazor WebAssembly"
        // https://swharden.com/blog/2021-01-07-blazor-canvas-animated-graphics/

        if (BallField.Balls.Count == 0)
            BallField.AddRandomBalls(50);

        BallField.StepForward();

        await this.Ctx.BeginBatchAsync();

        await this.Ctx.ClearRectAsync(0, 0, BallField.Width, BallField.Height);
        await this.Ctx.SetFillStyleAsync("#003366");
        await this.Ctx.FillRectAsync(0, 0, BallField.Width, BallField.Height);

        await this.Ctx.SetFontAsync("26px Segoe UI");
        await this.Ctx.SetFillStyleAsync("#FFFFFF");
        await this.Ctx.FillTextAsync("Blazor Canvas", 10, 30);

        await this.Ctx.SetFontAsync("16px consolas");
        await this.Ctx.FillTextAsync($"FPS: {fps:0.000}", 10, 50);

        await this.Ctx.SetStrokeStyleAsync("#FFFFFF");
        foreach (var ball in BallField.Balls)
        {
            await this.Ctx.BeginPathAsync();
            await this.Ctx.ArcAsync(ball.X, ball.Y, ball.R, 0, 2 * Math.PI, false);
            await this.Ctx.SetFillStyleAsync(ball.Color);
            await this.Ctx.FillAsync();
            await this.Ctx.StrokeAsync();
        }

        await this.Ctx.EndBatchAsync();
    }

    /// <summary>
    /// Called by CanvasHelper whenever the browser is resized.
    /// </summary>
    /// <param name="size"></param>
    public void CanvasResized(Size size)
    {
        Size = size;
        BallField.Resize(size.Width, size.Height);
    }
    
    // Handle mouse down events
    void MouseDown(CanvasMouseArgs args)
    {
        
    }

    // Handle mouse up events
    void MouseUp(CanvasMouseArgs args)
    {
        
    }

    // Handle mouse move events
    void MouseMove(CanvasMouseArgs args)
    {
        
    }
}

Run the app!

Animation

About


Languages

Language:C# 36.9%Language:HTML 34.1%Language:CSS 17.3%Language:JavaScript 11.7%