From 2a0b05dba1d1ada3d21256b94ed6b913233415b8 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Fri, 3 Jan 2025 06:20:39 +0100 Subject: [PATCH 01/50] :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/50] :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 9f1bca0372862f08503348b92e8d6ba952f445f3 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 14:22:36 +0100 Subject: [PATCH 03/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 57 +++++ .../skia/src/sksg/Recorder/DrawingContext.ts | 28 +++ packages/skia/src/sksg/Recorder/Player.ts | 43 ++++ packages/skia/src/sksg/Recorder/Recorder.ts | 73 ++++++ .../sksg/Recorder/commands/ColorFilters.ts | 123 +++++++++ .../src/sksg/__tests__/Declarations.spec.tsx | 235 ------------------ .../src/sksg/__tests__/MockDeclaration.ts | 138 ---------- packages/skia/src/sksg/nodes/Node.ts | 27 +- 8 files changed, 350 insertions(+), 374 deletions(-) create mode 100644 packages/skia/src/sksg/Recorder/Core.ts create mode 100644 packages/skia/src/sksg/Recorder/DrawingContext.ts create mode 100644 packages/skia/src/sksg/Recorder/Player.ts create mode 100644 packages/skia/src/sksg/Recorder/Recorder.ts create mode 100644 packages/skia/src/sksg/Recorder/commands/ColorFilters.ts delete mode 100644 packages/skia/src/sksg/__tests__/Declarations.spec.tsx delete mode 100644 packages/skia/src/sksg/__tests__/MockDeclaration.ts diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts new file mode 100644 index 0000000000..b2113788ba --- /dev/null +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -0,0 +1,57 @@ +import type { SharedValue } from "react-native-reanimated"; + +import type { ImageProps, NodeType } from "../../dom/types"; + +// TODO: remove string labels +export enum CommandType { + // Context + SavePaint = "SavePaint", + RestorePaint = "RestorePaint", + SaveCTM = "SaveCTM", + RestoreCTM = "RestoreCTM", + PushColorFilter = "PushColorFilter", + ComposeColorFilter = "ComposeColorFilter", + MaterializePaint = "MaterializePaint", + // Drawing + DrawImage = "DrawImage", +} + +export type Command = { + type: T; + [key: string]: unknown; +}; + +export const materializeProps = (command: Command) => { + if (command.animatedProps) { + const animatedProps = command.animatedProps as Record< + string, + SharedValue + >; + const commandProps = command.props as Record; + for (const key in animatedProps) { + commandProps[key] = animatedProps[key].value; + } + } +}; + +export const isCommand = ( + command: Command, + type: T +): command is Command => { + return command.type === type; +}; + +interface Props { + [CommandType.DrawImage]: ImageProps; +} + +interface DrawCommand extends Command { + props: T extends keyof Props ? Props[T] : never; +} + +export const isDrawCommand = ( + command: Command, + type: T +): command is DrawCommand => { + return command.type === type; +}; diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts new file mode 100644 index 0000000000..e511921685 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -0,0 +1,28 @@ +"worklet"; + +import type { SkCanvas, SkColorFilter, Skia, SkPaint } from "../../skia/types"; + +export class DrawingContext { + Skia: Skia; + canvas: SkCanvas; + paints: SkPaint[] = []; + colorFilters: SkColorFilter[] = []; + + constructor(Skia: Skia, canvas: SkCanvas) { + this.Skia = Skia; + this.canvas = canvas; + this.paints.push(Skia.Paint()); + } + + savePaint() { + this.paints.push(this.paint.copy()); + } + + get paint() { + return this.paints[this.paints.length - 1]; + } + + restorePaint() { + this.paints.pop(); + } +} diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts new file mode 100644 index 0000000000..9b7956eb49 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -0,0 +1,43 @@ +"worklet"; + +import { drawImage } from "../nodes/drawings"; + +import { + composeColorFilters, + isPushColorFilter, + pushColorFilter, + setColorFilters, +} from "./commands/ColorFilters"; +import { + CommandType, + isCommand, + isDrawCommand, + materializeProps, + type Command, +} from "./Core"; +import type { DrawingContext } from "./DrawingContext"; + +const play = (ctx: DrawingContext, command: Command) => { + materializeProps(command); + if (isCommand(command, CommandType.SavePaint)) { + ctx.savePaint(); + } else if (isCommand(command, CommandType.RestorePaint)) { + ctx.restorePaint(); + } else if (isCommand(command, CommandType.ComposeColorFilter)) { + composeColorFilters(ctx); + } else if (isCommand(command, CommandType.MaterializePaint)) { + setColorFilters(ctx); + } else if (isPushColorFilter(command)) { + pushColorFilter(ctx, command); + } else if (isDrawCommand(command, CommandType.DrawImage)) { + drawImage(ctx, command.props); + } else { + console.warn(`Unknown command: ${command.type}`); + } +}; + +export const replay = (ctx: DrawingContext, commands: Command[]) => { + commands.forEach((command) => { + play(ctx, command); + }); +}; diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts new file mode 100644 index 0000000000..95ed5cf924 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -0,0 +1,73 @@ +import type { SharedValue } from "react-native-reanimated"; + +import type { + CTMProps, + ImageProps, + NodeType, + PaintProps, +} from "../../dom/types"; +import type { AnimatedProps } from "../../renderer"; +import { isSharedValue } from "../nodes/utils"; + +import { CommandType } from "./Core"; +import type { Command } from "./Core"; + +export class Recorder { + commands: Command[] = []; + + private add(command: Command) { + if (command.props) { + const props = command.props as Record; + const animatedProps: Record> = {}; + let hasAnimatedProps = false; + for (const key in command.props) { + const prop = props[key]; + if (isSharedValue(prop)) { + props[key] = prop.value; + animatedProps[key] = prop; + hasAnimatedProps = true; + } + } + if (hasAnimatedProps) { + command.animatedProps = animatedProps; + } + } + this.commands.push(command); + } + + savePaint(props: AnimatedProps) { + this.add({ type: CommandType.SavePaint, props }); + } + + restorePaint() { + this.add({ type: CommandType.RestorePaint }); + } + + materializePaint() { + this.add({ type: CommandType.MaterializePaint }); + } + + pushColorFilter(colorFilterType: NodeType, props: AnimatedProps) { + this.add({ + type: CommandType.PushColorFilter, + colorFilterType, + props, + }); + } + + composeColorFilters() { + this.add({ type: CommandType.ComposeColorFilter }); + } + + saveCTM(props: AnimatedProps) { + this.add({ type: CommandType.SaveCTM, props }); + } + + restoreCTM() { + this.add({ type: CommandType.RestoreCTM }); + } + + drawImage(props: AnimatedProps) { + this.add({ type: CommandType.DrawImage, props }); + } +} diff --git a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts new file mode 100644 index 0000000000..f01997b9ad --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts @@ -0,0 +1,123 @@ +"worklet"; + +import { enumKey } from "../../../dom/nodes"; +import type { + BlendColorFilterProps, + LerpColorFilterProps, + MatrixColorFilterProps, +} from "../../../dom/types"; +import { NodeType } from "../../../dom/types"; +import type { SkColorFilter } from "../../../skia/types"; +import { BlendMode } from "../../../skia/types"; +import { CommandType } from "../Core"; +import type { Command } from "../Core"; +import type { DrawingContext } from "../DrawingContext"; + +export const isPushColorFilter = ( + command: Command +): command is Command => { + return command.type === CommandType.PushColorFilter; +}; + +type Props = { + [NodeType.BlendColorFilter]: BlendColorFilterProps; + [NodeType.MatrixColorFilter]: MatrixColorFilterProps; + [NodeType.LerpColorFilter]: LerpColorFilterProps; + [NodeType.LumaColorFilter]: Record; + [NodeType.LinearToSRGBGammaColorFilter]: Record; + [NodeType.SRGBToLinearGammaColorFilter]: Record; +}; + +interface PushColorFilter + extends Command { + colorFilterType: T; + props: Props[T]; +} + +const isBlendColorFilter = ( + command: Command +): command is PushColorFilter => { + return command.colorFilterType === NodeType.BlendColorFilter; +}; + +const isMatrixColorFilter = ( + command: Command +): command is PushColorFilter => { + return command.colorFilterType === NodeType.MatrixColorFilter; +}; + +const isLerpColorFilter = ( + command: Command +): command is PushColorFilter => { + return command.colorFilterType === NodeType.LerpColorFilter; +}; + +const isLumaColorFilter = ( + command: Command +): command is PushColorFilter => { + return command.colorFilterType === NodeType.LumaColorFilter; +}; + +const isLinearToSRGBGammaColorFilter = ( + command: Command +): command is PushColorFilter => { + return command.colorFilterType === NodeType.LinearToSRGBGammaColorFilter; +}; + +const isSRGBToLinearGammaColorFilter = ( + command: Command +): command is PushColorFilter => { + return command.colorFilterType === NodeType.SRGBToLinearGammaColorFilter; +}; + +export const composeColorFilters = (ctx: DrawingContext) => { + const inner = ctx.colorFilters.pop(); + const outer = ctx.colorFilters.pop(); + if (!inner || !outer) { + throw new Error("Missing color filters to compose"); + } + ctx.colorFilters.push(ctx.Skia.ColorFilter.MakeCompose(outer, inner)); +}; + +export const setColorFilters = (ctx: DrawingContext) => { + ctx.paint.setColorFilter( + ctx.colorFilters.reduceRight((inner, outer) => + inner ? ctx.Skia.ColorFilter.MakeCompose(outer, inner) : outer + ) + ); +}; + +export const pushColorFilter = ( + ctx: DrawingContext, + command: Command +) => { + let cf: SkColorFilter | undefined; + if (isBlendColorFilter(command)) { + const { props } = command; + const { mode } = props; + const color = ctx.Skia.Color(props.color); + cf = ctx.Skia.ColorFilter.MakeBlend(color, BlendMode[enumKey(mode)]); + } else if (isMatrixColorFilter(command)) { + const { matrix } = command.props; + cf = ctx.Skia.ColorFilter.MakeMatrix(matrix); + } else if (isLerpColorFilter(command)) { + const { props } = command; + const { t } = props; + const second = ctx.colorFilters.pop(); + const first = ctx.colorFilters.pop(); + if (!first || !second) { + throw new Error("LerpColorFilter requires two color filters"); + } + cf = ctx.Skia.ColorFilter.MakeLerp(t, first, second); + } else if (isLumaColorFilter(command)) { + cf = ctx.Skia.ColorFilter.MakeLumaColorFilter(); + } else if (isLinearToSRGBGammaColorFilter(command)) { + cf = ctx.Skia.ColorFilter.MakeLinearToSRGBGamma(); + } else if (isSRGBToLinearGammaColorFilter(command)) { + cf = ctx.Skia.ColorFilter.MakeSRGBToLinearGamma(); + } + if (!cf) { + throw new Error(`Unknown color filter type: ${command.colorFilterType}`); + } + ctx.colorFilters.push(cf); +}; diff --git a/packages/skia/src/sksg/__tests__/Declarations.spec.tsx b/packages/skia/src/sksg/__tests__/Declarations.spec.tsx deleted file mode 100644 index 41ad286cde..0000000000 --- a/packages/skia/src/sksg/__tests__/Declarations.spec.tsx +++ /dev/null @@ -1,235 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { NodeType } from "../../dom/types"; -import type { Node } from "../nodes"; - -import type { SkColorFilter } from "./MockDeclaration"; -import { - compose, - DeclarationContext, - DeclarationType, -} from "./MockDeclaration"; - -const makeSRGBToLinearGammaColorFilter = () => ({ - type: DeclarationType.ColorFilter as const, - tag: "SRGBToLinearGamma", -}); - -const makeBlendColorFilter = () => ({ - type: DeclarationType.ColorFilter as const, - tag: "Blend", -}); - -const makeMatrixColorFilter = () => ({ - type: DeclarationType.ColorFilter as const, - tag: "Matrix", -}); - -const makeLerpColorFilter = (children: SkColorFilter[]) => ({ - type: DeclarationType.ColorFilter as const, - tag: `Lerp(0.5, ${children[0].tag}, ${children[1].tag})`, -}); - -const composeColorFilters = ( - ctx: DeclarationContext, - node: Node, - cf: SkColorFilter -) => { - ctx.save(); - node.children.forEach((child) => processContext(ctx, child)); - const cf1 = ctx.colorFilters.popAllAsOne(); - ctx.restore(); - ctx.colorFilters.push(cf1 ? compose(cf, cf1) : cf); -}; - -const processContext = (ctx: DeclarationContext, node: Node) => { - switch (node.type) { - case NodeType.Group: - node.children.forEach((child) => processContext(ctx, child)); - break; - case NodeType.SRGBToLinearGammaColorFilter: { - const cf = makeSRGBToLinearGammaColorFilter(); - composeColorFilters(ctx, node, cf); - break; - } - case NodeType.BlendColorFilter: { - const cf = makeBlendColorFilter(); - composeColorFilters(ctx, node, cf); - break; - } - case NodeType.MatrixColorFilter: { - const cf = makeMatrixColorFilter(); - composeColorFilters(ctx, node, cf); - break; - } - case NodeType.LerpColorFilter: { - node.children.forEach((child) => processContext(ctx, child)); - const cf = makeLerpColorFilter(ctx.colorFilters.popAll()); - ctx.colorFilters.push(cf); - break; - } - } -}; - -describe("Declarations", () => { - it("should create a filter from a tree 1", () => { - const tree: Node = { - type: NodeType.Group, - isDeclaration: false, - props: {}, - children: [ - { - type: NodeType.SRGBToLinearGammaColorFilter, - isDeclaration: true, - props: {}, - children: [ - { - type: NodeType.BlendColorFilter, - isDeclaration: true, - props: { - color: "lightblue", - mode: "srcIn", - }, - children: [], - }, - ], - }, - ], - }; - const ctx = new DeclarationContext(); - processContext(ctx, tree); - const cf = ctx.colorFilters.popAllAsOne(); - expect(cf).toBeDefined(); - expect(cf!.tag).toBe("Compose(SRGBToLinearGamma, Blend)"); - }); - - it("should create a filter from a tree 2", () => { - const tree: Node = { - type: NodeType.Group, - isDeclaration: false, - props: {}, - children: [ - { - type: NodeType.LerpColorFilter, - isDeclaration: true, - props: { - t: 0.5, - }, - children: [ - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - values: [], - }, - children: [], - }, - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - values: [], - }, - children: [], - }, - ], - }, - ], - }; - - const ctx = new DeclarationContext(); - processContext(ctx, tree); - const cf = ctx.colorFilters.popAllAsOne(); - expect(cf).toBeDefined(); - expect(cf!.tag).toBe("Lerp(0.5, Matrix, Matrix)"); - }); - - it("should create a filter from a tree 3", () => { - const tree: Node = { - type: NodeType.Group, - isDeclaration: false, - props: {}, - children: [ - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - value: [], - }, - children: [], - }, - { - type: NodeType.SRGBToLinearGammaColorFilter, - isDeclaration: true, - props: {}, - children: [ - { - type: NodeType.LerpColorFilter, - isDeclaration: true, - props: { - t: 0.5, - }, - children: [ - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - values: [], - }, - children: [], - }, - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - values: [], - }, - children: [], - }, - ], - }, - ], - }, - ], - }; - - const ctx = new DeclarationContext(); - processContext(ctx, tree); - const cf = ctx.colorFilters.popAllAsOne(); - expect(cf).toBeDefined(); - expect(cf!.tag).toBe( - "Compose(Matrix, Compose(SRGBToLinearGamma, Lerp(0.5, Matrix, Matrix)))" - ); - }); - - it("should create a filter from a tree 4", () => { - const tree: Node = { - type: NodeType.Group, - isDeclaration: false, - props: {}, - children: [ - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - value: [], - }, - children: [ - { - type: NodeType.MatrixColorFilter, - isDeclaration: true, - props: { - value: [], - }, - children: [], - }, - ], - }, - ], - }; - const ctx = new DeclarationContext(); - processContext(ctx, tree); - const cf = ctx.colorFilters.popAllAsOne(); - expect(cf).toBeDefined(); - expect(cf!.tag).toBe("Compose(Matrix, Matrix)"); - }); -}); diff --git a/packages/skia/src/sksg/__tests__/MockDeclaration.ts b/packages/skia/src/sksg/__tests__/MockDeclaration.ts deleted file mode 100644 index d6504c9542..0000000000 --- a/packages/skia/src/sksg/__tests__/MockDeclaration.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -export enum DeclarationType { - ColorFilter, - ImageFilter, - Shader, - MaskFilter, - PathEffect, - Paint, -} - -interface Filter { - tag: string; -} - -export interface SkColorFilter extends Filter { - type: DeclarationType.ColorFilter; -} - -interface SkImageFilter extends Filter { - type: DeclarationType.ImageFilter; -} - -interface SkShader extends Filter { - type: DeclarationType.Shader; -} - -interface SkMaskFilter extends Filter { - type: DeclarationType.MaskFilter; -} - -interface SkPathEffect extends Filter { - type: DeclarationType.PathEffect; -} - -interface SkPaint extends Filter { - type: DeclarationType.Paint; -} - -type Composer = (outer: T, inner: T) => T; -export const compose: any = (outer: T, inner: T) => ({ - tag: `Compose(${outer.tag}, ${inner.tag})`, -}); - -export const composeDeclarations = (filters: T[], composer: Composer) => { - if (filters.length <= 1) { - return filters[0]; - } - return filters.reverse().reduce((inner, outer) => { - if (!inner) { - return outer; - } - return composer(outer, inner); - }); -}; - -class Declaration { - public decls: T[] = []; - public indexes = [0]; - public composer?: Composer; - - constructor(composer?: Composer) { - this.composer = composer; - } - - private get index() { - return this.indexes[this.indexes.length - 1]; - } - - save() { - this.indexes.push(this.decls.length); - } - - restore() { - this.indexes.pop(); - } - - pop() { - return this.decls.pop(); - } - - push(decl: T) { - this.decls.push(decl); - } - - popAll() { - return this.decls.splice(this.index, this.decls.length - this.index); - } - - popAllAsOne() { - if (this.decls.length === 0) { - return undefined; - } - if (!this.composer) { - throw new Error("No composer for this type of declaration"); - } - const decls = this.popAll(); - return composeDeclarations(decls, this.composer!); - } -} - -export class DeclarationContext { - readonly paints: Declaration; - readonly maskFilters: Declaration; - readonly shaders: Declaration; - readonly pathEffects: Declaration; - readonly imageFilters: Declaration; - readonly colorFilters: Declaration; - - constructor() { - const peComp: Composer = compose; - const ifComp: Composer = compose; - const cfComp: Composer = compose; - this.paints = new Declaration(); - this.maskFilters = new Declaration(); - this.shaders = new Declaration(); - this.pathEffects = new Declaration(peComp); - this.imageFilters = new Declaration(ifComp); - this.colorFilters = new Declaration(cfComp); - } - - save() { - this.paints.save(); - this.maskFilters.save(); - this.shaders.save(); - this.pathEffects.save(); - this.imageFilters.save(); - this.colorFilters.save(); - } - - restore() { - this.paints.restore(); - this.maskFilters.restore(); - this.shaders.restore(); - this.pathEffects.restore(); - this.imageFilters.restore(); - this.colorFilters.restore(); - } -} diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index b247e99240..2cc4a40f0e 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -1,4 +1,4 @@ -import type { NodeType } from "../../dom/types"; +import { NodeType } from "../../dom/types"; export interface Node { type: NodeType; @@ -7,6 +7,7 @@ export interface Node { children: Node[]; } +// TODO: Remove export const sortNodes = (children: Node[]) => { "worklet"; const declarations: Node[] = []; @@ -22,3 +23,27 @@ export const sortNodes = (children: Node[]) => { return { declarations, drawings }; }; + +export const sortNodeChildren = (parent: Node) => { + "worklet"; + const colorFilters: Node[] = []; + const drawings: Node[] = []; + const declarations: Node[] = []; + parent.children.forEach((node) => { + if ( + node.type === NodeType.BlendColorFilter || + node.type === NodeType.MatrixColorFilter || + node.type === NodeType.LerpColorFilter || + node.type === NodeType.LumaColorFilter || + node.type === NodeType.SRGBToLinearGammaColorFilter || + node.type === NodeType.LinearToSRGBGammaColorFilter + ) { + colorFilters.push(node); + } else if (node.isDeclaration) { + declarations.push(node); + } else { + drawings.push(node); + } + }); + return { colorFilters, drawings, declarations }; +}; From 29b8cdeff51a86cfd51dde9ca0b310216d7ca1f2 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 14:27:20 +0100 Subject: [PATCH 04/50] :wrench: --- apps/paper/src/Examples/Matrix/Matrix.tsx | 1 - packages/skia/src/sksg/Container.ts | 15 +++++--- packages/skia/src/sksg/Recorder/Core.ts | 2 +- packages/skia/src/sksg/Recorder/Visitor.ts | 41 ++++++++++++++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 packages/skia/src/sksg/Recorder/Visitor.ts diff --git a/apps/paper/src/Examples/Matrix/Matrix.tsx b/apps/paper/src/Examples/Matrix/Matrix.tsx index 423585991d..741c111d52 100644 --- a/apps/paper/src/Examples/Matrix/Matrix.tsx +++ b/apps/paper/src/Examples/Matrix/Matrix.tsx @@ -1,6 +1,5 @@ import { BlurMask, - Canvas, Canvas2, Fill, Group, diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index 4fd4211d68..a70bbd22b0 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -12,6 +12,11 @@ import { createStaticContext } from "./StaticContext"; import { createDrawingContext } from "./DrawingContext"; import type { Node } from "./nodes"; import { draw, isSharedValue } from "./nodes"; +import type { Command } from "./Recorder/Core"; +import { Recorder } from "./Recorder/Recorder"; +import { visit } from "./Recorder/Visitor"; +import { DrawingContext } from "./Recorder/DrawingContext"; +import { replay } from "./Recorder/Player"; const drawOnscreen = ( Skia: Skia, @@ -36,6 +41,7 @@ const drawOnscreen = ( export class Container { private _root: Node[] = []; private _staticCtx: StaticContext | null = null; + private _recording: Command[] | null = null; public unmounted = false; private values = new Set>(); @@ -64,6 +70,9 @@ export class Container { } this._root = root; this._staticCtx = createStaticContext(this.Skia); + const recorder = new Recorder(); + visit(recorder, root); + this._recording = recorder.commands; } clear() { @@ -104,9 +113,7 @@ export class Container { } drawOnCanvas(canvas: SkCanvas) { - const ctx = createDrawingContext(this.Skia, canvas, this._staticCtx!); - this.root.forEach((node) => { - draw(ctx, node); - }); + const ctx = new DrawingContext(this.Skia, canvas); + replay(ctx, this._recording!); } } diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index b2113788ba..ab73ccda19 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -1,6 +1,6 @@ import type { SharedValue } from "react-native-reanimated"; -import type { ImageProps, NodeType } from "../../dom/types"; +import type { ImageProps } from "../../dom/types"; // TODO: remove string labels export enum CommandType { diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts new file mode 100644 index 0000000000..c6a1d987a0 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { NodeType } from "../../dom/types"; +import type { Node } from "../nodes"; +import { sortNodeChildren } from "../nodes"; + +import type { Recorder } from "./Recorder"; + +const pushColorFilters = (recorder: Recorder, colorFilters: Node[]) => { + colorFilters.forEach((colorFilter) => { + recorder.pushColorFilter(colorFilter.type, colorFilter.props); + if (colorFilter.children.length > 0) { + pushColorFilters(recorder, colorFilter.children); + recorder.composeColorFilters(); + } + }); +}; + +const visitNode = (recorder: Recorder, node: Node) => { + const { colorFilters, drawings } = sortNodeChildren(node); + const shouldPushPaint = colorFilters.length > 0; + if (shouldPushPaint) { + recorder.savePaint({}); + pushColorFilters(recorder, colorFilters); + recorder.materializePaint(); + } + if (node.type === NodeType.Image) { + recorder.drawImage(node.props); + } + drawings.forEach((drawing) => { + visitNode(recorder, drawing); + }); + if (shouldPushPaint) { + recorder.restorePaint(); + } +}; + +export const visit = (recorder: Recorder, root: Node[]) => { + root.forEach((node) => { + visitNode(recorder, node); + }); +}; From ddeef0f9b7786a42ccdefdeb4e273ebf1ba0e13b Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 14:59:52 +0100 Subject: [PATCH 05/50] :wrench: --- .../e2e/Composition/ColorFilterComposition.spec.tsx | 10 ++++++++++ packages/skia/src/sksg/Container.ts | 1 + packages/skia/src/sksg/Recorder/Visitor.ts | 12 ++++++++++-- .../skia/src/sksg/Recorder/commands/ColorFilters.ts | 7 ++++--- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx index 937591dc61..57059e100e 100644 --- a/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx @@ -65,6 +65,16 @@ describe("Color Filter Composition", () => { const { oslo } = images; const { width, height } = surface; + // Push Matrix + + // Push Matrix + // Push Matrix + // Push Lerp + // Push LinearToSRGBGamma + // Compose + + // Compose + const image = await surface.draw( diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index a70bbd22b0..d0ca92e316 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -114,6 +114,7 @@ export class Container { drawOnCanvas(canvas: SkCanvas) { const ctx = new DrawingContext(this.Skia, canvas); + console.log(this._recording); replay(ctx, this._recording!); } } diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index c6a1d987a0..36c4fd2f06 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -6,13 +6,19 @@ import { sortNodeChildren } from "../nodes"; import type { Recorder } from "./Recorder"; const pushColorFilters = (recorder: Recorder, colorFilters: Node[]) => { + //let lastFilter: NodeType | null = null; colorFilters.forEach((colorFilter) => { - recorder.pushColorFilter(colorFilter.type, colorFilter.props); if (colorFilter.children.length > 0) { pushColorFilters(recorder, colorFilter.children); - recorder.composeColorFilters(); } + recorder.pushColorFilter(colorFilter.type, colorFilter.props); + //lastFilter = colorFilter.type; }); + // If the filter doesn't need children, we compose it + // const needsComposition = lastFilter !== NodeType.LerpColorFilter; + // if (needsComposition) { + // recorder.composeColorFilters(); + // } }; const visitNode = (recorder: Recorder, node: Node) => { @@ -21,6 +27,8 @@ const visitNode = (recorder: Recorder, node: Node) => { if (shouldPushPaint) { recorder.savePaint({}); pushColorFilters(recorder, colorFilters); + recorder.composeColorFilters(); + recorder.composeColorFilters(); recorder.materializePaint(); } if (node.type === NodeType.Image) { diff --git a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts index f01997b9ad..e823e4a904 100644 --- a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts @@ -73,10 +73,11 @@ const isSRGBToLinearGammaColorFilter = ( export const composeColorFilters = (ctx: DrawingContext) => { const inner = ctx.colorFilters.pop(); const outer = ctx.colorFilters.pop(); - if (!inner || !outer) { - throw new Error("Missing color filters to compose"); + if (inner && outer) { + ctx.colorFilters.push(ctx.Skia.ColorFilter.MakeCompose(outer, inner)); + } else if (inner) { + ctx.colorFilters.push(inner); } - ctx.colorFilters.push(ctx.Skia.ColorFilter.MakeCompose(outer, inner)); }; export const setColorFilters = (ctx: DrawingContext) => { From 7b38f974b0afbbb7f7b5e00e2ebc6fea6e5e5cd5 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 15:04:31 +0100 Subject: [PATCH 06/50] :wrench: --- .../Composition/ColorFilterComposition.spec.tsx | 11 ----------- packages/skia/src/sksg/Recorder/Visitor.ts | 15 ++++++--------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx index 57059e100e..096ab5dbee 100644 --- a/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx @@ -64,17 +64,6 @@ describe("Color Filter Composition", () => { it("should apply a color matrix to an image", async () => { const { oslo } = images; const { width, height } = surface; - - // Push Matrix - - // Push Matrix - // Push Matrix - // Push Lerp - // Push LinearToSRGBGamma - // Compose - - // Compose - const image = await surface.draw( diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 36c4fd2f06..fb506a4b17 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -6,19 +6,18 @@ import { sortNodeChildren } from "../nodes"; import type { Recorder } from "./Recorder"; const pushColorFilters = (recorder: Recorder, colorFilters: Node[]) => { - //let lastFilter: NodeType | null = null; colorFilters.forEach((colorFilter) => { if (colorFilter.children.length > 0) { pushColorFilters(recorder, colorFilter.children); } recorder.pushColorFilter(colorFilter.type, colorFilter.props); - //lastFilter = colorFilter.type; + const needsComposition = + colorFilter.type !== NodeType.LerpColorFilter && + colorFilter.children.length > 0; + if (needsComposition) { + recorder.composeColorFilters(); + } }); - // If the filter doesn't need children, we compose it - // const needsComposition = lastFilter !== NodeType.LerpColorFilter; - // if (needsComposition) { - // recorder.composeColorFilters(); - // } }; const visitNode = (recorder: Recorder, node: Node) => { @@ -27,8 +26,6 @@ const visitNode = (recorder: Recorder, node: Node) => { if (shouldPushPaint) { recorder.savePaint({}); pushColorFilters(recorder, colorFilters); - recorder.composeColorFilters(); - recorder.composeColorFilters(); recorder.materializePaint(); } if (node.type === NodeType.Image) { From 293692a95118f84e742eeda539bba90b22aceb2f Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 15:11:38 +0100 Subject: [PATCH 07/50] :wrench: --- .../__tests__/e2e/Composition/ColorFilterComposition.spec.tsx | 1 + packages/skia/src/sksg/HostConfig.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx index 096ab5dbee..937591dc61 100644 --- a/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx @@ -64,6 +64,7 @@ describe("Color Filter Composition", () => { it("should apply a color matrix to an image", async () => { const { oslo } = images; const { width, height } = surface; + const image = await surface.draw( diff --git a/packages/skia/src/sksg/HostConfig.ts b/packages/skia/src/sksg/HostConfig.ts index eb97bad790..c1ae5e0759 100644 --- a/packages/skia/src/sksg/HostConfig.ts +++ b/packages/skia/src/sksg/HostConfig.ts @@ -129,11 +129,13 @@ export const sksgHostConfig: SkiaHostConfig = { createInstance( type, - props, + propsWithChildren, container, _hostContext, _internalInstanceHandle ) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { children, ...props } = propsWithChildren as any; debug("createInstance", type); container.registerValues(props); const instance = { From f46c2e0210c7332def459c2958b532a30e2a6182 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 15:14:02 +0100 Subject: [PATCH 08/50] :wrench: --- packages/skia/src/sksg/Recorder/Visitor.ts | 3 +++ packages/skia/src/sksg/Recorder/commands/ColorFilters.ts | 8 +++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index fb506a4b17..b14f473af6 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -26,6 +26,9 @@ const visitNode = (recorder: Recorder, node: Node) => { if (shouldPushPaint) { recorder.savePaint({}); pushColorFilters(recorder, colorFilters); + if (colorFilters.length > 0) { + recorder.composeColorFilters(); + } recorder.materializePaint(); } if (node.type === NodeType.Image) { diff --git a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts index e823e4a904..e7f0fab135 100644 --- a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts @@ -81,11 +81,9 @@ export const composeColorFilters = (ctx: DrawingContext) => { }; export const setColorFilters = (ctx: DrawingContext) => { - ctx.paint.setColorFilter( - ctx.colorFilters.reduceRight((inner, outer) => - inner ? ctx.Skia.ColorFilter.MakeCompose(outer, inner) : outer - ) - ); + if (ctx.colorFilters.length > 0) { + ctx.paint.setColorFilter(ctx.colorFilters[ctx.colorFilters.length - 1]); + } }; export const pushColorFilter = ( From 8ddd35c90e20f7188a613be547cde3086c5f47a7 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 15:16:53 +0100 Subject: [PATCH 09/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 4 +++- packages/skia/src/sksg/Recorder/Player.ts | 4 +++- packages/skia/src/sksg/Recorder/Recorder.ts | 5 +++++ packages/skia/src/sksg/Recorder/Visitor.ts | 9 +++++++-- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index ab73ccda19..4a8f7964ed 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -1,6 +1,6 @@ import type { SharedValue } from "react-native-reanimated"; -import type { ImageProps } from "../../dom/types"; +import type { CircleProps, ImageProps } from "../../dom/types"; // TODO: remove string labels export enum CommandType { @@ -14,6 +14,7 @@ export enum CommandType { MaterializePaint = "MaterializePaint", // Drawing DrawImage = "DrawImage", + DrawCircle = "DrawCircle", } export type Command = { @@ -43,6 +44,7 @@ export const isCommand = ( interface Props { [CommandType.DrawImage]: ImageProps; + [CommandType.DrawCircle]: CircleProps; } interface DrawCommand extends Command { diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 9b7956eb49..cf196b54e5 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -1,6 +1,6 @@ "worklet"; -import { drawImage } from "../nodes/drawings"; +import { drawCircle, drawImage } from "../nodes/drawings"; import { composeColorFilters, @@ -31,6 +31,8 @@ const play = (ctx: DrawingContext, command: Command) => { pushColorFilter(ctx, command); } else if (isDrawCommand(command, CommandType.DrawImage)) { drawImage(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawCircle)) { + drawCircle(ctx, command.props); } else { console.warn(`Unknown command: ${command.type}`); } diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 95ed5cf924..5d56bde30c 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -1,6 +1,7 @@ import type { SharedValue } from "react-native-reanimated"; import type { + CircleProps, CTMProps, ImageProps, NodeType, @@ -70,4 +71,8 @@ export class Recorder { drawImage(props: AnimatedProps) { this.add({ type: CommandType.DrawImage, props }); } + + drawCircle(props: AnimatedProps) { + this.add({ type: CommandType.DrawCircle, props }); + } } diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index b14f473af6..9fd147c582 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -31,8 +31,13 @@ const visitNode = (recorder: Recorder, node: Node) => { } recorder.materializePaint(); } - if (node.type === NodeType.Image) { - recorder.drawImage(node.props); + switch (node.type) { + case NodeType.Image: + recorder.drawImage(node.props); + break; + case NodeType.Circle: + recorder.drawCircle(node.props); + break; } drawings.forEach((drawing) => { visitNode(recorder, drawing); From 51e2eb870e238b7bb427046fd6970d03fb571a96 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 15:35:06 +0100 Subject: [PATCH 10/50] :wrench: --- packages/skia/src/sksg/Container.ts | 2 +- packages/skia/src/sksg/Recorder/Core.ts | 3 +- packages/skia/src/sksg/Recorder/Player.ts | 5 ++ packages/skia/src/sksg/Recorder/Visitor.ts | 48 +++++++++++++ .../skia/src/sksg/Recorder/commands/CTM.ts | 71 +++++++++++++++++++ packages/skia/src/sksg/nodes/drawings.ts | 1 + 6 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 packages/skia/src/sksg/Recorder/commands/CTM.ts diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index d0ca92e316..32c2cf1613 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -15,8 +15,8 @@ import { draw, isSharedValue } from "./nodes"; import type { Command } from "./Recorder/Core"; import { Recorder } from "./Recorder/Recorder"; import { visit } from "./Recorder/Visitor"; -import { DrawingContext } from "./Recorder/DrawingContext"; import { replay } from "./Recorder/Player"; +import { DrawingContext } from "./Recorder/DrawingContext"; const drawOnscreen = ( Skia: Skia, diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 4a8f7964ed..b4127fd096 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -1,6 +1,6 @@ import type { SharedValue } from "react-native-reanimated"; -import type { CircleProps, ImageProps } from "../../dom/types"; +import type { CircleProps, CTMProps, ImageProps } from "../../dom/types"; // TODO: remove string labels export enum CommandType { @@ -45,6 +45,7 @@ export const isCommand = ( interface Props { [CommandType.DrawImage]: ImageProps; [CommandType.DrawCircle]: CircleProps; + [CommandType.SaveCTM]: CTMProps; } interface DrawCommand extends Command { diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index cf196b54e5..3238616172 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -8,6 +8,7 @@ import { pushColorFilter, setColorFilters, } from "./commands/ColorFilters"; +import { saveCTM } from "./commands/CTM"; import { CommandType, isCommand, @@ -29,6 +30,10 @@ const play = (ctx: DrawingContext, command: Command) => { setColorFilters(ctx); } else if (isPushColorFilter(command)) { pushColorFilter(ctx, command); + } else if (isDrawCommand(command, CommandType.SaveCTM)) { + saveCTM(ctx, command.props); + } else if (isCommand(command, CommandType.RestoreCTM)) { + ctx.canvas.restore(); } else if (isDrawCommand(command, CommandType.DrawImage)) { drawImage(ctx, command.props); } else if (isDrawCommand(command, CommandType.DrawCircle)) { diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 9fd147c582..d8200e9069 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -1,10 +1,51 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { CTMProps } from "../../dom/types"; import { NodeType } from "../../dom/types"; import type { Node } from "../nodes"; import { sortNodeChildren } from "../nodes"; import type { Recorder } from "./Recorder"; +const processCTM = ({ + clip, + invertClip, + transform, + origin, + matrix, + layer, +}: CTMProps) => { + 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 || + transform !== undefined || + origin !== undefined || + matrix !== undefined || + layer !== undefined + ) { + return ctm; + } + return null; +}; + const pushColorFilters = (recorder: Recorder, colorFilters: Node[]) => { colorFilters.forEach((colorFilter) => { if (colorFilter.children.length > 0) { @@ -31,6 +72,10 @@ const visitNode = (recorder: Recorder, node: Node) => { } recorder.materializePaint(); } + const ctm = processCTM(node.props); + if (ctm) { + recorder.saveCTM(ctm); + } switch (node.type) { case NodeType.Image: recorder.drawImage(node.props); @@ -45,6 +90,9 @@ const visitNode = (recorder: Recorder, node: Node) => { if (shouldPushPaint) { recorder.restorePaint(); } + if (ctm) { + recorder.restoreCTM(); + } }; export const visit = (recorder: Recorder, root: Node[]) => { diff --git a/packages/skia/src/sksg/Recorder/commands/CTM.ts b/packages/skia/src/sksg/Recorder/commands/CTM.ts new file mode 100644 index 0000000000..9e7a7ed7f6 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/CTM.ts @@ -0,0 +1,71 @@ +import { + isPathDef, + processPath, + processTransformProps2, +} from "../../../dom/nodes"; +import type { ClipDef, CTMProps } from "../../../dom/types"; +import type { Skia, SkPath, SkRect, SkRRect } from "../../../skia/types"; +import { ClipOp, isRRect } from "../../../skia/types"; +import type { DrawingContext } from "../DrawingContext"; + +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; +}; + +export const saveCTM = (ctx: DrawingContext, props: CTMProps) => { + const { canvas, Skia } = ctx; + 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); + } + } +}; diff --git a/packages/skia/src/sksg/nodes/drawings.ts b/packages/skia/src/sksg/nodes/drawings.ts index 350cdc6e66..ffbf53d4e1 100644 --- a/packages/skia/src/sksg/nodes/drawings.ts +++ b/packages/skia/src/sksg/nodes/drawings.ts @@ -143,6 +143,7 @@ export const drawImage = (ctx: LocalDrawingContext, props: ImageProps) => { }, rect ); + console.log({ src, dst, fit }); ctx.canvas.drawImageRect(image, src, dst, ctx.paint); } }; From a43fda917403a4581efb55628aa4227cf631fd23 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 15:49:33 +0100 Subject: [PATCH 11/50] :wrench: --- packages/skia/src/sksg/Container.ts | 2 +- packages/skia/src/sksg/Recorder/Core.ts | 8 +- .../skia/src/sksg/Recorder/DrawingContext.ts | 7 +- packages/skia/src/sksg/Recorder/Player.ts | 4 +- packages/skia/src/sksg/Recorder/Visitor.ts | 68 +++++++++++++++- .../skia/src/sksg/Recorder/commands/CTM.ts | 2 + .../skia/src/sksg/Recorder/commands/Paint.ts | 78 +++++++++++++++++++ packages/skia/src/sksg/nodes/drawings.ts | 1 - 8 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 packages/skia/src/sksg/Recorder/commands/Paint.ts diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index 32c2cf1613..896f160722 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -114,7 +114,7 @@ export class Container { drawOnCanvas(canvas: SkCanvas) { const ctx = new DrawingContext(this.Skia, canvas); - console.log(this._recording); + //console.log(this._recording); replay(ctx, this._recording!); } } diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index b4127fd096..0c3a43dbcf 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -1,6 +1,11 @@ import type { SharedValue } from "react-native-reanimated"; -import type { CircleProps, CTMProps, ImageProps } from "../../dom/types"; +import type { + CircleProps, + CTMProps, + ImageProps, + PaintProps, +} from "../../dom/types"; // TODO: remove string labels export enum CommandType { @@ -46,6 +51,7 @@ interface Props { [CommandType.DrawImage]: ImageProps; [CommandType.DrawCircle]: CircleProps; [CommandType.SaveCTM]: CTMProps; + [CommandType.SavePaint]: PaintProps; } interface DrawCommand extends Command { diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index e511921685..abc3bf9b0e 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -1,6 +1,11 @@ "worklet"; -import type { SkCanvas, SkColorFilter, Skia, SkPaint } from "../../skia/types"; +import { + type Skia, + type SkCanvas, + type SkColorFilter, + type SkPaint, +} from "../../skia/types"; export class DrawingContext { Skia: Skia; diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 3238616172..c4d53a72e2 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -9,6 +9,7 @@ import { setColorFilters, } from "./commands/ColorFilters"; import { saveCTM } from "./commands/CTM"; +import { setPaintProperties } from "./commands/Paint"; import { CommandType, isCommand, @@ -20,8 +21,9 @@ import type { DrawingContext } from "./DrawingContext"; const play = (ctx: DrawingContext, command: Command) => { materializeProps(command); - if (isCommand(command, CommandType.SavePaint)) { + if (isDrawCommand(command, CommandType.SavePaint)) { ctx.savePaint(); + setPaintProperties(ctx.Skia, ctx.paint, command.props); } else if (isCommand(command, CommandType.RestorePaint)) { ctx.restorePaint(); } else if (isCommand(command, CommandType.ComposeColorFilter)) { diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index d8200e9069..a0ff039c64 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -1,11 +1,72 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { CTMProps } from "../../dom/types"; +import type { CTMProps, DrawingNodeProps, PaintProps } from "../../dom/types"; import { NodeType } from "../../dom/types"; import type { Node } from "../nodes"; import { sortNodeChildren } from "../nodes"; import type { Recorder } from "./Recorder"; +export const processPaint = ({ + opacity, + color, + strokeWidth, + blendMode, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, +}: DrawingNodeProps) => { + 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 || + color !== undefined || + strokeWidth !== undefined || + blendMode !== undefined || + style !== undefined || + strokeJoin !== undefined || + strokeCap !== undefined || + strokeMiter !== undefined || + antiAlias !== undefined || + dither !== undefined + ) { + return paint; + } + return null; +}; + const processCTM = ({ clip, invertClip, @@ -63,9 +124,10 @@ const pushColorFilters = (recorder: Recorder, colorFilters: Node[]) => { const visitNode = (recorder: Recorder, node: Node) => { const { colorFilters, drawings } = sortNodeChildren(node); - const shouldPushPaint = colorFilters.length > 0; + const paint = processPaint(node.props); + const shouldPushPaint = paint || colorFilters.length > 0; if (shouldPushPaint) { - recorder.savePaint({}); + recorder.savePaint(paint ?? {}); pushColorFilters(recorder, colorFilters); if (colorFilters.length > 0) { recorder.composeColorFilters(); diff --git a/packages/skia/src/sksg/Recorder/commands/CTM.ts b/packages/skia/src/sksg/Recorder/commands/CTM.ts index 9e7a7ed7f6..bbb3b3145c 100644 --- a/packages/skia/src/sksg/Recorder/commands/CTM.ts +++ b/packages/skia/src/sksg/Recorder/commands/CTM.ts @@ -1,3 +1,5 @@ +"worklet"; + import { isPathDef, processPath, diff --git a/packages/skia/src/sksg/Recorder/commands/Paint.ts b/packages/skia/src/sksg/Recorder/commands/Paint.ts new file mode 100644 index 0000000000..588f590a80 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/Paint.ts @@ -0,0 +1,78 @@ +"worklet"; + +import { enumKey } from "../../../dom/nodes"; +import type { PaintProps } from "../../../dom/types"; +import { + BlendMode, + PaintStyle, + StrokeCap, + StrokeJoin, +} from "../../../skia/types"; +import type { SkPaint, Skia } from "../../../skia/types"; + +export 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 setPaintProperties = ( + Skia: Skia, + paint: SkPaint, + { + opacity, + color, + blendMode, + strokeWidth, + style, + strokeJoin, + strokeCap, + strokeMiter, + antiAlias, + dither, + }: PaintProps +) => { + 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 (blendMode !== undefined) { + paint.setBlendMode(BlendMode[enumKey(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); + } +}; diff --git a/packages/skia/src/sksg/nodes/drawings.ts b/packages/skia/src/sksg/nodes/drawings.ts index ffbf53d4e1..350cdc6e66 100644 --- a/packages/skia/src/sksg/nodes/drawings.ts +++ b/packages/skia/src/sksg/nodes/drawings.ts @@ -143,7 +143,6 @@ export const drawImage = (ctx: LocalDrawingContext, props: ImageProps) => { }, rect ); - console.log({ src, dst, fit }); ctx.canvas.drawImageRect(image, src, dst, ctx.paint); } }; From ac7c390b2809199f0e6c88571b8e023c0627415e Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 15:58:20 +0100 Subject: [PATCH 12/50] :wrench: --- .../static/img/color-filters/composition.png | Bin 15263 -> 12246 bytes .../__tests__/e2e/ColorFilters.spec.tsx | 40 ++++++++++++------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/apps/docs/static/img/color-filters/composition.png b/apps/docs/static/img/color-filters/composition.png index 4a79a7f8ace09fafa169e7058221e7a8033698b7..89c0bd123aaf7077481c3c142f43182a69ab46ca 100644 GIT binary patch literal 12246 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 C@Hbch literal 15263 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 diff --git a/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx index 7476b3ed2d..0e7921f5f3 100644 --- a/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx @@ -9,11 +9,11 @@ import { Image, Lerp, LinearToSRGBGamma, - SRGBToLinearGamma, } from "../../components"; import { docPath, checkImage, processResult } from "../../../__tests__/setup"; import { setupSkia } from "../../../skia/__tests__/setup"; import { fitRects } from "../../../dom/nodes"; +import { BlendMode } from "../../../skia/types"; const blackAndWhite = [ 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, @@ -57,20 +57,34 @@ describe("Color Filters", () => { ); checkImage(img, docPath("color-filters/color-blend.png")); }); - it("should use composition", async () => { - const { width } = surface; - const r = width / 2; - const img = await surface.draw( - - - - - - - + it("should build the reference result for should use composition", async () => { + const { surface: ckSurface, Skia, canvas } = setupSkia(wWidth, wHeight); + const paint = Skia.Paint(); + const outer = Skia.ColorFilter.MakeSRGBToLinearGamma(); + const inner = Skia.ColorFilter.MakeBlend( + Skia.Color("lightblue"), + BlendMode.SrcIn ); - checkImage(img, docPath("color-filters/composition.png")); + paint.setColorFilter(Skia.ColorFilter.MakeCompose(outer, inner)); + const r = (surface.width * 3) / 2; + canvas.drawCircle(r, r, r, paint); + canvas.drawCircle(r * 2, r, r, paint); + processResult(ckSurface, docPath("color-filters/composition.png")); }); + // it("should use composition", async () => { + // const { width } = surface; + // const r = width / 2; + // const img = await surface.draw( + // + // + // + // + // + // + // + // ); + // checkImage(img, docPath("color-filters/composition.png")); + // }); it("should build the reference result for simple-lerp.png", async () => { const { oslo } = images; const { surface: ckSurface, Skia, canvas } = setupSkia(wWidth, wHeight); From 2e7138a1fc2d040b5625cf757d07416c2c3d41aa Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 16:03:44 +0100 Subject: [PATCH 13/50] :wrench: --- .../__tests__/e2e/ColorFilters.spec.tsx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx index 0e7921f5f3..71ae98f2b2 100644 --- a/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/ColorFilters.spec.tsx @@ -9,6 +9,7 @@ import { Image, Lerp, LinearToSRGBGamma, + SRGBToLinearGamma, } from "../../components"; import { docPath, checkImage, processResult } from "../../../__tests__/setup"; import { setupSkia } from "../../../skia/__tests__/setup"; @@ -71,20 +72,20 @@ describe("Color Filters", () => { canvas.drawCircle(r * 2, r, r, paint); processResult(ckSurface, docPath("color-filters/composition.png")); }); - // it("should use composition", async () => { - // const { width } = surface; - // const r = width / 2; - // const img = await surface.draw( - // - // - // - // - // - // - // - // ); - // checkImage(img, docPath("color-filters/composition.png")); - // }); + it("should use composition", async () => { + const { width } = surface; + const r = width / 2; + const img = await surface.draw( + + + + + + + + ); + checkImage(img, docPath("color-filters/composition.png")); + }); it("should build the reference result for simple-lerp.png", async () => { const { oslo } = images; const { surface: ckSurface, Skia, canvas } = setupSkia(wWidth, wHeight); From 1a04fffcbddfad068f6769b141a05902222c2bf7 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 16:11:10 +0100 Subject: [PATCH 14/50] :wrench: --- .../ColorFilterComposition.spec.tsx | 25 +++++++++++++++++-- packages/skia/src/sksg/Recorder/Visitor.ts | 3 --- .../sksg/Recorder/commands/ColorFilters.ts | 14 ++++++----- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx index 937591dc61..5d656770e2 100644 --- a/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/Composition/ColorFilterComposition.spec.tsx @@ -7,12 +7,20 @@ import { height as wHeight, } from "../../setup"; import { + BlendColor, + Circle, ColorMatrix, + Group, Image, Lerp, LinearToSRGBGamma, + SRGBToLinearGamma, } from "../../../components"; -import { checkImage, processResult } from "../../../../__tests__/setup"; +import { + checkImage, + docPath, + processResult, +} from "../../../../__tests__/setup"; import { setupSkia } from "../../../../skia/__tests__/setup"; import { fitRects } from "../../../../dom/nodes"; @@ -60,7 +68,6 @@ describe("Color Filter Composition", () => { "snapshots/color-filter/color-filter-composition.png" ); }); - // TODO: a bug should be reported here it("should apply a color matrix to an image", async () => { const { oslo } = images; const { width, height } = surface; @@ -78,4 +85,18 @@ describe("Color Filter Composition", () => { ); checkImage(image, "snapshots/color-filter/color-filter-composition.png"); }); + it("should use composition", async () => { + const { width } = surface; + const r = width / 2; + const img = await surface.draw( + + + + + + + + ); + checkImage(img, docPath("color-filters/composition.png")); + }); }); diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index a0ff039c64..9b12b60e8d 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -129,9 +129,6 @@ const visitNode = (recorder: Recorder, node: Node) => { if (shouldPushPaint) { recorder.savePaint(paint ?? {}); pushColorFilters(recorder, colorFilters); - if (colorFilters.length > 0) { - recorder.composeColorFilters(); - } recorder.materializePaint(); } const ctm = processCTM(node.props); diff --git a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts index e7f0fab135..767e6c66c6 100644 --- a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts @@ -71,18 +71,20 @@ const isSRGBToLinearGammaColorFilter = ( }; export const composeColorFilters = (ctx: DrawingContext) => { - const inner = ctx.colorFilters.pop(); - const outer = ctx.colorFilters.pop(); - if (inner && outer) { + if (ctx.colorFilters.length > 1) { + const outer = ctx.colorFilters.pop()!; + const inner = ctx.colorFilters.pop()!; ctx.colorFilters.push(ctx.Skia.ColorFilter.MakeCompose(outer, inner)); - } else if (inner) { - ctx.colorFilters.push(inner); } }; export const setColorFilters = (ctx: DrawingContext) => { if (ctx.colorFilters.length > 0) { - ctx.paint.setColorFilter(ctx.colorFilters[ctx.colorFilters.length - 1]); + ctx.paint.setColorFilter( + ctx.colorFilters.reduceRight((inner, outer) => + inner ? ctx.Skia.ColorFilter.MakeCompose(outer, inner) : outer + ) + ); } }; From 2f9cb67da49cfcbd5173d8774e1ca33cf5f069c0 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 17:01:33 +0100 Subject: [PATCH 15/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 4 ++++ packages/skia/src/sksg/Recorder/DrawingContext.ts | 7 +------ packages/skia/src/sksg/Recorder/Player.ts | 5 +++++ packages/skia/src/sksg/Recorder/Recorder.ts | 11 ++++++++++- packages/skia/src/sksg/Recorder/Visitor.ts | 15 +++++++++++++-- packages/skia/src/sksg/nodes/Node.ts | 5 ++++- 6 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 0c3a43dbcf..6e9919fee2 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -1,6 +1,7 @@ import type { SharedValue } from "react-native-reanimated"; import type { + BlurMaskFilterProps, CircleProps, CTMProps, ImageProps, @@ -15,11 +16,13 @@ export enum CommandType { SaveCTM = "SaveCTM", RestoreCTM = "RestoreCTM", PushColorFilter = "PushColorFilter", + PushBlurMaskFilter = "PushBlurMaskFilter", ComposeColorFilter = "ComposeColorFilter", MaterializePaint = "MaterializePaint", // Drawing DrawImage = "DrawImage", DrawCircle = "DrawCircle", + DrawPaint = "DrawPaint", } export type Command = { @@ -52,6 +55,7 @@ interface Props { [CommandType.DrawCircle]: CircleProps; [CommandType.SaveCTM]: CTMProps; [CommandType.SavePaint]: PaintProps; + [CommandType.PushBlurMaskFilter]: BlurMaskFilterProps; } interface DrawCommand extends Command { diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index abc3bf9b0e..b24d4374a8 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -1,11 +1,6 @@ "worklet"; -import { - type Skia, - type SkCanvas, - type SkColorFilter, - type SkPaint, -} from "../../skia/types"; +import type { Skia, SkCanvas, SkColorFilter, SkPaint } from "../../skia/types"; export class DrawingContext { Skia: Skia; diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index c4d53a72e2..1dc6fb53c9 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -9,6 +9,7 @@ import { setColorFilters, } from "./commands/ColorFilters"; import { saveCTM } from "./commands/CTM"; +import { setBlurMaskFilter } from "./commands/ImageFilters"; import { setPaintProperties } from "./commands/Paint"; import { CommandType, @@ -32,10 +33,14 @@ const play = (ctx: DrawingContext, command: Command) => { setColorFilters(ctx); } else if (isPushColorFilter(command)) { pushColorFilter(ctx, command); + } else if (isDrawCommand(command, CommandType.PushBlurMaskFilter)) { + setBlurMaskFilter(ctx, command.props); } else if (isDrawCommand(command, CommandType.SaveCTM)) { saveCTM(ctx, command.props); } else if (isCommand(command, CommandType.RestoreCTM)) { ctx.canvas.restore(); + } else if (isCommand(command, CommandType.DrawPaint)) { + ctx.canvas.drawPaint(ctx.paint); } else if (isDrawCommand(command, CommandType.DrawImage)) { drawImage(ctx, command.props); } else if (isDrawCommand(command, CommandType.DrawCircle)) { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 5d56bde30c..4594e2bcd2 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -1,10 +1,11 @@ import type { SharedValue } from "react-native-reanimated"; import type { + NodeType, + BlurMaskFilterProps, CircleProps, CTMProps, ImageProps, - NodeType, PaintProps, } from "../../dom/types"; import type { AnimatedProps } from "../../renderer"; @@ -56,6 +57,10 @@ export class Recorder { }); } + pushBlurMaskFilter(props: AnimatedProps) { + this.add({ type: CommandType.PushBlurMaskFilter, props }); + } + composeColorFilters() { this.add({ type: CommandType.ComposeColorFilter }); } @@ -68,6 +73,10 @@ export class Recorder { this.add({ type: CommandType.RestoreCTM }); } + drawPaint() { + this.add({ type: CommandType.DrawPaint }); + } + drawImage(props: AnimatedProps) { this.add({ type: CommandType.DrawImage, props }); } diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 9b12b60e8d..7292bb6787 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -122,13 +122,21 @@ const pushColorFilters = (recorder: Recorder, colorFilters: Node[]) => { }); }; +const pushMaskFilters = (recorder: Recorder, maskFilters: Node[]) => { + if (maskFilters.length > 0) { + recorder.pushBlurMaskFilter(maskFilters[maskFilters.length - 1].props); + } +}; + const visitNode = (recorder: Recorder, node: Node) => { - const { colorFilters, drawings } = sortNodeChildren(node); + const { colorFilters, maskFilters, drawings } = sortNodeChildren(node); const paint = processPaint(node.props); - const shouldPushPaint = paint || colorFilters.length > 0; + const shouldPushPaint = + paint || colorFilters.length > 0 || maskFilters.length > 0; if (shouldPushPaint) { recorder.savePaint(paint ?? {}); pushColorFilters(recorder, colorFilters); + pushMaskFilters(recorder, maskFilters); recorder.materializePaint(); } const ctm = processCTM(node.props); @@ -136,6 +144,9 @@ const visitNode = (recorder: Recorder, node: Node) => { recorder.saveCTM(ctm); } switch (node.type) { + case NodeType.Fill: + recorder.drawPaint(); + break; case NodeType.Image: recorder.drawImage(node.props); break; diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index 2cc4a40f0e..d3af7c8c85 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -26,6 +26,7 @@ export const sortNodes = (children: Node[]) => { export const sortNodeChildren = (parent: Node) => { "worklet"; + const maskFilters: Node[] = []; const colorFilters: Node[] = []; const drawings: Node[] = []; const declarations: Node[] = []; @@ -39,11 +40,13 @@ export const sortNodeChildren = (parent: Node) => { node.type === NodeType.LinearToSRGBGammaColorFilter ) { colorFilters.push(node); + } else if (node.type === NodeType.BlurMaskFilter) { + maskFilters.push(node); } else if (node.isDeclaration) { declarations.push(node); } else { drawings.push(node); } }); - return { colorFilters, drawings, declarations }; + return { colorFilters, drawings, declarations, maskFilters }; }; From 94e2bfee37edfc667b3d6dbc0542756772097ba5 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 17:15:05 +0100 Subject: [PATCH 16/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 51 +++++++++++ packages/skia/src/sksg/Recorder/Player.ts | 56 ++++++++++++- packages/skia/src/sksg/Recorder/Recorder.ts | 84 +++++++++++++++++++ packages/skia/src/sksg/Recorder/Visitor.ts | 56 ++++++++++++- .../sksg/Recorder/commands/ImageFilters.ts | 18 ++++ 5 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 packages/skia/src/sksg/Recorder/commands/ImageFilters.ts diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 6e9919fee2..c3cfc2faa5 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -6,6 +6,23 @@ import type { CTMProps, ImageProps, PaintProps, + PointsProps, + PathProps, + RectProps, + RoundedRectProps, + OvalProps, + LineProps, + PatchProps, + VerticesProps, + DiffRectProps, + TextProps, + TextPathProps, + TextBlobProps, + GlyphsProps, + PictureProps, + ImageSVGProps, + ParagraphProps, + AtlasProps, } from "../../dom/types"; // TODO: remove string labels @@ -23,6 +40,23 @@ export enum CommandType { DrawImage = "DrawImage", DrawCircle = "DrawCircle", DrawPaint = "DrawPaint", + DrawPoints = "DrawPoints", + DrawPath = "DrawPath", + DrawRect = "DrawRect", + DrawRRect = "DrawRRect", + DrawOval = "DrawOval", + DrawLine = "DrawLine", + DrawPatch = "DrawPatch", + DrawVertices = "DrawVertices", + DrawDiffRect = "DrawDiffRect", + DrawText = "DrawText", + DrawTextPath = "DrawTextPath", + DrawTextBlob = "DrawTextBlob", + DrawGlyphs = "DrawGlyphs", + DrawPicture = "DrawPicture", + DrawImageSVG = "DrawImageSVG", + DrawParagraph = "DrawParagraph", + DrawAtlas = "DrawAtlas", } export type Command = { @@ -56,6 +90,23 @@ interface Props { [CommandType.SaveCTM]: CTMProps; [CommandType.SavePaint]: PaintProps; [CommandType.PushBlurMaskFilter]: BlurMaskFilterProps; + [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]: TextBlobProps; + [CommandType.DrawGlyphs]: GlyphsProps; + [CommandType.DrawPicture]: PictureProps; + [CommandType.DrawImageSVG]: ImageSVGProps; + [CommandType.DrawParagraph]: ParagraphProps; + [CommandType.DrawAtlas]: AtlasProps; } interface DrawCommand extends Command { diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 1dc6fb53c9..a527a38d33 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -1,6 +1,26 @@ "worklet"; -import { drawCircle, drawImage } from "../nodes/drawings"; +import { + drawCircle, + drawImage, + drawOval, + drawPath, + drawPoints, + drawRect, + drawRRect, + drawLine, + drawAtlas, + drawParagraph, + drawImageSVG, + drawPicture, + drawGlyphs, + drawTextBlob, + drawTextPath, + drawText, + drawDiffRect, + drawVertices, + drawPatch, +} from "../nodes/drawings"; import { composeColorFilters, @@ -45,6 +65,40 @@ const play = (ctx: DrawingContext, command: Command) => { drawImage(ctx, command.props); } else if (isDrawCommand(command, CommandType.DrawCircle)) { drawCircle(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPoints)) { + drawPoints(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPath)) { + drawPath(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawRect)) { + drawRect(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawRRect)) { + drawRRect(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawOval)) { + drawOval(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawLine)) { + drawLine(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPatch)) { + drawPatch(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawVertices)) { + drawVertices(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawDiffRect)) { + drawDiffRect(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawText)) { + drawText(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawTextPath)) { + drawTextPath(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawTextBlob)) { + drawTextBlob(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawGlyphs)) { + drawGlyphs(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPicture)) { + drawPicture(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawImageSVG)) { + drawImageSVG(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawParagraph)) { + drawParagraph(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawAtlas)) { + drawAtlas(ctx, command.props); } else { console.warn(`Unknown command: ${command.type}`); } diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 4594e2bcd2..7e22746e62 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -7,6 +7,23 @@ import type { CTMProps, ImageProps, PaintProps, + PointsProps, + PathProps, + RectProps, + RoundedRectProps, + OvalProps, + LineProps, + PatchProps, + VerticesProps, + DiffRectProps, + TextProps, + TextPathProps, + TextBlobProps, + GlyphsProps, + PictureProps, + ImageSVGProps, + ParagraphProps, + AtlasProps, } from "../../dom/types"; import type { AnimatedProps } from "../../renderer"; import { isSharedValue } from "../nodes/utils"; @@ -84,4 +101,71 @@ export class Recorder { drawCircle(props: AnimatedProps) { this.add({ type: CommandType.DrawCircle, props }); } + drawPoints(props: AnimatedProps) { + this.add({ type: CommandType.DrawPoints, props }); + } + + drawPath(props: AnimatedProps) { + this.add({ type: CommandType.DrawPath, props }); + } + + drawRect(props: AnimatedProps) { + this.add({ type: CommandType.DrawRect, props }); + } + + drawRRect(props: AnimatedProps) { + this.add({ type: CommandType.DrawRRect, props }); + } + + drawOval(props: AnimatedProps) { + this.add({ type: CommandType.DrawOval, props }); + } + + drawLine(props: AnimatedProps) { + this.add({ type: CommandType.DrawLine, props }); + } + + drawPatch(props: AnimatedProps) { + this.add({ type: CommandType.DrawPatch, props }); + } + + drawVertices(props: AnimatedProps) { + this.add({ type: CommandType.DrawVertices, props }); + } + + drawDiffRect(props: AnimatedProps) { + this.add({ type: CommandType.DrawDiffRect, props }); + } + + drawText(props: AnimatedProps) { + this.add({ type: CommandType.DrawText, props }); + } + + drawTextPath(props: AnimatedProps) { + this.add({ type: CommandType.DrawTextPath, props }); + } + + drawTextBlob(props: AnimatedProps) { + this.add({ type: CommandType.DrawTextBlob, props }); + } + + drawGlyphs(props: AnimatedProps) { + this.add({ type: CommandType.DrawGlyphs, props }); + } + + drawPicture(props: AnimatedProps) { + this.add({ type: CommandType.DrawPicture, props }); + } + + drawImageSVG(props: AnimatedProps) { + this.add({ type: CommandType.DrawImageSVG, props }); + } + + drawParagraph(props: AnimatedProps) { + this.add({ type: CommandType.DrawParagraph, props }); + } + + drawAtlas(props: AnimatedProps) { + this.add({ type: CommandType.DrawAtlas, props }); + } } diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 7292bb6787..a66f3d7ec2 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -129,8 +129,9 @@ const pushMaskFilters = (recorder: Recorder, maskFilters: Node[]) => { }; const visitNode = (recorder: Recorder, node: Node) => { + const { props } = node; const { colorFilters, maskFilters, drawings } = sortNodeChildren(node); - const paint = processPaint(node.props); + const paint = processPaint(props); const shouldPushPaint = paint || colorFilters.length > 0 || maskFilters.length > 0; if (shouldPushPaint) { @@ -139,7 +140,7 @@ const visitNode = (recorder: Recorder, node: Node) => { pushMaskFilters(recorder, maskFilters); recorder.materializePaint(); } - const ctm = processCTM(node.props); + const ctm = processCTM(props); if (ctm) { recorder.saveCTM(ctm); } @@ -153,6 +154,57 @@ const visitNode = (recorder: Recorder, node: Node) => { case NodeType.Circle: recorder.drawCircle(node.props); break; + case NodeType.Points: + recorder.drawPoints(props); + break; + case NodeType.Path: + recorder.drawPath(props); + break; + case NodeType.Rect: + recorder.drawRect(props); + break; + case NodeType.RRect: + recorder.drawRRect(props); + break; + case NodeType.Oval: + recorder.drawOval(props); + break; + case NodeType.Line: + recorder.drawLine(props); + break; + case NodeType.Patch: + recorder.drawPatch(props); + break; + case NodeType.Vertices: + recorder.drawVertices(props); + break; + case NodeType.DiffRect: + recorder.drawDiffRect(props); + break; + case NodeType.Text: + recorder.drawText(props); + break; + case NodeType.TextPath: + recorder.drawTextPath(props); + break; + case NodeType.TextBlob: + recorder.drawTextBlob(props); + break; + case NodeType.Glyphs: + recorder.drawGlyphs(props); + break; + case NodeType.Picture: + recorder.drawPicture(props); + break; + case NodeType.ImageSVG: + recorder.drawImageSVG(props); + break; + case NodeType.Paragraph: + recorder.drawParagraph(props); + break; + case NodeType.Atlas: + recorder.drawAtlas(props); + break; } drawings.forEach((drawing) => { visitNode(recorder, drawing); diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts new file mode 100644 index 0000000000..36b24ac640 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -0,0 +1,18 @@ +import { enumKey } from "../../../dom/nodes"; +import type { BlurMaskFilterProps } from "../../../dom/types"; +import { BlurStyle } from "../../../skia/types"; +import type { DrawingContext } from "../DrawingContext"; + +export const setBlurMaskFilter = ( + ctx: DrawingContext, + props: BlurMaskFilterProps +) => { + "worklet"; + const { blur, style, respectCTM } = props; + const mf = ctx.Skia.MaskFilter.MakeBlur( + BlurStyle[enumKey(style)], + blur, + respectCTM + ); + ctx.paint.setMaskFilter(mf); +}; From 2409cbb82b14b6b32086e7a251143c5e11b71794 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 17:22:55 +0100 Subject: [PATCH 17/50] :wrench: --- packages/skia/src/sksg/nodes/Node.ts | 48 +++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index d3af7c8c85..30925deb8a 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -28,6 +28,9 @@ export const sortNodeChildren = (parent: Node) => { "worklet"; const maskFilters: Node[] = []; const colorFilters: Node[] = []; + const shaders: Node[] = []; + const imageFilters: Node[] = []; + const pathEffects: Node[] = []; const drawings: Node[] = []; const declarations: Node[] = []; parent.children.forEach((node) => { @@ -42,11 +45,54 @@ export const sortNodeChildren = (parent: Node) => { colorFilters.push(node); } else if (node.type === NodeType.BlurMaskFilter) { maskFilters.push(node); + } else if ( + // Path Effects + node.type === NodeType.DiscretePathEffect || + node.type === NodeType.DashPathEffect || + node.type === NodeType.Path1DPathEffect || + node.type === NodeType.Path2DPathEffect || + node.type === NodeType.CornerPathEffect || + node.type === NodeType.SumPathEffect || + node.type === NodeType.Line2DPathEffect + ) { + pathEffects.push(node); + } else if ( + // Image Filters + node.type === NodeType.OffsetImageFilter || + node.type === NodeType.DisplacementMapImageFilter || + node.type === NodeType.BlurImageFilter || + node.type === NodeType.DropShadowImageFilter || + node.type === NodeType.MorphologyImageFilter || + node.type === NodeType.BlendImageFilter || + node.type === NodeType.RuntimeShaderImageFilter + ) { + imageFilters.push(node); + } else if ( + // Shaders + node.type === NodeType.Shader || + node.type === NodeType.ImageShader || + node.type === NodeType.ColorShader || + node.type === NodeType.Turbulence || + node.type === NodeType.FractalNoise || + node.type === NodeType.LinearGradient || + node.type === NodeType.RadialGradient || + node.type === NodeType.SweepGradient || + node.type === NodeType.TwoPointConicalGradient + ) { + shaders.push(node); } else if (node.isDeclaration) { declarations.push(node); } else { drawings.push(node); } }); - return { colorFilters, drawings, declarations, maskFilters }; + return { + colorFilters, + drawings, + declarations, + maskFilters, + shaders, + pathEffects, + imageFilters, + }; }; From 0aa8bb2b9d04e010027316040e971b33c2cd058d Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 17:46:41 +0100 Subject: [PATCH 18/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 1 + .../skia/src/sksg/Recorder/DrawingContext.ts | 9 +- packages/skia/src/sksg/Recorder/Player.ts | 4 + packages/skia/src/sksg/Recorder/Recorder.ts | 4 + packages/skia/src/sksg/Recorder/Visitor.ts | 18 +- .../sksg/Recorder/commands/ColorFilters.ts | 51 +--- .../src/sksg/Recorder/commands/Shaders.ts | 270 ++++++++++++++++++ 7 files changed, 314 insertions(+), 43 deletions(-) create mode 100644 packages/skia/src/sksg/Recorder/commands/Shaders.ts diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index c3cfc2faa5..0b26418f9b 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -34,6 +34,7 @@ export enum CommandType { RestoreCTM = "RestoreCTM", PushColorFilter = "PushColorFilter", PushBlurMaskFilter = "PushBlurMaskFilter", + PushShader = "PushShader", ComposeColorFilter = "ComposeColorFilter", MaterializePaint = "MaterializePaint", // Drawing diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index b24d4374a8..dd31088dc0 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -1,12 +1,19 @@ "worklet"; -import type { Skia, SkCanvas, SkColorFilter, SkPaint } from "../../skia/types"; +import type { + Skia, + SkCanvas, + SkColorFilter, + SkPaint, + SkShader, +} from "../../skia/types"; export class DrawingContext { Skia: Skia; canvas: SkCanvas; paints: SkPaint[] = []; colorFilters: SkColorFilter[] = []; + shaders: SkShader[] = []; constructor(Skia: Skia, canvas: SkCanvas) { this.Skia = Skia; diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index a527a38d33..58ae707e28 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -31,6 +31,7 @@ import { import { saveCTM } from "./commands/CTM"; import { setBlurMaskFilter } from "./commands/ImageFilters"; import { setPaintProperties } from "./commands/Paint"; +import { isPushShader, pushShader, setShaders } from "./commands/Shaders"; import { CommandType, isCommand, @@ -51,8 +52,11 @@ const play = (ctx: DrawingContext, command: Command) => { composeColorFilters(ctx); } else if (isCommand(command, CommandType.MaterializePaint)) { setColorFilters(ctx); + setShaders(ctx); } else if (isPushColorFilter(command)) { pushColorFilter(ctx, command); + } else if (isPushShader(command)) { + pushShader(ctx, command); } else if (isDrawCommand(command, CommandType.PushBlurMaskFilter)) { setBlurMaskFilter(ctx, command.props); } else if (isDrawCommand(command, CommandType.SaveCTM)) { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 7e22746e62..aae003019e 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -74,6 +74,10 @@ export class Recorder { }); } + pushShader(sharderType: NodeType, props: AnimatedProps) { + this.add({ type: CommandType.PushShader, sharderType, props }); + } + pushBlurMaskFilter(props: AnimatedProps) { this.add({ type: CommandType.PushBlurMaskFilter, props }); } diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index a66f3d7ec2..11357bad0a 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -122,6 +122,15 @@ const pushColorFilters = (recorder: Recorder, colorFilters: Node[]) => { }); }; +const pushShaders = (recorder: Recorder, shaders: Node[]) => { + shaders.forEach((shader) => { + if (shader.children.length > 0) { + pushShaders(recorder, shader.children); + } + recorder.pushShader(shader.type, shader.props); + }); +}; + const pushMaskFilters = (recorder: Recorder, maskFilters: Node[]) => { if (maskFilters.length > 0) { recorder.pushBlurMaskFilter(maskFilters[maskFilters.length - 1].props); @@ -130,14 +139,19 @@ const pushMaskFilters = (recorder: Recorder, maskFilters: Node[]) => { const visitNode = (recorder: Recorder, node: Node) => { const { props } = node; - const { colorFilters, maskFilters, drawings } = sortNodeChildren(node); + const { colorFilters, maskFilters, drawings, shaders } = + sortNodeChildren(node); const paint = processPaint(props); const shouldPushPaint = - paint || colorFilters.length > 0 || maskFilters.length > 0; + paint || + colorFilters.length > 0 || + maskFilters.length > 0 || + shaders.length > 0; if (shouldPushPaint) { recorder.savePaint(paint ?? {}); pushColorFilters(recorder, colorFilters); pushMaskFilters(recorder, maskFilters); + pushShaders(recorder, shaders); recorder.materializePaint(); } const ctm = processCTM(props); diff --git a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts index 767e6c66c6..b1aacddffe 100644 --- a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts @@ -34,40 +34,11 @@ interface PushColorFilter props: Props[T]; } -const isBlendColorFilter = ( - command: Command -): command is PushColorFilter => { - return command.colorFilterType === NodeType.BlendColorFilter; -}; - -const isMatrixColorFilter = ( - command: Command -): command is PushColorFilter => { - return command.colorFilterType === NodeType.MatrixColorFilter; -}; - -const isLerpColorFilter = ( - command: Command -): command is PushColorFilter => { - return command.colorFilterType === NodeType.LerpColorFilter; -}; - -const isLumaColorFilter = ( - command: Command -): command is PushColorFilter => { - return command.colorFilterType === NodeType.LumaColorFilter; -}; - -const isLinearToSRGBGammaColorFilter = ( - command: Command -): command is PushColorFilter => { - return command.colorFilterType === NodeType.LinearToSRGBGammaColorFilter; -}; - -const isSRGBToLinearGammaColorFilter = ( - command: Command -): command is PushColorFilter => { - return command.colorFilterType === NodeType.SRGBToLinearGammaColorFilter; +const isColorFilter = ( + command: Command, + type: T +): command is PushColorFilter => { + return command.colorFilterType === type; }; export const composeColorFilters = (ctx: DrawingContext) => { @@ -93,15 +64,15 @@ export const pushColorFilter = ( command: Command ) => { let cf: SkColorFilter | undefined; - if (isBlendColorFilter(command)) { + if (isColorFilter(command, NodeType.BlendColorFilter)) { const { props } = command; const { mode } = props; const color = ctx.Skia.Color(props.color); cf = ctx.Skia.ColorFilter.MakeBlend(color, BlendMode[enumKey(mode)]); - } else if (isMatrixColorFilter(command)) { + } else if (isColorFilter(command, NodeType.MatrixColorFilter)) { const { matrix } = command.props; cf = ctx.Skia.ColorFilter.MakeMatrix(matrix); - } else if (isLerpColorFilter(command)) { + } else if (isColorFilter(command, NodeType.LerpColorFilter)) { const { props } = command; const { t } = props; const second = ctx.colorFilters.pop(); @@ -110,11 +81,11 @@ export const pushColorFilter = ( throw new Error("LerpColorFilter requires two color filters"); } cf = ctx.Skia.ColorFilter.MakeLerp(t, first, second); - } else if (isLumaColorFilter(command)) { + } else if (isColorFilter(command, NodeType.LumaColorFilter)) { cf = ctx.Skia.ColorFilter.MakeLumaColorFilter(); - } else if (isLinearToSRGBGammaColorFilter(command)) { + } else if (isColorFilter(command, NodeType.LinearToSRGBGammaColorFilter)) { cf = ctx.Skia.ColorFilter.MakeLinearToSRGBGamma(); - } else if (isSRGBToLinearGammaColorFilter(command)) { + } else if (isColorFilter(command, NodeType.SRGBToLinearGammaColorFilter)) { cf = ctx.Skia.ColorFilter.MakeSRGBToLinearGamma(); } if (!cf) { diff --git a/packages/skia/src/sksg/Recorder/commands/Shaders.ts b/packages/skia/src/sksg/Recorder/commands/Shaders.ts new file mode 100644 index 0000000000..51086f7172 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/Shaders.ts @@ -0,0 +1,270 @@ +import { + enumKey, + fitRects, + getRect, + processGradientProps, + processTransformProps, + rect2rect, +} from "../../../dom/nodes"; +import { NodeType } from "../../../dom/types"; +import type { + ColorProps, + FractalNoiseProps, + ImageShaderProps, + LinearGradientProps, + RadialGradientProps, + ShaderProps, + SweepGradientProps, + TurbulenceProps, + TwoPointConicalGradientProps, +} from "../../../dom/types"; +import { + FilterMode, + MipmapMode, + processUniforms, + TileMode, +} from "../../../skia/types"; +import type { Command } from "../Core"; +import { CommandType } from "../Core"; +import type { DrawingContext } from "../DrawingContext"; + +const declareShader = (ctx: DrawingContext, props: ShaderProps) => { + "worklet"; + const { source, uniforms, ...transform } = props; + const m3 = ctx.Skia.Matrix(); + processTransformProps(m3, transform); + const shader = source.makeShaderWithChildren( + processUniforms(source, uniforms), + ctx.shaders.splice(0, ctx.shaders.length), + m3 + ); + ctx.shaders.push(shader); +}; + +const declareColorShader = (ctx: DrawingContext, props: ColorProps) => { + "worklet"; + const { color } = props; + const shader = ctx.Skia.Shader.MakeColor(ctx.Skia.Color(color)); + ctx.shaders.push(shader); +}; + +const declareFractalNoiseShader = ( + ctx: DrawingContext, + props: FractalNoiseProps +) => { + "worklet"; + const { freqX, freqY, octaves, seed, tileWidth, tileHeight } = props; + const shader = ctx.Skia.Shader.MakeFractalNoise( + freqX, + freqY, + octaves, + seed, + tileWidth, + tileHeight + ); + ctx.shaders.push(shader); +}; + +const declareTwoPointConicalGradientShader = ( + ctx: DrawingContext, + props: TwoPointConicalGradientProps +) => { + "worklet"; + const { startR, endR, start, end } = props; + const { colors, positions, mode, localMatrix, flags } = processGradientProps( + ctx.Skia, + props + ); + const shader = ctx.Skia.Shader.MakeTwoPointConicalGradient( + start, + startR, + end, + endR, + colors, + positions, + mode, + localMatrix, + flags + ); + ctx.shaders.push(shader); +}; + +const declareRadialGradientShader = ( + ctx: DrawingContext, + props: RadialGradientProps +) => { + "worklet"; + const { c, r } = props; + const { colors, positions, mode, localMatrix, flags } = processGradientProps( + ctx.Skia, + props + ); + const shader = ctx.Skia.Shader.MakeRadialGradient( + c, + r, + colors, + positions, + mode, + localMatrix, + flags + ); + ctx.shaders.push(shader); +}; + +const declareSweepGradientShader = ( + ctx: DrawingContext, + props: SweepGradientProps +) => { + "worklet"; + const { c, start, end } = props; + const { colors, positions, mode, localMatrix, flags } = processGradientProps( + ctx.Skia, + props + ); + const shader = ctx.Skia.Shader.MakeSweepGradient( + c.x, + c.y, + colors, + positions, + mode, + localMatrix, + flags, + start, + end + ); + ctx.shaders.push(shader); +}; + +const declareLinearGradientShader = ( + ctx: DrawingContext, + props: LinearGradientProps +) => { + "worklet"; + const { start, end } = props; + const { colors, positions, mode, localMatrix, flags } = processGradientProps( + ctx.Skia, + props + ); + const shader = ctx.Skia.Shader.MakeLinearGradient( + start, + end, + colors, + positions ?? null, + mode, + localMatrix, + flags + ); + ctx.shaders.push(shader); +}; + +const declareTurbulenceShader = ( + ctx: DrawingContext, + props: TurbulenceProps +) => { + "worklet"; + const { freqX, freqY, octaves, seed, tileWidth, tileHeight } = props; + const shader = ctx.Skia.Shader.MakeTurbulence( + freqX, + freqY, + octaves, + seed, + tileWidth, + tileHeight + ); + ctx.shaders.push(shader); +}; + +const declareImageShader = (ctx: DrawingContext, props: ImageShaderProps) => { + "worklet"; + const { fit, image, tx, ty, fm, mm, ...imageShaderProps } = props; + if (!image) { + return; + } + + const rct = getRect(ctx.Skia, imageShaderProps); + const m3 = ctx.Skia.Matrix(); + if (rct) { + const rects = fitRects( + fit, + { x: 0, y: 0, width: image.width(), height: image.height() }, + rct + ); + const [x, y, sx, sy] = rect2rect(rects.src, rects.dst); + m3.translate(x.translateX, y.translateY); + m3.scale(sx.scaleX, sy.scaleY); + } + const lm = ctx.Skia.Matrix(); + lm.concat(m3); + processTransformProps(lm, imageShaderProps); + const shader = image.makeShaderOptions( + TileMode[enumKey(tx)], + TileMode[enumKey(ty)], + FilterMode[enumKey(fm)], + MipmapMode[enumKey(mm)], + lm + ); + ctx.shaders.push(shader); +}; + +export const isPushShader = ( + command: Command +): command is Command => { + return command.type === CommandType.PushShader; +}; + +export const setShaders = (ctx: DrawingContext) => { + "worklet"; + if (ctx.shaders.length > 0) { + ctx.paint.setShader(ctx.shaders[ctx.shaders.length - 1]); + } +}; + +type Props = { + [NodeType.Shader]: ShaderProps; + [NodeType.ImageShader]: ImageShaderProps; + [NodeType.ColorShader]: ColorProps; + [NodeType.Turbulence]: TurbulenceProps; + [NodeType.FractalNoise]: FractalNoiseProps; + [NodeType.LinearGradient]: LinearGradientProps; + [NodeType.RadialGradient]: RadialGradientProps; + [NodeType.SweepGradient]: SweepGradientProps; + [NodeType.TwoPointConicalGradient]: TwoPointConicalGradientProps; +}; + +interface PushShader + extends Command { + colorFilterType: T; + props: Props[T]; +} + +const isShader = ( + command: Command, + type: T +): command is PushShader => { + return command.colorFilterType === type; +}; + +export const pushShader = ( + ctx: DrawingContext, + command: Command +) => { + if (isShader(command, NodeType.Shader)) { + declareShader(ctx, command.props); + } else if (isShader(command, NodeType.ImageShader)) { + declareImageShader(ctx, command.props); + } else if (isShader(command, NodeType.ColorShader)) { + declareColorShader(ctx, command.props); + } else if (isShader(command, NodeType.Turbulence)) { + declareTurbulenceShader(ctx, command.props); + } else if (isShader(command, NodeType.FractalNoise)) { + declareFractalNoiseShader(ctx, command.props); + } else if (isShader(command, NodeType.LinearGradient)) { + declareLinearGradientShader(ctx, command.props); + } else if (isShader(command, NodeType.RadialGradient)) { + declareRadialGradientShader(ctx, command.props); + } else if (isShader(command, NodeType.SweepGradient)) { + declareSweepGradientShader(ctx, command.props); + } else if (isShader(command, NodeType.TwoPointConicalGradient)) { + declareTwoPointConicalGradientShader(ctx, command.props); + } +}; From 213ba277c1090010748f536611d6bca555e356d7 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 17:57:09 +0100 Subject: [PATCH 19/50] :wrench: --- packages/skia/src/sksg/Recorder/Recorder.ts | 4 ++-- packages/skia/src/sksg/Recorder/commands/Shaders.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index aae003019e..ea3995092e 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -74,8 +74,8 @@ export class Recorder { }); } - pushShader(sharderType: NodeType, props: AnimatedProps) { - this.add({ type: CommandType.PushShader, sharderType, props }); + pushShader(shaderType: NodeType, props: AnimatedProps) { + this.add({ type: CommandType.PushShader, shaderType, props }); } pushBlurMaskFilter(props: AnimatedProps) { diff --git a/packages/skia/src/sksg/Recorder/commands/Shaders.ts b/packages/skia/src/sksg/Recorder/commands/Shaders.ts index 51086f7172..817f8dbe6c 100644 --- a/packages/skia/src/sksg/Recorder/commands/Shaders.ts +++ b/packages/skia/src/sksg/Recorder/commands/Shaders.ts @@ -233,7 +233,7 @@ type Props = { interface PushShader extends Command { - colorFilterType: T; + shaderType: T; props: Props[T]; } @@ -241,7 +241,7 @@ const isShader = ( command: Command, type: T ): command is PushShader => { - return command.colorFilterType === type; + return command.shaderType === type; }; export const pushShader = ( @@ -266,5 +266,7 @@ export const pushShader = ( declareSweepGradientShader(ctx, command.props); } else if (isShader(command, NodeType.TwoPointConicalGradient)) { declareTwoPointConicalGradientShader(ctx, command.props); + } else { + throw new Error(`Unknown shader type: ${command.shaderType}`); } }; From e36d50e88daa3bceafd7d82b09c0aaca5cbdafc7 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 18:33:02 +0100 Subject: [PATCH 20/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 2 + .../skia/src/sksg/Recorder/DrawingContext.ts | 2 + packages/skia/src/sksg/Recorder/Player.ts | 13 +- packages/skia/src/sksg/Recorder/Recorder.ts | 24 +++- packages/skia/src/sksg/Recorder/Visitor.ts | 20 ++- .../sksg/Recorder/commands/ImageFilters.ts | 115 +++++++++++++++++- packages/skia/src/sksg/nodes/Node.ts | 97 +++++++++------ 7 files changed, 226 insertions(+), 47 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 0b26418f9b..ee44022e64 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -34,8 +34,10 @@ export enum CommandType { RestoreCTM = "RestoreCTM", PushColorFilter = "PushColorFilter", PushBlurMaskFilter = "PushBlurMaskFilter", + PushImageFilter = "PushImageFilter", PushShader = "PushShader", ComposeColorFilter = "ComposeColorFilter", + ComposeImageFilter = "ComposeImageFilter", MaterializePaint = "MaterializePaint", // Drawing DrawImage = "DrawImage", diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index dd31088dc0..b39718ea98 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -6,6 +6,7 @@ import type { SkColorFilter, SkPaint, SkShader, + SkImageFilter, } from "../../skia/types"; export class DrawingContext { @@ -14,6 +15,7 @@ export class DrawingContext { paints: SkPaint[] = []; colorFilters: SkColorFilter[] = []; shaders: SkShader[] = []; + imageFilters: SkImageFilter[] = []; constructor(Skia: Skia, canvas: SkCanvas) { this.Skia = Skia; diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 58ae707e28..90741710d7 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -29,7 +29,13 @@ import { setColorFilters, } from "./commands/ColorFilters"; import { saveCTM } from "./commands/CTM"; -import { setBlurMaskFilter } from "./commands/ImageFilters"; +import { + setBlurMaskFilter, + isPushImageFilter, + pushImageFilter, + composeImageFilters, + setImageFilters, +} from "./commands/ImageFilters"; import { setPaintProperties } from "./commands/Paint"; import { isPushShader, pushShader, setShaders } from "./commands/Shaders"; import { @@ -53,10 +59,15 @@ const play = (ctx: DrawingContext, command: Command) => { } else if (isCommand(command, CommandType.MaterializePaint)) { setColorFilters(ctx); setShaders(ctx); + setImageFilters(ctx); } else if (isPushColorFilter(command)) { pushColorFilter(ctx, command); } else if (isPushShader(command)) { pushShader(ctx, command); + } else if (isPushImageFilter(command)) { + pushImageFilter(ctx, command); + } else if (isCommand(command, CommandType.ComposeImageFilter)) { + composeImageFilters(ctx); } else if (isDrawCommand(command, CommandType.PushBlurMaskFilter)) { setBlurMaskFilter(ctx, command.props); } else if (isDrawCommand(command, CommandType.SaveCTM)) { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index ea3995092e..b2cbca62ec 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -27,6 +27,7 @@ import type { } from "../../dom/types"; import type { AnimatedProps } from "../../renderer"; import { isSharedValue } from "../nodes/utils"; +import { isColorFilter, isImageFilter, isShader } from "../nodes"; import { CommandType } from "./Core"; import type { Command } from "./Core"; @@ -66,7 +67,21 @@ export class Recorder { this.add({ type: CommandType.MaterializePaint }); } + pushImageFilter(imageFilterType: NodeType, props: AnimatedProps) { + if (!isImageFilter(imageFilterType)) { + throw new Error("Invalid color filter type: " + imageFilterType); + } + this.add({ + type: CommandType.PushImageFilter, + imageFilterType, + props, + }); + } + pushColorFilter(colorFilterType: NodeType, props: AnimatedProps) { + if (!isColorFilter(colorFilterType)) { + throw new Error("Invalid color filter type: " + colorFilterType); + } this.add({ type: CommandType.PushColorFilter, colorFilterType, @@ -75,6 +90,9 @@ export class Recorder { } pushShader(shaderType: NodeType, props: AnimatedProps) { + if (!isShader(shaderType)) { + throw new Error("Invalid color filter type: " + shaderType); + } this.add({ type: CommandType.PushShader, shaderType, props }); } @@ -82,10 +100,14 @@ export class Recorder { this.add({ type: CommandType.PushBlurMaskFilter, props }); } - composeColorFilters() { + composeColorFilter() { this.add({ type: CommandType.ComposeColorFilter }); } + composeImageFilter() { + this.add({ type: CommandType.ComposeImageFilter }); + } + saveCTM(props: AnimatedProps) { this.add({ type: CommandType.SaveCTM, props }); } diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 11357bad0a..1f30c05e51 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -117,7 +117,22 @@ const pushColorFilters = (recorder: Recorder, colorFilters: Node[]) => { colorFilter.type !== NodeType.LerpColorFilter && colorFilter.children.length > 0; if (needsComposition) { - recorder.composeColorFilters(); + recorder.composeColorFilter(); + } + }); +}; + +const pushImageFilters = (recorder: Recorder, imageFilters: Node[]) => { + imageFilters.forEach((imageFilter) => { + if (imageFilter.children.length > 0) { + pushImageFilters(recorder, imageFilter.children); + } + recorder.pushImageFilter(imageFilter.type, imageFilter.props); + const needsComposition = + imageFilter.type !== NodeType.BlendImageFilter && + imageFilter.children.length > 0; + if (needsComposition) { + recorder.composeImageFilter(); } }); }; @@ -139,7 +154,7 @@ const pushMaskFilters = (recorder: Recorder, maskFilters: Node[]) => { const visitNode = (recorder: Recorder, node: Node) => { const { props } = node; - const { colorFilters, maskFilters, drawings, shaders } = + const { colorFilters, maskFilters, drawings, shaders, imageFilters } = sortNodeChildren(node); const paint = processPaint(props); const shouldPushPaint = @@ -150,6 +165,7 @@ const visitNode = (recorder: Recorder, node: Node) => { if (shouldPushPaint) { recorder.savePaint(paint ?? {}); pushColorFilters(recorder, colorFilters); + pushImageFilters(recorder, imageFilters); pushMaskFilters(recorder, maskFilters); pushShaders(recorder, shaders); recorder.materializePaint(); diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts index 36b24ac640..5392d62e2b 100644 --- a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -1,8 +1,67 @@ -import { enumKey } from "../../../dom/nodes"; -import type { BlurMaskFilterProps } from "../../../dom/types"; -import { BlurStyle } from "../../../skia/types"; +"worklet"; + +import { enumKey, processRadius } from "../../../dom/nodes"; +import type { + BlendImageFilterProps, + BlurImageFilterProps, + BlurMaskFilterProps, + DisplacementMapImageFilterProps, + DropShadowImageFilterProps, + MorphologyImageFilterProps, + OffsetImageFilterProps, + RuntimeShaderImageFilterProps, +} from "../../../dom/types"; +import { NodeType } from "../../../dom/types"; +import { BlurStyle, TileMode } from "../../../skia/types"; +import type { Command } from "../Core"; +import { CommandType } from "../Core"; import type { DrawingContext } from "../DrawingContext"; +export enum MorphologyOperator { + Erode, + Dilate, +} + +const declareBlurImageFilter = ( + ctx: DrawingContext, + props: BlurImageFilterProps +) => { + "worklet"; + const { mode, blur } = props; + const sigma = processRadius(ctx.Skia, blur); + const imgf = ctx.Skia.ImageFilter.MakeBlur( + sigma.x, + sigma.y, + TileMode[enumKey(mode)], + null + ); + ctx.imageFilters.push(imgf); +}; + +const declareMorphologyImageFilter = ( + ctx: DrawingContext, + props: MorphologyImageFilterProps +) => { + "worklet"; + const { operator } = props; + const r = processRadius(ctx.Skia, props.radius); + let imgf; + if (MorphologyOperator[enumKey(operator)] === MorphologyOperator.Erode) { + imgf = ctx.Skia.ImageFilter.MakeErode(r.x, r.y, null); + } else { + imgf = ctx.Skia.ImageFilter.MakeDilate(r.x, r.y, null); + } + return imgf; +}; + +export const composeImageFilters = (ctx: DrawingContext) => { + if (ctx.imageFilters.length > 1) { + const outer = ctx.imageFilters.pop()!; + const inner = ctx.imageFilters.pop()!; + ctx.imageFilters.push(ctx.Skia.ImageFilter.MakeCompose(outer, inner)); + } +}; + export const setBlurMaskFilter = ( ctx: DrawingContext, props: BlurMaskFilterProps @@ -16,3 +75,53 @@ export const setBlurMaskFilter = ( ); ctx.paint.setMaskFilter(mf); }; + +export const isPushImageFilter = ( + command: Command +): command is Command => { + return command.type === CommandType.PushImageFilter; +}; + +type Props = { + [NodeType.OffsetImageFilter]: OffsetImageFilterProps; + [NodeType.DisplacementMapImageFilter]: DisplacementMapImageFilterProps; + [NodeType.BlurImageFilter]: BlurImageFilterProps; + [NodeType.DropShadowImageFilter]: DropShadowImageFilterProps; + [NodeType.MorphologyImageFilter]: MorphologyImageFilterProps; + [NodeType.BlendImageFilter]: BlendImageFilterProps; + [NodeType.RuntimeShaderImageFilter]: RuntimeShaderImageFilterProps; +}; + +interface PushImageFilter + extends Command { + imageFilterType: T; + props: Props[T]; +} + +export const setImageFilters = (ctx: DrawingContext) => { + if (ctx.imageFilters.length > 0) { + ctx.paint.setImageFilter( + ctx.imageFilters.reduceRight((inner, outer) => + inner ? ctx.Skia.ImageFilter.MakeCompose(outer, inner) : outer + ) + ); + } +}; + +const isImageFilter = ( + command: Command, + type: T +): command is PushImageFilter => { + return command.imageFilterType === type; +}; + +export const pushImageFilter = ( + ctx: DrawingContext, + command: Command +) => { + if (isImageFilter(command, NodeType.BlurImageFilter)) { + declareBlurImageFilter(ctx, command.props); + } else if (isImageFilter(command, NodeType.MorphologyImageFilter)) { + declareMorphologyImageFilter(ctx, command.props); + } +}; diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index 30925deb8a..0a92082137 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -24,6 +24,59 @@ export const sortNodes = (children: Node[]) => { return { declarations, drawings }; }; +export const isColorFilter = (type: NodeType) => { + "worklet"; + return ( + type === NodeType.BlendColorFilter || + type === NodeType.MatrixColorFilter || + type === NodeType.LerpColorFilter || + type === NodeType.LumaColorFilter || + type === NodeType.SRGBToLinearGammaColorFilter || + type === NodeType.LinearToSRGBGammaColorFilter + ); +}; + +export const isPathEffect = (type: NodeType) => { + "worklet"; + return ( + type === NodeType.DiscretePathEffect || + type === NodeType.DashPathEffect || + type === NodeType.Path1DPathEffect || + type === NodeType.Path2DPathEffect || + type === NodeType.CornerPathEffect || + type === NodeType.SumPathEffect || + type === NodeType.Line2DPathEffect + ); +}; + +export const isImageFilter = (type: NodeType) => { + "worklet"; + return ( + type === NodeType.OffsetImageFilter || + type === NodeType.DisplacementMapImageFilter || + type === NodeType.BlurImageFilter || + type === NodeType.DropShadowImageFilter || + type === NodeType.MorphologyImageFilter || + type === NodeType.BlendImageFilter || + type === NodeType.RuntimeShaderImageFilter + ); +}; + +export const isShader = (type: NodeType) => { + "worklet"; + return ( + type === NodeType.Shader || + type === NodeType.ImageShader || + type === NodeType.ColorShader || + type === NodeType.Turbulence || + type === NodeType.FractalNoise || + type === NodeType.LinearGradient || + type === NodeType.RadialGradient || + type === NodeType.SweepGradient || + type === NodeType.TwoPointConicalGradient + ); +}; + export const sortNodeChildren = (parent: Node) => { "worklet"; const maskFilters: Node[] = []; @@ -34,51 +87,15 @@ export const sortNodeChildren = (parent: Node) => { const drawings: Node[] = []; const declarations: Node[] = []; parent.children.forEach((node) => { - if ( - node.type === NodeType.BlendColorFilter || - node.type === NodeType.MatrixColorFilter || - node.type === NodeType.LerpColorFilter || - node.type === NodeType.LumaColorFilter || - node.type === NodeType.SRGBToLinearGammaColorFilter || - node.type === NodeType.LinearToSRGBGammaColorFilter - ) { + if (isColorFilter(node.type)) { colorFilters.push(node); } else if (node.type === NodeType.BlurMaskFilter) { maskFilters.push(node); - } else if ( - // Path Effects - node.type === NodeType.DiscretePathEffect || - node.type === NodeType.DashPathEffect || - node.type === NodeType.Path1DPathEffect || - node.type === NodeType.Path2DPathEffect || - node.type === NodeType.CornerPathEffect || - node.type === NodeType.SumPathEffect || - node.type === NodeType.Line2DPathEffect - ) { + } else if (isPathEffect(node.type)) { pathEffects.push(node); - } else if ( - // Image Filters - node.type === NodeType.OffsetImageFilter || - node.type === NodeType.DisplacementMapImageFilter || - node.type === NodeType.BlurImageFilter || - node.type === NodeType.DropShadowImageFilter || - node.type === NodeType.MorphologyImageFilter || - node.type === NodeType.BlendImageFilter || - node.type === NodeType.RuntimeShaderImageFilter - ) { + } else if (isImageFilter(node.type)) { imageFilters.push(node); - } else if ( - // Shaders - node.type === NodeType.Shader || - node.type === NodeType.ImageShader || - node.type === NodeType.ColorShader || - node.type === NodeType.Turbulence || - node.type === NodeType.FractalNoise || - node.type === NodeType.LinearGradient || - node.type === NodeType.RadialGradient || - node.type === NodeType.SweepGradient || - node.type === NodeType.TwoPointConicalGradient - ) { + } else if (isShader(node.type)) { shaders.push(node); } else if (node.isDeclaration) { declarations.push(node); From 5759b1d34b163b32c2d3b6058cb4a79bfdc3ee81 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 23:01:07 +0100 Subject: [PATCH 21/50] :wrench: --- packages/skia/src/sksg/Recorder/Visitor.ts | 12 ++- .../sksg/Recorder/commands/ImageFilters.ts | 79 ++++++++++++++++++- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 1f30c05e51..63a637a320 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -154,13 +154,21 @@ const pushMaskFilters = (recorder: Recorder, maskFilters: Node[]) => { const visitNode = (recorder: Recorder, node: Node) => { const { props } = node; - const { colorFilters, maskFilters, drawings, shaders, imageFilters } = - sortNodeChildren(node); + const { + colorFilters, + maskFilters, + drawings, + shaders, + imageFilters, + pathEffects, + } = sortNodeChildren(node); const paint = processPaint(props); const shouldPushPaint = paint || colorFilters.length > 0 || maskFilters.length > 0 || + imageFilters.length > 0 || + pathEffects.length > 0 || shaders.length > 0; if (shouldPushPaint) { recorder.savePaint(paint ?? {}); diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts index 5392d62e2b..116ecefa89 100644 --- a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -12,7 +12,8 @@ import type { RuntimeShaderImageFilterProps, } from "../../../dom/types"; import { NodeType } from "../../../dom/types"; -import { BlurStyle, TileMode } from "../../../skia/types"; +import type { SkColor, Skia, SkImageFilter } from "../../../skia/types"; +import { BlendMode, BlurStyle, TileMode } from "../../../skia/types"; import type { Command } from "../Core"; import { CommandType } from "../Core"; import type { DrawingContext } from "../DrawingContext"; @@ -22,6 +23,43 @@ export enum MorphologyOperator { Dilate, } +const Black = Float32Array.of(0, 0, 0, 1); + +const MakeInnerShadow = ( + Skia: Skia, + shadowOnly: boolean | undefined, + dx: number, + dy: number, + sigmaX: number, + sigmaY: number, + color: SkColor, + input: SkImageFilter | null +) => { + "worklet"; + const sourceGraphic = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(Black, BlendMode.Dst), + null + ); + const sourceAlpha = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(Black, BlendMode.SrcIn), + null + ); + const f1 = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(color, BlendMode.SrcOut), + null + ); + const f2 = Skia.ImageFilter.MakeOffset(dx, dy, f1); + const f3 = Skia.ImageFilter.MakeBlur(sigmaX, sigmaY, TileMode.Decal, f2); + const f4 = Skia.ImageFilter.MakeBlend(BlendMode.SrcIn, sourceAlpha, f3); + if (shadowOnly) { + return f4; + } + return Skia.ImageFilter.MakeCompose( + input, + Skia.ImageFilter.MakeBlend(BlendMode.SrcOver, sourceGraphic, f4) + ); +}; + const declareBlurImageFilter = ( ctx: DrawingContext, props: BlurImageFilterProps @@ -51,7 +89,36 @@ const declareMorphologyImageFilter = ( } else { imgf = ctx.Skia.ImageFilter.MakeDilate(r.x, r.y, null); } - return imgf; + ctx.imageFilters.push(imgf); +}; + +export const declareOffsetImageFilter = ( + ctx: DrawingContext, + props: OffsetImageFilterProps +) => { + "worklet"; + const { x, y } = props; + const imgf = ctx.Skia.ImageFilter.MakeOffset(x, y, null); + ctx.imageFilters.push(imgf); +}; + +export const declareDropShadowImageFilter = ( + ctx: DrawingContext, + props: DropShadowImageFilterProps +) => { + "worklet"; + const { dx, dy, blur, shadowOnly, color: cl, inner } = props; + const color = ctx.Skia.Color(cl); + let factory; + if (inner) { + factory = MakeInnerShadow.bind(null, ctx.Skia, shadowOnly); + } else { + factory = shadowOnly + ? ctx.Skia.ImageFilter.MakeDropShadowOnly.bind(ctx.Skia.ImageFilter) + : ctx.Skia.ImageFilter.MakeDropShadow.bind(ctx.Skia.ImageFilter); + } + const imgf = factory(dx, dy, blur, blur, color, null); + ctx.imageFilters.push(imgf); }; export const composeImageFilters = (ctx: DrawingContext) => { @@ -123,5 +190,13 @@ export const pushImageFilter = ( declareBlurImageFilter(ctx, command.props); } else if (isImageFilter(command, NodeType.MorphologyImageFilter)) { declareMorphologyImageFilter(ctx, command.props); + } else if (isImageFilter(command, NodeType.BlendImageFilter)) { + } else if (isImageFilter(command, NodeType.DisplacementMapImageFilter)) { + } else if (isImageFilter(command, NodeType.DropShadowImageFilter)) { + declareDropShadowImageFilter(ctx, command.props); + } else if (isImageFilter(command, NodeType.OffsetImageFilter)) { + declareOffsetImageFilter(ctx, command.props); + } else { + throw new Error("Invalid image filter type: " + command.imageFilterType); } }; From 6b80b314ef6e729dd77ab0ab7262852bddbb0d17 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 23:06:04 +0100 Subject: [PATCH 22/50] :wrench: --- .../skia/src/sksg/Recorder/DrawingContext.ts | 26 +++++++++++++++++++ packages/skia/src/sksg/Recorder/Player.ts | 8 ++---- .../sksg/Recorder/commands/ColorFilters.ts | 10 ------- .../sksg/Recorder/commands/ImageFilters.ts | 10 ------- .../src/sksg/Recorder/commands/Shaders.ts | 7 ----- 5 files changed, 28 insertions(+), 33 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index b39718ea98..249bd9ba71 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -34,4 +34,30 @@ export class DrawingContext { restorePaint() { this.paints.pop(); } + + materializePaint() { + // Color Filters + if (this.colorFilters.length > 0) { + this.paint.setColorFilter( + this.colorFilters.reduceRight((inner, outer) => + inner ? this.Skia.ColorFilter.MakeCompose(outer, inner) : outer + ) + ); + } + // Shaders + if (this.shaders.length > 0) { + this.paint.setShader(this.shaders[this.shaders.length - 1]); + } + // Image Filters + if (this.imageFilters.length > 0) { + this.paint.setImageFilter( + this.imageFilters.reduceRight((inner, outer) => + inner ? this.Skia.ImageFilter.MakeCompose(outer, inner) : outer + ) + ); + } + this.colorFilters = []; + this.shaders = []; + this.imageFilters = []; + } } diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 90741710d7..84bf4650c0 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -26,7 +26,6 @@ import { composeColorFilters, isPushColorFilter, pushColorFilter, - setColorFilters, } from "./commands/ColorFilters"; import { saveCTM } from "./commands/CTM"; import { @@ -34,10 +33,9 @@ import { isPushImageFilter, pushImageFilter, composeImageFilters, - setImageFilters, } from "./commands/ImageFilters"; import { setPaintProperties } from "./commands/Paint"; -import { isPushShader, pushShader, setShaders } from "./commands/Shaders"; +import { isPushShader, pushShader } from "./commands/Shaders"; import { CommandType, isCommand, @@ -57,9 +55,7 @@ const play = (ctx: DrawingContext, command: Command) => { } else if (isCommand(command, CommandType.ComposeColorFilter)) { composeColorFilters(ctx); } else if (isCommand(command, CommandType.MaterializePaint)) { - setColorFilters(ctx); - setShaders(ctx); - setImageFilters(ctx); + ctx.materializePaint(); } else if (isPushColorFilter(command)) { pushColorFilter(ctx, command); } else if (isPushShader(command)) { diff --git a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts index b1aacddffe..a9fc96ed8b 100644 --- a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts @@ -49,16 +49,6 @@ export const composeColorFilters = (ctx: DrawingContext) => { } }; -export const setColorFilters = (ctx: DrawingContext) => { - if (ctx.colorFilters.length > 0) { - ctx.paint.setColorFilter( - ctx.colorFilters.reduceRight((inner, outer) => - inner ? ctx.Skia.ColorFilter.MakeCompose(outer, inner) : outer - ) - ); - } -}; - export const pushColorFilter = ( ctx: DrawingContext, command: Command diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts index 116ecefa89..5a9484882a 100644 --- a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -165,16 +165,6 @@ interface PushImageFilter props: Props[T]; } -export const setImageFilters = (ctx: DrawingContext) => { - if (ctx.imageFilters.length > 0) { - ctx.paint.setImageFilter( - ctx.imageFilters.reduceRight((inner, outer) => - inner ? ctx.Skia.ImageFilter.MakeCompose(outer, inner) : outer - ) - ); - } -}; - const isImageFilter = ( command: Command, type: T diff --git a/packages/skia/src/sksg/Recorder/commands/Shaders.ts b/packages/skia/src/sksg/Recorder/commands/Shaders.ts index 817f8dbe6c..0b9a801630 100644 --- a/packages/skia/src/sksg/Recorder/commands/Shaders.ts +++ b/packages/skia/src/sksg/Recorder/commands/Shaders.ts @@ -212,13 +212,6 @@ export const isPushShader = ( return command.type === CommandType.PushShader; }; -export const setShaders = (ctx: DrawingContext) => { - "worklet"; - if (ctx.shaders.length > 0) { - ctx.paint.setShader(ctx.shaders[ctx.shaders.length - 1]); - } -}; - type Props = { [NodeType.Shader]: ShaderProps; [NodeType.ImageShader]: ImageShaderProps; From 757688a88b43c325064f861eab4b3bd796c3eb73 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sat, 4 Jan 2025 23:17:16 +0100 Subject: [PATCH 23/50] :wrench: --- .../__tests__/e2e/ImageFilters.spec.tsx | 1 + packages/skia/src/sksg/Recorder/Visitor.ts | 8 +++- .../sksg/Recorder/commands/ImageFilters.ts | 45 ++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx index 1bc99a9d3f..7b6ed79a5f 100644 --- a/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx @@ -191,6 +191,7 @@ describe("Test Image Filters", () => { threshold: 0.05, }); }); + // TODO: build reference result here it("should show outer and inner shadows on text", async () => { const path = // eslint-disable-next-line max-len diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 63a637a320..78a232d339 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -2,7 +2,7 @@ import type { CTMProps, DrawingNodeProps, PaintProps } from "../../dom/types"; import { NodeType } from "../../dom/types"; import type { Node } from "../nodes"; -import { sortNodeChildren } from "../nodes"; +import { isImageFilter, isShader, sortNodeChildren } from "../nodes"; import type { Recorder } from "./Recorder"; @@ -127,7 +127,11 @@ const pushImageFilters = (recorder: Recorder, imageFilters: Node[]) => { if (imageFilter.children.length > 0) { pushImageFilters(recorder, imageFilter.children); } - recorder.pushImageFilter(imageFilter.type, imageFilter.props); + if (isImageFilter(imageFilter.type)) { + recorder.pushImageFilter(imageFilter.type, imageFilter.props); + } else if (isShader(imageFilter.type)) { + recorder.pushShader(imageFilter.type, imageFilter.props); + } const needsComposition = imageFilter.type !== NodeType.BlendImageFilter && imageFilter.children.length > 0; diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts index 5a9484882a..4fed49a256 100644 --- a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -13,7 +13,12 @@ import type { } from "../../../dom/types"; import { NodeType } from "../../../dom/types"; import type { SkColor, Skia, SkImageFilter } from "../../../skia/types"; -import { BlendMode, BlurStyle, TileMode } from "../../../skia/types"; +import { + BlendMode, + BlurStyle, + ColorChannel, + TileMode, +} from "../../../skia/types"; import type { Command } from "../Core"; import { CommandType } from "../Core"; import type { DrawingContext } from "../DrawingContext"; @@ -121,6 +126,42 @@ export const declareDropShadowImageFilter = ( ctx.imageFilters.push(imgf); }; +export const declareBlendImageFilter = ( + ctx: DrawingContext, + props: BlendImageFilterProps +) => { + "worklet"; + const { mode } = props; + const a = ctx.imageFilters.pop(); + const b = ctx.imageFilters.pop(); + if (!a || !b) { + throw new Error("BlendImageFilter requires two image filters"); + } + const imgf = ctx.Skia.ImageFilter.MakeBlend(mode, a, b); + ctx.imageFilters.push(imgf); +}; + +export const declareDisplacementMapImageFilter = ( + ctx: DrawingContext, + props: DisplacementMapImageFilterProps +) => { + "worklet"; + const { channelX, channelY, scale } = props; + const shader = ctx.shaders.pop(); + if (!shader) { + throw new Error("DisplacementMap expects a shader as child"); + } + const map = ctx.Skia.ImageFilter.MakeShader(shader, null); + const imgf = ctx.Skia.ImageFilter.MakeDisplacementMap( + ColorChannel[enumKey(channelX)], + ColorChannel[enumKey(channelY)], + scale, + map, + null + ); + ctx.imageFilters.push(imgf); +}; + export const composeImageFilters = (ctx: DrawingContext) => { if (ctx.imageFilters.length > 1) { const outer = ctx.imageFilters.pop()!; @@ -181,7 +222,9 @@ export const pushImageFilter = ( } else if (isImageFilter(command, NodeType.MorphologyImageFilter)) { declareMorphologyImageFilter(ctx, command.props); } else if (isImageFilter(command, NodeType.BlendImageFilter)) { + declareBlendImageFilter(ctx, command.props); } else if (isImageFilter(command, NodeType.DisplacementMapImageFilter)) { + declareDisplacementMapImageFilter(ctx, command.props); } else if (isImageFilter(command, NodeType.DropShadowImageFilter)) { declareDropShadowImageFilter(ctx, command.props); } else if (isImageFilter(command, NodeType.OffsetImageFilter)) { From ef0eb13c363b0370cbc34a27b2689d3f52cd549a Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 00:01:44 +0100 Subject: [PATCH 24/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 1 + packages/skia/src/sksg/Recorder/Player.ts | 16 +++++++++++++++- packages/skia/src/sksg/Recorder/Recorder.ts | 4 ++++ packages/skia/src/sksg/Recorder/Visitor.ts | 8 +++++++- 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index ee44022e64..99111858ae 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -39,6 +39,7 @@ export enum CommandType { ComposeColorFilter = "ComposeColorFilter", ComposeImageFilter = "ComposeImageFilter", MaterializePaint = "MaterializePaint", + SaveBackdropFilter = "SaveBackdropFilter", // Drawing DrawImage = "DrawImage", DrawCircle = "DrawCircle", diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 84bf4650c0..dd40bf81df 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -1,5 +1,6 @@ "worklet"; +import type { SkImageFilter } from "../../skia"; import { drawCircle, drawImage, @@ -47,7 +48,20 @@ import type { DrawingContext } from "./DrawingContext"; const play = (ctx: DrawingContext, command: Command) => { materializeProps(command); - if (isDrawCommand(command, CommandType.SavePaint)) { + if (isCommand(command, CommandType.SaveBackdropFilter)) { + let imageFilter: SkImageFilter | null = null; + const imgf = ctx.imageFilters.pop(); + if (imgf) { + imageFilter = imgf; + } else { + const cf = ctx.colorFilters.pop(); + if (cf) { + imageFilter = ctx.Skia.ImageFilter.MakeColorFilter(cf, null); + } + } + ctx.canvas.saveLayer(undefined, null, imageFilter); + ctx.canvas.restore(); + } else if (isDrawCommand(command, CommandType.SavePaint)) { ctx.savePaint(); setPaintProperties(ctx.Skia, ctx.paint, command.props); } else if (isCommand(command, CommandType.RestorePaint)) { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index b2cbca62ec..f1d8440f0c 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -120,6 +120,10 @@ export class Recorder { this.add({ type: CommandType.DrawPaint }); } + saveBackdropFilter() { + this.add({ type: CommandType.SaveBackdropFilter }); + } + drawImage(props: AnimatedProps) { this.add({ type: CommandType.DrawImage, props }); } diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 78a232d339..b63d730ef6 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -180,13 +180,19 @@ const visitNode = (recorder: Recorder, node: Node) => { pushImageFilters(recorder, imageFilters); pushMaskFilters(recorder, maskFilters); pushShaders(recorder, shaders); - recorder.materializePaint(); + // For mixed nodes like BackdropFilters we don't materialize the paint + if (node.type !== NodeType.BackdropFilter) { + recorder.materializePaint(); + } } const ctm = processCTM(props); if (ctm) { recorder.saveCTM(ctm); } switch (node.type) { + case NodeType.BackdropFilter: + recorder.saveBackdropFilter(); + break; case NodeType.Fill: recorder.drawPaint(); break; From aea00f571f022c2d368c91c05e0531fe0378bde2 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 00:03:31 +0100 Subject: [PATCH 25/50] :wrench: --- packages/skia/src/sksg/Recorder/DrawingContext.ts | 15 +++++++++++++++ packages/skia/src/sksg/Recorder/Player.ts | 14 +------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index 249bd9ba71..04fe624970 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -27,6 +27,21 @@ export class DrawingContext { this.paints.push(this.paint.copy()); } + saveBackdropFilter() { + let imageFilter: SkImageFilter | null = null; + const imgf = this.imageFilters.pop(); + if (imgf) { + imageFilter = imgf; + } else { + const cf = this.colorFilters.pop(); + if (cf) { + imageFilter = this.Skia.ImageFilter.MakeColorFilter(cf, null); + } + } + this.canvas.saveLayer(undefined, null, imageFilter); + this.canvas.restore(); + } + get paint() { return this.paints[this.paints.length - 1]; } diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index dd40bf81df..f2f5c49b52 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -1,6 +1,5 @@ "worklet"; -import type { SkImageFilter } from "../../skia"; import { drawCircle, drawImage, @@ -49,18 +48,7 @@ import type { DrawingContext } from "./DrawingContext"; const play = (ctx: DrawingContext, command: Command) => { materializeProps(command); if (isCommand(command, CommandType.SaveBackdropFilter)) { - let imageFilter: SkImageFilter | null = null; - const imgf = ctx.imageFilters.pop(); - if (imgf) { - imageFilter = imgf; - } else { - const cf = ctx.colorFilters.pop(); - if (cf) { - imageFilter = ctx.Skia.ImageFilter.MakeColorFilter(cf, null); - } - } - ctx.canvas.saveLayer(undefined, null, imageFilter); - ctx.canvas.restore(); + ctx.saveBackdropFilter(); } else if (isDrawCommand(command, CommandType.SavePaint)) { ctx.savePaint(); setPaintProperties(ctx.Skia, ctx.paint, command.props); From d554aab07eddd0008d736d86395177d58f3b6dfd Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 00:07:16 +0100 Subject: [PATCH 26/50] :wrench: --- packages/skia/src/sksg/Recorder/Visitor.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index b63d730ef6..59e3043cf5 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -181,7 +181,9 @@ const visitNode = (recorder: Recorder, node: Node) => { pushMaskFilters(recorder, maskFilters); pushShaders(recorder, shaders); // For mixed nodes like BackdropFilters we don't materialize the paint - if (node.type !== NodeType.BackdropFilter) { + if (node.type === NodeType.BackdropFilter) { + recorder.saveBackdropFilter(); + } else { recorder.materializePaint(); } } @@ -190,9 +192,6 @@ const visitNode = (recorder: Recorder, node: Node) => { recorder.saveCTM(ctm); } switch (node.type) { - case NodeType.BackdropFilter: - recorder.saveBackdropFilter(); - break; case NodeType.Fill: recorder.drawPaint(); break; From 7658131f4b0bd59c7e5e882938c8c4a61d917923 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 00:23:03 +0100 Subject: [PATCH 27/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 15 +++-- packages/skia/src/sksg/Recorder/Player.ts | 6 +- packages/skia/src/sksg/Recorder/Recorder.ts | 30 +++++++++ packages/skia/src/sksg/Recorder/Visitor.ts | 14 ++++- .../skia/src/sksg/Recorder/commands/Box.ts | 61 +++++++++++++++++++ 5 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 packages/skia/src/sksg/Recorder/commands/Box.ts diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 99111858ae..9b247c299e 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -41,6 +41,7 @@ export enum CommandType { MaterializePaint = "MaterializePaint", SaveBackdropFilter = "SaveBackdropFilter", // Drawing + DrawBox = "DrawBox", DrawImage = "DrawImage", DrawCircle = "DrawCircle", DrawPaint = "DrawPaint", @@ -68,15 +69,13 @@ export type Command = { [key: string]: unknown; }; -export const materializeProps = (command: Command) => { +export const materializeProps = (command: { + props: Record; + animatedProps?: Record>; +}) => { if (command.animatedProps) { - const animatedProps = command.animatedProps as Record< - string, - SharedValue - >; - const commandProps = command.props as Record; - for (const key in animatedProps) { - commandProps[key] = animatedProps[key].value; + for (const key in command.animatedProps) { + command.props[key] = command.animatedProps[key].value; } } }; diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index f2f5c49b52..0c3d25e4cf 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -22,6 +22,7 @@ import { drawPatch, } from "../nodes/drawings"; +import { drawBox, isBoxCommand } from "./commands/Box"; import { composeColorFilters, isPushColorFilter, @@ -46,7 +47,8 @@ import { import type { DrawingContext } from "./DrawingContext"; const play = (ctx: DrawingContext, command: Command) => { - materializeProps(command); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + materializeProps(command as any); if (isCommand(command, CommandType.SaveBackdropFilter)) { ctx.saveBackdropFilter(); } else if (isDrawCommand(command, CommandType.SavePaint)) { @@ -72,6 +74,8 @@ const play = (ctx: DrawingContext, command: Command) => { saveCTM(ctx, command.props); } else if (isCommand(command, CommandType.RestoreCTM)) { ctx.canvas.restore(); + } else if (isBoxCommand(command)) { + drawBox(ctx, command); } else if (isCommand(command, CommandType.DrawPaint)) { ctx.canvas.drawPaint(ctx.paint); } else if (isDrawCommand(command, CommandType.DrawImage)) { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index f1d8440f0c..9ebdff9633 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -24,6 +24,8 @@ import type { ImageSVGProps, ParagraphProps, AtlasProps, + BoxProps, + BoxShadowProps, } from "../../dom/types"; import type { AnimatedProps } from "../../renderer"; import { isSharedValue } from "../nodes/utils"; @@ -124,6 +126,34 @@ export class Recorder { this.add({ type: CommandType.SaveBackdropFilter }); } + drawBox( + boxProps: AnimatedProps, + shadows: { + props: BoxShadowProps; + animatedProps?: Record>; + }[] + ) { + shadows.forEach((shadow) => { + if (shadow.props) { + const props = shadow.props as unknown as Record; + const animatedProps: Record> = {}; + let hasAnimatedProps = false; + for (const key in shadow.props) { + const prop = props[key]; + if (isSharedValue(prop)) { + props[key] = prop.value; + animatedProps[key] = prop; + hasAnimatedProps = true; + } + } + if (hasAnimatedProps) { + shadow.animatedProps = animatedProps; + } + } + }); + this.add({ type: CommandType.DrawBox, props: boxProps, shadows }); + } + drawImage(props: AnimatedProps) { this.add({ type: CommandType.DrawImage, props }); } diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 59e3043cf5..8bc69bf1e1 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -1,5 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { CTMProps, DrawingNodeProps, PaintProps } from "../../dom/types"; +import type { + CTMProps, + DrawingNodeProps, + PaintProps, + BoxShadowProps, +} from "../../dom/types"; import { NodeType } from "../../dom/types"; import type { Node } from "../nodes"; import { isImageFilter, isShader, sortNodeChildren } from "../nodes"; @@ -192,6 +197,13 @@ const visitNode = (recorder: Recorder, node: Node) => { recorder.saveCTM(ctm); } switch (node.type) { + case NodeType.Box: + const shadows = node.children + .filter((n) => n.type === NodeType.BoxShadow) + // eslint-disable-next-line @typescript-eslint/no-shadow + .map(({ props }) => ({ props } as { props: BoxShadowProps })); + recorder.drawBox(props, shadows); + break; case NodeType.Fill: recorder.drawPaint(); break; diff --git a/packages/skia/src/sksg/Recorder/commands/Box.ts b/packages/skia/src/sksg/Recorder/commands/Box.ts new file mode 100644 index 0000000000..57fd686cf2 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/Box.ts @@ -0,0 +1,61 @@ +import { deflate, inflate } from "../../../dom/nodes"; +import type { BoxProps, BoxShadowProps } from "../../../dom/types"; +import { BlurStyle, ClipOp, isRRect } from "../../../skia/types"; +import type { Command } from "../Core"; +import { CommandType, materializeProps } from "../Core"; +import type { DrawingContext } from "../DrawingContext"; + +interface BoxCommand extends Command { + props: BoxProps; + shadows: { props: BoxShadowProps }[]; +} + +export const isBoxCommand = (command: Command): command is BoxCommand => { + return command.type === CommandType.DrawBox; +}; + +export const drawBox = (ctx: DrawingContext, command: BoxCommand) => { + command.shadows.forEach((shadow) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + materializeProps(shadow as any); + }); + const shadows = command.shadows.map((shadow) => shadow.props); + const { paint, Skia, canvas } = ctx; + const { box: defaultBox } = command.props; + const opacity = paint.getAlphaf(); + const box = isRRect(defaultBox) ? defaultBox : Skia.RRectXY(defaultBox, 0, 0); + shadows + .filter((shadow) => !shadow.inner) + .map((shadow) => { + const { color = "black", blur, spread = 0, dx = 0, dy = 0 } = shadow; + const lPaint = Skia.Paint(); + lPaint.setColor(Skia.Color(color)); + lPaint.setAlphaf(paint.getAlphaf() * opacity); + lPaint.setMaskFilter( + Skia.MaskFilter.MakeBlur(BlurStyle.Normal, blur, true) + ); + canvas.drawRRect(inflate(Skia, box, spread, spread, dx, dy), lPaint); + }); + + canvas.drawRRect(box, paint); + + shadows + .filter((shadow) => shadow.inner) + .map((shadow) => { + const { color = "black", blur, spread = 0, dx = 0, dy = 0 } = shadow; + const delta = Skia.Point(10 + Math.abs(dx), 10 + Math.abs(dy)); + canvas.save(); + canvas.clipRRect(box, ClipOp.Intersect, false); + const lPaint = Skia.Paint(); + lPaint.setColor(Skia.Color(color)); + lPaint.setAlphaf(paint.getAlphaf() * opacity); + + lPaint.setMaskFilter( + Skia.MaskFilter.MakeBlur(BlurStyle.Normal, blur, true) + ); + const inner = deflate(Skia, box, spread, spread, dx, dy); + const outer = inflate(Skia, box, delta.x, delta.y); + canvas.drawDRRect(outer, inner, lPaint); + canvas.restore(); + }); +}; From 763b1e98e51f12036e7665a9d79d7ca20c346e19 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 10:02:01 +0100 Subject: [PATCH 28/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 1 + packages/skia/src/sksg/Recorder/Player.ts | 3 +++ packages/skia/src/sksg/Recorder/Visitor.ts | 5 ++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 9b247c299e..269744cb83 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -40,6 +40,7 @@ export enum CommandType { ComposeImageFilter = "ComposeImageFilter", MaterializePaint = "MaterializePaint", SaveBackdropFilter = "SaveBackdropFilter", + SaveLayer = "SaveLayer", // Drawing DrawBox = "DrawBox", DrawImage = "DrawImage", diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 0c3d25e4cf..80fc717f99 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -51,6 +51,9 @@ const play = (ctx: DrawingContext, command: Command) => { materializeProps(command as any); if (isCommand(command, CommandType.SaveBackdropFilter)) { ctx.saveBackdropFilter(); + } else if (isCommand(command, CommandType.SaveLayer)) { + const paint = ctx.Skia.Paint(); + ctx.canvas.saveLayer(paint); } else if (isDrawCommand(command, CommandType.SavePaint)) { ctx.savePaint(); setPaintProperties(ctx.Skia, ctx.paint, command.props); diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 8bc69bf1e1..661b56d2c0 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -188,11 +188,14 @@ const visitNode = (recorder: Recorder, node: Node) => { // For mixed nodes like BackdropFilters we don't materialize the paint if (node.type === NodeType.BackdropFilter) { recorder.saveBackdropFilter(); + } else if (node.type === NodeType.Layer) { + recorder.saveLayer(); } else { recorder.materializePaint(); } } const ctm = processCTM(props); + const shouldRestore = !!ctm || node.type === NodeType.Layer; if (ctm) { recorder.saveCTM(ctm); } @@ -271,7 +274,7 @@ const visitNode = (recorder: Recorder, node: Node) => { if (shouldPushPaint) { recorder.restorePaint(); } - if (ctm) { + if (shouldRestore) { recorder.restoreCTM(); } }; From dd2e82d66c8b4c744546b207b87ad1605e51de51 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 10:08:00 +0100 Subject: [PATCH 29/50] :wrench: --- packages/skia/src/sksg/Recorder/Recorder.ts | 57 ++++++++++++--------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 9ebdff9633..8087e4db5f 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -37,20 +37,31 @@ import type { Command } from "./Core"; export class Recorder { commands: Command[] = []; + private processProps(props: Record) { + const animatedProps: Record> = {}; + let hasAnimatedProps = false; + + for (const key in props) { + const prop = props[key]; + if (isSharedValue(prop)) { + props[key] = prop.value; + animatedProps[key] = prop; + hasAnimatedProps = true; + } + } + + return { + props, + animatedProps: hasAnimatedProps ? animatedProps : undefined, + }; + } + private add(command: Command) { if (command.props) { - const props = command.props as Record; - const animatedProps: Record> = {}; - let hasAnimatedProps = false; - for (const key in command.props) { - const prop = props[key]; - if (isSharedValue(prop)) { - props[key] = prop.value; - animatedProps[key] = prop; - hasAnimatedProps = true; - } - } - if (hasAnimatedProps) { + const { animatedProps } = this.processProps( + command.props as Record + ); + if (animatedProps) { command.animatedProps = animatedProps; } } @@ -122,6 +133,10 @@ export class Recorder { this.add({ type: CommandType.DrawPaint }); } + saveLayer() { + this.add({ type: CommandType.SaveLayer }); + } + saveBackdropFilter() { this.add({ type: CommandType.SaveBackdropFilter }); } @@ -135,20 +150,14 @@ export class Recorder { ) { shadows.forEach((shadow) => { if (shadow.props) { - const props = shadow.props as unknown as Record; - const animatedProps: Record> = {}; - let hasAnimatedProps = false; - for (const key in shadow.props) { - const prop = props[key]; - if (isSharedValue(prop)) { - props[key] = prop.value; - animatedProps[key] = prop; - hasAnimatedProps = true; + if (shadow.props) { + const { animatedProps } = this.processProps( + shadow.props as unknown as Record + ); + if (animatedProps) { + shadow.animatedProps = animatedProps; } } - if (hasAnimatedProps) { - shadow.animatedProps = animatedProps; - } } }); this.add({ type: CommandType.DrawBox, props: boxProps, shadows }); From 27d4c15e46de1e0b5983e92676f6a1f9daff2fa3 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 10:58:35 +0100 Subject: [PATCH 30/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 3 ++ .../skia/src/sksg/Recorder/DrawingContext.ts | 15 ++++++++- packages/skia/src/sksg/Recorder/Player.ts | 17 +++++++++- packages/skia/src/sksg/Recorder/Recorder.ts | 21 +++++++++++- packages/skia/src/sksg/Recorder/Visitor.ts | 32 +++++++++++++++++++ packages/skia/src/sksg/nodes/Node.ts | 4 +++ 6 files changed, 89 insertions(+), 3 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 269744cb83..9f09a8a7c7 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -35,12 +35,15 @@ export enum CommandType { PushColorFilter = "PushColorFilter", PushBlurMaskFilter = "PushBlurMaskFilter", PushImageFilter = "PushImageFilter", + PushPathEffect = "PushPathEffect", PushShader = "PushShader", ComposeColorFilter = "ComposeColorFilter", ComposeImageFilter = "ComposeImageFilter", + ComposePathEffect = "ComposePathEffect", MaterializePaint = "MaterializePaint", SaveBackdropFilter = "SaveBackdropFilter", SaveLayer = "SaveLayer", + PushPaintDeclaration = "PushPaintDeclaration", // Drawing DrawBox = "DrawBox", DrawImage = "DrawImage", diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index 04fe624970..4b050f3991 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -7,6 +7,7 @@ import type { SkPaint, SkShader, SkImageFilter, + SkPathEffect, } from "../../skia/types"; export class DrawingContext { @@ -16,6 +17,8 @@ export class DrawingContext { colorFilters: SkColorFilter[] = []; shaders: SkShader[] = []; imageFilters: SkImageFilter[] = []; + pathEffects: SkPathEffect[] = []; + paintDeclarations: SkPaint[] = []; constructor(Skia: Skia, canvas: SkCanvas) { this.Skia = Skia; @@ -47,7 +50,7 @@ export class DrawingContext { } restorePaint() { - this.paints.pop(); + return this.paints.pop(); } materializePaint() { @@ -71,8 +74,18 @@ export class DrawingContext { ) ); } + // Path Effects + if (this.pathEffects.length > 0) { + this.paint.setPathEffect( + this.pathEffects.reduceRight((inner, outer) => + inner ? this.Skia.PathEffect.MakeCompose(outer, inner) : outer + ) + ); + } this.colorFilters = []; this.shaders = []; this.imageFilters = []; + this.pathEffects = []; + this.paintDeclarations = []; } } diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 80fc717f99..d9496eea46 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -36,6 +36,11 @@ import { composeImageFilters, } from "./commands/ImageFilters"; import { setPaintProperties } from "./commands/Paint"; +import { + composePathEffects, + isPushPathEffect, + pushPathEffect, +} from "./commands/PathEffects"; import { isPushShader, pushShader } from "./commands/Shaders"; import { CommandType, @@ -52,7 +57,7 @@ const play = (ctx: DrawingContext, command: Command) => { if (isCommand(command, CommandType.SaveBackdropFilter)) { ctx.saveBackdropFilter(); } else if (isCommand(command, CommandType.SaveLayer)) { - const paint = ctx.Skia.Paint(); + const paint = ctx.paintDeclarations.pop(); ctx.canvas.saveLayer(paint); } else if (isDrawCommand(command, CommandType.SavePaint)) { ctx.savePaint(); @@ -61,6 +66,12 @@ const play = (ctx: DrawingContext, command: Command) => { ctx.restorePaint(); } else if (isCommand(command, CommandType.ComposeColorFilter)) { composeColorFilters(ctx); + } else if (isCommand(command, CommandType.PushPaintDeclaration)) { + const paint = ctx.restorePaint(); + if (!paint) { + throw new Error("No paint declaration to push"); + } + ctx.paintDeclarations.push(paint); } else if (isCommand(command, CommandType.MaterializePaint)) { ctx.materializePaint(); } else if (isPushColorFilter(command)) { @@ -69,6 +80,10 @@ const play = (ctx: DrawingContext, command: Command) => { pushShader(ctx, command); } else if (isPushImageFilter(command)) { pushImageFilter(ctx, command); + } else if (isPushPathEffect(command)) { + pushPathEffect(ctx, command); + } else if (isCommand(command, CommandType.ComposePathEffect)) { + composePathEffects(ctx); } else if (isCommand(command, CommandType.ComposeImageFilter)) { composeImageFilters(ctx); } else if (isDrawCommand(command, CommandType.PushBlurMaskFilter)) { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 8087e4db5f..63719aedc4 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -29,7 +29,7 @@ import type { } from "../../dom/types"; import type { AnimatedProps } from "../../renderer"; import { isSharedValue } from "../nodes/utils"; -import { isColorFilter, isImageFilter, isShader } from "../nodes"; +import { isColorFilter, isImageFilter, isPathEffect, isShader } from "../nodes"; import { CommandType } from "./Core"; import type { Command } from "./Core"; @@ -76,10 +76,25 @@ export class Recorder { this.add({ type: CommandType.RestorePaint }); } + pushPaintDeclaration() { + this.add({ type: CommandType.PushPaintDeclaration }); + } + materializePaint() { this.add({ type: CommandType.MaterializePaint }); } + pushPathEffect(pathEffectType: NodeType, props: AnimatedProps) { + if (!isPathEffect(pathEffectType)) { + throw new Error("Invalid color filter type: " + pathEffectType); + } + this.add({ + type: CommandType.PushPathEffect, + pathEffectType, + props, + }); + } + pushImageFilter(imageFilterType: NodeType, props: AnimatedProps) { if (!isImageFilter(imageFilterType)) { throw new Error("Invalid color filter type: " + imageFilterType); @@ -113,6 +128,10 @@ export class Recorder { this.add({ type: CommandType.PushBlurMaskFilter, props }); } + composePathEffect() { + this.add({ type: CommandType.ComposePathEffect }); + } + composeColorFilter() { this.add({ type: CommandType.ComposeColorFilter }); } diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 661b56d2c0..369f9a5629 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -127,6 +127,21 @@ const pushColorFilters = (recorder: Recorder, colorFilters: Node[]) => { }); }; +const pushPathEffects = (recorder: Recorder, pathEffects: Node[]) => { + pathEffects.forEach((pathEffect) => { + if (pathEffect.children.length > 0) { + pushPathEffects(recorder, pathEffect.children); + } + recorder.pushPathEffect(pathEffect.type, pathEffect.props); + const needsComposition = + pathEffect.type !== NodeType.SumPathEffect && + pathEffect.children.length > 0; + if (needsComposition) { + recorder.composePathEffect(); + } + }); +}; + const pushImageFilters = (recorder: Recorder, imageFilters: Node[]) => { imageFilters.forEach((imageFilter) => { if (imageFilter.children.length > 0) { @@ -161,6 +176,20 @@ const pushMaskFilters = (recorder: Recorder, maskFilters: Node[]) => { } }; +const pushPaints = (recorder: Recorder, paints: Node[]) => { + paints.forEach((paint) => { + recorder.savePaint(paint.props); + const { colorFilters, maskFilters, shaders, imageFilters, pathEffects } = + sortNodeChildren(paint); + pushColorFilters(recorder, colorFilters); + pushImageFilters(recorder, imageFilters); + pushMaskFilters(recorder, maskFilters); + pushShaders(recorder, shaders); + pushPathEffects(recorder, pathEffects); + recorder.pushPaintDeclaration(); + }); +}; + const visitNode = (recorder: Recorder, node: Node) => { const { props } = node; const { @@ -170,6 +199,7 @@ const visitNode = (recorder: Recorder, node: Node) => { shaders, imageFilters, pathEffects, + paints, } = sortNodeChildren(node); const paint = processPaint(props); const shouldPushPaint = @@ -185,6 +215,8 @@ const visitNode = (recorder: Recorder, node: Node) => { pushImageFilters(recorder, imageFilters); pushMaskFilters(recorder, maskFilters); pushShaders(recorder, shaders); + pushPaints(recorder, paints); + pushPathEffects(recorder, pathEffects); // For mixed nodes like BackdropFilters we don't materialize the paint if (node.type === NodeType.BackdropFilter) { recorder.saveBackdropFilter(); diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index 0a92082137..47f6811e2c 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -85,6 +85,7 @@ export const sortNodeChildren = (parent: Node) => { const imageFilters: Node[] = []; const pathEffects: Node[] = []; const drawings: Node[] = []; + const paints: Node[] = []; const declarations: Node[] = []; parent.children.forEach((node) => { if (isColorFilter(node.type)) { @@ -99,6 +100,8 @@ export const sortNodeChildren = (parent: Node) => { shaders.push(node); } else if (node.isDeclaration) { declarations.push(node); + } else if (node.type === NodeType.Paint) { + paints.push(node); } else { drawings.push(node); } @@ -111,5 +114,6 @@ export const sortNodeChildren = (parent: Node) => { shaders, pathEffects, imageFilters, + paints, }; }; From cff210c82eb9973487b086f4fbaae03c9aaaa92f Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 11:04:38 +0100 Subject: [PATCH 31/50] :wrench: --- packages/skia/src/sksg/Recorder/Player.ts | 93 +++++----- .../src/sksg/Recorder/commands/PathEffects.ts | 165 ++++++++++++++++++ 2 files changed, 215 insertions(+), 43 deletions(-) create mode 100644 packages/skia/src/sksg/Recorder/commands/PathEffects.ts diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index d9496eea46..493930a04e 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -92,50 +92,57 @@ const play = (ctx: DrawingContext, command: Command) => { saveCTM(ctx, command.props); } else if (isCommand(command, CommandType.RestoreCTM)) { ctx.canvas.restore(); - } else if (isBoxCommand(command)) { - drawBox(ctx, command); - } else if (isCommand(command, CommandType.DrawPaint)) { - ctx.canvas.drawPaint(ctx.paint); - } else if (isDrawCommand(command, CommandType.DrawImage)) { - drawImage(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawCircle)) { - drawCircle(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawPoints)) { - drawPoints(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawPath)) { - drawPath(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawRect)) { - drawRect(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawRRect)) { - drawRRect(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawOval)) { - drawOval(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawLine)) { - drawLine(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawPatch)) { - drawPatch(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawVertices)) { - drawVertices(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawDiffRect)) { - drawDiffRect(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawText)) { - drawText(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawTextPath)) { - drawTextPath(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawTextBlob)) { - drawTextBlob(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawGlyphs)) { - drawGlyphs(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawPicture)) { - drawPicture(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawImageSVG)) { - drawImageSVG(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawParagraph)) { - drawParagraph(ctx, command.props); - } else if (isDrawCommand(command, CommandType.DrawAtlas)) { - drawAtlas(ctx, command.props); } else { - console.warn(`Unknown command: ${command.type}`); + const paints = [ctx.paint, ...ctx.paintDeclarations]; + paints.forEach((p) => { + ctx.paints.push(p); + if (isBoxCommand(command)) { + drawBox(ctx, command); + } else if (isCommand(command, CommandType.DrawPaint)) { + ctx.canvas.drawPaint(ctx.paint); + } else if (isDrawCommand(command, CommandType.DrawImage)) { + drawImage(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawCircle)) { + drawCircle(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPoints)) { + drawPoints(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPath)) { + drawPath(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawRect)) { + drawRect(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawRRect)) { + drawRRect(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawOval)) { + drawOval(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawLine)) { + drawLine(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPatch)) { + drawPatch(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawVertices)) { + drawVertices(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawDiffRect)) { + drawDiffRect(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawText)) { + drawText(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawTextPath)) { + drawTextPath(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawTextBlob)) { + drawTextBlob(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawGlyphs)) { + drawGlyphs(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawPicture)) { + drawPicture(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawImageSVG)) { + drawImageSVG(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawParagraph)) { + drawParagraph(ctx, command.props); + } else if (isDrawCommand(command, CommandType.DrawAtlas)) { + drawAtlas(ctx, command.props); + } else { + console.warn(`Unknown command: ${command.type}`); + } + ctx.paints.pop(); + }); } }; diff --git a/packages/skia/src/sksg/Recorder/commands/PathEffects.ts b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts new file mode 100644 index 0000000000..5e43241827 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts @@ -0,0 +1,165 @@ +import { enumKey, processPath } from "../../../dom/nodes"; +import { NodeType } from "../../../dom/types"; +import type { + CornerPathEffectProps, + DashPathEffectProps, + DiscretePathEffectProps, + Line2DPathEffectProps, + Path1DPathEffectProps, + Path2DPathEffectProps, +} from "../../../dom/types"; +import { Path1DEffectStyle } from "../../../skia/types"; +import { composeDeclarations } from "../../DeclarationContext"; +import type { Command } from "../Core"; +import { CommandType } from "../Core"; +import type { DrawingContext } from "../DrawingContext"; + +export const declareDiscretePathEffect = ( + ctx: DrawingContext, + props: DiscretePathEffectProps +) => { + "worklet"; + const { length, deviation, seed } = props; + const pe = ctx.Skia.PathEffect.MakeDiscrete(length, deviation, seed); + ctx.pathEffects.push(pe); +}; + +export const declarePath2DPathEffect = ( + ctx: DrawingContext, + props: Path2DPathEffectProps +) => { + "worklet"; + const { matrix } = props; + const path = processPath(ctx.Skia, props.path); + const pe = ctx.Skia.PathEffect.MakePath2D(matrix, path); + if (pe === null) { + throw new Error("Path2DPathEffect: invalid path"); + } + ctx.pathEffects.push(pe); +}; + +export const declareDashPathEffect = ( + ctx: DrawingContext, + props: DashPathEffectProps +) => { + "worklet"; + const { intervals, phase } = props; + const pe = ctx.Skia.PathEffect.MakeDash(intervals, phase); + ctx.pathEffects.push(pe); +}; + +export const declareCornerPathEffect = ( + ctx: DrawingContext, + props: CornerPathEffectProps +) => { + "worklet"; + const { r } = props; + const pe = ctx.Skia.PathEffect.MakeCorner(r); + if (pe === null) { + throw new Error("CornerPathEffect: couldn't create path effect"); + } + return pe; +}; + +export const declareSumPathEffect = (ctx: DrawingContext) => { + "worklet"; + // Note: decorateChildren functionality needs to be handled differently + const pes = ctx.pathEffects.splice(0, ctx.pathEffects.length); + const pe = composeDeclarations( + pes, + ctx.Skia.PathEffect.MakeSum.bind(ctx.Skia.PathEffect) + ); + ctx.pathEffects.push(pe); +}; + +export const declareLine2DPathEffect = ( + ctx: DrawingContext, + props: Line2DPathEffectProps +) => { + "worklet"; + const { width, matrix } = props; + const pe = ctx.Skia.PathEffect.MakeLine2D(width, matrix); + if (pe === null) { + throw new Error("Line2DPathEffect: could not create path effect"); + } + return pe; +}; + +export const declarePath1DPathEffect = ( + ctx: DrawingContext, + props: Path1DPathEffectProps +) => { + "worklet"; + const { advance, phase, style } = props; + const path = processPath(ctx.Skia, props.path); + const pe = ctx.Skia.PathEffect.MakePath1D( + path, + advance, + phase, + Path1DEffectStyle[enumKey(style)] + ); + if (pe === null) { + throw new Error("Path1DPathEffect: could not create path effect"); + } + return pe; +}; + +export const isPushPathEffect = ( + command: Command +): command is Command => { + return command.type === CommandType.PushPathEffect; +}; + +type Props = { + [NodeType.DiscretePathEffect]: DiscretePathEffectProps; + [NodeType.DashPathEffect]: DashPathEffectProps; + [NodeType.Path1DPathEffect]: Path1DPathEffectProps; + [NodeType.Path2DPathEffect]: Path2DPathEffectProps; + [NodeType.CornerPathEffect]: CornerPathEffectProps; + [NodeType.SumPathEffect]: Record; + [NodeType.Line2DPathEffect]: Line2DPathEffectProps; +}; + +interface PushPathEffect + extends Command { + pathEffectType: T; + props: Props[T]; +} + +const isPathEffect = ( + command: Command, + type: T +): command is PushPathEffect => { + return command.pathEffectType === type; +}; + +export const composePathEffects = (ctx: DrawingContext) => { + if (ctx.pathEffects.length > 1) { + const outer = ctx.pathEffects.pop()!; + const inner = ctx.pathEffects.pop()!; + ctx.pathEffects.push(ctx.Skia.PathEffect.MakeCompose(outer, inner)); + } +}; + +export const pushPathEffect = ( + ctx: DrawingContext, + command: Command +) => { + if (isPathEffect(command, NodeType.DiscretePathEffect)) { + declareDiscretePathEffect(ctx, command.props); + } else if (isPathEffect(command, NodeType.DashPathEffect)) { + declareDashPathEffect(ctx, command.props); + } else if (isPathEffect(command, NodeType.Path1DPathEffect)) { + declarePath1DPathEffect(ctx, command.props); + } else if (isPathEffect(command, NodeType.Path2DPathEffect)) { + declarePath2DPathEffect(ctx, command.props); + } else if (isPathEffect(command, NodeType.CornerPathEffect)) { + declareCornerPathEffect(ctx, command.props); + } else if (isPathEffect(command, NodeType.SumPathEffect)) { + declareSumPathEffect(ctx); + } else if (isPathEffect(command, NodeType.Line2DPathEffect)) { + declareLine2DPathEffect(ctx, command.props); + } else { + throw new Error("Invalid image filter type: " + command.imageFilterType); + } +}; From 7f783102da36c386f60059fe23a9cf340704bd1a Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 11:17:11 +0100 Subject: [PATCH 32/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 4 ++-- packages/skia/src/sksg/Recorder/Player.ts | 8 ++++++-- packages/skia/src/sksg/Recorder/Visitor.ts | 11 ++++++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 9f09a8a7c7..01abd64ae2 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -5,7 +5,6 @@ import type { CircleProps, CTMProps, ImageProps, - PaintProps, PointsProps, PathProps, RectProps, @@ -23,6 +22,7 @@ import type { ImageSVGProps, ParagraphProps, AtlasProps, + DrawingNodeProps, } from "../../dom/types"; // TODO: remove string labels @@ -95,7 +95,7 @@ interface Props { [CommandType.DrawImage]: ImageProps; [CommandType.DrawCircle]: CircleProps; [CommandType.SaveCTM]: CTMProps; - [CommandType.SavePaint]: PaintProps; + [CommandType.SavePaint]: DrawingNodeProps; [CommandType.PushBlurMaskFilter]: BlurMaskFilterProps; [CommandType.DrawPoints]: PointsProps; [CommandType.DrawPath]: PathProps; diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 493930a04e..f7712cdbd6 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -60,8 +60,12 @@ const play = (ctx: DrawingContext, command: Command) => { const paint = ctx.paintDeclarations.pop(); ctx.canvas.saveLayer(paint); } else if (isDrawCommand(command, CommandType.SavePaint)) { - ctx.savePaint(); - setPaintProperties(ctx.Skia, ctx.paint, command.props); + if (command.props.paint) { + ctx.paints.push(command.props.paint); + } else { + ctx.savePaint(); + setPaintProperties(ctx.Skia, ctx.paint, command.props); + } } else if (isCommand(command, CommandType.RestorePaint)) { ctx.restorePaint(); } else if (isCommand(command, CommandType.ComposeColorFilter)) { diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 369f9a5629..2a472cd9cf 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -2,7 +2,6 @@ import type { CTMProps, DrawingNodeProps, - PaintProps, BoxShadowProps, } from "../../dom/types"; import { NodeType } from "../../dom/types"; @@ -22,8 +21,9 @@ export const processPaint = ({ strokeMiter, antiAlias, dither, + paint: paintRef, }: DrawingNodeProps) => { - const paint: PaintProps = {}; + const paint: DrawingNodeProps = {}; if (opacity) { paint.opacity = opacity; } @@ -55,6 +55,10 @@ export const processPaint = ({ paint.dither = dither; } + if (paintRef) { + paint.paint = paintRef; + } + if ( opacity !== undefined || color !== undefined || @@ -65,7 +69,8 @@ export const processPaint = ({ strokeCap !== undefined || strokeMiter !== undefined || antiAlias !== undefined || - dither !== undefined + dither !== undefined || + paintRef !== undefined ) { return paint; } From 8db771accb2cbb89e8d9bf19c5c389634c7a9711 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 11:44:15 +0100 Subject: [PATCH 33/50] :wrench: --- packages/skia/src/renderer/Canvas.tsx | 158 +++++++++--------- packages/skia/src/sksg/Container.ts | 23 ++- packages/skia/src/sksg/Recorder/Core.ts | 2 + .../skia/src/sksg/Recorder/commands/Box.ts | 2 + .../sksg/Recorder/commands/ImageFilters.ts | 24 ++- .../src/sksg/Recorder/commands/PathEffects.ts | 16 +- .../src/sksg/Recorder/commands/Shaders.ts | 2 + 7 files changed, 126 insertions(+), 101 deletions(-) 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/Container.ts b/packages/skia/src/sksg/Container.ts index 896f160722..efcc227213 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 type { Command } from "./Recorder/Core"; import { Recorder } from "./Recorder/Recorder"; import { visit } from "./Recorder/Visitor"; @@ -21,17 +20,17 @@ import { DrawingContext } from "./Recorder/DrawingContext"; const drawOnscreen = ( Skia: Skia, nativeId: number, - root: Node[], - staticCtx: StaticContext + recording: Command[], + _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 ctx = new DrawingContext(Skia, canvas); + //console.log(this._recording); + replay(ctx, recording); const picture = rec.finishRecordingAsPicture(); const end = performance.now(); console.log("Recording time: ", end - start); @@ -62,10 +61,10 @@ export class Container { if (this.mapperId !== null) { Rea.stopMapper(this.mapperId); } - const { nativeId, Skia, _staticCtx } = this; + const { nativeId, Skia, _staticCtx, _recording } = this; this.mapperId = Rea.startMapper(() => { "worklet"; - drawOnscreen(Skia, nativeId, root, _staticCtx!); + drawOnscreen(Skia, nativeId, _recording!, _staticCtx!); }, Array.from(this.values)); } this._root = root; @@ -85,9 +84,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, _recording, _staticCtx } = this; Rea.runOnUI(() => { - drawOnscreen(Skia, nativeId, root, _staticCtx!); + drawOnscreen(Skia, nativeId, _recording!, _staticCtx!); })(); } } diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 01abd64ae2..8428645dce 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -1,3 +1,5 @@ +"worklet"; + import type { SharedValue } from "react-native-reanimated"; import type { diff --git a/packages/skia/src/sksg/Recorder/commands/Box.ts b/packages/skia/src/sksg/Recorder/commands/Box.ts index 57fd686cf2..3611988271 100644 --- a/packages/skia/src/sksg/Recorder/commands/Box.ts +++ b/packages/skia/src/sksg/Recorder/commands/Box.ts @@ -1,3 +1,5 @@ +"worklet"; + import { deflate, inflate } from "../../../dom/nodes"; import type { BoxProps, BoxShadowProps } from "../../../dom/types"; import { BlurStyle, ClipOp, isRRect } from "../../../skia/types"; diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts index 4fed49a256..5caaba908b 100644 --- a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -17,6 +17,7 @@ import { BlendMode, BlurStyle, ColorChannel, + processUniforms, TileMode, } from "../../../skia/types"; import type { Command } from "../Core"; @@ -97,7 +98,7 @@ const declareMorphologyImageFilter = ( ctx.imageFilters.push(imgf); }; -export const declareOffsetImageFilter = ( +const declareOffsetImageFilter = ( ctx: DrawingContext, props: OffsetImageFilterProps ) => { @@ -107,7 +108,7 @@ export const declareOffsetImageFilter = ( ctx.imageFilters.push(imgf); }; -export const declareDropShadowImageFilter = ( +const declareDropShadowImageFilter = ( ctx: DrawingContext, props: DropShadowImageFilterProps ) => { @@ -126,7 +127,7 @@ export const declareDropShadowImageFilter = ( ctx.imageFilters.push(imgf); }; -export const declareBlendImageFilter = ( +const declareBlendImageFilter = ( ctx: DrawingContext, props: BlendImageFilterProps ) => { @@ -141,7 +142,7 @@ export const declareBlendImageFilter = ( ctx.imageFilters.push(imgf); }; -export const declareDisplacementMapImageFilter = ( +const declareDisplacementMapImageFilter = ( ctx: DrawingContext, props: DisplacementMapImageFilterProps ) => { @@ -162,6 +163,19 @@ export const declareDisplacementMapImageFilter = ( ctx.imageFilters.push(imgf); }; +const declareRuntimeShaderImageFilter = ( + ctx: DrawingContext, + props: RuntimeShaderImageFilterProps +) => { + const { source, uniforms } = props; + const rtb = ctx.Skia.RuntimeShaderBuilder(source); + if (uniforms) { + processUniforms(source, uniforms, rtb); + } + const imgf = ctx.Skia.ImageFilter.MakeRuntimeShader(rtb, null, null); + ctx.imageFilters.push(imgf); +}; + export const composeImageFilters = (ctx: DrawingContext) => { if (ctx.imageFilters.length > 1) { const outer = ctx.imageFilters.pop()!; @@ -229,6 +243,8 @@ export const pushImageFilter = ( declareDropShadowImageFilter(ctx, command.props); } else if (isImageFilter(command, NodeType.OffsetImageFilter)) { declareOffsetImageFilter(ctx, command.props); + } else if (isImageFilter(command, NodeType.RuntimeShaderImageFilter)) { + declareRuntimeShaderImageFilter(ctx, command.props); } else { throw new Error("Invalid image filter type: " + command.imageFilterType); } diff --git a/packages/skia/src/sksg/Recorder/commands/PathEffects.ts b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts index 5e43241827..841e59ee0b 100644 --- a/packages/skia/src/sksg/Recorder/commands/PathEffects.ts +++ b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts @@ -1,3 +1,5 @@ +"worklet"; + import { enumKey, processPath } from "../../../dom/nodes"; import { NodeType } from "../../../dom/types"; import type { @@ -14,7 +16,7 @@ import type { Command } from "../Core"; import { CommandType } from "../Core"; import type { DrawingContext } from "../DrawingContext"; -export const declareDiscretePathEffect = ( +const declareDiscretePathEffect = ( ctx: DrawingContext, props: DiscretePathEffectProps ) => { @@ -24,7 +26,7 @@ export const declareDiscretePathEffect = ( ctx.pathEffects.push(pe); }; -export const declarePath2DPathEffect = ( +const declarePath2DPathEffect = ( ctx: DrawingContext, props: Path2DPathEffectProps ) => { @@ -38,7 +40,7 @@ export const declarePath2DPathEffect = ( ctx.pathEffects.push(pe); }; -export const declareDashPathEffect = ( +const declareDashPathEffect = ( ctx: DrawingContext, props: DashPathEffectProps ) => { @@ -48,7 +50,7 @@ export const declareDashPathEffect = ( ctx.pathEffects.push(pe); }; -export const declareCornerPathEffect = ( +const declareCornerPathEffect = ( ctx: DrawingContext, props: CornerPathEffectProps ) => { @@ -61,7 +63,7 @@ export const declareCornerPathEffect = ( return pe; }; -export const declareSumPathEffect = (ctx: DrawingContext) => { +const declareSumPathEffect = (ctx: DrawingContext) => { "worklet"; // Note: decorateChildren functionality needs to be handled differently const pes = ctx.pathEffects.splice(0, ctx.pathEffects.length); @@ -72,7 +74,7 @@ export const declareSumPathEffect = (ctx: DrawingContext) => { ctx.pathEffects.push(pe); }; -export const declareLine2DPathEffect = ( +const declareLine2DPathEffect = ( ctx: DrawingContext, props: Line2DPathEffectProps ) => { @@ -85,7 +87,7 @@ export const declareLine2DPathEffect = ( return pe; }; -export const declarePath1DPathEffect = ( +const declarePath1DPathEffect = ( ctx: DrawingContext, props: Path1DPathEffectProps ) => { diff --git a/packages/skia/src/sksg/Recorder/commands/Shaders.ts b/packages/skia/src/sksg/Recorder/commands/Shaders.ts index 0b9a801630..329c35631c 100644 --- a/packages/skia/src/sksg/Recorder/commands/Shaders.ts +++ b/packages/skia/src/sksg/Recorder/commands/Shaders.ts @@ -1,3 +1,5 @@ +"worklet"; + import { enumKey, fitRects, From d4b0be0da2801eccab6ab574979d78bb8bbd242b Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 14:38:31 +0100 Subject: [PATCH 34/50] :wrench: --- .../snapshots/image-filter/test-shadow.png | Bin 69233 -> 79807 bytes .../__tests__/e2e/ImageFilters.spec.tsx | 90 +++++++++++++++++- .../src/sksg/Recorder/commands/PathEffects.ts | 6 +- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/packages/skia/src/__tests__/snapshots/image-filter/test-shadow.png b/packages/skia/src/__tests__/snapshots/image-filter/test-shadow.png index fcc79901bbdcc288b69c166eb3c84765e8e754f0..2251d9229892afeb2d78f56079dbf22ccbda6c65 100644 GIT binary patch literal 79807 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

P-nJex}^k>+@}}pV*A+pQ0Z43Je@) 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

L^H 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 a7b>^i3!i+<}{WNv} literal 69233 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%0Aq9Xw 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 z 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 a9LRz=`Ufdgahd;SR!&M$vQon6%l`q# { + "worklet"; + const sourceGraphic = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(Black, BlendMode.Dst), + null + ); + const sourceAlpha = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(Black, BlendMode.SrcIn), + null + ); + const f1 = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeBlend(color, BlendMode.SrcOut), + null + ); + const f2 = Skia.ImageFilter.MakeOffset(dx, dy, f1); + const f3 = Skia.ImageFilter.MakeBlur(sigmaX, sigmaY, TileMode.Decal, f2); + const f4 = Skia.ImageFilter.MakeBlend(BlendMode.SrcIn, sourceAlpha, f3); + if (shadowOnly) { + return f4; + } + return Skia.ImageFilter.MakeCompose( + input, + Skia.ImageFilter.MakeBlend(BlendMode.SrcOver, sourceGraphic, f4) + ); +}; describe("Test Image Filters", () => { itRunsNodeOnly( @@ -191,7 +243,41 @@ describe("Test Image Filters", () => { threshold: 0.05, }); }); - // TODO: build reference result here + it("should build reference result outer and inner shadows on text", () => { + const { surface: ckSurface, canvas, Skia } = setupSkia(wWidth, wHeight); + const path = Skia.Path.MakeFromSVGString( + // eslint-disable-next-line max-len + "M62.477 2.273V75h-8.522L14.324 17.898h-.71V75H4.807V2.273h8.522l39.773 57.244h.71V2.273h8.665ZM78.963 75V20.454h8.381V75h-8.38Zm4.262-63.636c-1.634 0-3.042-.557-4.226-1.67-1.16-1.112-1.74-2.45-1.74-4.012 0-1.563.58-2.9 1.74-4.013C80.183.556 81.59 0 83.225 0c1.633 0 3.03.556 4.19 1.669 1.184 1.113 1.776 2.45 1.776 4.013 0 1.562-.592 2.9-1.776 4.013-1.16 1.112-2.557 1.669-4.19 1.669ZM124.853 76.136c-5.114 0-9.517-1.207-13.21-3.622-3.693-2.415-6.534-5.74-8.523-9.978-1.989-4.238-2.983-9.08-2.983-14.525 0-5.54 1.018-10.428 3.054-14.666 2.06-4.261 4.924-7.587 8.594-9.979 3.693-2.414 8.002-3.622 12.926-3.622 3.835 0 7.292.71 10.369 2.131 3.078 1.42 5.599 3.41 7.564 5.966 1.965 2.557 3.184 5.54 3.658 8.949h-8.381c-.639-2.486-2.059-4.688-4.261-6.605-2.178-1.942-5.114-2.912-8.807-2.912-3.267 0-6.132.852-8.594 2.556-2.438 1.681-4.344 4.06-5.717 7.138-1.35 3.054-2.024 6.641-2.024 10.76 0 4.214.663 7.884 1.988 11.009 1.35 3.125 3.244 5.551 5.682 7.28 2.462 1.728 5.351 2.592 8.665 2.592 2.178 0 4.155-.379 5.93-1.136a12.23 12.23 0 0 0 4.51-3.267c1.231-1.42 2.107-3.125 2.628-5.114h8.381c-.474 3.22-1.646 6.12-3.516 8.7-1.846 2.557-4.297 4.593-7.351 6.108-3.03 1.492-6.557 2.237-10.582 2.237ZM181.423 76.136c-5.256 0-9.79-1.16-13.601-3.48-3.788-2.344-6.712-5.61-8.772-9.8-2.036-4.215-3.054-9.116-3.054-14.703s1.018-10.511 3.054-14.772c2.06-4.285 4.925-7.623 8.594-10.015 3.693-2.414 8.002-3.622 12.926-3.622 2.841 0 5.647.474 8.417 1.42 2.769.948 5.291 2.487 7.563 4.617 2.273 2.107 4.084 4.9 5.434 8.38 1.349 3.481 2.024 7.766 2.024 12.856v3.551h-42.046v-7.244h33.523c0-3.078-.615-5.824-1.847-8.239-1.207-2.415-2.935-4.32-5.184-5.717-2.226-1.397-4.853-2.095-7.884-2.095-3.338 0-6.226.828-8.664 2.486a16.35 16.35 0 0 0-5.576 6.392c-1.302 2.627-1.953 5.445-1.953 8.451v4.83c0 4.12.71 7.611 2.131 10.476 1.444 2.84 3.444 5.007 6.001 6.498 2.557 1.468 5.528 2.202 8.914 2.202 2.201 0 4.19-.308 5.965-.923 1.8-.64 3.35-1.587 4.652-2.841 1.303-1.279 2.309-2.865 3.019-4.759l8.097 2.273a17.965 17.965 0 0 1-4.297 7.244c-2.013 2.06-4.498 3.67-7.458 4.83-2.959 1.136-6.285 1.704-9.978 1.704ZM32.648 123.273h8.806v51.988c0 4.641-.852 8.582-2.556 11.826-1.705 3.243-4.108 5.705-7.21 7.386-3.1 1.681-6.758 2.521-10.972 2.521-3.977 0-7.517-.722-10.618-2.166-3.101-1.468-5.54-3.551-7.315-6.25-1.776-2.699-2.664-5.907-2.664-9.623h8.665c0 2.059.51 3.858 1.527 5.397 1.042 1.515 2.462 2.699 4.261 3.551 1.8.853 3.848 1.279 6.144 1.279 2.533 0 4.687-.533 6.463-1.598 1.776-1.066 3.125-2.628 4.048-4.688.947-2.083 1.42-4.628 1.42-7.635v-51.988ZM80.126 197.136c-4.924 0-9.244-1.172-12.961-3.515-3.693-2.344-6.582-5.623-8.665-9.837-2.06-4.214-3.09-9.138-3.09-14.773 0-5.681 1.03-10.641 3.09-14.879 2.083-4.238 4.972-7.528 8.665-9.872 3.717-2.344 8.037-3.516 12.961-3.516 4.925 0 9.233 1.172 12.927 3.516 3.716 2.344 6.605 5.634 8.664 9.872 2.084 4.238 3.125 9.198 3.125 14.879 0 5.635-1.041 10.559-3.125 14.773-2.06 4.214-4.948 7.493-8.664 9.837-3.694 2.343-8.002 3.515-12.927 3.515Zm0-7.528c3.741 0 6.819-.959 9.233-2.876 2.415-1.918 4.203-4.439 5.363-7.564 1.16-3.125 1.74-6.511 1.74-10.157 0-3.645-.58-7.043-1.74-10.191-1.16-3.149-2.948-5.694-5.363-7.635-2.414-1.942-5.492-2.912-9.233-2.912-3.74 0-6.818.97-9.233 2.912-2.414 1.941-4.202 4.486-5.362 7.635-1.16 3.148-1.74 6.546-1.74 10.191 0 3.646.58 7.032 1.74 10.157 1.16 3.125 2.948 5.646 5.362 7.564 2.415 1.917 5.493 2.876 9.233 2.876ZM118.772 196v-72.727h8.38v26.846h.711c.615-.947 1.467-2.154 2.556-3.622 1.113-1.491 2.699-2.817 4.759-3.977 2.083-1.184 4.9-1.776 8.452-1.776 4.592 0 8.641 1.149 12.145 3.445 3.503 2.296 6.238 5.552 8.203 9.766 1.965 4.214 2.947 9.185 2.947 14.914 0 5.777-.982 10.784-2.947 15.022-1.965 4.214-4.688 7.481-8.168 9.801-3.48 2.296-7.493 3.444-12.038 3.444-3.504 0-6.31-.58-8.417-1.74-2.107-1.183-3.728-2.521-4.865-4.012-1.136-1.516-2.012-2.77-2.627-3.765h-.995V196h-8.096Zm8.238-27.273c0 4.12.604 7.754 1.811 10.902 1.208 3.125 2.971 5.576 5.292 7.351 2.32 1.752 5.161 2.628 8.522 2.628 3.504 0 6.428-.923 8.772-2.77 2.367-1.87 4.143-4.38 5.326-7.528 1.208-3.173 1.811-6.7 1.811-10.583 0-3.835-.591-7.291-1.775-10.369-1.16-3.101-2.924-5.552-5.291-7.351-2.344-1.823-5.292-2.734-8.843-2.734-3.409 0-6.273.864-8.593 2.592-2.321 1.705-4.072 4.096-5.256 7.173-1.184 3.054-1.776 6.617-1.776 10.689ZM190.824 123.273l-.71 52.272h-8.239l-.71-52.272h9.659Zm-4.829 73.295c-1.752 0-3.256-.627-4.51-1.882-1.255-1.255-1.882-2.758-1.882-4.51 0-1.752.627-3.255 1.882-4.51 1.254-1.255 2.758-1.882 4.51-1.882 1.752 0 3.255.627 4.51 1.882 1.254 1.255 1.882 2.758 1.882 4.51a6.02 6.02 0 0 1-.888 3.196 6.634 6.634 0 0 1-2.308 2.344c-.947.568-2.013.852-3.196.852Z" + )!; + + const paint = Skia.Paint(); + paint.setColor(Skia.Color("#add8e6")); + canvas.drawPaint(paint); + paint.setColor(Skia.Color("red")); + const img1 = MakeInnerShadow( + Skia, + false, + 0, + 4, + 1, + 1, + Skia.Color("#00FF00"), + null + ); + const img2 = Skia.ImageFilter.MakeDropShadow( + 0, + 4, + 0, + 0, + Skia.Color("#0000ff"), + null + ); + paint.setImageFilter(Skia.ImageFilter.MakeCompose(img1, img2)); + canvas.scale(3, 3); + canvas.drawPath(path, paint); + canvas.restore(); + processResult(ckSurface, "snapshots/image-filter/test-shadow.png"); + }); it("should show outer and inner shadows on text", async () => { const path = // eslint-disable-next-line max-len diff --git a/packages/skia/src/sksg/Recorder/commands/PathEffects.ts b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts index 841e59ee0b..af77e58faa 100644 --- a/packages/skia/src/sksg/Recorder/commands/PathEffects.ts +++ b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts @@ -60,7 +60,7 @@ const declareCornerPathEffect = ( if (pe === null) { throw new Error("CornerPathEffect: couldn't create path effect"); } - return pe; + ctx.pathEffects.push(pe); }; const declareSumPathEffect = (ctx: DrawingContext) => { @@ -84,7 +84,7 @@ const declareLine2DPathEffect = ( if (pe === null) { throw new Error("Line2DPathEffect: could not create path effect"); } - return pe; + ctx.pathEffects.push(pe); }; const declarePath1DPathEffect = ( @@ -103,7 +103,7 @@ const declarePath1DPathEffect = ( if (pe === null) { throw new Error("Path1DPathEffect: could not create path effect"); } - return pe; + ctx.pathEffects.push(pe); }; export const isPushPathEffect = ( From 0202e2c3dd897f4967c5fecc6a6ed41bd25fbc39 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 15:00:09 +0100 Subject: [PATCH 35/50] :wrench: --- packages/skia/src/sksg/Container.ts | 2 +- packages/skia/src/sksg/Recorder/Core.ts | 2 +- packages/skia/src/sksg/Recorder/DrawingContext.ts | 1 - packages/skia/src/sksg/Recorder/Player.ts | 4 +++- packages/skia/src/sksg/Recorder/Recorder.ts | 4 ++-- packages/skia/src/sksg/Recorder/Visitor.ts | 2 +- packages/skia/src/sksg/nodes/Node.ts | 4 ++-- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index efcc227213..d8bebb478c 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -113,7 +113,7 @@ export class Container { drawOnCanvas(canvas: SkCanvas) { const ctx = new DrawingContext(this.Skia, canvas); - //console.log(this._recording); + console.log(this._recording); replay(ctx, this._recording!); } } diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 8428645dce..2596fb258e 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -45,7 +45,7 @@ export enum CommandType { MaterializePaint = "MaterializePaint", SaveBackdropFilter = "SaveBackdropFilter", SaveLayer = "SaveLayer", - PushPaintDeclaration = "PushPaintDeclaration", + RestorePaintDeclaration = "RestorePaintDeclaration", // Drawing DrawBox = "DrawBox", DrawImage = "DrawImage", diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index 4b050f3991..0c2dabb60b 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -86,6 +86,5 @@ export class DrawingContext { this.shaders = []; this.imageFilters = []; this.pathEffects = []; - this.paintDeclarations = []; } } diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index f7712cdbd6..da9e9d65be 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -70,7 +70,7 @@ const play = (ctx: DrawingContext, command: Command) => { ctx.restorePaint(); } else if (isCommand(command, CommandType.ComposeColorFilter)) { composeColorFilters(ctx); - } else if (isCommand(command, CommandType.PushPaintDeclaration)) { + } else if (isCommand(command, CommandType.RestorePaintDeclaration)) { const paint = ctx.restorePaint(); if (!paint) { throw new Error("No paint declaration to push"); @@ -98,6 +98,8 @@ const play = (ctx: DrawingContext, command: Command) => { ctx.canvas.restore(); } else { const paints = [ctx.paint, ...ctx.paintDeclarations]; + ctx.paintDeclarations = []; + console.log({ paints }); paints.forEach((p) => { ctx.paints.push(p); if (isBoxCommand(command)) { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 63719aedc4..f6349cce58 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -76,8 +76,8 @@ export class Recorder { this.add({ type: CommandType.RestorePaint }); } - pushPaintDeclaration() { - this.add({ type: CommandType.PushPaintDeclaration }); + restorePaintDeclaration() { + this.add({ type: CommandType.RestorePaintDeclaration }); } materializePaint() { diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index 2a472cd9cf..fe932e4ff1 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -191,7 +191,7 @@ const pushPaints = (recorder: Recorder, paints: Node[]) => { pushMaskFilters(recorder, maskFilters); pushShaders(recorder, shaders); pushPathEffects(recorder, pathEffects); - recorder.pushPaintDeclaration(); + recorder.restorePaintDeclaration(); }); }; diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index 47f6811e2c..e417fb61a4 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -98,10 +98,10 @@ export const sortNodeChildren = (parent: Node) => { imageFilters.push(node); } else if (isShader(node.type)) { shaders.push(node); - } else if (node.isDeclaration) { - declarations.push(node); } else if (node.type === NodeType.Paint) { paints.push(node); + } else if (node.isDeclaration) { + declarations.push(node); } else { drawings.push(node); } From 49901ccf6e472a1f7e56479a81496298e5679980 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 15:06:50 +0100 Subject: [PATCH 36/50] :wrench: --- packages/skia/src/sksg/Recorder/Player.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index da9e9d65be..5908c277d1 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -99,7 +99,6 @@ const play = (ctx: DrawingContext, command: Command) => { } else { const paints = [ctx.paint, ...ctx.paintDeclarations]; ctx.paintDeclarations = []; - console.log({ paints }); paints.forEach((p) => { ctx.paints.push(p); if (isBoxCommand(command)) { From c7b4df18ff54c78526b34dd9ae5157b9beac77a8 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 15:34:25 +0100 Subject: [PATCH 37/50] :wrench: --- packages/skia/src/sksg/Recorder/Player.ts | 1 + packages/skia/src/sksg/Recorder/Visitor.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 5908c277d1..d87d281263 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -71,6 +71,7 @@ const play = (ctx: DrawingContext, command: Command) => { } else if (isCommand(command, CommandType.ComposeColorFilter)) { composeColorFilters(ctx); } else if (isCommand(command, CommandType.RestorePaintDeclaration)) { + ctx.materializePaint(); const paint = ctx.restorePaint(); if (!paint) { throw new Error("No paint declaration to push"); diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index fe932e4ff1..b46d7e659f 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -220,7 +220,6 @@ const visitNode = (recorder: Recorder, node: Node) => { pushImageFilters(recorder, imageFilters); pushMaskFilters(recorder, maskFilters); pushShaders(recorder, shaders); - pushPaints(recorder, paints); pushPathEffects(recorder, pathEffects); // For mixed nodes like BackdropFilters we don't materialize the paint if (node.type === NodeType.BackdropFilter) { @@ -231,6 +230,7 @@ const visitNode = (recorder: Recorder, node: Node) => { recorder.materializePaint(); } } + pushPaints(recorder, paints); const ctm = processCTM(props); const shouldRestore = !!ctm || node.type === NodeType.Layer; if (ctm) { From 75bfcea1262431daa74ecc31464f0b84de7347ad Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 16:40:55 +0100 Subject: [PATCH 38/50] :wrench: --- packages/skia/src/dom/types/ImageFilters.ts | 3 ++- packages/skia/src/sksg/Container.ts | 2 +- packages/skia/src/sksg/Recorder/Player.ts | 1 + packages/skia/src/sksg/Recorder/Recorder.ts | 4 +-- packages/skia/src/sksg/Recorder/Visitor.ts | 27 ++++++++++--------- .../sksg/Recorder/commands/ImageFilters.ts | 19 ++++++------- .../src/sksg/Recorder/commands/Shaders.ts | 15 +++++++++++ packages/skia/src/sksg/nodes/Node.ts | 13 ++++++--- packages/skia/src/sksg/nodes/imageFilters.ts | 7 +++-- 9 files changed, 60 insertions(+), 31 deletions(-) diff --git a/packages/skia/src/dom/types/ImageFilters.ts b/packages/skia/src/dom/types/ImageFilters.ts index 7c950b5cea..6f11c1fee6 100644 --- a/packages/skia/src/dom/types/ImageFilters.ts +++ b/packages/skia/src/dom/types/ImageFilters.ts @@ -24,8 +24,9 @@ export interface RuntimeShaderImageFilterProps extends ChildrenProps { uniforms?: Uniforms; } +// TODO: delete export interface BlendImageFilterProps extends ChildrenProps { - mode: BlendMode; + mode: SkEnum; } export interface MorphologyImageFilterProps extends ChildrenProps { diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index d8bebb478c..efcc227213 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -113,7 +113,7 @@ export class Container { drawOnCanvas(canvas: SkCanvas) { const ctx = new DrawingContext(this.Skia, canvas); - console.log(this._recording); + //console.log(this._recording); replay(ctx, this._recording!); } } diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index d87d281263..2413122fba 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -57,6 +57,7 @@ const play = (ctx: DrawingContext, command: Command) => { if (isCommand(command, CommandType.SaveBackdropFilter)) { ctx.saveBackdropFilter(); } else if (isCommand(command, CommandType.SaveLayer)) { + ctx.materializePaint(); const paint = ctx.paintDeclarations.pop(); ctx.canvas.saveLayer(paint); } else if (isDrawCommand(command, CommandType.SavePaint)) { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index f6349cce58..f7f8bb4093 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -1,7 +1,7 @@ import type { SharedValue } from "react-native-reanimated"; +import { NodeType } from "../../dom/types"; import type { - NodeType, BlurMaskFilterProps, CircleProps, CTMProps, @@ -118,7 +118,7 @@ export class Recorder { } pushShader(shaderType: NodeType, props: AnimatedProps) { - if (!isShader(shaderType)) { + if (!isShader(shaderType) && !(shaderType === NodeType.Blend)) { throw new Error("Invalid color filter type: " + shaderType); } this.add({ type: CommandType.PushShader, shaderType, props }); diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index b46d7e659f..e970d68a3d 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -24,38 +24,38 @@ export const processPaint = ({ paint: paintRef, }: DrawingNodeProps) => { const paint: DrawingNodeProps = {}; - if (opacity) { + if (opacity !== undefined) { paint.opacity = opacity; } - if (color) { + if (color !== undefined) { paint.color = color; } - if (strokeWidth) { + if (strokeWidth !== undefined) { paint.strokeWidth = strokeWidth; } - if (blendMode) { + if (blendMode !== undefined) { paint.blendMode = blendMode; } - if (style) { + if (style !== undefined) { paint.style = style; } - if (strokeJoin) { + if (strokeJoin !== undefined) { paint.strokeJoin = strokeJoin; } - if (strokeCap) { + if (strokeCap !== undefined) { paint.strokeCap = strokeCap; } - if (strokeMiter) { + if (strokeMiter !== undefined) { paint.strokeMiter = strokeMiter; } - if (antiAlias) { + if (antiAlias !== undefined) { paint.antiAlias = antiAlias; } - if (dither) { + if (dither !== undefined) { paint.dither = dither; } - if (paintRef) { + if (paintRef !== undefined) { paint.paint = paintRef; } @@ -224,13 +224,14 @@ const visitNode = (recorder: Recorder, node: Node) => { // For mixed nodes like BackdropFilters we don't materialize the paint if (node.type === NodeType.BackdropFilter) { recorder.saveBackdropFilter(); - } else if (node.type === NodeType.Layer) { - recorder.saveLayer(); } else { recorder.materializePaint(); } } pushPaints(recorder, paints); + if (node.type === NodeType.Layer) { + recorder.saveLayer(); + } const ctm = processCTM(props); const shouldRestore = !!ctm || node.type === NodeType.Layer; if (ctm) { diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts index 5caaba908b..5924c3cda7 100644 --- a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -12,7 +12,7 @@ import type { RuntimeShaderImageFilterProps, } from "../../../dom/types"; import { NodeType } from "../../../dom/types"; -import type { SkColor, Skia, SkImageFilter } from "../../../skia/types"; +import type { SkColor, SkImageFilter, Skia } from "../../../skia/types"; import { BlendMode, BlurStyle, @@ -20,6 +20,7 @@ import { processUniforms, TileMode, } from "../../../skia/types"; +import { composeDeclarations } from "../../DeclarationContext"; import type { Command } from "../Core"; import { CommandType } from "../Core"; import type { DrawingContext } from "../DrawingContext"; @@ -132,14 +133,14 @@ const declareBlendImageFilter = ( props: BlendImageFilterProps ) => { "worklet"; - const { mode } = props; - const a = ctx.imageFilters.pop(); - const b = ctx.imageFilters.pop(); - if (!a || !b) { - throw new Error("BlendImageFilter requires two image filters"); - } - const imgf = ctx.Skia.ImageFilter.MakeBlend(mode, a, b); - ctx.imageFilters.push(imgf); + const blend = BlendMode[enumKey(props.mode)]; + // Blend ImageFilters + const imageFilters = ctx.imageFilters.splice(0, ctx.imageFilters.length); + const composer = ctx.Skia.ImageFilter.MakeBlend.bind( + ctx.Skia.ImageFilter, + blend + ); + ctx.imageFilters.push(composeDeclarations(imageFilters, composer)); }; const declareDisplacementMapImageFilter = ( diff --git a/packages/skia/src/sksg/Recorder/commands/Shaders.ts b/packages/skia/src/sksg/Recorder/commands/Shaders.ts index 329c35631c..e652681441 100644 --- a/packages/skia/src/sksg/Recorder/commands/Shaders.ts +++ b/packages/skia/src/sksg/Recorder/commands/Shaders.ts @@ -10,6 +10,7 @@ import { } from "../../../dom/nodes"; import { NodeType } from "../../../dom/types"; import type { + BlendProps, ColorProps, FractalNoiseProps, ImageShaderProps, @@ -21,11 +22,13 @@ import type { TwoPointConicalGradientProps, } from "../../../dom/types"; import { + BlendMode, FilterMode, MipmapMode, processUniforms, TileMode, } from "../../../skia/types"; +import { composeDeclarations } from "../../DeclarationContext"; import type { Command } from "../Core"; import { CommandType } from "../Core"; import type { DrawingContext } from "../DrawingContext"; @@ -208,6 +211,15 @@ const declareImageShader = (ctx: DrawingContext, props: ImageShaderProps) => { ctx.shaders.push(shader); }; +const declareBlend = (ctx: DrawingContext, props: BlendProps) => { + const blend = BlendMode[enumKey(props.mode as BlendProps["mode"])]; + const shaders = ctx.shaders.splice(0, ctx.shaders.length); + if (shaders.length > 0) { + const composer = ctx.Skia.Shader.MakeBlend.bind(ctx.Skia.Shader, blend); + ctx.shaders.push(composeDeclarations(shaders, composer)); + } +}; + export const isPushShader = ( command: Command ): command is Command => { @@ -224,6 +236,7 @@ type Props = { [NodeType.RadialGradient]: RadialGradientProps; [NodeType.SweepGradient]: SweepGradientProps; [NodeType.TwoPointConicalGradient]: TwoPointConicalGradientProps; + [NodeType.Blend]: BlendProps; }; interface PushShader @@ -261,6 +274,8 @@ export const pushShader = ( declareSweepGradientShader(ctx, command.props); } else if (isShader(command, NodeType.TwoPointConicalGradient)) { declareTwoPointConicalGradientShader(ctx, command.props); + } else if (isShader(command, NodeType.Blend)) { + declareBlend(ctx, command.props); } else { throw new Error(`Unknown shader type: ${command.shaderType}`); } diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/nodes/Node.ts index e417fb61a4..98e5021408 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/nodes/Node.ts @@ -86,7 +86,6 @@ export const sortNodeChildren = (parent: Node) => { const pathEffects: Node[] = []; const drawings: Node[] = []; const paints: Node[] = []; - const declarations: Node[] = []; parent.children.forEach((node) => { if (isColorFilter(node.type)) { colorFilters.push(node); @@ -100,8 +99,17 @@ export const sortNodeChildren = (parent: Node) => { shaders.push(node); } else if (node.type === NodeType.Paint) { paints.push(node); + } else if (node.type === NodeType.Blend) { + if (node.children[0] && isImageFilter(node.children[0].type)) { + node.type = NodeType.BlendImageFilter; + imageFilters.push(node); + } else { + node.type = NodeType.Blend; + shaders.push(node); + } + // TODO: remove isDeclaration from node } else if (node.isDeclaration) { - declarations.push(node); + throw new Error("Unknown declaration type: " + node.type); } else { drawings.push(node); } @@ -109,7 +117,6 @@ export const sortNodeChildren = (parent: Node) => { return { colorFilters, drawings, - declarations, maskFilters, shaders, pathEffects, diff --git a/packages/skia/src/sksg/nodes/imageFilters.ts b/packages/skia/src/sksg/nodes/imageFilters.ts index 457e3d16f5..378d3dfe54 100644 --- a/packages/skia/src/sksg/nodes/imageFilters.ts +++ b/packages/skia/src/sksg/nodes/imageFilters.ts @@ -210,13 +210,16 @@ export const declareBlendImageFilter = ( props: BlendImageFilterProps ) => { "worklet"; - const { mode } = props; const a = ctx.imageFilters.pop(); const b = ctx.imageFilters.pop(); if (!a || !b) { throw new Error("BlendImageFilter requires two image filters"); } - const imgf = ctx.Skia.ImageFilter.MakeBlend(mode, a, b); + const imgf = ctx.Skia.ImageFilter.MakeBlend( + BlendMode[enumKey(props.mode)], + a, + b + ); ctx.imageFilters.push(imgf); }; From 5da1e316ff22ba32c648e631b6f44fc26e94d095 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 17:01:56 +0100 Subject: [PATCH 39/50] :wrench: --- packages/skia/src/dom/types/Node.ts | 4 - packages/skia/src/renderer/Canvas.tsx | 158 ++++--- .../skia/src/renderer/__tests__/setup.tsx | 2 +- packages/skia/src/sksg/Container.ts | 4 +- packages/skia/src/sksg/DeclarationContext.ts | 85 ---- packages/skia/src/sksg/DrawingContext.ts | 238 ---------- packages/skia/src/sksg/HostConfig.ts | 2 +- packages/skia/src/sksg/{nodes => }/Node.ts | 2 +- packages/skia/src/sksg/Recorder/Player.ts | 3 +- packages/skia/src/sksg/Recorder/Recorder.ts | 4 +- packages/skia/src/sksg/Recorder/Visitor.ts | 4 +- .../commands/Drawing.ts} | 15 +- .../sksg/Recorder/commands/ImageFilters.ts | 2 +- .../src/sksg/Recorder/commands/PathEffects.ts | 2 +- .../src/sksg/Recorder/commands/Shaders.ts | 2 +- packages/skia/src/sksg/nodes/colorFilters.ts | 79 ---- packages/skia/src/sksg/nodes/context.ts | 409 ------------------ packages/skia/src/sksg/nodes/imageFilters.ts | 238 ---------- packages/skia/src/sksg/nodes/index.ts | 3 - packages/skia/src/sksg/nodes/paint.ts | 75 ---- packages/skia/src/sksg/nodes/pathEffects.ts | 118 ----- packages/skia/src/sksg/nodes/shaders.ts | 210 --------- packages/skia/src/sksg/{nodes => }/utils.ts | 15 +- 23 files changed, 112 insertions(+), 1562 deletions(-) delete mode 100644 packages/skia/src/sksg/DeclarationContext.ts delete mode 100644 packages/skia/src/sksg/DrawingContext.ts rename packages/skia/src/sksg/{nodes => }/Node.ts (98%) rename packages/skia/src/sksg/{nodes/drawings.ts => Recorder/commands/Drawing.ts} (97%) delete mode 100644 packages/skia/src/sksg/nodes/colorFilters.ts delete mode 100644 packages/skia/src/sksg/nodes/context.ts delete mode 100644 packages/skia/src/sksg/nodes/imageFilters.ts delete mode 100644 packages/skia/src/sksg/nodes/index.ts delete mode 100644 packages/skia/src/sksg/nodes/paint.ts delete mode 100644 packages/skia/src/sksg/nodes/pathEffects.ts delete mode 100644 packages/skia/src/sksg/nodes/shaders.ts rename packages/skia/src/sksg/{nodes => }/utils.ts (63%) diff --git a/packages/skia/src/dom/types/Node.ts b/packages/skia/src/dom/types/Node.ts index fc85eed9dd..25e8291158 100644 --- a/packages/skia/src/dom/types/Node.ts +++ b/packages/skia/src/dom/types/Node.ts @@ -1,6 +1,5 @@ import type { GroupProps } from "./Common"; import type { NodeType } from "./NodeType"; -import type { DeclarationContext } from "../../sksg/DeclarationContext"; export interface Node

{ type: NodeType; @@ -18,9 +17,6 @@ export interface Node

{ export type Invalidate = () => void; export interface DeclarationNode

extends Node

{ - //declarationType: DeclarationType; - decorate(ctx: DeclarationContext): void; - setInvalidate(invalidate: Invalidate): void; } diff --git a/packages/skia/src/renderer/Canvas.tsx b/packages/skia/src/renderer/Canvas.tsx index 91c804d7fd..bf6f300334 100644 --- a/packages/skia/src/renderer/Canvas.tsx +++ b/packages/skia/src/renderer/Canvas.tsx @@ -1,24 +1,32 @@ -import { - forwardRef, - useCallback, +import React, { useEffect, - useImperativeHandle, + useCallback, useMemo, + forwardRef, useRef, } from "react"; -import type { LayoutChangeEvent, ViewProps } from "react-native"; -import type { SharedValue } from "react-native-reanimated"; +import type { + RefObject, + ReactNode, + MutableRefObject, + ForwardedRef, + FunctionComponent, +} from "react"; +import type { LayoutChangeEvent } from "react-native"; -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 { SkiaDomView } from "../views"; import type { SkiaBaseViewProps } from "../views"; -const NativeSkiaPictureView = SkiaPictureViewNativeComponent; +import { SkiaRoot } from "./Reconciler"; + +export const useCanvasRef = () => useRef(null); + +export interface CanvasProps extends SkiaBaseViewProps { + ref?: RefObject; + children: ReactNode; + mode?: "default" | "continuous"; +} -// TODO: no need to go through the JS thread for this const useOnSizeEvent = ( resultValue: SkiaBaseViewProps["onSize"], onLayout?: (event: LayoutChangeEvent) => void @@ -38,40 +46,39 @@ const useOnSizeEvent = ( ); }; -export interface CanvasProps extends ViewProps { - debug?: boolean; - opaque?: boolean; - onSize?: SharedValue; - mode?: "continuous" | "default"; -} - -export const Canvas = forwardRef( +export const Canvas = forwardRef( ( { - mode, - debug, - opaque, children, - onSize, + style, + debug, + mode = "default", + onSize: _onSize, onLayout: _onLayout, - ...viewProps - }: CanvasProps, - ref + ...props + }, + forwardedRef ) => { - const rafId = useRef(null); - const onLayout = useOnSizeEvent(onSize, _onLayout); - // Native ID - const nativeId = useMemo(() => { - return SkiaViewNativeId.current++; - }, []); + 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]); - // Root - const root = useMemo(() => new SkiaSGRoot(Skia, nativeId), [nativeId]); + const root = useMemo( + () => new SkiaRoot(redraw, getNativeId), + [redraw, getNativeId] + ); - // Render effects + // Render effect useEffect(() => { root.render(children); - }, [children, root]); + }, [children, root, redraw]); useEffect(() => { return () => { @@ -79,50 +86,41 @@ 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/renderer/__tests__/setup.tsx b/packages/skia/src/renderer/__tests__/setup.tsx index 0d28252ee7..5c3bc2e8e2 100644 --- a/packages/skia/src/renderer/__tests__/setup.tsx +++ b/packages/skia/src/renderer/__tests__/setup.tsx @@ -14,7 +14,7 @@ import { isPath } from "../../skia/types"; import { E2E } from "../../__tests__/setup"; import { LoadSkiaWeb } from "../../web/LoadSkiaWeb"; import { SkiaSGRoot } from "../../sksg/Reconciler"; -import type { Node } from "../../sksg/nodes"; +import type { Node } from "../../sksg/Node"; import { SkiaObject } from "./e2e/setup"; diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index efcc227213..53d28ea488 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -9,8 +9,8 @@ import { import type { StaticContext } from "./StaticContext"; import { createStaticContext } from "./StaticContext"; -import type { Node } from "./nodes"; -import { isSharedValue } from "./nodes"; +import type { Node } from "./Node"; +import { isSharedValue } from "./utils"; import type { Command } from "./Recorder/Core"; import { Recorder } from "./Recorder/Recorder"; import { visit } from "./Recorder/Visitor"; diff --git a/packages/skia/src/sksg/DeclarationContext.ts b/packages/skia/src/sksg/DeclarationContext.ts deleted file mode 100644 index cb8c74c1e6..0000000000 --- a/packages/skia/src/sksg/DeclarationContext.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { - SkShader, - SkPaint, - SkImageFilter, - SkMaskFilter, - SkPathEffect, - Skia, - SkColorFilter, -} from "../skia/types"; - -type Composer = (outer: T, inner: T) => T; - -export const composeDeclarations = (filters: T[], composer: Composer) => { - "worklet"; - const len = filters.length; - if (len <= 1) { - return filters[0]; - } - return filters.reduceRight((inner, outer) => - inner ? composer(outer, inner) : outer - ); -}; - -const createDeclaration = (composer?: Composer) => { - "worklet"; - const state = { - decls: [] as T[], - indexes: [0], - }; - - return { - save: () => { - state.indexes.push(state.decls.length); - }, - restore: () => { - state.indexes.pop(); - }, - pop: () => state.decls.pop(), - push: (decl: T) => { - state.decls.push(decl); - }, - popAll: () => { - const idx = state.indexes[state.indexes.length - 1]; - return state.decls.splice(idx, state.decls.length - idx); - }, - popAllAsOne: () => { - if (state.decls.length === 0) { - return undefined; - } - if (!composer) { - throw new Error("No composer for this type of declaration"); - } - if (!state.decls.length) { - return undefined; - } - if (!composer) { - throw new Error("No composer for this type of declaration"); - } - - const idx = state.indexes[state.indexes.length - 1]; - const decls = state.decls.splice(idx, state.decls.length - idx); - return composeDeclarations(decls, composer); - }, - }; -}; - -export const createDeclarationContext = (Skia: Skia) => { - "worklet"; - const composers = { - pathEffect: Skia.PathEffect.MakeCompose.bind(Skia.PathEffect), - imageFilter: Skia.ImageFilter.MakeCompose.bind(Skia.ImageFilter), - colorFilter: Skia.ColorFilter.MakeCompose.bind(Skia.ColorFilter), - }; - return { - Skia, - paints: createDeclaration(), - maskFilters: createDeclaration(), - shaders: createDeclaration(), - pathEffects: createDeclaration(composers.pathEffect), - imageFilters: createDeclaration(composers.imageFilter), - colorFilters: createDeclaration(composers.colorFilter), - }; -}; - -export type DeclarationContext = ReturnType; 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/HostConfig.ts b/packages/skia/src/sksg/HostConfig.ts index c1ae5e0759..38a5a10017 100644 --- a/packages/skia/src/sksg/HostConfig.ts +++ b/packages/skia/src/sksg/HostConfig.ts @@ -5,7 +5,7 @@ import { DefaultEventPriority } from "react-reconciler/constants"; import { NodeType } from "../dom/types"; import { shallowEq } from "../renderer/typeddash"; -import type { Node } from "./nodes/Node"; +import type { Node } from "./Node"; import type { Container } from "./Container"; const DEBUG = false; diff --git a/packages/skia/src/sksg/nodes/Node.ts b/packages/skia/src/sksg/Node.ts similarity index 98% rename from packages/skia/src/sksg/nodes/Node.ts rename to packages/skia/src/sksg/Node.ts index 98e5021408..0f1146f073 100644 --- a/packages/skia/src/sksg/nodes/Node.ts +++ b/packages/skia/src/sksg/Node.ts @@ -1,4 +1,4 @@ -import { NodeType } from "../../dom/types"; +import { NodeType } from "../dom/types"; export interface Node { type: NodeType; diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 2413122fba..f1bd5eca2d 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -20,8 +20,7 @@ import { drawDiffRect, drawVertices, drawPatch, -} from "../nodes/drawings"; - +} from "./commands/Drawing"; import { drawBox, isBoxCommand } from "./commands/Box"; import { composeColorFilters, diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index f7f8bb4093..9f84560597 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -28,8 +28,8 @@ import type { BoxShadowProps, } from "../../dom/types"; import type { AnimatedProps } from "../../renderer"; -import { isSharedValue } from "../nodes/utils"; -import { isColorFilter, isImageFilter, isPathEffect, isShader } from "../nodes"; +import { isSharedValue } from "../utils"; +import { isColorFilter, isImageFilter, isPathEffect, isShader } from "../Node"; import { CommandType } from "./Core"; import type { Command } from "./Core"; diff --git a/packages/skia/src/sksg/Recorder/Visitor.ts b/packages/skia/src/sksg/Recorder/Visitor.ts index e970d68a3d..802930d945 100644 --- a/packages/skia/src/sksg/Recorder/Visitor.ts +++ b/packages/skia/src/sksg/Recorder/Visitor.ts @@ -5,8 +5,8 @@ import type { BoxShadowProps, } from "../../dom/types"; import { NodeType } from "../../dom/types"; -import type { Node } from "../nodes"; -import { isImageFilter, isShader, sortNodeChildren } from "../nodes"; +import type { Node } from "../Node"; +import { isImageFilter, isShader, sortNodeChildren } from "../Node"; import type { Recorder } from "./Recorder"; diff --git a/packages/skia/src/sksg/nodes/drawings.ts b/packages/skia/src/sksg/Recorder/commands/Drawing.ts similarity index 97% rename from packages/skia/src/sksg/nodes/drawings.ts rename to packages/skia/src/sksg/Recorder/commands/Drawing.ts index 350cdc6e66..ac2501d48e 100644 --- a/packages/skia/src/sksg/nodes/drawings.ts +++ b/packages/skia/src/sksg/Recorder/commands/Drawing.ts @@ -8,7 +8,7 @@ import { processPath, processRect, processRRect, -} from "../../dom/nodes"; +} from "../../../dom/nodes"; import type { AtlasProps, BoxProps, @@ -32,15 +32,15 @@ import type { TextPathProps, TextProps, VerticesProps, -} from "../../dom/types"; -import { saturate } from "../../renderer/processors"; +} from "../../../dom/types"; +import { saturate } from "../../../renderer/processors"; import type { SkCanvas, SkPaint, SkPoint, SkRSXform, Skia, -} from "../../skia/types"; +} from "../../../skia/types"; import { BlendMode, BlurStyle, @@ -49,10 +49,9 @@ import { isRRect, PointMode, VertexMode, -} from "../../skia/types"; - -import type { Node } from "./Node"; -import { materialize } from "./utils"; +} from "../../../skia/types"; +import type { Node } from "../../Node"; +import { materialize } from "../../utils"; interface LocalDrawingContext { Skia: Skia; diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts index 5924c3cda7..9cc5a673ae 100644 --- a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -20,7 +20,7 @@ import { processUniforms, TileMode, } from "../../../skia/types"; -import { composeDeclarations } from "../../DeclarationContext"; +import { composeDeclarations } from "../../utils"; import type { Command } from "../Core"; import { CommandType } from "../Core"; import type { DrawingContext } from "../DrawingContext"; diff --git a/packages/skia/src/sksg/Recorder/commands/PathEffects.ts b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts index af77e58faa..cc24ed2cb8 100644 --- a/packages/skia/src/sksg/Recorder/commands/PathEffects.ts +++ b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts @@ -11,7 +11,7 @@ import type { Path2DPathEffectProps, } from "../../../dom/types"; import { Path1DEffectStyle } from "../../../skia/types"; -import { composeDeclarations } from "../../DeclarationContext"; +import { composeDeclarations } from "../../utils"; import type { Command } from "../Core"; import { CommandType } from "../Core"; import type { DrawingContext } from "../DrawingContext"; diff --git a/packages/skia/src/sksg/Recorder/commands/Shaders.ts b/packages/skia/src/sksg/Recorder/commands/Shaders.ts index e652681441..b5d98cee89 100644 --- a/packages/skia/src/sksg/Recorder/commands/Shaders.ts +++ b/packages/skia/src/sksg/Recorder/commands/Shaders.ts @@ -28,7 +28,7 @@ import { processUniforms, TileMode, } from "../../../skia/types"; -import { composeDeclarations } from "../../DeclarationContext"; +import { composeDeclarations } from "../../utils"; import type { Command } from "../Core"; import { CommandType } from "../Core"; import type { DrawingContext } from "../DrawingContext"; diff --git a/packages/skia/src/sksg/nodes/colorFilters.ts b/packages/skia/src/sksg/nodes/colorFilters.ts deleted file mode 100644 index ad82866609..0000000000 --- a/packages/skia/src/sksg/nodes/colorFilters.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { enumKey } from "../../dom/nodes"; -import type { - BlendColorFilterProps, - LerpColorFilterProps, - MatrixColorFilterProps, -} from "../../dom/types"; -import type { SkColorFilter } from "../../skia/types"; -import { BlendMode } from "../../skia/types"; -import type { DeclarationContext } from "../DeclarationContext"; - -export const composeColorFilters = ( - ctx: DeclarationContext, - cf: SkColorFilter, - processChildren: () => void -) => { - "worklet"; - const { Skia } = ctx; - ctx.colorFilters.save(); - processChildren(); - const cf1 = ctx.colorFilters.popAllAsOne(); - ctx.colorFilters.restore(); - ctx.colorFilters.push(cf1 ? Skia.ColorFilter.MakeCompose(cf, cf1) : cf); -}; - -export const makeBlendColorFilter = ( - ctx: DeclarationContext, - props: BlendColorFilterProps -) => { - "worklet"; - const { mode } = props; - const color = ctx.Skia.Color(props.color); - const cf = ctx.Skia.ColorFilter.MakeBlend(color, BlendMode[enumKey(mode)]); - return cf; -}; - -export const makeSRGBToLinearGammaColorFilter = (ctx: DeclarationContext) => { - "worklet"; - const cf = ctx.Skia.ColorFilter.MakeSRGBToLinearGamma(); - return cf; -}; - -export const makeLinearToSRGBGammaColorFilter = (ctx: DeclarationContext) => { - "worklet"; - const cf = ctx.Skia.ColorFilter.MakeLinearToSRGBGamma(); - return cf; -}; - -export const declareLerpColorFilter = ( - ctx: DeclarationContext, - props: LerpColorFilterProps -) => { - "worklet"; - const { t } = props; - const second = ctx.colorFilters.pop(); - const first = ctx.colorFilters.pop(); - if (!first || !second) { - throw new Error( - "LerpColorFilterNode: missing two color filters as children" - ); - } - const cf = ctx.Skia.ColorFilter.MakeLerp(t, first, second); - ctx.colorFilters.push(cf); -}; - -export const makeMatrixColorFilter = ( - ctx: DeclarationContext, - props: MatrixColorFilterProps -) => { - "worklet"; - const { matrix } = props; - const cf = ctx.Skia.ColorFilter.MakeMatrix(matrix); - return cf; -}; - -export const makeLumaColorFilter = (ctx: DeclarationContext) => { - "worklet"; - const cf = ctx.Skia.ColorFilter.MakeLumaColorFilter(); - return cf; -}; diff --git a/packages/skia/src/sksg/nodes/context.ts b/packages/skia/src/sksg/nodes/context.ts deleted file mode 100644 index 0961aaed90..0000000000 --- a/packages/skia/src/sksg/nodes/context.ts +++ /dev/null @@ -1,409 +0,0 @@ -/* 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 { 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 { - composeColorFilters, - declareLerpColorFilter, - makeBlendColorFilter, - makeLinearToSRGBGammaColorFilter, - makeLumaColorFilter, - makeMatrixColorFilter, - makeSRGBToLinearGammaColorFilter, -} from "./colorFilters"; -import { - composeImageFilters, - declareBlend, - declareBlendImageFilter, - declareBlurMaskFilter, - declareDisplacementMapImageFilter, - makeBlurImageFilter, - makeDropShadowImageFilter, - makeMorphologyImageFilter, - makeOffsetImageFilter, - makeRuntimeShaderImageFilter, -} from "./imageFilters"; -import { materialize } from "./utils"; -import { - declareColorShader, - declareFractalNoiseShader, - declareImageShader, - declareLinearGradientShader, - declareRadialGradientShader, - declareShader, - declareSweepGradientShader, - declareTurbulenceShader, - declareTwoPointConicalGradientShader, -} from "./shaders"; -import { declarePaint } from "./paint"; -import { - composePathEffects, - declareSumPathEffect, - makeCornerPathEffect, - makeDashPathEffect, - makeDiscretePathEffect, - makeLine2DPathEffect, - makePath1DPathEffect, - makePath2DPathEffect, -} from "./pathEffects"; - -function processDeclarations(ctx: DeclarationContext, node: Node) { - "worklet"; - const processChildren = () => - node.children.forEach((child) => processDeclarations(ctx, child)); - const { type } = node; - const props = materialize(node.props); - switch (type) { - // Mask Filter - case NodeType.BlurMaskFilter: { - declareBlurMaskFilter(ctx, props); - break; - } - // Color Filters - case NodeType.LerpColorFilter: { - processChildren(); - declareLerpColorFilter(ctx, props); - break; - } - case NodeType.Blend: { - processChildren(); - declareBlend(ctx, props); - break; - } - case NodeType.BlendColorFilter: { - const cf = makeBlendColorFilter(ctx, props); - composeColorFilters(ctx, cf, processChildren); - break; - } - case NodeType.SRGBToLinearGammaColorFilter: { - const cf = makeSRGBToLinearGammaColorFilter(ctx); - composeColorFilters(ctx, cf, processChildren); - break; - } - case NodeType.LinearToSRGBGammaColorFilter: { - const cf = makeLinearToSRGBGammaColorFilter(ctx); - composeColorFilters(ctx, cf, processChildren); - break; - } - case NodeType.MatrixColorFilter: { - const cf = makeMatrixColorFilter(ctx, props); - composeColorFilters(ctx, cf, processChildren); - break; - } - case NodeType.LumaColorFilter: { - const cf = makeLumaColorFilter(ctx); - composeColorFilters(ctx, cf, processChildren); - break; - } - // Shaders - case NodeType.Shader: { - processChildren(); - declareShader(ctx, props); - break; - } - case NodeType.ImageShader: { - declareImageShader(ctx, props); - break; - } - case NodeType.ColorShader: { - declareColorShader(ctx, props); - break; - } - case NodeType.Turbulence: { - declareTurbulenceShader(ctx, props); - break; - } - case NodeType.FractalNoise: { - declareFractalNoiseShader(ctx, props); - break; - } - case NodeType.LinearGradient: { - declareLinearGradientShader(ctx, props); - break; - } - case NodeType.RadialGradient: { - declareRadialGradientShader(ctx, props); - break; - } - case NodeType.SweepGradient: { - declareSweepGradientShader(ctx, props); - break; - } - case NodeType.TwoPointConicalGradient: { - declareTwoPointConicalGradientShader(ctx, props); - break; - } - // Image Filters - case NodeType.BlurImageFilter: { - const imgf = makeBlurImageFilter(ctx, props); - composeImageFilters(ctx, imgf, processChildren); - break; - } - case NodeType.OffsetImageFilter: { - const imgf = makeOffsetImageFilter(ctx, props); - composeImageFilters(ctx, imgf, processChildren); - break; - } - case NodeType.DisplacementMapImageFilter: { - processChildren(); - declareDisplacementMapImageFilter(ctx, props); - break; - } - case NodeType.DropShadowImageFilter: { - const imgf = makeDropShadowImageFilter(ctx, props); - composeImageFilters(ctx, imgf, processChildren); - break; - } - case NodeType.MorphologyImageFilter: { - const imgf = makeMorphologyImageFilter(ctx, props); - composeImageFilters(ctx, imgf, processChildren); - break; - } - case NodeType.BlendImageFilter: { - processChildren(); - declareBlendImageFilter(ctx, props); - break; - } - case NodeType.RuntimeShaderImageFilter: { - const imgf = makeRuntimeShaderImageFilter(ctx, props); - composeImageFilters(ctx, imgf, processChildren); - break; - } - // Path Effects - case NodeType.SumPathEffect: { - processChildren(); - declareSumPathEffect(ctx); - break; - } - case NodeType.CornerPathEffect: { - const pf = makeCornerPathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - case NodeType.Path1DPathEffect: { - const pf = makePath1DPathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - case NodeType.Path2DPathEffect: { - const pf = makePath2DPathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - case NodeType.Line2DPathEffect: { - const pf = makeLine2DPathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - case NodeType.DashPathEffect: { - const pf = makeDashPathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - case NodeType.DiscretePathEffect: { - const pf = makeDiscretePathEffect(ctx, props); - composePathEffects(ctx, pf, processChildren); - break; - } - // Paint - case NodeType.Paint: - processChildren(); - declarePaint(ctx, props); - break; - default: - 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(); - } -} diff --git a/packages/skia/src/sksg/nodes/imageFilters.ts b/packages/skia/src/sksg/nodes/imageFilters.ts deleted file mode 100644 index 378d3dfe54..0000000000 --- a/packages/skia/src/sksg/nodes/imageFilters.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { enumKey, processRadius } from "../../dom/nodes"; -import type { - BlendImageFilterProps, - BlendProps, - BlurImageFilterProps, - BlurMaskFilterProps, - DisplacementMapImageFilterProps, - DropShadowImageFilterProps, - MorphologyImageFilterProps, - OffsetImageFilterProps, - RuntimeShaderImageFilterProps, -} from "../../dom/types"; -import type { SkColor, Skia, SkImageFilter } from "../../skia/types"; -import { - BlendMode, - BlurStyle, - ColorChannel, - processUniforms, - TileMode, -} from "../../skia/types"; -import type { DeclarationContext } from "../DeclarationContext"; -import { composeDeclarations } from "../DeclarationContext"; - -export enum MorphologyOperator { - Erode, - Dilate, -} - -const Black = Float32Array.of(0, 0, 0, 1); - -const MakeInnerShadow = ( - Skia: Skia, - shadowOnly: boolean | undefined, - dx: number, - dy: number, - sigmaX: number, - sigmaY: number, - color: SkColor, - input: SkImageFilter | null -) => { - "worklet"; - const sourceGraphic = Skia.ImageFilter.MakeColorFilter( - Skia.ColorFilter.MakeBlend(Black, BlendMode.Dst), - null - ); - const sourceAlpha = Skia.ImageFilter.MakeColorFilter( - Skia.ColorFilter.MakeBlend(Black, BlendMode.SrcIn), - null - ); - const f1 = Skia.ImageFilter.MakeColorFilter( - Skia.ColorFilter.MakeBlend(color, BlendMode.SrcOut), - null - ); - const f2 = Skia.ImageFilter.MakeOffset(dx, dy, f1); - const f3 = Skia.ImageFilter.MakeBlur(sigmaX, sigmaY, TileMode.Decal, f2); - const f4 = Skia.ImageFilter.MakeBlend(BlendMode.SrcIn, sourceAlpha, f3); - if (shadowOnly) { - return f4; - } - return Skia.ImageFilter.MakeCompose( - input, - Skia.ImageFilter.MakeBlend(BlendMode.SrcOver, sourceGraphic, f4) - ); -}; - -export const declareBlend = (ctx: DeclarationContext, props: BlendProps) => { - "worklet"; - const { Skia } = ctx; - const blend = BlendMode[enumKey(props.mode as BlendProps["mode"])]; - // Blend ImageFilters - const imageFilters = ctx.imageFilters.popAll(); - if (imageFilters.length > 0) { - const composer = Skia.ImageFilter.MakeBlend.bind(Skia.ImageFilter, blend); - ctx.imageFilters.push(composeDeclarations(imageFilters, composer)); - } - // Blend Shaders - const shaders = ctx.shaders.popAll(); - if (shaders.length > 0) { - const composer = Skia.Shader.MakeBlend.bind(Skia.Shader, blend); - ctx.shaders.push(composeDeclarations(shaders, composer)); - } -}; - -export const composeImageFilters = ( - ctx: DeclarationContext, - imgf1: SkImageFilter, - processChildren: () => void -) => { - "worklet"; - const { Skia } = ctx; - ctx.imageFilters.save(); - ctx.colorFilters.save(); - processChildren(); - let imgf2 = ctx.imageFilters.popAllAsOne(); - const cf = ctx.colorFilters.popAllAsOne(); - ctx.imageFilters.restore(); - ctx.colorFilters.restore(); - if (cf) { - imgf2 = Skia.ImageFilter.MakeCompose( - imgf2 ?? null, - Skia.ImageFilter.MakeColorFilter(cf, null) - ); - } - const imgf = imgf2 ? Skia.ImageFilter.MakeCompose(imgf1, imgf2) : imgf1; - ctx.imageFilters.push(imgf); -}; - -const input = (ctx: DeclarationContext) => { - "worklet"; - return ctx.imageFilters.pop() ?? null; -}; - -export const makeOffsetImageFilter = ( - ctx: DeclarationContext, - props: OffsetImageFilterProps -) => { - "worklet"; - const { x, y } = props; - return ctx.Skia.ImageFilter.MakeOffset(x, y, null); -}; - -export const declareDisplacementMapImageFilter = ( - ctx: DeclarationContext, - props: DisplacementMapImageFilterProps -) => { - "worklet"; - const { channelX, channelY, scale } = props; - const shader = ctx.shaders.pop(); - if (!shader) { - throw new Error("DisplacementMap expects a shader as child"); - } - const map = ctx.Skia.ImageFilter.MakeShader(shader, null); - const imgf = ctx.Skia.ImageFilter.MakeDisplacementMap( - ColorChannel[enumKey(channelX)], - ColorChannel[enumKey(channelY)], - scale, - map, - input(ctx) - ); - ctx.imageFilters.push(imgf); -}; - -export const makeBlurImageFilter = ( - ctx: DeclarationContext, - props: BlurImageFilterProps -) => { - "worklet"; - const { mode, blur } = props; - const sigma = processRadius(ctx.Skia, blur); - const imgf = ctx.Skia.ImageFilter.MakeBlur( - sigma.x, - sigma.y, - TileMode[enumKey(mode)], - input(ctx) - ); - return imgf; -}; - -export const makeDropShadowImageFilter = ( - ctx: DeclarationContext, - props: DropShadowImageFilterProps -) => { - "worklet"; - const { dx, dy, blur, shadowOnly, color: cl, inner } = props; - const color = ctx.Skia.Color(cl); - let factory; - if (inner) { - factory = MakeInnerShadow.bind(null, ctx.Skia, shadowOnly); - } else { - factory = shadowOnly - ? ctx.Skia.ImageFilter.MakeDropShadowOnly.bind(ctx.Skia.ImageFilter) - : ctx.Skia.ImageFilter.MakeDropShadow.bind(ctx.Skia.ImageFilter); - } - const imgf = factory(dx, dy, blur, blur, color, input(ctx)); - return imgf; -}; - -export const makeMorphologyImageFilter = ( - ctx: DeclarationContext, - props: MorphologyImageFilterProps -) => { - "worklet"; - const { operator } = props; - const r = processRadius(ctx.Skia, props.radius); - let imgf; - if (MorphologyOperator[enumKey(operator)] === MorphologyOperator.Erode) { - imgf = ctx.Skia.ImageFilter.MakeErode(r.x, r.y, input(ctx)); - } else { - imgf = ctx.Skia.ImageFilter.MakeDilate(r.x, r.y, input(ctx)); - } - return imgf; -}; - -export const makeRuntimeShaderImageFilter = ( - ctx: DeclarationContext, - props: RuntimeShaderImageFilterProps -) => { - "worklet"; - const { source, uniforms } = props; - const rtb = ctx.Skia.RuntimeShaderBuilder(source); - if (uniforms) { - processUniforms(source, uniforms, rtb); - } - const imgf = ctx.Skia.ImageFilter.MakeRuntimeShader(rtb, null, input(ctx)); - return imgf; -}; - -export const declareBlendImageFilter = ( - ctx: DeclarationContext, - props: BlendImageFilterProps -) => { - "worklet"; - const a = ctx.imageFilters.pop(); - const b = ctx.imageFilters.pop(); - if (!a || !b) { - throw new Error("BlendImageFilter requires two image filters"); - } - const imgf = ctx.Skia.ImageFilter.MakeBlend( - BlendMode[enumKey(props.mode)], - a, - b - ); - ctx.imageFilters.push(imgf); -}; - -export const declareBlurMaskFilter = ( - ctx: DeclarationContext, - props: BlurMaskFilterProps -) => { - "worklet"; - const { blur, style, respectCTM } = props; - const mf = ctx.Skia.MaskFilter.MakeBlur( - BlurStyle[enumKey(style)], - blur, - respectCTM - ); - ctx.maskFilters.push(mf); -}; diff --git a/packages/skia/src/sksg/nodes/index.ts b/packages/skia/src/sksg/nodes/index.ts deleted file mode 100644 index 168641aba0..0000000000 --- a/packages/skia/src/sksg/nodes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./Node"; -export * from "./context"; -export * from "./utils"; diff --git a/packages/skia/src/sksg/nodes/paint.ts b/packages/skia/src/sksg/nodes/paint.ts deleted file mode 100644 index b16575d167..0000000000 --- a/packages/skia/src/sksg/nodes/paint.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { enumKey } from "../../dom/nodes"; -import type { PaintProps } from "../../dom/types"; -import { BlendMode, PaintStyle, StrokeCap, StrokeJoin } from "../../skia/types"; -import type { DeclarationContext } from "../DeclarationContext"; - -export const declarePaint = (ctx: DeclarationContext, props: PaintProps) => { - "worklet"; - const { - color, - strokeWidth, - blendMode, - style, - strokeJoin, - strokeCap, - strokeMiter, - opacity, - antiAlias, - dither, - } = props; - const paint = ctx.Skia.Paint(); - if (color !== undefined) { - paint.setColor(ctx.Skia.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 (opacity !== undefined) { - paint.setAlphaf(opacity); - } - if (antiAlias !== undefined) { - paint.setAntiAlias(antiAlias); - } - if (dither !== undefined) { - paint.setDither(dither); - } - //ctx.save(); - - const colorFilter = ctx.colorFilters.popAllAsOne(); - const imageFilter = ctx.imageFilters.popAllAsOne(); - const shader = ctx.shaders.pop(); - const maskFilter = ctx.maskFilters.pop(); - const pathEffect = ctx.pathEffects.popAllAsOne(); - //ctx.restore(); - if (imageFilter) { - paint.setImageFilter(imageFilter); - } - if (shader) { - paint.setShader(shader); - } - if (pathEffect) { - paint.setPathEffect(pathEffect); - } - if (colorFilter) { - paint.setColorFilter(colorFilter); - } - if (maskFilter) { - paint.setMaskFilter(maskFilter); - } - ctx.paints.push(paint); -}; diff --git a/packages/skia/src/sksg/nodes/pathEffects.ts b/packages/skia/src/sksg/nodes/pathEffects.ts deleted file mode 100644 index f60daf0454..0000000000 --- a/packages/skia/src/sksg/nodes/pathEffects.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { enumKey, processPath } from "../../dom/nodes"; -import type { - CornerPathEffectProps, - DashPathEffectProps, - DiscretePathEffectProps, - Line2DPathEffectProps, - Path1DPathEffectProps, - Path2DPathEffectProps, -} from "../../dom/types"; -import type { SkPathEffect } from "../../skia/types"; -import { Path1DEffectStyle } from "../../skia/types"; -import { - composeDeclarations, - type DeclarationContext, -} from "../DeclarationContext"; - -export const composePathEffects = ( - ctx: DeclarationContext, - pe: SkPathEffect, - processChildren: () => void -) => { - "worklet"; - const { Skia } = ctx; - ctx.pathEffects.save(); - processChildren(); - const pe1 = ctx.pathEffects.popAllAsOne(); - ctx.pathEffects.restore(); - ctx.pathEffects.push(pe1 ? Skia.PathEffect.MakeCompose(pe, pe1) : pe); -}; - -export const makeDiscretePathEffect = ( - ctx: DeclarationContext, - props: DiscretePathEffectProps -) => { - "worklet"; - const { length, deviation, seed } = props; - return ctx.Skia.PathEffect.MakeDiscrete(length, deviation, seed); -}; - -export const makePath2DPathEffect = ( - ctx: DeclarationContext, - props: Path2DPathEffectProps -) => { - "worklet"; - const { matrix } = props; - const path = processPath(ctx.Skia, props.path); - const pe = ctx.Skia.PathEffect.MakePath2D(matrix, path); - if (pe === null) { - throw new Error("Path2DPathEffect: invalid path"); - } - return pe; -}; - -export const makeDashPathEffect = ( - ctx: DeclarationContext, - props: DashPathEffectProps -) => { - "worklet"; - const { intervals, phase } = props; - const pe = ctx.Skia.PathEffect.MakeDash(intervals, phase); - return pe; -}; - -export const makeCornerPathEffect = ( - ctx: DeclarationContext, - props: CornerPathEffectProps -) => { - "worklet"; - const { r } = props; - const pe = ctx.Skia.PathEffect.MakeCorner(r); - if (pe === null) { - throw new Error("CornerPathEffect: couldn't create path effect"); - } - return pe; -}; - -export const declareSumPathEffect = (ctx: DeclarationContext) => { - "worklet"; - // Note: decorateChildren functionality needs to be handled differently - const pes = ctx.pathEffects.popAll(); - const pe = composeDeclarations( - pes, - ctx.Skia.PathEffect.MakeSum.bind(ctx.Skia.PathEffect) - ); - ctx.pathEffects.push(pe); -}; - -export const makeLine2DPathEffect = ( - ctx: DeclarationContext, - props: Line2DPathEffectProps -) => { - "worklet"; - const { width, matrix } = props; - const pe = ctx.Skia.PathEffect.MakeLine2D(width, matrix); - if (pe === null) { - throw new Error("Line2DPathEffect: could not create path effect"); - } - return pe; -}; - -export const makePath1DPathEffect = ( - ctx: DeclarationContext, - props: Path1DPathEffectProps -) => { - "worklet"; - const { advance, phase, style } = props; - const path = processPath(ctx.Skia, props.path); - const pe = ctx.Skia.PathEffect.MakePath1D( - path, - advance, - phase, - Path1DEffectStyle[enumKey(style)] - ); - if (pe === null) { - throw new Error("Path1DPathEffect: could not create path effect"); - } - return pe; -}; diff --git a/packages/skia/src/sksg/nodes/shaders.ts b/packages/skia/src/sksg/nodes/shaders.ts deleted file mode 100644 index 3d2005e88a..0000000000 --- a/packages/skia/src/sksg/nodes/shaders.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { - enumKey, - fitRects, - getRect, - processGradientProps, - processTransformProps, - rect2rect, -} from "../../dom/nodes"; -import type { - ColorProps, - FractalNoiseProps, - ImageShaderProps, - LinearGradientProps, - RadialGradientProps, - ShaderProps, - SweepGradientProps, - TurbulenceProps, - TwoPointConicalGradientProps, -} from "../../dom/types"; -import { - FilterMode, - MipmapMode, - processUniforms, - TileMode, -} from "../../skia/types"; -import type { DeclarationContext } from "../DeclarationContext"; - -export const declareShader = (ctx: DeclarationContext, props: ShaderProps) => { - "worklet"; - const { source, uniforms, ...transform } = props; - const m3 = ctx.Skia.Matrix(); - processTransformProps(m3, transform); - const shader = source.makeShaderWithChildren( - processUniforms(source, uniforms), - ctx.shaders.popAll(), - m3 - ); - ctx.shaders.push(shader); -}; - -export const declareColorShader = ( - ctx: DeclarationContext, - props: ColorProps -) => { - "worklet"; - const { color } = props; - const shader = ctx.Skia.Shader.MakeColor(ctx.Skia.Color(color)); - ctx.shaders.push(shader); -}; - -export const declareFractalNoiseShader = ( - ctx: DeclarationContext, - props: FractalNoiseProps -) => { - "worklet"; - const { freqX, freqY, octaves, seed, tileWidth, tileHeight } = props; - const shader = ctx.Skia.Shader.MakeFractalNoise( - freqX, - freqY, - octaves, - seed, - tileWidth, - tileHeight - ); - ctx.shaders.push(shader); -}; - -export const declareTwoPointConicalGradientShader = ( - ctx: DeclarationContext, - props: TwoPointConicalGradientProps -) => { - "worklet"; - const { startR, endR, start, end } = props; - const { colors, positions, mode, localMatrix, flags } = processGradientProps( - ctx.Skia, - props - ); - const shader = ctx.Skia.Shader.MakeTwoPointConicalGradient( - start, - startR, - end, - endR, - colors, - positions, - mode, - localMatrix, - flags - ); - ctx.shaders.push(shader); -}; - -export const declareRadialGradientShader = ( - ctx: DeclarationContext, - props: RadialGradientProps -) => { - "worklet"; - const { c, r } = props; - const { colors, positions, mode, localMatrix, flags } = processGradientProps( - ctx.Skia, - props - ); - const shader = ctx.Skia.Shader.MakeRadialGradient( - c, - r, - colors, - positions, - mode, - localMatrix, - flags - ); - ctx.shaders.push(shader); -}; - -export const declareSweepGradientShader = ( - ctx: DeclarationContext, - props: SweepGradientProps -) => { - "worklet"; - const { c, start, end } = props; - const { colors, positions, mode, localMatrix, flags } = processGradientProps( - ctx.Skia, - props - ); - const shader = ctx.Skia.Shader.MakeSweepGradient( - c.x, - c.y, - colors, - positions, - mode, - localMatrix, - flags, - start, - end - ); - ctx.shaders.push(shader); -}; - -export const declareLinearGradientShader = ( - ctx: DeclarationContext, - props: LinearGradientProps -) => { - "worklet"; - const { start, end } = props; - const { colors, positions, mode, localMatrix, flags } = processGradientProps( - ctx.Skia, - props - ); - const shader = ctx.Skia.Shader.MakeLinearGradient( - start, - end, - colors, - positions ?? null, - mode, - localMatrix, - flags - ); - ctx.shaders.push(shader); -}; - -export const declareTurbulenceShader = ( - ctx: DeclarationContext, - props: TurbulenceProps -) => { - "worklet"; - const { freqX, freqY, octaves, seed, tileWidth, tileHeight } = props; - const shader = ctx.Skia.Shader.MakeTurbulence( - freqX, - freqY, - octaves, - seed, - tileWidth, - tileHeight - ); - ctx.shaders.push(shader); -}; - -export const declareImageShader = ( - ctx: DeclarationContext, - props: ImageShaderProps -) => { - "worklet"; - const { fit, image, tx, ty, fm, mm, ...imageShaderProps } = props; - if (!image) { - return; - } - - const rct = getRect(ctx.Skia, imageShaderProps); - const m3 = ctx.Skia.Matrix(); - if (rct) { - const rects = fitRects( - fit, - { x: 0, y: 0, width: image.width(), height: image.height() }, - rct - ); - const [x, y, sx, sy] = rect2rect(rects.src, rects.dst); - m3.translate(x.translateX, y.translateY); - m3.scale(sx.scaleX, sy.scaleY); - } - const lm = ctx.Skia.Matrix(); - lm.concat(m3); - processTransformProps(lm, imageShaderProps); - const shader = image.makeShaderOptions( - TileMode[enumKey(tx)], - TileMode[enumKey(ty)], - FilterMode[enumKey(fm)], - MipmapMode[enumKey(mm)], - lm - ); - ctx.shaders.push(shader); -}; diff --git a/packages/skia/src/sksg/nodes/utils.ts b/packages/skia/src/sksg/utils.ts similarity index 63% rename from packages/skia/src/sksg/nodes/utils.ts rename to packages/skia/src/sksg/utils.ts index 0071f4a1d7..66945d036d 100644 --- a/packages/skia/src/sksg/nodes/utils.ts +++ b/packages/skia/src/sksg/utils.ts @@ -1,6 +1,6 @@ import type { SharedValue } from "react-native-reanimated"; -import { mapKeys } from "../../renderer/typeddash"; +import { mapKeys } from "../renderer/typeddash"; export const isSharedValue = ( value: unknown @@ -21,3 +21,16 @@ export const materialize = (props: T) => { }); return result; }; + +type Composer = (outer: T, inner: T) => T; + +export const composeDeclarations = (filters: T[], composer: Composer) => { + "worklet"; + const len = filters.length; + if (len <= 1) { + return filters[0]; + } + return filters.reduceRight((inner, outer) => + inner ? composer(outer, inner) : outer + ); +}; From 0c9e7eacfb31e013b0e587612424ba1c77869bfe Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 17:28:26 +0100 Subject: [PATCH 40/50] :wrench: --- packages/skia/src/renderer/Canvas.tsx | 158 +++++++++--------- packages/skia/src/sksg/Container.ts | 40 +++-- .../skia/src/sksg/Recorder/DrawingContext.ts | 13 +- packages/skia/src/sksg/Recorder/Player.ts | 3 + packages/skia/src/sksg/Recorder/Recorder.ts | 6 + packages/skia/src/sksg/Recorder/Recording.ts | 16 ++ packages/skia/src/sksg/StaticContext.ts | 9 - 7 files changed, 135 insertions(+), 110 deletions(-) create mode 100644 packages/skia/src/sksg/Recorder/Recording.ts delete mode 100644 packages/skia/src/sksg/StaticContext.ts 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/Container.ts b/packages/skia/src/sksg/Container.ts index 53d28ea488..ace1b47f3f 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -7,30 +7,23 @@ import { HAS_REANIMATED_3, } from "../external/reanimated/renderHelpers"; -import type { StaticContext } from "./StaticContext"; -import { createStaticContext } from "./StaticContext"; import type { Node } from "./Node"; import { isSharedValue } from "./utils"; -import type { Command } from "./Recorder/Core"; import { Recorder } from "./Recorder/Recorder"; import { visit } from "./Recorder/Visitor"; import { replay } from "./Recorder/Player"; import { DrawingContext } from "./Recorder/DrawingContext"; +import type { Recording } from "./Recorder/Recording"; -const drawOnscreen = ( - Skia: Skia, - nativeId: number, - recording: Command[], - _staticCtx: StaticContext -) => { +const drawOnscreen = (Skia: Skia, nativeId: number, recording: Recording) => { "worklet"; const rec = Skia.PictureRecorder(); const canvas = rec.beginRecording(); const start = performance.now(); - const ctx = new DrawingContext(Skia, canvas); + const ctx = new DrawingContext(Skia, recording.paintPool, canvas); //console.log(this._recording); - replay(ctx, recording); + replay(ctx, recording.commands); const picture = rec.finishRecordingAsPicture(); const end = performance.now(); console.log("Recording time: ", end - start); @@ -39,8 +32,7 @@ const drawOnscreen = ( export class Container { private _root: Node[] = []; - private _staticCtx: StaticContext | null = null; - private _recording: Command[] | null = null; + private _recording: Recording | null = null; public unmounted = false; private values = new Set>(); @@ -61,17 +53,16 @@ export class Container { if (this.mapperId !== null) { Rea.stopMapper(this.mapperId); } - const { nativeId, Skia, _staticCtx, _recording } = this; + const { nativeId, Skia, _recording } = this; this.mapperId = Rea.startMapper(() => { "worklet"; - drawOnscreen(Skia, nativeId, _recording!, _staticCtx!); + drawOnscreen(Skia, nativeId, _recording!); }, Array.from(this.values)); } this._root = root; - this._staticCtx = createStaticContext(this.Skia); const recorder = new Recorder(); visit(recorder, root); - this._recording = recorder.commands; + this._recording = recorder.finishAsRecording(this.Skia); } clear() { @@ -84,9 +75,9 @@ export class Container { throw new Error("React Native Skia only supports Reanimated 3 and above"); } if (isOnscreen) { - const { nativeId, Skia, _recording, _staticCtx } = this; + const { nativeId, Skia, _recording } = this; Rea.runOnUI(() => { - drawOnscreen(Skia, nativeId, _recording!, _staticCtx!); + drawOnscreen(Skia, nativeId, _recording!); })(); } } @@ -112,8 +103,15 @@ export class Container { } drawOnCanvas(canvas: SkCanvas) { - const ctx = new DrawingContext(this.Skia, canvas); + if (!this._recording) { + throw new Error("No recording to draw"); + } + const ctx = new DrawingContext( + this.Skia, + this._recording.paintPool, + canvas + ); //console.log(this._recording); - replay(ctx, this._recording!); + replay(ctx, this._recording.commands); } } diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index 0c2dabb60b..893d5bcee3 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -19,15 +19,24 @@ export class DrawingContext { imageFilters: SkImageFilter[] = []; pathEffects: SkPathEffect[] = []; paintDeclarations: SkPaint[] = []; + paintPool: SkPaint[] = []; - constructor(Skia: Skia, canvas: SkCanvas) { + constructor(Skia: Skia, paintPool: SkPaint[], canvas: SkCanvas) { this.Skia = Skia; this.canvas = canvas; this.paints.push(Skia.Paint()); + this.paintPool = paintPool; } savePaint() { - this.paints.push(this.paint.copy()); + const i = this.paints.length; + if (!this.paintPool[i]) { + this.paintPool.push(this.Skia.Paint()); + } + const paint = this.paintPool[i]; + const parentPaint = this.paint; + paint.assign(parentPaint); + this.paints.push(paint); } saveBackdropFilter() { diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index f1bd5eca2d..8b76ab51e8 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -1,5 +1,7 @@ "worklet"; +import type { SkPaint } from "../../skia"; + import { drawCircle, drawImage, @@ -49,6 +51,7 @@ import { type Command, } from "./Core"; import type { DrawingContext } from "./DrawingContext"; +import type { Recording } from "./Recording"; const play = (ctx: DrawingContext, command: Command) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 9f84560597..5eb56b8a7a 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -30,9 +30,11 @@ import type { import type { AnimatedProps } from "../../renderer"; import { isSharedValue } from "../utils"; import { isColorFilter, isImageFilter, isPathEffect, isShader } from "../Node"; +import type { Skia } from "../../skia/types"; import { CommandType } from "./Core"; import type { Command } from "./Core"; +import { createRecording } from "./Recording"; export class Recorder { commands: Command[] = []; @@ -68,6 +70,10 @@ export class Recorder { this.commands.push(command); } + finishAsRecording(Skia: Skia) { + return createRecording(Skia, this.commands); + } + savePaint(props: AnimatedProps) { this.add({ type: CommandType.SavePaint, props }); } diff --git a/packages/skia/src/sksg/Recorder/Recording.ts b/packages/skia/src/sksg/Recorder/Recording.ts new file mode 100644 index 0000000000..9481e51674 --- /dev/null +++ b/packages/skia/src/sksg/Recorder/Recording.ts @@ -0,0 +1,16 @@ +import type { Skia, SkPaint } from "../../skia/types"; + +import type { Command } from "./Core"; + +export interface Recording { + commands: Command[]; + paintPool: SkPaint[]; +} + +export const createRecording = ( + Skia: Skia, + commands: Command[] +): Recording => ({ + commands, + paintPool: [Skia.Paint()], +}); diff --git a/packages/skia/src/sksg/StaticContext.ts b/packages/skia/src/sksg/StaticContext.ts deleted file mode 100644 index 06539bf945..0000000000 --- a/packages/skia/src/sksg/StaticContext.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Skia, SkPaint } from "../skia/types"; - -export interface StaticContext { - paints: SkPaint[]; -} - -export const createStaticContext = (Skia: Skia) => { - return { paints: [Skia.Paint()] }; -}; From 640128eca31af8a930bf622d9600c89b4f180bdc Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 17:54:04 +0100 Subject: [PATCH 41/50] :wrenc: --- packages/skia/src/renderer/Canvas.tsx | 158 ++++++++++++------------ packages/skia/src/sksg/Container.ts | 8 +- packages/skia/src/sksg/Recorder/Core.ts | 116 +++++++++++------ 3 files changed, 160 insertions(+), 122 deletions(-) diff --git a/packages/skia/src/renderer/Canvas.tsx b/packages/skia/src/renderer/Canvas.tsx index 91c804d7fd..bf6f300334 100644 --- a/packages/skia/src/renderer/Canvas.tsx +++ b/packages/skia/src/renderer/Canvas.tsx @@ -1,24 +1,32 @@ -import { - forwardRef, - useCallback, +import React, { useEffect, - useImperativeHandle, + useCallback, useMemo, + forwardRef, useRef, } from "react"; -import type { LayoutChangeEvent, ViewProps } from "react-native"; -import type { SharedValue } from "react-native-reanimated"; +import type { + RefObject, + ReactNode, + MutableRefObject, + ForwardedRef, + FunctionComponent, +} from "react"; +import type { LayoutChangeEvent } from "react-native"; -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 { SkiaDomView } from "../views"; import type { SkiaBaseViewProps } from "../views"; -const NativeSkiaPictureView = SkiaPictureViewNativeComponent; +import { SkiaRoot } from "./Reconciler"; + +export const useCanvasRef = () => useRef(null); + +export interface CanvasProps extends SkiaBaseViewProps { + ref?: RefObject; + children: ReactNode; + mode?: "default" | "continuous"; +} -// TODO: no need to go through the JS thread for this const useOnSizeEvent = ( resultValue: SkiaBaseViewProps["onSize"], onLayout?: (event: LayoutChangeEvent) => void @@ -38,40 +46,39 @@ const useOnSizeEvent = ( ); }; -export interface CanvasProps extends ViewProps { - debug?: boolean; - opaque?: boolean; - onSize?: SharedValue; - mode?: "continuous" | "default"; -} - -export const Canvas = forwardRef( +export const Canvas = forwardRef( ( { - mode, - debug, - opaque, children, - onSize, + style, + debug, + mode = "default", + onSize: _onSize, onLayout: _onLayout, - ...viewProps - }: CanvasProps, - ref + ...props + }, + forwardedRef ) => { - const rafId = useRef(null); - const onLayout = useOnSizeEvent(onSize, _onLayout); - // Native ID - const nativeId = useMemo(() => { - return SkiaViewNativeId.current++; - }, []); + 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]); - // Root - const root = useMemo(() => new SkiaSGRoot(Skia, nativeId), [nativeId]); + const root = useMemo( + () => new SkiaRoot(redraw, getNativeId), + [redraw, getNativeId] + ); - // Render effects + // Render effect useEffect(() => { root.render(children); - }, [children, root]); + }, [children, root, redraw]); useEffect(() => { return () => { @@ -79,50 +86,41 @@ 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/Container.ts b/packages/skia/src/sksg/Container.ts index ace1b47f3f..b276dcdffa 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -19,14 +19,14 @@ const drawOnscreen = (Skia: Skia, nativeId: number, recording: Recording) => { "worklet"; const rec = Skia.PictureRecorder(); const canvas = rec.beginRecording(); - const start = performance.now(); + // const start = performance.now(); const ctx = new DrawingContext(Skia, recording.paintPool, canvas); - //console.log(this._recording); + //console.log(recording.commands); replay(ctx, recording.commands); const picture = rec.finishRecordingAsPicture(); - const end = performance.now(); - console.log("Recording time: ", end - start); + //const end = performance.now(); + //console.log("Recording time: ", end - start); SkiaViewApi.setJsiProperty(nativeId, "picture", picture); }; diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 2596fb258e..3cb61cd635 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -27,47 +27,87 @@ import type { DrawingNodeProps, } from "../../dom/types"; -// TODO: remove string labels +// export enum CommandType { +// // Context +// SavePaint = "SavePaint", +// RestorePaint = "RestorePaint", +// SaveCTM = "SaveCTM", +// RestoreCTM = "RestoreCTM", +// PushColorFilter = "PushColorFilter", +// PushBlurMaskFilter = "PushBlurMaskFilter", +// PushImageFilter = "PushImageFilter", +// PushPathEffect = "PushPathEffect", +// PushShader = "PushShader", +// ComposeColorFilter = "ComposeColorFilter", +// ComposeImageFilter = "ComposeImageFilter", +// ComposePathEffect = "ComposePathEffect", +// MaterializePaint = "MaterializePaint", +// SaveBackdropFilter = "SaveBackdropFilter", +// SaveLayer = "SaveLayer", +// RestorePaintDeclaration = "RestorePaintDeclaration", +// // Drawing +// DrawBox = "DrawBox", +// DrawImage = "DrawImage", +// DrawCircle = "DrawCircle", +// DrawPaint = "DrawPaint", +// DrawPoints = "DrawPoints", +// DrawPath = "DrawPath", +// DrawRect = "DrawRect", +// DrawRRect = "DrawRRect", +// DrawOval = "DrawOval", +// DrawLine = "DrawLine", +// DrawPatch = "DrawPatch", +// DrawVertices = "DrawVertices", +// DrawDiffRect = "DrawDiffRect", +// DrawText = "DrawText", +// DrawTextPath = "DrawTextPath", +// DrawTextBlob = "DrawTextBlob", +// DrawGlyphs = "DrawGlyphs", +// DrawPicture = "DrawPicture", +// DrawImageSVG = "DrawImageSVG", +// DrawParagraph = "DrawParagraph", +// DrawAtlas = "DrawAtlas", +// } export enum CommandType { // Context - SavePaint = "SavePaint", - RestorePaint = "RestorePaint", - SaveCTM = "SaveCTM", - RestoreCTM = "RestoreCTM", - PushColorFilter = "PushColorFilter", - PushBlurMaskFilter = "PushBlurMaskFilter", - PushImageFilter = "PushImageFilter", - PushPathEffect = "PushPathEffect", - PushShader = "PushShader", - ComposeColorFilter = "ComposeColorFilter", - ComposeImageFilter = "ComposeImageFilter", - ComposePathEffect = "ComposePathEffect", - MaterializePaint = "MaterializePaint", - SaveBackdropFilter = "SaveBackdropFilter", - SaveLayer = "SaveLayer", - RestorePaintDeclaration = "RestorePaintDeclaration", + SavePaint, + RestorePaint, + SaveCTM, + RestoreCTM, + PushColorFilter, + PushBlurMaskFilter, + PushImageFilter, + PushPathEffect, + PushShader, + ComposeColorFilter, + ComposeImageFilter, + ComposePathEffect, + MaterializePaint, + SaveBackdropFilter, + SaveLayer, + RestorePaintDeclaration, // Drawing - DrawBox = "DrawBox", - DrawImage = "DrawImage", - DrawCircle = "DrawCircle", - DrawPaint = "DrawPaint", - DrawPoints = "DrawPoints", - DrawPath = "DrawPath", - DrawRect = "DrawRect", - DrawRRect = "DrawRRect", - DrawOval = "DrawOval", - DrawLine = "DrawLine", - DrawPatch = "DrawPatch", - DrawVertices = "DrawVertices", - DrawDiffRect = "DrawDiffRect", - DrawText = "DrawText", - DrawTextPath = "DrawTextPath", - DrawTextBlob = "DrawTextBlob", - DrawGlyphs = "DrawGlyphs", - DrawPicture = "DrawPicture", - DrawImageSVG = "DrawImageSVG", - DrawParagraph = "DrawParagraph", - DrawAtlas = "DrawAtlas", + DrawBox, + DrawImage, + DrawCircle, + DrawPaint, + DrawPoints, + DrawPath, + DrawRect, + DrawRRect, + DrawOval, + DrawLine, + DrawPatch, + DrawVertices, + DrawDiffRect, + DrawText, + DrawTextPath, + DrawTextBlob, + DrawGlyphs, + DrawPicture, + DrawImageSVG, + DrawParagraph, + DrawAtlas, } export type Command = { From cb18b2d2420616ab4dffb310ffe20996001f66c3 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 18:48:42 +0100 Subject: [PATCH 42/50] :wrench: --- packages/skia/src/sksg/HostConfig.ts | 49 +---------------------- packages/skia/src/sksg/Node.ts | 21 ---------- packages/skia/src/sksg/Recorder/Player.ts | 3 -- 3 files changed, 1 insertion(+), 72 deletions(-) diff --git a/packages/skia/src/sksg/HostConfig.ts b/packages/skia/src/sksg/HostConfig.ts index 38a5a10017..a2825bf2da 100644 --- a/packages/skia/src/sksg/HostConfig.ts +++ b/packages/skia/src/sksg/HostConfig.ts @@ -2,7 +2,7 @@ import type { Fiber, HostConfig } from "react-reconciler"; import { DefaultEventPriority } from "react-reconciler/constants"; -import { NodeType } from "../dom/types"; +import type { NodeType } from "../dom/types"; import { shallowEq } from "../renderer/typeddash"; import type { Node } from "./Node"; @@ -15,51 +15,6 @@ export const debug = (...args: Parameters) => { } }; -const isDeclaration = (type: NodeType) => { - "worklet"; - return ( - // BlurMaskFilters - type === NodeType.BlurMaskFilter || - // ImageFilters - type === NodeType.BlendImageFilter || - type === NodeType.BlurImageFilter || - type === NodeType.OffsetImageFilter || - type === NodeType.DropShadowImageFilter || - type === NodeType.MorphologyImageFilter || - type === NodeType.DisplacementMapImageFilter || - type === NodeType.RuntimeShaderImageFilter || - // ColorFilters - type === NodeType.MatrixColorFilter || - type === NodeType.BlendColorFilter || - type === NodeType.LumaColorFilter || - type === NodeType.LinearToSRGBGammaColorFilter || - type === NodeType.SRGBToLinearGammaColorFilter || - type === NodeType.LerpColorFilter || - // Shaders - type === NodeType.Shader || - type === NodeType.ImageShader || - type === NodeType.ColorShader || - type === NodeType.Turbulence || - type === NodeType.FractalNoise || - type === NodeType.LinearGradient || - type === NodeType.RadialGradient || - type === NodeType.SweepGradient || - type === NodeType.TwoPointConicalGradient || - // Path Effects - type === NodeType.CornerPathEffect || - type === NodeType.DiscretePathEffect || - type === NodeType.DashPathEffect || - type === NodeType.Path1DPathEffect || - type === NodeType.Path2DPathEffect || - type === NodeType.SumPathEffect || - type === NodeType.Line2DPathEffect || - // Mixed - type === NodeType.Blend || - // Paint - type === NodeType.Paint - ); -}; - type Instance = Node; type Props = object; @@ -140,7 +95,6 @@ export const sksgHostConfig: SkiaHostConfig = { container.registerValues(props); const instance = { type, - isDeclaration: isDeclaration(type), props, children: [], }; @@ -233,7 +187,6 @@ export const sksgHostConfig: SkiaHostConfig = { type: instance.type, props: newProps, children: keepChildren ? [...instance.children] : [], - isDeclaration: instance.isDeclaration, }; }, diff --git a/packages/skia/src/sksg/Node.ts b/packages/skia/src/sksg/Node.ts index 0f1146f073..4b1344aa57 100644 --- a/packages/skia/src/sksg/Node.ts +++ b/packages/skia/src/sksg/Node.ts @@ -2,28 +2,10 @@ import { NodeType } from "../dom/types"; export interface Node { type: NodeType; - isDeclaration: boolean; props: Props; children: Node[]; } -// TODO: Remove -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 }; -}; - export const isColorFilter = (type: NodeType) => { "worklet"; return ( @@ -107,9 +89,6 @@ export const sortNodeChildren = (parent: Node) => { node.type = NodeType.Blend; shaders.push(node); } - // TODO: remove isDeclaration from node - } else if (node.isDeclaration) { - throw new Error("Unknown declaration type: " + node.type); } else { drawings.push(node); } diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index 8b76ab51e8..f1bd5eca2d 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -1,7 +1,5 @@ "worklet"; -import type { SkPaint } from "../../skia"; - import { drawCircle, drawImage, @@ -51,7 +49,6 @@ import { type Command, } from "./Core"; import type { DrawingContext } from "./DrawingContext"; -import type { Recording } from "./Recording"; const play = (ctx: DrawingContext, command: Command) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any From dc74210a4775b2cad9f5f242d936cefcd7ec7225 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 19:22:04 +0100 Subject: [PATCH 43/50] :wrench: --- apps/paper/src/Examples/Matrix/Symbol.tsx | 4 ++-- packages/skia/src/sksg/Container.ts | 5 +++-- packages/skia/src/sksg/Recorder/DrawingContext.ts | 12 ++++++------ packages/skia/src/sksg/Recorder/Recorder.ts | 6 ------ packages/skia/src/sksg/Recorder/Recording.ts | 9 +++------ 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/apps/paper/src/Examples/Matrix/Symbol.tsx b/apps/paper/src/Examples/Matrix/Symbol.tsx index 215d0a2107..e2287ae05a 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 = 32; -export const ROWS = 64; +export const COLS = 8; +export const ROWS = 16; const pos = vec(0, 0); interface SymbolProps { diff --git a/packages/skia/src/sksg/Container.ts b/packages/skia/src/sksg/Container.ts index b276dcdffa..186ebb88ce 100644 --- a/packages/skia/src/sksg/Container.ts +++ b/packages/skia/src/sksg/Container.ts @@ -13,7 +13,7 @@ import { Recorder } from "./Recorder/Recorder"; import { visit } from "./Recorder/Visitor"; import { replay } from "./Recorder/Player"; import { DrawingContext } from "./Recorder/DrawingContext"; -import type { Recording } from "./Recorder/Recording"; +import { createRecording, type Recording } from "./Recorder/Recording"; const drawOnscreen = (Skia: Skia, nativeId: number, recording: Recording) => { "worklet"; @@ -21,6 +21,7 @@ const drawOnscreen = (Skia: Skia, nativeId: number, recording: Recording) => { const canvas = rec.beginRecording(); // const start = performance.now(); + // TODO: because the pool is not a shared value here, it is copied on every frame const ctx = new DrawingContext(Skia, recording.paintPool, canvas); //console.log(recording.commands); replay(ctx, recording.commands); @@ -62,7 +63,7 @@ export class Container { this._root = root; const recorder = new Recorder(); visit(recorder, root); - this._recording = recorder.finishAsRecording(this.Skia); + this._recording = createRecording(recorder.commands); } clear() { diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index 893d5bcee3..d9a1b130ea 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -24,19 +24,19 @@ export class DrawingContext { constructor(Skia: Skia, paintPool: SkPaint[], canvas: SkCanvas) { this.Skia = Skia; this.canvas = canvas; - this.paints.push(Skia.Paint()); + paintPool[0] = this.Skia.Paint(); + this.paints.push(paintPool[0]); this.paintPool = paintPool; } savePaint() { const i = this.paints.length; if (!this.paintPool[i]) { - this.paintPool.push(this.Skia.Paint()); + this.paintPool[i] = this.Skia.Paint(); } - const paint = this.paintPool[i]; - const parentPaint = this.paint; - paint.assign(parentPaint); - this.paints.push(paint); + this.paintPool[i].reset(); + this.paintPool[i].assign(this.paint); + this.paints.push(this.paintPool[i]); } saveBackdropFilter() { diff --git a/packages/skia/src/sksg/Recorder/Recorder.ts b/packages/skia/src/sksg/Recorder/Recorder.ts index 5eb56b8a7a..9f84560597 100644 --- a/packages/skia/src/sksg/Recorder/Recorder.ts +++ b/packages/skia/src/sksg/Recorder/Recorder.ts @@ -30,11 +30,9 @@ import type { import type { AnimatedProps } from "../../renderer"; import { isSharedValue } from "../utils"; import { isColorFilter, isImageFilter, isPathEffect, isShader } from "../Node"; -import type { Skia } from "../../skia/types"; import { CommandType } from "./Core"; import type { Command } from "./Core"; -import { createRecording } from "./Recording"; export class Recorder { commands: Command[] = []; @@ -70,10 +68,6 @@ export class Recorder { this.commands.push(command); } - finishAsRecording(Skia: Skia) { - return createRecording(Skia, this.commands); - } - savePaint(props: AnimatedProps) { this.add({ type: CommandType.SavePaint, props }); } diff --git a/packages/skia/src/sksg/Recorder/Recording.ts b/packages/skia/src/sksg/Recorder/Recording.ts index 9481e51674..2b5cf661a0 100644 --- a/packages/skia/src/sksg/Recorder/Recording.ts +++ b/packages/skia/src/sksg/Recorder/Recording.ts @@ -1,4 +1,4 @@ -import type { Skia, SkPaint } from "../../skia/types"; +import type { SkPaint } from "../../skia/types"; import type { Command } from "./Core"; @@ -7,10 +7,7 @@ export interface Recording { paintPool: SkPaint[]; } -export const createRecording = ( - Skia: Skia, - commands: Command[] -): Recording => ({ +export const createRecording = (commands: Command[]): Recording => ({ commands, - paintPool: [Skia.Paint()], + paintPool: [], }); From 146c562feff921080a8950483b5f6a1ae71ac4d0 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 19:39:02 +0100 Subject: [PATCH 44/50] :wrench --- packages/skia/src/sksg/Recorder/DrawingContext.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index d9a1b130ea..f155eb8afe 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -30,13 +30,7 @@ export class DrawingContext { } savePaint() { - const i = this.paints.length; - if (!this.paintPool[i]) { - this.paintPool[i] = this.Skia.Paint(); - } - this.paintPool[i].reset(); - this.paintPool[i].assign(this.paint); - this.paints.push(this.paintPool[i]); + this.paints.push(this.paint.copy()); } saveBackdropFilter() { From f0bf910eac774026e1fd8a3943eaee653fcb5e11 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 19:46:57 +0100 Subject: [PATCH 45/50] :wrench: --- packages/skia/src/sksg/Recorder/DrawingContext.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index f155eb8afe..88483f2a52 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -20,6 +20,7 @@ export class DrawingContext { pathEffects: SkPathEffect[] = []; paintDeclarations: SkPaint[] = []; paintPool: SkPaint[] = []; + nextPaintIndex = 1; constructor(Skia: Skia, paintPool: SkPaint[], canvas: SkCanvas) { this.Skia = Skia; @@ -30,7 +31,15 @@ export class DrawingContext { } savePaint() { - this.paints.push(this.paint.copy()); + // Get next available paint from pool or create new one if needed + if (this.nextPaintIndex >= this.paintPool.length) { + this.paintPool.push(this.Skia.Paint()); + } + + const nextPaint = this.paintPool[this.nextPaintIndex]; + nextPaint.assign(this.paint); // Reuse allocation by copying properties + this.paints.push(nextPaint); + this.nextPaintIndex++; } saveBackdropFilter() { From 5aed2a91be7c10bd4649096b4a4907ccd3129899 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 21:00:26 +0100 Subject: [PATCH 46/50] :wrench: --- .../src/sksg/Recorder/commands/Drawing.ts | 81 ++++++------------- 1 file changed, 23 insertions(+), 58 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/commands/Drawing.ts b/packages/skia/src/sksg/Recorder/commands/Drawing.ts index ac2501d48e..94565bb988 100644 --- a/packages/skia/src/sksg/Recorder/commands/Drawing.ts +++ b/packages/skia/src/sksg/Recorder/commands/Drawing.ts @@ -34,13 +34,7 @@ import type { VerticesProps, } from "../../../dom/types"; import { saturate } from "../../../renderer/processors"; -import type { - SkCanvas, - SkPaint, - SkPoint, - SkRSXform, - Skia, -} from "../../../skia/types"; +import type { SkPoint, SkRSXform } from "../../../skia/types"; import { BlendMode, BlurStyle, @@ -52,27 +46,22 @@ import { } from "../../../skia/types"; import type { Node } from "../../Node"; import { materialize } from "../../utils"; +import type { DrawingContext } from "../DrawingContext"; -interface LocalDrawingContext { - Skia: Skia; - canvas: SkCanvas; - paint: SkPaint; -} - -export const drawLine = (ctx: LocalDrawingContext, props: LineProps) => { +export const drawLine = (ctx: DrawingContext, props: LineProps) => { "worklet"; const { p1, p2 } = props; ctx.canvas.drawLine(p1.x, p1.y, p2.x, p2.y, ctx.paint); }; -export const drawOval = (ctx: LocalDrawingContext, props: OvalProps) => { +export const drawOval = (ctx: DrawingContext, props: OvalProps) => { "worklet"; const rect = processRect(ctx.Skia, props); ctx.canvas.drawOval(rect, ctx.paint); }; export const drawBox = ( - ctx: LocalDrawingContext, + ctx: DrawingContext, props: BoxProps, // eslint-disable-next-line @typescript-eslint/no-explicit-any children: Node[] @@ -126,7 +115,7 @@ export const drawBox = ( }); }; -export const drawImage = (ctx: LocalDrawingContext, props: ImageProps) => { +export const drawImage = (ctx: DrawingContext, props: ImageProps) => { "worklet"; const { image } = props; if (image) { @@ -146,16 +135,13 @@ export const drawImage = (ctx: LocalDrawingContext, props: ImageProps) => { } }; -export const drawPoints = (ctx: LocalDrawingContext, props: PointsProps) => { +export const drawPoints = (ctx: DrawingContext, props: PointsProps) => { "worklet"; const { points, mode } = props; ctx.canvas.drawPoints(PointMode[enumKey(mode)], points, ctx.paint); }; -export const drawVertices = ( - ctx: LocalDrawingContext, - props: VerticesProps -) => { +export const drawVertices = (ctx: DrawingContext, props: VerticesProps) => { "worklet"; const { mode, textures, colors, indices, blendMode } = props; const vertexMode = mode ? VertexMode[enumKey(mode)] : VertexMode.Triangles; @@ -172,19 +158,13 @@ export const drawVertices = ( ctx.canvas.drawVertices(vertices, blend, ctx.paint); }; -export const drawDiffRect = ( - ctx: LocalDrawingContext, - props: DiffRectProps -) => { +export const drawDiffRect = (ctx: DrawingContext, props: DiffRectProps) => { "worklet"; const { outer, inner } = props; ctx.canvas.drawDRRect(outer, inner, ctx.paint); }; -export const drawTextPath = ( - ctx: LocalDrawingContext, - props: TextPathProps -) => { +export const drawTextPath = (ctx: DrawingContext, props: TextPathProps) => { "worklet"; const path = processPath(ctx.Skia, props.path); const { font, initialOffset } = props; @@ -223,7 +203,7 @@ export const drawTextPath = ( } }; -export const drawText = (ctx: LocalDrawingContext, props: TextProps) => { +export const drawText = (ctx: DrawingContext, props: TextProps) => { "worklet"; const { text, x, y, font } = props; if (font != null) { @@ -231,7 +211,7 @@ export const drawText = (ctx: LocalDrawingContext, props: TextProps) => { } }; -export const drawPatch = (ctx: LocalDrawingContext, props: PatchProps) => { +export const drawPatch = (ctx: DrawingContext, props: PatchProps) => { "worklet"; const { texture, blendMode, patch } = props; const defaultBlendMode = props.colors ? BlendMode.DstOver : BlendMode.SrcOver; @@ -262,7 +242,7 @@ export const drawPatch = (ctx: LocalDrawingContext, props: PatchProps) => { ctx.canvas.drawPatch(points, colors, texture, mode, ctx.paint); }; -export const drawPath = (ctx: LocalDrawingContext, props: PathProps) => { +export const drawPath = (ctx: DrawingContext, props: PathProps) => { "worklet"; const { start: trimStart, @@ -293,25 +273,19 @@ export const drawPath = (ctx: LocalDrawingContext, props: PathProps) => { ctx.canvas.drawPath(path, ctx.paint); }; -export const drawRect = (ctx: LocalDrawingContext, props: RectProps) => { +export const drawRect = (ctx: DrawingContext, props: RectProps) => { "worklet"; const derived = processRect(ctx.Skia, props); ctx.canvas.drawRect(derived, ctx.paint); }; -export const drawRRect = ( - ctx: LocalDrawingContext, - props: RoundedRectProps -) => { +export const drawRRect = (ctx: DrawingContext, props: RoundedRectProps) => { "worklet"; const derived = processRRect(ctx.Skia, props); ctx.canvas.drawRRect(derived, ctx.paint); }; -export const drawTextBlob = ( - ctx: LocalDrawingContext, - props: TextBlobProps -) => { +export const drawTextBlob = (ctx: DrawingContext, props: TextBlobProps) => { "worklet"; const { blob, x, y } = props; ctx.canvas.drawTextBlob(blob, x, y, ctx.paint); @@ -322,7 +296,7 @@ interface ProcessedGlyphs { positions: SkPoint[]; } -export const drawGlyphs = (ctx: LocalDrawingContext, props: GlyphsProps) => { +export const drawGlyphs = (ctx: DrawingContext, props: GlyphsProps) => { "worklet"; const derived = props.glyphs.reduce( (acc, glyph) => { @@ -340,10 +314,7 @@ export const drawGlyphs = (ctx: LocalDrawingContext, props: GlyphsProps) => { } }; -export const drawImageSVG = ( - ctx: LocalDrawingContext, - props: ImageSVGProps -) => { +export const drawImageSVG = (ctx: DrawingContext, props: ImageSVGProps) => { "worklet"; const { canvas } = ctx; const { svg } = props; @@ -361,10 +332,7 @@ export const drawImageSVG = ( canvas.restore(); }; -export const drawParagraph = ( - ctx: LocalDrawingContext, - props: ParagraphProps -) => { +export const drawParagraph = (ctx: DrawingContext, props: ParagraphProps) => { "worklet"; const { paragraph, x, y, width } = props; if (paragraph) { @@ -373,13 +341,13 @@ export const drawParagraph = ( } }; -export const drawPicture = (ctx: LocalDrawingContext, props: PictureProps) => { +export const drawPicture = (ctx: DrawingContext, props: PictureProps) => { "worklet"; const { picture } = props; ctx.canvas.drawPicture(picture); }; -export const drawAtlas = (ctx: LocalDrawingContext, props: AtlasProps) => { +export const drawAtlas = (ctx: DrawingContext, props: AtlasProps) => { "worklet"; const { image, sprites, transforms, colors, blendMode } = props; const blend = blendMode ? BlendMode[enumKey(blendMode)] : undefined; @@ -388,17 +356,14 @@ export const drawAtlas = (ctx: LocalDrawingContext, props: AtlasProps) => { } }; -export const drawCircle = (ctx: LocalDrawingContext, props: CircleProps) => { +export const drawCircle = (ctx: DrawingContext, props: CircleProps) => { "worklet"; const { c } = processCircle(props); const { r } = props; ctx.canvas.drawCircle(c.x, c.y, r, ctx.paint); }; -export const drawFill = ( - ctx: LocalDrawingContext, - _props: DrawingNodeProps -) => { +export const drawFill = (ctx: DrawingContext, _props: DrawingNodeProps) => { "worklet"; ctx.canvas.drawPaint(ctx.paint); }; From dbdaff8ff74649a2d77840f2f51b0b3a731b27d4 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Sun, 5 Jan 2025 21:32:13 +0100 Subject: [PATCH 47/50] :wrench: --- packages/skia/src/sksg/Recorder/Core.ts | 5 +++-- packages/skia/src/sksg/Recorder/Player.ts | 5 +++-- packages/skia/src/sksg/Recorder/commands/Box.ts | 4 ++-- packages/skia/src/sksg/Recorder/commands/CTM.ts | 3 +-- packages/skia/src/sksg/Recorder/commands/ColorFilters.ts | 6 ++++-- packages/skia/src/sksg/Recorder/commands/ImageFilters.ts | 7 +++++-- packages/skia/src/sksg/Recorder/commands/Paint.ts | 3 +-- packages/skia/src/sksg/Recorder/commands/PathEffects.ts | 6 ++++-- packages/skia/src/sksg/Recorder/commands/Shaders.ts | 6 ++++-- 9 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/skia/src/sksg/Recorder/Core.ts b/packages/skia/src/sksg/Recorder/Core.ts index 3cb61cd635..a55cf70dce 100644 --- a/packages/skia/src/sksg/Recorder/Core.ts +++ b/packages/skia/src/sksg/Recorder/Core.ts @@ -1,5 +1,3 @@ -"worklet"; - import type { SharedValue } from "react-native-reanimated"; import type { @@ -119,6 +117,7 @@ export const materializeProps = (command: { props: Record; animatedProps?: Record>; }) => { + "worklet"; if (command.animatedProps) { for (const key in command.animatedProps) { command.props[key] = command.animatedProps[key].value; @@ -130,6 +129,7 @@ export const isCommand = ( command: Command, type: T ): command is Command => { + "worklet"; return command.type === type; }; @@ -166,5 +166,6 @@ export const isDrawCommand = ( command: Command, type: T ): command is DrawCommand => { + "worklet"; return command.type === type; }; diff --git a/packages/skia/src/sksg/Recorder/Player.ts b/packages/skia/src/sksg/Recorder/Player.ts index f1bd5eca2d..457cc653db 100644 --- a/packages/skia/src/sksg/Recorder/Player.ts +++ b/packages/skia/src/sksg/Recorder/Player.ts @@ -1,5 +1,3 @@ -"worklet"; - import { drawCircle, drawImage, @@ -51,6 +49,8 @@ import { import type { DrawingContext } from "./DrawingContext"; const play = (ctx: DrawingContext, command: Command) => { + "worklet"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any materializeProps(command as any); if (isCommand(command, CommandType.SaveBackdropFilter)) { @@ -153,6 +153,7 @@ const play = (ctx: DrawingContext, command: Command) => { }; export const replay = (ctx: DrawingContext, commands: Command[]) => { + "worklet"; commands.forEach((command) => { play(ctx, command); }); diff --git a/packages/skia/src/sksg/Recorder/commands/Box.ts b/packages/skia/src/sksg/Recorder/commands/Box.ts index 3611988271..58cfdb7357 100644 --- a/packages/skia/src/sksg/Recorder/commands/Box.ts +++ b/packages/skia/src/sksg/Recorder/commands/Box.ts @@ -1,5 +1,3 @@ -"worklet"; - import { deflate, inflate } from "../../../dom/nodes"; import type { BoxProps, BoxShadowProps } from "../../../dom/types"; import { BlurStyle, ClipOp, isRRect } from "../../../skia/types"; @@ -13,10 +11,12 @@ interface BoxCommand extends Command { } export const isBoxCommand = (command: Command): command is BoxCommand => { + "worklet"; return command.type === CommandType.DrawBox; }; export const drawBox = (ctx: DrawingContext, command: BoxCommand) => { + "worklet"; command.shadows.forEach((shadow) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any materializeProps(shadow as any); diff --git a/packages/skia/src/sksg/Recorder/commands/CTM.ts b/packages/skia/src/sksg/Recorder/commands/CTM.ts index bbb3b3145c..f16b55cdf5 100644 --- a/packages/skia/src/sksg/Recorder/commands/CTM.ts +++ b/packages/skia/src/sksg/Recorder/commands/CTM.ts @@ -1,5 +1,3 @@ -"worklet"; - import { isPathDef, processPath, @@ -32,6 +30,7 @@ const computeClip = ( }; export const saveCTM = (ctx: DrawingContext, props: CTMProps) => { + "worklet"; const { canvas, Skia } = ctx; const { clip: rawClip, diff --git a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts index a9fc96ed8b..276ffd10c1 100644 --- a/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ColorFilters.ts @@ -1,5 +1,3 @@ -"worklet"; - import { enumKey } from "../../../dom/nodes"; import type { BlendColorFilterProps, @@ -16,6 +14,7 @@ import type { DrawingContext } from "../DrawingContext"; export const isPushColorFilter = ( command: Command ): command is Command => { + "worklet"; return command.type === CommandType.PushColorFilter; }; @@ -38,10 +37,12 @@ const isColorFilter = ( command: Command, type: T ): command is PushColorFilter => { + "worklet"; return command.colorFilterType === type; }; export const composeColorFilters = (ctx: DrawingContext) => { + "worklet"; if (ctx.colorFilters.length > 1) { const outer = ctx.colorFilters.pop()!; const inner = ctx.colorFilters.pop()!; @@ -53,6 +54,7 @@ export const pushColorFilter = ( ctx: DrawingContext, command: Command ) => { + "worklet"; let cf: SkColorFilter | undefined; if (isColorFilter(command, NodeType.BlendColorFilter)) { const { props } = command; diff --git a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts index 9cc5a673ae..d54a217587 100644 --- a/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts +++ b/packages/skia/src/sksg/Recorder/commands/ImageFilters.ts @@ -1,5 +1,3 @@ -"worklet"; - import { enumKey, processRadius } from "../../../dom/nodes"; import type { BlendImageFilterProps, @@ -168,6 +166,7 @@ const declareRuntimeShaderImageFilter = ( ctx: DrawingContext, props: RuntimeShaderImageFilterProps ) => { + "worklet"; const { source, uniforms } = props; const rtb = ctx.Skia.RuntimeShaderBuilder(source); if (uniforms) { @@ -178,6 +177,7 @@ const declareRuntimeShaderImageFilter = ( }; export const composeImageFilters = (ctx: DrawingContext) => { + "worklet"; if (ctx.imageFilters.length > 1) { const outer = ctx.imageFilters.pop()!; const inner = ctx.imageFilters.pop()!; @@ -202,6 +202,7 @@ export const setBlurMaskFilter = ( export const isPushImageFilter = ( command: Command ): command is Command => { + "worklet"; return command.type === CommandType.PushImageFilter; }; @@ -225,6 +226,7 @@ const isImageFilter = ( command: Command, type: T ): command is PushImageFilter => { + "worklet"; return command.imageFilterType === type; }; @@ -232,6 +234,7 @@ export const pushImageFilter = ( ctx: DrawingContext, command: Command ) => { + "worklet"; if (isImageFilter(command, NodeType.BlurImageFilter)) { declareBlurImageFilter(ctx, command.props); } else if (isImageFilter(command, NodeType.MorphologyImageFilter)) { diff --git a/packages/skia/src/sksg/Recorder/commands/Paint.ts b/packages/skia/src/sksg/Recorder/commands/Paint.ts index 588f590a80..d2ba489f7b 100644 --- a/packages/skia/src/sksg/Recorder/commands/Paint.ts +++ b/packages/skia/src/sksg/Recorder/commands/Paint.ts @@ -1,5 +1,3 @@ -"worklet"; - import { enumKey } from "../../../dom/nodes"; import type { PaintProps } from "../../../dom/types"; import { @@ -42,6 +40,7 @@ export const setPaintProperties = ( dither, }: PaintProps ) => { + "worklet"; if (opacity !== undefined) { paint.setAlphaf(paint.getAlphaf() * opacity); } diff --git a/packages/skia/src/sksg/Recorder/commands/PathEffects.ts b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts index cc24ed2cb8..72f4154cec 100644 --- a/packages/skia/src/sksg/Recorder/commands/PathEffects.ts +++ b/packages/skia/src/sksg/Recorder/commands/PathEffects.ts @@ -1,5 +1,3 @@ -"worklet"; - import { enumKey, processPath } from "../../../dom/nodes"; import { NodeType } from "../../../dom/types"; import type { @@ -109,6 +107,7 @@ const declarePath1DPathEffect = ( export const isPushPathEffect = ( command: Command ): command is Command => { + "worklet"; return command.type === CommandType.PushPathEffect; }; @@ -132,10 +131,12 @@ const isPathEffect = ( command: Command, type: T ): command is PushPathEffect => { + "worklet"; return command.pathEffectType === type; }; export const composePathEffects = (ctx: DrawingContext) => { + "worklet"; if (ctx.pathEffects.length > 1) { const outer = ctx.pathEffects.pop()!; const inner = ctx.pathEffects.pop()!; @@ -147,6 +148,7 @@ export const pushPathEffect = ( ctx: DrawingContext, command: Command ) => { + "worklet"; if (isPathEffect(command, NodeType.DiscretePathEffect)) { declareDiscretePathEffect(ctx, command.props); } else if (isPathEffect(command, NodeType.DashPathEffect)) { diff --git a/packages/skia/src/sksg/Recorder/commands/Shaders.ts b/packages/skia/src/sksg/Recorder/commands/Shaders.ts index b5d98cee89..1304d6f5ec 100644 --- a/packages/skia/src/sksg/Recorder/commands/Shaders.ts +++ b/packages/skia/src/sksg/Recorder/commands/Shaders.ts @@ -1,5 +1,3 @@ -"worklet"; - import { enumKey, fitRects, @@ -212,6 +210,7 @@ const declareImageShader = (ctx: DrawingContext, props: ImageShaderProps) => { }; const declareBlend = (ctx: DrawingContext, props: BlendProps) => { + "worklet"; const blend = BlendMode[enumKey(props.mode as BlendProps["mode"])]; const shaders = ctx.shaders.splice(0, ctx.shaders.length); if (shaders.length > 0) { @@ -223,6 +222,7 @@ const declareBlend = (ctx: DrawingContext, props: BlendProps) => { export const isPushShader = ( command: Command ): command is Command => { + "worklet"; return command.type === CommandType.PushShader; }; @@ -249,6 +249,7 @@ const isShader = ( command: Command, type: T ): command is PushShader => { + "worklet"; return command.shaderType === type; }; @@ -256,6 +257,7 @@ export const pushShader = ( ctx: DrawingContext, command: Command ) => { + "worklet"; if (isShader(command, NodeType.Shader)) { declareShader(ctx, command.props); } else if (isShader(command, NodeType.ImageShader)) { From 3bd7ba4c4caacf042a2766b30eb62791250fc3bc Mon Sep 17 00:00:00 2001 From: William Candillon Date: Mon, 6 Jan 2025 10:03:44 +0100 Subject: [PATCH 48/50] :wrench: --- apps/paper/src/Examples/Matrix/Matrix.tsx | 2 +- apps/paper/src/Examples/Matrix/Symbol.tsx | 2 +- packages/skia/src/sksg/Container.ts | 6 +- .../skia/src/sksg/Recorder/DrawingContext.ts | 152 +++++++++++------- 4 files changed, 95 insertions(+), 67 deletions(-) diff --git a/apps/paper/src/Examples/Matrix/Matrix.tsx b/apps/paper/src/Examples/Matrix/Matrix.tsx index 741c111d52..b3f02251e4 100644 --- a/apps/paper/src/Examples/Matrix/Matrix.tsx +++ b/apps/paper/src/Examples/Matrix/Matrix.tsx @@ -44,7 +44,7 @@ export const Matrix = () => { - + {cols.map((_i, i) => rows.map((_j, j) => ( { @@ -22,7 +22,7 @@ const drawOnscreen = (Skia: Skia, nativeId: number, recording: Recording) => { // const start = performance.now(); // TODO: because the pool is not a shared value here, it is copied on every frame - const ctx = new DrawingContext(Skia, recording.paintPool, canvas); + const ctx = createDrawingContext(Skia, recording.paintPool, canvas); //console.log(recording.commands); replay(ctx, recording.commands); const picture = rec.finishRecordingAsPicture(); @@ -107,7 +107,7 @@ export class Container { if (!this._recording) { throw new Error("No recording to draw"); } - const ctx = new DrawingContext( + const ctx = createDrawingContext( this.Skia, this._recording.paintPool, canvas diff --git a/packages/skia/src/sksg/Recorder/DrawingContext.ts b/packages/skia/src/sksg/Recorder/DrawingContext.ts index 88483f2a52..49451d3f0d 100644 --- a/packages/skia/src/sksg/Recorder/DrawingContext.ts +++ b/packages/skia/src/sksg/Recorder/DrawingContext.ts @@ -1,5 +1,3 @@ -"worklet"; - import type { Skia, SkCanvas, @@ -10,93 +8,123 @@ import type { SkPathEffect, } from "../../skia/types"; -export class DrawingContext { - Skia: Skia; - canvas: SkCanvas; - paints: SkPaint[] = []; - colorFilters: SkColorFilter[] = []; - shaders: SkShader[] = []; - imageFilters: SkImageFilter[] = []; - pathEffects: SkPathEffect[] = []; - paintDeclarations: SkPaint[] = []; - paintPool: SkPaint[] = []; - nextPaintIndex = 1; +export const createDrawingContext = ( + Skia: Skia, + paintPool: SkPaint[], + canvas: SkCanvas +) => { + "worklet"; + + // State (formerly class fields) + const paints: SkPaint[] = []; + const colorFilters: SkColorFilter[] = []; + const shaders: SkShader[] = []; + const imageFilters: SkImageFilter[] = []; + const pathEffects: SkPathEffect[] = []; + const paintDeclarations: SkPaint[] = []; - constructor(Skia: Skia, paintPool: SkPaint[], canvas: SkCanvas) { - this.Skia = Skia; - this.canvas = canvas; - paintPool[0] = this.Skia.Paint(); - this.paints.push(paintPool[0]); - this.paintPool = paintPool; - } + let nextPaintIndex = 1; - savePaint() { + // Initialize first paint + paintPool[0] = Skia.Paint(); + paints.push(paintPool[0]); + + // Methods (formerly class methods) + const savePaint = () => { // Get next available paint from pool or create new one if needed - if (this.nextPaintIndex >= this.paintPool.length) { - this.paintPool.push(this.Skia.Paint()); + if (nextPaintIndex >= paintPool.length) { + paintPool.push(Skia.Paint()); } - const nextPaint = this.paintPool[this.nextPaintIndex]; - nextPaint.assign(this.paint); // Reuse allocation by copying properties - this.paints.push(nextPaint); - this.nextPaintIndex++; - } + const nextPaint = paintPool[nextPaintIndex]; + nextPaint.assign(getCurrentPaint()); // Reuse allocation by copying properties + paints.push(nextPaint); + nextPaintIndex++; + }; - saveBackdropFilter() { + const saveBackdropFilter = () => { let imageFilter: SkImageFilter | null = null; - const imgf = this.imageFilters.pop(); + const imgf = imageFilters.pop(); if (imgf) { imageFilter = imgf; } else { - const cf = this.colorFilters.pop(); + const cf = colorFilters.pop(); if (cf) { - imageFilter = this.Skia.ImageFilter.MakeColorFilter(cf, null); + imageFilter = Skia.ImageFilter.MakeColorFilter(cf, null); } } - this.canvas.saveLayer(undefined, null, imageFilter); - this.canvas.restore(); - } + canvas.saveLayer(undefined, null, imageFilter); + canvas.restore(); + }; - get paint() { - return this.paints[this.paints.length - 1]; - } + // Equivalent to the `get paint()` getter in the original class + const getCurrentPaint = () => { + return paints[paints.length - 1]; + }; - restorePaint() { - return this.paints.pop(); - } + const restorePaint = () => { + return paints.pop(); + }; - materializePaint() { + const materializePaint = () => { // Color Filters - if (this.colorFilters.length > 0) { - this.paint.setColorFilter( - this.colorFilters.reduceRight((inner, outer) => - inner ? this.Skia.ColorFilter.MakeCompose(outer, inner) : outer + if (colorFilters.length > 0) { + getCurrentPaint().setColorFilter( + colorFilters.reduceRight((inner, outer) => + inner ? Skia.ColorFilter.MakeCompose(outer, inner) : outer ) ); } // Shaders - if (this.shaders.length > 0) { - this.paint.setShader(this.shaders[this.shaders.length - 1]); + if (shaders.length > 0) { + getCurrentPaint().setShader(shaders[shaders.length - 1]); } // Image Filters - if (this.imageFilters.length > 0) { - this.paint.setImageFilter( - this.imageFilters.reduceRight((inner, outer) => - inner ? this.Skia.ImageFilter.MakeCompose(outer, inner) : outer + if (imageFilters.length > 0) { + getCurrentPaint().setImageFilter( + imageFilters.reduceRight((inner, outer) => + inner ? Skia.ImageFilter.MakeCompose(outer, inner) : outer ) ); } // Path Effects - if (this.pathEffects.length > 0) { - this.paint.setPathEffect( - this.pathEffects.reduceRight((inner, outer) => - inner ? this.Skia.PathEffect.MakeCompose(outer, inner) : outer + if (pathEffects.length > 0) { + getCurrentPaint().setPathEffect( + pathEffects.reduceRight((inner, outer) => + inner ? Skia.PathEffect.MakeCompose(outer, inner) : outer ) ); } - this.colorFilters = []; - this.shaders = []; - this.imageFilters = []; - this.pathEffects = []; - } -} + + // Clear arrays + colorFilters.length = 0; + shaders.length = 0; + imageFilters.length = 0; + pathEffects.length = 0; + }; + + // Return an object containing the Skia reference, the canvas, and the methods + return { + // Public fields + Skia, + canvas, + paints, + colorFilters, + shaders, + imageFilters, + pathEffects, + paintDeclarations, + paintPool, + + // Public methods + savePaint, + saveBackdropFilter, + get paint() { + return paints[paints.length - 1]; + }, // the "getter" for the current paint + restorePaint, + materializePaint, + }; +}; + +export type DrawingContext = ReturnType; From 936a664892057bdcd39f9e8d49807670f8a9e553 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Mon, 6 Jan 2025 10:23:05 +0100 Subject: [PATCH 49/50] :green_heart: --- apps/paper/src/Examples/Matrix/Matrix.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/paper/src/Examples/Matrix/Matrix.tsx b/apps/paper/src/Examples/Matrix/Matrix.tsx index b3f02251e4..a1c0640b40 100644 --- a/apps/paper/src/Examples/Matrix/Matrix.tsx +++ b/apps/paper/src/Examples/Matrix/Matrix.tsx @@ -1,6 +1,6 @@ import { BlurMask, - Canvas2, + Canvas, Fill, Group, useClock, @@ -41,7 +41,7 @@ export const Matrix = () => { } const symbols = font.getGlyphIDs("abcdefghijklmnopqrstuvwxyz"); return ( - + @@ -60,6 +60,6 @@ export const Matrix = () => { )) )} - + ); }; From 7f091fd4605a3d3220b9aa488e40c0a8880e0b0f Mon Sep 17 00:00:00 2001 From: William Candillon Date: Mon, 6 Jan 2025 11:23:57 +0100 Subject: [PATCH 50/50] :wrench: --- .../skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx index 2ddc4ceae2..907dce9c9a 100644 --- a/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx +++ b/packages/skia/src/renderer/__tests__/e2e/ImageFilters.spec.tsx @@ -292,7 +292,9 @@ describe("Test Image Filters", () => { ); - checkImage(img, "snapshots/image-filter/test-shadow.png"); + checkImage(img, "snapshots/image-filter/test-shadow.png", { + maxPixelDiff: 1500, + }); }); itRunsE2eOnly("use the displacement map as documented", async () => { const { oslo } = images;