From e1a4609233a81c8739111fa398851214e50032fe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 10:15:12 +0100 Subject: [PATCH] render avalonia content. --- samples/ControlCatalog/MainView.xaml | 4 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 17 ++- src/Web/Avalonia.Web/AvaloniaView.cs | 61 +++------- .../webapp/modules/avalonia/canvas.ts | 111 +++++++++++++++--- 4 files changed, 129 insertions(+), 64 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 15cdb93896..55dec57b70 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -4,7 +4,7 @@ xmlns:controls="clr-namespace:ControlSamples;assembly=ControlSamples" xmlns:models="clr-namespace:ControlCatalog.Models" xmlns:pages="clr-namespace:ControlCatalog.Pages"> - - + + diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index 2580f52666..13b53f6cc4 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -31,11 +31,11 @@ public partial class AvaloniaRuntime } - public static GLInfo InitialiseGL (JSObject canvas) + public static GLInfo InitialiseGL (JSObject canvas, Action renderFrameCallback) { InterceptGLObject(); - var info = InitGL(canvas, "testCanvas"); + var info = InitGL(canvas, "testCanvas", renderFrameCallback); var glInfo = new GLInfo( @@ -48,14 +48,21 @@ public partial class AvaloniaRuntime return glInfo; } + [JSImport("Canvas.requestAnimationFrame", "avalonia.ts")] + public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); + [JSImport("Canvas.createCanvas", "avalonia.ts")] public static partial JSObject CreateCanvas(JSObject container); - [JSImport("Canvas.Foo", "avalonia.ts")] - public static partial void Foo(JSObject canvas); + [JSImport("Canvas.setCanvasSize", "avalonia.ts")] + public static partial void SetCanvasSize(JSObject canvas, int height, int width); [JSImport("Canvas.initGL", "avalonia.ts")] - private static partial JSObject InitGL(JSObject canvas, string canvasId); + private static partial JSObject InitGL( + JSObject canvas, + string canvasId, + [JSMarshalAs] + Action renderFrameCallback); [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] public static partial bool IsFileApiSupported(); diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 54652bc7e2..2ee4655e40 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -1,12 +1,6 @@ using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; using System.Runtime.InteropServices.JavaScript; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Embedding; using Avalonia.Controls.Platform; using Avalonia.Input.TextInput; @@ -14,7 +8,6 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; using Avalonia.Web.Blazor; using Avalonia.Web.Interop; - using SkiaSharp; using static Avalonia.Web.AvaloniaRuntime; @@ -55,23 +48,17 @@ namespace Avalonia.Web public AvaloniaView() { - Console.WriteLine("In AvaloniaView"); var div = GetElementById("out"); - Console.WriteLine("got div"); - var canvas = AvaloniaRuntime.CreateCanvas(div); + var canvas = CreateCanvas(div); canvas.SetProperty("id", "mycanvas"); _topLevelImpl = new RazorViewTopLevelImpl(this); _topLevel = new EmbeddableControlRoot(_topLevelImpl); - Console.WriteLine("created toplevel"); - _topLevel.Prepare(); - Console.WriteLine("Prepped"); - _topLevel.Renderer.Start(); InputHelper.SubscribeKeyboardEvents( @@ -87,7 +74,7 @@ namespace Avalonia.Web if (_useGL) { - _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas); + _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas, OnRenderFrame); Console.WriteLine("jsglinfo created - init gl"); } else @@ -120,22 +107,26 @@ namespace Avalonia.Web // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); } - // _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - - Threading.Dispatcher.UIThread.Post(async () => - { - //_interop.RequestAnimationFrame(true); + AvaloniaRuntime.SetCanvasSize(canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - // _sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged); - // _dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged); + _topLevelImpl.SetClientSize(_canvasSize, _dpi); - //_sizeWatcher.Start(); - }); + RequestAnimationFrame(canvas, true); } - - internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + private void OnRenderFrame() { - //_currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback); + if (_useGL && (_jsGlInfo == null)) + { + Console.WriteLine("nothing to render"); + return; + } + if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) + { + Console.WriteLine("nothing to render"); + return; + } + + ManualTriggerRenderTimer.Instance.RaiseTick(); } public Control? Content @@ -158,22 +149,6 @@ namespace Avalonia.Web //return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); } - private void OnRenderFrame() - { - if (_useGL && (_jsGlInfo == null)) - { - Console.WriteLine("nothing to render"); - return; - } - if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) - { - Console.WriteLine("nothing to render"); - return; - } - - ManualTriggerRenderTimer.Instance.RaiseTick(); - } - private void ForceBlit() { // Note: this is technically a hack, but it's a kinda unique use case when diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 9698e72f82..a6e3c5acb4 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -8,16 +8,16 @@ type SKGLViewInfo = { depth: number; } -type SKHtmlCanvasElement = { - SKHtmlCanvas: Canvas | undefined +type CanvasElement = { + Canvas: Canvas | undefined } & HTMLCanvasElement export class Canvas { static elements: Map; - //htmlCanvas: HTMLCanvasElement; + htmlCanvas: HTMLCanvasElement; glInfo?: SKGLViewInfo; - //renderFrameCallback: DotNet.DotNetObject; + renderFrameCallback: () => void; renderLoopEnabled: boolean = false; renderLoopRequest: number = 0; newWidth?: number; @@ -31,17 +31,17 @@ export class Canvas { return canvas; } - public static initGL(element: HTMLCanvasElement, elementId: string): SKGLViewInfo | null { + public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { console.log("inside initGL"); - var view = Canvas.init(true, element, elementId); + var view = Canvas.init(true, element, elementId, renderFrameCallback); if (!view || !view.glInfo) return null; return view.glInfo; } - static init(useGL: boolean, element: HTMLCanvasElement, elementId: string): Canvas | null { - var htmlCanvas = element as SKHtmlCanvasElement; + static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null { + var htmlCanvas = element as CanvasElement; if (!htmlCanvas) { console.error(`No canvas element was provided.`); return null; @@ -51,17 +51,16 @@ export class Canvas { Canvas.elements = new Map(); Canvas.elements.set(elementId, element); - const view = new Canvas(useGL, element); + const view = new Canvas(useGL, element, renderFrameCallback); - htmlCanvas.SKHtmlCanvas = view; + htmlCanvas.Canvas = view; return view; } - - public constructor(useGL: boolean, element: HTMLCanvasElement) { - //this.htmlCanvas = element; - //this.renderFrameCallback = callback; + public constructor(useGL: boolean, element: HTMLCanvasElement, renderFrameCallback: () => void) { + this.htmlCanvas = element; + this.renderFrameCallback = renderFrameCallback; if (useGL) { const ctx = Canvas.createWebGLContext(element); @@ -79,6 +78,7 @@ export class Canvas { // read values const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); + this.glInfo = { context: ctx, fboId: fbo ? fbo.id : 0, @@ -89,6 +89,88 @@ export class Canvas { } } + public setEnableRenderLoop(enable: boolean) { + this.renderLoopEnabled = enable; + + // either start the new frame or cancel the existing one + if (enable) { + //console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); + this.requestAnimationFrame(); + } else if (this.renderLoopRequest !== 0) { + window.cancelAnimationFrame(this.renderLoopRequest); + this.renderLoopRequest = 0; + } + } + + public requestAnimationFrame(renderLoop?: boolean) { + // optionally update the render loop + if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) + this.setEnableRenderLoop(renderLoop); + + // skip because we have a render loop + if (this.renderLoopRequest !== 0) + return; + + // add the draw to the next frame + this.renderLoopRequest = window.requestAnimationFrame(() => { + if (this.glInfo) { + var GL = (globalThis as any).AvaloniaGL; + // make current + GL.makeContextCurrent(this.glInfo.context); + } + + if (this.htmlCanvas.width != this.newWidth) { + this.htmlCanvas.width = this.newWidth || 0; + } + + if (this.htmlCanvas.height != this.newHeight) { + this.htmlCanvas.height = this.newHeight || 0; + } + + this.renderFrameCallback(); + this.renderLoopRequest = 0; + + // we may want to draw the next frame + if (this.renderLoopEnabled) + this.requestAnimationFrame(); + }); + } + + public setCanvasSize(width: number, height: number) { + this.newWidth = width; + this.newHeight = height; + + if (this.htmlCanvas.width != this.newWidth) { + this.htmlCanvas.width = this.newWidth; + } + + if (this.htmlCanvas.height != this.newHeight) { + this.htmlCanvas.height = this.newHeight; + } + + if (this.glInfo) { + var GL = (globalThis as any).AvaloniaGL; + // make current + GL.makeContextCurrent(this.glInfo.context); + } + } + + public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) { + const htmlCanvas = element as CanvasElement; + if (!htmlCanvas || !htmlCanvas.Canvas) + return; + + htmlCanvas.Canvas.setCanvasSize(width, height); + } + + public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) { + const htmlCanvas = element as CanvasElement; + if (!htmlCanvas || !htmlCanvas.Canvas) + return; + + htmlCanvas.Canvas.requestAnimationFrame(renderLoop); + } + static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { const contextAttributes = { alpha: 1, @@ -109,6 +191,7 @@ export class Canvas { var GL = (globalThis as any).AvaloniaGL; let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); + if (!ctx && contextAttributes.majorVersion > 1) { console.warn('Falling back to WebGL 1.0'); contextAttributes.majorVersion = 1;