Skip to content

Commit

Permalink
Remove dep on React Native Web (#1578)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcandillon authored May 17, 2023
1 parent a6148b1 commit 68f0573
Show file tree
Hide file tree
Showing 16 changed files with 259 additions and 52 deletions.
2 changes: 2 additions & 0 deletions docs/docs/getting-started/web.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ While this is a substantial file size, you have control over the user experience
We provide direct integrations with [Expo](#Expo) and [Remotion](#Remotion).
Below you will also find the manual installation steps to run the module on any React Native Web projects.

You can use React Native Skia without React Native Web.

## Expo

Using React Native Skia on Expo web is reasonably straightforward.
Expand Down
12 changes: 12 additions & 0 deletions example/jestSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ jest.mock("react-native-reanimated", () => {
jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper");

jest.mock("@shopify/react-native-skia", () => {
jest.mock("../package/src/Platform", () => {
const Noop = () => undefined;
return {
OS: "web",
PixelRatio: 1,
requireNativeComponent: Noop,
resolveAsset: Noop,
findNodeHandle: Noop,
NativeModules: Noop,
View: Noop,
};
});
return require("../package/src/mock").Mock(global.CanvasKit);
});

Expand Down
20 changes: 17 additions & 3 deletions package/jestSetup.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
/* globals jest */
jest.mock("@shopify/react-native-skia", () =>
require("@shopify/react-native-skia/lib/commonjs/mock").Mock(global.CanvasKit)
);
jest.mock("@shopify/react-native-skia", () => {
jest.mock("@shopify/react-native-skia/lib/commonjs/Platform", () => {
const Noop = () => undefined;
return {
OS: "web",
PixelRatio: 1,
requireNativeComponent: Noop,
resolveAsset: Noop,
findNodeHandle: Noop,
NativeModules: Noop,
View: Noop,
};
});
return require("@shopify/react-native-skia/lib/commonjs/mock").Mock(
global.CanvasKit
);
});
20 changes: 20 additions & 0 deletions package/src/Platform/IPlatform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { HostComponent, NodeHandle, ViewComponent } from "react-native";

import type { DataModule } from "../skia/types";

export interface IPlatform {
OS: string;
requireNativeComponent: <T>(viewName: string) => HostComponent<T>;
PixelRatio: number;
NativeModules: Record<string, any>;
findNodeHandle: (
componentOrHandle:
| null
| number
| React.Component<any, any>
| React.ComponentClass<any>
) => null | NodeHandle;
resolveAsset: (source: DataModule) => string;
View: typeof ViewComponent;
}
28 changes: 28 additions & 0 deletions package/src/Platform/Platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
Image,
PixelRatio,
requireNativeComponent,
Platform as RNPlatform,
findNodeHandle,
NativeModules,
View,
} from "react-native";

import type { DataModule } from "../skia/types";
import { isRNModule } from "../skia/types";

import type { IPlatform } from "./IPlatform";

export const Platform: IPlatform = {
OS: RNPlatform.OS,
PixelRatio: PixelRatio.get(),
requireNativeComponent,
resolveAsset: (source: DataModule) => {
return isRNModule(source)
? Image.resolveAssetSource(source).uri
: source.default;
},
findNodeHandle,
NativeModules,
View,
};
136 changes: 136 additions & 0 deletions package/src/Platform/Platform.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type { RefObject, CSSProperties } from "react";
import React, { useLayoutEffect, useMemo, useRef } from "react";
import type { LayoutChangeEvent, ViewComponent, ViewProps } from "react-native";

import type { DataModule } from "../skia/types";
import { isRNModule } from "../skia/types";

import type { IPlatform } from "./IPlatform";

// eslint-disable-next-line max-len
// https://github.com/necolas/react-native-web/blob/master/packages/react-native-web/src/modules/useElementLayout/index.js
const DOM_LAYOUT_HANDLER_NAME = "__reactLayoutHandler";
type OnLayout = ((event: LayoutChangeEvent) => void) | undefined;
type Div = HTMLDivElement & {
__reactLayoutHandler: OnLayout;
};

let resizeObserver: ResizeObserver | null = null;

const getObserver = () => {
if (resizeObserver == null) {
resizeObserver = new window.ResizeObserver(function (entries) {
entries.forEach((entry) => {
const node = entry.target as Div;
const { left, top, width, height } = entry.contentRect;
const onLayout = node[DOM_LAYOUT_HANDLER_NAME];
if (typeof onLayout === "function") {
// setTimeout 0 is taken from react-native-web (UIManager)
setTimeout(
() =>
onLayout({
timeStamp: Date.now(),
nativeEvent: { layout: { x: left, y: top, width, height } },
currentTarget: 0,
target: 0,
bubbles: false,
cancelable: false,
defaultPrevented: false,
eventPhase: 0,
isDefaultPrevented() {
throw new Error("Method not supported on web.");
},
isPropagationStopped() {
throw new Error("Method not supported on web.");
},
persist() {
throw new Error("Method not supported on web.");
},
preventDefault() {
throw new Error("Method not supported on web.");
},
stopPropagation() {
throw new Error("Method not supported on web.");
},
isTrusted: true,
type: "",
}),
0
);
}
});
});
}
return resizeObserver;
};

const useElementLayout = (ref: RefObject<Div>, onLayout: OnLayout) => {
const observer = getObserver();

useLayoutEffect(() => {
const node = ref.current;
if (node !== null) {
node[DOM_LAYOUT_HANDLER_NAME] = onLayout;
}
}, [ref, onLayout]);

useLayoutEffect(() => {
const node = ref.current;
if (node != null && observer != null) {
if (typeof node[DOM_LAYOUT_HANDLER_NAME] === "function") {
observer.observe(node);
} else {
observer.unobserve(node);
}
}
return () => {
if (node != null && observer != null) {
observer.unobserve(node);
}
};
}, [observer, ref]);
};

const View = (({ children, onLayout, style: rawStyle }: ViewProps) => {
const style = useMemo(() => (rawStyle ?? {}) as CSSProperties, [rawStyle]);
const ref = useRef<Div>(null);
useElementLayout(ref, onLayout);
const cssStyles = useMemo(() => {
return {
...style,
display: "flex",
flexDirection: style.flexDirection || "column",
flexWrap: style.flexWrap || "nowrap",
justifyContent: style.justifyContent || "flex-start",
alignItems: style.alignItems || "stretch",
alignContent: style.alignContent || "stretch",
};
}, [style]);

return (
<div ref={ref} style={cssStyles}>
{children}
</div>
);
}) as unknown as typeof ViewComponent;

export const Platform: IPlatform = {
OS: "web",
PixelRatio: window.devicePixelRatio,
requireNativeComponent: () => {
throw new Error("requireNativeComponent is not supported on the web");
},
resolveAsset: (source: DataModule) => {
if (isRNModule(source)) {
throw new Error(
"Image source is a number - this is not supported on the web"
);
}
return source.default;
},
findNodeHandle: () => {
throw new Error("findNodeHandle is not supported on the web");
},
NativeModules: {},
View,
};
1 change: 1 addition & 0 deletions package/src/Platform/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./Platform";
4 changes: 2 additions & 2 deletions package/src/skia/NativeSetup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { NativeModules, Platform } from "react-native";
import { Platform } from "../Platform";

if (Platform.OS !== "web" && global.SkiaApi == null) {
// Initialize RN Skia
const SkiaModule = NativeModules.RNSkia;
const SkiaModule = Platform.NativeModules.RNSkia;
if (SkiaModule == null || typeof SkiaModule.install !== "function") {
throw new Error(
"Native RNSkia Module cannot be found! Make sure you correctly " +
Expand Down
19 changes: 4 additions & 15 deletions package/src/skia/core/Data.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,8 @@
import { useEffect, useRef, useState } from "react";
import { Image } from "react-native";

import { Skia } from "../Skia";
import { isRNModule } from "../types";
import type {
SkData,
DataModule,
DataSourceParam,
JsiDisposable,
} from "../types";

const resolveAsset = (source: DataModule) => {
return isRNModule(source)
? Image.resolveAssetSource(source).uri
: source.default;
};
import type { SkData, DataSourceParam, JsiDisposable } from "../types";
import { Platform } from "../../Platform";

const factoryWrapper = <T>(
data2: SkData,
Expand Down Expand Up @@ -42,7 +30,8 @@ const loadData = <T>(
resolve(factoryWrapper(Skia.Data.fromBytes(source), factory, onError))
);
} else {
const uri = typeof source === "string" ? source : resolveAsset(source);
const uri =
typeof source === "string" ? source : Platform.resolveAsset(source);
return Skia.Data.fromURI(uri).then((d) =>
factoryWrapper(d, factory, onError)
);
Expand Down
5 changes: 2 additions & 3 deletions package/src/skia/core/Image.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { findNodeHandle, Platform } from "react-native";

import { Platform } from "../../Platform";
import { Skia } from "../Skia";
import type { DataSourceParam, SkImage } from "../types";

Expand Down Expand Up @@ -48,7 +47,7 @@ export const makeImageFromView = <
);
}
}
const viewTag = findNodeHandle(viewRef.current);
const viewTag = Platform.findNodeHandle(viewRef.current);
if (viewTag !== null && viewTag !== 0) {
return Skia.Image.MakeImageFromViewTag(viewTag);
}
Expand Down
43 changes: 24 additions & 19 deletions package/src/views/SkiaBaseWebView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
import React from "react";
import type { PointerEvent } from "react";
import type { LayoutChangeEvent } from "react-native";
import { PixelRatio, View } from "react-native";

import type { SkRect, SkCanvas } from "../skia/types";
import type { SkiaValue } from "../values";
import { JsiSkSurface } from "../skia/web/JsiSkSurface";
import { Platform } from "../Platform";

import type { DrawMode, SkiaBaseViewProps, TouchInfo } from "./types";
import { TouchType } from "./types";

const pd = PixelRatio.get();
const pd = Platform.PixelRatio;

export abstract class SkiaBaseWebView<
TProps extends SkiaBaseViewProps
Expand All @@ -38,17 +38,16 @@ export abstract class SkiaBaseWebView<
this._unsubscriptions = [];
}

private onLayout(evt: LayoutChangeEvent) {
private onLayoutEvent(evt: LayoutChangeEvent) {
const { CanvasKit } = global;
const { width, height } = evt.nativeEvent.layout;
this.width = width;
this.height = height;
// Reset canvas / surface on layout change
if (this._canvasRef.current) {
const canvas = this._canvasRef.current;
canvas.width = width * pd;
canvas.height = height * pd;
const surface = CanvasKit.MakeWebGLCanvasSurface(this._canvasRef.current);
const canvas = this._canvasRef.current;
if (canvas) {
this.width = canvas.clientWidth;
this.height = canvas.clientHeight;
canvas.width = this.width * pd;
canvas.height = this.height * pd;
const surface = CanvasKit.MakeWebGLCanvasSurface(canvas);
if (!surface) {
throw new Error("Could not create surface");
}
Expand Down Expand Up @@ -178,21 +177,27 @@ export abstract class SkiaBaseWebView<
return (evt: PointerEvent) => this.handleTouchEvent(evt, touchType);
}

private onStart = this.createTouchHandler(TouchType.Start);
private onActive = this.createTouchHandler(TouchType.Active);
private onCancel = this.createTouchHandler(TouchType.Cancelled);
private onEnd = this.createTouchHandler(TouchType.End);
private onLayout = this.onLayoutEvent.bind(this);

render() {
const { mode, debug = false, ...viewProps } = this.props;
return (
<View {...viewProps} onLayout={this.onLayout.bind(this)}>
<Platform.View {...viewProps} onLayout={this.onLayout}>
<canvas
ref={this._canvasRef}
style={{ display: "flex", flex: 1 }}
onPointerDown={this.createTouchHandler(TouchType.Start)}
onPointerMove={this.createTouchHandler(TouchType.Active)}
onPointerUp={this.createTouchHandler(TouchType.End)}
onPointerCancel={this.createTouchHandler(TouchType.Cancelled)}
onPointerLeave={this.createTouchHandler(TouchType.End)}
onPointerOut={this.createTouchHandler(TouchType.End)}
onPointerDown={this.onStart}
onPointerMove={this.onActive}
onPointerUp={this.onEnd}
onPointerCancel={this.onCancel}
onPointerLeave={this.onEnd}
onPointerOut={this.onEnd}
/>
</View>
</Platform.View>
);
}
}
4 changes: 2 additions & 2 deletions package/src/views/SkiaDomView.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React from "react";
import { requireNativeComponent, Platform } from "react-native";
import type { HostComponent } from "react-native";

import type { SkRect } from "../skia/types";
import type { SkiaValue } from "../values";
import { Platform } from "../Platform";

import { SkiaViewApi } from "./api";
import { SkiaViewNativeId } from "./SkiaView";
import type { NativeSkiaViewProps, SkiaDomViewProps } from "./types";

const NativeSkiaDomView: HostComponent<SkiaDomViewProps> =
Platform.OS !== "web"
? requireNativeComponent<NativeSkiaViewProps>("SkiaDomView")
? Platform.requireNativeComponent<NativeSkiaViewProps>("SkiaDomView")
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
(null as any);

Expand Down
Loading

0 comments on commit 68f0573

Please sign in to comment.