Skip to content

Commit

Permalink
Factorize Data Loading (#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcandillon authored Jun 30, 2022
1 parent 9527658 commit 813afaf
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 51 deletions.
Binary file added package/src/__tests__/snapshots/font/green.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added package/src/__tests__/snapshots/font/red.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions package/src/renderer/__tests__/Data.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import path from "path";

import React from "react";

import { processResult } from "../../__tests__/setup";
import { Fill } from "../components";
import * as SkiaRenderer from "../index";
import type { SkData } from "../../skia/types/Data/Data";

import { mountCanvas, nodeRequire, Skia } from "./setup";

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface EmptyProps {}

const CheckFont = ({}: EmptyProps) => {
const { useFont } = require("../../skia/core/Font");
const font = useFont(
nodeRequire(
path.resolve(__dirname, "../../skia/__tests__/assets/Roboto-Medium.ttf")
)
);
if (!font) {
return <Fill color="red" />;
}
return <Fill color="green" />;
};

const CheckDataCollection = ({}: EmptyProps) => {
const { useDataCollection } = require("../../skia/core/Data");
const font = useDataCollection(
[
nodeRequire(
path.resolve(__dirname, "../../skia/__tests__/assets/Roboto-Medium.ttf")
),
nodeRequire(
path.resolve(__dirname, "../../skia/__tests__/assets/Roboto-Medium.ttf")
),
],
(data: SkData) => Skia.Typeface.MakeFreeTypeFaceFromData(data)
);
if (!font) {
return <Fill color="red" />;
}
return <Fill color="green" />;
};

describe("Data Loading", () => {
it("Loads renderer without Skia", async () => {
expect(SkiaRenderer).toBeDefined();
});
it("Should load a font file", async () => {
const { surface, draw } = mountCanvas(<CheckFont />);
draw();
processResult(surface, "snapshots/font/red.png");
await wait(500);
draw();
processResult(surface, "snapshots/font/green.png");
});

it("Should load many font files", async () => {
const { surface, draw } = mountCanvas(<CheckDataCollection />);
draw();
processResult(surface, "snapshots/font/red.png");
await wait(500);
draw();
processResult(surface, "snapshots/font/green.png");
});
});
12 changes: 12 additions & 0 deletions package/src/renderer/__tests__/setup.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from "fs";

import React from "react";
import type { ReactNode } from "react";
import ReactReconciler from "react-reconciler";
Expand All @@ -13,6 +15,15 @@ import { LoadSkia } from "../../web";

export let Skia: ReturnType<typeof JsiSkApi>;

jest.mock("react-native", () => ({
Platform: { OS: "web" },
Image: {
resolveAssetSource: jest.fn,
},
}));

export const nodeRequire = (uri: string) => fs.readFileSync(uri);

beforeAll(async () => {
await LoadSkia();
Skia = JsiSkApi(global.CanvasKit);
Expand All @@ -39,6 +50,7 @@ export const drawOnNode = (element: ReactNode) => {
};

export const mountCanvas = (element: ReactNode) => {
global.SkiaApi = Skia;
expect(Skia).toBeDefined();
const surface = Skia.Surface.Make(width, height)!;
expect(surface).toBeDefined();
Expand Down
117 changes: 67 additions & 50 deletions package/src/skia/core/Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,71 +11,88 @@ const resolveAsset = (source: ReturnType<typeof require>) => {
: Image.resolveAssetSource(source).uri;
};

export const useDataCollection = <T>(
sources: DataSource[],
factory: (data: SkData[]) => T,
deps: DependencyList = []
const factoryWrapper = <T>(
data2: SkData,
factory: (data: SkData) => T,
onError?: (err: Error) => void
) => {
const [data, setData] = useState<T | null>(null);
useEffect(() => {
const bytesOrURIs = sources.map((source) => {
if (source instanceof Uint8Array) {
return source;
}
return typeof source === "string" ? source : resolveAsset(source);
});
Promise.all(
bytesOrURIs.map((bytesOrURI) =>
bytesOrURI instanceof Uint8Array
? Skia.Data.fromBytes(bytesOrURI)
: Skia.Data.fromURI(bytesOrURI)
)
).then((d) => setData(factory(d)));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return data;
const factoryResult = factory(data2);
if (factoryResult === null) {
onError && onError(new Error("Could not load data"));
return null;
} else {
return factoryResult;
}
};

export const useRawData = <T>(
source: DataSource | null | undefined,
const loadDataCollection = <T>(
sources: DataSource[],
factory: (data: SkData) => T,
onError?: (err: Error) => void
): Promise<(T | null)[]> =>
Promise.all(sources.map((source) => loadData(source, factory, onError)));

const loadData = <T>(
source: DataSource,
factory: (data: SkData) => T,
onError?: (err: Error) => void
): Promise<T | null> => {
if (source instanceof Uint8Array) {
return new Promise((resolve) =>
resolve(factoryWrapper(Skia.Data.fromBytes(source), factory, onError))
);
} else {
const uri = typeof source === "string" ? source : resolveAsset(source);
return Skia.Data.fromURI(uri).then((d) =>
factoryWrapper(d, factory, onError)
);
}
};

type Source = DataSource | null | undefined;

const useLoading = <T>(
source: Source,
loader: () => Promise<T | null>,
deps: DependencyList = []
) => {
const [data, setData] = useState<T | null>(null);
const prevSourceRef = useRef<DataSource | null | undefined>();
const prevSourceRef = useRef<Source>();
useEffect(() => {
// Track to avoid re-fetching the same data
if (prevSourceRef.current !== source) {
prevSourceRef.current = source;
if (source !== null && source !== undefined) {
const factoryWrapper = (data2: SkData) => {
const factoryResult = factory(data2);
if (factoryResult === null) {
onError && onError(new Error("Could not load data"));
setData(null);
} else {
setData(factoryResult);
}
};
if (source instanceof Uint8Array) {
factoryWrapper(Skia.Data.fromBytes(source));
} else {
const uri =
typeof source === "string" ? source : resolveAsset(source);
Skia.Data.fromURI(uri).then((d) => factoryWrapper(d));
}
} else {
// new source is null or undefined -> remove cached data
setData(null);
}
loader().then(setData);
} else {
setData(null);
}
}, [factory, onError, source]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return data;
};

export const useDataCollection = <T>(
sources: DataSource[],
factory: (data: SkData) => T,
onError?: (err: Error) => void,
deps?: DependencyList
) =>
useLoading(
sources,
() => loadDataCollection(sources, factory, onError),
deps
);

export const useRawData = <T>(
source: DataSource | null | undefined,
factory: (data: SkData) => T,
onError?: (err: Error) => void,
deps?: DependencyList
) => useLoading(source, () => loadData(source, factory, onError), deps);

const identity = (data: SkData) => data;

export const useData = (
source: DataSource | null | undefined,
onError?: (err: Error) => void
) => useRawData(source, identity, onError);
onError?: (err: Error) => void,
deps?: DependencyList
) => useRawData(source, identity, onError, deps);
7 changes: 6 additions & 1 deletion package/src/skia/core/Typeface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ import { useRawData } from "./Data";
export const useTypeface = (
source: DataSource | null | undefined,
onError?: (err: Error) => void
) => useRawData(source, Skia.Typeface.MakeFreeTypeFaceFromData, onError);
) =>
useRawData(
source,
Skia.Typeface.MakeFreeTypeFaceFromData.bind(Skia.Typeface),
onError
);

0 comments on commit 813afaf

Please sign in to comment.