├── .gitattributes ├── .gitignore ├── README.md ├── SkiaSharp.Views.Blazor ├── Internal │ ├── ActionHelper.cs │ ├── DpiWatcherInterop.cs │ ├── FloatFloatActionHelper.cs │ ├── JSModuleInterop.cs │ ├── SKCanvasViewInterop.cs │ ├── SKGLViewInterop.cs │ ├── SizeWatcherInterop.cs │ └── ValueStopwatch.cs ├── SKCanvasView.razor ├── SKCanvasView.razor.cs ├── SKGLView.razor ├── SKGLView.razor.cs ├── SKPaintGLSurfaceEventArgs.cs ├── SKPaintSurfaceEventArgs.cs ├── SkiaSharp.Views.Blazor.csproj ├── nuget │ └── build │ │ └── net6.0 │ │ └── SkiaSharp.Views.Blazor.props ├── tsconfig.json └── wwwroot │ ├── .gitignore │ ├── DpiWatcher.js │ ├── DpiWatcher.ts │ ├── SKCanvasView.js │ ├── SKCanvasView.ts │ ├── SKGLView.js │ ├── SKGLView.ts │ ├── SizeWatcher.js │ ├── SizeWatcher.ts │ └── types │ ├── dotnet │ ├── extras.d.ts │ └── index.d.ts │ └── emscripten │ └── index.d.ts ├── SkiaSharp.gif ├── blazor-native.sln ├── blazor-native ├── App.razor ├── Pages │ ├── GPU.razor │ └── Index.razor ├── Program.cs ├── Properties │ └── launchSettings.json ├── Shared │ ├── MainLayout.razor │ ├── MainLayout.razor.css │ ├── NavMenu.razor │ └── NavMenu.razor.css ├── SkiaSharpSample.csproj ├── _Imports.razor └── wwwroot │ ├── css │ ├── app.css │ ├── bootstrap │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ └── open-iconic │ │ └── font │ │ ├── css │ │ └── open-iconic-bootstrap.min.css │ │ └── fonts │ │ ├── open-iconic.eot │ │ ├── open-iconic.otf │ │ ├── open-iconic.svg │ │ ├── open-iconic.ttf │ │ └── open-iconic.woff │ ├── favicon.ico │ ├── index.html │ └── js │ └── app.js ├── maui-g.gif ├── nuget.config └── skiasharp-blazor-wasm.png /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SkiaSharp on Blazor Web Assembly 2 | 3 | This repo is now archived because it has grown up! Check out the new package on NuGet.org: https://www.nuget.org/packages/SkiaSharp.Views.Blazor 4 | 5 | If there are any problems/bugs/suggestions, please open an issue at SkiaSharp: https://github.com/mono/SkiaSharp/issues 6 | 7 | ![A cool drawing](SkiaSharp.gif) 8 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/Internal/ActionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using Microsoft.JSInterop; 4 | 5 | namespace SkiaSharp.Views.Blazor.Internal 6 | { 7 | [EditorBrowsable(EditorBrowsableState.Never)] 8 | public class ActionHelper 9 | { 10 | private readonly Action action; 11 | 12 | public ActionHelper(Action action) 13 | { 14 | this.action = action; 15 | } 16 | 17 | [JSInvokable] 18 | public void Invoke() => action?.Invoke(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/Internal/DpiWatcherInterop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.JSInterop; 4 | 5 | namespace SkiaSharp.Views.Blazor.Internal 6 | { 7 | internal class DpiWatcherInterop : JSModuleInterop 8 | { 9 | private const string JsFilename = "./_content/SkiaSharp.Views.Blazor/DpiWatcher.js"; 10 | private const string StartSymbol = "DpiWatcher.start"; 11 | private const string StopSymbol = "DpiWatcher.stop"; 12 | private const string GetDpiSymbol = "DpiWatcher.getDpi"; 13 | 14 | private static DpiWatcherInterop? instance; 15 | 16 | private event Action? callbacksEvent; 17 | private readonly FloatFloatActionHelper callbackHelper; 18 | 19 | private DotNetObjectReference? callbackReference; 20 | 21 | public static async Task ImportAsync(IJSRuntime js, Action? callback = null) 22 | { 23 | var interop = Get(js); 24 | await interop.ImportAsync(); 25 | if (callback != null) 26 | interop.Subscribe(callback); 27 | return interop; 28 | } 29 | 30 | public static DpiWatcherInterop Get(IJSRuntime js) => 31 | instance ??= new DpiWatcherInterop(js); 32 | 33 | private DpiWatcherInterop(IJSRuntime js) 34 | : base(js, JsFilename) 35 | { 36 | callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n)); 37 | } 38 | 39 | protected override void OnDisposingModule() => 40 | Stop(); 41 | 42 | public void Subscribe(Action callback) 43 | { 44 | var shouldStart = callbacksEvent == null; 45 | 46 | callbacksEvent += callback; 47 | 48 | var dpi = shouldStart 49 | ? Start() 50 | : GetDpi(); 51 | 52 | callback(dpi); 53 | } 54 | 55 | public void Unsubscribe(Action callback) 56 | { 57 | callbacksEvent -= callback; 58 | 59 | if (callbacksEvent == null) 60 | Stop(); 61 | } 62 | 63 | private double Start() 64 | { 65 | if (callbackReference != null) 66 | return GetDpi(); 67 | 68 | callbackReference = DotNetObjectReference.Create(callbackHelper); 69 | 70 | return Invoke(StartSymbol, callbackReference); 71 | } 72 | 73 | private void Stop() 74 | { 75 | if (callbackReference == null) 76 | return; 77 | 78 | Invoke(StopSymbol); 79 | 80 | callbackReference?.Dispose(); 81 | callbackReference = null; 82 | } 83 | 84 | public double GetDpi() => 85 | Invoke(GetDpiSymbol); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/Internal/FloatFloatActionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using Microsoft.JSInterop; 4 | 5 | namespace SkiaSharp.Views.Blazor.Internal 6 | { 7 | [EditorBrowsable(EditorBrowsableState.Never)] 8 | public class FloatFloatActionHelper 9 | { 10 | private readonly Action action; 11 | 12 | public FloatFloatActionHelper(Action action) 13 | { 14 | this.action = action; 15 | } 16 | 17 | [JSInvokable] 18 | public void Invoke(float width, float height) => action?.Invoke(width, height); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/Internal/JSModuleInterop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.JSInterop; 4 | 5 | namespace SkiaSharp.Views.Blazor.Internal 6 | { 7 | internal class JSModuleInterop : IDisposable 8 | { 9 | private readonly Task moduleTask; 10 | private IJSUnmarshalledObjectReference? module; 11 | 12 | public JSModuleInterop(IJSRuntime js, string filename) 13 | { 14 | if (js is not IJSInProcessRuntime) 15 | throw new NotSupportedException("SkiaSharp currently only works on Web Assembly."); 16 | 17 | moduleTask = js.InvokeAsync("import", filename).AsTask(); 18 | } 19 | 20 | public async Task ImportAsync() 21 | { 22 | module = await moduleTask; 23 | } 24 | 25 | public void Dispose() 26 | { 27 | OnDisposingModule(); 28 | Module.Dispose(); 29 | } 30 | 31 | protected IJSUnmarshalledObjectReference Module => 32 | module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first."); 33 | 34 | protected void Invoke(string identifier, params object?[]? args) => 35 | Module.InvokeVoid(identifier, args); 36 | 37 | protected TValue Invoke(string identifier, params object?[]? args) => 38 | Module.Invoke(identifier, args); 39 | 40 | protected virtual void OnDisposingModule() { } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/Internal/SKCanvasViewInterop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.JSInterop; 5 | 6 | namespace SkiaSharp.Views.Blazor.Internal 7 | { 8 | internal class SKCanvasViewInterop : JSModuleInterop 9 | { 10 | private const string JsFilename = "./_content/SkiaSharp.Views.Blazor/SKCanvasView.js"; 11 | private const string InvalidateSymbol = "SKCanvasView.invalidate"; 12 | 13 | private readonly ElementReference htmlCanvas; 14 | 15 | public static async Task ImportAsync(IJSRuntime js, ElementReference element) 16 | { 17 | var interop = new SKCanvasViewInterop(js, element); 18 | await interop.ImportAsync(); 19 | return interop; 20 | } 21 | 22 | public SKCanvasViewInterop(IJSRuntime js, ElementReference element) 23 | : base(js, JsFilename) 24 | { 25 | htmlCanvas = element; 26 | } 27 | 28 | public bool Invalidate(IntPtr intPtr, SKSizeI rawSize) => 29 | Invoke(InvalidateSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/Internal/SKGLViewInterop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.JSInterop; 5 | 6 | namespace SkiaSharp.Views.Blazor.Internal 7 | { 8 | internal class SKGLViewInterop : JSModuleInterop 9 | { 10 | private const string JsFilename = "./_content/SkiaSharp.Views.Blazor/SKGLView.js"; 11 | private const string InitSymbol = "SKGLView.init"; 12 | private const string DeinitSymbol = "SKGLView.deinit"; 13 | private const string RequestAnimationFrameSymbol = "SKGLView.requestAnimationFrame"; 14 | 15 | private readonly ElementReference htmlCanvas; 16 | private readonly string htmlElementId; 17 | private readonly ActionHelper callbackHelper; 18 | 19 | private DotNetObjectReference? callbackReference; 20 | 21 | public static async Task<(SKGLViewInterop, Info)> ImportAsync(IJSRuntime js, ElementReference element, Action callback) 22 | { 23 | var interop = new SKGLViewInterop(js, element, callback); 24 | await interop.ImportAsync(); 25 | var info = interop.Init(); 26 | return (interop, info); 27 | } 28 | 29 | public SKGLViewInterop(IJSRuntime js, ElementReference element, Action renderFrameCallback) 30 | : base(js, JsFilename) 31 | { 32 | htmlCanvas = element; 33 | htmlElementId = element.Id; 34 | 35 | callbackHelper = new ActionHelper(renderFrameCallback); 36 | } 37 | 38 | protected override void OnDisposingModule() => 39 | Deinit(); 40 | 41 | public Info Init() 42 | { 43 | if (callbackReference != null) 44 | throw new InvalidOperationException("Unable to initialize the same canvas more than once."); 45 | 46 | callbackReference = DotNetObjectReference.Create(callbackHelper); 47 | 48 | return Invoke(InitSymbol, htmlCanvas, htmlElementId, callbackReference); 49 | } 50 | 51 | public void Deinit() 52 | { 53 | if (callbackReference == null) 54 | return; 55 | 56 | Invoke(DeinitSymbol, htmlElementId); 57 | 58 | callbackReference?.Dispose(); 59 | } 60 | 61 | public void RequestAnimationFrame(bool enableRenderLoop, int rawWidth, int rawHeight) => 62 | Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop, rawWidth, rawHeight); 63 | 64 | public record Info(int ContextId, uint FboId, int Stencils, int Samples, int Depth); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/Internal/SizeWatcherInterop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.JSInterop; 5 | 6 | namespace SkiaSharp.Views.Blazor.Internal 7 | { 8 | internal class SizeWatcherInterop : JSModuleInterop 9 | { 10 | private const string JsFilename = "./_content/SkiaSharp.Views.Blazor/SizeWatcher.js"; 11 | private const string ObserveSymbol = "SizeWatcher.observe"; 12 | private const string UnobserveSymbol = "SizeWatcher.unobserve"; 13 | 14 | private readonly ElementReference htmlElement; 15 | private readonly string htmlElementId; 16 | private readonly FloatFloatActionHelper callbackHelper; 17 | 18 | private DotNetObjectReference? callbackReference; 19 | 20 | public static async Task ImportAsync(IJSRuntime js, ElementReference element, Action callback) 21 | { 22 | var interop = new SizeWatcherInterop(js, element, callback); 23 | await interop.ImportAsync(); 24 | interop.Start(); 25 | return interop; 26 | } 27 | 28 | public SizeWatcherInterop(IJSRuntime js, ElementReference element, Action callback) 29 | : base(js, JsFilename) 30 | { 31 | htmlElement = element; 32 | htmlElementId = element.Id; 33 | callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y))); 34 | } 35 | 36 | protected override void OnDisposingModule() => 37 | Stop(); 38 | 39 | public void Start() 40 | { 41 | if (callbackReference != null) 42 | return; 43 | 44 | callbackReference = DotNetObjectReference.Create(callbackHelper); 45 | 46 | Invoke(ObserveSymbol, htmlElement, htmlElementId, callbackReference); 47 | } 48 | 49 | public void Stop() 50 | { 51 | if (callbackReference == null) 52 | return; 53 | 54 | Invoke(UnobserveSymbol, htmlElementId); 55 | 56 | callbackReference?.Dispose(); 57 | callbackReference = null; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/Internal/ValueStopwatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace SkiaSharp.Views.Blazor.Internal 5 | { 6 | internal struct ValueStopwatch 7 | { 8 | private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; 9 | 10 | private readonly long _startTimestamp; 11 | 12 | private ValueStopwatch(long startTimestamp) 13 | { 14 | _startTimestamp = startTimestamp; 15 | } 16 | 17 | public bool IsActive => _startTimestamp != 0; 18 | 19 | public static ValueStopwatch StartNew() => 20 | new ValueStopwatch(Stopwatch.GetTimestamp()); 21 | 22 | public TimeSpan GetElapsedTime() 23 | { 24 | // Start timestamp can't be zero in an initialized ValueStopwatch. It would have to be literally the first thing executed when the machine boots to be 0. 25 | // So it being 0 is a clear indication of default(ValueStopwatch) 26 | if (!IsActive) 27 | throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time."); 28 | 29 | var end = Stopwatch.GetTimestamp(); 30 | var timestampDelta = end - _startTimestamp; 31 | var ticks = (long)(TimestampToTicks * timestampDelta); 32 | return new TimeSpan(ticks); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/SKCanvasView.razor: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/SKCanvasView.razor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Components; 6 | using Microsoft.JSInterop; 7 | using SkiaSharp.Views.Blazor.Internal; 8 | 9 | namespace SkiaSharp.Views.Blazor 10 | { 11 | public partial class SKCanvasView : IDisposable 12 | { 13 | private SKCanvasViewInterop interop = null!; 14 | private SizeWatcherInterop sizeWatcher = null!; 15 | private DpiWatcherInterop dpiWatcher = null!; 16 | private ElementReference htmlCanvas; 17 | 18 | private SKSizeI pixelSize; 19 | private byte[]? pixels; 20 | private GCHandle pixelsHandle; 21 | private bool ignorePixelScaling; 22 | private double dpi; 23 | private SKSize canvasSize; 24 | 25 | #if DEBUG 26 | public TimeSpan LastFrameDuration; 27 | #endif 28 | 29 | [Inject] 30 | IJSRuntime JS { get; set; } = null!; 31 | 32 | [Parameter] 33 | public bool IgnorePixelScaling 34 | { 35 | get => ignorePixelScaling; 36 | set 37 | { 38 | if (ignorePixelScaling != value) 39 | { 40 | ignorePixelScaling = value; 41 | Invalidate(); 42 | } 43 | } 44 | } 45 | 46 | [Parameter] 47 | public Action? OnPaintSurface { get; set; } 48 | 49 | [Parameter(CaptureUnmatchedValues = true)] 50 | public IReadOnlyDictionary? AdditionalAttributes { get; set; } 51 | 52 | protected override async Task OnAfterRenderAsync(bool firstRender) 53 | { 54 | if (firstRender) 55 | { 56 | interop = await SKCanvasViewInterop.ImportAsync(JS, htmlCanvas); 57 | sizeWatcher = await SizeWatcherInterop.ImportAsync(JS, htmlCanvas, OnSizeChanged); 58 | dpiWatcher = await DpiWatcherInterop.ImportAsync(JS, OnDpiChanged); 59 | } 60 | } 61 | 62 | public void Invalidate() 63 | { 64 | if (canvasSize.Width <= 0 || canvasSize.Height <= 0 || dpi <= 0) 65 | return; 66 | 67 | var info = CreateBitmap(out var unscaledSize); 68 | var userVisibleSize = IgnorePixelScaling ? unscaledSize : info.Size; 69 | 70 | using (var surface = SKSurface.Create(info, pixelsHandle.AddrOfPinnedObject(), info.RowBytes)) 71 | { 72 | if (IgnorePixelScaling) 73 | { 74 | var canvas = surface.Canvas; 75 | canvas.Scale((float)dpi); 76 | canvas.Save(); 77 | } 78 | 79 | OnPaintSurface?.Invoke(new SKPaintSurfaceEventArgs(surface, info.WithSize(userVisibleSize), info)); 80 | } 81 | 82 | interop.Invalidate(pixelsHandle.AddrOfPinnedObject(), info.Size); 83 | } 84 | 85 | private SKImageInfo CreateBitmap(out SKSizeI unscaledSize) 86 | { 87 | var size = CreateSize(out unscaledSize); 88 | var info = new SKImageInfo(size.Width, size.Height, SKImageInfo.PlatformColorType, SKAlphaType.Opaque); 89 | 90 | if (pixels == null || pixelSize.Width != info.Width || pixelSize.Height != info.Height) 91 | { 92 | FreeBitmap(); 93 | 94 | pixels = new byte[info.BytesSize]; 95 | pixelsHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned); 96 | pixelSize = info.Size; 97 | } 98 | 99 | return info; 100 | } 101 | 102 | private SKSizeI CreateSize(out SKSizeI unscaledSize) 103 | { 104 | unscaledSize = SKSizeI.Empty; 105 | 106 | var w = canvasSize.Width; 107 | var h = canvasSize.Height; 108 | 109 | if (!IsPositive(w) || !IsPositive(h)) 110 | return SKSizeI.Empty; 111 | 112 | unscaledSize = new SKSizeI((int)w, (int)h); 113 | return new SKSizeI((int)(w * dpi), (int)(h * dpi)); 114 | 115 | static bool IsPositive(double value) 116 | { 117 | return !double.IsNaN(value) && !double.IsInfinity(value) && value > 0; 118 | } 119 | } 120 | 121 | private void FreeBitmap() 122 | { 123 | if (pixels != null) 124 | { 125 | pixelsHandle.Free(); 126 | pixels = null; 127 | } 128 | } 129 | 130 | private void OnDpiChanged(double newDpi) 131 | { 132 | dpi = newDpi; 133 | 134 | Invalidate(); 135 | } 136 | 137 | private void OnSizeChanged(SKSize newSize) 138 | { 139 | canvasSize = newSize; 140 | 141 | Invalidate(); 142 | } 143 | 144 | public void Dispose() 145 | { 146 | dpiWatcher.Unsubscribe(OnDpiChanged); 147 | sizeWatcher.Dispose(); 148 | interop.Dispose(); 149 | 150 | FreeBitmap(); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/SKGLView.razor: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/SKGLView.razor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Components; 5 | using Microsoft.JSInterop; 6 | using SkiaSharp.Views.Blazor.Internal; 7 | 8 | namespace SkiaSharp.Views.Blazor 9 | { 10 | public partial class SKGLView : IDisposable 11 | { 12 | private SKGLViewInterop interop = null!; 13 | private SizeWatcherInterop sizeWatcher = null!; 14 | private DpiWatcherInterop dpiWatcher = null!; 15 | private SKGLViewInterop.Info jsInfo = null!; 16 | private ElementReference htmlCanvas; 17 | 18 | private const int ResourceCacheBytes = 256 * 1024 * 1024; // 256 MB 19 | private const SKColorType colorType = SKColorType.Rgba8888; 20 | private const GRSurfaceOrigin surfaceOrigin = GRSurfaceOrigin.BottomLeft; 21 | 22 | private GRContext? context; 23 | private GRGlInterface? glInterface; 24 | private GRBackendRenderTarget? renderTarget; 25 | private SKSize renderTargetSize; 26 | private SKSurface? surface; 27 | private SKCanvas? canvas; 28 | private bool enableRenderLoop; 29 | private double dpi; 30 | private SKSize canvasSize; 31 | 32 | [Inject] 33 | IJSRuntime JS { get; set; } = null!; 34 | 35 | [Parameter] 36 | public Action? OnPaintSurface { get; set; } 37 | 38 | [Parameter] 39 | public bool EnableRenderLoop 40 | { 41 | get => enableRenderLoop; 42 | set 43 | { 44 | if (enableRenderLoop != value) 45 | { 46 | enableRenderLoop = value; 47 | Invalidate(); 48 | } 49 | } 50 | } 51 | 52 | [Parameter(CaptureUnmatchedValues = true)] 53 | public IReadOnlyDictionary? AdditionalAttributes { get; set; } 54 | 55 | protected override async Task OnAfterRenderAsync(bool firstRender) 56 | { 57 | if (firstRender) 58 | { 59 | (interop, jsInfo) = await SKGLViewInterop.ImportAsync(JS, htmlCanvas, OnRenderFrame); 60 | sizeWatcher = await SizeWatcherInterop.ImportAsync(JS, htmlCanvas, OnSizeChanged); 61 | dpiWatcher = await DpiWatcherInterop.ImportAsync(JS, OnDpiChanged); 62 | } 63 | } 64 | 65 | public void Invalidate() 66 | { 67 | if (canvasSize.Width <= 0 || canvasSize.Height <= 0 || dpi <= 0 || jsInfo == null) 68 | return; 69 | 70 | interop.RequestAnimationFrame(EnableRenderLoop, (int)(canvasSize.Width * dpi), (int)(canvasSize.Height * dpi)); 71 | } 72 | 73 | private void OnRenderFrame() 74 | { 75 | if (canvasSize.Width <= 0 || canvasSize.Height <= 0 || dpi <= 0 || jsInfo == null) 76 | return; 77 | 78 | // create the SkiaSharp context 79 | if (context == null) 80 | { 81 | glInterface = GRGlInterface.Create(); 82 | context = GRContext.CreateGl(glInterface); 83 | 84 | // bump the default resource cache limit 85 | context.SetResourceCacheLimit(ResourceCacheBytes); 86 | } 87 | 88 | // get the new surface size 89 | var newSize = CreateSize(); 90 | 91 | // manage the drawing surface 92 | if (renderTarget == null || renderTargetSize != newSize || !renderTarget.IsValid) 93 | { 94 | // create or update the dimensions 95 | renderTargetSize = newSize; 96 | 97 | var glInfo = new GRGlFramebufferInfo(jsInfo.FboId, colorType.ToGlSizedFormat()); 98 | 99 | // destroy the old surface 100 | surface?.Dispose(); 101 | surface = null; 102 | canvas = null; 103 | 104 | // re-create the render target 105 | renderTarget?.Dispose(); 106 | renderTarget = new GRBackendRenderTarget(newSize.Width, newSize.Height, jsInfo.Samples, jsInfo.Stencils, glInfo); 107 | } 108 | 109 | // create the surface 110 | if (surface == null) 111 | { 112 | surface = SKSurface.Create(context, renderTarget, surfaceOrigin, colorType); 113 | canvas = surface.Canvas; 114 | } 115 | 116 | using (new SKAutoCanvasRestore(canvas, true)) 117 | { 118 | // start drawing 119 | OnPaintSurface?.Invoke(new SKPaintGLSurfaceEventArgs(surface, renderTarget, surfaceOrigin, colorType)); 120 | } 121 | 122 | // update the control 123 | canvas?.Flush(); 124 | context.Flush(); 125 | } 126 | 127 | private void OnDpiChanged(double newDpi) 128 | { 129 | dpi = newDpi; 130 | 131 | Invalidate(); 132 | } 133 | 134 | private void OnSizeChanged(SKSize newSize) 135 | { 136 | canvasSize = newSize; 137 | 138 | Invalidate(); 139 | } 140 | 141 | private SKSizeI CreateSize() 142 | { 143 | var w = canvasSize.Width; 144 | var h = canvasSize.Height; 145 | 146 | if (!IsPositive(w) || !IsPositive(h)) 147 | return SKSizeI.Empty; 148 | 149 | return new SKSizeI((int)(w * dpi), (int)(h * dpi)); 150 | 151 | static bool IsPositive(double value) 152 | { 153 | return !double.IsNaN(value) && !double.IsInfinity(value) && value > 0; 154 | } 155 | } 156 | 157 | public void Dispose() 158 | { 159 | dpiWatcher.Unsubscribe(OnDpiChanged); 160 | sizeWatcher.Dispose(); 161 | interop.Dispose(); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/SKPaintGLSurfaceEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SkiaSharp.Views.Blazor 4 | { 5 | public class SKPaintGLSurfaceEventArgs : EventArgs 6 | { 7 | public SKPaintGLSurfaceEventArgs(SKSurface surface, GRBackendRenderTarget renderTarget) 8 | : this(surface, renderTarget, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888) 9 | { 10 | } 11 | 12 | public SKPaintGLSurfaceEventArgs(SKSurface surface, GRBackendRenderTarget renderTarget, GRSurfaceOrigin origin, SKColorType colorType) 13 | { 14 | Surface = surface; 15 | BackendRenderTarget = renderTarget; 16 | ColorType = colorType; 17 | Origin = origin; 18 | } 19 | 20 | public SKSurface Surface { get; } 21 | 22 | public GRBackendRenderTarget BackendRenderTarget { get; } 23 | 24 | public SKColorType ColorType { get; } 25 | 26 | public GRSurfaceOrigin Origin { get; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/SKPaintSurfaceEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SkiaSharp.Views.Blazor 4 | { 5 | public class SKPaintSurfaceEventArgs : EventArgs 6 | { 7 | public SKPaintSurfaceEventArgs(SKSurface surface, SKImageInfo info) 8 | : this(surface, info, info) 9 | { 10 | } 11 | 12 | public SKPaintSurfaceEventArgs(SKSurface surface, SKImageInfo info, SKImageInfo rawInfo) 13 | { 14 | Surface = surface; 15 | Info = info; 16 | RawInfo = rawInfo; 17 | } 18 | 19 | public SKSurface Surface { get; } 20 | 21 | public SKImageInfo Info { get; } 22 | 23 | public SKImageInfo RawInfo { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/SkiaSharp.Views.Blazor.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/nuget/build/net6.0/SkiaSharp.Views.Blazor.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "noEmitOnError": true, 5 | "removeComments": false, 6 | "sourceMap": false, 7 | "target": "ES2020", 8 | "module": "ES2020", 9 | "outDir": "wwwroot" 10 | }, 11 | "exclude": [ 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/.gitignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/DpiWatcher.js: -------------------------------------------------------------------------------- 1 | export class DpiWatcher { 2 | static getDpi() { 3 | return window.devicePixelRatio; 4 | } 5 | static start(callback) { 6 | console.info(`Starting DPI watcher with callback ${callback._id}...`); 7 | DpiWatcher.lastDpi = window.devicePixelRatio; 8 | DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); 9 | DpiWatcher.callback = callback; 10 | return DpiWatcher.lastDpi; 11 | } 12 | static stop() { 13 | console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`); 14 | window.clearInterval(DpiWatcher.timerId); 15 | DpiWatcher.callback = undefined; 16 | } 17 | static update() { 18 | if (!DpiWatcher.callback) 19 | return; 20 | const currentDpi = window.devicePixelRatio; 21 | const lastDpi = DpiWatcher.lastDpi; 22 | DpiWatcher.lastDpi = currentDpi; 23 | if (Math.abs(lastDpi - currentDpi) > 0.001) { 24 | DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/DpiWatcher.ts: -------------------------------------------------------------------------------- 1 |  2 | export class DpiWatcher { 3 | static lastDpi: number; 4 | static timerId: number; 5 | static callback: DotNet.DotNetObjectReference; 6 | 7 | public static getDpi() { 8 | return window.devicePixelRatio; 9 | } 10 | 11 | public static start(callback: DotNet.DotNetObjectReference): number { 12 | console.info(`Starting DPI watcher with callback ${callback._id}...`); 13 | 14 | DpiWatcher.lastDpi = window.devicePixelRatio; 15 | DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); 16 | DpiWatcher.callback = callback; 17 | 18 | return DpiWatcher.lastDpi; 19 | } 20 | 21 | public static stop() { 22 | console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`); 23 | 24 | window.clearInterval(DpiWatcher.timerId); 25 | 26 | DpiWatcher.callback = undefined; 27 | } 28 | 29 | static update() { 30 | if (!DpiWatcher.callback) 31 | return; 32 | 33 | const currentDpi = window.devicePixelRatio; 34 | const lastDpi = DpiWatcher.lastDpi; 35 | DpiWatcher.lastDpi = currentDpi; 36 | 37 | if (Math.abs(lastDpi - currentDpi) > 0.001) { 38 | DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/SKCanvasView.js: -------------------------------------------------------------------------------- 1 | export class SKCanvasView { 2 | static invalidate(htmlCanvas, pData, width, height) { 3 | if (!htmlCanvas) { 4 | console.error(`No canvas element was provided.`); 5 | return false; 6 | } 7 | if (!pData || width <= 0 || width <= 0) 8 | return false; 9 | var ctx = htmlCanvas.getContext('2d'); 10 | if (!ctx) { 11 | console.error(`Failed to obtain 2D canvas context.`); 12 | return false; 13 | } 14 | // make sure the canvas is scaled correctly for the drawing 15 | htmlCanvas.width = width; 16 | htmlCanvas.height = height; 17 | // set the canvas to be the bytes 18 | var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4); 19 | var imageData = new ImageData(buffer, width, height); 20 | ctx.putImageData(imageData, 0, 0); 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/SKCanvasView.ts: -------------------------------------------------------------------------------- 1 |  2 | declare let Module: EmscriptenModule; 3 | 4 | export class SKCanvasView { 5 | public static invalidate(htmlCanvas: HTMLCanvasElement, pData: number, width: number, height: number) { 6 | if (!htmlCanvas) { 7 | console.error(`No canvas element was provided.`); 8 | return false; 9 | } 10 | 11 | if (!pData || width <= 0 || width <= 0) 12 | return false; 13 | 14 | var ctx = htmlCanvas.getContext('2d'); 15 | if (!ctx) { 16 | console.error(`Failed to obtain 2D canvas context.`); 17 | return false; 18 | } 19 | 20 | // make sure the canvas is scaled correctly for the drawing 21 | htmlCanvas.width = width; 22 | htmlCanvas.height = height; 23 | 24 | // set the canvas to be the bytes 25 | var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4); 26 | var imageData = new ImageData(buffer, width, height); 27 | ctx.putImageData(imageData, 0, 0); 28 | 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/SKGLView.js: -------------------------------------------------------------------------------- 1 | export class SKGLView { 2 | constructor(element, callback) { 3 | this.renderLoopEnabled = false; 4 | this.renderLoopRequest = 0; 5 | this.htmlCanvas = element; 6 | const ctx = this.createWebGLContext(this.htmlCanvas); 7 | if (!ctx) { 8 | console.error(`Failed to create WebGL context: err ${ctx}`); 9 | return null; 10 | } 11 | // make current 12 | GL.makeContextCurrent(ctx); 13 | // read values 14 | const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); 15 | this.info = { 16 | context: ctx, 17 | fboId: fbo ? fbo.id : 0, 18 | stencil: GLctx.getParameter(GLctx.STENCIL_BITS), 19 | sample: 0, 20 | depth: GLctx.getParameter(GLctx.DEPTH_BITS), 21 | }; 22 | this.renderFrameCallback = callback; 23 | } 24 | static init(element, elementId, callback) { 25 | var htmlCanvas = element; 26 | if (!htmlCanvas) { 27 | console.error(`No canvas element was provided.`); 28 | return null; 29 | } 30 | if (!SKGLView.elements) 31 | SKGLView.elements = new Map(); 32 | SKGLView.elements[elementId] = element; 33 | const view = new SKGLView(element, callback); 34 | htmlCanvas.SKGLView = view; 35 | return view.info; 36 | } 37 | static deinit(elementId) { 38 | if (!elementId) 39 | return; 40 | const element = SKGLView.elements[elementId]; 41 | SKGLView.elements.delete(elementId); 42 | const htmlCanvas = element; 43 | if (!htmlCanvas || !htmlCanvas.SKGLView) 44 | return; 45 | htmlCanvas.SKGLView.deinit(); 46 | htmlCanvas.SKGLView = undefined; 47 | } 48 | static requestAnimationFrame(element, renderLoop, width, height) { 49 | const htmlCanvas = element; 50 | if (!htmlCanvas || !htmlCanvas.SKGLView) 51 | return; 52 | htmlCanvas.SKGLView.requestAnimationFrame(renderLoop, width, height); 53 | } 54 | static setEnableRenderLoop(element, enable) { 55 | const htmlCanvas = element; 56 | if (!htmlCanvas || !htmlCanvas.SKGLView) 57 | return; 58 | htmlCanvas.SKGLView.setEnableRenderLoop(enable); 59 | } 60 | deinit() { 61 | this.setEnableRenderLoop(false); 62 | } 63 | requestAnimationFrame(renderLoop, width, height) { 64 | // optionally update the render loop 65 | if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) 66 | this.setEnableRenderLoop(renderLoop); 67 | // make sure the canvas is scaled correctly for the drawing 68 | if (width && height) { 69 | this.htmlCanvas.width = width; 70 | this.htmlCanvas.height = height; 71 | } 72 | // skip because we have a render loop 73 | if (this.renderLoopRequest !== 0) 74 | return; 75 | // add the draw to the next frame 76 | this.renderLoopRequest = window.requestAnimationFrame(() => { 77 | // make current 78 | GL.makeContextCurrent(this.info.context); 79 | this.renderFrameCallback.invokeMethod('Invoke'); 80 | this.renderLoopRequest = 0; 81 | // we may want to draw the next frame 82 | if (this.renderLoopEnabled) 83 | this.requestAnimationFrame(); 84 | }); 85 | } 86 | setEnableRenderLoop(enable) { 87 | this.renderLoopEnabled = enable; 88 | // either start the new frame or cancel the existing one 89 | if (enable) { 90 | console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); 91 | this.requestAnimationFrame(); 92 | } 93 | else if (this.renderLoopRequest !== 0) { 94 | window.cancelAnimationFrame(this.renderLoopRequest); 95 | this.renderLoopRequest = 0; 96 | } 97 | } 98 | createWebGLContext(htmlCanvas) { 99 | const contextAttributes = { 100 | alpha: 1, 101 | depth: 1, 102 | stencil: 8, 103 | antialias: 1, 104 | premultipliedAlpha: 1, 105 | preserveDrawingBuffer: 0, 106 | preferLowPowerToHighPerformance: 0, 107 | failIfMajorPerformanceCaveat: 0, 108 | majorVersion: 2, 109 | minorVersion: 0, 110 | enableExtensionsByDefault: 1, 111 | explicitSwapControl: 0, 112 | renderViaOffscreenBackBuffer: 0, 113 | }; 114 | let ctx = GL.createContext(htmlCanvas, contextAttributes); 115 | if (!ctx && contextAttributes.majorVersion > 1) { 116 | console.warn('Falling back to WebGL 1.0'); 117 | contextAttributes.majorVersion = 1; 118 | contextAttributes.minorVersion = 0; 119 | ctx = GL.createContext(htmlCanvas, contextAttributes); 120 | } 121 | return ctx; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/SKGLView.ts: -------------------------------------------------------------------------------- 1 |  2 | declare let GL: any; 3 | declare let GLctx: WebGLRenderingContext; 4 | 5 | type SKGLViewInfo = { 6 | context: WebGLRenderingContext | WebGL2RenderingContext | undefined; 7 | fboId: number; 8 | stencil: number; 9 | sample: number; 10 | depth: number; 11 | } 12 | 13 | type SKGLViewCanvasElement = { 14 | SKGLView: SKGLView 15 | } & HTMLCanvasElement 16 | 17 | export class SKGLView { 18 | static elements: Map; 19 | 20 | htmlCanvas: HTMLCanvasElement; 21 | info: SKGLViewInfo; 22 | renderFrameCallback: DotNet.DotNetObjectReference; 23 | renderLoopEnabled: boolean = false; 24 | renderLoopRequest: number = 0; 25 | 26 | public static init(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo { 27 | var htmlCanvas = element as SKGLViewCanvasElement; 28 | if (!htmlCanvas) { 29 | console.error(`No canvas element was provided.`); 30 | return null; 31 | } 32 | 33 | if (!SKGLView.elements) 34 | SKGLView.elements = new Map(); 35 | SKGLView.elements[elementId] = element; 36 | 37 | const view = new SKGLView(element, callback); 38 | 39 | htmlCanvas.SKGLView = view; 40 | 41 | return view.info; 42 | } 43 | 44 | public static deinit(elementId: string) { 45 | if (!elementId) 46 | return; 47 | 48 | const element = SKGLView.elements[elementId]; 49 | SKGLView.elements.delete(elementId); 50 | 51 | const htmlCanvas = element as SKGLViewCanvasElement; 52 | if (!htmlCanvas || !htmlCanvas.SKGLView) 53 | return; 54 | 55 | htmlCanvas.SKGLView.deinit(); 56 | htmlCanvas.SKGLView = undefined; 57 | } 58 | 59 | public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean, width?: number, height?: number) { 60 | const htmlCanvas = element as SKGLViewCanvasElement; 61 | if (!htmlCanvas || !htmlCanvas.SKGLView) 62 | return; 63 | 64 | htmlCanvas.SKGLView.requestAnimationFrame(renderLoop, width, height); 65 | } 66 | 67 | public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) { 68 | const htmlCanvas = element as SKGLViewCanvasElement; 69 | if (!htmlCanvas || !htmlCanvas.SKGLView) 70 | return; 71 | 72 | htmlCanvas.SKGLView.setEnableRenderLoop(enable); 73 | } 74 | 75 | public constructor(element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) { 76 | this.htmlCanvas = element; 77 | 78 | const ctx = this.createWebGLContext(this.htmlCanvas); 79 | if (!ctx) { 80 | console.error(`Failed to create WebGL context: err ${ctx}`); 81 | return null; 82 | } 83 | 84 | // make current 85 | GL.makeContextCurrent(ctx); 86 | 87 | // read values 88 | const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); 89 | this.info = { 90 | context: ctx, 91 | fboId: fbo ? fbo.id : 0, 92 | stencil: GLctx.getParameter(GLctx.STENCIL_BITS), 93 | sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES) 94 | depth: GLctx.getParameter(GLctx.DEPTH_BITS), 95 | }; 96 | 97 | this.renderFrameCallback = callback; 98 | } 99 | 100 | public deinit() { 101 | this.setEnableRenderLoop(false); 102 | } 103 | 104 | public requestAnimationFrame(renderLoop?: boolean, width?: number, height?: number) { 105 | // optionally update the render loop 106 | if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) 107 | this.setEnableRenderLoop(renderLoop); 108 | 109 | // make sure the canvas is scaled correctly for the drawing 110 | if (width && height) { 111 | this.htmlCanvas.width = width; 112 | this.htmlCanvas.height = height; 113 | } 114 | 115 | // skip because we have a render loop 116 | if (this.renderLoopRequest !== 0) 117 | return; 118 | 119 | // add the draw to the next frame 120 | this.renderLoopRequest = window.requestAnimationFrame(() => { 121 | // make current 122 | GL.makeContextCurrent(this.info.context); 123 | 124 | this.renderFrameCallback.invokeMethod('Invoke'); 125 | this.renderLoopRequest = 0; 126 | 127 | // we may want to draw the next frame 128 | if (this.renderLoopEnabled) 129 | this.requestAnimationFrame(); 130 | }); 131 | } 132 | 133 | public setEnableRenderLoop(enable: boolean) { 134 | this.renderLoopEnabled = enable; 135 | 136 | // either start the new frame or cancel the existing one 137 | if (enable) { 138 | console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); 139 | this.requestAnimationFrame(); 140 | } else if (this.renderLoopRequest !== 0) { 141 | window.cancelAnimationFrame(this.renderLoopRequest); 142 | this.renderLoopRequest = 0; 143 | } 144 | } 145 | 146 | createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { 147 | const contextAttributes = { 148 | alpha: 1, 149 | depth: 1, 150 | stencil: 8, 151 | antialias: 1, 152 | premultipliedAlpha: 1, 153 | preserveDrawingBuffer: 0, 154 | preferLowPowerToHighPerformance: 0, 155 | failIfMajorPerformanceCaveat: 0, 156 | majorVersion: 2, 157 | minorVersion: 0, 158 | enableExtensionsByDefault: 1, 159 | explicitSwapControl: 0, 160 | renderViaOffscreenBackBuffer: 0, 161 | }; 162 | 163 | let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); 164 | if (!ctx && contextAttributes.majorVersion > 1) { 165 | console.warn('Falling back to WebGL 1.0'); 166 | contextAttributes.majorVersion = 1; 167 | contextAttributes.minorVersion = 0; 168 | ctx = GL.createContext(htmlCanvas, contextAttributes); 169 | } 170 | 171 | return ctx; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/SizeWatcher.js: -------------------------------------------------------------------------------- 1 | export class SizeWatcher { 2 | static observe(element, elementId, callback) { 3 | if (!element || !callback) 4 | return; 5 | console.info(`Adding size watcher observation with callback ${callback._id}...`); 6 | SizeWatcher.init(); 7 | const watcherElement = element; 8 | watcherElement.SizeWatcher = { 9 | callback: callback 10 | }; 11 | SizeWatcher.elements[elementId] = element; 12 | SizeWatcher.observer.observe(element); 13 | SizeWatcher.invoke(element); 14 | } 15 | static unobserve(elementId) { 16 | if (!elementId || !SizeWatcher.observer) 17 | return; 18 | console.info('Removing size watcher observation...'); 19 | const element = SizeWatcher.elements[elementId]; 20 | SizeWatcher.elements.delete(elementId); 21 | SizeWatcher.observer.unobserve(element); 22 | } 23 | static init() { 24 | if (SizeWatcher.observer) 25 | return; 26 | console.info('Starting size watcher...'); 27 | SizeWatcher.elements = new Map(); 28 | SizeWatcher.observer = new ResizeObserver((entries) => { 29 | for (let entry of entries) { 30 | SizeWatcher.invoke(entry.target); 31 | } 32 | }); 33 | } 34 | static invoke(element) { 35 | const watcherElement = element; 36 | const instance = watcherElement.SizeWatcher; 37 | if (!instance || !instance.callback) 38 | return; 39 | return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/SizeWatcher.ts: -------------------------------------------------------------------------------- 1 |  2 | type SizeWatcherElement = { 3 | SizeWatcher: SizeWatcherInstance; 4 | } & HTMLElement 5 | 6 | type SizeWatcherInstance = { 7 | callback: DotNet.DotNetObjectReference; 8 | } 9 | 10 | export class SizeWatcher { 11 | static observer: ResizeObserver; 12 | static elements: Map; 13 | 14 | public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) { 15 | if (!element || !callback) 16 | return; 17 | 18 | console.info(`Adding size watcher observation with callback ${callback._id}...`); 19 | 20 | SizeWatcher.init(); 21 | 22 | const watcherElement = element as SizeWatcherElement; 23 | watcherElement.SizeWatcher = { 24 | callback: callback 25 | }; 26 | 27 | SizeWatcher.elements[elementId] = element; 28 | SizeWatcher.observer.observe(element); 29 | 30 | SizeWatcher.invoke(element); 31 | } 32 | 33 | public static unobserve(elementId: string) { 34 | if (!elementId || !SizeWatcher.observer) 35 | return; 36 | 37 | console.info('Removing size watcher observation...'); 38 | 39 | const element = SizeWatcher.elements[elementId]; 40 | 41 | SizeWatcher.elements.delete(elementId); 42 | SizeWatcher.observer.unobserve(element); 43 | } 44 | 45 | static init() { 46 | if (SizeWatcher.observer) 47 | return; 48 | 49 | console.info('Starting size watcher...'); 50 | 51 | SizeWatcher.elements = new Map(); 52 | SizeWatcher.observer = new ResizeObserver((entries) => { 53 | for (let entry of entries) { 54 | SizeWatcher.invoke(entry.target); 55 | } 56 | }); 57 | } 58 | 59 | static invoke(element: Element) { 60 | const watcherElement = element as SizeWatcherElement; 61 | const instance = watcherElement.SizeWatcher; 62 | 63 | if (!instance || !instance.callback) 64 | return; 65 | 66 | return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/types/dotnet/extras.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare namespace DotNet { 3 | interface DotNetObjectReference extends DotNet.DotNetObject { 4 | _id: number; 5 | dispose(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/types/dotnet/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for non-npm package @blazor/javascript-interop 3.1 2 | // Project: https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interop?view=aspnetcore-3.1 3 | // Definitions by: Piotr Błażejewicz (Peter Blazejewicz) 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | // Minimum TypeScript Version: 3.0 6 | 7 | // Here be dragons! 8 | // This is community-maintained definition file intended to ease the process of developing 9 | // high quality JavaScript interop code to be used in Blazor application from your C# .Net code. 10 | // Could be removed without a notice in case official definition types ships with Blazor itself. 11 | 12 | // tslint:disable:no-unnecessary-generics 13 | 14 | declare namespace DotNet { 15 | /** 16 | * Invokes the specified .NET public method synchronously. Not all hosting scenarios support 17 | * synchronous invocation, so if possible use invokeMethodAsync instead. 18 | * 19 | * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. 20 | * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. 21 | * @param args Arguments to pass to the method, each of which must be JSON-serializable. 22 | * @returns The result of the operation. 23 | */ 24 | function invokeMethod(assemblyName: string, methodIdentifier: string, ...args: any[]): T; 25 | /** 26 | * Invokes the specified .NET public method asynchronously. 27 | * 28 | * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. 29 | * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. 30 | * @param args Arguments to pass to the method, each of which must be JSON-serializable. 31 | * @returns A promise representing the result of the operation. 32 | */ 33 | function invokeMethodAsync(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise; 34 | /** 35 | * Represents the .NET instance passed by reference to JavaScript. 36 | */ 37 | interface DotNetObject { 38 | /** 39 | * Invokes the specified .NET instance public method synchronously. Not all hosting scenarios support 40 | * synchronous invocation, so if possible use invokeMethodAsync instead. 41 | * 42 | * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. 43 | * @param args Arguments to pass to the method, each of which must be JSON-serializable. 44 | * @returns The result of the operation. 45 | */ 46 | invokeMethod(methodIdentifier: string, ...args: any[]): T; 47 | /** 48 | * Invokes the specified .NET instance public method asynchronously. 49 | * 50 | * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. 51 | * @param args Arguments to pass to the method, each of which must be JSON-serializable. 52 | * @returns A promise representing the result of the operation. 53 | */ 54 | invokeMethodAsync(methodIdentifier: string, ...args: any[]): Promise; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SkiaSharp.Views.Blazor/wwwroot/types/emscripten/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Emscripten 1.39.16 2 | // Project: https://emscripten.org 3 | // Definitions by: Kensuke Matsuzaki 4 | // Periklis Tsirakidis 5 | // Bumsik Kim 6 | // Louis DeScioli 7 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 8 | // TypeScript Version: 2.2 9 | 10 | /** Other WebAssembly declarations, for compatibility with older versions of Typescript */ 11 | declare namespace WebAssembly { 12 | interface Module {} 13 | } 14 | 15 | declare namespace Emscripten { 16 | interface FileSystemType {} 17 | type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER'; 18 | 19 | type JSType = 'number' | 'string' | 'array' | 'boolean'; 20 | type TypeCompatibleWithC = number | string | any[] | boolean; 21 | 22 | type CIntType = 'i8' | 'i16' | 'i32' | 'i64'; 23 | type CFloatType = 'float' | 'double'; 24 | type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*'; 25 | type CType = CIntType | CFloatType | CPointerType; 26 | 27 | type WebAssemblyImports = Array<{ 28 | name: string; 29 | kind: string; 30 | }>; 31 | 32 | type WebAssemblyExports = Array<{ 33 | module: string; 34 | name: string; 35 | kind: string; 36 | }>; 37 | 38 | interface CCallOpts { 39 | async?: boolean | undefined; 40 | } 41 | } 42 | 43 | interface EmscriptenModule { 44 | print(str: string): void; 45 | printErr(str: string): void; 46 | arguments: string[]; 47 | environment: Emscripten.EnvironmentType; 48 | preInit: Array<{ (): void }>; 49 | preRun: Array<{ (): void }>; 50 | postRun: Array<{ (): void }>; 51 | onAbort: { (what: any): void }; 52 | onRuntimeInitialized: { (): void }; 53 | preinitializedWebGLContext: WebGLRenderingContext; 54 | noInitialRun: boolean; 55 | noExitRuntime: boolean; 56 | logReadFiles: boolean; 57 | filePackagePrefixURL: string; 58 | wasmBinary: ArrayBuffer; 59 | 60 | destroy(object: object): void; 61 | getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer; 62 | instantiateWasm( 63 | imports: Emscripten.WebAssemblyImports, 64 | successCallback: (module: WebAssembly.Module) => void, 65 | ): Emscripten.WebAssemblyExports; 66 | locateFile(url: string, scriptDirectory: string): string; 67 | onCustomMessage(event: MessageEvent): void; 68 | 69 | // USE_TYPED_ARRAYS == 1 70 | HEAP: Int32Array; 71 | IHEAP: Int32Array; 72 | FHEAP: Float64Array; 73 | 74 | // USE_TYPED_ARRAYS == 2 75 | HEAP8: Int8Array; 76 | HEAP16: Int16Array; 77 | HEAP32: Int32Array; 78 | HEAPU8: Uint8Array; 79 | HEAPU16: Uint16Array; 80 | HEAPU32: Uint32Array; 81 | HEAPF32: Float32Array; 82 | HEAPF64: Float64Array; 83 | 84 | TOTAL_STACK: number; 85 | TOTAL_MEMORY: number; 86 | FAST_MEMORY: number; 87 | 88 | addOnPreRun(cb: () => any): void; 89 | addOnInit(cb: () => any): void; 90 | addOnPreMain(cb: () => any): void; 91 | addOnExit(cb: () => any): void; 92 | addOnPostRun(cb: () => any): void; 93 | 94 | preloadedImages: any; 95 | preloadedAudios: any; 96 | 97 | _malloc(size: number): number; 98 | _free(ptr: number): void; 99 | } 100 | 101 | /** 102 | * A factory function is generated when setting the `MODULARIZE` build option 103 | * to `1` in your Emscripten build. It return a Promise that resolves to an 104 | * initialized, ready-to-call `EmscriptenModule` instance. 105 | * 106 | * By default, the factory function will be named `Module`. It's recommended to 107 | * use the `EXPORT_ES6` option, in which the factory function will be the 108 | * default export. If used without `EXPORT_ES6`, the factory function will be a 109 | * global variable. You can rename the variable using the `EXPORT_NAME` build 110 | * option. It's left to you to declare any global variables as needed in your 111 | * application's types. 112 | * @param moduleOverrides Default properties for the initialized module. 113 | */ 114 | type EmscriptenModuleFactory = ( 115 | moduleOverrides?: Partial, 116 | ) => Promise; 117 | 118 | declare namespace FS { 119 | interface Lookup { 120 | path: string; 121 | node: FSNode; 122 | } 123 | 124 | interface FSStream {} 125 | interface FSNode {} 126 | interface ErrnoError {} 127 | 128 | let ignorePermissions: boolean; 129 | let trackingDelegate: any; 130 | let tracking: any; 131 | let genericErrors: any; 132 | 133 | // 134 | // paths 135 | // 136 | function lookupPath(path: string, opts: any): Lookup; 137 | function getPath(node: FSNode): string; 138 | 139 | // 140 | // nodes 141 | // 142 | function isFile(mode: number): boolean; 143 | function isDir(mode: number): boolean; 144 | function isLink(mode: number): boolean; 145 | function isChrdev(mode: number): boolean; 146 | function isBlkdev(mode: number): boolean; 147 | function isFIFO(mode: number): boolean; 148 | function isSocket(mode: number): boolean; 149 | 150 | // 151 | // devices 152 | // 153 | function major(dev: number): number; 154 | function minor(dev: number): number; 155 | function makedev(ma: number, mi: number): number; 156 | function registerDevice(dev: number, ops: any): void; 157 | 158 | // 159 | // core 160 | // 161 | function syncfs(populate: boolean, callback: (e: any) => any): void; 162 | function syncfs(callback: (e: any) => any, populate?: boolean): void; 163 | function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any; 164 | function unmount(mountpoint: string): void; 165 | 166 | function mkdir(path: string, mode?: number): any; 167 | function mkdev(path: string, mode?: number, dev?: number): any; 168 | function symlink(oldpath: string, newpath: string): any; 169 | function rename(old_path: string, new_path: string): void; 170 | function rmdir(path: string): void; 171 | function readdir(path: string): any; 172 | function unlink(path: string): void; 173 | function readlink(path: string): string; 174 | function stat(path: string, dontFollow?: boolean): any; 175 | function lstat(path: string): any; 176 | function chmod(path: string, mode: number, dontFollow?: boolean): void; 177 | function lchmod(path: string, mode: number): void; 178 | function fchmod(fd: number, mode: number): void; 179 | function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void; 180 | function lchown(path: string, uid: number, gid: number): void; 181 | function fchown(fd: number, uid: number, gid: number): void; 182 | function truncate(path: string, len: number): void; 183 | function ftruncate(fd: number, len: number): void; 184 | function utime(path: string, atime: number, mtime: number): void; 185 | function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream; 186 | function close(stream: FSStream): void; 187 | function llseek(stream: FSStream, offset: number, whence: number): any; 188 | function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number; 189 | function write( 190 | stream: FSStream, 191 | buffer: ArrayBufferView, 192 | offset: number, 193 | length: number, 194 | position?: number, 195 | canOwn?: boolean, 196 | ): number; 197 | function allocate(stream: FSStream, offset: number, length: number): void; 198 | function mmap( 199 | stream: FSStream, 200 | buffer: ArrayBufferView, 201 | offset: number, 202 | length: number, 203 | position: number, 204 | prot: number, 205 | flags: number, 206 | ): any; 207 | function ioctl(stream: FSStream, cmd: any, arg: any): any; 208 | function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array; 209 | function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string; 210 | function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array; 211 | function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void; 212 | 213 | // 214 | // module-level FS code 215 | // 216 | function cwd(): string; 217 | function chdir(path: string): void; 218 | function init( 219 | input: null | (() => number | null), 220 | output: null | ((c: number) => any), 221 | error: null | ((c: number) => any), 222 | ): void; 223 | 224 | function createLazyFile( 225 | parent: string | FSNode, 226 | name: string, 227 | url: string, 228 | canRead: boolean, 229 | canWrite: boolean, 230 | ): FSNode; 231 | function createPreloadedFile( 232 | parent: string | FSNode, 233 | name: string, 234 | url: string, 235 | canRead: boolean, 236 | canWrite: boolean, 237 | onload?: () => void, 238 | onerror?: () => void, 239 | dontCreateFile?: boolean, 240 | canOwn?: boolean, 241 | ): void; 242 | function createDataFile( 243 | parent: string | FSNode, 244 | name: string, 245 | data: ArrayBufferView, 246 | canRead: boolean, 247 | canWrite: boolean, 248 | canOwn: boolean, 249 | ): FSNode; 250 | } 251 | 252 | declare var MEMFS: Emscripten.FileSystemType; 253 | declare var NODEFS: Emscripten.FileSystemType; 254 | declare var IDBFS: Emscripten.FileSystemType; 255 | 256 | // Below runtime function/variable declarations are exportable by 257 | // -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge 258 | // EmscriptenModule interface to add runtime functions. 259 | // 260 | // For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" 261 | // You can access ccall() via Module["ccall"]. In this case, you should 262 | // extend EmscriptenModule to pass the compiler check like the following: 263 | // 264 | // interface YourOwnEmscriptenModule extends EmscriptenModule { 265 | // ccall: typeof ccall; 266 | // } 267 | // 268 | // See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function 269 | 270 | declare function ccall( 271 | ident: string, 272 | returnType: Emscripten.JSType | null, 273 | argTypes: Emscripten.JSType[], 274 | args: Emscripten.TypeCompatibleWithC[], 275 | opts?: Emscripten.CCallOpts, 276 | ): any; 277 | declare function cwrap( 278 | ident: string, 279 | returnType: Emscripten.JSType | null, 280 | argTypes: Emscripten.JSType[], 281 | opts?: Emscripten.CCallOpts, 282 | ): (...args: any[]) => any; 283 | 284 | declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void; 285 | declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number; 286 | 287 | declare function allocate( 288 | slab: number[] | ArrayBufferView | number, 289 | types: Emscripten.CType | Emscripten.CType[], 290 | allocator: number, 291 | ptr?: number, 292 | ): number; 293 | 294 | declare function stackAlloc(size: number): number; 295 | declare function stackSave(): number; 296 | declare function stackRestore(ptr: number): void; 297 | 298 | declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string; 299 | declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void; 300 | declare function lengthBytesUTF8(str: string): number; 301 | declare function allocateUTF8(str: string): number; 302 | declare function allocateUTF8OnStack(str: string): number; 303 | declare function UTF16ToString(ptr: number): string; 304 | declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void; 305 | declare function lengthBytesUTF16(str: string): number; 306 | declare function UTF32ToString(ptr: number): string; 307 | declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void; 308 | declare function lengthBytesUTF32(str: string): number; 309 | 310 | declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[]; 311 | declare function intArrayToString(array: number[]): string; 312 | declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void; 313 | declare function writeArrayToMemory(array: number[], buffer: number): void; 314 | declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void; 315 | 316 | declare function addRunDependency(id: any): void; 317 | declare function removeRunDependency(id: any): void; 318 | 319 | declare function addFunction(func: (...args: any[]) => any, signature?: string): number; 320 | declare function removeFunction(funcPtr: number): void; 321 | 322 | declare var ALLOC_NORMAL: number; 323 | declare var ALLOC_STACK: number; 324 | declare var ALLOC_STATIC: number; 325 | declare var ALLOC_DYNAMIC: number; 326 | declare var ALLOC_NONE: number; 327 | -------------------------------------------------------------------------------- /SkiaSharp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattleibow/SkiaSharpBlazorWebAssembly/df21776df70a0df0ddba0313a16d907ace4d4597/SkiaSharp.gif -------------------------------------------------------------------------------- /blazor-native.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31717.149 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharpSample", "blazor-native\SkiaSharpSample.csproj", "{9E2EAE6A-F845-4619-9388-DA08954DC0C6}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Views.Blazor", "SkiaSharp.Views.Blazor\SkiaSharp.Views.Blazor.csproj", "{44901DA3-B5D8-46EA-A7E7-B0A7B1C08AA6}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {9E2EAE6A-F845-4619-9388-DA08954DC0C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {9E2EAE6A-F845-4619-9388-DA08954DC0C6}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {9E2EAE6A-F845-4619-9388-DA08954DC0C6}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {9E2EAE6A-F845-4619-9388-DA08954DC0C6}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {44901DA3-B5D8-46EA-A7E7-B0A7B1C08AA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {44901DA3-B5D8-46EA-A7E7-B0A7B1C08AA6}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {44901DA3-B5D8-46EA-A7E7-B0A7B1C08AA6}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {44901DA3-B5D8-46EA-A7E7-B0A7B1C08AA6}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {E05D6D98-130F-4EF1-9B6B-431C06C35419} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /blazor-native/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /blazor-native/Pages/GPU.razor: -------------------------------------------------------------------------------- 1 | @page "/gpu" 2 | 3 |

GPU (WebGL) Canvas

4 | 5 |

The canvas below is using WebGL. See the great FPS!

6 | 7 |
8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 | @code { 18 | int tickIndex = 0; 19 | long tickSum = 0; 20 | long[] tickList = new long[100]; 21 | long lastTick = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); 22 | 23 | void OnPaintSurface(SKPaintGLSurfaceEventArgs e) 24 | { 25 | // the the canvas and properties 26 | var canvas = e.Surface.Canvas; 27 | 28 | // make sure the canvas is blank 29 | canvas.Clear(SKColors.White); 30 | 31 | using var paint = new SKPaint 32 | { 33 | IsAntialias = true, 34 | StrokeWidth = 10f, 35 | StrokeCap = SKStrokeCap.Round, 36 | TextAlign = SKTextAlign.Center, 37 | TextSize = 24, 38 | }; 39 | 40 | var surfaceSize = e.BackendRenderTarget.Size; 41 | var clockSize = Math.Min(surfaceSize.Width, surfaceSize.Height) * 0.4f; 42 | var center = new SKPoint(surfaceSize.Width / 2f, surfaceSize.Height / 2f); 43 | var now = DateTime.Now; 44 | var fps = GetCurrentFPS(); 45 | 46 | // draw the fps counter 47 | canvas.DrawText($"{fps:0.00}fps", surfaceSize.Width / 2, surfaceSize.Height - 10f, paint); 48 | 49 | // draw the clock 50 | canvas.RotateDegrees(-90f, center.X, center.Y); 51 | 52 | // hours 53 | paint.StrokeWidth = 9f; 54 | canvas.Save(); 55 | canvas.Translate(center); 56 | canvas.RotateDegrees(360f * (now.Hour / 12f)); 57 | canvas.DrawLine(0, 0, clockSize * 0.4f, 0, paint); 58 | canvas.Restore(); 59 | 60 | // minutes 61 | paint.StrokeWidth = 6f; 62 | canvas.Save(); 63 | canvas.Translate(center); 64 | canvas.RotateDegrees(360f * (now.Minute / 60f)); 65 | canvas.DrawLine(0, 0, clockSize * 0.6f, 0, paint); 66 | canvas.Restore(); 67 | 68 | // seconds 69 | paint.StrokeWidth = 3f; 70 | canvas.Save(); 71 | canvas.Translate(center); 72 | canvas.RotateDegrees(360f * ((now.Second * 1000f + now.Millisecond) / 1000f / 60f)); 73 | canvas.DrawLine(0, 0, clockSize * 0.8f, 0, paint); 74 | canvas.Restore(); 75 | 76 | // center 77 | canvas.DrawCircle(center, 20f, paint); 78 | 79 | // border 80 | paint.Style = SKPaintStyle.Stroke; 81 | canvas.DrawCircle(center, clockSize * 0.9f, paint); 82 | } 83 | 84 | double GetCurrentFPS() 85 | { 86 | var newTick = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); 87 | var delta = newTick - lastTick; 88 | lastTick = newTick; 89 | 90 | tickSum -= tickList[tickIndex]; 91 | tickSum += delta; 92 | tickList[tickIndex] = delta; 93 | 94 | if (++tickIndex == tickList.Length) 95 | tickIndex = 0; 96 | 97 | return 1000.0 / ((double)tickSum / tickList.Length); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /blazor-native/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |

Raster (Bitmap) Canvas

4 | 5 |

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

6 | 7 |
8 |
9 |
10 | 11 | 16 | 17 |
18 |
19 |
20 | 21 | @code { 22 | SKCanvasView skiaView = null!; 23 | SKPoint? touchLocation; 24 | [Inject] IJSRuntime JS { get; set; } = null!; 25 | 26 | void OnPaintSurface(SKPaintSurfaceEventArgs e) 27 | { 28 | // the the canvas and properties 29 | var canvas = e.Surface.Canvas; 30 | 31 | // make sure the canvas is blank 32 | canvas.Clear(SKColors.White); 33 | 34 | // decide what the text looks like 35 | using var paint = new SKPaint 36 | { 37 | Color = SKColors.Black, 38 | IsAntialias = true, 39 | Style = SKPaintStyle.Fill, 40 | TextAlign = SKTextAlign.Center, 41 | TextSize = 24 42 | }; 43 | 44 | // adjust the location based on the pointer 45 | var coord = (touchLocation is SKPoint loc) 46 | ? new SKPoint(loc.X, loc.Y) 47 | : new SKPoint(e.Info.Width / 2, (e.Info.Height + paint.TextSize) / 2); 48 | 49 | // draw some text 50 | canvas.DrawText("SkiaSharp", coord, paint); 51 | } 52 | 53 | void OnPointerDown(PointerEventArgs e) 54 | { 55 | touchLocation = new SKPoint((float)e.OffsetX, (float)e.OffsetY); 56 | skiaView.Invalidate(); 57 | } 58 | 59 | void OnPointerMove(PointerEventArgs e) 60 | { 61 | if (touchLocation == null) 62 | return; 63 | 64 | touchLocation = new SKPoint((float)e.OffsetX, (float)e.OffsetY); 65 | skiaView.Invalidate(); 66 | } 67 | 68 | void OnPointerUp(PointerEventArgs e) 69 | { 70 | touchLocation = null; 71 | skiaView.Invalidate(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /blazor-native/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using SkiaSharpSample; 6 | 7 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 8 | 9 | builder.RootComponents.Add("#app"); 10 | 11 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 12 | 13 | await builder.Build().RunAsync(); 14 | -------------------------------------------------------------------------------- /blazor-native/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:13961", 7 | "sslPort": 44319 8 | } 9 | }, 10 | "profiles": { 11 | "SkiaSharpSample": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": "true", 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /blazor-native/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /blazor-native/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | .main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .main > div { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /blazor-native/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  7 | 8 |
9 | 21 |
22 | 23 | @code { 24 | private bool collapseNavMenu = true; 25 | 26 | private string? NavMenuCssClass => 27 | collapseNavMenu ? "collapse" : null; 28 | 29 | private void ToggleNavMenu() 30 | { 31 | collapseNavMenu = !collapseNavMenu; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /blazor-native/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /blazor-native/SkiaSharpSample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | true 7 | 8 | 9 | 10 | 11 | false 12 | -O1 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /blazor-native/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using SkiaSharpSample 10 | @using SkiaSharpSample.Shared 11 | @using SkiaSharp 12 | @using SkiaSharp.Views.Blazor 13 | -------------------------------------------------------------------------------- /blazor-native/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | .content { 18 | padding-top: 1.1rem; 19 | } 20 | 21 | .valid.modified:not([type=checkbox]) { 22 | outline: 1px solid #26b050; 23 | } 24 | 25 | .invalid { 26 | outline: 1px solid red; 27 | } 28 | 29 | .validation-message { 30 | color: red; 31 | } 32 | 33 | #blazor-error-ui { 34 | background: lightyellow; 35 | bottom: 0; 36 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 37 | display: none; 38 | left: 0; 39 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 40 | position: fixed; 41 | width: 100%; 42 | z-index: 1000; 43 | } 44 | 45 | #blazor-error-ui .dismiss { 46 | cursor: pointer; 47 | position: absolute; 48 | right: 0.75rem; 49 | top: 0.5rem; 50 | } 51 | 52 | .canvas-container { 53 | line-height: 1; 54 | } 55 | 56 | .canvas-container canvas { 57 | width: 100%; 58 | height: 300px; 59 | } -------------------------------------------------------------------------------- /blazor-native/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /blazor-native/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattleibow/SkiaSharpBlazorWebAssembly/df21776df70a0df0ddba0313a16d907ace4d4597/blazor-native/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /blazor-native/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattleibow/SkiaSharpBlazorWebAssembly/df21776df70a0df0ddba0313a16d907ace4d4597/blazor-native/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /blazor-native/wwwroot/css/open-iconic/font/fonts/open-iconic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 9 | By P.J. Onori 10 | Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) 11 | 12 | 13 | 14 | 27 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 74 | 76 | 79 | 81 | 84 | 86 | 88 | 91 | 93 | 95 | 98 | 100 | 102 | 104 | 106 | 109 | 112 | 115 | 117 | 121 | 123 | 125 | 127 | 130 | 132 | 134 | 136 | 138 | 141 | 143 | 145 | 147 | 149 | 151 | 153 | 155 | 157 | 159 | 162 | 165 | 167 | 169 | 172 | 174 | 177 | 179 | 181 | 183 | 185 | 189 | 191 | 194 | 196 | 198 | 200 | 202 | 205 | 207 | 209 | 211 | 213 | 215 | 218 | 220 | 222 | 224 | 226 | 228 | 230 | 232 | 234 | 236 | 238 | 241 | 243 | 245 | 247 | 249 | 251 | 253 | 256 | 259 | 261 | 263 | 265 | 267 | 269 | 272 | 274 | 276 | 280 | 282 | 285 | 287 | 289 | 292 | 295 | 298 | 300 | 302 | 304 | 306 | 309 | 312 | 314 | 316 | 318 | 320 | 322 | 324 | 326 | 330 | 334 | 338 | 340 | 343 | 345 | 347 | 349 | 351 | 353 | 355 | 358 | 360 | 363 | 365 | 367 | 369 | 371 | 373 | 375 | 377 | 379 | 381 | 383 | 386 | 388 | 390 | 392 | 394 | 396 | 399 | 401 | 404 | 406 | 408 | 410 | 412 | 414 | 416 | 419 | 421 | 423 | 425 | 428 | 431 | 435 | 438 | 440 | 442 | 444 | 446 | 448 | 451 | 453 | 455 | 457 | 460 | 462 | 464 | 466 | 468 | 471 | 473 | 477 | 479 | 481 | 483 | 486 | 488 | 490 | 492 | 494 | 496 | 499 | 501 | 504 | 506 | 509 | 512 | 515 | 517 | 520 | 522 | 524 | 526 | 529 | 532 | 534 | 536 | 539 | 542 | 543 | 544 | -------------------------------------------------------------------------------- /blazor-native/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattleibow/SkiaSharpBlazorWebAssembly/df21776df70a0df0ddba0313a16d907ace4d4597/blazor-native/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /blazor-native/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattleibow/SkiaSharpBlazorWebAssembly/df21776df70a0df0ddba0313a16d907ace4d4597/blazor-native/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /blazor-native/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattleibow/SkiaSharpBlazorWebAssembly/df21776df70a0df0ddba0313a16d907ace4d4597/blazor-native/wwwroot/favicon.ico -------------------------------------------------------------------------------- /blazor-native/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SkiaSharpSample 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Loading...
16 | 17 |
18 | An unhandled error has occurred. 19 | Reload 20 | 🗙 21 |
22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /blazor-native/wwwroot/js/app.js: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /maui-g.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattleibow/SkiaSharpBlazorWebAssembly/df21776df70a0df0ddba0313a16d907ace4d4597/maui-g.gif -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /skiasharp-blazor-wasm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattleibow/SkiaSharpBlazorWebAssembly/df21776df70a0df0ddba0313a16d907ace4d4597/skiasharp-blazor-wasm.png --------------------------------------------------------------------------------