diff --git a/apps/paper/src/Examples/Matrix/Matrix.tsx b/apps/paper/src/Examples/Matrix/Matrix.tsx index 2302e0e92d..423585991d 100644 --- a/apps/paper/src/Examples/Matrix/Matrix.tsx +++ b/apps/paper/src/Examples/Matrix/Matrix.tsx @@ -1,6 +1,7 @@ import { BlurMask, Canvas, + Canvas2, Fill, Group, useClock, @@ -41,7 +42,7 @@ export const Matrix = () => { } const symbols = font.getGlyphIDs("abcdefghijklmnopqrstuvwxyz"); return ( - + @@ -60,6 +61,6 @@ export const Matrix = () => { )) )} - + ); }; diff --git a/apps/paper/src/Examples/Matrix/Symbol.tsx b/apps/paper/src/Examples/Matrix/Symbol.tsx index 5c038311a0..215d0a2107 100644 --- a/apps/paper/src/Examples/Matrix/Symbol.tsx +++ b/apps/paper/src/Examples/Matrix/Symbol.tsx @@ -4,8 +4,8 @@ import { interpolateColors, vec, Glyphs } from "@shopify/react-native-skia"; import type { SharedValue } from "react-native-reanimated"; import { useDerivedValue } from "react-native-reanimated"; -export const COLS = 8; -export const ROWS = 15; +export const COLS = 32; +export const ROWS = 64; const pos = vec(0, 0); interface SymbolProps { @@ -38,7 +38,7 @@ export const Symbol = ({ }, [timestamp]); const opacity = useDerivedValue(() => { - const idx = Math.round(timestamp.value / 100); + const idx = Math.round(timestamp.value / 75); return stream[(stream.length - j + idx) % stream.length]; }, [timestamp]); diff --git a/packages/skia/cpp/api/JsiSkPaint.h b/packages/skia/cpp/api/JsiSkPaint.h index fce2faf2fd..e7b1e4839f 100644 --- a/packages/skia/cpp/api/JsiSkPaint.h +++ b/packages/skia/cpp/api/JsiSkPaint.h @@ -27,6 +27,12 @@ class JsiSkPaint : public JsiSkWrappingSharedPtrHostObject { public: EXPORT_JSI_API_TYPENAME(JsiSkPaint, Paint) + JSI_HOST_FUNCTION(assign) { + SkPaint* paint = JsiSkPaint::fromValue(runtime, arguments[0]).get(); + *getObject() = *paint; + return jsi::Value::undefined(); + } + JSI_HOST_FUNCTION(copy) { const auto *paint = getObject().get(); return jsi::Object::createFromHostObject( @@ -163,7 +169,8 @@ class JsiSkPaint : public JsiSkWrappingSharedPtrHostObject { return jsi::Value::undefined(); } - JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkPaint, copy), + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkPaint, assign), + JSI_EXPORT_FUNC(JsiSkPaint, copy), JSI_EXPORT_FUNC(JsiSkPaint, reset), JSI_EXPORT_FUNC(JsiSkPaint, getAlphaf), JSI_EXPORT_FUNC(JsiSkPaint, getColor), diff --git a/packages/skia/src/skia/types/Paint/Paint.ts b/packages/skia/src/skia/types/Paint/Paint.ts index 0216a1458f..007a4ecedf 100644 --- a/packages/skia/src/skia/types/Paint/Paint.ts +++ b/packages/skia/src/skia/types/Paint/Paint.ts @@ -45,6 +45,8 @@ export interface SkPaint extends SkJSIInstance<"Paint"> { */ reset(): void; + assign(paint: SkPaint): void; + /** * Retrieves the alpha and RGB unpremultiplied. RGB are extended sRGB values * (sRGB gamut, and encoded with the sRGB transfer function). diff --git a/packages/skia/src/skia/web/JsiSkPaint.ts b/packages/skia/src/skia/web/JsiSkPaint.ts index 029ace1cfc..4254b93a56 100644 --- a/packages/skia/src/skia/web/JsiSkPaint.ts +++ b/packages/skia/src/skia/web/JsiSkPaint.ts @@ -34,6 +34,10 @@ export class JsiSkPaint extends HostObject implements SkPaint { return new JsiSkPaint(this.CanvasKit, this.ref.copy()); } + assign(paint: JsiSkPaint) { + this.ref = paint.ref.copy(); + } + reset() { this.ref = new this.CanvasKit.Paint(); } diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index 20a78e2c27..4fd4211d68 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -7,26 +7,35 @@ import { HAS_REANIMATED_3, } from "../external/reanimated/renderHelpers"; +import type { StaticContext } from "./StaticContext"; +import { createStaticContext } from "./StaticContext"; import { createDrawingContext } from "./DrawingContext"; import type { Node } from "./nodes"; import { draw, isSharedValue } from "./nodes"; -const drawOnscreen = (Skia: Skia, nativeId: number, root: Node[]) => { +const drawOnscreen = ( + Skia: Skia, + nativeId: number, + root: Node[], + staticCtx: StaticContext +) => { "worklet"; const rec = Skia.PictureRecorder(); const canvas = rec.beginRecording(); - // TODO: This is only support from 3.15 and above (check the exact version) - // This could be polyfilled in C++ if needed (or in JS via functions only?) - const ctx = createDrawingContext(Skia, canvas); + const start = performance.now(); + const ctx = createDrawingContext(Skia, canvas, staticCtx); root.forEach((node) => { draw(ctx, node); }); const picture = rec.finishRecordingAsPicture(); + const end = performance.now(); + console.log("Recording time: ", end - start); SkiaViewApi.setJsiProperty(nativeId, "picture", picture); }; export class Container { - public _root: Node[] = []; + private _root: Node[] = []; + private _staticCtx: StaticContext | null = null; public unmounted = false; private values = new Set>(); @@ -47,13 +56,14 @@ export class Container { if (this.mapperId !== null) { Rea.stopMapper(this.mapperId); } - const { nativeId, Skia } = this; + const { nativeId, Skia, _staticCtx } = this; this.mapperId = Rea.startMapper(() => { "worklet"; - drawOnscreen(Skia, nativeId, root); + drawOnscreen(Skia, nativeId, root, _staticCtx!); }, Array.from(this.values)); } this._root = root; + this._staticCtx = createStaticContext(this.Skia); } clear() { @@ -66,9 +76,9 @@ export class Container { throw new Error("React Native Skia only supports Reanimated 3 and above"); } if (isOnscreen) { - const { nativeId, Skia, root } = this; + const { nativeId, Skia, root, _staticCtx } = this; Rea.runOnUI(() => { - drawOnscreen(Skia, nativeId, root); + drawOnscreen(Skia, nativeId, root, _staticCtx!); })(); } } @@ -94,7 +104,7 @@ export class Container { } drawOnCanvas(canvas: SkCanvas) { - const ctx = createDrawingContext(this.Skia, canvas); + const ctx = createDrawingContext(this.Skia, canvas, this._staticCtx!); this.root.forEach((node) => { draw(ctx, node); }); diff --git a/packages/skia/src/sksg/DrawingContext.ts b/packages/skia/src/sksg/DrawingContext.ts index 677cd0150b..d27532b790 100644 --- a/packages/skia/src/sksg/DrawingContext.ts +++ b/packages/skia/src/sksg/DrawingContext.ts @@ -22,7 +22,8 @@ import type { SkPaint, } from "../skia/types"; -import type { DeclarationContext } from "./DeclarationContext"; +import { createDeclarationContext } from "./DeclarationContext"; +import type { StaticContext } from "./StaticContext"; const computeClip = ( Skia: Skia, @@ -61,9 +62,15 @@ const processColor = ( } }; -export const createDrawingContext = (Skia: Skia, canvas: SkCanvas) => { +export const createDrawingContext = ( + Skia: Skia, + canvas: SkCanvas, + staticCtx: StaticContext +) => { "worklet"; const state = { + staticCtx, + declCtx: createDeclarationContext(Skia), paints: [Skia.Paint()], }; @@ -71,22 +78,20 @@ export const createDrawingContext = (Skia: Skia, canvas: SkCanvas) => { return state.paints[state.paints.length - 1]; }; - const processPaint = ( - { - opacity, - color, - strokeWidth, - blendMode, - style, - strokeJoin, - strokeCap, - strokeMiter, - antiAlias, - dither, - paint: paintProp, - }: DrawingNodeProps, - declCtx: DeclarationContext - ) => { + const processPaint = ({ + opacity, + color, + strokeWidth, + blendMode, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, + paint: paintProp, + }: DrawingNodeProps) => { + const { declCtx } = state; if (paintProp) { declCtx.paints.push(paintProp); return true; @@ -116,7 +121,14 @@ export const createDrawingContext = (Skia: Skia, canvas: SkCanvas) => { pathEffect !== undefined ) { if (!shouldRestore) { - state.paints.push(getPaint().copy()); + const i = state.paints.length; + if (!state.staticCtx.paints[i]) { + state.staticCtx.paints.push(Skia.Paint()); + } + const paint = state.staticCtx.paints[i]; + const parentPaint = getPaint(); + paint.assign(parentPaint); + state.paints.push(paint); shouldRestore = true; } } @@ -215,11 +227,11 @@ export const createDrawingContext = (Skia: Skia, canvas: SkCanvas) => { return { Skia, canvas, - save: () => state.paints.push(getPaint().copy()), restore: () => state.paints.pop(), getPaint, processPaint, processMatrixAndClipping, + declCtx: state.declCtx, }; }; diff --git a/packages/skia/src/sksg/StaticContext.ts b/packages/skia/src/sksg/StaticContext.ts new file mode 100644 index 0000000000..06539bf945 --- /dev/null +++ b/packages/skia/src/sksg/StaticContext.ts @@ -0,0 +1,9 @@ +import type { Skia, SkPaint } from "../skia/types"; + +export interface StaticContext { + paints: SkPaint[]; +} + +export const createStaticContext = (Skia: Skia) => { + return { paints: [Skia.Paint()] }; +}; diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index 7cd9cba646..b247e99240 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -6,3 +6,19 @@ export interface Node { props: Props; children: Node[]; } + +export const sortNodes = (children: Node[]) => { + "worklet"; + const declarations: Node[] = []; + const drawings: Node[] = []; + + children.forEach((node) => { + if (node.isDeclaration) { + declarations.push(node); + } else { + drawings.push(node); + } + }); + + return { declarations, drawings }; +}; diff --git a/packages/skia/src/sksg/nodes/context.ts b/packages/skia/src/sksg/nodes/context.ts index bf6ffdbb11..0961aaed90 100644 --- a/packages/skia/src/sksg/nodes/context.ts +++ b/packages/skia/src/sksg/nodes/context.ts @@ -8,7 +8,7 @@ import { type DeclarationContext, } from "../DeclarationContext"; -import type { Node } from "./Node"; +import { sortNodes, type Node } from "./Node"; import { drawAtlas, drawBox, @@ -248,21 +248,18 @@ function processDeclarations(ctx: DeclarationContext, node: Node) { const preProcessContext = ( ctx: DrawingContext, props: DrawingNodeProps, - node: Node + declarationChildren: Node[] ) => { "worklet"; const shouldRestoreMatrix = ctx.processMatrixAndClipping(props, props.layer); - const declCtx = createDeclarationContext(ctx.Skia); - node.children.forEach((child) => { - if (child.isDeclaration) { - processDeclarations(declCtx, child); - } + declarationChildren.forEach((child) => { + processDeclarations(ctx.declCtx, child); }); - const shouldRestorePaint = ctx.processPaint(props, declCtx); + const shouldRestorePaint = ctx.processPaint(props); return { shouldRestoreMatrix, shouldRestorePaint, - extraPaints: declCtx.paints.popAll(), + extraPaints: ctx.declCtx.paints.popAll(), }; }; @@ -318,10 +315,12 @@ export function draw(ctx: DrawingContext, node: Node) { return; } const { type, props: rawProps, children } = node; + // Regular nodes const props = materialize(rawProps); + const { declarations, drawings } = sortNodes(children); const { shouldRestoreMatrix, shouldRestorePaint, extraPaints } = - preProcessContext(ctx, props, node); + preProcessContext(ctx, props, declarations); const paints = [ctx.getPaint(), ...extraPaints]; paints.forEach((paint) => { const lctx = { paint, Skia: ctx.Skia, canvas: ctx.canvas }; @@ -398,10 +397,8 @@ export function draw(ctx: DrawingContext, node: Node) { } } }); - children.forEach((child) => { - if (!child.isDeclaration) { - draw(ctx, child); - } + drawings.forEach((child) => { + draw(ctx, child); }); if (shouldRestoreMatrix) { ctx.canvas.restore();