From 2a0b05dba1d1ada3d21256b94ed6b913233415b8 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 06:20:39 +0100 Subject: [PATCH 01/27] :wrench: --- apps/paper/src/Examples/Matrix/Matrix.tsx | 5 ++-- apps/paper/src/Examples/Matrix/Symbol.tsx | 6 ++--- packages/skia/cpp/api/JsiSkPaint.h | 9 ++++++- packages/skia/src/skia/types/Paint/Paint.ts | 2 ++ packages/skia/src/skia/web/JsiSkPaint.ts | 4 +++ packages/skia/src/sksg/Container.ts | 29 +++++++++++++++------ packages/skia/src/sksg/DrawingContext.ts | 18 ++++++++++--- packages/skia/src/sksg/StaticContext.ts | 9 +++++++ 8 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 packages/skia/src/sksg/StaticContext.ts 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..9f3f232819 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -7,26 +7,38 @@ 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); + console.log("Static context paints: ", staticCtx.paints.length); 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 +59,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 +79,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 +107,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..7560fe82ea 100644 --- a/packages/skia/src/sksg/DrawingContext.ts +++ b/packages/skia/src/sksg/DrawingContext.ts @@ -23,6 +23,7 @@ import type { } from "../skia/types"; import type { DeclarationContext } from "./DeclarationContext"; +import type { StaticContext } from "./StaticContext"; const computeClip = ( Skia: Skia, @@ -61,9 +62,14 @@ const processColor = ( } }; -export const createDrawingContext = (Skia: Skia, canvas: SkCanvas) => { +export const createDrawingContext = ( + Skia: Skia, + canvas: SkCanvas, + staticCtx: StaticContext +) => { "worklet"; const state = { + staticCtx, paints: [Skia.Paint()], }; @@ -116,7 +122,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,7 +228,6 @@ export const createDrawingContext = (Skia: Skia, canvas: SkCanvas) => { return { Skia, canvas, - save: () => state.paints.push(getPaint().copy()), restore: () => state.paints.pop(), getPaint, processPaint, 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()] }; +}; From 804992c2b070a1bed33301c0948ae5776ed87365 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 06:41:57 +0100 Subject: [PATCH 02/27] :wrench: --- packages/skia/src/sksg/Container.ts | 3 --- packages/skia/src/sksg/DrawingContext.ts | 34 ++++++++++++------------ packages/skia/src/sksg/nodes/Node.ts | 16 +++++++++++ packages/skia/src/sksg/nodes/context.ts | 25 ++++++++--------- 4 files changed, 44 insertions(+), 34 deletions(-) diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index 9f3f232819..4fd4211d68 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -22,8 +22,6 @@ const drawOnscreen = ( "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 start = performance.now(); const ctx = createDrawingContext(Skia, canvas, staticCtx); root.forEach((node) => { @@ -32,7 +30,6 @@ const drawOnscreen = ( const picture = rec.finishRecordingAsPicture(); const end = performance.now(); console.log("Recording time: ", end - start); - console.log("Static context paints: ", staticCtx.paints.length); SkiaViewApi.setJsiProperty(nativeId, "picture", picture); }; diff --git a/packages/skia/src/sksg/DrawingContext.ts b/packages/skia/src/sksg/DrawingContext.ts index 7560fe82ea..d27532b790 100644 --- a/packages/skia/src/sksg/DrawingContext.ts +++ b/packages/skia/src/sksg/DrawingContext.ts @@ -22,7 +22,7 @@ import type { SkPaint, } from "../skia/types"; -import type { DeclarationContext } from "./DeclarationContext"; +import { createDeclarationContext } from "./DeclarationContext"; import type { StaticContext } from "./StaticContext"; const computeClip = ( @@ -70,6 +70,7 @@ export const createDrawingContext = ( "worklet"; const state = { staticCtx, + declCtx: createDeclarationContext(Skia), paints: [Skia.Paint()], }; @@ -77,22 +78,20 @@ export const createDrawingContext = ( 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; @@ -232,6 +231,7 @@ export const createDrawingContext = ( getPaint, processPaint, processMatrixAndClipping, + declCtx: state.declCtx, }; }; 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(); From 00fa9babcb52da065b467a3f81c732a8bfedb8f0 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 11:43:12 +0100 Subject: [PATCH 03/27] :wrench: --- packages/skia/src/sksg/Container.ts | 23 ++++++++++--------- packages/skia/src/sksg/StaticContext.ts | 7 ++++-- packages/skia/src/sksg/nodes/Node.ts | 30 ++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index 4fd4211d68..ee894e45b7 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -12,22 +12,18 @@ import { createStaticContext } from "./StaticContext"; import { createDrawingContext } from "./DrawingContext"; import type { Node } from "./nodes"; import { draw, isSharedValue } from "./nodes"; +import { Recorder } from "./recorder/Recorder"; +import { record } from "./recorder/Visitor"; +import { playback } from "./recorder/Playback"; const drawOnscreen = ( Skia: Skia, nativeId: number, - root: Node[], staticCtx: StaticContext ) => { "worklet"; - const rec = Skia.PictureRecorder(); - const canvas = rec.beginRecording(); const start = performance.now(); - const ctx = createDrawingContext(Skia, canvas, staticCtx); - root.forEach((node) => { - draw(ctx, node); - }); - const picture = rec.finishRecordingAsPicture(); + const picture = playback(Skia, staticCtx); const end = performance.now(); console.log("Recording time: ", end - start); SkiaViewApi.setJsiProperty(nativeId, "picture", picture); @@ -59,11 +55,16 @@ export class Container { const { nativeId, Skia, _staticCtx } = this; this.mapperId = Rea.startMapper(() => { "worklet"; - drawOnscreen(Skia, nativeId, root, _staticCtx!); + drawOnscreen(Skia, nativeId, _staticCtx!); }, Array.from(this.values)); } this._root = root; this._staticCtx = createStaticContext(this.Skia); + const recorder = new Recorder(); + root.forEach((node) => { + record(recorder, node); + }); + this._staticCtx.commands = recorder.commands; } clear() { @@ -76,9 +77,9 @@ export class Container { throw new Error("React Native Skia only supports Reanimated 3 and above"); } if (isOnscreen) { - const { nativeId, Skia, root, _staticCtx } = this; + const { nativeId, Skia, _staticCtx } = this; Rea.runOnUI(() => { - drawOnscreen(Skia, nativeId, root, _staticCtx!); + drawOnscreen(Skia, nativeId, _staticCtx!); })(); } } diff --git a/packages/skia/src/sksg/StaticContext.ts b/packages/skia/src/sksg/StaticContext.ts index 06539bf945..8ad06b50b8 100644 --- a/packages/skia/src/sksg/StaticContext.ts +++ b/packages/skia/src/sksg/StaticContext.ts @@ -1,9 +1,12 @@ import type { Skia, SkPaint } from "../skia/types"; +import type { Command } from "./recorder/Recorder"; + export interface StaticContext { paints: SkPaint[]; + commands: Command[]; } -export const createStaticContext = (Skia: Skia) => { - return { paints: [Skia.Paint()] }; +export const createStaticContext = (Skia: Skia): StaticContext => { + return { paints: [Skia.Paint()], commands: [] }; }; diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index b247e99240..a026953995 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -1,12 +1,40 @@ +import type { SharedValue } from "react-native-reanimated"; + import type { NodeType } from "../../dom/types"; -export interface Node { +import { isSharedValue } from "./utils"; + +type UnknownProps = Record; + +type AnimatedProps = Partial<{ [K in keyof T]: SharedValue }>; + +export interface Node { type: NodeType; isDeclaration: boolean; props: Props; + animatedProps?: AnimatedProps; children: Node[]; } +export const splitProps = (props: T) => { + "worklet"; + let hasAnimatedProps = false; + const animatedProps: Partial> = {}; + Object.keys(props).forEach((key) => { + const name = key as keyof T; + const value = props[name]; + if (isSharedValue(value)) { + hasAnimatedProps = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + animatedProps[name] = value as any; + } + }); + return { + props, + animatedProps: hasAnimatedProps ? animatedProps : undefined, + }; +}; + export const sortNodes = (children: Node[]) => { "worklet"; const declarations: Node[] = []; From 9042de093da7915cfa7590b9f30d66e650e9f27e Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 11:43:19 +0100 Subject: [PATCH 04/27] :wrench: --- packages/skia/src/sksg/recorder/Paint.ts | 6 ++ packages/skia/src/sksg/recorder/Playback.ts | 88 +++++++++++++++++++ packages/skia/src/sksg/recorder/Recorder.ts | 53 +++++++++++ packages/skia/src/sksg/recorder/Visitor.ts | 97 +++++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 packages/skia/src/sksg/recorder/Paint.ts create mode 100644 packages/skia/src/sksg/recorder/Playback.ts create mode 100644 packages/skia/src/sksg/recorder/Recorder.ts create mode 100644 packages/skia/src/sksg/recorder/Visitor.ts diff --git a/packages/skia/src/sksg/recorder/Paint.ts b/packages/skia/src/sksg/recorder/Paint.ts new file mode 100644 index 0000000000..a3c9539156 --- /dev/null +++ b/packages/skia/src/sksg/recorder/Paint.ts @@ -0,0 +1,6 @@ +import type { Color } from "../../skia/types"; + +export interface PaintProps { + color?: Color; + opacity?: number; +} diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts new file mode 100644 index 0000000000..40af391412 --- /dev/null +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -0,0 +1,88 @@ +"worklet"; + +import type { SharedValue } from "react-native-reanimated"; + +import type { GlyphsProps } from "../../dom/types"; +import { exhaustiveCheck } from "../../renderer/typeddash"; +import type { Skia } from "../../skia/types"; +import { isSharedValue } from "../nodes"; +import { drawGlyphs } from "../nodes/drawings"; +import type { StaticContext } from "../StaticContext"; + +import type { PaintProps } from "./Paint"; +import { CommandType } from "./Recorder"; + +const materializeValue = (value: T | SharedValue) => { + return (isSharedValue(value) ? value.value : value) as T; +}; + +const processColor = ( + Skia: Skia, + color: number | string | Float32Array | number[] +) => { + "worklet"; + if (typeof color === "string" || typeof color === "number") { + return Skia.Color(color); + } else if (Array.isArray(color) || color instanceof Float32Array) { + return color instanceof Float32Array ? color : new Float32Array(color); + } else { + throw new Error( + `Invalid color type: ${typeof color}. Expected number, string, or array.` + ); + } +}; + +export const playback = (Skia: Skia, staticCtx: StaticContext) => { + const recorder = Skia.PictureRecorder(); + const canvas = recorder.beginRecording(); + const { commands } = staticCtx; + const paints = [staticCtx.paints[0]]; + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + let paint = paints[paints.length - 1]; + const { props } = command; + if (command.animatedProps) { + Object.keys(command.animatedProps).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + props[key] = command.animatedProps[key].value; + }); + } + const ctx = { canvas, Skia, paint }; + switch (command.type) { + case CommandType.PushPaint: { + const j = paints.length; + if (!staticCtx.paints[j]) { + staticCtx.paints.push(Skia.Paint()); + } + const childPaint = staticCtx.paints[j]; + childPaint.assign(paint); + paints.push(childPaint); + paint = childPaint; + const { opacity, color } = props as PaintProps; + if (opacity !== undefined) { + paint.setAlphaf(paint.getAlphaf() * materializeValue(opacity)); + } + if (color !== undefined) { + const currentOpacity = paint.getAlphaf(); + paint.setShader(null); + paint.setColor(processColor(Skia, materializeValue(color))); + paint.setAlphaf(currentOpacity * paint.getAlphaf()); + } + break; + } + case CommandType.PopPaint: + paints.pop(); + break; + case CommandType.DrawPaint: + canvas.drawPaint(paint); + break; + case CommandType.DrawGlyphs: + drawGlyphs(ctx, props as GlyphsProps); + break; + default: + exhaustiveCheck(command.type); + } + } + return recorder.finishRecordingAsPicture(); +}; diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts new file mode 100644 index 0000000000..a4b874ce93 --- /dev/null +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -0,0 +1,53 @@ +"worklet"; + +import type { SharedValue } from "react-native-reanimated"; + +import type { GlyphsProps } from "../../dom/types"; +import { splitProps } from "../nodes"; + +import type { PaintProps } from "./Paint"; + +export enum CommandType { + PushPaint, + PopPaint, + DrawPaint, + DrawGlyphs, +} + +type CommandProps = { + [CommandType.PushPaint]: PaintProps; + [CommandType.PopPaint]: null; + [CommandType.DrawPaint]: null; + [CommandType.DrawGlyphs]: GlyphsProps; +}; + +type AnimatedProps = { + [P in keyof T]: SharedValue; +}; + +export interface Command { + type: T; + props: CommandProps[T]; + animatedProps?: Partial>; +} + +export class Recorder { + public commands: Command[] = []; + + pushPaint(props: PaintProps) { + this.commands.push({ type: CommandType.PushPaint, props }); + } + + popPaint() { + this.commands.push({ type: CommandType.PopPaint, props: null }); + } + + drawPaint() { + this.commands.push({ type: CommandType.DrawPaint, props: null }); + } + + drawGlyphs(glyphsProps: GlyphsProps) { + const { props, animatedProps } = splitProps(glyphsProps); + this.commands.push({ type: CommandType.DrawGlyphs, props, animatedProps }); + } +} diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts new file mode 100644 index 0000000000..cbbcd7e740 --- /dev/null +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +"worklet"; + +import type { DrawingNodeProps } from "../../dom/types"; +import { NodeType } from "../../dom/types"; +import type { Node } from "../nodes"; + +import type { PaintProps } from "./Paint"; +import type { Recorder } from "./Recorder"; + +function processPaint({ + opacity, + color, + strokeWidth, + blendMode, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, + paint: paintProp, +}: DrawingNodeProps) { + let shouldRestore = false; + const paint: PaintProps = {}; + if ( + opacity !== undefined || + color !== undefined || + strokeWidth !== undefined || + blendMode !== undefined || + style !== undefined || + strokeJoin !== undefined || + strokeCap !== undefined || + strokeMiter !== undefined || + antiAlias !== undefined || + dither !== undefined + ) { + shouldRestore = true; + } + + if (opacity !== undefined) { + paint.opacity = opacity; + } + if (color !== undefined) { + paint.color = color; + } + // if (strokeWidth !== undefined) { + // paint.setStrokeWidth(strokeWidth); + // } + // if (blendMode !== undefined) { + // paint.setBlendMode(BlendMode[enumKey(blendMode)]); + // } + // if (style !== undefined) { + // paint.setStyle(PaintStyle[enumKey(style)]); + // } + // if (strokeJoin !== undefined) { + // paint.setStrokeJoin(StrokeJoin[enumKey(strokeJoin)]); + // } + // if (strokeCap !== undefined) { + // paint.setStrokeCap(StrokeCap[enumKey(strokeCap)]); + // } + // if (strokeMiter !== undefined) { + // paint.setStrokeMiter(strokeMiter); + // } + // if (antiAlias !== undefined) { + // paint.setAntiAlias(antiAlias); + // } + // if (dither !== undefined) { + // paint.setDither(dither); + // } + if (shouldRestore) { + return paint; + } + return null; +} + +export function record(recorder: Recorder, root: Node) { + const { type, props, children } = root; + const paint = processPaint(props as DrawingNodeProps); + if (paint) { + recorder.pushPaint(paint); + } + switch (type) { + case NodeType.Fill: + recorder.drawPaint(); + break; + case NodeType.Glyphs: + recorder.drawGlyphs(props); + break; + } + children.forEach((child) => { + record(recorder, child); + }); + if (paint) { + recorder.popPaint(); + } +} From bbff9f3b9dcf2e20f00cdccb91bb3c1e46a717f7 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 11:53:16 +0100 Subject: [PATCH 05/27] :wrench: --- apps/paper/src/Examples/Breathe/Breathe.tsx | 5 +++-- packages/skia/src/sksg/recorder/Paint.ts | 4 +++- packages/skia/src/sksg/recorder/Playback.ts | 15 +++++++++++---- packages/skia/src/sksg/recorder/Recorder.ts | 9 ++++++++- packages/skia/src/sksg/recorder/Visitor.ts | 9 ++++++--- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/apps/paper/src/Examples/Breathe/Breathe.tsx b/apps/paper/src/Examples/Breathe/Breathe.tsx index 5425839faa..1c25ae82b4 100644 --- a/apps/paper/src/Examples/Breathe/Breathe.tsx +++ b/apps/paper/src/Examples/Breathe/Breathe.tsx @@ -9,6 +9,7 @@ import { Group, polar2Canvas, mix, + Canvas2, } from "@shopify/react-native-skia"; import type { SharedValue } from "react-native-reanimated"; import { useDerivedValue } from "react-native-reanimated"; @@ -68,7 +69,7 @@ export const Breathe = () => { return ( - + @@ -78,7 +79,7 @@ export const Breathe = () => { ); })} - + ); }; diff --git a/packages/skia/src/sksg/recorder/Paint.ts b/packages/skia/src/sksg/recorder/Paint.ts index a3c9539156..4506117825 100644 --- a/packages/skia/src/sksg/recorder/Paint.ts +++ b/packages/skia/src/sksg/recorder/Paint.ts @@ -1,6 +1,8 @@ -import type { Color } from "../../skia/types"; +import type { SkEnum } from "../../dom/types"; +import type { BlendMode, Color } from "../../skia/types"; export interface PaintProps { color?: Color; opacity?: number; + blendMode?: SkEnum; } diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index 40af391412..7d3daeb094 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -2,12 +2,13 @@ import type { SharedValue } from "react-native-reanimated"; -import type { GlyphsProps } from "../../dom/types"; +import type { CircleProps, GlyphsProps } from "../../dom/types"; import { exhaustiveCheck } from "../../renderer/typeddash"; -import type { Skia } from "../../skia/types"; +import { BlendMode, type Skia } from "../../skia/types"; import { isSharedValue } from "../nodes"; -import { drawGlyphs } from "../nodes/drawings"; +import { drawCircle, drawGlyphs } from "../nodes/drawings"; import type { StaticContext } from "../StaticContext"; +import { enumKey } from "../../dom/nodes"; import type { PaintProps } from "./Paint"; import { CommandType } from "./Recorder"; @@ -59,7 +60,7 @@ export const playback = (Skia: Skia, staticCtx: StaticContext) => { childPaint.assign(paint); paints.push(childPaint); paint = childPaint; - const { opacity, color } = props as PaintProps; + const { opacity, color, blendMode } = props as PaintProps; if (opacity !== undefined) { paint.setAlphaf(paint.getAlphaf() * materializeValue(opacity)); } @@ -69,6 +70,9 @@ export const playback = (Skia: Skia, staticCtx: StaticContext) => { paint.setColor(processColor(Skia, materializeValue(color))); paint.setAlphaf(currentOpacity * paint.getAlphaf()); } + if (blendMode !== undefined) { + paint.setBlendMode(BlendMode[enumKey(materializeValue(blendMode))]); + } break; } case CommandType.PopPaint: @@ -80,6 +84,9 @@ export const playback = (Skia: Skia, staticCtx: StaticContext) => { case CommandType.DrawGlyphs: drawGlyphs(ctx, props as GlyphsProps); break; + case CommandType.DrawCircle: + drawCircle(ctx, props as CircleProps); + break; default: exhaustiveCheck(command.type); } diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index a4b874ce93..e1d0053cdd 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -2,7 +2,7 @@ import type { SharedValue } from "react-native-reanimated"; -import type { GlyphsProps } from "../../dom/types"; +import type { CircleProps, GlyphsProps } from "../../dom/types"; import { splitProps } from "../nodes"; import type { PaintProps } from "./Paint"; @@ -12,6 +12,7 @@ export enum CommandType { PopPaint, DrawPaint, DrawGlyphs, + DrawCircle, } type CommandProps = { @@ -19,6 +20,7 @@ type CommandProps = { [CommandType.PopPaint]: null; [CommandType.DrawPaint]: null; [CommandType.DrawGlyphs]: GlyphsProps; + [CommandType.DrawCircle]: CircleProps; }; type AnimatedProps = { @@ -50,4 +52,9 @@ export class Recorder { const { props, animatedProps } = splitProps(glyphsProps); this.commands.push({ type: CommandType.DrawGlyphs, props, animatedProps }); } + + drawCircle(circleProps: CircleProps) { + const { props, animatedProps } = splitProps(circleProps); + this.commands.push({ type: CommandType.DrawCircle, props, animatedProps }); + } } diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index cbbcd7e740..eb25eda6ff 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -47,9 +47,9 @@ function processPaint({ // if (strokeWidth !== undefined) { // paint.setStrokeWidth(strokeWidth); // } - // if (blendMode !== undefined) { - // paint.setBlendMode(BlendMode[enumKey(blendMode)]); - // } + if (blendMode !== undefined) { + paint.blendMode = blendMode; + } // if (style !== undefined) { // paint.setStyle(PaintStyle[enumKey(style)]); // } @@ -87,6 +87,9 @@ export function record(recorder: Recorder, root: Node) { case NodeType.Glyphs: recorder.drawGlyphs(props); break; + case NodeType.Circle: + recorder.drawCircle(props); + break; } children.forEach((child) => { record(recorder, child); From 4e9f5cd7c9caf46999d34e697aa9d119b608a146 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 12:12:05 +0100 Subject: [PATCH 06/27] :wrench: --- apps/paper/src/Examples/Breathe/Breathe.tsx | 4 +- apps/paper/src/Examples/Matrix/Matrix.tsx | 5 +- packages/skia/src/dom/types/Common.ts | 12 +- packages/skia/src/renderer/Canvas.tsx | 158 ++++++++++---------- packages/skia/src/sksg/recorder/Playback.ts | 81 +++++++++- packages/skia/src/sksg/recorder/Recorder.ts | 15 +- packages/skia/src/sksg/recorder/Visitor.ts | 76 +++++----- 7 files changed, 223 insertions(+), 128 deletions(-) diff --git a/apps/paper/src/Examples/Breathe/Breathe.tsx b/apps/paper/src/Examples/Breathe/Breathe.tsx index 1c25ae82b4..e0d096a22a 100644 --- a/apps/paper/src/Examples/Breathe/Breathe.tsx +++ b/apps/paper/src/Examples/Breathe/Breathe.tsx @@ -69,7 +69,7 @@ export const Breathe = () => { return ( - + @@ -79,7 +79,7 @@ export const Breathe = () => { ); })} - + ); }; diff --git a/apps/paper/src/Examples/Matrix/Matrix.tsx b/apps/paper/src/Examples/Matrix/Matrix.tsx index 423585991d..2302e0e92d 100644 --- a/apps/paper/src/Examples/Matrix/Matrix.tsx +++ b/apps/paper/src/Examples/Matrix/Matrix.tsx @@ -1,7 +1,6 @@ import { BlurMask, Canvas, - Canvas2, Fill, Group, useClock, @@ -42,7 +41,7 @@ export const Matrix = () => { } const symbols = font.getGlyphIDs("abcdefghijklmnopqrstuvwxyz"); return ( - + @@ -61,6 +60,6 @@ export const Matrix = () => { )) )} - + ); }; diff --git a/packages/skia/src/dom/types/Common.ts b/packages/skia/src/dom/types/Common.ts index 81ee8d2041..fad5b45b03 100644 --- a/packages/skia/src/dom/types/Common.ts +++ b/packages/skia/src/dom/types/Common.ts @@ -70,6 +70,12 @@ export interface TransformProps { matrix?: InputMatrix; } +export interface CTMProps extends TransformProps { + clip?: ClipDef; + invertClip?: boolean; + layer?: SkPaint | boolean; +} + export interface PaintProps extends ChildrenProps { color?: Color; strokeWidth?: number; @@ -83,8 +89,4 @@ export interface PaintProps extends ChildrenProps { dither?: boolean; } -export interface GroupProps extends PaintProps, TransformProps { - clip?: ClipDef; - invertClip?: boolean; - layer?: SkPaint | boolean; -} +export interface GroupProps extends PaintProps, CTMProps {} diff --git a/packages/skia/src/renderer/Canvas.tsx b/packages/skia/src/renderer/Canvas.tsx index bf6f300334..91c804d7fd 100644 --- a/packages/skia/src/renderer/Canvas.tsx +++ b/packages/skia/src/renderer/Canvas.tsx @@ -1,32 +1,24 @@ -import React, { - useEffect, +import { + forwardRef, useCallback, + useEffect, + useImperativeHandle, useMemo, - forwardRef, useRef, } from "react"; -import type { - RefObject, - ReactNode, - MutableRefObject, - ForwardedRef, - FunctionComponent, -} from "react"; -import type { LayoutChangeEvent } from "react-native"; +import type { LayoutChangeEvent, ViewProps } from "react-native"; +import type { SharedValue } from "react-native-reanimated"; -import { SkiaDomView } from "../views"; +import { SkiaViewNativeId } from "../views/SkiaViewNativeId"; +import SkiaPictureViewNativeComponent from "../specs/SkiaPictureViewNativeComponent"; +import type { SkRect, SkSize } from "../skia/types"; +import { SkiaSGRoot } from "../sksg/Reconciler"; +import { Skia } from "../skia"; import type { SkiaBaseViewProps } from "../views"; -import { SkiaRoot } from "./Reconciler"; - -export const useCanvasRef = () => useRef(null); - -export interface CanvasProps extends SkiaBaseViewProps { - ref?: RefObject; - children: ReactNode; - mode?: "default" | "continuous"; -} +const NativeSkiaPictureView = SkiaPictureViewNativeComponent; +// TODO: no need to go through the JS thread for this const useOnSizeEvent = ( resultValue: SkiaBaseViewProps["onSize"], onLayout?: (event: LayoutChangeEvent) => void @@ -46,39 +38,40 @@ const useOnSizeEvent = ( ); }; -export const Canvas = forwardRef( +export interface CanvasProps extends ViewProps { + debug?: boolean; + opaque?: boolean; + onSize?: SharedValue; + mode?: "continuous" | "default"; +} + +export const Canvas = forwardRef( ( { - children, - style, + mode, debug, - mode = "default", - onSize: _onSize, + opaque, + children, + onSize, onLayout: _onLayout, - ...props - }, - forwardedRef + ...viewProps + }: CanvasProps, + ref ) => { - const onLayout = useOnSizeEvent(_onSize, _onLayout); - const innerRef = useCanvasRef(); - const ref = useCombinedRefs(forwardedRef, innerRef); - const redraw = useCallback(() => { - innerRef.current?.redraw(); - }, [innerRef]); - const getNativeId = useCallback(() => { - const id = innerRef.current?.nativeId ?? -1; - return id; - }, [innerRef]); + const rafId = useRef(null); + const onLayout = useOnSizeEvent(onSize, _onLayout); + // Native ID + const nativeId = useMemo(() => { + return SkiaViewNativeId.current++; + }, []); - const root = useMemo( - () => new SkiaRoot(redraw, getNativeId), - [redraw, getNativeId] - ); + // Root + const root = useMemo(() => new SkiaSGRoot(Skia, nativeId), [nativeId]); - // Render effect + // Render effects useEffect(() => { root.render(children); - }, [children, root, redraw]); + }, [children, root]); useEffect(() => { return () => { @@ -86,41 +79,50 @@ export const Canvas = forwardRef( }; }, [root]); + const requestRedraw = useCallback(() => { + rafId.current = requestAnimationFrame(() => { + root.render(children); + if (mode === "continuous") { + requestRedraw(); + } + }); + }, [children, mode, root]); + + useEffect(() => { + if (mode === "continuous") { + requestRedraw(); + } + return () => { + if (rafId.current !== null) { + cancelAnimationFrame(rafId.current); + } + }; + }, [mode, requestRedraw]); + + // Component methods + useImperativeHandle(ref, () => ({ + makeImageSnapshot: (rect?: SkRect) => { + return SkiaViewApi.makeImageSnapshot(nativeId, rect); + }, + makeImageSnapshotAsync: (rect?: SkRect) => { + return SkiaViewApi.makeImageSnapshotAsync(nativeId, rect); + }, + redraw: () => { + SkiaViewApi.requestRedraw(nativeId); + }, + getNativeId: () => { + return nativeId; + }, + })); return ( - ); } -) as FunctionComponent>; - -/** - * Combines a list of refs into a single ref. This can be used to provide - * both a forwarded ref and an internal ref keeping the same functionality - * on both of the refs. - * @param refs Array of refs to combine - * @returns A single ref that can be used in a ref prop. - */ -const useCombinedRefs = ( - ...refs: Array | ForwardedRef> -) => { - const targetRef = React.useRef(null); - React.useEffect(() => { - refs.forEach((ref) => { - if (ref) { - if (typeof ref === "function") { - ref(targetRef.current); - } else { - ref.current = targetRef.current; - } - } - }); - }, [refs]); - return targetRef; -}; +); diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index 7d3daeb094..f539445e04 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -2,13 +2,24 @@ import type { SharedValue } from "react-native-reanimated"; -import type { CircleProps, GlyphsProps } from "../../dom/types"; +import type { + CircleProps, + ClipDef, + CTMProps, + GlyphsProps, +} from "../../dom/types"; import { exhaustiveCheck } from "../../renderer/typeddash"; -import { BlendMode, type Skia } from "../../skia/types"; +import { BlendMode, ClipOp, isRRect } from "../../skia/types"; +import type { SkPath, SkRect, SkRRect, Skia } from "../../skia/types"; import { isSharedValue } from "../nodes"; import { drawCircle, drawGlyphs } from "../nodes/drawings"; import type { StaticContext } from "../StaticContext"; -import { enumKey } from "../../dom/nodes"; +import { + enumKey, + isPathDef, + processPath, + processTransformProps2, +} from "../../dom/nodes"; import type { PaintProps } from "./Paint"; import { CommandType } from "./Recorder"; @@ -17,6 +28,27 @@ const materializeValue = (value: T | SharedValue) => { return (isSharedValue(value) ? value.value : value) as T; }; +const computeClip = ( + Skia: Skia, + clip: ClipDef | undefined +): + | undefined + | { clipPath: SkPath } + | { clipRect: SkRect } + | { clipRRect: SkRRect } => { + "worklet"; + if (clip) { + if (isPathDef(clip)) { + return { clipPath: processPath(Skia, clip) }; + } else if (isRRect(clip)) { + return { clipRRect: clip }; + } else { + return { clipRect: clip }; + } + } + return undefined; +}; + const processColor = ( Skia: Skia, color: number | string | Float32Array | number[] @@ -78,6 +110,49 @@ export const playback = (Skia: Skia, staticCtx: StaticContext) => { case CommandType.PopPaint: paints.pop(); break; + case CommandType.PushCTM: { + const { + clip: rawClip, + invertClip, + matrix, + transform, + origin, + layer, + } = props as CTMProps; + const hasTransform = matrix !== undefined || transform !== undefined; + const clip = computeClip(Skia, rawClip); + const hasClip = clip !== undefined; + const op = invertClip ? ClipOp.Difference : ClipOp.Intersect; + const m3 = processTransformProps2(Skia, { matrix, transform, origin }); + const shouldSave = hasTransform || hasClip || !!layer; + if (shouldSave) { + if (layer) { + if (typeof layer === "boolean") { + canvas.saveLayer(); + } else { + canvas.saveLayer(layer); + } + } else { + canvas.save(); + } + } + if (m3) { + canvas.concat(m3); + } + if (clip) { + if ("clipRect" in clip) { + canvas.clipRect(clip.clipRect, op, true); + } else if ("clipRRect" in clip) { + canvas.clipRRect(clip.clipRRect, op, true); + } else { + canvas.clipPath(clip.clipPath, op, true); + } + } + break; + } + case CommandType.PopCTM: + canvas.restore(); + break; case CommandType.DrawPaint: canvas.drawPaint(paint); break; diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index e1d0053cdd..3841c7fffd 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -2,7 +2,7 @@ import type { SharedValue } from "react-native-reanimated"; -import type { CircleProps, GlyphsProps } from "../../dom/types"; +import type { CircleProps, CTMProps, GlyphsProps } from "../../dom/types"; import { splitProps } from "../nodes"; import type { PaintProps } from "./Paint"; @@ -10,6 +10,8 @@ import type { PaintProps } from "./Paint"; export enum CommandType { PushPaint, PopPaint, + PushCTM, + PopCTM, DrawPaint, DrawGlyphs, DrawCircle, @@ -18,6 +20,8 @@ export enum CommandType { type CommandProps = { [CommandType.PushPaint]: PaintProps; [CommandType.PopPaint]: null; + [CommandType.PushCTM]: CTMProps; + [CommandType.PopCTM]: null; [CommandType.DrawPaint]: null; [CommandType.DrawGlyphs]: GlyphsProps; [CommandType.DrawCircle]: CircleProps; @@ -44,6 +48,15 @@ export class Recorder { this.commands.push({ type: CommandType.PopPaint, props: null }); } + pushCTM(ctmProps: CTMProps) { + const { props, animatedProps } = splitProps(ctmProps); + this.commands.push({ type: CommandType.PushCTM, props, animatedProps }); + } + + popCTM() { + this.commands.push({ type: CommandType.PopCTM, props: null }); + } + drawPaint() { this.commands.push({ type: CommandType.DrawPaint, props: null }); } diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index eb25eda6ff..50607700f7 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ "worklet"; -import type { DrawingNodeProps } from "../../dom/types"; +import type { CTMProps, DrawingNodeProps } from "../../dom/types"; import { NodeType } from "../../dom/types"; import type { Node } from "../nodes"; @@ -21,8 +21,19 @@ function processPaint({ dither, paint: paintProp, }: DrawingNodeProps) { - let shouldRestore = false; - const paint: PaintProps = {}; + const paint: PaintProps = { + opacity, + color, + strokeWidth, + blendMode, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, + }; + if ( opacity !== undefined || color !== undefined || @@ -35,41 +46,27 @@ function processPaint({ antiAlias !== undefined || dither !== undefined ) { - shouldRestore = true; + return paint; } + return null; +} - if (opacity !== undefined) { - paint.opacity = opacity; - } - if (color !== undefined) { - paint.color = color; - } - // if (strokeWidth !== undefined) { - // paint.setStrokeWidth(strokeWidth); - // } - if (blendMode !== undefined) { - paint.blendMode = blendMode; - } - // if (style !== undefined) { - // paint.setStyle(PaintStyle[enumKey(style)]); - // } - // if (strokeJoin !== undefined) { - // paint.setStrokeJoin(StrokeJoin[enumKey(strokeJoin)]); - // } - // if (strokeCap !== undefined) { - // paint.setStrokeCap(StrokeCap[enumKey(strokeCap)]); - // } - // if (strokeMiter !== undefined) { - // paint.setStrokeMiter(strokeMiter); - // } - // if (antiAlias !== undefined) { - // paint.setAntiAlias(antiAlias); - // } - // if (dither !== undefined) { - // paint.setDither(dither); - // } - if (shouldRestore) { - return paint; +function processCTM({ clip, invertClip, transform, origin, matrix }: CTMProps) { + const ctm: CTMProps = { + clip, + invertClip, + transform, + origin, + matrix, + }; + if ( + clip !== undefined || + invertClip !== undefined || + transform !== undefined || + origin !== undefined || + matrix !== undefined + ) { + return ctm; } return null; } @@ -77,9 +74,13 @@ function processPaint({ export function record(recorder: Recorder, root: Node) { const { type, props, children } = root; const paint = processPaint(props as DrawingNodeProps); + const ctm = processCTM(props as DrawingNodeProps); if (paint) { recorder.pushPaint(paint); } + if (ctm) { + recorder.pushCTM(ctm); + } switch (type) { case NodeType.Fill: recorder.drawPaint(); @@ -97,4 +98,7 @@ export function record(recorder: Recorder, root: Node) { if (paint) { recorder.popPaint(); } + if (ctm) { + recorder.popCTM(); + } } From 30151fcebd45ec75f1659dca9a2f03e6b696c2f7 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 12:16:17 +0100 Subject: [PATCH 07/27] :wrench: --- packages/skia/src/sksg/nodes/Node.ts | 5 +---- packages/skia/src/sksg/recorder/Paint.ts | 9 ++------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index a026953995..c69aded445 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -4,15 +4,12 @@ import type { NodeType } from "../../dom/types"; import { isSharedValue } from "./utils"; -type UnknownProps = Record; - type AnimatedProps = Partial<{ [K in keyof T]: SharedValue }>; -export interface Node { +export interface Node { type: NodeType; isDeclaration: boolean; props: Props; - animatedProps?: AnimatedProps; children: Node[]; } diff --git a/packages/skia/src/sksg/recorder/Paint.ts b/packages/skia/src/sksg/recorder/Paint.ts index 4506117825..f9cb36ef64 100644 --- a/packages/skia/src/sksg/recorder/Paint.ts +++ b/packages/skia/src/sksg/recorder/Paint.ts @@ -1,8 +1,3 @@ -import type { SkEnum } from "../../dom/types"; -import type { BlendMode, Color } from "../../skia/types"; +import type { PaintProps as CompPaintProps } from "../../dom/types"; -export interface PaintProps { - color?: Color; - opacity?: number; - blendMode?: SkEnum; -} +export type PaintProps = CompPaintProps; From c1289c575eaf77d343faa633bd4b525d40e46853 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 12:24:14 +0100 Subject: [PATCH 08/27] :wrench: --- packages/skia/src/sksg/recorder/Visitor.ts | 41 ++++++++++++---------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index 50607700f7..0afd8017d2 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -3,24 +3,27 @@ import type { CTMProps, DrawingNodeProps } from "../../dom/types"; import { NodeType } from "../../dom/types"; -import type { Node } from "../nodes"; +import { sortNodes, type Node } from "../nodes"; import type { PaintProps } from "./Paint"; import type { Recorder } from "./Recorder"; -function processPaint({ - opacity, - color, - strokeWidth, - blendMode, - style, - strokeJoin, - strokeCap, - strokeMiter, - antiAlias, - dither, - paint: paintProp, -}: DrawingNodeProps) { +function processPaint( + { + opacity, + color, + strokeWidth, + blendMode, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, + paint: paintProp, + }: DrawingNodeProps, + children: Node[] +) { const paint: PaintProps = { opacity, color, @@ -44,7 +47,8 @@ function processPaint({ strokeCap !== undefined || strokeMiter !== undefined || antiAlias !== undefined || - dither !== undefined + dither !== undefined || + children.length > 0 ) { return paint; } @@ -73,8 +77,9 @@ function processCTM({ clip, invertClip, transform, origin, matrix }: CTMProps) { export function record(recorder: Recorder, root: Node) { const { type, props, children } = root; - const paint = processPaint(props as DrawingNodeProps); - const ctm = processCTM(props as DrawingNodeProps); + const { drawings, declarations } = sortNodes(children); + const paint = processPaint(props, declarations); + const ctm = processCTM(props); if (paint) { recorder.pushPaint(paint); } @@ -92,7 +97,7 @@ export function record(recorder: Recorder, root: Node) { recorder.drawCircle(props); break; } - children.forEach((child) => { + drawings.forEach((child) => { record(recorder, child); }); if (paint) { From 68e7651ff563d5d14abda99a1788a69349e4b5df Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 12:33:11 +0100 Subject: [PATCH 09/27] :wrench: --- packages/skia/src/sksg/nodes/context.ts | 2 +- packages/skia/src/sksg/recorder/Paint.ts | 5 +++- packages/skia/src/sksg/recorder/Playback.ts | 27 ++++++++++++++++++++- packages/skia/src/sksg/recorder/Visitor.ts | 1 + 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/skia/src/sksg/nodes/context.ts b/packages/skia/src/sksg/nodes/context.ts index 0961aaed90..d53934c59d 100644 --- a/packages/skia/src/sksg/nodes/context.ts +++ b/packages/skia/src/sksg/nodes/context.ts @@ -77,7 +77,7 @@ import { makePath2DPathEffect, } from "./pathEffects"; -function processDeclarations(ctx: DeclarationContext, node: Node) { +export function processDeclarations(ctx: DeclarationContext, node: Node) { "worklet"; const processChildren = () => node.children.forEach((child) => processDeclarations(ctx, child)); diff --git a/packages/skia/src/sksg/recorder/Paint.ts b/packages/skia/src/sksg/recorder/Paint.ts index f9cb36ef64..373856dbea 100644 --- a/packages/skia/src/sksg/recorder/Paint.ts +++ b/packages/skia/src/sksg/recorder/Paint.ts @@ -1,3 +1,6 @@ import type { PaintProps as CompPaintProps } from "../../dom/types"; +import type { Node } from "../nodes"; -export type PaintProps = CompPaintProps; +export interface PaintProps extends Omit { + children: Node[]; +} diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index f539445e04..77a201a6c4 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -11,7 +11,7 @@ import type { import { exhaustiveCheck } from "../../renderer/typeddash"; import { BlendMode, ClipOp, isRRect } from "../../skia/types"; import type { SkPath, SkRect, SkRRect, Skia } from "../../skia/types"; -import { isSharedValue } from "../nodes"; +import { isSharedValue, processDeclarations } from "../nodes"; import { drawCircle, drawGlyphs } from "../nodes/drawings"; import type { StaticContext } from "../StaticContext"; import { @@ -20,6 +20,7 @@ import { processPath, processTransformProps2, } from "../../dom/nodes"; +import { createDeclarationContext } from "../DeclarationContext"; import type { PaintProps } from "./Paint"; import { CommandType } from "./Recorder"; @@ -69,6 +70,7 @@ export const playback = (Skia: Skia, staticCtx: StaticContext) => { const recorder = Skia.PictureRecorder(); const canvas = recorder.beginRecording(); const { commands } = staticCtx; + const declCtx = createDeclarationContext(Skia); const paints = [staticCtx.paints[0]]; for (let i = 0; i < commands.length; i++) { const command = commands[i]; @@ -105,6 +107,29 @@ export const playback = (Skia: Skia, staticCtx: StaticContext) => { if (blendMode !== undefined) { paint.setBlendMode(BlendMode[enumKey(materializeValue(blendMode))]); } + (props as PaintProps).children.forEach((child) => { + processDeclarations(declCtx, child); + }); + const colorFilter = declCtx.colorFilters.popAllAsOne(); + const imageFilter = declCtx.imageFilters.popAllAsOne(); + const shader = declCtx.shaders.pop(); + const maskFilter = declCtx.maskFilters.pop(); + const pathEffect = declCtx.pathEffects.popAllAsOne(); + if (colorFilter) { + paint.setColorFilter(colorFilter); + } + if (imageFilter) { + paint.setImageFilter(imageFilter); + } + if (shader) { + paint.setShader(shader); + } + if (maskFilter) { + paint.setMaskFilter(maskFilter); + } + if (pathEffect) { + paint.setPathEffect(pathEffect); + } break; } case CommandType.PopPaint: diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index 0afd8017d2..cac1936c24 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -35,6 +35,7 @@ function processPaint( strokeMiter, antiAlias, dither, + children, }; if ( From 79ffd663cbb11ee3c2585f8fe0775e51678a38d2 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 12:35:36 +0100 Subject: [PATCH 10/27] :wrench: --- apps/paper/src/Examples/Matrix/Matrix.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/paper/src/Examples/Matrix/Matrix.tsx b/apps/paper/src/Examples/Matrix/Matrix.tsx index 2302e0e92d..4613a44c67 100644 --- a/apps/paper/src/Examples/Matrix/Matrix.tsx +++ b/apps/paper/src/Examples/Matrix/Matrix.tsx @@ -44,7 +44,6 @@ export const Matrix = () => { - {cols.map((_i, i) => rows.map((_j, j) => ( Date: Fri, 3 Jan 2025 13:02:01 +0100 Subject: [PATCH 11/27] :wrench: --- apps/paper/src/Examples/Breathe/Breathe.tsx | 2 +- packages/skia/src/sksg/nodes/Node.ts | 14 ++-- packages/skia/src/sksg/recorder/Playback.ts | 90 ++++++++++++++++++++- packages/skia/src/sksg/recorder/Recorder.ts | 73 +++++++++++++---- packages/skia/src/sksg/recorder/Visitor.ts | 67 +++++++++++++-- 5 files changed, 219 insertions(+), 27 deletions(-) diff --git a/apps/paper/src/Examples/Breathe/Breathe.tsx b/apps/paper/src/Examples/Breathe/Breathe.tsx index e0d096a22a..486920a43c 100644 --- a/apps/paper/src/Examples/Breathe/Breathe.tsx +++ b/apps/paper/src/Examples/Breathe/Breathe.tsx @@ -72,7 +72,7 @@ export const Breathe = () => { - + {new Array(6).fill(0).map((_, index) => { return ( diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index c69aded445..b9f630e2e5 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import type { SharedValue } from "react-native-reanimated"; import type { NodeType } from "../../dom/types"; @@ -13,17 +14,20 @@ export interface Node { children: Node[]; } -export const splitProps = (props: T) => { +export const splitProps = (props: T) => { "worklet"; + if (props === null) { + return { props: null }; + } let hasAnimatedProps = false; const animatedProps: Partial> = {}; Object.keys(props).forEach((key) => { - const name = key as keyof T; - const value = props[name]; + // @ts-ignore + const value = props[key]; if (isSharedValue(value)) { hasAnimatedProps = true; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - animatedProps[name] = value as any; + // @ts-ignore + animatedProps[key] = value; } }); return { diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index 77a201a6c4..a2fac67878 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -3,16 +3,53 @@ import type { SharedValue } from "react-native-reanimated"; import type { + AtlasProps, CircleProps, ClipDef, CTMProps, + DiffRectProps, GlyphsProps, + ImageProps, + ImageSVGProps, + LineProps, + OvalProps, + ParagraphProps, + PatchProps, + PathProps, + PictureProps, + PointsProps, + RectProps, + RoundedRectProps, + TextBlobProps, + TextPathProps, + TextProps, + VerticesProps, } from "../../dom/types"; import { exhaustiveCheck } from "../../renderer/typeddash"; import { BlendMode, ClipOp, isRRect } from "../../skia/types"; import type { SkPath, SkRect, SkRRect, Skia } from "../../skia/types"; import { isSharedValue, processDeclarations } from "../nodes"; -import { drawCircle, drawGlyphs } from "../nodes/drawings"; +import { + drawAtlas, + drawCircle, + drawDiffRect, + drawGlyphs, + drawImage, + drawImageSVG, + drawLine, + drawOval, + drawParagraph, + drawPatch, + drawPath, + drawPicture, + drawPoints, + drawRect, + drawRRect, + drawText, + drawTextBlob, + drawTextPath, + drawVertices, +} from "../nodes/drawings"; import type { StaticContext } from "../StaticContext"; import { enumKey, @@ -187,6 +224,57 @@ export const playback = (Skia: Skia, staticCtx: StaticContext) => { case CommandType.DrawCircle: drawCircle(ctx, props as CircleProps); break; + case CommandType.DrawImage: + drawImage(ctx, props as ImageProps); + break; + case CommandType.DrawAtlas: + drawAtlas(ctx, props as AtlasProps); + break; + case CommandType.DrawDiffRect: + drawDiffRect(ctx, props as DiffRectProps); + break; + case CommandType.DrawImageSVG: + drawImageSVG(ctx, props as ImageSVGProps); + break; + case CommandType.DrawLine: + drawLine(ctx, props as LineProps); + break; + case CommandType.DrawOval: + drawOval(ctx, props as OvalProps); + break; + case CommandType.DrawParagraph: + drawParagraph(ctx, props as ParagraphProps); + break; + case CommandType.DrawPatch: + drawPatch(ctx, props as PatchProps); + break; + case CommandType.DrawPath: + drawPath(ctx, props as PathProps); + break; + case CommandType.DrawPicture: + drawPicture(ctx, props as PictureProps); + break; + case CommandType.DrawPoints: + drawPoints(ctx, props as PointsProps); + break; + case CommandType.DrawRect: + drawRect(ctx, props as RectProps); + break; + case CommandType.DrawRRect: + drawRRect(ctx, props as RoundedRectProps); + break; + case CommandType.DrawText: + drawText(ctx, props as TextProps); + break; + case CommandType.DrawTextBlob: + drawTextBlob(ctx, props as TextBlobProps); + break; + case CommandType.DrawTextPath: + drawTextPath(ctx, props as TextPathProps); + break; + case CommandType.DrawVertices: + drawVertices(ctx, props as VerticesProps); + break; default: exhaustiveCheck(command.type); } diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index 3841c7fffd..638c44b7a2 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -1,8 +1,26 @@ -"worklet"; - import type { SharedValue } from "react-native-reanimated"; -import type { CircleProps, CTMProps, GlyphsProps } from "../../dom/types"; +import type { + AtlasProps, + CircleProps, + CTMProps, + DiffRectProps, + GlyphsProps, + ImageProps, + ImageSVGProps, + LineProps, + OvalProps, + ParagraphProps, + PatchProps, + PathProps, + PictureProps, + PointsProps, + RectProps, + RoundedRectProps, + TextPathProps, + TextProps, + VerticesProps, +} from "../../dom/types"; import { splitProps } from "../nodes"; import type { PaintProps } from "./Paint"; @@ -15,6 +33,23 @@ export enum CommandType { DrawPaint, DrawGlyphs, DrawCircle, + DrawImage, + DrawPicture, + DrawImageSVG, + DrawParagraph, + DrawAtlas, + DrawPoints, + DrawPath, + DrawRect, + DrawRRect, + DrawOval, + DrawLine, + DrawPatch, + DrawVertices, + DrawDiffRect, + DrawText, + DrawTextPath, + DrawTextBlob, } type CommandProps = { @@ -25,6 +60,23 @@ type CommandProps = { [CommandType.DrawPaint]: null; [CommandType.DrawGlyphs]: GlyphsProps; [CommandType.DrawCircle]: CircleProps; + [CommandType.DrawImage]: ImageProps; + [CommandType.DrawPicture]: PictureProps; + [CommandType.DrawImageSVG]: ImageSVGProps; + [CommandType.DrawParagraph]: ParagraphProps; + [CommandType.DrawAtlas]: AtlasProps; + [CommandType.DrawPoints]: PointsProps; + [CommandType.DrawPath]: PathProps; + [CommandType.DrawRect]: RectProps; + [CommandType.DrawRRect]: RoundedRectProps; + [CommandType.DrawOval]: OvalProps; + [CommandType.DrawLine]: LineProps; + [CommandType.DrawPatch]: PatchProps; + [CommandType.DrawVertices]: VerticesProps; + [CommandType.DrawDiffRect]: DiffRectProps; + [CommandType.DrawText]: TextProps; + [CommandType.DrawTextPath]: TextPathProps; + [CommandType.DrawTextBlob]: TextProps; }; type AnimatedProps = { @@ -57,17 +109,8 @@ export class Recorder { this.commands.push({ type: CommandType.PopCTM, props: null }); } - drawPaint() { - this.commands.push({ type: CommandType.DrawPaint, props: null }); - } - - drawGlyphs(glyphsProps: GlyphsProps) { - const { props, animatedProps } = splitProps(glyphsProps); - this.commands.push({ type: CommandType.DrawGlyphs, props, animatedProps }); - } - - drawCircle(circleProps: CircleProps) { - const { props, animatedProps } = splitProps(circleProps); - this.commands.push({ type: CommandType.DrawCircle, props, animatedProps }); + draw(type: T, drawProps: CommandProps[T]) { + const { props, animatedProps } = splitProps(drawProps); + this.commands.push({ type, props, animatedProps }); } } diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index cac1936c24..2d9f0ecae4 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -6,7 +6,7 @@ import { NodeType } from "../../dom/types"; import { sortNodes, type Node } from "../nodes"; import type { PaintProps } from "./Paint"; -import type { Recorder } from "./Recorder"; +import { CommandType, type Recorder } from "./Recorder"; function processPaint( { @@ -88,14 +88,71 @@ export function record(recorder: Recorder, root: Node) { recorder.pushCTM(ctm); } switch (type) { - case NodeType.Fill: - recorder.drawPaint(); + case NodeType.Glyphs: + recorder.draw(CommandType.DrawGlyphs, props); + break; + case NodeType.Circle: + recorder.draw(CommandType.DrawCircle, props); + break; + case NodeType.Image: + recorder.draw(CommandType.DrawImage, props); + break; + case NodeType.Points: + recorder.draw(CommandType.DrawPoints, props); + break; + case NodeType.Path: + recorder.draw(CommandType.DrawPath, props); + break; + case NodeType.Rect: + recorder.draw(CommandType.DrawRect, props); + break; + case NodeType.RRect: + recorder.draw(CommandType.DrawRRect, props); + break; + case NodeType.Oval: + recorder.draw(CommandType.DrawOval, props); + break; + case NodeType.Line: + recorder.draw(CommandType.DrawLine, props); + break; + case NodeType.Patch: + recorder.draw(CommandType.DrawPatch, props); + break; + case NodeType.Vertices: + recorder.draw(CommandType.DrawVertices, props); + break; + case NodeType.DiffRect: + recorder.draw(CommandType.DrawDiffRect, props); + break; + case NodeType.Text: + recorder.draw(CommandType.DrawText, props); + break; + case NodeType.TextPath: + recorder.draw(CommandType.DrawTextPath, props); + break; + case NodeType.TextBlob: + recorder.draw(CommandType.DrawTextBlob, props); break; case NodeType.Glyphs: - recorder.drawGlyphs(props); + recorder.draw(CommandType.DrawGlyphs, props); + break; + case NodeType.Picture: + recorder.draw(CommandType.DrawPicture, props); + break; + case NodeType.ImageSVG: + recorder.draw(CommandType.DrawImageSVG, props); + break; + case NodeType.Paragraph: + recorder.draw(CommandType.DrawParagraph, props); + break; + case NodeType.Atlas: + recorder.draw(CommandType.DrawAtlas, props); break; case NodeType.Circle: - recorder.drawCircle(props); + recorder.draw(CommandType.DrawCircle, props); + break; + case NodeType.Fill: + recorder.draw(CommandType.DrawPaint, props); break; } drawings.forEach((child) => { From 464a12069e8130bf96166ce329c97b8948812a76 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 13:13:31 +0100 Subject: [PATCH 12/27] :wrench: --- packages/skia/src/sksg/Container.ts | 14 +++--- packages/skia/src/sksg/recorder/Playback.ts | 54 ++++++++++++++++++--- packages/skia/src/sksg/recorder/Visitor.ts | 4 +- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index ee894e45b7..fa1b995471 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -9,9 +9,8 @@ import { import type { StaticContext } from "./StaticContext"; import { createStaticContext } from "./StaticContext"; -import { createDrawingContext } from "./DrawingContext"; import type { Node } from "./nodes"; -import { draw, isSharedValue } from "./nodes"; +import { isSharedValue } from "./nodes"; import { Recorder } from "./recorder/Recorder"; import { record } from "./recorder/Visitor"; import { playback } from "./recorder/Playback"; @@ -23,7 +22,11 @@ const drawOnscreen = ( ) => { "worklet"; const start = performance.now(); - const picture = playback(Skia, staticCtx); + + const recorder = Skia.PictureRecorder(); + const canvas = recorder.beginRecording(); + playback(Skia, canvas, staticCtx); + const picture = recorder.finishRecordingAsPicture(); const end = performance.now(); console.log("Recording time: ", end - start); SkiaViewApi.setJsiProperty(nativeId, "picture", picture); @@ -105,9 +108,6 @@ export class Container { } drawOnCanvas(canvas: SkCanvas) { - const ctx = createDrawingContext(this.Skia, canvas, this._staticCtx!); - this.root.forEach((node) => { - draw(ctx, node); - }); + playback(this.Skia, canvas, this._staticCtx!); } } diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index a2fac67878..955b05ff7c 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -26,8 +26,15 @@ import type { VerticesProps, } from "../../dom/types"; import { exhaustiveCheck } from "../../renderer/typeddash"; -import { BlendMode, ClipOp, isRRect } from "../../skia/types"; -import type { SkPath, SkRect, SkRRect, Skia } from "../../skia/types"; +import { + BlendMode, + ClipOp, + isRRect, + PaintStyle, + StrokeCap, + StrokeJoin, +} from "../../skia/types"; +import type { SkPath, SkRect, SkRRect, Skia, SkCanvas } from "../../skia/types"; import { isSharedValue, processDeclarations } from "../nodes"; import { drawAtlas, @@ -103,9 +110,11 @@ const processColor = ( } }; -export const playback = (Skia: Skia, staticCtx: StaticContext) => { - const recorder = Skia.PictureRecorder(); - const canvas = recorder.beginRecording(); +export const playback = ( + Skia: Skia, + canvas: SkCanvas, + staticCtx: StaticContext +) => { const { commands } = staticCtx; const declCtx = createDeclarationContext(Skia); const paints = [staticCtx.paints[0]]; @@ -131,7 +140,18 @@ export const playback = (Skia: Skia, staticCtx: StaticContext) => { childPaint.assign(paint); paints.push(childPaint); paint = childPaint; - const { opacity, color, blendMode } = props as PaintProps; + const { + opacity, + color, + blendMode, + strokeWidth, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, + } = props as PaintProps; if (opacity !== undefined) { paint.setAlphaf(paint.getAlphaf() * materializeValue(opacity)); } @@ -144,6 +164,27 @@ export const playback = (Skia: Skia, staticCtx: StaticContext) => { if (blendMode !== undefined) { paint.setBlendMode(BlendMode[enumKey(materializeValue(blendMode))]); } + if (strokeWidth !== undefined) { + paint.setStrokeWidth(strokeWidth); + } + if (style !== undefined) { + paint.setStyle(PaintStyle[enumKey(style)]); + } + if (strokeJoin !== undefined) { + paint.setStrokeJoin(StrokeJoin[enumKey(strokeJoin)]); + } + if (strokeCap !== undefined) { + paint.setStrokeCap(StrokeCap[enumKey(strokeCap)]); + } + if (strokeMiter !== undefined) { + paint.setStrokeMiter(strokeMiter); + } + if (antiAlias !== undefined) { + paint.setAntiAlias(antiAlias); + } + if (dither !== undefined) { + paint.setDither(dither); + } (props as PaintProps).children.forEach((child) => { processDeclarations(declCtx, child); }); @@ -279,5 +320,4 @@ export const playback = (Skia: Skia, staticCtx: StaticContext) => { exhaustiveCheck(command.type); } } - return recorder.finishRecordingAsPicture(); }; diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index 2d9f0ecae4..f08449616b 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -20,8 +20,8 @@ function processPaint( strokeMiter, antiAlias, dither, - paint: paintProp, - }: DrawingNodeProps, + }: // paint: paintProp, + DrawingNodeProps, children: Node[] ) { const paint: PaintProps = { From 8444efb96c54f57b95cd76f36f1d630f53f2fb2a Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 13:42:26 +0100 Subject: [PATCH 13/27] :wrench: --- packages/skia/src/sksg/recorder/Playback.ts | 40 ++++++++++++++++++++- packages/skia/src/sksg/recorder/Recorder.ts | 15 ++++++++ packages/skia/src/sksg/recorder/Visitor.ts | 23 +++++++++--- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index 955b05ff7c..7edab5c2f3 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -34,7 +34,15 @@ import { StrokeCap, StrokeJoin, } from "../../skia/types"; -import type { SkPath, SkRect, SkRRect, Skia, SkCanvas } from "../../skia/types"; +import type { + SkPath, + SkRect, + SkRRect, + Skia, + SkCanvas, + SkImageFilter, +} from "../../skia/types"; +import type { Node } from "../nodes"; import { isSharedValue, processDeclarations } from "../nodes"; import { drawAtlas, @@ -253,6 +261,36 @@ export const playback = ( } break; } + case CommandType.PushLayer: { + const dCtx = createDeclarationContext(ctx.Skia); + processDeclarations(dCtx, props as Node); + const p = dCtx.paints.pop(); + if (p) { + ctx.canvas.saveLayer(p); + } + break; + } + case CommandType.PopLayer: + canvas.restore(); + break; + case CommandType.BackdropFilter: { + let imageFilter: SkImageFilter | null = null; + // TODO: can we use the main declaration context here? + const dCtx = createDeclarationContext(ctx.Skia); + processDeclarations(dCtx, props as Node); + const imgf = dCtx.imageFilters.pop(); + if (imgf) { + imageFilter = imgf; + } else { + const cf = dCtx.colorFilters.pop(); + if (cf) { + imageFilter = Skia.ImageFilter.MakeColorFilter(cf, null); + } + } + canvas.saveLayer(undefined, null, imageFilter); + canvas.restore(); + break; + } case CommandType.PopCTM: canvas.restore(); break; diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index 638c44b7a2..3ab2b2ed3c 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -21,6 +21,7 @@ import type { TextProps, VerticesProps, } from "../../dom/types"; +import type { Node } from "../nodes"; import { splitProps } from "../nodes"; import type { PaintProps } from "./Paint"; @@ -50,6 +51,9 @@ export enum CommandType { DrawText, DrawTextPath, DrawTextBlob, + BackdropFilter, + PushLayer, + PopLayer, } type CommandProps = { @@ -77,6 +81,9 @@ type CommandProps = { [CommandType.DrawText]: TextProps; [CommandType.DrawTextPath]: TextPathProps; [CommandType.DrawTextBlob]: TextProps; + [CommandType.BackdropFilter]: Node; + [CommandType.PushLayer]: Node[]; + [CommandType.PopLayer]: null; }; type AnimatedProps = { @@ -109,6 +116,14 @@ export class Recorder { this.commands.push({ type: CommandType.PopCTM, props: null }); } + pushLayer(props: Node[]) { + this.commands.push({ type: CommandType.PushLayer, props }); + } + + popLayer() { + this.commands.push({ type: CommandType.PopLayer, props: null }); + } + draw(type: T, drawProps: CommandProps[T]) { const { props, animatedProps } = splitProps(drawProps); this.commands.push({ type, props, animatedProps }); diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index f08449616b..2500b2f100 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -81,6 +81,7 @@ export function record(recorder: Recorder, root: Node) { const { drawings, declarations } = sortNodes(children); const paint = processPaint(props, declarations); const ctm = processCTM(props); + let skipChildren = false; if (paint) { recorder.pushPaint(paint); } @@ -88,6 +89,18 @@ export function record(recorder: Recorder, root: Node) { recorder.pushCTM(ctm); } switch (type) { + case NodeType.BackdropFilter: + recorder.draw(CommandType.BackdropFilter, declarations[0]); + skipChildren = true; + break; + case NodeType.Layer: + recorder.pushLayer(declarations); + drawings.forEach((child) => { + record(recorder, child); + }); + recorder.popLayer(); + skipChildren = true; + break; case NodeType.Glyphs: recorder.draw(CommandType.DrawGlyphs, props); break; @@ -152,12 +165,14 @@ export function record(recorder: Recorder, root: Node) { recorder.draw(CommandType.DrawCircle, props); break; case NodeType.Fill: - recorder.draw(CommandType.DrawPaint, props); + recorder.draw(CommandType.DrawPaint, null); break; } - drawings.forEach((child) => { - record(recorder, child); - }); + if (!skipChildren) { + drawings.forEach((child) => { + record(recorder, child); + }); + } if (paint) { recorder.popPaint(); } From f757c1af4c8609f41528dcc34fd797862608ce7c Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 14:35:00 +0100 Subject: [PATCH 14/27] :wrench: --- packages/skia/src/sksg/recorder/Playback.ts | 2 +- packages/skia/src/sksg/recorder/Visitor.ts | 27 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index 7edab5c2f3..7488336bcf 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -263,7 +263,7 @@ export const playback = ( } case CommandType.PushLayer: { const dCtx = createDeclarationContext(ctx.Skia); - processDeclarations(dCtx, props as Node); + processDeclarations(dCtx, (props as Node[])[0]); const p = dCtx.paints.pop(); if (p) { ctx.canvas.saveLayer(p); diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index 2500b2f100..aae123bfae 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -76,6 +76,25 @@ function processCTM({ clip, invertClip, transform, origin, matrix }: CTMProps) { return null; } +const extraPaints = (node: Node) => { + const nodes: Node[] = []; + node.children.forEach((child) => { + if (child.type === NodeType.Paint) { + const clone: Node = { + type: node.type, + isDeclaration: node.isDeclaration, + children: [...child.children], + props: { ...node.props, ...child.props }, + }; + nodes.push(clone); + } + }); + if (nodes.length === 0) { + return null; + } + return nodes; +}; + export function record(recorder: Recorder, root: Node) { const { type, props, children } = root; const { drawings, declarations } = sortNodes(children); @@ -88,6 +107,7 @@ export function record(recorder: Recorder, root: Node) { if (ctm) { recorder.pushCTM(ctm); } + switch (type) { case NodeType.BackdropFilter: recorder.draw(CommandType.BackdropFilter, declarations[0]); @@ -168,6 +188,13 @@ export function record(recorder: Recorder, root: Node) { recorder.draw(CommandType.DrawPaint, null); break; } + const nodes = extraPaints(root); + if (nodes) { + nodes.forEach((node) => { + record(recorder, node); + }); + return; + } if (!skipChildren) { drawings.forEach((child) => { record(recorder, child); From e88a1ae14f048cc7c357eb1d5c3ac6e935f3f5b2 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 15:04:38 +0100 Subject: [PATCH 15/27] :wrench: --- packages/skia/src/sksg/recorder/Playback.ts | 63 ++++++++++++--------- packages/skia/src/sksg/recorder/Recorder.ts | 9 ++- packages/skia/src/sksg/recorder/Visitor.ts | 16 +++++- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index 7488336bcf..b72db14ca9 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -41,6 +41,7 @@ import type { Skia, SkCanvas, SkImageFilter, + SkPaint, } from "../../skia/types"; import type { Node } from "../nodes"; import { isSharedValue, processDeclarations } from "../nodes"; @@ -193,31 +194,37 @@ export const playback = ( if (dither !== undefined) { paint.setDither(dither); } - (props as PaintProps).children.forEach((child) => { - processDeclarations(declCtx, child); - }); - const colorFilter = declCtx.colorFilters.popAllAsOne(); - const imageFilter = declCtx.imageFilters.popAllAsOne(); - const shader = declCtx.shaders.pop(); - const maskFilter = declCtx.maskFilters.pop(); - const pathEffect = declCtx.pathEffects.popAllAsOne(); - if (colorFilter) { - paint.setColorFilter(colorFilter); - } - if (imageFilter) { - paint.setImageFilter(imageFilter); - } - if (shader) { - paint.setShader(shader); - } - if (maskFilter) { - paint.setMaskFilter(maskFilter); - } - if (pathEffect) { - paint.setPathEffect(pathEffect); + const pProps = props as PaintProps; + if (pProps.children.length > 0) { + pProps.children.forEach((child) => { + processDeclarations(declCtx, child); + }); + const colorFilter = declCtx.colorFilters.popAllAsOne(); + const imageFilter = declCtx.imageFilters.popAllAsOne(); + const shader = declCtx.shaders.pop(); + const maskFilter = declCtx.maskFilters.pop(); + const pathEffect = declCtx.pathEffects.popAllAsOne(); + if (colorFilter) { + paint.setColorFilter(colorFilter); + } + if (imageFilter) { + paint.setImageFilter(imageFilter); + } + if (shader) { + paint.setShader(shader); + } + if (maskFilter) { + paint.setMaskFilter(maskFilter); + } + if (pathEffect) { + paint.setPathEffect(pathEffect); + } } break; } + case CommandType.PushStaticPaint: + paints.push(props as SkPaint); + break; case CommandType.PopPaint: paints.pop(); break; @@ -263,10 +270,14 @@ export const playback = ( } case CommandType.PushLayer: { const dCtx = createDeclarationContext(ctx.Skia); - processDeclarations(dCtx, (props as Node[])[0]); - const p = dCtx.paints.pop(); - if (p) { - ctx.canvas.saveLayer(p); + if (props) { + processDeclarations(dCtx, props as Node); + const p = dCtx.paints.pop(); + if (p) { + ctx.canvas.saveLayer(p); + } + } else { + ctx.canvas.saveLayer(); } break; } diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index 3ab2b2ed3c..db81398c1c 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -23,6 +23,7 @@ import type { } from "../../dom/types"; import type { Node } from "../nodes"; import { splitProps } from "../nodes"; +import type { SkPaint } from "../../skia"; import type { PaintProps } from "./Paint"; @@ -54,6 +55,7 @@ export enum CommandType { BackdropFilter, PushLayer, PopLayer, + PushStaticPaint, } type CommandProps = { @@ -84,6 +86,7 @@ type CommandProps = { [CommandType.BackdropFilter]: Node; [CommandType.PushLayer]: Node[]; [CommandType.PopLayer]: null; + [CommandType.PushStaticPaint]: SkPaint; }; type AnimatedProps = { @@ -103,6 +106,10 @@ export class Recorder { this.commands.push({ type: CommandType.PushPaint, props }); } + pushStaticPaint(props: SkPaint) { + this.commands.push({ type: CommandType.PushStaticPaint, props }); + } + popPaint() { this.commands.push({ type: CommandType.PopPaint, props: null }); } @@ -116,7 +123,7 @@ export class Recorder { this.commands.push({ type: CommandType.PopCTM, props: null }); } - pushLayer(props: Node[]) { + pushLayer(props: Node) { this.commands.push({ type: CommandType.PushLayer, props }); } diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index aae123bfae..c87640e927 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -97,6 +97,18 @@ const extraPaints = (node: Node) => { export function record(recorder: Recorder, root: Node) { const { type, props, children } = root; + if (props.paint) { + recorder.pushStaticPaint(props.paint); + const clone = { + type: root.type, + isDeclaration: root.isDeclaration, + props: { ...root.props, paint: undefined }, + children: [...root.children], + }; + record(recorder, clone); + recorder.popPaint(); + return; + } const { drawings, declarations } = sortNodes(children); const paint = processPaint(props, declarations); const ctm = processCTM(props); @@ -114,8 +126,8 @@ export function record(recorder: Recorder, root: Node) { skipChildren = true; break; case NodeType.Layer: - recorder.pushLayer(declarations); - drawings.forEach((child) => { + recorder.pushLayer(children[0]); + children.forEach((child) => { record(recorder, child); }); recorder.popLayer(); From 38bab9de2d9188853bea88ebffe1a34393867483 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 16:07:18 +0100 Subject: [PATCH 16/27] :wrench: --- packages/skia/src/sksg/recorder/Playback.ts | 12 ++--- packages/skia/src/sksg/recorder/Visitor.ts | 56 +++++++++++++++++++-- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index b72db14ca9..366cd6cd37 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -270,15 +270,9 @@ export const playback = ( } case CommandType.PushLayer: { const dCtx = createDeclarationContext(ctx.Skia); - if (props) { - processDeclarations(dCtx, props as Node); - const p = dCtx.paints.pop(); - if (p) { - ctx.canvas.saveLayer(p); - } - } else { - ctx.canvas.saveLayer(); - } + processDeclarations(dCtx, props as Node); + const p = declCtx.paints.pop(); + ctx.canvas.saveLayer(p); break; } case CommandType.PopLayer: diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index c87640e927..aa687dc840 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -102,7 +102,20 @@ export function record(recorder: Recorder, root: Node) { const clone = { type: root.type, isDeclaration: root.isDeclaration, - props: { ...root.props, paint: undefined }, + props: { + ...root.props, + paint: undefined, + color: undefined, + strokeWidth: undefined, + blendMode: undefined, + style: undefined, + strokeJoin: undefined, + strokeCap: undefined, + strokeMiter: undefined, + opacity: undefined, + antiAlias: undefined, + dither: undefined, + }, children: [...root.children], }; record(recorder, clone); @@ -126,13 +139,46 @@ export function record(recorder: Recorder, root: Node) { skipChildren = true; break; case NodeType.Layer: - recorder.pushLayer(children[0]); - children.forEach((child) => { - record(recorder, child); + const [layer, ...remainingChildren] = children; + let hasLayer = false; + if (layer && layer.isDeclaration && layer.type === NodeType.Paint) { + hasLayer = true; + recorder.pushLayer(layer); + } + remainingChildren.forEach((child) => { + if (!child.isDeclaration) { + record(recorder, child); + } }); - recorder.popLayer(); + if (hasLayer) { + recorder.popLayer(); + } skipChildren = true; break; + /* + if (node.type === NodeType.Layer) { + let hasLayer = false; + const [layer, ...children] = node.children; + if (layer.isDeclaration) { + const declCtx = createDeclarationContext(ctx.Skia); + processDeclarations(declCtx, layer); + const paint = declCtx.paints.pop(); + if (paint) { + hasLayer = true; + ctx.canvas.saveLayer(paint); + } + } + children.map((child) => { + if (!child.isDeclaration) { + draw(ctx, child); + } + }); + if (hasLayer) { + ctx.canvas.restore(); + } + return; + } + */ case NodeType.Glyphs: recorder.draw(CommandType.DrawGlyphs, props); break; From 0cea4030007bc670c74444e66c740c573d32ce69 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 16:24:35 +0100 Subject: [PATCH 17/27] :wrench: --- packages/skia/src/sksg/recorder/Visitor.ts | 47 +++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index aa687dc840..b504c8cbbe 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -56,20 +56,29 @@ function processPaint( return null; } -function processCTM({ clip, invertClip, transform, origin, matrix }: CTMProps) { +function processCTM({ + clip, + invertClip, + transform, + origin, + matrix, + layer, +}: CTMProps) { const ctm: CTMProps = { clip, invertClip, transform, origin, matrix, + layer, }; if ( clip !== undefined || invertClip !== undefined || transform !== undefined || origin !== undefined || - matrix !== undefined + matrix !== undefined || + layer !== undefined ) { return ctm; } @@ -96,6 +105,23 @@ const extraPaints = (node: Node) => { }; export function record(recorder: Recorder, root: Node) { + if (root.type === NodeType.Layer) { + const [layer, ...remainingChildren] = root.children; + let hasLayer = false; + if (layer.isDeclaration) { + hasLayer = true; + recorder.pushLayer(layer); + } + remainingChildren.forEach((child) => { + if (!child.isDeclaration) { + record(recorder, child); + } + }); + if (hasLayer) { + recorder.popLayer(); + } + return; + } const { type, props, children } = root; if (props.paint) { recorder.pushStaticPaint(props.paint); @@ -138,23 +164,6 @@ export function record(recorder: Recorder, root: Node) { recorder.draw(CommandType.BackdropFilter, declarations[0]); skipChildren = true; break; - case NodeType.Layer: - const [layer, ...remainingChildren] = children; - let hasLayer = false; - if (layer && layer.isDeclaration && layer.type === NodeType.Paint) { - hasLayer = true; - recorder.pushLayer(layer); - } - remainingChildren.forEach((child) => { - if (!child.isDeclaration) { - record(recorder, child); - } - }); - if (hasLayer) { - recorder.popLayer(); - } - skipChildren = true; - break; /* if (node.type === NodeType.Layer) { let hasLayer = false; From c2a5902ebb252bc262fb9474c440ce7e377785df Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 17:31:44 +0100 Subject: [PATCH 18/27] :wrench: --- apps/paper/src/Examples/Matrix/Matrix.tsx | 1 + packages/skia/src/sksg/recorder/Playback.ts | 5 ++- packages/skia/src/sksg/recorder/Visitor.ts | 46 ++++----------------- 3 files changed, 12 insertions(+), 40 deletions(-) diff --git a/apps/paper/src/Examples/Matrix/Matrix.tsx b/apps/paper/src/Examples/Matrix/Matrix.tsx index 4613a44c67..a1c0640b40 100644 --- a/apps/paper/src/Examples/Matrix/Matrix.tsx +++ b/apps/paper/src/Examples/Matrix/Matrix.tsx @@ -44,6 +44,7 @@ export const Matrix = () => { + {cols.map((_i, i) => rows.map((_j, j) => ( { const nodes: Node[] = []; node.children.forEach((child) => { if (child.type === NodeType.Paint) { - const clone: Node = { + const clone = { type: node.type, isDeclaration: node.isDeclaration, - children: [...child.children], + children: child.children, props: { ...node.props, ...child.props }, }; nodes.push(clone); @@ -107,21 +106,16 @@ const extraPaints = (node: Node) => { export function record(recorder: Recorder, root: Node) { if (root.type === NodeType.Layer) { const [layer, ...remainingChildren] = root.children; - let hasLayer = false; - if (layer.isDeclaration) { - hasLayer = true; + if (layer.isDeclaration && layer.type === NodeType.Paint) { recorder.pushLayer(layer); - } - remainingChildren.forEach((child) => { - if (!child.isDeclaration) { + remainingChildren.forEach((child) => { record(recorder, child); - } - }); - if (hasLayer) { + }); recorder.popLayer(); } return; } + const { type, props, children } = root; if (props.paint) { recorder.pushStaticPaint(props.paint); @@ -142,7 +136,7 @@ export function record(recorder: Recorder, root: Node) { antiAlias: undefined, dither: undefined, }, - children: [...root.children], + children: root.children, }; record(recorder, clone); recorder.popPaint(); @@ -164,30 +158,6 @@ export function record(recorder: Recorder, root: Node) { recorder.draw(CommandType.BackdropFilter, declarations[0]); skipChildren = true; break; - /* - if (node.type === NodeType.Layer) { - let hasLayer = false; - const [layer, ...children] = node.children; - if (layer.isDeclaration) { - const declCtx = createDeclarationContext(ctx.Skia); - processDeclarations(declCtx, layer); - const paint = declCtx.paints.pop(); - if (paint) { - hasLayer = true; - ctx.canvas.saveLayer(paint); - } - } - children.map((child) => { - if (!child.isDeclaration) { - draw(ctx, child); - } - }); - if (hasLayer) { - ctx.canvas.restore(); - } - return; - } - */ case NodeType.Glyphs: recorder.draw(CommandType.DrawGlyphs, props); break; From e7a86e166919a1f150582bcae87fbb23fcc66699 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 17:37:51 +0100 Subject: [PATCH 19/27] :wrench: --- packages/skia/src/sksg/recorder/Playback.ts | 6 ++++++ packages/skia/src/sksg/recorder/Recorder.ts | 3 +++ packages/skia/src/sksg/recorder/Visitor.ts | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index ddce189a8c..3f554c06c0 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -4,6 +4,7 @@ import type { SharedValue } from "react-native-reanimated"; import type { AtlasProps, + BoxProps, CircleProps, ClipDef, CTMProps, @@ -47,6 +48,7 @@ import type { Node } from "../nodes"; import { isSharedValue, processDeclarations } from "../nodes"; import { drawAtlas, + drawBox, drawCircle, drawDiffRect, drawGlyphs, @@ -300,6 +302,10 @@ export const playback = ( case CommandType.PopCTM: canvas.restore(); break; + case CommandType.DrawBox: + const payload = props as { props: BoxProps; children: Node[] }; + drawBox(ctx, payload.props, payload.children); + break; case CommandType.DrawPaint: canvas.drawPaint(paint); break; diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index db81398c1c..ad4679303f 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -2,6 +2,7 @@ import type { SharedValue } from "react-native-reanimated"; import type { AtlasProps, + BoxProps, CircleProps, CTMProps, DiffRectProps, @@ -52,6 +53,7 @@ export enum CommandType { DrawText, DrawTextPath, DrawTextBlob, + DrawBox, BackdropFilter, PushLayer, PopLayer, @@ -87,6 +89,7 @@ type CommandProps = { [CommandType.PushLayer]: Node[]; [CommandType.PopLayer]: null; [CommandType.PushStaticPaint]: SkPaint; + [CommandType.DrawBox]: { props: BoxProps; children: Node[] }; }; type AnimatedProps = { diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index e5425fb644..0e221d6c15 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -154,6 +154,10 @@ export function record(recorder: Recorder, root: Node) { } switch (type) { + case NodeType.Box: + recorder.draw(CommandType.DrawBox, { props, children }); + skipChildren = true; + break; case NodeType.BackdropFilter: recorder.draw(CommandType.BackdropFilter, declarations[0]); skipChildren = true; From 064cb4b1183c7e52cfa0dc8b14b7311502aa03d6 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 17:43:38 +0100 Subject: [PATCH 20/27] :wrench: --- packages/skia/src/sksg/DrawingContext.ts | 238 ----------------------- packages/skia/src/sksg/nodes/context.ts | 196 +------------------ 2 files changed, 2 insertions(+), 432 deletions(-) delete mode 100644 packages/skia/src/sksg/DrawingContext.ts diff --git a/packages/skia/src/sksg/DrawingContext.ts b/packages/skia/src/sksg/DrawingContext.ts deleted file mode 100644 index d27532b790..0000000000 --- a/packages/skia/src/sksg/DrawingContext.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { - enumKey, - isPathDef, - processPath, - processTransformProps2, -} from "../dom/nodes"; -import type { ClipDef, DrawingNodeProps, GroupProps } from "../dom/types"; -import { - BlendMode, - ClipOp, - isRRect, - PaintStyle, - StrokeCap, - StrokeJoin, -} from "../skia/types"; -import type { - SkPath, - SkRect, - SkRRect, - SkCanvas, - Skia, - SkPaint, -} from "../skia/types"; - -import { createDeclarationContext } from "./DeclarationContext"; -import type { StaticContext } from "./StaticContext"; - -const computeClip = ( - Skia: Skia, - clip: ClipDef | undefined -): - | undefined - | { clipPath: SkPath } - | { clipRect: SkRect } - | { clipRRect: SkRRect } => { - "worklet"; - if (clip) { - if (isPathDef(clip)) { - return { clipPath: processPath(Skia, clip) }; - } else if (isRRect(clip)) { - return { clipRRect: clip }; - } else { - return { clipRect: clip }; - } - } - return undefined; -}; - -const processColor = ( - Skia: Skia, - color: number | string | Float32Array | number[] -) => { - "worklet"; - if (typeof color === "string" || typeof color === "number") { - return Skia.Color(color); - } else if (Array.isArray(color) || color instanceof Float32Array) { - return color instanceof Float32Array ? color : new Float32Array(color); - } else { - throw new Error( - `Invalid color type: ${typeof color}. Expected number, string, or array.` - ); - } -}; - -export const createDrawingContext = ( - Skia: Skia, - canvas: SkCanvas, - staticCtx: StaticContext -) => { - "worklet"; - const state = { - staticCtx, - declCtx: createDeclarationContext(Skia), - paints: [Skia.Paint()], - }; - - const getPaint = () => { - return state.paints[state.paints.length - 1]; - }; - - 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; - } - let shouldRestore = false; - const colorFilter = declCtx.colorFilters.popAllAsOne(); - const imageFilter = declCtx.imageFilters.popAllAsOne(); - const shader = declCtx.shaders.pop(); - const maskFilter = declCtx.maskFilters.pop(); - const pathEffect = declCtx.pathEffects.popAllAsOne(); - - if ( - opacity !== undefined || - color !== undefined || - strokeWidth !== undefined || - blendMode !== undefined || - style !== undefined || - strokeJoin !== undefined || - strokeCap !== undefined || - strokeMiter !== undefined || - antiAlias !== undefined || - dither !== undefined || - colorFilter !== undefined || - imageFilter !== undefined || - shader !== undefined || - maskFilter !== undefined || - pathEffect !== undefined - ) { - if (!shouldRestore) { - 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; - } - } - - const paint = getPaint(); - if (opacity !== undefined) { - paint.setAlphaf(paint.getAlphaf() * opacity); - } - if (color !== undefined) { - const currentOpacity = paint.getAlphaf(); - paint.setShader(null); - paint.setColor(processColor(Skia, color)); - paint.setAlphaf(currentOpacity * paint.getAlphaf()); - } - if (strokeWidth !== undefined) { - paint.setStrokeWidth(strokeWidth); - } - if (blendMode !== undefined) { - paint.setBlendMode(BlendMode[enumKey(blendMode)]); - } - if (style !== undefined) { - paint.setStyle(PaintStyle[enumKey(style)]); - } - if (strokeJoin !== undefined) { - paint.setStrokeJoin(StrokeJoin[enumKey(strokeJoin)]); - } - if (strokeCap !== undefined) { - paint.setStrokeCap(StrokeCap[enumKey(strokeCap)]); - } - if (strokeMiter !== undefined) { - paint.setStrokeMiter(strokeMiter); - } - if (antiAlias !== undefined) { - paint.setAntiAlias(antiAlias); - } - if (dither !== undefined) { - paint.setDither(dither); - } - if (colorFilter) { - paint.setColorFilter(colorFilter); - } - if (imageFilter) { - paint.setImageFilter(imageFilter); - } - if (shader) { - paint.setShader(shader); - } - if (maskFilter) { - paint.setMaskFilter(maskFilter); - } - if (pathEffect) { - paint.setPathEffect(pathEffect); - } - return shouldRestore; - }; - - const processMatrixAndClipping = ( - props: GroupProps, - layer?: boolean | SkPaint - ) => { - const hasTransform = - props.matrix !== undefined || props.transform !== undefined; - const clip = computeClip(Skia, props.clip); - const hasClip = clip !== undefined; - const op = props.invertClip ? ClipOp.Difference : ClipOp.Intersect; - const m3 = processTransformProps2(Skia, props); - const shouldSave = hasTransform || hasClip || !!layer; - - if (shouldSave) { - if (layer) { - if (typeof layer === "boolean") { - canvas.saveLayer(); - } else { - canvas.saveLayer(layer); - } - } else { - canvas.save(); - } - } - - if (m3) { - canvas.concat(m3); - } - if (clip) { - if ("clipRect" in clip) { - canvas.clipRect(clip.clipRect, op, true); - } else if ("clipRRect" in clip) { - canvas.clipRRect(clip.clipRRect, op, true); - } else { - canvas.clipPath(clip.clipPath, op, true); - } - } - return shouldSave; - }; - - return { - Skia, - canvas, - restore: () => state.paints.pop(), - getPaint, - processPaint, - processMatrixAndClipping, - declCtx: state.declCtx, - }; -}; - -export type DrawingContext = ReturnType; diff --git a/packages/skia/src/sksg/nodes/context.ts b/packages/skia/src/sksg/nodes/context.ts index d53934c59d..7c3626b15e 100644 --- a/packages/skia/src/sksg/nodes/context.ts +++ b/packages/skia/src/sksg/nodes/context.ts @@ -1,37 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { NodeType } from "../../dom/types"; -import type { DrawingNodeProps } from "../../dom/types"; -import type { DrawingContext } from "../DrawingContext"; -import type { SkImageFilter } from "../../skia/types"; -import { - createDeclarationContext, - type DeclarationContext, -} from "../DeclarationContext"; +import { type DeclarationContext } from "../DeclarationContext"; -import { sortNodes, type Node } from "./Node"; -import { - drawAtlas, - drawBox, - drawCircle, - drawDiffRect, - drawFill, - drawGlyphs, - drawImage, - drawImageSVG, - drawLine, - drawOval, - drawParagraph, - drawPatch, - drawPath, - drawPicture, - drawPoints, - drawRect, - drawRRect, - drawText, - drawTextBlob, - drawTextPath, - drawVertices, -} from "./drawings"; +import { type Node } from "./Node"; import { composeColorFilters, declareLerpColorFilter, @@ -244,166 +215,3 @@ export function processDeclarations(ctx: DeclarationContext, node: Node) { console.log("Unknown declaration node: ", type); } } - -const preProcessContext = ( - ctx: DrawingContext, - props: DrawingNodeProps, - declarationChildren: Node[] -) => { - "worklet"; - const shouldRestoreMatrix = ctx.processMatrixAndClipping(props, props.layer); - declarationChildren.forEach((child) => { - processDeclarations(ctx.declCtx, child); - }); - const shouldRestorePaint = ctx.processPaint(props); - return { - shouldRestoreMatrix, - shouldRestorePaint, - extraPaints: ctx.declCtx.paints.popAll(), - }; -}; - -const drawBackdropFilter = (ctx: DrawingContext, node: Node) => { - "worklet"; - const { canvas, Skia } = ctx; - const child = node.children[0]; - let imageFilter: SkImageFilter | null = null; - if (child.isDeclaration) { - const declCtx = createDeclarationContext(ctx.Skia); - processDeclarations(declCtx, child); - const imgf = declCtx.imageFilters.pop(); - if (imgf) { - imageFilter = imgf; - } else { - const cf = declCtx.colorFilters.pop(); - if (cf) { - imageFilter = Skia.ImageFilter.MakeColorFilter(cf, null); - } - } - } - canvas.saveLayer(undefined, null, imageFilter); - canvas.restore(); -}; - -export function draw(ctx: DrawingContext, node: Node) { - "worklet"; - // Special mixed nodes - if (node.type === NodeType.BackdropFilter) { - drawBackdropFilter(ctx, node); - return; - } - if (node.type === NodeType.Layer) { - let hasLayer = false; - const [layer, ...children] = node.children; - if (layer.isDeclaration) { - const declCtx = createDeclarationContext(ctx.Skia); - processDeclarations(declCtx, layer); - const paint = declCtx.paints.pop(); - if (paint) { - hasLayer = true; - ctx.canvas.saveLayer(paint); - } - } - children.map((child) => { - if (!child.isDeclaration) { - draw(ctx, child); - } - }); - if (hasLayer) { - ctx.canvas.restore(); - } - 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, declarations); - const paints = [ctx.getPaint(), ...extraPaints]; - paints.forEach((paint) => { - const lctx = { paint, Skia: ctx.Skia, canvas: ctx.canvas }; - switch (type) { - case NodeType.Box: - drawBox(lctx, props, node.children); - break; - case NodeType.Image: - drawImage(lctx, props); - break; - case NodeType.Points: - drawPoints(lctx, props); - break; - case NodeType.Path: - drawPath(lctx, props); - break; - case NodeType.Rect: - drawRect(lctx, props); - break; - case NodeType.RRect: - drawRRect(lctx, props); - break; - case NodeType.Oval: - drawOval(lctx, props); - break; - case NodeType.Line: - drawLine(lctx, props); - break; - case NodeType.Patch: - drawPatch(lctx, props); - break; - case NodeType.Vertices: - drawVertices(lctx, props); - break; - case NodeType.DiffRect: - drawDiffRect(lctx, props); - break; - case NodeType.Text: - drawText(lctx, props); - break; - case NodeType.TextPath: - drawTextPath(lctx, props); - break; - case NodeType.TextBlob: - drawTextBlob(lctx, props); - break; - case NodeType.Glyphs: - drawGlyphs(lctx, props); - break; - case NodeType.Picture: - drawPicture(lctx, props); - break; - case NodeType.ImageSVG: - drawImageSVG(lctx, props); - break; - case NodeType.Paragraph: - drawParagraph(lctx, props); - break; - case NodeType.Atlas: - drawAtlas(lctx, props); - break; - case NodeType.Circle: - drawCircle(lctx, props); - break; - case NodeType.Fill: - drawFill(lctx, props); - break; - case NodeType.Group: - // TODO: do nothing - break; - default: - if (!node.isDeclaration) { - console.warn(`Unsupported node type: ${type}`); - } - } - }); - drawings.forEach((child) => { - draw(ctx, child); - }); - if (shouldRestoreMatrix) { - ctx.canvas.restore(); - } - if (shouldRestorePaint) { - ctx.restore(); - } -} From 05dfeacf259a049bf838a327263c2b35d01509a4 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 19:49:14 +0100 Subject: [PATCH 21/27] :wrench: --- packages/skia/src/sksg/nodes/drawings.ts | 16 +----- packages/skia/src/sksg/recorder/Playback.ts | 28 +++++++---- packages/skia/src/sksg/recorder/Recorder.ts | 56 +++++++++++++-------- packages/skia/src/sksg/recorder/Visitor.ts | 2 +- 4 files changed, 56 insertions(+), 46 deletions(-) diff --git a/packages/skia/src/sksg/nodes/drawings.ts b/packages/skia/src/sksg/nodes/drawings.ts index 350cdc6e66..3285f9108c 100644 --- a/packages/skia/src/sksg/nodes/drawings.ts +++ b/packages/skia/src/sksg/nodes/drawings.ts @@ -3,7 +3,6 @@ import { enumKey, fitRects, inflate, - NodeType, processCircle, processPath, processRect, @@ -51,9 +50,6 @@ import { VertexMode, } from "../../skia/types"; -import type { Node } from "./Node"; -import { materialize } from "./utils"; - interface LocalDrawingContext { Skia: Skia; canvas: SkCanvas; @@ -75,22 +71,14 @@ export const drawOval = (ctx: LocalDrawingContext, props: OvalProps) => { export const drawBox = ( ctx: LocalDrawingContext, props: BoxProps, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - children: Node[] + shadows: BoxShadowProps[] ) => { "worklet"; const { paint, Skia, canvas } = ctx; const { box: defaultBox } = props; const opacity = paint.getAlphaf(); const box = isRRect(defaultBox) ? defaultBox : Skia.RRectXY(defaultBox, 0, 0); - const shadows = children - .map((node) => { - if (node.type === NodeType.BoxShadow) { - return materialize(node.props); - } - return null; - }) - .filter((n): n is BoxShadowProps => n !== null); + shadows .filter((shadow) => !shadow.inner) .map((shadow) => { diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index 3f554c06c0..6e679c26e9 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ "worklet"; import type { SharedValue } from "react-native-reanimated"; @@ -121,6 +122,15 @@ const processColor = ( } }; +const materializeProps = (props: any, animatedProps?: any) => { + if (animatedProps) { + Object.keys(animatedProps).forEach((key) => { + props[key] = animatedProps[key].value; + }); + } + return props; +}; + export const playback = ( Skia: Skia, canvas: SkCanvas, @@ -132,14 +142,9 @@ export const playback = ( for (let i = 0; i < commands.length; i++) { const command = commands[i]; let paint = paints[paints.length - 1]; - const { props } = command; - if (command.animatedProps) { - Object.keys(command.animatedProps).forEach((key) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - props[key] = command.animatedProps[key].value; - }); - } + const { props, payload, animatedProps } = command; + materializeProps(props, animatedProps); + const ctx = { canvas, Skia, paint }; switch (command.type) { case CommandType.PushPaint: { @@ -303,8 +308,11 @@ export const playback = ( canvas.restore(); break; case CommandType.DrawBox: - const payload = props as { props: BoxProps; children: Node[] }; - drawBox(ctx, payload.props, payload.children); + drawBox( + ctx, + props as BoxProps, + payload.map((s: any) => materializeProps(s.props, s.animatedProps)) + ); break; case CommandType.DrawPaint: canvas.drawPaint(paint); diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index ad4679303f..177071320c 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -1,26 +1,28 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import type { SharedValue } from "react-native-reanimated"; -import type { - AtlasProps, - BoxProps, - CircleProps, - CTMProps, - DiffRectProps, - GlyphsProps, - ImageProps, - ImageSVGProps, - LineProps, - OvalProps, - ParagraphProps, - PatchProps, - PathProps, - PictureProps, - PointsProps, - RectProps, - RoundedRectProps, - TextPathProps, - TextProps, - VerticesProps, +import { + NodeType, + type AtlasProps, + type BoxProps, + type CircleProps, + type CTMProps, + type DiffRectProps, + type GlyphsProps, + type ImageProps, + type ImageSVGProps, + type LineProps, + type OvalProps, + type ParagraphProps, + type PatchProps, + type PathProps, + type PictureProps, + type PointsProps, + type RectProps, + type RoundedRectProps, + type TextPathProps, + type TextProps, + type VerticesProps, } from "../../dom/types"; import type { Node } from "../nodes"; import { splitProps } from "../nodes"; @@ -100,6 +102,7 @@ export interface Command { type: T; props: CommandProps[T]; animatedProps?: Partial>; + payload?: any; } export class Recorder { @@ -134,6 +137,17 @@ export class Recorder { this.commands.push({ type: CommandType.PopLayer, props: null }); } + drawBox(props: BoxProps, children: Node[]) { + this.commands.push({ + type: CommandType.DrawBox, + props, + payload: children + .filter((child) => child.type === NodeType.BoxShadow) + .map((child) => child.props) + .map((p) => splitProps(p)), + }); + } + draw(type: T, drawProps: CommandProps[T]) { const { props, animatedProps } = splitProps(drawProps); this.commands.push({ type, props, animatedProps }); diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index 0e221d6c15..9dda8bfba4 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -155,7 +155,7 @@ export function record(recorder: Recorder, root: Node) { switch (type) { case NodeType.Box: - recorder.draw(CommandType.DrawBox, { props, children }); + recorder.drawBox(props, children); skipChildren = true; break; case NodeType.BackdropFilter: From b22a7898d23837ff00ad8ac964cd1bffa6096f84 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 19:56:36 +0100 Subject: [PATCH 22/27] :wrench: --- packages/skia/src/sksg/recorder/Playback.ts | 8 ++- packages/skia/src/sksg/recorder/Recorder.ts | 55 +++++++++++---------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index 6e679c26e9..e26b1c44d3 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -79,6 +79,7 @@ import { import { createDeclarationContext } from "../DeclarationContext"; import type { PaintProps } from "./Paint"; +import type { DrawBoxCommand } from "./Recorder"; import { CommandType } from "./Recorder"; const materializeValue = (value: T | SharedValue) => { @@ -142,7 +143,7 @@ export const playback = ( for (let i = 0; i < commands.length; i++) { const command = commands[i]; let paint = paints[paints.length - 1]; - const { props, payload, animatedProps } = command; + const { props, animatedProps } = command; materializeProps(props, animatedProps); const ctx = { canvas, Skia, paint }; @@ -308,10 +309,13 @@ export const playback = ( canvas.restore(); break; case CommandType.DrawBox: + const drawCommand = command as DrawBoxCommand; drawBox( ctx, props as BoxProps, - payload.map((s: any) => materializeProps(s.props, s.animatedProps)) + drawCommand.shadows.map((s: any) => + materializeProps(s.props, s.animatedProps) + ) ); break; case CommandType.DrawPaint: diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index 177071320c..e24523e3ec 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -1,28 +1,28 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import type { SharedValue } from "react-native-reanimated"; -import { - NodeType, - type AtlasProps, - type BoxProps, - type CircleProps, - type CTMProps, - type DiffRectProps, - type GlyphsProps, - type ImageProps, - type ImageSVGProps, - type LineProps, - type OvalProps, - type ParagraphProps, - type PatchProps, - type PathProps, - type PictureProps, - type PointsProps, - type RectProps, - type RoundedRectProps, - type TextPathProps, - type TextProps, - type VerticesProps, +import { NodeType } from "../../dom/types"; +import type { + BoxShadowProps, + AtlasProps, + BoxProps, + CircleProps, + CTMProps, + DiffRectProps, + GlyphsProps, + ImageProps, + ImageSVGProps, + LineProps, + OvalProps, + ParagraphProps, + PatchProps, + PathProps, + PictureProps, + PointsProps, + RectProps, + RoundedRectProps, + TextPathProps, + TextProps, + VerticesProps, } from "../../dom/types"; import type { Node } from "../nodes"; import { splitProps } from "../nodes"; @@ -102,7 +102,10 @@ export interface Command { type: T; props: CommandProps[T]; animatedProps?: Partial>; - payload?: any; +} + +export interface DrawBoxCommand extends Command { + shadows: BoxShadowProps[]; } export class Recorder { @@ -141,11 +144,11 @@ export class Recorder { this.commands.push({ type: CommandType.DrawBox, props, - payload: children + shadows: children .filter((child) => child.type === NodeType.BoxShadow) .map((child) => child.props) .map((p) => splitProps(p)), - }); + } as Command); } draw(type: T, drawProps: CommandProps[T]) { From 6849654d1db1d0e4db52c1d5fd04b0235ae7046b Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 20:49:58 +0100 Subject: [PATCH 23/27] :wrench: --- .../src/sksg/__tests__/Declarations.spec.tsx | 50 ++++++++++++++ packages/skia/src/sksg/recorder/Recorder.ts | 67 +++---------------- 2 files changed, 58 insertions(+), 59 deletions(-) diff --git a/packages/skia/src/sksg/__tests__/Declarations.spec.tsx b/packages/skia/src/sksg/__tests__/Declarations.spec.tsx index 41ad286cde..f43de13785 100644 --- a/packages/skia/src/sksg/__tests__/Declarations.spec.tsx +++ b/packages/skia/src/sksg/__tests__/Declarations.spec.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { NodeType } from "../../dom/types"; import type { Node } from "../nodes"; +import { CommandType } from "../recorder/Recorder"; import type { SkColorFilter } from "./MockDeclaration"; import { @@ -71,6 +72,55 @@ const processContext = (ctx: DeclarationContext, node: Node) => { }; describe("Declarations", () => { + it("Deeply nested declarations can be flattened", () => { + const commands = [ + { + type: CommandType.PushColorFilter, + }, + { + type: CommandType.PushColorFilter, + }, + { + type: CommandType.PopColorFilter, + nodeType: NodeType.BlendColorFilter, + props: { + color: "lightblue", + mode: "srcIn", + }, + }, + { + type: CommandType.PopColorFilter, + nodeType: NodeType.SRGBToLinearGammaColorFilter, + props: {}, + }, + ]; + const ctx = new DeclarationContext(); + commands.forEach((command) => { + switch (command.type) { + case CommandType.PushColorFilter: + ctx.colorFilters.save(); + break; + case CommandType.PopColorFilter: + const { nodeType } = command; + switch (nodeType) { + case NodeType.SRGBToLinearGammaColorFilter: + ctx.colorFilters.push(makeSRGBToLinearGammaColorFilter()); + break; + case NodeType.BlendColorFilter: + const cf = ctx.colorFilters.popAllAsOne(); + if (cf) { + ctx.colorFilters.push(compose(makeBlendColorFilter(), cf)); + } else { + ctx.colorFilters.push(makeBlendColorFilter()); + } + break; + } + } + }); + const cf = ctx.colorFilters.popAllAsOne(); + expect(cf).toBeDefined(); + console.log(cf!.tag); + }); it("should create a filter from a tree 1", () => { const tree: Node = { type: NodeType.Group, diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index e24523e3ec..4f11a770ae 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -1,29 +1,7 @@ import type { SharedValue } from "react-native-reanimated"; import { NodeType } from "../../dom/types"; -import type { - BoxShadowProps, - AtlasProps, - BoxProps, - CircleProps, - CTMProps, - DiffRectProps, - GlyphsProps, - ImageProps, - ImageSVGProps, - LineProps, - OvalProps, - ParagraphProps, - PatchProps, - PathProps, - PictureProps, - PointsProps, - RectProps, - RoundedRectProps, - TextPathProps, - TextProps, - VerticesProps, -} from "../../dom/types"; +import type { BoxShadowProps, BoxProps, CTMProps } from "../../dom/types"; import type { Node } from "../nodes"; import { splitProps } from "../nodes"; import type { SkPaint } from "../../skia"; @@ -60,48 +38,19 @@ export enum CommandType { PushLayer, PopLayer, PushStaticPaint, + PushColorFilter, + PopColorFilter, } -type CommandProps = { - [CommandType.PushPaint]: PaintProps; - [CommandType.PopPaint]: null; - [CommandType.PushCTM]: CTMProps; - [CommandType.PopCTM]: null; - [CommandType.DrawPaint]: null; - [CommandType.DrawGlyphs]: GlyphsProps; - [CommandType.DrawCircle]: CircleProps; - [CommandType.DrawImage]: ImageProps; - [CommandType.DrawPicture]: PictureProps; - [CommandType.DrawImageSVG]: ImageSVGProps; - [CommandType.DrawParagraph]: ParagraphProps; - [CommandType.DrawAtlas]: AtlasProps; - [CommandType.DrawPoints]: PointsProps; - [CommandType.DrawPath]: PathProps; - [CommandType.DrawRect]: RectProps; - [CommandType.DrawRRect]: RoundedRectProps; - [CommandType.DrawOval]: OvalProps; - [CommandType.DrawLine]: LineProps; - [CommandType.DrawPatch]: PatchProps; - [CommandType.DrawVertices]: VerticesProps; - [CommandType.DrawDiffRect]: DiffRectProps; - [CommandType.DrawText]: TextProps; - [CommandType.DrawTextPath]: TextPathProps; - [CommandType.DrawTextBlob]: TextProps; - [CommandType.BackdropFilter]: Node; - [CommandType.PushLayer]: Node[]; - [CommandType.PopLayer]: null; - [CommandType.PushStaticPaint]: SkPaint; - [CommandType.DrawBox]: { props: BoxProps; children: Node[] }; -}; - type AnimatedProps = { [P in keyof T]: SharedValue; }; -export interface Command { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface Command { type: T; - props: CommandProps[T]; - animatedProps?: Partial>; + props: Props; + animatedProps?: Partial>; } export interface DrawBoxCommand extends Command { @@ -151,7 +100,7 @@ export class Recorder { } as Command); } - draw(type: T, drawProps: CommandProps[T]) { + draw(type: T, drawProps: object) { const { props, animatedProps } = splitProps(drawProps); this.commands.push({ type, props, animatedProps }); } From 852af27077354123f3df84db0f181921d28cc7ed Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 21:47:39 +0100 Subject: [PATCH 24/27] :wrench: --- packages/skia/src/sksg/Container.ts | 1 + packages/skia/src/sksg/recorder/Paint.ts | 5 +- packages/skia/src/sksg/recorder/Playback.ts | 93 ++++++++++++----- packages/skia/src/sksg/recorder/Recorder.ts | 87 +++++++++------- packages/skia/src/sksg/recorder/Visitor.ts | 110 ++++++++++++++------ 5 files changed, 198 insertions(+), 98 deletions(-) diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index fa1b995471..acfda673ff 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -67,6 +67,7 @@ export class Container { root.forEach((node) => { record(recorder, node); }); + console.log(recorder.commands); this._staticCtx.commands = recorder.commands; } diff --git a/packages/skia/src/sksg/recorder/Paint.ts b/packages/skia/src/sksg/recorder/Paint.ts index 373856dbea..114b2660a3 100644 --- a/packages/skia/src/sksg/recorder/Paint.ts +++ b/packages/skia/src/sksg/recorder/Paint.ts @@ -1,6 +1,3 @@ import type { PaintProps as CompPaintProps } from "../../dom/types"; -import type { Node } from "../nodes"; -export interface PaintProps extends Omit { - children: Node[]; -} +export type PaintProps = Omit; diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index e26b1c44d3..5ee954e427 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -5,6 +5,7 @@ import type { SharedValue } from "react-native-reanimated"; import type { AtlasProps, + BlendColorFilterProps, BoxProps, CircleProps, ClipDef, @@ -44,6 +45,7 @@ import type { SkCanvas, SkImageFilter, SkPaint, + SkColorFilter, } from "../../skia/types"; import type { Node } from "../nodes"; import { isSharedValue, processDeclarations } from "../nodes"; @@ -73,6 +75,7 @@ import type { StaticContext } from "../StaticContext"; import { enumKey, isPathDef, + NodeType, processPath, processTransformProps2, } from "../../dom/nodes"; @@ -202,31 +205,25 @@ export const playback = ( if (dither !== undefined) { paint.setDither(dither); } - const pProps = props as PaintProps; - if (pProps.children.length > 0) { - pProps.children.forEach((child) => { - processDeclarations(declCtx, child); - }); - const colorFilter = declCtx.colorFilters.popAllAsOne(); - const imageFilter = declCtx.imageFilters.popAllAsOne(); - const shader = declCtx.shaders.pop(); - const maskFilter = declCtx.maskFilters.pop(); - const pathEffect = declCtx.pathEffects.popAllAsOne(); - if (colorFilter) { - paint.setColorFilter(colorFilter); - } - if (imageFilter) { - paint.setImageFilter(imageFilter); - } - if (shader) { - paint.setShader(shader); - } - if (maskFilter) { - paint.setMaskFilter(maskFilter); - } - if (pathEffect) { - paint.setPathEffect(pathEffect); - } + const colorFilter = declCtx.colorFilters.popAllAsOne(); + const imageFilter = declCtx.imageFilters.popAllAsOne(); + const shader = declCtx.shaders.pop(); + const maskFilter = declCtx.maskFilters.pop(); + const pathEffect = declCtx.pathEffects.popAllAsOne(); + if (colorFilter) { + paint.setColorFilter(colorFilter); + } + if (imageFilter) { + paint.setImageFilter(imageFilter); + } + if (shader) { + paint.setShader(shader); + } + if (maskFilter) { + paint.setMaskFilter(maskFilter); + } + if (pathEffect) { + paint.setPathEffect(pathEffect); } break; } @@ -236,6 +233,52 @@ export const playback = ( case CommandType.PopPaint: paints.pop(); break; + case CommandType.PushColorFilter: + declCtx.colorFilters.save(); + break; + case CommandType.PopColorFilter: + const cf = declCtx.colorFilters.popAllAsOne(); + let outer: SkColorFilter; + switch (command.nodeType) { + case NodeType.SRGBToLinearGammaColorFilter: + outer = Skia.ColorFilter.MakeSRGBToLinearGamma(); + break; + case NodeType.LinearToSRGBGammaColorFilter: + outer = Skia.ColorFilter.MakeLinearToSRGBGamma(); + break; + case NodeType.BlendColorFilter: + const { mode } = props as BlendColorFilterProps; + const color = ctx.Skia.Color(props.color); + outer = ctx.Skia.ColorFilter.MakeBlend( + color, + BlendMode[enumKey(mode)] + ); + break; + case NodeType.MatrixColorFilter: + const { matrix } = props; + outer = ctx.Skia.ColorFilter.MakeMatrix(matrix); + break; + case NodeType.LumaColorFilter: + outer = Skia.ColorFilter.MakeLumaColorFilter(); + break; + case NodeType.LerpColorFilter: + const { t } = props; + const second = declCtx.colorFilters.pop(); + const first = declCtx.colorFilters.pop(); + if (!first || !second) { + throw new Error( + "LerpColorFilterNode: missing two color filters as children" + ); + } + outer = Skia.ColorFilter.MakeLerp(t, first, second); + break; + } + if (cf) { + declCtx.colorFilters.push(Skia.ColorFilter.MakeCompose(outer!, cf)); + } else { + declCtx.colorFilters.push(outer!); + } + break; case CommandType.PushCTM: { const { clip: rawClip, diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index 4f11a770ae..2598344fea 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -9,37 +9,37 @@ import type { SkPaint } from "../../skia"; import type { PaintProps } from "./Paint"; export enum CommandType { - PushPaint, - PopPaint, - PushCTM, - PopCTM, - DrawPaint, - DrawGlyphs, - DrawCircle, - DrawImage, - DrawPicture, - DrawImageSVG, - DrawParagraph, - DrawAtlas, - DrawPoints, - DrawPath, - DrawRect, - DrawRRect, - DrawOval, - DrawLine, - DrawPatch, - DrawVertices, - DrawDiffRect, - DrawText, - DrawTextPath, - DrawTextBlob, - DrawBox, - BackdropFilter, - PushLayer, - PopLayer, - PushStaticPaint, - PushColorFilter, - PopColorFilter, + PushPaint = "PushPaint", + PopPaint = "PopPaint", + PushCTM = "PushCTM", + PopCTM = "PopCTM", + DrawPaint = "DrawPaint", + DrawGlyphs = "DrawGlyphs", + DrawCircle = "DrawCircle", + DrawImage = "DrawImage", + DrawPicture = "DrawPicture", + DrawImageSVG = "DrawImageSVG", + DrawParagraph = "DrawParagraph", + DrawAtlas = "DrawAtlas", + DrawPoints = "DrawPoints", + DrawPath = "DrawPath", + DrawRect = "DrawRect", + DrawRRect = "DrawRRect", + DrawOval = "DrawOval", + DrawLine = "DrawLine", + DrawPatch = "DrawPatch", + DrawVertices = "DrawVertices", + DrawDiffRect = "DrawDiffRect", + DrawText = "DrawText", + DrawTextPath = "DrawTextPath", + DrawTextBlob = "DrawTextBlob", + DrawBox = "DrawBox", + BackdropFilter = "BackdropFilter", + PushLayer = "PushLayer", + PopLayer = "PopLayer", + PushStaticPaint = "PushStaticPaint", + PushColorFilter = "PushColorFilter", + PopColorFilter = "PopColorFilter", } type AnimatedProps = { @@ -47,11 +47,12 @@ type AnimatedProps = { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface Command { +export type Command = { type: T; props: Props; animatedProps?: Partial>; -} + [key: string]: unknown; +}; export interface DrawBoxCommand extends Command { shadows: BoxShadowProps[]; @@ -60,6 +61,14 @@ export interface DrawBoxCommand extends Command { export class Recorder { public commands: Command[] = []; + pushColorFilter() { + this.commands.push({ type: CommandType.PushColorFilter, props: null }); + } + + popColorFilter(nodeType: NodeType, props: object) { + this.commands.push({ type: CommandType.PopColorFilter, props, nodeType }); + } + pushPaint(props: PaintProps) { this.commands.push({ type: CommandType.PushPaint, props }); } @@ -97,11 +106,15 @@ export class Recorder { .filter((child) => child.type === NodeType.BoxShadow) .map((child) => child.props) .map((p) => splitProps(p)), - } as Command); + }); } - draw(type: T, drawProps: object) { - const { props, animatedProps } = splitProps(drawProps); - this.commands.push({ type, props, animatedProps }); + draw(type: T, drawProps: object | null) { + if (drawProps) { + const { props, animatedProps } = splitProps(drawProps); + this.commands.push({ type, props, animatedProps }); + } else { + this.commands.push({ type, props: null }); + } } } diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index 9dda8bfba4..428939c78f 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -23,19 +23,37 @@ function processPaint( }: DrawingNodeProps, children: Node[] ) { - const paint: PaintProps = { - opacity, - color, - strokeWidth, - blendMode, - style, - strokeJoin, - strokeCap, - strokeMiter, - antiAlias, - dither, - children, - }; + const paint: PaintProps = {}; + if (opacity) { + paint.opacity = opacity; + } + if (color) { + paint.color = color; + } + if (strokeWidth) { + paint.strokeWidth = strokeWidth; + } + if (blendMode) { + paint.blendMode = blendMode; + } + if (style) { + paint.style = style; + } + if (strokeJoin) { + paint.strokeJoin = strokeJoin; + } + if (strokeCap) { + paint.strokeCap = strokeCap; + } + if (strokeMiter) { + paint.strokeMiter = strokeMiter; + } + if (antiAlias) { + paint.antiAlias = antiAlias; + } + if (dither) { + paint.dither = dither; + } if ( opacity !== undefined || @@ -63,14 +81,25 @@ function processCTM({ matrix, layer, }: CTMProps) { - const ctm: CTMProps = { - clip, - invertClip, - transform, - origin, - matrix, - layer, - }; + const ctm: CTMProps = {}; + if (clip) { + ctm.clip = clip; + } + if (invertClip) { + ctm.invertClip = invertClip; + } + if (transform) { + ctm.transform = transform; + } + if (origin) { + ctm.origin = origin; + } + if (matrix) { + ctm.matrix = matrix; + } + if (layer) { + ctm.layer = layer; + } if ( clip !== undefined || invertClip !== undefined || @@ -124,20 +153,20 @@ export function record(recorder: Recorder, root: Node) { isDeclaration: root.isDeclaration, props: { ...root.props, - paint: undefined, - color: undefined, - strokeWidth: undefined, - blendMode: undefined, - style: undefined, - strokeJoin: undefined, - strokeCap: undefined, - strokeMiter: undefined, - opacity: undefined, - antiAlias: undefined, - dither: undefined, }, children: root.children, }; + delete clone.props.paint; + delete clone.props.color; + delete clone.props.strokeWidth; + delete clone.props.blendMode; + delete clone.props.style; + delete clone.props.strokeJoin; + delete clone.props.strokeCap; + delete clone.props.strokeMiter; + delete clone.props.opacity; + delete clone.props.antiAlias; + delete clone.props.dither; record(recorder, clone); recorder.popPaint(); return; @@ -153,6 +182,23 @@ export function record(recorder: Recorder, root: Node) { recorder.pushCTM(ctm); } + declarations.forEach((decl) => { + switch (decl.type) { + case NodeType.LumaColorFilter: + case NodeType.SRGBToLinearGammaColorFilter: + case NodeType.LinearToSRGBGammaColorFilter: + case NodeType.LerpColorFilter: + case NodeType.BlendColorFilter: + case NodeType.MatrixColorFilter: + recorder.pushColorFilter(); + decl.children.forEach((child) => { + record(recorder, child); + }); + recorder.popColorFilter(decl.type, decl.props); + break; + } + }); + switch (type) { case NodeType.Box: recorder.drawBox(props, children); From a3f5dbad20abbf04bffaf6dee170bda13ccf202f Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 21:50:09 +0100 Subject: [PATCH 25/27] :wrench: --- packages/skia/src/sksg/recorder/Visitor.ts | 34 +++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index 428939c78f..acc6b6ce27 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -113,6 +113,25 @@ function processCTM({ return null; } +const recordDeclaration = (recorder: Recorder, decl: Node) => { + switch (decl.type) { + case NodeType.LumaColorFilter: + case NodeType.SRGBToLinearGammaColorFilter: + case NodeType.LinearToSRGBGammaColorFilter: + case NodeType.LerpColorFilter: + case NodeType.BlendColorFilter: + case NodeType.MatrixColorFilter: + recorder.pushColorFilter(); + decl.children.forEach((child) => { + if (child.isDeclaration) { + recordDeclaration(recorder, child); + } + }); + recorder.popColorFilter(decl.type, decl.props); + break; + } +}; + const extraPaints = (node: Node) => { const nodes: Node[] = []; node.children.forEach((child) => { @@ -183,20 +202,7 @@ export function record(recorder: Recorder, root: Node) { } declarations.forEach((decl) => { - switch (decl.type) { - case NodeType.LumaColorFilter: - case NodeType.SRGBToLinearGammaColorFilter: - case NodeType.LinearToSRGBGammaColorFilter: - case NodeType.LerpColorFilter: - case NodeType.BlendColorFilter: - case NodeType.MatrixColorFilter: - recorder.pushColorFilter(); - decl.children.forEach((child) => { - record(recorder, child); - }); - recorder.popColorFilter(decl.type, decl.props); - break; - } + recordDeclaration(recorder, decl); }); switch (type) { From a467baeffa0800ab565ee0f09186166c32dff240 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 21:51:08 +0100 Subject: [PATCH 26/27] :wrench: --- packages/skia/src/sksg/HostConfig.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/skia/src/sksg/HostConfig.ts b/packages/skia/src/sksg/HostConfig.ts index eb97bad790..1ec77c195a 100644 --- a/packages/skia/src/sksg/HostConfig.ts +++ b/packages/skia/src/sksg/HostConfig.ts @@ -129,12 +129,15 @@ export const sksgHostConfig: SkiaHostConfig = { createInstance( type, - props, + rawProps, container, _hostContext, _internalInstanceHandle ) { debug("createInstance", type); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const { children, ...props } = rawProps; container.registerValues(props); const instance = { type, From 7a82ade721c2977a0894e2d4b74ddb0084f16b85 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 23:50:53 +0100 Subject: [PATCH 27/27] :wrench: --- packages/skia/src/sksg/DeclarationContext.ts | 2 +- .../src/sksg/__tests__/Declarations.spec.tsx | 1 - packages/skia/src/sksg/recorder/Playback.ts | 65 +++++++++++-------- packages/skia/src/sksg/recorder/Recorder.ts | 10 +++ packages/skia/src/sksg/recorder/Visitor.ts | 6 ++ 5 files changed, 56 insertions(+), 28 deletions(-) diff --git a/packages/skia/src/sksg/DeclarationContext.ts b/packages/skia/src/sksg/DeclarationContext.ts index cb8c74c1e6..1c71eab6e7 100644 --- a/packages/skia/src/sksg/DeclarationContext.ts +++ b/packages/skia/src/sksg/DeclarationContext.ts @@ -16,7 +16,7 @@ export const composeDeclarations = (filters: T[], composer: Composer) => { if (len <= 1) { return filters[0]; } - return filters.reduceRight((inner, outer) => + return filters.reduce((inner, outer) => inner ? composer(outer, inner) : outer ); }; diff --git a/packages/skia/src/sksg/__tests__/Declarations.spec.tsx b/packages/skia/src/sksg/__tests__/Declarations.spec.tsx index f43de13785..82e2d41b87 100644 --- a/packages/skia/src/sksg/__tests__/Declarations.spec.tsx +++ b/packages/skia/src/sksg/__tests__/Declarations.spec.tsx @@ -119,7 +119,6 @@ describe("Declarations", () => { }); const cf = ctx.colorFilters.popAllAsOne(); expect(cf).toBeDefined(); - console.log(cf!.tag); }); it("should create a filter from a tree 1", () => { const tree: Node = { diff --git a/packages/skia/src/sksg/recorder/Playback.ts b/packages/skia/src/sksg/recorder/Playback.ts index 5ee954e427..c98dc1fe85 100644 --- a/packages/skia/src/sksg/recorder/Playback.ts +++ b/packages/skia/src/sksg/recorder/Playback.ts @@ -6,6 +6,7 @@ import type { SharedValue } from "react-native-reanimated"; import type { AtlasProps, BlendColorFilterProps, + BlurMaskFilterProps, BoxProps, CircleProps, ClipDef, @@ -31,6 +32,7 @@ import type { import { exhaustiveCheck } from "../../renderer/typeddash"; import { BlendMode, + BlurStyle, ClipOp, isRRect, PaintStyle, @@ -79,6 +81,7 @@ import { processPath, processTransformProps2, } from "../../dom/nodes"; +import type { DeclarationContext } from "../DeclarationContext"; import { createDeclarationContext } from "../DeclarationContext"; import type { PaintProps } from "./Paint"; @@ -126,6 +129,29 @@ const processColor = ( } }; +const materializePaint = (paint: SkPaint, declCtx: DeclarationContext) => { + const colorFilter = declCtx.colorFilters.popAllAsOne(); + const imageFilter = declCtx.imageFilters.popAllAsOne(); + const shader = declCtx.shaders.pop(); + const maskFilter = declCtx.maskFilters.pop(); + const pathEffect = declCtx.pathEffects.popAllAsOne(); + if (colorFilter) { + paint.setColorFilter(colorFilter); + } + if (imageFilter) { + paint.setImageFilter(imageFilter); + } + if (shader) { + paint.setShader(shader); + } + if (maskFilter) { + paint.setMaskFilter(maskFilter); + } + if (pathEffect) { + paint.setPathEffect(pathEffect); + } +}; + const materializeProps = (props: any, animatedProps?: any) => { if (animatedProps) { Object.keys(animatedProps).forEach((key) => { @@ -205,26 +231,6 @@ export const playback = ( if (dither !== undefined) { paint.setDither(dither); } - const colorFilter = declCtx.colorFilters.popAllAsOne(); - const imageFilter = declCtx.imageFilters.popAllAsOne(); - const shader = declCtx.shaders.pop(); - const maskFilter = declCtx.maskFilters.pop(); - const pathEffect = declCtx.pathEffects.popAllAsOne(); - if (colorFilter) { - paint.setColorFilter(colorFilter); - } - if (imageFilter) { - paint.setImageFilter(imageFilter); - } - if (shader) { - paint.setShader(shader); - } - if (maskFilter) { - paint.setMaskFilter(maskFilter); - } - if (pathEffect) { - paint.setPathEffect(pathEffect); - } break; } case CommandType.PushStaticPaint: @@ -233,11 +239,19 @@ export const playback = ( case CommandType.PopPaint: paints.pop(); break; + case CommandType.PushBlurMaskFilter: + const { style, blur, respectCTM } = props as BlurMaskFilterProps; + const maskFilter = Skia.MaskFilter.MakeBlur( + BlurStyle[enumKey(style)], + blur, + respectCTM + ); + declCtx.maskFilters.push(maskFilter); + break; case CommandType.PushColorFilter: declCtx.colorFilters.save(); break; case CommandType.PopColorFilter: - const cf = declCtx.colorFilters.popAllAsOne(); let outer: SkColorFilter; switch (command.nodeType) { case NodeType.SRGBToLinearGammaColorFilter: @@ -273,11 +287,7 @@ export const playback = ( outer = Skia.ColorFilter.MakeLerp(t, first, second); break; } - if (cf) { - declCtx.colorFilters.push(Skia.ColorFilter.MakeCompose(outer!, cf)); - } else { - declCtx.colorFilters.push(outer!); - } + declCtx.colorFilters.push(outer!); break; case CommandType.PushCTM: { const { @@ -330,6 +340,9 @@ export const playback = ( case CommandType.PopLayer: canvas.restore(); break; + case CommandType.FinishDeclaration: + materializePaint(paint, declCtx); + break; case CommandType.BackdropFilter: { let imageFilter: SkImageFilter | null = null; // TODO: can we use the main declaration context here? diff --git a/packages/skia/src/sksg/recorder/Recorder.ts b/packages/skia/src/sksg/recorder/Recorder.ts index 2598344fea..2a12cc2356 100644 --- a/packages/skia/src/sksg/recorder/Recorder.ts +++ b/packages/skia/src/sksg/recorder/Recorder.ts @@ -40,6 +40,8 @@ export enum CommandType { PushStaticPaint = "PushStaticPaint", PushColorFilter = "PushColorFilter", PopColorFilter = "PopColorFilter", + FinishDeclaration = "FinishDeclaration", + PushBlurMaskFilter = "PushBlurMaskFilter", } type AnimatedProps = { @@ -69,6 +71,10 @@ export class Recorder { this.commands.push({ type: CommandType.PopColorFilter, props, nodeType }); } + pushBlurMaskFilter(props: object) { + this.commands.push({ type: CommandType.PushBlurMaskFilter, props }); + } + pushPaint(props: PaintProps) { this.commands.push({ type: CommandType.PushPaint, props }); } @@ -98,6 +104,10 @@ export class Recorder { this.commands.push({ type: CommandType.PopLayer, props: null }); } + finishDeclaration() { + this.commands.push({ type: CommandType.FinishDeclaration, props: null }); + } + drawBox(props: BoxProps, children: Node[]) { this.commands.push({ type: CommandType.DrawBox, diff --git a/packages/skia/src/sksg/recorder/Visitor.ts b/packages/skia/src/sksg/recorder/Visitor.ts index acc6b6ce27..ac099a13a6 100644 --- a/packages/skia/src/sksg/recorder/Visitor.ts +++ b/packages/skia/src/sksg/recorder/Visitor.ts @@ -115,6 +115,9 @@ function processCTM({ const recordDeclaration = (recorder: Recorder, decl: Node) => { switch (decl.type) { + case NodeType.BlurMaskFilter: + recorder.pushBlurMaskFilter(decl.props); + break; case NodeType.LumaColorFilter: case NodeType.SRGBToLinearGammaColorFilter: case NodeType.LinearToSRGBGammaColorFilter: @@ -204,6 +207,9 @@ export function record(recorder: Recorder, root: Node) { declarations.forEach((decl) => { recordDeclaration(recorder, decl); }); + if (paint) { + recorder.finishDeclaration(); + } switch (type) { case NodeType.Box: