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

Support ASP.NET Blazor Web Assembly Components #1811

Merged
merged 43 commits into from
Oct 11, 2021
Merged
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4497019
Add inital work for the blazor views
mattleibow Sep 19, 2021
f18d5cf
Update Blazor and add other parts
mattleibow Sep 20, 2021
cd6fb2b
tweaks and ci fix
mattleibow Sep 20, 2021
075bc9a
update .net
mattleibow Sep 20, 2021
765779d
remove the generated js
mattleibow Sep 20, 2021
87f1606
Use new things
mattleibow Sep 21, 2021
934c16c
add this back
mattleibow Sep 21, 2021
95ebde0
Move to a more sync api
mattleibow Sep 21, 2021
6a057aa
Merge branch 'main' into dev/blazor
mattleibow Sep 23, 2021
71892dd
Update azure-pipelines.yml
mattleibow Sep 28, 2021
1ffd482
Merge branch 'main' into dev/blazor
mattleibow Sep 28, 2021
31fdbfb
Install more SDKs
mattleibow Sep 28, 2021
063d581
Bump .net fx
mattleibow Sep 28, 2021
9d9edb1
numbers!
mattleibow Sep 28, 2021
bd73cfd
Updates
mattleibow Oct 2, 2021
c2b2b8f
temporarily override the version
mattleibow Oct 2, 2021
d5891f4
A few more things
mattleibow Oct 3, 2021
92f5436
try this
mattleibow Oct 3, 2021
aae763c
more samples
mattleibow Oct 3, 2021
a959ed6
things
mattleibow Oct 3, 2021
cbb29f4
versions
mattleibow Oct 3, 2021
ff0849b
this
mattleibow Oct 3, 2021
0b2a5e8
-ErrorAction Continue
mattleibow Oct 3, 2021
467ca15
not that
mattleibow Oct 3, 2021
9446d24
5286358
mattleibow Oct 3, 2021
7fb6858
why?
mattleibow Oct 3, 2021
600110c
this
mattleibow Oct 3, 2021
5542acf
winui
mattleibow Oct 4, 2021
a94c177
Actually include the js files in the package
mattleibow Oct 5, 2021
8bac570
need that
mattleibow Oct 5, 2021
a0caaf6
hmmm
mattleibow Oct 5, 2021
13ee476
edited the wrong file ...
mattleibow Oct 5, 2021
0eec8f0
Unify the element apis
mattleibow Oct 9, 2021
02a7d00
oops
mattleibow Oct 9, 2021
becbebe
Depend on released things
mattleibow Oct 9, 2021
c75dee6
also preview only
mattleibow Oct 9, 2021
d780417
[EditorBrowsable(EditorBrowsableState.Never)]
mattleibow Oct 9, 2021
08b4a8b
use later versions of python
mattleibow Oct 9, 2021
d24354a
Use the old image for now
mattleibow Oct 9, 2021
f10567f
ignore install fails
mattleibow Oct 9, 2021
db4830c
try this
mattleibow Oct 9, 2021
4675733
Remove old items
mattleibow Oct 11, 2021
78c43eb
Add versions
mattleibow Oct 11, 2021
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
Prev Previous commit
Next Next commit
Update Blazor and add other parts
mattleibow committed Sep 20, 2021
commit f18d5cfa0fda8d1bddbe8e30f04f94d40f1d1873
2 changes: 2 additions & 0 deletions VERSIONS.txt
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ Microsoft.WindowsAppSDK.WinUI release 1.0.0-experimental1
Microsoft.WindowsAppSDK.InteractiveExperiences release 1.0.0-experimental1
Microsoft.Maui.Graphics release 6.0.100-preview.7.358
Microsoft.Windows.SDK.NET.Ref release 10.0.18362.15
Microsoft.AspNetCore.Components.Web release 6.0.0-rc.2.21455.6

# additional references used by the tooling
OpenTK.GLControl reference 1.1.2349.61993
@@ -81,6 +82,7 @@ SkiaSharp.Views.WinUI nuget 2.88.0
SkiaSharp.Views.Maui.Core nuget 2.88.0
SkiaSharp.Views.Maui.Controls nuget 2.88.0
SkiaSharp.Views.Maui.Controls.Compatibility nuget 2.88.0
SkiaSharp.Views.Blazor nuget 2.88.0
SkiaSharp.HarfBuzz nuget 2.88.0
SkiaSharp.Vulkan.SharpVk nuget 2.88.0
HarfBuzzSharp nuget 2.8.2
1 change: 1 addition & 0 deletions build.cake
Original file line number Diff line number Diff line change
@@ -92,6 +92,7 @@ var TRACKED_NUGETS = new Dictionary<string, Version> {
{ "SkiaSharp.Views.Maui.Core", new Version (1, 57, 0) },
{ "SkiaSharp.Views.Maui.Controls", new Version (1, 57, 0) },
{ "SkiaSharp.Views.Maui.Controls.Compatibility", new Version (1, 57, 0) },
{ "SkiaSharp.Views.Blazor", new Version (1, 57, 0) },
{ "HarfBuzzSharp", new Version (1, 0, 0) },
{ "HarfBuzzSharp.NativeAssets.Android", new Version (1, 0, 0) },
{ "HarfBuzzSharp.NativeAssets.iOS", new Version (1, 0, 0) },
52 changes: 52 additions & 0 deletions nuget/SkiaSharp.Views.Blazor.nuspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<package>
<metadata>

<!-- package -->
<id>SkiaSharp.Views.Blazor</id>
<title>SkiaSharp for ASP.NET Blazor</title>
<version>1.0.0</version>
<description>
SkiaSharp for ASP.NET Blazor is a set of views that can be used to draw on the screen.
</description>
<summary>
SkiaSharp for ASP.NET Blazor is a set of views that can be used to draw on the screen.
</summary>
<releaseNotes>
Please visit https://go.microsoft.com/fwlink/?linkid=868517 to view the release notes.
</releaseNotes>
<projectUrl>https://go.microsoft.com/fwlink/?linkid=868515</projectUrl>
<iconUrl>https://go.microsoft.com/fwlink/?linkid=2130524</iconUrl>
<tags>ui asp.net graphics blazor cross-platform skiasharp</tags>

<!-- legal -->
<licenseUrl>https://go.microsoft.com/fwlink/?linkid=868514</licenseUrl>
<authors>Microsoft</authors>
<owners>Microsoft</owners>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
<copyright>© Microsoft Corporation. All rights reserved.</copyright>

<dependencies>
<group targetFramework="net6.0">
<dependency id="SkiaSharp" version="1.0.0" />
<dependency id="SkiaSharp.NativeAssets.WebAssembly" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Components.Web" version="1.0.0" />
</group>
</dependencies>

</metadata>
<files>

<!-- SkiaSharp.Views.UWP.dll -->
<file src="lib/net6.0/SkiaSharp.Views.Blazor.dll" />
<file src="lib/net6.0/SkiaSharp.Views.Blazor.pdb" />

<!-- the build bits -->
<file src="build/net6.0/SkiaSharp.Views.Blazor.props" />
<file src="build/net6.0/SkiaSharp.Views.Blazor.props" target="buildTransitive/net6.0/SkiaSharp.Views.Blazor.props" />

<!-- legal -->
<file src="LICENSE.txt" />

</files>
</package>
37 changes: 37 additions & 0 deletions samples/Basic/Blazor/WebAssembly/SkiaSharpSample.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31717.149
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharpSample", "SkiaSharpSample\SkiaSharpSample.csproj", "{DB5BC6AE-C110-4CA0-9E1E-0328E29989EB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Views.Blazor", "..\..\..\..\source\SkiaSharp.Views.Blazor\SkiaSharp.Views.Blazor\SkiaSharp.Views.Blazor.csproj", "{7BDDB4FB-B8A4-42C3-AA6B-C97AF2877622}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp", "..\..\..\..\binding\SkiaSharp\SkiaSharp.csproj", "{8073311E-E158-4608-8479-512B711C0812}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DB5BC6AE-C110-4CA0-9E1E-0328E29989EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DB5BC6AE-C110-4CA0-9E1E-0328E29989EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DB5BC6AE-C110-4CA0-9E1E-0328E29989EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB5BC6AE-C110-4CA0-9E1E-0328E29989EB}.Release|Any CPU.Build.0 = Release|Any CPU
{7BDDB4FB-B8A4-42C3-AA6B-C97AF2877622}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7BDDB4FB-B8A4-42C3-AA6B-C97AF2877622}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7BDDB4FB-B8A4-42C3-AA6B-C97AF2877622}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7BDDB4FB-B8A4-42C3-AA6B-C97AF2877622}.Release|Any CPU.Build.0 = Release|Any CPU
{8073311E-E158-4608-8479-512B711C0812}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8073311E-E158-4608-8479-512B711C0812}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8073311E-E158-4608-8479-512B711C0812}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8073311E-E158-4608-8479-512B711C0812}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {66F81B5B-452E-48B0-A360-017D6840BBD2}
EndGlobalSection
EndGlobal
10 changes: 10 additions & 0 deletions samples/Basic/Blazor/WebAssembly/SkiaSharpSample/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
99 changes: 99 additions & 0 deletions samples/Basic/Blazor/WebAssembly/SkiaSharpSample/Pages/GPU.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
@page "/gpu"

<h1>GPU (WebGL) Canvas</h1>

<p>The canvas below is using WebGL. See the great FPS!</p>

<div class="container">
<div class="row">
<div class="col border rounded p-2 canvas-container">

<SKGLView OnPaintSurface="OnPaintSurface" EnableRenderLoop="true" />

</div>
</div>
</div>

@code {
int tickIndex = 0;
int tickSum = 0;
int[] tickList = new int[100];
int lastTick = Math.Abs(Environment.TickCount);

void OnPaintSurface(SKPaintGLSurfaceEventArgs e)
{
// the the canvas and properties
var canvas = e.Surface.Canvas;

// make sure the canvas is blank
canvas.Clear(SKColors.White);

using var paint = new SKPaint
{
IsAntialias = true,
StrokeWidth = 10f,
StrokeCap = SKStrokeCap.Round,
TextAlign = SKTextAlign.Center,
TextSize = 24,
};

var surfaceSize = e.BackendRenderTarget.Size;
var clockSize = Math.Min(surfaceSize.Width, surfaceSize.Height) * 0.4f;
var center = new SKPoint(surfaceSize.Width / 2f, surfaceSize.Height / 2f);
var now = DateTime.Now;
var fps = CalcAverageTick();

// draw the fps counter
canvas.DrawText($"{fps:0.00}fps", surfaceSize.Width / 2, surfaceSize.Height - 10f, paint);

// draw the clock
canvas.RotateDegrees(-90f, center.X, center.Y);

// hours
paint.StrokeWidth = 9f;
canvas.Save();
canvas.Translate(center);
canvas.RotateDegrees(360f * (now.Hour / 12f));
canvas.DrawLine(0, 0, clockSize * 0.4f, 0, paint);
canvas.Restore();

// minutes
paint.StrokeWidth = 6f;
canvas.Save();
canvas.Translate(center);
canvas.RotateDegrees(360f * (now.Minute / 60f));
canvas.DrawLine(0, 0, clockSize * 0.6f, 0, paint);
canvas.Restore();

// seconds
paint.StrokeWidth = 3f;
canvas.Save();
canvas.Translate(center);
canvas.RotateDegrees(360f * ((now.Second * 1000f + now.Millisecond) / 1000f / 60f));
canvas.DrawLine(0, 0, clockSize * 0.8f, 0, paint);
canvas.Restore();

// center
canvas.DrawCircle(center, 20f, paint);

// border
paint.Style = SKPaintStyle.Stroke;
canvas.DrawCircle(center, clockSize * 0.9f, paint);
}

double CalcAverageTick()
{
var newTick = Math.Abs(Environment.TickCount);
var delta = Math.Max(newTick, lastTick) - Math.Min(newTick, lastTick);
lastTick = newTick;

tickSum -= tickList[tickIndex];
tickSum += delta;
tickList[tickIndex] = delta;

if (++tickIndex == tickList.Length)
tickIndex = 0;

return ((double)tickSum / tickList.Length);
}
}
73 changes: 73 additions & 0 deletions samples/Basic/Blazor/WebAssembly/SkiaSharpSample/Pages/Index.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
@page "/"

<h1>Raster (Bitmap) Canvas</h1>

<p>The canvas below is using pixels in memory. Click and drag to move the text.</p>

<div class="container">
<div class="row">
<div class="col border rounded p-2 canvas-container">

<SKCanvasView
@ref="skiaView" OnPaintSurface="OnPaintSurface" IgnorePixelScaling="true"
@onpointerdown="OnPointerDown"
@onpointermove="OnPointerMove"
@onpointerup="OnPointerUp" />

</div>
</div>
</div>

@code {
SKCanvasView skiaView = null!;
SKPoint? touchLocation;
[Inject] IJSRuntime JS { get; set; } = null!;

void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
// the the canvas and properties
var canvas = e.Surface.Canvas;

// make sure the canvas is blank
canvas.Clear(SKColors.White);

// decide what the text looks like
using var paint = new SKPaint
{
Color = SKColors.Black,
IsAntialias = true,
Style = SKPaintStyle.Fill,
TextAlign = SKTextAlign.Center,
TextSize = 24
};

// adjust the location based on the pointer
var coord = (touchLocation is SKPoint loc)
? new SKPoint(loc.X, loc.Y)
: new SKPoint(e.Info.Width / 2, (e.Info.Height + paint.TextSize) / 2);

// draw some text
canvas.DrawText("SkiaSharp", coord, paint);
}

void OnPointerDown(PointerEventArgs e)
{
touchLocation = new SKPoint((float)e.OffsetX, (float)e.OffsetY);
skiaView.Invalidate();
}

void OnPointerMove(PointerEventArgs e)
{
if (touchLocation == null)
return;

touchLocation = new SKPoint((float)e.OffsetX, (float)e.OffsetY);
skiaView.Invalidate();
}

void OnPointerUp(PointerEventArgs e)
{
touchLocation = null;
skiaView.Invalidate();
}
}
13 changes: 13 additions & 0 deletions samples/Basic/Blazor/WebAssembly/SkiaSharpSample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Net.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using SkiaSharpSample;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.RootComponents.Add<App>("#app");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

await builder.Build().RunAsync();
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:13961",
"sslPort": 44319
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"SkiaSharpSample": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@inherits LayoutComponentBase

<div class="page">
<div class="sidebar">
<NavMenu />
</div>

<div class="main">
<div class="top-row px-4">
<a href="https://github.com/mono/SkiaSharp" target="_blank" class="ml-md-auto">About</a>
</div>

<div class="content px-4">
@Body
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}

.main {
flex: 1;
}

.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}

.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}

.top-row ::deep a, .top-row .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
}

.top-row a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}

@media (max-width: 640.98px) {
.top-row:not(.auth) {
display: none;
}

.top-row.auth {
justify-content: space-between;
}

.top-row a, .top-row .btn-link {
margin-left: 0;
}
}

@media (min-width: 641px) {
.page {
flex-direction: row;
}

.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}

.top-row {
position: sticky;
top: 0;
z-index: 1;
}

.main > div {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">SkiaSharp on Blazor</a>
<button class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Raster Canvas
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="gpu">
<span class="oi oi-plus" aria-hidden="true"></span> GPU Canvas
</NavLink>
</li>
</ul>
</div>

@code {
private bool collapseNavMenu = true;

private string? NavMenuCssClass =>
collapseNavMenu ? "collapse" : null;

private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}

.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}

.navbar-brand {
font-size: 1.1rem;
}

.oi {
width: 2rem;
font-size: 1.1rem;
vertical-align: text-top;
top: -2px;
}

.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}

.nav-item:first-of-type {
padding-top: 1rem;
}

.nav-item:last-of-type {
padding-bottom: 1rem;
}

.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}

.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.25);
color: white;
}

.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}

@media (min-width: 641px) {
.navbar-toggler {
display: none;
}

.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<Import Project="..\..\..\..\..\output\SkiaSharp\nuget\build\wasm\SkiaSharp.props" Condition="Exists('..\..\..\..\..\output\SkiaSharp\nuget\build\wasm\SkiaSharp.props')" />
<Import Project="..\..\..\..\..\output\SkiaSharp.Views.Blazor\nuget\build\net6.0\SkiaSharp.Views.Blazor.props" Condition="Exists('..\..\..\..\..\output\SkiaSharp.Views.Blazor\nuget\build\net6.0\SkiaSharp.Views.Blazor.props')" />

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<!-- In debug, make builds faster by reducing optimizations -->
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<WasmNativeStrip>false</WasmNativeStrip>
<EmccCompileOptimizationFlag>-O1</EmccCompileOptimizationFlag>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0-rc.2.21455.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0-rc.2.21455.6" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\..\binding\SkiaSharp\SkiaSharp.csproj" />
<ProjectReference Include="..\..\..\..\..\source\SkiaSharp.Views.Blazor\SkiaSharp.Views.Blazor\SkiaSharp.Views.Blazor.csproj" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions samples/Basic/Blazor/WebAssembly/SkiaSharpSample/_Imports.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using SkiaSharpSample
@using SkiaSharpSample.Shared
@using SkiaSharp
@using SkiaSharp.Views.Blazor
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');

html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

a, .btn-link {
color: #0366d6;
}

.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}

.content {
padding-top: 1.1rem;
}

.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}

.invalid {
outline: 1px solid red;
}

.validation-message {
color: red;
}

#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}

#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

.canvas-container {
line-height: 1;
}

.canvas-container canvas {
width: 100%;
height: 300px;
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>SkiaSharpSample</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="SkiaSharpSample.styles.css" rel="stylesheet" />
</head>

<body>
<div id="app">Loading...</div>

<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="js/app.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>

</html>
24 changes: 12 additions & 12 deletions scripts/azure-templates-bootstrapper.yml
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ parameters:
condition: succeeded() # whether or not to run this template
shouldPublish: true # whether or not to publish the artifacts
configuration: $(CONFIGURATION) # the build configuration
buildExternals: '' # the build number to download externals from
buildExternals: '5223089' # the build number to download externals from
verbosity: $(VERBOSITY) # the level of verbosity to use when building
docker: '' # the Docker image to build and use
dockerArgs: '' # any additional arguments to pass to docker build
@@ -31,18 +31,18 @@ parameters:
artifactName: '' # the name of the artifact to merge this run into

jobs:
# - ${{ if and(ne(parameters.buildExternals, ''), startsWith(parameters.name, 'native_')) }}:
# - template: azure-templates-download.yml
# parameters:
# name: ${{ parameters.name }}
# displayName: ${{ parameters.displayName }}
# vmImage: ${{ parameters.vmImage }}
# condition: ${{ parameters.condition }}
# postBuildSteps: ${{ parameters.postBuildSteps }}
# buildExternals: ${{ parameters.buildExternals }}
# artifactName: ${{ parameters.artifactName }}
- ${{ if and(ne(parameters.buildExternals, ''), startsWith(parameters.name, 'native_')) }}:
- template: azure-templates-download.yml
parameters:
name: ${{ parameters.name }}
displayName: ${{ parameters.displayName }}
vmImage: ${{ parameters.vmImage }}
condition: ${{ parameters.condition }}
postBuildSteps: ${{ parameters.postBuildSteps }}
buildExternals: ${{ parameters.buildExternals }}
artifactName: ${{ parameters.artifactName }}

# - ${{ if or(eq(parameters.buildExternals, ''), not(startsWith(parameters.name, 'native_'))) }}:
- ${{ if or(eq(parameters.buildExternals, ''), not(startsWith(parameters.name, 'native_'))) }}:
- job: ${{ parameters.name }}
displayName: ${{ parameters.displayName }}
timeoutInMinutes: 120
2 changes: 1 addition & 1 deletion source/SkiaSharp.Build.props
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
<BuildingForNet6 Condition="'$(BuildingForNet6)' == '' and $(IsWindows)">true</BuildingForNet6>
<BuildingForNet6 Condition="'$(BuildingForNet6)' == ''">false</BuildingForNet6>
<MDocDocumentationDirectory>$(MSBuildThisFileDirectory)..\docs\SkiaSharpAPI</MDocDocumentationDirectory>
<LangVersion>8.0</LangVersion>
<LangVersion>9.0</LangVersion>
</PropertyGroup>

<PropertyGroup>
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ namespace SkiaSharp.Views.Blazor.Internal
[EditorBrowsable(EditorBrowsableState.Never)]
public class ActionHelper
{
private Action action;
private readonly Action action;

public ActionHelper(Action action)
{
Original file line number Diff line number Diff line change
@@ -1,72 +1,78 @@
using Microsoft.JSInterop;
using System;
using System.ComponentModel;
using System.Threading.Tasks;

namespace SkiaSharp.Views.Blazor.Internal
{
public static class DpiWatcherInterop
internal class DpiWatcherInterop : JSModuleInterop
{
private const string JsFilename = "./_content/SkiaSharp.Views.Blazor/DpiWatcher.js";
private const string StartSymbol = "DpiWatcher.start";
private const string StopSymbol = "DpiWatcher.stop";
private const string GetDpiSymbol = "DpiWatcher.getDpi";

private static Lazy<Task<IJSObjectReference>> moduleTask = null!;
private static event Action<double>? DpiChangedInternal;
private static DpiWatcherInterop? instance;

public static event Action<double> DpiChanged
{
add
{
if (DpiChangedInternal == null)
Start();

DpiChangedInternal += value;
}
remove
{
DpiChangedInternal -= value;

if (DpiChangedInternal == null)
Stop();
}
}
private event Action<double>? callbacksEvent;
private readonly FloatFloatActionHelper callbackHelper;

private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;

[JSInvokable]
[EditorBrowsable(EditorBrowsableState.Never)]
public static void UpdateDpi(double oldDpi, double newDpi)
public static DpiWatcherInterop Get(IJSRuntime js) =>
instance ??= new DpiWatcherInterop(js);

private DpiWatcherInterop(IJSRuntime js)
: base(js, JsFilename)
{
DpiChangedInternal?.Invoke(newDpi);
callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n));
}

internal static void Init(IJSRuntime js)
protected override Task OnDisposingModuleAsync() =>
StopAsync();

public async Task SubscribeAsync(Action<double> callback)
{
if (moduleTask != null)
return;
var shouldStart = callbacksEvent == null;

moduleTask = new(() => js.InvokeAsync<IJSObjectReference>("import", JsFilename).AsTask());
callbacksEvent += callback;

var dpi = shouldStart
? await StartAsync()
: await GetDpiAsync();

callback(dpi);
}

private static async void Start()
public async Task UnsubscribeAsync(Action<double> callback)
{
var module = await moduleTask.Value;
callbacksEvent -= callback;

await module.InvokeVoidAsync(StartSymbol);
if (callbacksEvent == null)
await StopAsync();
}

private static async void Stop()
private async Task<double> StartAsync()
{
var module = await moduleTask.Value;
if (callbackReference != null)
return await GetDpiAsync();

callbackReference = DotNetObjectReference.Create(callbackHelper);

await module.InvokeVoidAsync(StopSymbol);
return await InvokeAsync<double>(StartSymbol, callbackReference);
}

public static async Task<double> GetDpiAsync()
private async Task StopAsync()
{
var module = await moduleTask.Value;
if (callbackReference == null)
return;

await InvokeAsync(StopSymbol);

return await module.InvokeAsync<double>(GetDpiSymbol);
callbackReference?.Dispose();
callbackReference = null;
}

public Task<double> GetDpiAsync() =>
InvokeAsync<double>(GetDpiSymbol);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.JSInterop;
using System;
using System.ComponentModel;

namespace SkiaSharp.Views.Blazor.Internal
{
[EditorBrowsable(EditorBrowsableState.Never)]
public class FloatFloatActionHelper
{
private readonly Action<float, float> action;

public FloatFloatActionHelper(Action<float, float> action)
{
this.action = action;
}

[JSInvokable]
public void Invoke(float width, float height) => action?.Invoke(width, height);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Microsoft.JSInterop;
using System;
using System.Threading.Tasks;

namespace SkiaSharp.Views.Blazor.Internal
{
internal class JSModuleInterop : IAsyncDisposable
{
private readonly Lazy<Task<IJSObjectReference>> moduleTask;

public JSModuleInterop(IJSRuntime js, string filename)
{
moduleTask = new(() => js.InvokeAsync<IJSObjectReference>("import", filename).AsTask());
}

public async ValueTask DisposeAsync()
{
if (!ModuleImported)
return;

await OnDisposingModuleAsync();

var module = await GetModuleAsync();

await module.DisposeAsync();
}

public bool ModuleImported =>
moduleTask.IsValueCreated;

protected Task<IJSObjectReference> GetModuleAsync() =>
moduleTask.Value;

protected async Task InvokeAsync(string identifier, params object?[]? args)
{
var module = await GetModuleAsync();
await module.InvokeVoidAsync(identifier, args);
}

protected async Task<TValue> InvokeAsync<TValue>(string identifier, params object?[]? args)
{
var module = await GetModuleAsync();
return await module.InvokeAsync<TValue>(identifier, args);
}

protected virtual Task OnDisposingModuleAsync() =>
Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -5,37 +5,20 @@

namespace SkiaSharp.Views.Blazor.Internal
{
internal class SKCanvasViewInterop : IAsyncDisposable
internal class SKCanvasViewInterop : JSModuleInterop
{
private const string JsFilename = "./_content/SkiaSharp.Views.Blazor/SKCanvasView.js";
private const string InvalidateSymbol = "SKCanvasView.invalidateCanvas";
private const string InvalidateSymbol = "SKCanvasView.invalidate";

private readonly Lazy<Task<IJSObjectReference>> moduleTask;
private readonly ElementReference htmlCanvas;

public SKCanvasViewInterop(IJSRuntime js)
public SKCanvasViewInterop(IJSRuntime js, ElementReference element)
: base(js, JsFilename)
{
moduleTask = new(() => js.InvokeAsync<IJSObjectReference>("import", JsFilename).AsTask());
htmlCanvas = element;
}

public async ValueTask DisposeAsync()
{
if (!moduleTask.IsValueCreated)
return;

var module = await moduleTask.Value;

await module.DisposeAsync();
}

public async Task<bool> InvalidateCanvasAsync(ElementReference htmlCanvas, IntPtr intPtr, SKSizeI rawSize)
{
var module = await moduleTask.Value;

return await module.InvokeAsync<bool>(
InvalidateSymbol,
htmlCanvas,
intPtr.ToInt64(),
rawSize.Width, rawSize.Height);
}
public Task<bool> InvalidateCanvasAsync(IntPtr intPtr, SKSizeI rawSize) =>
InvokeAsync<bool>(InvalidateSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
}
}
Original file line number Diff line number Diff line change
@@ -5,49 +5,54 @@

namespace SkiaSharp.Views.Blazor.Internal
{
internal class SKGLViewInterop : IAsyncDisposable
internal class SKGLViewInterop : JSModuleInterop
{
private const string JsFilename = "./_content/SkiaSharp.Views.Blazor/SKGLView.js";
private const string InitSymbol = "SKGLView.init";
private const string DeinitSymbol = "SKGLView.deinit";
private const string RequestAnimationFrameSymbol = "SKGLView.requestAnimationFrame";

private readonly Lazy<Task<IJSObjectReference>> moduleTask;
private readonly ActionHelper actionHelper;
private readonly DotNetObjectReference<ActionHelper> callbackReference;
private readonly ElementReference htmlCanvas;
private readonly string htmlElementId;
private readonly ActionHelper callbackHelper;

public SKGLViewInterop(IJSRuntime js, Action renderFrameCallback)
{
moduleTask = new(() => js.InvokeAsync<IJSObjectReference>("import", JsFilename).AsTask());
actionHelper = new ActionHelper(renderFrameCallback);
callbackReference = DotNetObjectReference.Create(actionHelper);
}
private DotNetObjectReference<ActionHelper>? callbackReference;

public async ValueTask DisposeAsync()
public SKGLViewInterop(IJSRuntime js, ElementReference element, Action renderFrameCallback)
: base(js, JsFilename)
{
callbackReference.Dispose();

if (!moduleTask.IsValueCreated)
return;

var module = await moduleTask.Value;
htmlCanvas = element;
htmlElementId = element.Id;

await module.DisposeAsync();
callbackHelper = new ActionHelper(renderFrameCallback);
}

public async Task<Info> InitAsync(ElementReference htmlCanvas)
protected override Task OnDisposingModuleAsync() =>
DeinitAsync();

public Task<Info> InitAsync()
{
var module = await moduleTask.Value;
if (callbackReference != null)
throw new InvalidOperationException("Unable to initialize the same canvas more than once.");

callbackReference = DotNetObjectReference.Create(callbackHelper);

return await module.InvokeAsync<Info>(InitSymbol, htmlCanvas, callbackReference);
return InvokeAsync<Info>(InitSymbol, htmlCanvas, htmlElementId, callbackReference);
}

public async Task RequestAnimationFrameAsync(ElementReference htmlCanvas, bool enableRenderLoop, int rawWidth, int rawHeight)
public async Task DeinitAsync()
{
var module = await moduleTask.Value;
if (callbackReference == null)
return;

await module.InvokeVoidAsync(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop, rawWidth, rawHeight);
await InvokeAsync(DeinitSymbol, htmlElementId);

callbackReference?.Dispose();
}

public Task RequestAnimationFrameAsync(bool enableRenderLoop, int rawWidth, int rawHeight) =>
InvokeAsync(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop, rawWidth, rawHeight);

public record Info(int ContextId, uint FboId, int Stencils, int Samples, int Depth);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.Threading.Tasks;

namespace SkiaSharp.Views.Blazor.Internal
{
internal class SizeWatcherInterop : JSModuleInterop
{
private const string JsFilename = "./_content/SkiaSharp.Views.Blazor/SizeWatcher.js";
private const string ObserveSymbol = "SizeWatcher.observe";
private const string UnobserveSymbol = "SizeWatcher.unobserve";

private readonly ElementReference htmlElement;
private readonly string htmlElementId;
private readonly FloatFloatActionHelper callbackHelper;

private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;

public SizeWatcherInterop(IJSRuntime js, ElementReference element, Action<SKSize> callback)
: base(js, JsFilename)
{
htmlElement = element;
htmlElementId = element.Id;
callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y)));
}

protected override Task OnDisposingModuleAsync() =>
StopAsync();

public async Task StartAsync()
{
if (callbackReference != null)
return;

callbackReference = DotNetObjectReference.Create(callbackHelper);

await InvokeAsync(ObserveSymbol, htmlElement, htmlElementId, callbackReference);
}

public async Task StopAsync()
{
if (callbackReference == null)
return;

await InvokeAsync(UnobserveSymbol, htmlElementId);

callbackReference?.Dispose();
callbackReference = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<canvas @ref="htmlCanvas" class="skcanvasview" width="@(Width * Dpi)" height="@(Height * Dpi)" />
<canvas @ref="htmlCanvas" @attributes="AdditionalAttributes" />
Original file line number Diff line number Diff line change
@@ -2,30 +2,29 @@
using Microsoft.JSInterop;
using SkiaSharp.Views.Blazor.Internal;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace SkiaSharp.Views.Blazor
{
public partial class SKCanvasView : IDisposable
public partial class SKCanvasView : IAsyncDisposable
{
private SKCanvasViewInterop interop = null!;
private SizeWatcherInterop sizeWatcher = null!;
private DpiWatcherInterop dpiWatcher = null!;
private ElementReference htmlCanvas;

private SKSizeI pixelSize;
private byte[]? pixels;
private GCHandle pixelsHandle;
private bool ignorePixelScaling;
private ElementReference htmlCanvas;
private double dpi;
private SKSize canvasSize;

[Inject]
IJSRuntime JS { get; set; } = null!;

[Parameter]
public double Width { get; set; }

[Parameter]
public double Height { get; set; }

[Parameter]
public bool IgnorePixelScaling
{
@@ -43,59 +42,49 @@ public bool IgnorePixelScaling
[Parameter]
public EventCallback<SKPaintSurfaceEventArgs> OnPaintSurface { get; set; }

public double Dpi { get; private set; }

public SKSize CanvasSize { get; private set; }
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
interop = new SKCanvasViewInterop(JS);
interop = new SKCanvasViewInterop(JS, htmlCanvas);

DpiWatcherInterop.Init(JS);
DpiWatcherInterop.DpiChanged += OnDpiChanged;
sizeWatcher = new SizeWatcherInterop(JS, htmlCanvas, OnSizeChanged);
await sizeWatcher.StartAsync();

OnDpiChanged(await DpiWatcherInterop.GetDpiAsync());
dpiWatcher = DpiWatcherInterop.Get(JS);
await dpiWatcher.SubscribeAsync(OnDpiChanged);
}
}

protected virtual Task InvokeOnPaintSurfaceAsync(SKPaintSurfaceEventArgs e)
{
return OnPaintSurface.InvokeAsync(e);
}

public async void Invalidate()
{
await InvalidateAsync();
}

public async Task InvalidateAsync()
{
if (Width <= 0 || Height <= 0 || Dpi <= 0)
{
CanvasSize = SKSize.Empty;
if (canvasSize.Width <= 0 || canvasSize.Height <= 0 || dpi <= 0)
return;
}

var info = CreateBitmap(out var unscaledSize);
var userVisibleSize = IgnorePixelScaling ? unscaledSize : info.Size;

using (var surface = SKSurface.Create(info, pixelsHandle.AddrOfPinnedObject(), info.RowBytes))
{
CanvasSize = userVisibleSize;

if (IgnorePixelScaling)
{
var canvas = surface.Canvas;
canvas.Scale((float)Dpi);
canvas.Scale((float)dpi);
canvas.Save();
}

await InvokeOnPaintSurfaceAsync(new SKPaintSurfaceEventArgs(surface, info.WithSize(userVisibleSize), info));
await OnPaintSurface.InvokeAsync(new SKPaintSurfaceEventArgs(surface, info.WithSize(userVisibleSize), info));
}

await interop.InvalidateCanvasAsync(htmlCanvas, pixelsHandle.AddrOfPinnedObject(), info.Size);
await interop.InvalidateCanvasAsync(pixelsHandle.AddrOfPinnedObject(), info.Size);
}

private SKImageInfo CreateBitmap(out SKSizeI unscaledSize)
@@ -119,14 +108,14 @@ private SKSizeI CreateSize(out SKSizeI unscaledSize)
{
unscaledSize = SKSizeI.Empty;

var w = Width;
var h = Height;
var w = canvasSize.Width;
var h = canvasSize.Height;

if (!IsPositive(w) || !IsPositive(h))
return SKSizeI.Empty;

unscaledSize = new SKSizeI((int)w, (int)h);
return new SKSizeI((int)(w * Dpi), (int)(h * Dpi));
return new SKSizeI((int)(w * dpi), (int)(h * dpi));

static bool IsPositive(double value)
{
@@ -145,14 +134,25 @@ private void FreeBitmap()

private void OnDpiChanged(double newDpi)
{
Dpi = newDpi;
dpi = newDpi;

Invalidate();
}

public void Dispose()
private void OnSizeChanged(SKSize newSize)
{
DpiWatcherInterop.DpiChanged -= OnDpiChanged;
canvasSize = newSize;

Invalidate();
}

public async ValueTask DisposeAsync()
{
await dpiWatcher.UnsubscribeAsync(OnDpiChanged);
await sizeWatcher.DisposeAsync();
await interop.DisposeAsync();

FreeBitmap();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<canvas @ref="htmlCanvas" class="skglview" width="@(Width * dpi)" height="@(Height * dpi)" />
<canvas @ref="htmlCanvas" @attributes="AdditionalAttributes" />
Original file line number Diff line number Diff line change
@@ -2,15 +2,17 @@
using Microsoft.JSInterop;
using SkiaSharp.Views.Blazor.Internal;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SkiaSharp.Views.Blazor
{
public partial class SKGLView : IAsyncDisposable
{
private SKGLViewInterop interop = null!;
private SKGLViewInterop.Info? jsInfo;

private SizeWatcherInterop sizeWatcher = null!;
private DpiWatcherInterop dpiWatcher = null!;
private SKGLViewInterop.Info jsInfo = null!;
private ElementReference htmlCanvas;

private const int ResourceCacheBytes = 256 * 1024 * 1024; // 256 MB
@@ -20,20 +22,16 @@ public partial class SKGLView : IAsyncDisposable
private GRContext? context;
private GRGlInterface? glInterface;
private GRBackendRenderTarget? renderTarget;
private SKSize renderTargetSize;
private SKSurface? surface;
private SKCanvas? canvas;
private double dpi;
private bool enableRenderLoop;
private double dpi;
private SKSize canvasSize;

[Inject]
IJSRuntime JS { get; set; } = null!;

[Parameter]
public double Width { get; set; }

[Parameter]
public double Height { get; set; }

[Parameter]
public EventCallback<SKPaintGLSurfaceEventArgs> OnPaintSurface { get; set; }

@@ -51,52 +49,41 @@ public bool EnableRenderLoop
}
}

public GRContext? GRContext => context;

public SKSize CanvasSize { get; private set; }
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
interop = new SKGLViewInterop(JS, RenderFrame);
jsInfo = await interop.InitAsync(htmlCanvas);
interop = new SKGLViewInterop(JS, htmlCanvas, OnRenderFrame);
jsInfo = await interop.InitAsync();

DpiWatcherInterop.Init(JS);
DpiWatcherInterop.DpiChanged += OnDpiChanged;
sizeWatcher = new SizeWatcherInterop(JS, htmlCanvas, OnSizeChanged);
await sizeWatcher.StartAsync();

OnDpiChanged(await DpiWatcherInterop.GetDpiAsync());
dpiWatcher = DpiWatcherInterop.Get(JS);
await dpiWatcher.SubscribeAsync(OnDpiChanged);
}
}

protected virtual Task InvokeOnPaintSurfaceAsync(SKPaintGLSurfaceEventArgs e)
{
return OnPaintSurface.InvokeAsync(e);
}

public async void Invalidate()
{
await InvalidateAsync();
}

public async Task InvalidateAsync()
{
if (Width <= 0 || Height <= 0 || dpi <= 0 || jsInfo == null)
{
CanvasSize = SKSize.Empty;
if (canvasSize.Width <= 0 || canvasSize.Height <= 0 || dpi <= 0 || jsInfo == null)
return;
}

await interop.RequestAnimationFrameAsync(htmlCanvas, EnableRenderLoop, (int)(Width * dpi), (int)(Height * dpi));
await interop.RequestAnimationFrameAsync(EnableRenderLoop, (int)(canvasSize.Width * dpi), (int)(canvasSize.Height * dpi));
}

private void RenderFrame()
private void OnRenderFrame()
{
if (Width <= 0 || Height <= 0 || dpi <= 0 || jsInfo == null)
{
CanvasSize = SKSize.Empty;
if (canvasSize.Width <= 0 || canvasSize.Height <= 0 || dpi <= 0 || jsInfo == null)
return;
}

// create the SkiaSharp context
if (context == null)
@@ -109,13 +96,13 @@ private void RenderFrame()
}

// get the new surface size
var newSize = CreateSize(out _);
var newSize = CreateSize();

// manage the drawing surface
if (renderTarget == null || CanvasSize != newSize || !renderTarget.IsValid)
if (renderTarget == null || renderTargetSize != newSize || !renderTarget.IsValid)
{
// create or update the dimensions
CanvasSize = newSize;
renderTargetSize = newSize;

var glInfo = new GRGlFramebufferInfo(jsInfo.FboId, colorType.ToGlSizedFormat());

@@ -139,7 +126,7 @@ private void RenderFrame()
using (new SKAutoCanvasRestore(canvas, true))
{
// start drawing
InvokeOnPaintSurfaceAsync(new SKPaintGLSurfaceEventArgs(surface, renderTarget, surfaceOrigin, colorType));
OnPaintSurface.InvokeAsync(new SKPaintGLSurfaceEventArgs(surface, renderTarget, surfaceOrigin, colorType));
}

// update the control
@@ -154,17 +141,21 @@ private void OnDpiChanged(double newDpi)
Invalidate();
}

private SKSizeI CreateSize(out SKSizeI unscaledSize)
private void OnSizeChanged(SKSize newSize)
{
unscaledSize = SKSizeI.Empty;
canvasSize = newSize;

var w = Width;
var h = Height;
Invalidate();
}

private SKSizeI CreateSize()
{
var w = canvasSize.Width;
var h = canvasSize.Height;

if (!IsPositive(w) || !IsPositive(h))
return SKSizeI.Empty;

unscaledSize = new SKSizeI((int)w, (int)h);
return new SKSizeI((int)(w * dpi), (int)(h * dpi));

static bool IsPositive(double value)
@@ -175,9 +166,9 @@ static bool IsPositive(double value)

public async ValueTask DisposeAsync()
{
DpiWatcherInterop.DpiChanged -= OnDpiChanged;

await dpiWatcher.UnsubscribeAsync(OnDpiChanged);
await sizeWatcher.DisposeAsync();
await interop.DisposeAsync();
}
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>SkiaSharp.Views.Blazor</RootNamespace>
<AssemblyName>SkiaSharp.Views.Blazor</AssemblyName>
<PackagingGroup>SkiaSharp.Views.Blazor</PackagingGroup>
<Nullable>enable</Nullable>
<DefineConstants>$(DefineConstants);__BLAZOR__</DefineConstants>
</PropertyGroup>
@@ -12,6 +15,10 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0-rc.2.21455.6" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.4.2" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\binding\SkiaSharp\SkiaSharp.csproj" />
</ItemGroup>

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<Project>

<PropertyGroup>
<WasmBuildNative Condition="'$(WasmBuildNative)' == ''">true</WasmBuildNative>
</PropertyGroup>

<ItemGroup>
<NativeFileReference Include="$(SkiaSharpStaticLibraryPath)\2.0.23\*.a" />
</ItemGroup>

</Project>
14 changes: 14 additions & 0 deletions source/SkiaSharp.Views.Blazor/SkiaSharp.Views.Blazor/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": false,
"target": "ES2020",
"module": "ES2020",
"outDir": "wwwroot"
},
"exclude": [
"node_modules"
]
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@

export class DpiWatcher {
static getDpi() {
return window.devicePixelRatio;
}

static start() {
static start(callback) {
console.info(`Starting DPI watcher with callback ${callback._id}...`);
DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback;
return DpiWatcher.lastDpi;
}

static stop() {
console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
window.clearInterval(DpiWatcher.timerId);
DpiWatcher.callback = undefined;
}

static update() {
if (!DpiWatcher.callback)
return;
const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi;
DpiWatcher.lastDpi = currentDpi;

if (Math.abs(lastDpi - currentDpi) > 0.001) {
DotNet.invokeMethodAsync('SkiaSharp.Views.Blazor', 'UpdateDpi', lastDpi, currentDpi);
DpiWatcher.callback.invokeMethodAsync('Invoke', lastDpi, currentDpi);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

export class DpiWatcher {
static lastDpi: number;
static timerId: number;
static callback: DotNet.DotNetObjectReference;

public static getDpi() {
return window.devicePixelRatio;
}

public static start(callback: DotNet.DotNetObjectReference): number {
console.info(`Starting DPI watcher with callback ${callback._id}...`);

DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback;

return DpiWatcher.lastDpi;
}

public static stop() {
console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);

window.clearInterval(DpiWatcher.timerId);

DpiWatcher.callback = undefined;
}

static update() {
if (!DpiWatcher.callback)
return;

const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi;
DpiWatcher.lastDpi = currentDpi;

if (Math.abs(lastDpi - currentDpi) > 0.001) {
DpiWatcher.callback.invokeMethodAsync('Invoke', lastDpi, currentDpi);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,23 @@

export class SKCanvasView {
static invalidateCanvas(htmlCanvas, pData, width, height) {
static invalidate(htmlCanvas, pData, width, height) {
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
return false;
}

if (!pData || width <= 0 || width <= 0)
return false;

var ctx = htmlCanvas.getContext('2d');
if (!ctx) {
console.error(`Failed to obtain 2D canvas context.`);
return false;
}

SKCanvasView.resizeCanvas(htmlCanvas, width, height);

// make sure the canvas is scaled correctly for the drawing
htmlCanvas.width = width;
htmlCanvas.height = height;
// set the canvas to be the bytes
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
var imageData = new ImageData(buffer, width, height);
ctx.putImageData(imageData, 0, 0);

return true;
}

static resizeCanvas(htmlCanvas, width, height) {
if (!htmlCanvas)
return;

const newWidth = (width / window.devicePixelRatio) + "px";
const newHeight = (height / window.devicePixelRatio) + 'px';

if (htmlCanvas.style.width != newWidth)
htmlCanvas.style.width = newWidth;
if (htmlCanvas.style.height != newHeight)
htmlCanvas.style.height = newHeight;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

declare let Module: EmscriptenModule;

export class SKCanvasView {
public static invalidate(htmlCanvas: HTMLCanvasElement, pData: number, width: number, height: number) {
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
return false;
}

if (!pData || width <= 0 || width <= 0)
return false;

var ctx = htmlCanvas.getContext('2d');
if (!ctx) {
console.error(`Failed to obtain 2D canvas context.`);
return false;
}

// make sure the canvas is scaled correctly for the drawing
htmlCanvas.width = width;
htmlCanvas.height = height;

// set the canvas to be the bytes
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
var imageData = new ImageData(buffer, width, height);
ctx.putImageData(imageData, 0, 0);

return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,99 +1,105 @@

export class SKGLView {
static init(htmlCanvas, callback) {
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
return null;
}

var ctx = SKGLView.createWebGLContext(htmlCanvas);
if (!ctx || ctx < 0) {
constructor(element, callback) {
this.renderLoopEnabled = false;
this.renderLoopRequest = 0;
this.htmlCanvas = element;
const ctx = this.createWebGLContext(this.htmlCanvas);
if (!ctx) {
console.error(`Failed to create WebGL context: err ${ctx}`);
return null;
}

// make current
GL.makeContextCurrent(ctx);

// read values
var fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
var info = {
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
this.info = {
context: ctx,
fboId: fbo ? fbo.id : 0,
stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
sample: 0,
depth: GLctx.getParameter(GLctx.DEPTH_BITS),
};

htmlCanvas.SKGLView = {
info: info,
renderLoopEnabled: false,
renderLoopRequest: 0,
renderFrameCallback: callback
};

return info;
this.renderFrameCallback = callback;
}
static init(element, elementId, callback) {
var htmlCanvas = element;
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
return null;
}
if (!SKGLView.elements)
SKGLView.elements = new Map();
SKGLView.elements[elementId] = element;
const view = new SKGLView(element, callback);
htmlCanvas.SKGLView = view;
return view.info;
}
static deinit(elementId) {
if (!elementId)
return;
const element = SKGLView.elements[elementId];
SKGLView.elements.delete(elementId);
const htmlCanvas = element;
if (!htmlCanvas || !htmlCanvas.SKGLView)
return;
htmlCanvas.SKGLView.deinit();
htmlCanvas.SKGLView = undefined;
}
static requestAnimationFrame(element, renderLoop, width, height) {
const htmlCanvas = element;
if (!htmlCanvas || !htmlCanvas.SKGLView)
return;
htmlCanvas.SKGLView.requestAnimationFrame(renderLoop, width, height);
}

static requestAnimationFrame(htmlCanvas, renderLoop, width, height) {
if (!htmlCanvas)
static setEnableRenderLoop(element, enable) {
const htmlCanvas = element;
if (!htmlCanvas || !htmlCanvas.SKGLView)
return;

htmlCanvas.SKGLView.setEnableRenderLoop(enable);
}
deinit() {
this.setEnableRenderLoop(false);
}
requestAnimationFrame(renderLoop, width, height) {
// optionally update the render loop
if (renderLoop !== undefined && htmlCanvas.SKGLView.renderLoopEnabled !== renderLoop)
SKGLView.setEnableRenderLoop(htmlCanvas, renderLoop);

if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
this.setEnableRenderLoop(renderLoop);
// make sure the canvas is scaled correctly for the drawing
SKGLView.resizeCanvas(htmlCanvas, width, height);

if (width && height) {
this.htmlCanvas.width = width;
this.htmlCanvas.height = height;
}
// skip because we have a render loop
if (htmlCanvas.SKGLView.renderLoopRequest !== 0)
if (this.renderLoopRequest !== 0)
return;

// add the draw to the next frame
htmlCanvas.SKGLView.renderLoopRequest = window.requestAnimationFrame(() => {
htmlCanvas.SKGLView.renderFrameCallback
this.renderLoopRequest = window.requestAnimationFrame(() => {
// make current
GL.makeContextCurrent(this.info.context);
this.renderFrameCallback
.invokeMethodAsync('Invoke')
.then(function () {
htmlCanvas.SKGLView.renderLoopRequest = 0;

// we may want to draw the next frame
if (htmlCanvas.SKGLView.renderLoopEnabled)
SKGLView.requestAnimationFrame(htmlCanvas);
});
.then(() => {
this.renderLoopRequest = 0;
// we may want to draw the next frame
if (this.renderLoopEnabled)
this.requestAnimationFrame();
});
});
}

static setEnableRenderLoop(htmlCanvas, enable) {
if (!htmlCanvas)
return;

htmlCanvas.SKGLView.renderLoopEnabled = enable;

setEnableRenderLoop(enable) {
this.renderLoopEnabled = enable;
// either start the new frame or cancel the existing one
if (enable) {
SKGLView.requestAnimationFrame(htmlCanvas);
} else if (htmlCanvas.SKGLView.renderLoopRequest !== 0) {
window.cancelAnimationFrame(htmlCanvas.SKGLView.renderLoopRequest);
htmlCanvas.SKGLView.renderLoopRequest = 0;
console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
this.requestAnimationFrame();
}
else if (this.renderLoopRequest !== 0) {
window.cancelAnimationFrame(this.renderLoopRequest);
this.renderLoopRequest = 0;
}
}

static resizeCanvas(htmlCanvas, width, height) {
if (!htmlCanvas)
return;

const newWidth = (width / window.devicePixelRatio) + "px";
const newHeight = (height / window.devicePixelRatio) + 'px';

if (htmlCanvas.style.width != newWidth)
htmlCanvas.style.width = newWidth;
if (htmlCanvas.style.height != newHeight)
htmlCanvas.style.height = newHeight;
}

static createWebGLContext(htmlCanvas) {
var contextAttributes = {
createWebGLContext(htmlCanvas) {
const contextAttributes = {
alpha: 1,
depth: 1,
stencil: 8,
@@ -108,15 +114,13 @@ export class SKGLView {
explicitSwapControl: 0,
renderViaOffscreenBackBuffer: 0,
};

var ctx = GL.createContext(htmlCanvas, contextAttributes);
let ctx = GL.createContext(htmlCanvas, contextAttributes);
if (!ctx && contextAttributes.majorVersion > 1) {
console.warn('Falling back to WebGL 1.0');
contextAttributes.majorVersion = 1;
contextAttributes.minorVersion = 0;
ctx = GL.createContext(htmlCanvas, contextAttributes);
}

return ctx;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@

declare let GL: any;
declare let GLctx: WebGLRenderingContext;

type SKGLViewInfo = {
context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
fboId: number;
stencil: number;
sample: number;
depth: number;
}

type SKGLViewCanvasElement = {
SKGLView: SKGLView
} & HTMLCanvasElement

export class SKGLView {
static elements: Map<string, HTMLCanvasElement>;

htmlCanvas: HTMLCanvasElement;
info: SKGLViewInfo;
renderFrameCallback: DotNet.DotNetObjectReference;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;

public static init(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo {
var htmlCanvas = element as SKGLViewCanvasElement;
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
return null;
}

if (!SKGLView.elements)
SKGLView.elements = new Map<string, HTMLCanvasElement>();
SKGLView.elements[elementId] = element;

const view = new SKGLView(element, callback);

htmlCanvas.SKGLView = view;

return view.info;
}

public static deinit(elementId: string) {
if (!elementId)
return;

const element = SKGLView.elements[elementId];
SKGLView.elements.delete(elementId);

const htmlCanvas = element as SKGLViewCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKGLView)
return;

htmlCanvas.SKGLView.deinit();
htmlCanvas.SKGLView = undefined;
}

public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean, width?: number, height?: number) {
const htmlCanvas = element as SKGLViewCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKGLView)
return;

htmlCanvas.SKGLView.requestAnimationFrame(renderLoop, width, height);
}

public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
const htmlCanvas = element as SKGLViewCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKGLView)
return;

htmlCanvas.SKGLView.setEnableRenderLoop(enable);
}

public constructor(element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) {
this.htmlCanvas = element;

const ctx = this.createWebGLContext(this.htmlCanvas);
if (!ctx) {
console.error(`Failed to create WebGL context: err ${ctx}`);
return null;
}

// make current
GL.makeContextCurrent(ctx);

// read values
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
this.info = {
context: ctx,
fboId: fbo ? fbo.id : 0,
stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
depth: GLctx.getParameter(GLctx.DEPTH_BITS),
};

this.renderFrameCallback = callback;
}

public deinit() {
this.setEnableRenderLoop(false);
}

public requestAnimationFrame(renderLoop?: boolean, width?: number, height?: number) {
// optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
this.setEnableRenderLoop(renderLoop);

// make sure the canvas is scaled correctly for the drawing
if (width && height) {
this.htmlCanvas.width = width;
this.htmlCanvas.height = height;
}

// skip because we have a render loop
if (this.renderLoopRequest !== 0)
return;

// add the draw to the next frame
this.renderLoopRequest = window.requestAnimationFrame(() => {
// make current
GL.makeContextCurrent(this.info.context);

this.renderFrameCallback
.invokeMethodAsync('Invoke')
.then(() => {
this.renderLoopRequest = 0;

// we may want to draw the next frame
if (this.renderLoopEnabled)
this.requestAnimationFrame();
});
});
}

public setEnableRenderLoop(enable: boolean) {
this.renderLoopEnabled = enable;

// either start the new frame or cancel the existing one
if (enable) {
console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
this.requestAnimationFrame();
} else if (this.renderLoopRequest !== 0) {
window.cancelAnimationFrame(this.renderLoopRequest);
this.renderLoopRequest = 0;
}
}

createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
const contextAttributes = {
alpha: 1,
depth: 1,
stencil: 8,
antialias: 1,
premultipliedAlpha: 1,
preserveDrawingBuffer: 0,
preferLowPowerToHighPerformance: 0,
failIfMajorPerformanceCaveat: 0,
majorVersion: 2,
minorVersion: 0,
enableExtensionsByDefault: 1,
explicitSwapControl: 0,
renderViaOffscreenBackBuffer: 0,
};

let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
if (!ctx && contextAttributes.majorVersion > 1) {
console.warn('Falling back to WebGL 1.0');
contextAttributes.majorVersion = 1;
contextAttributes.minorVersion = 0;
ctx = GL.createContext(htmlCanvas, contextAttributes);
}

return ctx;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export class SizeWatcher {
static observe(element, elementId, callback) {
if (!element || !callback)
return;
console.info(`Adding size watcher observation with callback ${callback._id}...`);
SizeWatcher.init();
const watcherElement = element;
watcherElement.SizeWatcher = {
callback: callback
};
SizeWatcher.elements[elementId] = element;
SizeWatcher.observer.observe(element);
SizeWatcher.invokeAsync(element);
}
static unobserve(elementId) {
if (!elementId || !SizeWatcher.observer)
return;
console.info('Removing size watcher observation...');
const element = SizeWatcher.elements[elementId];
SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
}
static init() {
if (SizeWatcher.observer)
return;
console.info('Starting size watcher...');
SizeWatcher.elements = new Map();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (let entry of entries) {
SizeWatcher.invokeAsync(entry.target);
}
});
}
static invokeAsync(element) {
const watcherElement = element;
const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback)
return;
return instance.callback.invokeMethodAsync('Invoke', element.clientWidth, element.clientHeight);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@

type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance;
} & HTMLElement

type SizeWatcherInstance = {
callback: DotNet.DotNetObjectReference;
}

export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;

public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) {
if (!element || !callback)
return;

console.info(`Adding size watcher observation with callback ${callback._id}...`);

SizeWatcher.init();

const watcherElement = element as SizeWatcherElement;
watcherElement.SizeWatcher = {
callback: callback
};

SizeWatcher.elements[elementId] = element;
SizeWatcher.observer.observe(element);

SizeWatcher.invokeAsync(element);
}

public static unobserve(elementId: string) {
if (!elementId || !SizeWatcher.observer)
return;

console.info('Removing size watcher observation...');

const element = SizeWatcher.elements[elementId];

SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
}

static init() {
if (SizeWatcher.observer)
return;

console.info('Starting size watcher...');

SizeWatcher.elements = new Map<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (let entry of entries) {
SizeWatcher.invokeAsync(entry.target);
}
});
}

static invokeAsync(element: Element): Promise<unknown> {
const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher;

if (!instance || !instance.callback)
return;

return instance.callback.invokeMethodAsync('Invoke', element.clientWidth, element.clientHeight);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

declare namespace DotNet {
interface DotNetObjectReference extends DotNet.DotNetObject {
_id: number;
dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Type definitions for non-npm package @blazor/javascript-interop 3.1
// Project: https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interop?view=aspnetcore-3.1
// Definitions by: Piotr Błażejewicz (Peter Blazejewicz) <https://github.com/peterblazejewicz>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// Minimum TypeScript Version: 3.0

// Here be dragons!
// This is community-maintained definition file intended to ease the process of developing
// high quality JavaScript interop code to be used in Blazor application from your C# .Net code.
// Could be removed without a notice in case official definition types ships with Blazor itself.

// tslint:disable:no-unnecessary-generics

declare namespace DotNet {
/**
* Invokes the specified .NET public method synchronously. Not all hosting scenarios support
* synchronous invocation, so if possible use invokeMethodAsync instead.
*
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method.
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
* @returns The result of the operation.
*/
function invokeMethod<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): T;
/**
* Invokes the specified .NET public method asynchronously.
*
* @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method.
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
* @returns A promise representing the result of the operation.
*/
function invokeMethodAsync<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise<T>;
/**
* Represents the .NET instance passed by reference to JavaScript.
*/
interface DotNetObject {
/**
* Invokes the specified .NET instance public method synchronously. Not all hosting scenarios support
* synchronous invocation, so if possible use invokeMethodAsync instead.
*
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
* @returns The result of the operation.
*/
invokeMethod<T>(methodIdentifier: string, ...args: any[]): T;
/**
* Invokes the specified .NET instance public method asynchronously.
*
* @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier.
* @param args Arguments to pass to the method, each of which must be JSON-serializable.
* @returns A promise representing the result of the operation.
*/
invokeMethodAsync<T>(methodIdentifier: string, ...args: any[]): Promise<T>;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
// Type definitions for Emscripten 1.39.16
// Project: https://emscripten.org
// Definitions by: Kensuke Matsuzaki <https://github.com/zakki>
// Periklis Tsirakidis <https://github.com/periklis>
// Bumsik Kim <https://github.com/kbumsik>
// Louis DeScioli <https://github.com/lourd>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.2

/** Other WebAssembly declarations, for compatibility with older versions of Typescript */
declare namespace WebAssembly {
interface Module {}
}

declare namespace Emscripten {
interface FileSystemType {}
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER';

type JSType = 'number' | 'string' | 'array' | 'boolean';
type TypeCompatibleWithC = number | string | any[] | boolean;

type CIntType = 'i8' | 'i16' | 'i32' | 'i64';
type CFloatType = 'float' | 'double';
type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*';
type CType = CIntType | CFloatType | CPointerType;

type WebAssemblyImports = Array<{
name: string;
kind: string;
}>;

type WebAssemblyExports = Array<{
module: string;
name: string;
kind: string;
}>;

interface CCallOpts {
async?: boolean | undefined;
}
}

interface EmscriptenModule {
print(str: string): void;
printErr(str: string): void;
arguments: string[];
environment: Emscripten.EnvironmentType;
preInit: Array<{ (): void }>;
preRun: Array<{ (): void }>;
postRun: Array<{ (): void }>;
onAbort: { (what: any): void };
onRuntimeInitialized: { (): void };
preinitializedWebGLContext: WebGLRenderingContext;
noInitialRun: boolean;
noExitRuntime: boolean;
logReadFiles: boolean;
filePackagePrefixURL: string;
wasmBinary: ArrayBuffer;

destroy(object: object): void;
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
instantiateWasm(
imports: Emscripten.WebAssemblyImports,
successCallback: (module: WebAssembly.Module) => void,
): Emscripten.WebAssemblyExports;
locateFile(url: string, scriptDirectory: string): string;
onCustomMessage(event: MessageEvent): void;

// USE_TYPED_ARRAYS == 1
HEAP: Int32Array;
IHEAP: Int32Array;
FHEAP: Float64Array;

// USE_TYPED_ARRAYS == 2
HEAP8: Int8Array;
HEAP16: Int16Array;
HEAP32: Int32Array;
HEAPU8: Uint8Array;
HEAPU16: Uint16Array;
HEAPU32: Uint32Array;
HEAPF32: Float32Array;
HEAPF64: Float64Array;

TOTAL_STACK: number;
TOTAL_MEMORY: number;
FAST_MEMORY: number;

addOnPreRun(cb: () => any): void;
addOnInit(cb: () => any): void;
addOnPreMain(cb: () => any): void;
addOnExit(cb: () => any): void;
addOnPostRun(cb: () => any): void;

preloadedImages: any;
preloadedAudios: any;

_malloc(size: number): number;
_free(ptr: number): void;
}

/**
* A factory function is generated when setting the `MODULARIZE` build option
* to `1` in your Emscripten build. It return a Promise that resolves to an
* initialized, ready-to-call `EmscriptenModule` instance.
*
* By default, the factory function will be named `Module`. It's recommended to
* use the `EXPORT_ES6` option, in which the factory function will be the
* default export. If used without `EXPORT_ES6`, the factory function will be a
* global variable. You can rename the variable using the `EXPORT_NAME` build
* option. It's left to you to declare any global variables as needed in your
* application's types.
* @param moduleOverrides Default properties for the initialized module.
*/
type EmscriptenModuleFactory<T extends EmscriptenModule = EmscriptenModule> = (
moduleOverrides?: Partial<T>,
) => Promise<T>;

declare namespace FS {
interface Lookup {
path: string;
node: FSNode;
}

interface FSStream {}
interface FSNode {}
interface ErrnoError {}

let ignorePermissions: boolean;
let trackingDelegate: any;
let tracking: any;
let genericErrors: any;

//
// paths
//
function lookupPath(path: string, opts: any): Lookup;
function getPath(node: FSNode): string;

//
// nodes
//
function isFile(mode: number): boolean;
function isDir(mode: number): boolean;
function isLink(mode: number): boolean;
function isChrdev(mode: number): boolean;
function isBlkdev(mode: number): boolean;
function isFIFO(mode: number): boolean;
function isSocket(mode: number): boolean;

//
// devices
//
function major(dev: number): number;
function minor(dev: number): number;
function makedev(ma: number, mi: number): number;
function registerDevice(dev: number, ops: any): void;

//
// core
//
function syncfs(populate: boolean, callback: (e: any) => any): void;
function syncfs(callback: (e: any) => any, populate?: boolean): void;
function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any;
function unmount(mountpoint: string): void;

function mkdir(path: string, mode?: number): any;
function mkdev(path: string, mode?: number, dev?: number): any;
function symlink(oldpath: string, newpath: string): any;
function rename(old_path: string, new_path: string): void;
function rmdir(path: string): void;
function readdir(path: string): any;
function unlink(path: string): void;
function readlink(path: string): string;
function stat(path: string, dontFollow?: boolean): any;
function lstat(path: string): any;
function chmod(path: string, mode: number, dontFollow?: boolean): void;
function lchmod(path: string, mode: number): void;
function fchmod(fd: number, mode: number): void;
function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void;
function lchown(path: string, uid: number, gid: number): void;
function fchown(fd: number, uid: number, gid: number): void;
function truncate(path: string, len: number): void;
function ftruncate(fd: number, len: number): void;
function utime(path: string, atime: number, mtime: number): void;
function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream;
function close(stream: FSStream): void;
function llseek(stream: FSStream, offset: number, whence: number): any;
function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number;
function write(
stream: FSStream,
buffer: ArrayBufferView,
offset: number,
length: number,
position?: number,
canOwn?: boolean,
): number;
function allocate(stream: FSStream, offset: number, length: number): void;
function mmap(
stream: FSStream,
buffer: ArrayBufferView,
offset: number,
length: number,
position: number,
prot: number,
flags: number,
): any;
function ioctl(stream: FSStream, cmd: any, arg: any): any;
function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array;
function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string;
function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array;
function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void;

//
// module-level FS code
//
function cwd(): string;
function chdir(path: string): void;
function init(
input: null | (() => number | null),
output: null | ((c: number) => any),
error: null | ((c: number) => any),
): void;

function createLazyFile(
parent: string | FSNode,
name: string,
url: string,
canRead: boolean,
canWrite: boolean,
): FSNode;
function createPreloadedFile(
parent: string | FSNode,
name: string,
url: string,
canRead: boolean,
canWrite: boolean,
onload?: () => void,
onerror?: () => void,
dontCreateFile?: boolean,
canOwn?: boolean,
): void;
function createDataFile(
parent: string | FSNode,
name: string,
data: ArrayBufferView,
canRead: boolean,
canWrite: boolean,
canOwn: boolean,
): FSNode;
}

declare var MEMFS: Emscripten.FileSystemType;
declare var NODEFS: Emscripten.FileSystemType;
declare var IDBFS: Emscripten.FileSystemType;

// Below runtime function/variable declarations are exportable by
// -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge
// EmscriptenModule interface to add runtime functions.
//
// For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
// You can access ccall() via Module["ccall"]. In this case, you should
// extend EmscriptenModule to pass the compiler check like the following:
//
// interface YourOwnEmscriptenModule extends EmscriptenModule {
// ccall: typeof ccall;
// }
//
// See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function

declare function ccall(
ident: string,
returnType: Emscripten.JSType | null,
argTypes: Emscripten.JSType[],
args: Emscripten.TypeCompatibleWithC[],
opts?: Emscripten.CCallOpts,
): any;
declare function cwrap(
ident: string,
returnType: Emscripten.JSType | null,
argTypes: Emscripten.JSType[],
opts?: Emscripten.CCallOpts,
): (...args: any[]) => any;

declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void;
declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number;

declare function allocate(
slab: number[] | ArrayBufferView | number,
types: Emscripten.CType | Emscripten.CType[],
allocator: number,
ptr?: number,
): number;

declare function stackAlloc(size: number): number;
declare function stackSave(): number;
declare function stackRestore(ptr: number): void;

declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string;
declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF8(str: string): number;
declare function allocateUTF8(str: string): number;
declare function allocateUTF8OnStack(str: string): number;
declare function UTF16ToString(ptr: number): string;
declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF16(str: string): number;
declare function UTF32ToString(ptr: number): string;
declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF32(str: string): number;

declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
declare function intArrayToString(array: number[]): string;
declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
declare function writeArrayToMemory(array: number[], buffer: number): void;
declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;

declare function addRunDependency(id: any): void;
declare function removeRunDependency(id: any): void;

declare function addFunction(func: (...args: any[]) => any, signature?: string): number;
declare function removeFunction(funcPtr: number): void;

declare var ALLOC_NORMAL: number;
declare var ALLOC_STACK: number;
declare var ALLOC_STATIC: number;
declare var ALLOC_DYNAMIC: number;
declare var ALLOC_NONE: number;