From 4b1b381f0dc94cb834691c8499a75d390393ec8d Mon Sep 17 00:00:00 2001 From: gabenovig Date: Thu, 4 Sep 2025 13:21:31 -0400 Subject: [PATCH 1/3] feat: add webglContextAttributes prop for web platform Adds support for configuring WebGL context attributes on web platform, particularly useful for enabling preserveDrawingBuffer for canvas capture scenarios like screenshots or image exports. - Add webglContextAttributes prop to CanvasProps and SkiaBaseViewProps - Implement context attribute conversion (boolean to 0/1) in SkiaBaseWebView - Pass attributes to CanvasKit.MakeWebGLCanvasSurface - Web-only feature, ignored on native platforms Example usage: {/* Canvas content */} --- packages/skia/src/renderer/Canvas.tsx | 22 +++++++++++++ .../SkiaPictureViewNativeComponent.web.ts | 17 ++++++++++ packages/skia/src/views/SkiaBaseWebView.tsx | 31 ++++++++++++++++++- packages/skia/src/views/types.ts | 23 ++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) diff --git a/packages/skia/src/renderer/Canvas.tsx b/packages/skia/src/renderer/Canvas.tsx index 7ba53a5efa..5e5972b522 100644 --- a/packages/skia/src/renderer/Canvas.tsx +++ b/packages/skia/src/renderer/Canvas.tsx @@ -56,6 +56,28 @@ export interface CanvasProps extends Omit { onSize?: SharedValue; colorSpace?: "p3" | "srgb"; ref?: React.Ref; + /** + * WebGL context attributes for web platform. + * Allows configuration of the WebGL rendering context. + * Only applicable when running on web platform. + * + * Note: Boolean values will be automatically converted to 0/1 for CanvasKit compatibility. + */ + webglContextAttributes?: { + alpha?: boolean; + depth?: boolean; + stencil?: boolean; + antialias?: boolean; + premultipliedAlpha?: boolean; + preserveDrawingBuffer?: boolean; + preferLowPowerToHighPerformance?: boolean; + failIfMajorPerformanceCaveat?: boolean; + enableExtensionsByDefault?: boolean; + explicitSwapControl?: boolean; + renderViaOffscreenBackBuffer?: boolean; + majorVersion?: number; + minorVersion?: number; + }; } export const Canvas = ({ diff --git a/packages/skia/src/specs/SkiaPictureViewNativeComponent.web.ts b/packages/skia/src/specs/SkiaPictureViewNativeComponent.web.ts index fd74cc2739..a1782053e7 100644 --- a/packages/skia/src/specs/SkiaPictureViewNativeComponent.web.ts +++ b/packages/skia/src/specs/SkiaPictureViewNativeComponent.web.ts @@ -7,6 +7,21 @@ export interface NativeProps extends ViewProps { debug?: boolean; opaque?: boolean; nativeID: string; + webglContextAttributes?: { + alpha?: boolean; + depth?: boolean; + stencil?: boolean; + antialias?: boolean; + premultipliedAlpha?: boolean; + preserveDrawingBuffer?: boolean; + preferLowPowerToHighPerformance?: boolean; + failIfMajorPerformanceCaveat?: boolean; + enableExtensionsByDefault?: boolean; + explicitSwapControl?: boolean; + renderViaOffscreenBackBuffer?: boolean; + majorVersion?: number; + minorVersion?: number; + }; } const SkiaPictureViewNativeComponent = ({ @@ -14,6 +29,7 @@ const SkiaPictureViewNativeComponent = ({ debug, opaque, onLayout, + webglContextAttributes, ...viewProps }: NativeProps) => { return createElement(SkiaPictureView, { @@ -21,6 +37,7 @@ const SkiaPictureViewNativeComponent = ({ debug, opaque, onLayout, + webglContextAttributes, ...viewProps, }); }; diff --git a/packages/skia/src/views/SkiaBaseWebView.tsx b/packages/skia/src/views/SkiaBaseWebView.tsx index d7f16d5ef2..f288acc3a6 100644 --- a/packages/skia/src/views/SkiaBaseWebView.tsx +++ b/packages/skia/src/views/SkiaBaseWebView.tsx @@ -41,7 +41,36 @@ export abstract class SkiaBaseWebView< this.height = canvas.clientHeight; canvas.width = this.width * pd; canvas.height = this.height * pd; - const surface = CanvasKit.MakeWebGLCanvasSurface(canvas); + + // Convert boolean webglContextAttributes to numbers for CanvasKit + let contextAttributes: any = undefined; + if (this.props.webglContextAttributes) { + contextAttributes = {}; + const attrs = this.props.webglContextAttributes; + + // Convert boolean values to 0/1 for CanvasKit compatibility + if (attrs.alpha !== undefined) contextAttributes.alpha = attrs.alpha ? 1 : 0; + if (attrs.depth !== undefined) contextAttributes.depth = attrs.depth ? 1 : 0; + if (attrs.stencil !== undefined) contextAttributes.stencil = attrs.stencil ? 1 : 0; + if (attrs.antialias !== undefined) contextAttributes.antialias = attrs.antialias ? 1 : 0; + if (attrs.premultipliedAlpha !== undefined) contextAttributes.premultipliedAlpha = attrs.premultipliedAlpha ? 1 : 0; + if (attrs.preserveDrawingBuffer !== undefined) contextAttributes.preserveDrawingBuffer = attrs.preserveDrawingBuffer ? 1 : 0; + if (attrs.preferLowPowerToHighPerformance !== undefined) contextAttributes.preferLowPowerToHighPerformance = attrs.preferLowPowerToHighPerformance ? 1 : 0; + if (attrs.failIfMajorPerformanceCaveat !== undefined) contextAttributes.failIfMajorPerformanceCaveat = attrs.failIfMajorPerformanceCaveat ? 1 : 0; + if (attrs.enableExtensionsByDefault !== undefined) contextAttributes.enableExtensionsByDefault = attrs.enableExtensionsByDefault ? 1 : 0; + if (attrs.explicitSwapControl !== undefined) contextAttributes.explicitSwapControl = attrs.explicitSwapControl ? 1 : 0; + if (attrs.renderViaOffscreenBackBuffer !== undefined) contextAttributes.renderViaOffscreenBackBuffer = attrs.renderViaOffscreenBackBuffer ? 1 : 0; + + // Pass through numeric values as-is + if (attrs.majorVersion !== undefined) contextAttributes.majorVersion = attrs.majorVersion; + if (attrs.minorVersion !== undefined) contextAttributes.minorVersion = attrs.minorVersion; + } + + const surface = CanvasKit.MakeWebGLCanvasSurface( + canvas, + undefined, // colorSpace - using undefined to maintain default + contextAttributes + ); const ctx = canvas.getContext("webgl2"); if (ctx) { ctx.drawingBufferColorSpace = "display-p3"; diff --git a/packages/skia/src/views/types.ts b/packages/skia/src/views/types.ts index 224024a3f6..2cdf29561c 100644 --- a/packages/skia/src/views/types.ts +++ b/packages/skia/src/views/types.ts @@ -31,6 +31,29 @@ export interface SkiaBaseViewProps extends ViewProps { onSize?: SharedValue; opaque?: boolean; + + /** + * WebGL context attributes for web platform. + * Allows configuration of the WebGL rendering context. + * Only applicable when running on web platform. + * + * Note: Boolean values will be automatically converted to 0/1 for CanvasKit compatibility. + */ + webglContextAttributes?: { + alpha?: boolean; + depth?: boolean; + stencil?: boolean; + antialias?: boolean; + premultipliedAlpha?: boolean; + preserveDrawingBuffer?: boolean; + preferLowPowerToHighPerformance?: boolean; + failIfMajorPerformanceCaveat?: boolean; + enableExtensionsByDefault?: boolean; + explicitSwapControl?: boolean; + renderViaOffscreenBackBuffer?: boolean; + majorVersion?: number; + minorVersion?: number; + }; } export interface SkiaPictureViewNativeProps extends SkiaBaseViewProps { From c8f5f33a3740d0f7040dc68d1c076d82cded0a3e Mon Sep 17 00:00:00 2001 From: gabenovig Date: Thu, 4 Sep 2025 16:04:39 -0400 Subject: [PATCH 2/3] simplify conversion from bool to numerical value -update our context attribute types to be specific -respect linting, no nested ternaries --- packages/skia/src/views/SkiaBaseWebView.tsx | 37 +++++++++------------ 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/skia/src/views/SkiaBaseWebView.tsx b/packages/skia/src/views/SkiaBaseWebView.tsx index f288acc3a6..46cf74a86d 100644 --- a/packages/skia/src/views/SkiaBaseWebView.tsx +++ b/packages/skia/src/views/SkiaBaseWebView.tsx @@ -41,31 +41,26 @@ export abstract class SkiaBaseWebView< this.height = canvas.clientHeight; canvas.width = this.width * pd; canvas.height = this.height * pd; - - // Convert boolean webglContextAttributes to numbers for CanvasKit - let contextAttributes: any = undefined; + + // Convert WebGL context attributes from boolean to numeric for CanvasKit + let contextAttributes: Record | undefined; if (this.props.webglContextAttributes) { contextAttributes = {}; const attrs = this.props.webglContextAttributes; - - // Convert boolean values to 0/1 for CanvasKit compatibility - if (attrs.alpha !== undefined) contextAttributes.alpha = attrs.alpha ? 1 : 0; - if (attrs.depth !== undefined) contextAttributes.depth = attrs.depth ? 1 : 0; - if (attrs.stencil !== undefined) contextAttributes.stencil = attrs.stencil ? 1 : 0; - if (attrs.antialias !== undefined) contextAttributes.antialias = attrs.antialias ? 1 : 0; - if (attrs.premultipliedAlpha !== undefined) contextAttributes.premultipliedAlpha = attrs.premultipliedAlpha ? 1 : 0; - if (attrs.preserveDrawingBuffer !== undefined) contextAttributes.preserveDrawingBuffer = attrs.preserveDrawingBuffer ? 1 : 0; - if (attrs.preferLowPowerToHighPerformance !== undefined) contextAttributes.preferLowPowerToHighPerformance = attrs.preferLowPowerToHighPerformance ? 1 : 0; - if (attrs.failIfMajorPerformanceCaveat !== undefined) contextAttributes.failIfMajorPerformanceCaveat = attrs.failIfMajorPerformanceCaveat ? 1 : 0; - if (attrs.enableExtensionsByDefault !== undefined) contextAttributes.enableExtensionsByDefault = attrs.enableExtensionsByDefault ? 1 : 0; - if (attrs.explicitSwapControl !== undefined) contextAttributes.explicitSwapControl = attrs.explicitSwapControl ? 1 : 0; - if (attrs.renderViaOffscreenBackBuffer !== undefined) contextAttributes.renderViaOffscreenBackBuffer = attrs.renderViaOffscreenBackBuffer ? 1 : 0; - - // Pass through numeric values as-is - if (attrs.majorVersion !== undefined) contextAttributes.majorVersion = attrs.majorVersion; - if (attrs.minorVersion !== undefined) contextAttributes.minorVersion = attrs.minorVersion; + + // Iterate through all attributes and convert booleans to 0/1 + for (const [key, value] of Object.entries(attrs)) { + if (value !== undefined) { + // Convert boolean to number (0/1), pass through numbers as-is + if (typeof value === "boolean") { + contextAttributes[key] = value ? 1 : 0; + } else { + contextAttributes[key] = value; + } + } + } } - + const surface = CanvasKit.MakeWebGLCanvasSurface( canvas, undefined, // colorSpace - using undefined to maintain default From b7c5ce90565a6239236dca84c52f8653fbc0dbb7 Mon Sep 17 00:00:00 2001 From: gabenovig Date: Fri, 5 Sep 2025 09:25:23 -0400 Subject: [PATCH 3/3] use WebGLOptions directly -don't cast to bool --- packages/skia/src/renderer/Canvas.tsx | 20 +++-------------- .../SkiaPictureViewNativeComponent.web.ts | 17 ++------------ packages/skia/src/views/SkiaBaseWebView.tsx | 21 +----------------- packages/skia/src/views/types.ts | 22 ++++--------------- 4 files changed, 10 insertions(+), 70 deletions(-) diff --git a/packages/skia/src/renderer/Canvas.tsx b/packages/skia/src/renderer/Canvas.tsx index 5e5972b522..447bb687d7 100644 --- a/packages/skia/src/renderer/Canvas.tsx +++ b/packages/skia/src/renderer/Canvas.tsx @@ -14,6 +14,7 @@ import type { ViewProps, } from "react-native"; import { type SharedValue } from "react-native-reanimated"; +import type { WebGLOptions } from "canvaskit-wasm"; import { SkiaViewNativeId } from "../views/SkiaViewNativeId"; import SkiaPictureViewNativeComponent from "../specs/SkiaPictureViewNativeComponent"; @@ -60,24 +61,9 @@ export interface CanvasProps extends Omit { * WebGL context attributes for web platform. * Allows configuration of the WebGL rendering context. * Only applicable when running on web platform. - * - * Note: Boolean values will be automatically converted to 0/1 for CanvasKit compatibility. + * Uses CanvasKit's WebGLOptions type directly - all values must be numeric (0 or 1 for boolean flags). */ - webglContextAttributes?: { - alpha?: boolean; - depth?: boolean; - stencil?: boolean; - antialias?: boolean; - premultipliedAlpha?: boolean; - preserveDrawingBuffer?: boolean; - preferLowPowerToHighPerformance?: boolean; - failIfMajorPerformanceCaveat?: boolean; - enableExtensionsByDefault?: boolean; - explicitSwapControl?: boolean; - renderViaOffscreenBackBuffer?: boolean; - majorVersion?: number; - minorVersion?: number; - }; + webglContextAttributes?: WebGLOptions; } export const Canvas = ({ diff --git a/packages/skia/src/specs/SkiaPictureViewNativeComponent.web.ts b/packages/skia/src/specs/SkiaPictureViewNativeComponent.web.ts index a1782053e7..6e31797569 100644 --- a/packages/skia/src/specs/SkiaPictureViewNativeComponent.web.ts +++ b/packages/skia/src/specs/SkiaPictureViewNativeComponent.web.ts @@ -1,5 +1,6 @@ import type { ViewProps } from "react-native"; import { createElement } from "react"; +import type { WebGLOptions } from "canvaskit-wasm"; import { SkiaPictureView } from "../views/SkiaPictureView.web"; @@ -7,21 +8,7 @@ export interface NativeProps extends ViewProps { debug?: boolean; opaque?: boolean; nativeID: string; - webglContextAttributes?: { - alpha?: boolean; - depth?: boolean; - stencil?: boolean; - antialias?: boolean; - premultipliedAlpha?: boolean; - preserveDrawingBuffer?: boolean; - preferLowPowerToHighPerformance?: boolean; - failIfMajorPerformanceCaveat?: boolean; - enableExtensionsByDefault?: boolean; - explicitSwapControl?: boolean; - renderViaOffscreenBackBuffer?: boolean; - majorVersion?: number; - minorVersion?: number; - }; + webglContextAttributes?: WebGLOptions; } const SkiaPictureViewNativeComponent = ({ diff --git a/packages/skia/src/views/SkiaBaseWebView.tsx b/packages/skia/src/views/SkiaBaseWebView.tsx index 46cf74a86d..49b8a84ec0 100644 --- a/packages/skia/src/views/SkiaBaseWebView.tsx +++ b/packages/skia/src/views/SkiaBaseWebView.tsx @@ -42,29 +42,10 @@ export abstract class SkiaBaseWebView< canvas.width = this.width * pd; canvas.height = this.height * pd; - // Convert WebGL context attributes from boolean to numeric for CanvasKit - let contextAttributes: Record | undefined; - if (this.props.webglContextAttributes) { - contextAttributes = {}; - const attrs = this.props.webglContextAttributes; - - // Iterate through all attributes and convert booleans to 0/1 - for (const [key, value] of Object.entries(attrs)) { - if (value !== undefined) { - // Convert boolean to number (0/1), pass through numbers as-is - if (typeof value === "boolean") { - contextAttributes[key] = value ? 1 : 0; - } else { - contextAttributes[key] = value; - } - } - } - } - const surface = CanvasKit.MakeWebGLCanvasSurface( canvas, undefined, // colorSpace - using undefined to maintain default - contextAttributes + this.props.webglContextAttributes // undefined if not explicitly provided ); const ctx = canvas.getContext("webgl2"); if (ctx) { diff --git a/packages/skia/src/views/types.ts b/packages/skia/src/views/types.ts index 2cdf29561c..c1dce2a249 100644 --- a/packages/skia/src/views/types.ts +++ b/packages/skia/src/views/types.ts @@ -1,5 +1,6 @@ import type { ViewProps } from "react-native"; import type { SharedValue } from "react-native-reanimated"; +import type { WebGLOptions } from "canvaskit-wasm"; import type { Node } from "../dom/types"; import type { SkImage, SkPicture, SkRect, SkSize } from "../skia/types"; @@ -31,29 +32,14 @@ export interface SkiaBaseViewProps extends ViewProps { onSize?: SharedValue; opaque?: boolean; - + /** * WebGL context attributes for web platform. * Allows configuration of the WebGL rendering context. * Only applicable when running on web platform. - * - * Note: Boolean values will be automatically converted to 0/1 for CanvasKit compatibility. + * Uses CanvasKit's WebGLOptions type directly - all values must be numeric (0 or 1 for boolean flags). */ - webglContextAttributes?: { - alpha?: boolean; - depth?: boolean; - stencil?: boolean; - antialias?: boolean; - premultipliedAlpha?: boolean; - preserveDrawingBuffer?: boolean; - preferLowPowerToHighPerformance?: boolean; - failIfMajorPerformanceCaveat?: boolean; - enableExtensionsByDefault?: boolean; - explicitSwapControl?: boolean; - renderViaOffscreenBackBuffer?: boolean; - majorVersion?: number; - minorVersion?: number; - }; + webglContextAttributes?: WebGLOptions; } export interface SkiaPictureViewNativeProps extends SkiaBaseViewProps {