source/SkiaSharp.Views/SkiaSharp.Views.Blazor/wwwroot/SKHtmlCanvas.ts (178 lines of code) (raw):

// aliases for emscripten declare let GL: any; declare let GLctx: WebGLRenderingContext; declare let Module: EmscriptenModule; // container for gl info type SKGLViewInfo = { context: WebGLRenderingContext | WebGL2RenderingContext | undefined; fboId: number; stencil: number; sample: number; depth: number; } // alias for a potential skia html canvas type SKHtmlCanvasElement = { SKHtmlCanvas: SKHtmlCanvas } & HTMLCanvasElement export class SKHtmlCanvas { static elements: Map<string, HTMLCanvasElement>; htmlCanvas: HTMLCanvasElement; glInfo: SKGLViewInfo; renderFrameCallback: DotNet.DotNetObjectReference; renderLoopEnabled: boolean = false; renderLoopRequest: number = 0; public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo { var view = SKHtmlCanvas.init(true, element, elementId, callback); if (!view || !view.glInfo) return null; return view.glInfo; } public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): boolean { var view = SKHtmlCanvas.init(false, element, elementId, callback); if (!view) return false; return true; } static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKHtmlCanvas { var htmlCanvas = element as SKHtmlCanvasElement; if (!htmlCanvas) { console.error(`No canvas element was provided.`); return null; } if (!SKHtmlCanvas.elements) SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>(); SKHtmlCanvas.elements[elementId] = element; const view = new SKHtmlCanvas(useGL, element, callback); htmlCanvas.SKHtmlCanvas = view; return view; } public static deinit(elementId: string) { if (!elementId) return; const element = SKHtmlCanvas.elements[elementId]; SKHtmlCanvas.elements.delete(elementId); const htmlCanvas = element as SKHtmlCanvasElement; if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) return; htmlCanvas.SKHtmlCanvas.deinit(); htmlCanvas.SKHtmlCanvas = undefined; } public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean, width?: number, height?: number) { const htmlCanvas = element as SKHtmlCanvasElement; if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) return; htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop, width, height); } public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) { const htmlCanvas = element as SKHtmlCanvasElement; if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) return; htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable); } public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) { const htmlCanvas = element as SKHtmlCanvasElement; if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) return; htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height); } public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) { this.htmlCanvas = element; this.renderFrameCallback = callback; if (useGL) { const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas); if (!ctx) { console.error(`Failed to create WebGL context: err ${ctx}`); return null; } // make current const GL = SKHtmlCanvas.getGL(); GL.makeContextCurrent(ctx); // read values const GLctx = SKHtmlCanvas.getGLctx(); const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); this.glInfo = { context: ctx, fboId: fbo ? fbo.id : 0, stencil: GLctx.getParameter(GLctx.STENCIL_BITS), sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES) depth: GLctx.getParameter(GLctx.DEPTH_BITS), }; } } public deinit() { this.setEnableRenderLoop(false); } public requestAnimationFrame(renderLoop?: boolean, width?: number, height?: number) { // optionally update the render loop if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) this.setEnableRenderLoop(renderLoop); // make sure the canvas is scaled correctly for the drawing if (width && height) { this.htmlCanvas.width = width; this.htmlCanvas.height = height; } // skip because we have a render loop if (this.renderLoopRequest !== 0) return; // add the draw to the next frame this.renderLoopRequest = window.requestAnimationFrame(() => { if (this.glInfo) { // make current const GL = SKHtmlCanvas.getGL(); GL.makeContextCurrent(this.glInfo.context); } this.renderFrameCallback.invokeMethod('Invoke'); this.renderLoopRequest = 0; // we may want to draw the next frame if (this.renderLoopEnabled) this.requestAnimationFrame(); }); } public setEnableRenderLoop(enable: boolean) { this.renderLoopEnabled = enable; // either start the new frame or cancel the existing one if (enable) { //console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); this.requestAnimationFrame(); } else if (this.renderLoopRequest !== 0) { window.cancelAnimationFrame(this.renderLoopRequest); this.renderLoopRequest = 0; } } public putImageData(pData: number, width: number, height: number): boolean { if (this.glInfo || !pData || width <= 0 || width <= 0) return false; var ctx = this.htmlCanvas.getContext('2d'); if (!ctx) { console.error(`Failed to obtain 2D canvas context.`); return false; } // make sure the canvas is scaled correctly for the drawing this.htmlCanvas.width = width; this.htmlCanvas.height = height; // set the canvas to be the bytes var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4); var imageData = new ImageData(buffer, width, height); ctx.putImageData(imageData, 0, 0); return true; } static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { const contextAttributes = { alpha: 1, depth: 1, stencil: 8, antialias: 1, premultipliedAlpha: 1, preserveDrawingBuffer: 0, preferLowPowerToHighPerformance: 0, failIfMajorPerformanceCaveat: 0, majorVersion: 2, minorVersion: 0, enableExtensionsByDefault: 1, explicitSwapControl: 0, renderViaOffscreenBackBuffer: 0, }; const GL = SKHtmlCanvas.getGL(); let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); if (!ctx && contextAttributes.majorVersion > 1) { console.warn('Falling back to WebGL 1.0'); contextAttributes.majorVersion = 1; contextAttributes.minorVersion = 0; ctx = GL.createContext(htmlCanvas, contextAttributes); } return ctx; } static getGL(): any { return (globalThis as any).SkiaSharpGL || (Module as any).GL || GL; } static getGLctx(): WebGLRenderingContext { const GL = SKHtmlCanvas.getGL(); return GL.currentContext && GL.currentContext.GLctx || GLctx; } }