// CanvasKit-backed stand-in for `@shopify/react-native-skia`. // // The web build aliases the package name to THIS module (vite `resolve.alias`, // tsconfig `paths`), so every renderer we share with the app — `frameUtils.ts`, // `skiaImage.ts`, the ported `exportEngine.ts` — runs unchanged on CanvasKit. // // W0 probe results that shaped this file (measured in Node, canvaskit-wasm 0.42): // - the default build CANNOT encode JPEG (`encodeToBytes(JPEG)` → null), the // `canvaskit-wasm/full` build can → always import the full build (+0.9MB wasm); // - `Skia.ImageFilter.MakeMatrixConvolution` DOES NOT EXIST in CanvasKit → the // two convolution steps (CLARITY, output sharpen) are re-implemented as an // SkSL runtime effect, see convolve.ts; // - `Surface.MakeOffscreen` does not exist either → raster `MakeSurface` only // (workers have no GPU context, which is fine: the render is CPU anyway); // - `Skia.Font`/`Typeface.MakeFreeTypeFaceFromData`/`Canvas.drawText` all work, // and `assets/NotoEmoji-GPS.ttf` really paints 📍/📷 (1731 lit pixels in the // probe) → GPS watermark needs no Canvas2D fallback. import CanvasKitInit from 'canvaskit-wasm/full'; // Live binding: modules import `{ Skia }` once, `initSkia()` fills it in before // the first render. ESM live bindings make the late assignment visible. export let Skia: any = null; export let CK: any = null; export type SkCanvas = any; export type SkPaint = any; export type SkImage = any; export type SkShader = any; export type SkSurface = any; export type SkTypeface = any; export type SkFont = any; // CanvasKit's EMBind objects are released with `delete()`; the RN Skia API (and // therefore the shared renderers) calls `dispose()`. const DISPOSABLE = [ 'Image', 'Surface', 'Paint', 'Shader', 'ImageFilter', 'ColorFilter', 'Typeface', 'Font', 'RuntimeEffect', 'Path', 'Canvas', 'BlendMode', ]; function patchPrototypes(canvasKit: any): void { for (const name of DISPOSABLE) { const proto = canvasKit[name]?.prototype; if (proto && typeof proto.delete === 'function' && typeof proto.dispose !== 'function') { proto.dispose = function dispose() { this.delete(); }; } } // frameUtils draws strokes with the numeric RN style (0 = fill, 1 = stroke). const setStyle = canvasKit.Paint.prototype.setStyle; canvasKit.Paint.prototype.setStyle = function (style: any) { return setStyle.call( this, typeof style === 'number' ? (style === 1 ? canvasKit.PaintStyle.Stroke : canvasKit.PaintStyle.Fill) : style ); }; } export interface InitSkiaOptions { // Browser: path of canvaskit.wasm (Vite serves it as an asset). Node: omitted. locateFile?: (file: string) => string; } function rasterSurface(w: number, h: number) { return CK.MakeSurface(w, h); } // ponytail: one OffscreenCanvas per surface. The pipeline holds at most ~3 at // once, far under the browser's ~16 live WebGL contexts; add a size-keyed pool // if batch rendering ever keeps more alive. function gpuSurface(w: number, h: number) { if (typeof OffscreenCanvas === 'undefined') return null; try { const canvas = new OffscreenCanvas(w, h); const surf = CK.MakeWebGLCanvasSurface(canvas); if (!surf) return null; const dispose = surf.dispose.bind(surf); surf.dispose = () => { try { dispose(); } finally { canvas.width = 0; canvas.height = 0; } }; return surf; } catch { return null; // no GL context available — caller falls back to raster } } export async function initSkia(options: InitSkiaOptions = {}): Promise { if (Skia) return Skia; const canvasKit = await (CanvasKitInit as any)( options.locateFile ? { locateFile: options.locateFile } : undefined ); CK = canvasKit; patchPrototypes(canvasKit); Skia = { // --- geometry / color --------------------------------------------------- XYWHRect: (x: number, y: number, w: number, h: number) => canvasKit.XYWHRect(x, y, w, h), Point: (x: number, y: number) => [x, y], Color: (value: string) => value === 'transparent' ? canvasKit.Color(0, 0, 0, 0) : canvasKit.parseColorString(value), Color4f: (r: number, g: number, b: number, a: number) => canvasKit.Color4f(r, g, b, a), // --- factories ---------------------------------------------------------- Paint: () => new canvasKit.Paint(), Font: (typeface: any, size: number) => new canvasKit.Font(typeface, size), Data: { // CanvasKit exposes no Data class; callers pass bytes straight through. fromBytes: (bytes: Uint8Array) => bytes, }, Image: { MakeImageFromEncoded: (data: Uint8Array) => canvasKit.MakeImageFromEncoded( data instanceof Uint8Array ? data : new Uint8Array(data as ArrayBuffer) ), }, Typeface: { MakeFreeTypeFaceFromData: (data: ArrayBuffer) => canvasKit.Typeface.MakeFreeTypeFaceFromData(data), }, ColorFilter: { MakeMatrix: (m: number[]) => canvasKit.ColorFilter.MakeMatrix(m), MakeCompose: (outer: any, inner: any) => canvasKit.ColorFilter.MakeCompose(outer, inner), }, ImageFilter: { MakeBlur: (sx: number, sy: number, tile: any, input: any) => canvasKit.ImageFilter.MakeBlur(sx, sy, tile, input ?? null), MakeCompose: (outer: any, inner: any) => canvasKit.ImageFilter.MakeCompose(outer ?? null, inner ?? null), MakeColorFilter: (cf: any, input: any) => canvasKit.ImageFilter.MakeColorFilter(cf, input ?? null), }, Shader: { MakeRadialGradient: ( center: number[], radius: number, colors: any[], pos: number[], mode: any ) => canvasKit.Shader.MakeRadialGradient(center, radius, colors, pos, mode), }, RuntimeEffect: { Make: (sksl: string) => canvasKit.RuntimeEffect.Make(sksl), }, Surface: { // A WebGL-backed surface is not a nicety here, it is the difference // between usable and unusable: the same 12MP pipeline that finishes in // well under a second on a GL surface took 3.2s for ONE tone pass and // 13.7s for the CLARITY tap in the Node CPU raster benchmark. Raster is // the fallback for a context-less environment (Node spikes, tests). Make: (w: number, h: number) => rasterSurface(w, h), MakeOffscreen: (w: number, h: number) => gpuSurface(w, h) ?? rasterSurface(w, h), }, FontMgr: { System: () => fontMgr, }, // --- enums -------------------------------------------------------------- BlendMode: canvasKit.BlendMode, TileMode: canvasKit.TileMode, FilterMode: canvasKit.FilterMode, MipmapMode: canvasKit.MipmapMode, FontStyle: canvasKit.FontStyle, ImageFormat: canvasKit.ImageFormat, ColorType: canvasKit.ColorType, ColorSpace: canvasKit.ColorSpace, AlphaType: canvasKit.AlphaType, PaintStyle: canvasKit.PaintStyle, }; fontMgr = makeFontMgr(canvasKit); return Skia; } // Named-family lookup for the custom watermark's FONT option. Only faces the // bundle ships can be matched on web; anything else falls back to the caller's // default typeface — the honest behaviour when there is no OS font service. let fontMgr: any = null; function makeFontMgr(canvasKit: any) { const provider = canvasKit.TypefaceFontProvider.Make(); return { registerFace(typeface: any, familyName: string) { provider.registerFont(typeface, familyName); }, matchFamilyStyle(family: string) { if (!family) return null; try { return provider.matchFamilyStyle?.(family, canvasKit.FontStyle.Normal) ?? null; } catch { return null; } }, }; }