Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3.0.0-preview4, batch rendering, async, server-side #24

Merged
merged 11 commits into from
Apr 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ dotnet_naming_style.first_upper.capitalization = first_word_upper
# prefix_interface_interface_with_i - Interfaces must be PascalCase and the first character of an interface must be an 'I'
dotnet_naming_style.prefix_interface_interface_with_i.capitalization = pascal_case
dotnet_naming_style.prefix_interface_interface_with_i.required_prefix = I
# underscore_camel_case - Define the _camelCase style
dotnet_naming_style.underscore_camel_case.capitalization = camel_case
dotnet_naming_style.underscore_camel_case.required_prefix = _

# Naming Rules
# Constant fields must be PascalCase
Expand All @@ -109,18 +112,18 @@ dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pasca
dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = warning
dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields
dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case
# Private readonly fields must be camelCase
# Private readonly fields must be _camelCase
dotnet_naming_rule.private_readonly_fields_must_be_camel_case.severity = warning
dotnet_naming_rule.private_readonly_fields_must_be_camel_case.symbols = private_readonly_fields
dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = camel_case
dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = underscore_camel_case
# Public and internal fields must be PascalCase
dotnet_naming_rule.public_internal_fields_must_be_pascal_case.severity = warning
dotnet_naming_rule.public_internal_fields_must_be_pascal_case.symbols = public_internal_fields
dotnet_naming_rule.public_internal_fields_must_be_pascal_case.style = pascal_case
# Private and protected fields must be camelCase
# Private and protected fields must be _camelCase
dotnet_naming_rule.private_protected_fields_must_be_camel_case.severity = warning
dotnet_naming_rule.private_protected_fields_must_be_camel_case.symbols = private_protected_fields
dotnet_naming_rule.private_protected_fields_must_be_camel_case.style = camel_case
dotnet_naming_rule.private_protected_fields_must_be_camel_case.style = underscore_camel_case
# Public members must be capitalized
dotnet_naming_rule.public_members_must_be_capitalized.severity = warning
dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols
Expand Down
4 changes: 4 additions & 0 deletions .vsts-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ variables:
buildConfiguration: 'Release'

steps:
- task: DotNetCoreInstaller@0
inputs:
packageType: 'sdk'
version: '3.0.100-preview4-011223'
- task: Npm@1
inputs:
command: 'install'
Expand Down
5 changes: 5 additions & 0 deletions .vsts-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ variables:
buildConfiguration: 'Release'

steps:
- task: DotNetCoreInstaller@0
inputs:
packageType: 'sdk'
version: '3.0.100-preview4-011223'

- task: Npm@1
inputs:
command: 'install'
Expand Down
21 changes: 18 additions & 3 deletions Canvas.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
# Visual Studio Version 16
VisualStudioVersion = 16.0.28711.60
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B286BCBD-DAD8-4DE7-9334-3DE18DF233AF}"
EndProject
Expand All @@ -13,12 +13,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazor.Extensions.Canvas.Test", "test\Blazor.Extensions.Canvas.Test\Blazor.Extensions.Canvas.Test.csproj", "{C4BB6A39-28E6-454D-8679-92562CEAD0A9}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazor.Extensions.Canvas.Test.ClientSide", "test\Blazor.Extensions.Canvas.Test.ClientSide\Blazor.Extensions.Canvas.Test.ClientSide.csproj", "{C4BB6A39-28E6-454D-8679-92562CEAD0A9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazor.Extensions.Canvas.JS", "src\Blazor.Extensions.Canvas.JS\Blazor.Extensions.Canvas.JS.csproj", "{1C49147F-7C73-4962-A71C-6A193970D058}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazor.Extensions.Canvas", "src\Blazor.Extensions.Canvas\Blazor.Extensions.Canvas.csproj", "{CB6A1BDA-7768-4A0C-A802-D3AE0C19C120}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazor.Extensions.Canvas.Test.ServerSide", "test\Blazor.Extensions.Canvas.Test.ServerSide\Blazor.Extensions.Canvas.Test.ServerSide.csproj", "{D2242105-73D1-4F44-9A80-13D51B6B35FE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -65,6 +67,18 @@ Global
{CB6A1BDA-7768-4A0C-A802-D3AE0C19C120}.Release|x64.Build.0 = Release|Any CPU
{CB6A1BDA-7768-4A0C-A802-D3AE0C19C120}.Release|x86.ActiveCfg = Release|Any CPU
{CB6A1BDA-7768-4A0C-A802-D3AE0C19C120}.Release|x86.Build.0 = Release|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Debug|x64.ActiveCfg = Debug|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Debug|x64.Build.0 = Debug|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Debug|x86.ActiveCfg = Debug|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Debug|x86.Build.0 = Debug|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Release|Any CPU.Build.0 = Release|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Release|x64.ActiveCfg = Release|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Release|x64.Build.0 = Release|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Release|x86.ActiveCfg = Release|Any CPU
{D2242105-73D1-4F44-9A80-13D51B6B35FE}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -73,6 +87,7 @@ Global
{C4BB6A39-28E6-454D-8679-92562CEAD0A9} = {20DAA632-F8AD-4C5F-9E5F-FC82B7CB56A7}
{1C49147F-7C73-4962-A71C-6A193970D058} = {B286BCBD-DAD8-4DE7-9334-3DE18DF233AF}
{CB6A1BDA-7768-4A0C-A802-D3AE0C19C120} = {B286BCBD-DAD8-4DE7-9334-3DE18DF233AF}
{D2242105-73D1-4F44-9A80-13D51B6B35FE} = {20DAA632-F8AD-4C5F-9E5F-FC82B7CB56A7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A97C0A4B-E309-4485-BB76-898B37BFBFFF}
Expand Down
80 changes: 64 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ Blazor Extensions are a set of packages with the goal of adding useful things to

This package wraps [HTML5 Canvas](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas) APIs.

> **NOTE**: Only Canvas 2d is supported. WebGL will come later (contributions are welcome!).
Both Canvas 2D and WebGL are supported.

Both client and server-side scenarios using either Blazor or Razor Components are supported.

**NOTE** Currently targets the v3.0.0-preview4 version of Blazor/Razor Components, which has a limitation regarding static files included in component libraries (aspnet/AspNetCore#6349). As a temporary workaround, manually add the `blazor.extensions.canvas.js` file in a `<script>` tag in the `<head>` element of your project website.

# Installation

Expand All @@ -26,8 +30,6 @@ Install-Package Blazor.Extensions.Canvas

## Usage

The following snippet shows how to consume the Canvas API in a Blazor component.

On your `_ViewImports.cshtml` add the `using` and TagHelper entries:

```c#
Expand All @@ -38,33 +40,79 @@ On your `_ViewImports.cshtml` add the `using` and TagHelper entries:
On your .cshtml add a `BECanvas` and make sure you set the `ref` to a field on your component:

```c#
@page "/"
@inherits IndexComponent

<h1>Canvas demo!!!</h1>

<BECanvas ref="@_canvasReference"></BECanvas>
```

On your component C# code (regardless if inline on .cshtml or in a .cs file), from a `BECanvasComponent` reference, create a `Canvas2dContext`, and then use the context methods to draw on the canvas:
### 2D

On your component C# code (regardless if inline on .razor or in a .cs file), from a `BECanvasComponent` reference, create a `Canvas2DContext`, and then use the context methods to draw on the canvas:

```c#
private Canvas2dContext _context;
private Canvas2DContext _context;

protected BECanvasComponent _canvasReference;

protected override void OnAfterRender()
protected override async Task OnAfterRenderAsync()
{
this._context = this._canvasReference.CreateCanvas2d();
this._context.FillStyle = "green";
this._context = await this._canvasReference.CreateCanvas2DAsync();
await this._context.SetFillStyleAsync("green");

await this._context.FillRectAsync(10, 100, 100, 100);

await this._context.SetFontAsync("48px serif");
await this._context.StrokeTextAsync("Hello Blazor!!!", 10, 100);
}
```

**NOTE** You cannot call `CreateCanvas2DAsync` in `OnInitAsync`, because the underlying `<canvas>` element is not yet present in the generated markup.

### WebGL

this._context.FillRect(10, 100, 100, 100);
On your component C# code (regardless if inline on .razor or in a .cs file), from a `BECanvasComponent` reference, create a `WebGLContext`, and then use the context methods to draw on the canvas:

```c#
private WebGLContext _context;

protected BECanvasComponent _canvasReference;

this._context.Font = "48px serif";
this._context.StrokeText("Hello Blazor!!!", 10, 100);
protected override async Task OnAfterRenderAsync()
{
this._context = await this._canvasReference.CreateWebGLAsync();

await this._context.ClearColorAsync(0, 0, 0, 1);
await this._context.ClearAsync(BufferBits.COLOR_BUFFER_BIT);
}
```

**NOTE** You cannot call `CreateWebGLAsync` in `OnInitAsync`, because the underlying `<canvas>` element is not yet present in the generated markup.

### Call Batching

All javascript interop are batched as needed to improve performance. In high-performance scenarios this behavior will not have any effect: each call will execute immediately. In low-performance scenarios, consective calls to canvas APIs will be queued. JavaScript interop calls will be made with each batch of queued commands sequentially, to avoid the performance impact of multiple concurrent interop calls.

When using server-side Razor Components, because of the server-side rendering mechanism, only the last drawing operation executed will appear to render on the client, overwriting all previous operations. In the example code above, for example, drawing the triangles would appear to "erase" the black background drawn immediately before, leaving the canvas transparent.

To avoid this issue, all WebGL **drawing** operations should be explicitly preceded and followed by `BeginBatchAsync` and `EndBatchAsync` calls.

For example:

```c#
await this._context.ClearColorAsync(0, 0, 0, 1); // this call does not draw anything, so it does not need to be included in the explicit batch

await this._context.BeginBatchAsync(); // begin the explicit batch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how easy it would be but what if you make this return an IDisposable which calls EndBatchAsync on disposal. The syntax would look something like this:

using (this._context.BeginBatchAsync()) {
  await this._context.ClearAsync(BufferBits.COLOR_BUFFER_BIT);
  await this._context.DrawArraysAsync(Primitive.TRIANGLES, 0, 3);
}

However I'm not sure if this is going to work because of all the async stuff

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like the fact that this paradigm prevents accidentally beginning a batch and forgetting to end it. You're correct that the async "end batch" call complicates things, though. I could probably implement it using either a 'fire-and-forget' task, or a blocking call like the ones that are currently used in the old API wrappers. Either solution has a few major drawbacks, though, and I'm not sure they would be good solutions.

A fire-and-forget async call in a Dispose method would mean that the operation isn't necessarily complete after control exits the using block, and there would be no way to await on it. That would prevent any follow-up code that depends on the operations inside the block being actually finished. Probably not feasible from a usability perspective.

A blocking call avoids that issue, but it has another problem: what if the EndBatchAsync call fails? Dispose isn't supposed to fail, and using statements tend to swallow exceptions in their try block if the finally block also throws. In the worst case the call might block indefinitely, leaving the thread blocked in the using statement's implied finally clause.

The IAsyncDisposable proposal aims to resolve these concerns and some others. If it makes it into c# 8.0 we could take advantage of it. That proposal has been around a while, though, and I haven't seen any official word that it's going to arrive soon.


await this._context.ClearAsync(BufferBits.COLOR_BUFFER_BIT);
await this._context.DrawArraysAsync(Primitive.TRIANGLES, 0, 3);

await this._context.EndBatchAsync(); // execute all currently batched calls
```

It is best to structure your code so that `BeginBatchAsync` and `EndBatchAsync` surround as few calls as possible. That will allow the automatic batching behavior to send calls in the most efficient manner possible, and avoid unnecessary performance impacts.

Methods which return values are never batched. Such methods may be called at any time, *even after calling `BeginBatchAsync`*, without interrupting the batching of other calls.

***NOTE*** The "overwriting" behavior of server-side code is unpredictable, and shouldn't be relied on as a feature. In low-performance situations calls can be batched automatically, even when you don't explicitly use `BeginBatchAsync` and `EndBatchAsync`.

# Contributions and feedback

Please feel free to use the component, open issues, fix bugs or provide feedback.
Expand Down
2 changes: 2 additions & 0 deletions blazor.extensions.canvas.js

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions src/Blazor.Extensions.Canvas.JS/Blazor.Extensions.Canvas.JS.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
Expand All @@ -14,7 +14,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Blazor.Build" Version="0.7.0" />
<WebpackInputs Include="**\*.ts" Exclude="dist\**;node_modules\**" />
</ItemGroup>

Expand All @@ -30,4 +29,16 @@
<EmbeddedResource Include="dist\**\*.js" LogicalName="blazor:js:%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
</Target>

<ItemGroup>
<WebpackOutputs Include="dist\**\*.js" />
</ItemGroup>

<Target Name="CopyDist" AfterTargets="RunWebpack" DependsOnTargets="RunWebpack">
<Copy SourceFiles="@(WebpackOutputs)" DestinationFolder="..\..\test\Blazor.Extensions.Canvas.Test.ServerSide\wwwroot\dist" SkipUnchangedFiles="true" />
</Target>

<Target Name="CopyDistForServerSideTest" AfterTargets="RunWebpack" DependsOnTargets="RunWebpack">
<Copy SourceFiles="@(WebpackOutputs)" DestinationFolder="..\.." SkipUnchangedFiles="true" />
</Target>
</Project>
25 changes: 24 additions & 1 deletion src/Blazor.Extensions.Canvas.JS/src/CanvasContextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class ContextManager {

public setProperty = (canvas: HTMLCanvasElement, property: string, value: any) => {
const context = this.getContext(canvas);
context[property] = this.deserialize(property, value);
this.setPropertyWithContext(context, property, value);
}

public getProperty = (canvas: HTMLCanvasElement, property: string) => {
Expand All @@ -50,9 +50,32 @@ export class ContextManager {

public call = (canvas: HTMLCanvasElement, method: string, args: any) => {
const context = this.getContext(canvas);
return this.callWithContext(context, method, args);
}

public callBatch = (canvas: HTMLCanvasElement, batchedCalls: any[][]) => {
const context = this.getContext(canvas);
for (let i = 0; i < batchedCalls.length; i++) {
let params = batchedCalls[i].slice(2);
if (batchedCalls[i][1]) {
this.callWithContext(context, batchedCalls[i][0], params);
} else {
this.setPropertyWithContext(
context,
batchedCalls[i][0],
Array.isArray(params) && params.length > 0 ? params[0] : null);
}
}
}

private callWithContext = (context: any, method: string, args: any) => {
return this.serialize(this.prototypes[method].apply(context, args != undefined ? args.map((value) => this.deserialize(method, value)) : []));
}

private setPropertyWithContext = (context: any, property: string, value: any) => {
context[property] = this.deserialize(property, value);
}

private getContext = (canvas: HTMLCanvasElement) => {
if (!canvas) throw new Error('Invalid canvas.');

Expand Down
3 changes: 0 additions & 3 deletions src/Blazor.Extensions.Canvas/BECanvas.cshtml

This file was deleted.

3 changes: 3 additions & 0 deletions src/Blazor.Extensions.Canvas/BECanvas.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@inherits BECanvasComponent

<canvas id="@Id" width="@Width" height="@Height" ref="_canvasRef"></canvas>
13 changes: 8 additions & 5 deletions src/Blazor.Extensions.Canvas/BECanvasComponent.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;

namespace Blazor.Extensions
{
public class BECanvasComponent : BlazorComponent
public class BECanvasComponent : ComponentBase
{
[Parameter]
protected long Height { get; set; }
Expand All @@ -13,8 +13,11 @@ public class BECanvasComponent : BlazorComponent
protected long Width { get; set; }

protected readonly string Id = Guid.NewGuid().ToString();
protected ElementRef canvasRef;
protected ElementRef _canvasRef;

internal ElementRef CanvasReference => this.canvasRef;
internal ElementRef CanvasReference => this._canvasRef;

[Inject]
internal IJSRuntime JSRuntime { get; set; }
}
}
9 changes: 7 additions & 2 deletions src/Blazor.Extensions.Canvas/Blazor.Extensions.Canvas.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@
<TargetsForTfmSpecificBuildOutput>$(TargetsForTfmSpecificBuildOutput);IncludeP2POutput</TargetsForTfmSpecificBuildOutput>
</PropertyGroup>

<PropertyGroup>
<LangVersion>Preview</LangVersion>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Blazor.Browser" Version="0.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.Build" Version="0.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Blazor" Version="3.0.0-preview4-19216-03" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.Build" Version="3.0.0-preview4-19216-03" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading